mirror of
https://github.com/imjasonh/nescript
synced 2026-07-11 02:02:58 +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:
parent
201664ea04
commit
2fe943b056
19 changed files with 683 additions and 118 deletions
|
|
@ -403,19 +403,30 @@ fn link_banked_switchable_banks_are_padded_with_ff() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn link_banked_preserves_switchable_bank_payload_bytes() {
|
||||
// When a caller provides raw bytes for a switchable bank, the
|
||||
// linker must splice them in verbatim at the start of that
|
||||
// bank's slot. This is the hook the compiler uses to ship data
|
||||
// tables without touching the fixed bank.
|
||||
fn link_banked_assembles_switchable_bank_instructions() {
|
||||
// When a caller populates a switchable bank's instruction
|
||||
// stream, the linker must assemble those instructions at the
|
||||
// bank's $8000 base and splice the resulting bytes into the
|
||||
// bank's slot. We use a label + a couple of NOPs so the byte
|
||||
// pattern is unambiguous: NOP NOP NOP would be three $EA bytes
|
||||
// at the very start of the bank.
|
||||
let linker = Linker::with_mapper(Mirroring::Horizontal, Mapper::UxROM);
|
||||
let user_code = vec![Instruction::implied(NOP)];
|
||||
let data = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42, 0x13];
|
||||
let banks = vec![PrgBank::with_data("DataBank", data.clone())];
|
||||
let bank_code = vec![
|
||||
Instruction::new(NOP, AM::Label("__bank_payload".into())),
|
||||
Instruction::implied(NOP),
|
||||
Instruction::implied(NOP),
|
||||
Instruction::implied(NOP),
|
||||
];
|
||||
let banks = vec![PrgBank::with_instructions(
|
||||
"DataBank",
|
||||
bank_code,
|
||||
Vec::new(),
|
||||
)];
|
||||
let rom = linker.link_banked(&user_code, &[], &[], &[], &banks);
|
||||
// Bank 0 starts at offset 16. Verify payload lands at the very
|
||||
// start.
|
||||
assert_eq!(&rom[16..16 + data.len()], &data[..]);
|
||||
// Bank 0 starts at offset 16. Verify the three NOP bytes land
|
||||
// at the very start (the label pseudo-op emits zero bytes).
|
||||
assert_eq!(&rom[16..19], &[0xEA, 0xEA, 0xEA]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -455,28 +466,30 @@ fn link_banked_fixed_bank_contains_bank_select_subroutine() {
|
|||
|
||||
#[test]
|
||||
fn link_banked_fixed_bank_contains_trampolines_for_declared_banks() {
|
||||
// When a bank declares an entry label, the linker must emit a
|
||||
// matching `__tramp_<name>` stub in the fixed bank. We check
|
||||
// by constructing a bank with an entry label and verifying the
|
||||
// assembled labels map (via the indirect check: the ROM builds
|
||||
// without panicking on unresolved labels, which means the
|
||||
// trampoline's target label — here spliced via a dummy NOP
|
||||
// label in the fixed bank — resolved).
|
||||
// When a bank requests a trampoline, the linker must emit a
|
||||
// matching `__tramp_<name>` stub in the fixed bank that JSRs
|
||||
// the entry label inside the switchable bank. We check by
|
||||
// constructing a bank with both an entry-label-defining
|
||||
// instruction stream and a matching trampoline request, then
|
||||
// verifying the linker doesn't panic on unresolved fixups (the
|
||||
// banked-bank label seeding is what makes the JSR inside the
|
||||
// trampoline resolve correctly).
|
||||
let linker = Linker::with_mapper(Mirroring::Horizontal, Mapper::MMC1);
|
||||
// We splice a fake target label into the user code so the
|
||||
// trampoline's internal JSR resolves. This simulates the path
|
||||
// codegen will eventually take (emit the entry label alongside
|
||||
// the banked user code; the linker resolves it via the banked
|
||||
// assembler).
|
||||
let user_code = vec![
|
||||
Instruction::new(NOP, AM::Label("__fake_bank_entry".into())),
|
||||
Instruction::implied(NOP),
|
||||
let user_code = vec![Instruction::implied(NOP)];
|
||||
// The switchable bank holds the entry label and a tiny RTS so
|
||||
// there's something for the trampoline to JSR into.
|
||||
let bank_code = vec![
|
||||
Instruction::new(NOP, AM::Label("__ir_fn_helper".into())),
|
||||
Instruction::implied(RTS),
|
||||
];
|
||||
let banks = vec![PrgBank {
|
||||
name: "Level1".into(),
|
||||
data: Vec::new(),
|
||||
entry_label: Some("__fake_bank_entry".into()),
|
||||
}];
|
||||
let banks = vec![PrgBank::with_instructions(
|
||||
"Level1",
|
||||
bank_code,
|
||||
vec![BankTrampoline {
|
||||
tramp_label: "__tramp_helper".into(),
|
||||
entry_label: "__ir_fn_helper".into(),
|
||||
}],
|
||||
)];
|
||||
// Should not panic — trampoline and entry label both present.
|
||||
let rom = linker.link_banked(&user_code, &[], &[], &[], &banks);
|
||||
let info = rom::validate_ines(&rom).unwrap();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue