mirror of
https://github.com/imjasonh/nescript
synced 2026-07-12 10:39:31 +00:00
Add jsnes emulator harness and fix four codegen bugs it surfaced
Running the compiled example ROMs through a headless puppeteer +
local jsnes harness exposed four latent bugs that the
header-structure-only integration tests couldn't catch:
- src/asm/mod.rs: the first pass treated ANY instruction with
`AddressingMode::Label` as a label definition, silently dropping
every `JMP`/`JSR` to a label. Now only `NOP + Label` is a label
def; other opcodes emit the opcode byte plus a 2-byte absolute
fixup resolved in pass two. Without this, every example crashed
with "invalid opcode at $1xxx" once the CPU fell through into
the math runtime and hit an unbalanced `RTS`.
- src/ir/lowering.rs (lower_handler): handler-local `VarDecl`s
(e.g. `var i: u8 = 0` inside a `while`) were pushed onto
`current_locals` but the handler built its own throwaway
`locals` list, so those var ids never got RAM addresses and
every `LoadVar`/`StoreVar` for them silently emitted nothing.
Seed `current_locals` with the state's declared locals and
reuse it so `lower_statement`'s appends flow through to the
`IrFunction`. Fixes the black screen in `arrays_and_functions`.
- src/ir/lowering.rs (global init): struct-literal initializers
on globals (`var player: Player = Player { x: 120, ... }`) fell
through to `eval_const`, which returned `None` for a
non-literal, so no init code was emitted. Now the per-field
synthetic globals each get their own `init_value`. Fixes the
black screen in `structs_enums_for`.
- src/codegen/mod.rs: the legacy AST codegen was emitting
`JSR __fn_poke` / treating `peek` as `LDA #0` for the hardware
intrinsics. It only "worked" before because the broken
assembler swallowed the bogus JSR. Handle `poke`/`peek` as
direct STA/LDA to a compile-time-constant absolute address,
matching the IR codegen's intrinsic path.
The harness lives in `tests/emulator/`: a tiny HTML page that
wraps the `jsnes` npm package, driven by a puppeteer script that
loads each ROM, runs ~180 frames, snapshots the canvas, and
records a smoke-test verdict (booted without a CPU crash, non-zero
pixels rendered, frames differ over time). `npm install && node
run_examples.mjs` from `tests/emulator/` runs the full sweep.
9/9 example ROMs now load, render, and animate where expected.
All 324 unit + 35 integration tests still pass.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
This commit is contained in:
parent
593a333eec
commit
81f3fd7de0
17 changed files with 1479 additions and 10 deletions
|
|
@ -80,8 +80,31 @@ impl Assembler {
|
|||
fn emit_instruction(&mut self, inst: &Instruction) {
|
||||
match &inst.mode {
|
||||
AddressingMode::Label(name) => {
|
||||
// This should be a label definition, not an instruction
|
||||
self.labels.insert(name.clone(), self.current_address());
|
||||
// A `NOP` with a `Label` mode is the label-definition
|
||||
// pseudo-instruction: it records the current address and
|
||||
// emits no bytes. Any other opcode paired with `Label` is
|
||||
// an actual jump-style instruction targeting that label
|
||||
// (e.g. `JMP __ir_main_loop`, `JSR __ir_fn_frame`), which
|
||||
// needs an opcode byte plus a 2-byte absolute-address
|
||||
// fixup resolved in the second pass.
|
||||
if inst.opcode == Opcode::NOP {
|
||||
self.labels.insert(name.clone(), self.current_address());
|
||||
} else if let Some(op) = opcodes::encode(inst.opcode, &AddressingMode::Absolute(0))
|
||||
{
|
||||
self.output.push(op);
|
||||
self.fixups.push(Fixup {
|
||||
offset: self.output.len(),
|
||||
label: name.clone(),
|
||||
kind: FixupKind::Absolute,
|
||||
});
|
||||
self.output.push(0); // placeholder low byte
|
||||
self.output.push(0); // placeholder high byte
|
||||
} else {
|
||||
panic!(
|
||||
"opcode {:?} cannot target a label (no absolute encoding)",
|
||||
inst.opcode
|
||||
);
|
||||
}
|
||||
}
|
||||
AddressingMode::LabelRelative(name) => {
|
||||
// Branch to label — emit opcode + placeholder
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue