1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00
Commit graph

24 commits

Author SHA1 Message Date
Claude
54acb9ee38
Bug B: runtime OAM cursor so draw inside loops actually works
`IrCodeGen::next_oam_slot` incremented at *compile time*: one
`draw` statement = one fixed OAM slot, baked into absolute-mode
stores at codegen. A `draw` inside a `while`/`for`/`loop` body
was lowered once and then always wrote to the same four OAM
bytes every iteration, so only the last iteration was ever
visible. The writeup in the earlier PR called this "bug B".

Fix: reserve ZP `$09` as `ZP_OAM_CURSOR`, reset it to 0 at the
top of every frame handler (right after the existing OAM clear
loop), and lower each `DrawSprite` IR op to:

    LDY $09               ; load cursor
    LDA <y_temp>
    STA $0200,Y           ; sprite Y
    LDA #tile
    STA $0201,Y           ; tile
    LDA #0
    STA $0202,Y           ; attr
    LDA <x_temp>
    STA $0203,Y           ; sprite X
    INC $09 x4            ; bump cursor by 4

Cost is ~+6 bytes per `draw` over the old static form. At 64
slots the u8 cursor wraps naturally, giving classic NES
"too many sprites" flicker instead of a silent compile-time
drop. `next_oam_slot` and its resets are gone from the IR
codegen entirely.

Secondary fix: `for i in 0..N` counters are now registered as
handler locals. `lower_statement` created a `VarId` for the
counter via `get_or_create_var` but never pushed it onto
`current_locals`, so the IR codegen's `var_addrs` lookup
returned `None` for every `StoreVar(i)` / `LoadVar(i)` and
silently emitted nothing. The counter stayed at 0 forever,
the loop spun indefinitely, and every iteration wrote the
first array element into OAM — turning all 64 sprites into
the same smiley. Same class as the handler-local `var` decl
bug from the earlier PR, just for for-loop variables.

Smoke-test deltas (all 14/14 still pass):
- arrays_and_functions: 104 -> 260  (player + 4 enemies)
- bitwise_ops:          104 -> 416  (player + flag sprites + pips)
- loop_break_continue:  208 -> 208  (already fixed by the earlier pass)
- structs_enums_for:    104 -> 260  (player + 4 enemies)

Regression tests:
- `ir_codegen::more_tests::ir_codegen_draw_sprite` — checks a
  single `draw` emits `LDY cursor`, four `STA $020N,Y`, and
  four `INC cursor`.
- `ir_codegen::more_tests::ir_codegen_multi_oam_uses_sequential_slots`
  — rewritten for the new form: each draw gets its own
  `LDY cursor` + 4 `INC cursor`.
- `ir_codegen::more_tests::ir_codegen_draw_in_loop_...` —
  proves a `draw` inside a `while` compiles to ONE cursor-based
  draw (not N unrolled statics and not zero), and asserts no
  stray `STA $0204`/`$0208`/... absolute stores — those would
  indicate bug B has regressed.
- `ir::tests::for_loop_counter_is_registered_as_handler_local`
  — verifies `for i in 0..N` pushes `i` onto `current_locals`
  so the IR codegen allocates it.

Smoke-test tightening: `tests/emulator/run_examples.mjs` now
has per-example `minNonBlack` floors. `arrays_and_functions`,
`structs_enums_for`, `loop_break_continue`, and `bitwise_ops`
all require multi-sprite rendering — if the OAM cursor bug
comes back, the smoke test fails loudly instead of passing on
the default `nonBlack > 0` check.

The legacy AST codegen in `src/codegen/mod.rs` still uses the
compile-time `next_oam_slot` approach. It's only reachable via
`--use-ast`, none of the examples use it, and its integration
tests only check iNES structure — left alone on purpose.

https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 20:20:20 +00:00
Claude
f49dbce686
Fix three compiler bugs exposed by array-using examples
Landing bug A from the previous writeup plus two adjacent bugs
that the fix exposed. All three miscompile anything that uses a
u8[N] global with a literal initializer.

1. Array-literal globals are now actually initialized.
   `lower_program` only expanded `Expr::StructLiteral` into per-
   field synthetic globals — `Expr::ArrayLiteral` hit
   `eval_const`, returned `None`, and the array boot-cleared to
   zero. `IrGlobal` now carries an `init_array: Vec<u8>`
   populated by lowering, and the IR codegen startup loop emits
   one `LDA #byte; STA base+i` pair per element.

2. Local variables no longer overlap array globals.
   `IrCodeGen::new` advanced `local_ram_next` past
   `max_global_base + 1` — for an array at `$0300-$0303` it
   placed the first handler-local at `$0301`, inside the array.
   The frame handler's stores through the local then corrupted
   the array mid-frame. The allocator now walks the analyzer's
   `VarAllocation` list and advances past `address + size` for
   every RAM global, not just the base.

3. Peephole `remove_redundant_loads` honors indexed LDAs.
   The pass tracked `LDA Immediate/ZeroPage/Absolute` but let
   `LDA AbsoluteX/AbsoluteY/ZeroPageX/IndirectX/IndirectY` fall
   through the match, leaving the A-equivalence tracker
   unchanged. A later `LDA #v` that happened to match a stale
   entry from BEFORE the indexed load would then be dropped as
   "already in A" — a silent miscompile that turned every
   `draw Sprite at: (arr[i], arr[j])` pattern into garbage
   (the second array index would be computed from `arr[i]`'s
   value, reading way out of bounds). Indexed LDAs now clear
   the tracker.

Regression tests:
- `src/codegen/peephole.rs`: a synthetic
  `LDA #0; TAX; LDA AbsX(arr1); STA temp; LDA #0; TAX;
   LDA AbsX(arr2); ...` sequence asserts both `LDA #0`s survive.
- `src/ir/tests.rs`: verifies `var xs: u8[4] = [1,2,3,4]`
  populates `IrGlobal::init_array` with `[1,2,3,4]`.
- `tests/integration_test.rs`: two IR-codegen tests — one checks
  the startup instructions contain `LDA #v; STA base+i` for
  every element, the other compiles a handler-local var
  alongside an array global and asserts no post-init stores
  land inside the array.

Smoke test impact (14/14 still passing, now more visible):
- arrays_and_functions:  56 -> 104 nonBlack, now animated
- loop_break_continue:   52 -> 208 (player + 3 hazards visible)
- structs_enums_for:     52 -> 104 (player + enemy visible)

Existing examples unchanged; no remaining work for bug B
(static OAM slot allocation in loops) — that's the next PR.

https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 19:32:22 +00:00
Claude
81f3fd7de0
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
2026-04-12 18:46:58 +00:00
Claude
496925344d
Language: poke() and peek() hardware intrinsics
Common PPU/APU/mapper access previously required either variable
aliases or inline asm. Now two built-in intrinsics handle the
single-register case directly:

    poke(0x2006, 0x3F)     // STA \$3F, \$2006
    poke(0x2006, 0x00)
    poke(0x2007, 0x0F)
    var status: u8 = peek(0x2002)

- Analyzer: \`poke\` / \`peek\` are recognized as built-in intrinsics
  so they don't require a function declaration. Arity is still
  checked (E0203 on mismatch).
- IR: new \`IrOp::Poke(u16, IrTemp)\` and \`IrOp::Peek(IrTemp, u16)\`
  variants carrying the compile-time constant address.
- IR lowering: recognizes the \`poke\`/\`peek\` call names, evaluates
  the address as a const expression, and emits the intrinsic op.
  Falls back to a regular call if the address isn't a constant.
- IR codegen: emits a single LDA/STA in ZP or absolute mode based
  on whether the address fits in a byte.
- Optimizer: Poke has a source temp (liveness), Peek has a dest
  (new value); both pass through the existing passes.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 17:40:34 +00:00
Claude
a45944e0f9
Language: raw asm { ... } blocks
raw asm {
        LDA #\$42
        STA \$00
    }

Unlike regular \`asm\`, \`raw asm\` does not perform \`{var}\`
substitution — the body is passed to the inline parser verbatim.
Useful for writing completely unmanaged bytes that don't rely on
the analyzer's variable allocations, e.g. mapper init snippets.

Implementation:
- Parser: \`KwRaw\` followed by \`KwAsm\` emits
  \`Statement::RawAsm(body, span)\`.
- IR lowering: prepends a \`\\0RAW\\0\` marker to the body when
  emitting \`IrOp::InlineAsm\` so the codegen can distinguish raw
  from regular without adding a second op variant.
- IR codegen: strips the marker and skips substitution when present.
- AST codegen: same, handling \`Statement::RawAsm\` directly.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 17:34:17 +00:00
Claude
c8ae433a7c
Language: struct literals
struct Vec2 { x: u8, y: u8 }
    var pos: Vec2 = Vec2 { x: 100, y: 50 }
    on frame {
        pos = Vec2 { x: pos.x + 1, y: pos.y }
    }

- AST: new \`Expr::StructLiteral(name, fields, span)\` variant
- Parser: in expression position, \`Ident {\` enters struct-literal
  mode when the new \`restrict_struct_literals\` flag is off.
  \`if\`/\`while\`/\`for\` conditions set the flag so the \`{\` keeps
  going to the following block. Condition contexts can still use
  struct literals by parenthesizing them.
- Analyzer: validates that the struct type exists, each named field
  belongs to it, and each field value has a compatible type.
- IR lowering: desugars \`var = StructLiteral { ... }\` (both in
  assignments and variable initializers) into per-field StoreVar
  operations against the analyzer-synthesized \`var.field\`
  variables. No IR type for struct values is needed.
- AST codegen: no-op (legacy path).
- examples/structs_enums_for.ne now uses a struct literal for the
  initial \`player\` state instead of per-field assignments.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 17:15:57 +00:00
Claude
34b312537d
IR codegen: allocate storage for function-local variables
Function bodies can declare local variables with \`var NAME: u8 = …\`.
Previously the lowering created a VarId for them but didn't track it
on the \`IrFunction.locals\` list, so the IR codegen had no address
to map it to and \`LoadVar\` / \`StoreVar\` silently did nothing. The
generated function body read and wrote random temp slots.

Fixes:

- Lowering: replaced the per-function \`locals\` local with a
  long-lived \`current_locals\` field; \`lower_function\` resets it
  on entry and moves it into the \`IrFunction\` at exit. Each
  \`Statement::VarDecl\` inside a function body appends to
  \`current_locals\`.
- IR codegen: iterate every function's \`locals\` list. Params 0..4
  still map to \$04-\$07, and the remaining locals get addresses in
  main RAM starting at \$0300. Each function's locals are disjoint,
  so nested calls don't corrupt each other's state.
- Integration test \`program_with_function_local_variables\`
  exercises nested calls with function-local state to guard against
  regression.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 17:01:12 +00:00
Claude
240da57b54
Language: for i in start..end loops
Adds a \`for NAME in START..END { BODY }\` half-open range loop:

    for i in 0..8 {
        total += arr[i]
    }

- Lexer: \`for\`, \`in\` keywords and the \`..\` range operator
- AST: new \`Statement::For\` variant with var/start/end/body
- Parser: \`parse_for\` after \`while\` / \`loop\`
- Analyzer: registers the loop variable as a u8 symbol for the body
  (restoring any shadowed outer symbol afterwards), allocates it via
  the normal RAM allocator, and tracks it as "used"
- IR lowering: desugars to \`var = start; while var < end { body;
  var = var + 1 }\` using a \`for_step\` continue-edge block so
  \`continue\` properly increments the index
- AST codegen: no-op (legacy path doesn't need for loops)
- Tests: parse + full-pipeline integration

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 16:55:18 +00:00
Claude
8f0e0635df
IR lowering: constant expression folding
Constants can now reference other constants and use arithmetic /
bitwise / comparison operators. The lowering runs \`eval_const\` on
each \`ConstDecl\` and \`VarDecl\` initializer, threading the existing
\`const_values\` map for identifier lookups.

Examples that now work:

    const BASE: u8 = 10
    const OFFSET: u8 = BASE + 5       // 15
    const MASK: u8 = FLAG_A | FLAG_B  // bitwise-or of two earlier consts
    var counter: u8 = BASE            // initializer is now 10

Enum variants are registered before constants so a const like
\`const FIRST: u8 = VariantA\` also resolves at compile time.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 16:48:29 +00:00
Claude
e16b765959
Parser: audio statements (play / start_music / stop_music)
The \`play\`, \`start_music\`, and \`stop_music\` keywords were lexed
but not parsed. Programs using them failed with a generic "unexpected
token" error. Now:

- \`Statement::Play(sfx_name, span)\`
- \`Statement::StartMusic(track_name, span)\`
- \`Statement::StopMusic(span)\`

parse successfully and flow through analyzer / IR lowering / codegen
as no-ops. Semantics match the spec but produce no output — no audio
driver exists yet. Users who need sound can still wire in custom
code via \`asm { ... }\` blocks.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 16:38:59 +00:00
Claude
359241d906
Codegen: MMC3 on_scanline IRQ dispatch (minimal)
Wires \`on scanline(N)\` handlers through IR lowering and codegen:

- IR lowering: each scanline handler becomes a regular IR function
  named \`{state}_scanline_{N}\`
- IR codegen: when any scanline handler exists, emits MMC3 IRQ setup
  at program start (\$C000 latch, \$C001 reload, \$E001 enable, CLI)
  and a \`__irq_user\` handler that saves registers, acknowledges via
  \$E000, JSRs the scanline handler, restores registers, and RTIs
- Linker: vector table prefers \`__irq_user\` over the default \`__irq\`
  stub when both exist

Scope of this first pass is intentionally minimal: supports ONE
scanline handler per program (the first one found in IR function
order). Per-state dispatch and multi-scanline reload will come later.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 16:22:54 +00:00
Claude
225d40cea5
Language: struct types
Adds composite \`struct\` types with field access:

    struct Vec2 { x: u8, y: u8 }

    var pos: Vec2
    pos.x = 100
    pos.y = pos.x + 5

- Lexer: \`struct\` keyword
- AST: \`StructDecl\` with \`StructField\` list; \`NesType::Struct(name)\`
  for struct-typed variable declarations; \`Expr::FieldAccess\` and
  \`LValue::Field\` for reads/writes
- Parser: top-level \`struct Name { field: type, ... }\` (optional
  trailing comma) and \`ident.field\` syntax in both expression and
  lvalue position
- Analyzer: \`register_struct\` computes contiguous field offsets
  (no padding) and stores them in \`struct_layouts\`. Struct variables
  synthesize a \`VarAllocation\` per field under the name
  \`"struct_var.field"\`, and \`Expr::FieldAccess\` / \`LValue::Field\`
  resolve against those. Unknown struct types and unknown fields
  emit E0201.
- IR lowering + AST codegen: treat struct field access as ordinary
  variable access against the synthetic per-field symbols. No new IR
  ops are needed.

v1 structs only support primitive fields (u8/i8/bool). Nested structs,
u16 fields, and array fields are not yet allowed.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 16:18:05 +00:00
Claude
dbfcb313bc
Language: enum declarations
Adds \`enum Name { V1, V2, ... }\` as a top-level declaration. Each
variant is registered as a \`u8\` constant equal to its index in the
declaration. Variant names are global and must be unique across all
enums and other symbols (E0501 on collision).

- Lexer: \`enum\` keyword
- AST: \`EnumDecl { name, variants, span }\` field on \`Program\`
- Parser: top-level \`enum\` syntax with optional trailing commas
- Analyzer: \`register_enum\` flattens variants into the symbol table
- IR lowering and AST codegen: variants resolve through the existing
  \`const_values\` path
- Tests cover parsing, duplicate detection, and usage

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 11:36:44 +00:00
Claude
121b0b1968
Inline assembly: asm { ... } blocks
- Lexer: after \`asm\` keyword, next \`{\` triggers raw-text capture of
  the body until the matching \`}\`, emitted as a new \`AsmBody\` token
- Parser: \`asm { ... }\` produces \`Statement::InlineAsm(body, span)\`
- Analyzer: treats inline asm as opaque (no checks)
- IR: new \`IrOp::InlineAsm(String)\` variant that passes the body
  through the optimizer unchanged
- \`src/asm/inline_parser.rs\`: minimal 6502 mnemonic parser supporting
  every addressing mode we emit elsewhere (immediate, ZP/ABS with X/Y,
  indirect, indirect-X/Y, labels, branches, implied, accumulator)
- Both IR and AST codegen splice parsed instructions inline
- Integration test covers a mix of implied + immediate + ZP + A modes

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 11:16:18 +00:00
Claude
d609b77cd7
IR codegen: scroll, debug.log, debug.assert
Adds Scroll / DebugLog / DebugAssert variants to IrOp, wires them into
the IR lowering, optimizer liveness tracking, and IR codegen. Scroll
emits two PPU $2005 writes; debug statements emit writes to $4800 when
IrCodeGen::with_debug(true) is set, stripped otherwise. These were the
last feature gaps versus the AST codegen path, so IR codegen is now a
full replacement.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 10:43:53 +00:00
Claude
8a6441071e
IR codegen: Player 2 controller support
ReadInput now takes an explicit player index (0 = P1, 1 = P2). The IR
lowering for ButtonRead threads the player through, and the IR codegen
selects the correct zero-page input byte ($01 for P1, $08 for P2).

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 10:40:42 +00:00
Claude
5660a8b7c3
IR codegen: state dispatch, multi-OAM, and transition support
State machine dispatch:
- IrProgram now stores states (Vec<String>) and start_state
- Lowering captures state metadata before walking the AST
- IR codegen generates a main loop with vblank wait and CMP+BNE+JMP
  dispatch table, matching the AST codegen's layout
- Each frame handler ends with JMP __ir_main_loop
- current_state initialized to the start state's index at boot

Multi-OAM support:
- next_oam_slot counter, reset at the start of each _frame function
- Sequential allocation of 4-byte OAM entries at $0200 + slot*4
- Silently drops draws beyond slot 63 (OAM full)

Transition codegen:
- IrOp::Transition now looks up the target state's index from
  state_indices, writes it to ZP $03, and JMPs back to main loop
- Previously this was a no-op placeholder

Shared constants:
- ZP_FRAME_FLAG ($00) and ZP_CURRENT_STATE ($03) match AST codegen

Tests: 271 total (5 new IR codegen tests + 2 new integration tests)
All 7 examples compile through --use-ir, including multi-state games
and programs with transitions and functions.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 10:33:58 +00:00
Claude
1ede169f1e
Implement IR-based code generator (--use-ir)
New src/codegen/ir_codegen.rs walks IrProgram and emits 6502 instructions.
This enables optimizer passes to actually affect the output ROM.

Design:
- Each IR temp gets a zero-page slot at $80 + temp_index
- Functions reset the temp counter at entry (temps don't outlive functions)
- Globals map by name to their analyzer-assigned zero-page addresses
- Operands are loaded into A, computed, stored back to the dest slot

Handles all IrOp variants:
- LoadImm, LoadVar, StoreVar (basic loads/stores)
- Add/Sub (CLC+ADC / SEC+SBC)
- Mul (JSR __multiply runtime routine)
- And/Or/Xor (zero-page operands)
- ShiftLeft/ShiftRight (repeated ASL/LSR)
- Negate/Complement (EOR #$FF + optional two's complement)
- CmpEq/Ne/Lt/Gt/LtEq/GtEq (CMP + conditional branch + 0/1)
- ArrayLoad/ArrayStore (TAX + ZeroPageX/AbsoluteX)
- Call (ZP param passing + JSR)
- DrawSprite (OAM slot 0 write, uses sprite_tiles map)
- ReadInput (LDA $01, P1 input)
- WaitFrame (poll frame flag at $00)

All terminators:
- Jump (JMP to block label)
- Branch (LDA temp + BNE true / JMP false)
- Return (optional value in A + RTS)
- Unreachable (BRK)

IR lowering fixes:
- ReadInput now has a destination IrTemp (was a side-effect-only op)
- ButtonRead uses the proper input temp instead of uninitialized register
- Logical AND/OR use new emit_move helper (OR with zero) instead of
  bogus raw VarId for path merging

CLI:
- New --use-ir flag on `build` subcommand to opt in to IR codegen
- Default remains AST codegen (for now); IR codegen is experimental

All 7 examples compile through the IR pipeline and produce valid iNES ROMs.

Tests: 266 total (7 new ir_codegen unit + 2 new integration).

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 10:23:43 +00:00
Claude
45b2b2a279
Add debug.log and debug.assert statements
Parser:
- debug.log(expr, ...) and debug.assert(cond) syntax
- parse_debug_statement() handles both methods
- New AST variants: Statement::DebugLog and Statement::DebugAssert

Codegen:
- CodeGen::with_debug(bool) to toggle debug instrumentation
- DebugLog writes each expression to $4800 (Mesen debug port)
- DebugAssert evaluates condition; on false, writes $FF to $4800 + BRK
- Both are stripped entirely when debug_mode = false (release builds)

CLI:
- --debug flag now wired through to CodeGen
- Without --debug, all debug statements produce zero bytes

Analyzer:
- Type-checks DebugAssert condition as bool
- Walks DebugLog args for reads (used_vars tracking)

IR lowering:
- DebugLog/DebugAssert are no-ops (codegen handles them directly)

254 tests (5 new: 3 parser + 2 codegen).

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 10:11:32 +00:00
Claude
6430c3a935
Sprite resolution, asset wiring, shift-assign, unreachable state warning
Sprite/asset pipeline:
- Linker::link_with_assets() places sprite CHR data in ROM at correct tile
- assets::resolve_sprites() walks Program for inline sprite bytes
- CodeGen::with_sprites() maps sprite names to tile indices
- gen_draw() uses correct tile index from sprite declarations
- main.rs wires the full resolution pipeline

Shift-assign operators (<<= and >>=):
- AssignOp::ShiftLeftAssign and ShiftRightAssign variants
- Parser handles in both statement and array index contexts
- Codegen emits ASL A / LSR A
- IR lowering maps to ShiftLeft/ShiftRight ops

Unreachable state warning (W0104):
- BFS from start state finds reachable states via transitions
- States not reached produce W0104 warning

Error polish helpers:
- suggest_var_name() for "did you mean" suggestions
- emit_undefined_var() for E0502 with typo hints
- Used by analyzer for better diagnostics

242 tests pass, clippy clean.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 10:01:44 +00:00
Claude
5434dda114
M4+M5: Optimizer passes, type casting, bank switching, math runtime
Milestone 4 — Optimization & Polish:
- Strength reduction: multiply by power-of-2 → shift left
- Zero-page promotion analysis: rank variables by access frequency
- `as` type casting expression in parser/AST/analyzer
- `scroll(x, y)` statement
- `--asm-dump` flag for viewing generated assembly
- Extended optimizer tests (strength reduction, frequency analysis)

Milestone 5 — Bank Switching & Release:
- Mapper support: MMC1 (1), UxROM (2), MMC3 (4) in parser and ROM builder
- Bank declarations: `bank Name: prg` / `bank Name: chr`
- Linker::with_mapper for mapper-aware ROM generation
- Software multiply (8x8→16, shift-and-add algorithm)
- Software divide (8÷8→8, restoring division algorithm)
- ROM tests for mapper encoding round-trip
- Integration test for MMC1 compilation

210 tests total (18 new), all pre-commit checks pass.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 00:22:11 +00:00
Claude
058f7a87b6
M3: Asset pipeline, sprite/palette/background declarations, debug symbols
Parser extensions:
- sprite declarations with chr: @chr("file.png"), @binary("file.bin"), or inline [hex]
- palette declarations with colors: [0x0F, 0x00, 0x10, 0x20]
- background declarations with chr: asset source
- @chr/@binary asset source parsing
- load_background and set_palette statements
- --debug CLI flag (plumbed through, not yet wired to codegen)

Asset pipeline (new module):
- PNG → CHR tile conversion using image crate (8x8 tiles, 2-bitplane encoding)
- NES color palette table (all 64 standard NES colors as RGB)
- Nearest-color matching (Euclidean distance in RGB space)

Debug module (new):
- Source map (ROM address → source Span mapping)
- Debug symbols with variable address table
- Mesen-compatible .mlb label export
- .sym symbol table export

192 tests total (13 new: 5 parser + 3 asset + 5 debug)

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 00:09:47 +00:00
Claude
192d9c5c3d
M2: Add call graph analysis, recursion detection, and optimizer
Analyzer extensions:
- Call graph construction from function bodies and state handlers
- DFS-based recursion detection (direct and mutual) with E0402 errors
- Max call depth computation per entry point with E0401 enforcement
- Function declarations registered as symbols (E0503 for undefined calls)
- Collects calls from all statement/expression types recursively

Optimizer (new module):
- Constant folding: evaluate known-constant arithmetic at compile time
- Dead code elimination: remove ops with unused destination temps
- Both operate per-basic-block in a single pass

171 tests total (22 new: 6 analyzer + 11 IR lowering + 5 optimizer)

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 23:32:12 +00:00
Claude
664ccc05db
M2: Add function/array parsing, IR data structures and lowering
Parser extensions:
- Function declarations with params and return types (fun, inline fun)
- Array type syntax (u8[16]), array literal expressions ([1, 2, 3])
- fast/slow variable placement hints
- Functions stored in Program AST

IR module (new):
- IrProgram, IrFunction, IrBasicBlock, IrOp, IrTerminator types
- AST-to-IR lowering pass with:
  - Expression lowering to virtual temps
  - Control flow (if/else, while, loop) to basic blocks with branches
  - Break/continue via loop context stack
  - Short-circuit logical and/or
  - Button reads, draw sprites, wait_frame
  - Constants inlined as LoadImm
  - State handlers lowered as separate IR functions

Tests: 6 new parser tests, 11 new IR lowering tests (153 total)

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 23:25:26 +00:00