mirror of
https://github.com/imjasonh/nescript
synced 2026-07-07 00:22:50 +00:00
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).
68 lines
2.5 KiB
Text
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
|