1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-14 19:46:46 +00:00
Commit graph

80 commits

Author SHA1 Message Date
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
0a3e830acf
Codegen: peephole branch folding + dead JMP elimination
Two new passes that save 2 instructions per conditional:

- \`fold_branch_over_jmp\` rewrites \`BNE taken; JMP else_block;
  taken:\` into \`BEQ else_block; taken:\`. Emitted by the IR codegen
  for every \`if\` without a matching \`else\`. Bounded lookahead of
  ~60 instructions keeps the rewritten branch within the signed
  8-bit reach (-128..+127).
- \`remove_jmp_to_next_label\` drops \`JMP foo; foo:\` since the JMP
  is a no-op — execution falls through to the label anyway.

Instruction count impact (IR vs AST, lower is better):

                         before            after
    arrays_and_functions 262 vs 236   ->   243 vs 235
    bouncing_ball        135 vs 144   ->   122 vs 143
    coin_cavern          290 vs 291   ->   261 vs 289
    hello_sprite          73 vs  64   ->    64 vs  63
    mmc1_banked          261 vs 247   ->   235 vs 246
    sprites_and_palettes 121 vs 113   ->   108 vs 112
    state_machine        233 vs 229   ->   209 vs 228

IR codegen now beats the AST codegen on 5 of 7 examples and is
competitive on the other two.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 16:44:27 +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
1778ae0ec8
IR codegen: fix function call target label + parameter passing
Two bugs in the IR codegen's function call path:

1. \`IrOp::Call\` was emitting \`JSR __fn_name\` but functions are
   defined at \`__ir_fn_name\`. The JSR went to an unresolved label
   and the generated ROM would jump to garbage.

2. Caller stored arguments to ZP \$04-\$07 but the callee's
   \`LoadVar\` for each parameter VarId resolved to a temp slot at
   \$80+ because parameter VarIds weren't in \`var_addrs\`. The
   callee never read the arguments.

Fixes:

- Register each function's first four parameter VarIds at
  \$04-\$07 in \`var_addrs\` during codegen construction, so
  \`LoadVar\` for a parameter emits \`LDA \$04\` / \`\$05\` / etc.
- Call emits \`JSR __ir_fn_{name}\` matching the function label.

Adds a unit test that exercises both sides of the contract:
the caller must STA to \$04/\$05 and JSR to \`__ir_fn_sum\`, and the
callee must LDA from \$04 to read the first parameter.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 16:33:06 +00:00
Claude
08322abda4
Codegen: on_scanline per-state dispatch + NMI reload
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
2026-04-12 16:29:15 +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
9c102cb68e
Codegen: copy propagation + dead LDA elimination in peephole
Adds two new passes that dramatically close the IR-vs-AST output
size gap:

- **copy_propagate_temps**: tracks the source of each \`STA\` into an
  IR temp slot (\$80-\$FF). Subsequent \`LDA\` of that slot is rewritten
  to load from the original source directly. The same rewrite applies
  to arithmetic/logical ops that read a temp (\`ADC\`/\`SBC\`/\`AND\`/
  \`ORA\`/\`EOR\`/\`CMP\`). The now-unused \`STA\` is picked up by the
  dead-store pass on the next iteration.
- **remove_dead_loads**: drops an \`LDA\` whose value is immediately
  overwritten by another A-clobbering load (\`LDA\`/\`PLA\`/\`TXA\`/\`TYA\`).

The existing dead-store pass is also extended to scan through labels
and branches within a function body, since IR temp slots are
SSA-unique per function. Function boundaries (\`__ir_fn_\`/\`__fn_\` labels,
\`RTS\`/\`RTI\`) still stop the scan.

Instruction count comparison (IR vs AST codegen on examples/):

                             before            after
    arrays_and_functions.ne  359 vs 236   ->   264 vs 236  (-95 ir)
    bouncing_ball.ne         192 vs 144   ->   135 vs 144  (-57 ir, IR now wins)
    coin_cavern.ne           412 vs 291   ->   290 vs 291  (-122 ir, IR ties)
    hello_sprite.ne          116 vs 64    ->    73 vs 64
    mmc1_banked.ne           378 vs 247   ->   261 vs 247
    sprites_and_palettes.ne  194 vs 113   ->   121 vs 113
    state_machine.ne         323 vs 229   ->   233 vs 229

IR is now competitive with the AST codegen on every example and
beats it on bouncing_ball and coin_cavern.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 16:06:26 +00:00
Claude
e1bddbc0db
Codegen: A-value tracking peephole for redundant LDA elimination
Tracks what the accumulator currently holds across a linear run of
instructions. When we see \`LDA addr\` or \`LDA #val\` and A is
already known to hold that value, the load is dropped. A-modifying
ops (ADC, SBC, shifts, transfers from X/Y, etc.) clear the tracker.
Any control-flow instruction or label resets it too, so JSR / branches
remain safe.

Reduces STA/LDA count in bouncing_ball from 113 to 108.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 11:42:04 +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
8b35ba9e13
Codegen: peephole dead-store elimination for IR temps
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
2026-04-12 11:27:16 +00:00
Claude
b7c806523d
Codegen: peephole pass for redundant STA/LDA pairs
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
2026-04-12 11:19:56 +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
adf50aaa7f
IR codegen: on_enter handlers, frame handler return-to-main-loop
- 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
2026-04-12 10:36:31 +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
18e9bed258
Add Player 2 controller support
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
2026-04-12 10:05:12 +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
1bbc2bc160
Implement break/continue/return/transition/call and multi-OAM sprites
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
2026-04-12 02:07:00 +00:00
Claude
40632f192c
Implement codegen for state dispatch, functions, arrays, math, scroll
State machine dispatch:
- Assign each state a numeric index, store in ZP $03
- Main loop dispatch table: CMP + BNE + JMP trampoline pattern
  (avoids branch range limits for large programs)
- on_enter/on_exit handlers generated as JSR targets
- Transition statement writes state index + JSR enter/exit handlers

Function calls:
- Function bodies emitted as labeled subroutines with RTS
- Statement::Call generates parameter passing via ZP + JSR
- Statement::Return generates RTS (with value in A if present)
- Parameter slots at ZP $04-$07

Break/continue:
- Loop stack tracks continue/break label pairs
- Break generates JMP to break_label
- Continue generates JMP to continue_label
- While and Loop push/pop the stack

Array indexing:
- LValue::ArrayIndex generates TAX + STA absolute,X
- Expr::ArrayIndex generates TAX + LDA absolute,X / ZP,X
- Compound array assignments (+=, -=, &=, |=, ^=) load-modify-store

Scroll:
- scroll(x, y) writes to PPU $2005 twice (X then Y)

Math:
- Multiply generates JSR __multiply (shift-and-add routine)
- Divide generates JSR __divide (restoring division)
- Modulo loads remainder from $03 after divide
- ShiftLeft generates ASL A, ShiftRight generates LSR A
- Math routines wired into linker

Error validations:
- E0203 for assignment to const variables
- Break/continue outside loop detection (in_loop tracking)

233 tests (8 new codegen + 2 analyzer + 2 integration), all passing.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 02:04:49 +00:00
Claude
bc165ed647
Fix bugs found during code review
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
2026-04-12 01:00:27 +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
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
Claude
fd5a940c89
Add example builds to CI, document sprite name behavior
- 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
2026-04-11 22:55:43 +00:00
Claude
39ca246151
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap

143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)

CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00