1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-07 00:22:50 +00:00
nescript/examples/uxrom_user_banked.ne
Claude 2fe943b056
codegen: user code in switchable banks via cross-bank trampolines
Adds a `bank Foo { fun bar() { ... } }` parser form so user functions
can opt into living in a switchable PRG bank instead of the fixed
bank, plus the IR codegen, runtime, and linker work to make calls
across the bank boundary actually run. Programs that don't use the
new syntax produce byte-identical ROMs to before — verified by
rebuilding every existing example and diffing.

Pipeline shape:

* Parser accepts both `bank Foo: prg` (legacy reserved slot) and
  `bank Foo { fun ... }` (functions land in the named bank). Nested
  functions get tagged `bank: Some("Foo")` on the FunDecl + IrFunction.
* Analyzer bumps the user zero-page start past `$10` whenever the
  program declares any banked function, so `__bank_select`'s STA into
  ZP_BANK_CURRENT can't clobber a user variable. Programs without
  banked functions keep the legacy `$10` start.
* IrCodeGen emits each banked function into its own per-bank
  instruction stream (`banked_streams: HashMap<String, Vec<Instruction>>`)
  while the fixed-bank stream gets the dispatcher loop + state
  handlers + top-level functions, exactly like before. Cross-bank
  calls from the fixed bank rewrite `JSR __ir_fn_<name>` to
  `JSR __tramp_<name>`; in-bank calls stay direct. Banked → fixed
  calls are direct (the fixed bank is always mapped at $C000-$FFFF).
  Banked → other-banked calls aren't supported in this pass and
  panic loudly during codegen.
* Runtime's `gen_bank_trampoline` takes the trampoline label and
  entry label as parameters now (one trampoline per banked function,
  not one per bank) so the linker can request any number of stubs.
* Linker assembles banked banks twice: a discovery pass to learn
  each bank's labels, then a final pass that seeds the merged label
  table so banked code can JSR into the fixed bank's runtime helpers
  (math, audio, etc.). The fixed-bank assembler is also seeded with
  the cross-bank labels so the trampolines' `JSR __ir_fn_<name>`
  resolves into the bank's $8000 window. New `asm::assemble_with_labels`
  / `asm::assemble_discover_labels` helpers wire this up.
* PrgBank carries `Vec<Instruction>` + a list of `BankTrampoline`
  requests now, replacing the old `data: Vec<u8>` + single
  `entry_label: Option<String>` shape. The compiler populates both
  from the codegen output; the linker's two-pass assembly handles
  the rest.

New example: `examples/uxrom_user_banked.ne` puts a sprite-stepping
helper inside `bank Extras { fun step_animation() { ... } }`. The
fixed-bank state handler calls it via the generated trampoline, and
the harness golden locks in pixel + audio output at frame 180.

UxROM is the only mapper exercised by the new example. MMC1 and
MMC3 also work through the same path (the linker emits the right
mapper-specific bank-select code), but no example uses them yet —
the existing `mmc1_banked.ne` / `mmc3_per_state_split.ne` keep
their fixed-bank-only layout.

Limitations carried forward:
* No banked → banked cross-bank calls (panics in codegen).
* No greedy size-packing; placement is explicit-only.
* MMC3 state handlers don't get banked (the per-state split path
  is untouched).
2026-04-14 11:41:20 +00:00

68 lines
2.5 KiB
Text

// UxROM User-Banked — first NEScript example to put real user code
// inside a switchable bank. The `bank Extras { fun ... }` block tells
// the linker that `step_animation` lives in the named PRG bank
// instead of the fixed bank; the IR codegen emits a
// `__tramp_step_animation` stub in the fixed bank, and every call
// from a state handler now goes through the trampoline (which
// selects bank 0 at $8000-$BFFF, JSRs the function, then restores
// the fixed bank before returning).
//
// UxROM is the friendliest mapper for this work because its
// `__bank_select` routine is a single bus-conflict-safe write to
// $FFF0; MMC1's serial shift register and MMC3's two-register dance
// would both work but add more moving parts to an already-large
// pipeline change.
//
// The example sweeps a smiley left-and-right entirely from inside
// the banked helper. The helper reads two globals (`px`, `dir`) and
// updates them in place — no parameters, no return value, so the
// fixed-bank call site is a single `JSR __tramp_step_animation`.
// The harness captures frame 180 somewhere along the sweep, so any
// regression in bank-switching, trampoline emission, or banked-stream
// assembly will flip the golden.
//
// Build: cargo run -- build examples/uxrom_user_banked.ne
game "UxROM User Banked" {
mapper: UxROM
mirroring: horizontal
}
// The banked helper drives `px` directly so the fixed-bank state
// handler is just `JSR __tramp_step_animation; draw Smiley`. `dir`
// is 0 while sweeping right and 1 while sweeping left.
var px: u8 = 110
var dir: u8 = 0
bank Extras {
// Step the smiley one pixel each frame, bouncing off x = 110
// and x = 150. The whole computation lives in the switchable
// bank: nothing here references any fixed-bank function, so the
// bank's instruction stream resolves cleanly during the linker's
// two-pass assembly without needing a second trampoline.
fun step_animation() {
if dir == 0 {
px = px + 1
if px == 150 {
dir = 1
}
} else {
px = px - 1
if px == 110 {
dir = 0
}
}
}
}
on frame {
// Trampoline call: the codegen emits `JSR __tramp_step_animation`
// because the function is tagged `bank: Some("Extras")`. The
// trampoline lives in the fixed bank, switches to bank 0, JSRs
// `__ir_fn_step_animation` at its $8000-window address, then
// switches back before returning.
step_animation()
draw Smiley at: (px, 120)
}
start Main