Extends the \`on_scanline\` codegen to support multiple scanline
handlers across states:
- \`__irq_user\` now dispatches by \`current_state\`: each state with a
scanline handler gets a CMP/BNE/JSR entry in the dispatch table.
States without a handler fall through to just acknowledge the IRQ.
- New \`__ir_mmc3_reload\` helper that (re)loads the MMC3 counter
latch based on \`current_state\`. States without a scanline handler
fall through to disable the IRQ (\$E000 write).
- Linker detects the \`__ir_mmc3_reload\` label in user code and
splices a JSR into it at the top of the NMI handler, so the
counter is reloaded once per frame with the current state's
target scanline.
- IRQ handler no longer re-enables IRQ on ACK (the NMI reload now
handles that) so it won't fire multiple times per frame.
- Program init chooses the start state's scanline count (if any) or
the first scanline handler found as a fallback.
Also fixes \`dump_asm\`: a \`NOP\` with a \`Label\` operand is a label
definition, but any other opcode with a \`Label\` operand is a real
instruction like \`JSR foo\`. The old dump was hiding JSR/JMP targets.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
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
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
Documents the \`enum Name { Variant, ... }\` syntax and adds
\`--dump-ir\` and \`--use-ast\` to the CLI flag table. Also adds
an integration test covering enum-variant-as-condition and variant
assignment through the full compile pipeline.
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
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
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
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