1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00
nescript/examples/uxrom_banked_to_banked.ne
Claude db3a4adc57
codegen: support banked → banked cross-bank function calls
Programs that put functions in switchable banks can now call across
bank boundaries — `bank A { fun step() { helper() } }` where
`helper` lives in `bank B` used to panic in the IR codegen. Three
small pieces unblock it:

1. **Generic trampoline.** `runtime/gen_bank_trampoline` no longer
   takes a `fixed_bank_index` argument. Instead it reads the
   caller's current bank from `ZP_BANK_CURRENT`, pushes it on the
   hardware stack, switches to the target, JSRs the entry, then
   pulls and restores the saved bank. The same per-callee stub
   works for fixed→banked and banked→banked direction; nested
   trampolines compose because each PHA/PLA pair sits inside its
   own JSR/RTS frame. `gen_mapper_init` seeds `ZP_BANK_CURRENT`
   with the fixed bank index for any banked mapper so the very
   first cross-bank call from the fixed bank still restores to
   the fixed bank (matching pre-banked-banked semantics).

2. **Codegen drops the panic.** The `Some(from), Some(to)` arm in
   the call-resolution switch now emits `JSR __tramp_<name>` like
   the fixed→banked case instead of panicking. Banked→fixed calls
   still go direct (the fixed bank is always mapped at $C000).

3. **Bank-namespaced local labels.** Two banks emitting the same
   `__ir_cmp_e_8` would trip the linker's discovery-pass duplicate-
   label check the moment any banked code generated a comparison.
   The new `local_label_suffix` helper prefixes the suffix with the
   current bank name when banked code is being emitted, leaving
   fixed-bank label generation untouched (so existing examples are
   byte-identical apart from the trampoline / init bytes
   themselves).

The new `examples/uxrom_banked_to_banked.ne` demonstrates the path
end-to-end: `bank Logic { fun step() { ... clamp() } }` calls
`bank Helpers { fun clamp() { ... } }` once per frame. The harness
golden is committed alongside it. The five existing banked example
ROMs change byte-for-byte because of the new trampoline shape and
the seed-ZP_BANK_CURRENT init, but their emulator goldens still
match exactly — observable behaviour is unchanged.

https://claude.ai/code/session_01KEczoNUX3WmcFLfq6iAQxB
2026-04-15 02:37:19 +00:00

74 lines
2.6 KiB
Text

// UxROM Banked-to-Banked — first NEScript example to exercise a
// switchable-bank function calling *another* switchable-bank function.
//
// The previous user-banked example (`uxrom_user_banked.ne`) only put
// fixed → banked calls through the trampoline path; here `bank Logic`
// holds `step()` and `bank Helpers` holds `clamp()`, and `step` calls
// `clamp` once per frame. The codegen emits `JSR __tramp_clamp` from
// inside bank Logic, which lands in the fixed-bank trampoline that
// saves the current bank (Logic), switches to Helpers, runs the body,
// then restores Logic on the way out — see runtime/gen_bank_trampoline
// for the PHA/PLA implementation.
//
// The harness captures frame 180 somewhere along the sweep, so any
// regression in the trampoline's save/restore would either leave the
// wrong bank mapped at $8000 (subsequent sprite reads would corrupt)
// or crash before the OAM update happened.
//
// Build: cargo run -- build examples/uxrom_banked_to_banked.ne
game "UxROM Banked to Banked" {
mapper: UxROM
mirroring: horizontal
}
// Globals live in the fixed bank's RAM and are reachable from any
// bank via direct zero-page / absolute addressing — bank switching
// only affects the $8000-$BFFF code window.
var px: u8 = 80
var dir: u8 = 0 // 0 = sweep right, 1 = sweep left
bank Logic {
// The "main" banked function. Lives in bank Logic and calls
// into bank Helpers via the fixed-bank trampoline emitted
// by the linker.
fun step() {
if dir == 0 {
px = px + 1
} else {
px = px - 1
}
// Cross-bank call into Helpers. The codegen sees a
// current_bank of "Logic" and a callee bank of "Helpers"
// and emits `JSR __tramp_clamp` — the trampoline lives
// in the fixed bank, saves the caller's bank
// (ZP_BANK_CURRENT == Logic), switches to Helpers, runs
// the body, then restores Logic before returning here.
clamp()
}
}
bank Helpers {
// Bounce the sprite between two pixel rails. Self-contained
// — only reads/writes the global zero-page slots, no calls
// back out of the bank, so the trampoline never has to
// recursively unwind a third level.
fun clamp() {
if px == 176 {
dir = 1
}
if px == 80 {
dir = 0
}
}
}
on frame {
// Single fixed → banked trampoline call. Inside, `step` does a
// banked → banked call into `clamp` — that second hop is the
// path the new trampoline implementation enables.
step()
draw Smiley at: (px, 112)
}
start Main