1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 00:45:38 +00:00
nescript/examples/nested_structs.ne

57 lines
1.9 KiB
Text
Raw Normal View History

analyzer/lowering: support nested struct fields and array struct fields Struct field types beyond the v1 scalar set (`u8`, `i8`, `u16`, `bool`) used to error out with `E0201: struct fields must be u8/i8/u16/bool`. The size accumulator already handled them correctly — what was missing was: (1) the analyzer side that synthesizes per-leaf symbols and allocations for nested structs plus a single array-typed symbol for array fields, (2) the parser's chained-field-access path, and (3) the IR-lowering recursion through nested struct literal initializers and array literal field values. The synthetic-variable model carries through unchanged: a `var p: Player` where `Player { pos: Vec2, hp: u8, inv: u8[4] }` and `Vec2 { x: u8, y: u8 }` produces flat allocations for `p.pos.x`, `p.pos.y`, `p.hp`, and `p.inv`, plus an intermediate `p.pos` Struct symbol so dotted-name lookups still resolve. Array fields get a single allocation with the array type so the existing `Expr::ArrayIndex` lowering path handles `p.inv[i]` without changes. Array-of-structs is still rejected with E0201 because the synthetic model can't index per-element layouts without further codegen work. The parser change is the only structural move: `parse_primary` and `parse_assign_or_call` now loop the dot chain into a single joined identifier so `p.pos.x` becomes `FieldAccess("p.pos", "x")` and `p.inv[0]` becomes `ArrayIndex("p.inv", 0)`. The downstream analyzer and IR lowering use the same `format!("{name}.{field}")` join they already used for one-level access — no plumbing changes required. Includes a new `examples/nested_structs.ne` that exercises both features end-to-end with two `Hero` instances carrying nested positions and inventory arrays. The reproducibility tripwire ROM is committed alongside it and the emulator harness has a matching pair of golden files. https://claude.ai/code/session_01KEczoNUX3WmcFLfq6iAQxB
2026-04-15 02:19:49 +00:00
// Demonstrates nested struct fields and array fields inside structs.
//
// Two `Hero` instances each carry a `Vec2` position (a nested struct)
// and a small inventory `u8[4]` (an array field). Both heroes scoot
// across the screen in opposite directions, and their inventory bytes
// are advanced once per frame so the analyzer's flat-allocation model
// is exercised end-to-end.
//
// Compile with the default IR codegen:
// nescript build examples/nested_structs.ne
game "NestedStructs" { mapper: NROM }
// Inner structs must be declared before any struct that nests them —
// the analyzer doesn't topologically sort declarations.
struct Vec2 {
x: u8,
y: u8,
}
// `pos` is a nested-struct field; `inv` is an array field. Both are
// fully addressable as `hero.pos.x` / `hero.inv[i]`. Layout in RAM
// is contiguous: pos.x, pos.y, hp, inv[0..3] — total 7 bytes per
// instance.
struct Hero {
pos: Vec2,
hp: u8,
inv: u8[4],
}
codereview: address six findings from a fresh review pass A focused review of the branch surfaced two correctness bugs and four important polish items in the new features. None of the existing example goldens shift — every fix is gated on conditions that don't fire in the committed examples. **debug.frame_overran() never reset between frames in implicit wait_frame programs.** The IR-level WaitFrame op cleared $07FE, but the implicit main-loop flag-clear that runs between dispatch iterations only cleared ZP_FRAME_FLAG. A program whose `on frame { ... }` body had no explicit `wait_frame` would latch $07FE to 1 on the first miss and never reset, breaking `debug.assert(not debug.frame_overran())` guards. The dispatch loop now also clears $07FE in debug builds, mirroring the WaitFrame path. New regression test asserts the main loop emits exactly one STA $07FE in a no-wait_frame debug build. **Metasprite base-tile resolution silently miscompiled for `@chr` / `@binary` sprites.** The IR lowering walks `program.sprites` to compute base tile indices but assumes 1 tile per non-Inline source, while the real asset resolver reads the file. The analyzer now hard-rejects the combination with a clear "use inline pixels" hint instead of letting it compile to a visual glitch. New analyzer test `analyze_metasprite_with_external_chr_sprite_errors` covers it. **next_sprite_tile capping silently allowed CHR overlap.** The pipeline used `.min(255)` which would let a background tile overwrite a sprite tile when the sprite range filled the pattern table. Now hard-errors via CompileError::AssetResolution when the sprite range >= 256 *and* the program declares any `@nametable(...)` background. Inline backgrounds aren't affected. **Linker silently truncated background CHR overflow.** The `if end <= chr.len()` guard at the CHR copy site dropped any auto-CHR bytes that would have run past the pattern table. Replaced with a debug assertion since the resolver should have caught it upstream — defense in depth. **Stale comment in nested_structs.ne** said struct literals "don't accept array fields yet" while the example itself demonstrates inline array fields working through `expand_struct_literal_init`. Comment updated. **Misleading sentinel comment in audio.rs** described the pitch envelope's trailing zero as a runtime sentinel; in practice the volume tick `JMP`s to `__audio_sfx_done` first and the pitch update block never reads the trailing byte. Rewrote the comment to clarify it's padding for predictable blob length. Also tidies up two minor items the reviewer flagged: - `flatten_struct_fields` rebuilt the `struct_sizes` HashMap on every leaf field; hoisted the snapshot to the function entry. - Integration tests called `resolve_backgrounds(..., 0)` (the new `next_sprite_tile` parameter); changed to `1` so a future PNG-nametable test fixture won't accidentally overwrite the runtime smiley at tile 0. https://claude.ai/code/session_01KEczoNUX3WmcFLfq6iAQxB
2026-04-15 03:56:42 +00:00
// Hero positions are initialized inline. Both the nested
// `Vec2 { ... }` and the inline `inv: [1, 2, 3, 4]` array
// initializers are unpacked into per-leaf-field IR globals by
// `expand_struct_literal_init` in src/ir/lowering.rs, so each
// leaf gets its own `LDA #imm; STA addr` pair at reset.
analyzer/lowering: support nested struct fields and array struct fields Struct field types beyond the v1 scalar set (`u8`, `i8`, `u16`, `bool`) used to error out with `E0201: struct fields must be u8/i8/u16/bool`. The size accumulator already handled them correctly — what was missing was: (1) the analyzer side that synthesizes per-leaf symbols and allocations for nested structs plus a single array-typed symbol for array fields, (2) the parser's chained-field-access path, and (3) the IR-lowering recursion through nested struct literal initializers and array literal field values. The synthetic-variable model carries through unchanged: a `var p: Player` where `Player { pos: Vec2, hp: u8, inv: u8[4] }` and `Vec2 { x: u8, y: u8 }` produces flat allocations for `p.pos.x`, `p.pos.y`, `p.hp`, and `p.inv`, plus an intermediate `p.pos` Struct symbol so dotted-name lookups still resolve. Array fields get a single allocation with the array type so the existing `Expr::ArrayIndex` lowering path handles `p.inv[i]` without changes. Array-of-structs is still rejected with E0201 because the synthetic model can't index per-element layouts without further codegen work. The parser change is the only structural move: `parse_primary` and `parse_assign_or_call` now loop the dot chain into a single joined identifier so `p.pos.x` becomes `FieldAccess("p.pos", "x")` and `p.inv[0]` becomes `ArrayIndex("p.inv", 0)`. The downstream analyzer and IR lowering use the same `format!("{name}.{field}")` join they already used for one-level access — no plumbing changes required. Includes a new `examples/nested_structs.ne` that exercises both features end-to-end with two `Hero` instances carrying nested positions and inventory arrays. The reproducibility tripwire ROM is committed alongside it and the emulator harness has a matching pair of golden files. https://claude.ai/code/session_01KEczoNUX3WmcFLfq6iAQxB
2026-04-15 02:19:49 +00:00
var hero1: Hero = Hero { pos: Vec2 { x: 32, y: 96 }, hp: 100, inv: [1, 2, 3, 4] }
var hero2: Hero = Hero { pos: Vec2 { x: 200, y: 128 }, hp: 100, inv: [10, 20, 30, 40] }
on frame {
// Walk both heroes — hero1 moves right, hero2 moves left, both
// wrap around at the screen edge so the demo runs forever.
hero1.pos.x += 1
hero2.pos.x -= 1
// Cycle the inventory bytes so the synthetic-array allocation
// path takes a real read/write pair every frame.
hero1.inv[0] += 1
hero2.inv[0] += 1
draw Smiley at: (hero1.pos.x, hero1.pos.y)
draw Smiley at: (hero2.pos.x, hero2.pos.y)
wait_frame
}
start Main