1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-09 09:18:01 +00:00

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).
This commit is contained in:
Claude 2026-04-14 11:41:20 +00:00
parent 201664ea04
commit 2fe943b056
No known key found for this signature in database
19 changed files with 683 additions and 118 deletions

View file

@ -1480,34 +1480,36 @@ pub fn gen_bank_select(mapper: Mapper) -> Vec<Instruction> {
/// Generate a cross-bank trampoline stub. Placed in the fixed bank
/// and called by user code (also in the fixed bank) via
/// `JSR __tramp_<bank_name>`. Behavior:
/// `JSR <tramp_label>`. Behavior:
///
/// 1. Save the caller's bank number (always the fixed bank's index).
/// 2. Load the target bank number into A, JSR `__bank_select`.
/// 3. JSR the entry label in the target bank.
/// 4. Load the fixed bank number, JSR `__bank_select` to restore.
/// 5. RTS.
/// 1. Load the target bank number into A, JSR `__bank_select`.
/// 2. JSR the user-supplied entry label inside the target bank.
/// 3. Load the fixed bank number, JSR `__bank_select` to restore.
/// 4. RTS.
///
/// `bank_name` is the user-declared bank name from the `.ne` source.
/// `entry_label` is the label inside that bank the trampoline
/// should call (conventionally `__bank_<name>_entry`). `bank_index`
/// is the physical bank number. `fixed_bank_index` is the physical
/// bank number of the fixed bank (always `total_banks - 1`).
/// `tramp_label` is the label that callers will JSR (the IR codegen
/// emits `JSR __tramp_<fn_name>` at every cross-bank call site).
/// `entry_label` is the label inside the target bank that holds the
/// callee's first instruction — conventionally `__ir_fn_<fn_name>`,
/// the same label IR codegen would have emitted for an in-bank call.
/// `bank_index` is the physical PRG bank number of the target bank.
/// `fixed_bank_index` is the physical bank number of the fixed bank
/// (always `total_banks - 1`).
#[must_use]
pub fn gen_bank_trampoline(
bank_name: &str,
tramp_label: &str,
entry_label: &str,
bank_index: u8,
fixed_bank_index: u8,
) -> Vec<Instruction> {
let mut out = Vec::new();
let tramp_label = format!("__tramp_{bank_name}");
out.push(Instruction::new(NOP, AM::Label(tramp_label)));
out.push(Instruction::new(NOP, AM::Label(tramp_label.to_string())));
// Switch to target bank.
out.push(Instruction::new(LDA, AM::Immediate(bank_index)));
out.push(Instruction::new(JSR, AM::Label("__bank_select".into())));
// Call the user's entry point in that bank. The label lives in
// the switchable bank and is resolved during banked assembly.
// the switchable bank and is resolved by the linker after the
// banked code is assembled.
out.push(Instruction::new(JSR, AM::Label(entry_label.to_string())));
// Restore the fixed bank.
out.push(Instruction::new(LDA, AM::Immediate(fixed_bank_index)));

View file

@ -741,10 +741,12 @@ fn trampoline_switches_target_then_restores_fixed() {
// A trampoline must JSR `__bank_select` twice: once with the
// target bank's index, once with the fixed bank's index. The
// two LDA immediates in the stub should match those two bank
// numbers in order.
let t = gen_bank_trampoline("Level1", "__bank_Level1_entry", 0, 3);
// numbers in order. The trampoline name is the label callers
// will JSR (one trampoline per banked function); the entry
// label is whatever lives in the switchable bank.
let t = gen_bank_trampoline("__tramp_helper", "__ir_fn_helper", 0, 3);
// First instruction is the trampoline label.
assert!(matches!(&t[0].mode, AM::Label(n) if n == "__tramp_Level1"));
assert!(matches!(&t[0].mode, AM::Label(n) if n == "__tramp_helper"));
// Extract the sequence of immediate loads.
let imms: Vec<u8> = t
.iter()
@ -772,7 +774,7 @@ fn trampoline_switches_target_then_restores_fixed() {
.collect();
assert_eq!(
jsrs,
vec!["__bank_select", "__bank_Level1_entry", "__bank_select"],
vec!["__bank_select", "__ir_fn_helper", "__bank_select"],
"trampoline JSRs must dispatch in the correct order"
);
// Final instruction returns to caller.
@ -780,11 +782,14 @@ fn trampoline_switches_target_then_restores_fixed() {
}
#[test]
fn trampoline_label_derives_from_bank_name() {
// Trampoline labels are consistently named `__tramp_<bank>` so
// codegen can reference them without knowing bank indices.
let t = gen_bank_trampoline("MusicData", "__music_entry", 1, 3);
assert!(matches!(&t[0].mode, AM::Label(n) if n == "__tramp_MusicData"));
fn trampoline_label_uses_caller_supplied_name() {
// The codegen picks the trampoline label (typically
// `__tramp_<fn_name>`) so it can JSR it from the call site
// without knowing bank indices. `gen_bank_trampoline` should
// emit that exact label as its leading pseudo-op so the
// assembler resolves the JSR.
let t = gen_bank_trampoline("__tramp_big_helper", "__ir_fn_big_helper", 1, 3);
assert!(matches!(&t[0].mode, AM::Label(n) if n == "__tramp_big_helper"));
}
#[test]