Adds \`IrProgram::pretty()\` for a readable IR dump and wires it to a
new \`--dump-ir\` build flag. Useful for debugging IR-level issues
(optimizer, lowering). Refactored \`compile()\` to take a
\`CompileOptions\` struct to keep clippy's \`fn-params-excessive-bools\`
lint happy.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
Adds a per-instruction scan: for each \`STA slot\` where slot is an
IR temp (\$80-\$FF), walks forward through the instruction stream.
If the slot is overwritten with no intervening read, the original
STA is dead and is removed. Conservatively bails at any control-flow
boundary (jump, branch, call, return, label definition) since a
jumper or callee might read the slot.
Complements the existing peephole passes.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
Previously the unused-variable warning only ran on globals. Now it
walks \`state.locals\` too, so state-scoped variables that are
declared and never read also emit W0103. Factored out to
\`check_unused_var\` to share the \`_\`-prefix exemption and help text.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
Adds src/codegen/peephole.rs with two passes that run to fixed point
after codegen:
- \`STA slot; LDA slot\` over an IR temp (\$80-\$FF): the LDA is
redundant since A already holds the value. Dropped.
- \`LDA addr; STA addr\` (same address): the STA is a no-op since
the byte was just loaded from that slot. Dropped.
Conservative on user variables (not IR temps) so intervening IRQs or
function calls can't invalidate the optimization. This is the biggest
single win for IR codegen output quality, since IR codegen currently
stores every temp to ZP regardless of reuse.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
- 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
When a block contains a statement after \`return\`, \`break\`,
\`continue\`, or \`transition\`, the trailing statement is unreachable
and the analyzer now emits W0104 pointing at it. The warning includes
a secondary label on the terminator showing where execution stops.
Adds \`Statement::span\` helper to the AST so diagnostics can point at
arbitrary statements cleanly.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
The linear RAM allocator now checks for overflow. The zero-page region
is capped at \$80 (leaving \$80-\$FF for IR codegen temp slots), and the
main RAM allocator stops at \$0800 (end of NES internal RAM). Overflow
emits E0301 with a helpful note pointing at the declaration.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
Emits W0101 on \`a * b\`, \`a / b\`, \`a % b\` expressions where both
operands are non-constant (neither is an integer literal). These lower
to calls into the software multiply/divide routines and can blow the
6502 cycle budget. Multiplying by a constant is still silent because
the optimizer can strength-reduce power-of-2 cases to shifts and small
factors to add chains.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
Adds parser support for \`on scanline(N) { ... }\` event handlers
inside state bodies. The analyzer enforces that scanline handlers
are only allowed with the MMC3 mapper (the only mapper we target
with scanline IRQ support), and validates the body like any other
state handler.
Codegen for the scanline IRQ vector is still TODO — the handler
is parsed and validated but not yet hooked up to the MMC3 IRQ
counter. When we get to it, the linker needs to install an IRQ
handler that writes to \$C000/\$C001/\$E000/\$E001.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
Emits W0102 when a \`loop\` body contains no \`break\`, \`return\`,
\`transition\`, or \`wait_frame\`, since such loops spin forever and
prevent vblank from being handled. The check recurses into nested
if/while bodies so that \`break\` behind a condition still counts.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
Tracks the current function's declared return type and checks that
`return value` statements match it. Returning from a void function
with a value produces E0203; returning a value with the wrong type
produces E0201. State handler `return value` is still silently
accepted (the value is discarded).
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
Tracks parameter types per function in a signature map, and validates
that each Statement::Call and Expr::Call has the right number of
arguments (E0203). Also type-checks each argument against its declared
parameter type. Previously, calls with the wrong argument count silently
passed semantic analysis and only broke later at codegen or runtime.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
The IR-based codegen now matches all features of the AST codegen
(state dispatch, multi-OAM, P1/P2 input, scroll, debug.log/assert),
so flip the default. The legacy AST codegen is still available via
--use-ast for comparison and fallback during validation.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
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
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
- Initial program boot calls the start state's on_enter handler if it exists
- Transition IR op now JSRs the target state's on_enter handler after
updating current_state and before jumping to the main dispatch loop
- Frame handlers translate their Return(None) terminator to
JMP __ir_main_loop instead of RTS (they're not called via JSR)
- Added function_exists() helper and function_names HashSet for quick lookup
- Added in_frame_handler flag to track context during terminator emission
All 271 tests still pass, all 7 examples still compile through --use-ir.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
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
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
resolve_sprites() now handles all three AssetSource variants:
- Inline(bytes): use directly (existing behavior)
- Binary(path): read raw bytes from file relative to source_dir
- Chr(path): convert PNG to CHR via png_to_chr()
Missing files are silently skipped rather than erroring, so
declarations can reference assets that haven't been added yet.
This keeps existing tests that use placeholder file paths working.
Updated future-work.md: moved include directive, P2 controller,
sprite resolution, shift-assign, debug statements, warnings, and
asset wiring to the "completed" section. Remaining work is IR codegen,
audio, on_scanline, and language features (structs/enums).
Tests: 257 total (3 new resolve_sprites tests)
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
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
Parser/preprocess module:
- New src/parser/preprocess.rs handles source-level include inlining
- 'include "path"' at line start splices the target file's content
- Paths resolved relative to the including file's directory
- Circular include detection via visited set (canonicalized paths)
- Non-cycle re-includes allowed (sibling branches can reuse files)
main.rs:
- Both compile() and check() run preprocess_source() before parsing
- Include resolution failures produce a clear error message
Tests: 4 new preprocess unit tests (249 total)
- passthrough, include line parsing, temp-file roundtrip
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
Parser:
- p1.button.X and p2.button.X syntax
- Emits Expr::ButtonRead with Some(Player::P1) or Some(Player::P2)
- Plain button.X still works (defaults to P1)
- Removed #[allow(dead_code)] from Player::P1 and Player::P2
Runtime:
- NMI handler now reads both $4016 (JOY1) and $4017 (JOY2) in
the same loop, shifting 8 bits into ZP_INPUT_P1 ($01) and
ZP_INPUT_P2 ($08) simultaneously
- JOY2 register constant added
Codegen:
- Button reads select correct ZP address based on Player variant
- gen_condition and gen_expr both handle P1/P2 dispatching
Tests: 245 (3 new parser tests: p1/p2 button read + shift-assign)
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
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
Control flow:
- Break: JMP to break_label from loop_stack
- Continue: JMP to continue_label from loop_stack
- Return: evaluate expr into A (if present), RTS
- Transition: write state index to ZP $03, JMP __main_loop
- Call: evaluate args to ZP param slots, JSR __fn_name
Multi-OAM sprite support:
- next_oam_slot counter (0-63) increments per draw
- Reset to 0 at the start of each frame handler
- Each draw writes to sequential 4-byte OAM entries ($0200+slot*4)
- Silently stops drawing if >64 sprites per frame (OAM full)
All 233 tests pass, clippy clean.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
Issues found by systematic review of all compiler modules.
Each fix includes a regression test.
Parser fixes:
- Add EOF checks to function call arg parsing loops (lines 984, 1269)
to prevent infinite loops on unterminated call expressions
- Fix span end positions: use .end instead of .start for 9 declaration
span computations (var, const, fun, state, bank, sprite, palette,
background, block)
- Add missing compound assignment operators (&= |= ^=) to
parse_assign_op() for array element assignments
Codegen fix:
- Fix NOT operator: old code used EOR #$FF + AND #$01 which produced
wrong results for non-boolean inputs (e.g., NOT 0x42 returned 1
instead of 0). New code uses BEQ/branch to properly map any nonzero
value to 0 and zero to 1.
Assembler fixes:
- Add branch offset range validation: assert offset is in -128..127
range instead of silently wrapping on overflow
- Panic on unresolved labels instead of silently leaving placeholder
0x00 bytes in the output
Verified non-issues (reviewer false positives):
- NMI P register: 6502 hardware pushes P automatically on NMI entry;
RTI restores it. PHP/PLP not needed.
- Controller read buffer: 8 iterations of LSR+ROL fully replace all
8 bits, so prior contents don't matter.
- advance() panic: tokens always has at least Eof from lexer.
4 new regression tests, 225 total.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
Fuzz targets (cargo +nightly fuzz):
- fuzz_lexer: lexer must never panic on arbitrary input
- fuzz_parser: parser must never panic on arbitrary input
- fuzz_compile: full pipeline (parse → analyze → IR → optimize →
codegen → assemble → link) must never panic
Results: 3.3M iterations across all targets, zero crashes.
Seed corpus from all 7 examples + crafted edge cases.
11 new parser edge case tests:
- draw_followed_by_statement: regression for keyword-arg lookahead bug
- draw_with_frame_followed_by_statement: same with frame: arg
- nested_if_else_chain: 3-deep else-if chain
- deeply_nested_blocks: 4-deep nested if blocks
- empty_function_body: fun noop() {}
- empty_state_handlers: on enter/exit/frame {}
- button_start_in_condition: "start" keyword as button name
- button_select_in_condition: "select" keyword as button name
- multiple_draws_in_sequence: 3 draws followed by assignment
- parser_no_panic_on_garbage: 25 malformed inputs
- lexer_no_panic_on_garbage: 10 malformed inputs
221 tests total, all pre-commit checks pass.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
README: project overview, quick start, feature list, example table
LICENSE: MIT
4 new examples covering all language features:
- arrays_and_functions: arrays, while loops, inline/regular functions
- state_machine: multi-state flow with on enter/exit handlers
- sprites_and_palettes: inline CHR data, palette switching, scroll, cast
- mmc1_banked: MMC1 mapper, bank declarations, software multiply
Parser fix: draw statement keyword-arg parsing now checks for ':'
lookahead before consuming an identifier, preventing it from eating
the next statement as a keyword argument (e.g., `i += 1` after a draw).
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
Pipeline:
- main.rs now runs IR lowering and optimization before codegen
- IR is built and optimized but output still uses AST-based codegen
(IR-based codegen is a future improvement)
Coin Cavern example (examples/coin_cavern.ne):
- 3-state game: Title → Playing → GameOver
- Functions (clamp_x), constants, gravity physics, coin collection
- Demonstrates most M2 language features
Integration tests (14 total, 7 new):
- program_with_functions: functions with params and return values
- program_with_while_loop: while loops compile correctly
- program_with_fast_slow_vars: placement hints accepted
- program_with_multi_state_transitions: 3-state cycle
- coin_cavern_compiles: full Coin Cavern example
- ir_pipeline_produces_ir: validates IR lowering + optimizer
- error_test_recursion_detected: E0402 for recursive functions
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
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
- CI: new "Build Examples" job compiles all .ne files and validates
the output ROMs have correct iNES headers
- Examples: add notes explaining that sprite names (Smiley, Ball) are
parsed but not yet resolved — all draws use the built-in CHR tile 0.
Custom sprite declarations come in M3.
- Codegen: explicit `let _ = &draw.sprite_name` to document the
intentional skip, with comment about M2/M3 scope
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
Two example .ne programs with compiled .nes ROMs:
- hello_sprite: d-pad controlled sprite movement
- bouncing_ball: auto-bouncing sprite with edge detection
Includes README with build instructions and emulator setup.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3