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

compiler: i16 / SRAM saves / inline-asm dot labels / docs

Another batch from the cc65/nesdoug catalogue. All gated on
parser-level opt-in or default-false attributes so existing
programs produce byte-identical ROMs (no committed .nes file
changed).

**§A — `i16` signed 16-bit type:**
- New `KwI16` lexer token, `NesType::I16` AST variant, parser
  case in `parse_type`. Type-size and integer-type tables
  treat `i16` like `u16` (2 bytes, integer).
- IR lowering accepts `i16` everywhere it accepts `u16` for
  wide-load / wide-store / widen-narrow paths.
- New constant fold for `UnaryOp::Negate(IntLiteral(v))` that
  emits the wide two's-complement form. Without it, `var vy:
  i16 = -10` would zero-extend to `$00F6` (= 246) instead of
  sign-extending to `$FFF6` (= -10). Negative literals now
  store the right bytes.
- Comparisons reuse the existing unsigned 16-bit compare ops
  (matching the existing `i8` behaviour). Documented in the
  `NesType::I16` doc comment and in `future-work.md` §A.
- Example `examples/i16_demo.ne` with committed golden.
- Tests cover the literal-fold sign-extension and end-to-end
  compile of the example.

**§S — SRAM / battery-backed saves:**
- New `save { var ... }` top-level block. Lexer + parser opt
  into a dedicated `KwSave` token. Analyzer allocates save
  vars from a separate `next_sram_addr` bump pointer starting
  at `$6000`, capped at `$8000` (8 KB cartridge SRAM window).
- Linker reads `analysis.has_battery_saves` and flips iNES
  byte-6 bit-1 via the new `RomBuilder::set_battery` /
  `Linker::with_battery` chain.
- New `W0111` warning for save-var initializers — SRAM is
  preserved across power cycles, so an init expression would
  either silently never run or clobber persisted data on
  every boot. The warning teaches the user about the
  magic-byte sentinel pattern.
- Struct fields in save blocks are explicitly rejected for now
  (the field-flattening path uses the main-RAM allocator).
- Example `examples/sram_demo.ne` with committed golden, plus
  4 integration tests.

**§D (partial) — inline-asm `.label:` syntax:**
- Codegen-side mangler rewrites `.IDENT` → `__ilab_<N>_IDENT`
  per inline-asm block, where `<N>` is the call site's
  monotonic suffix. Two `asm { .loop: ... }` blocks in the
  same function now coexist without colliding in the linker's
  label table.
- Bounds checks on `.` placement: `$2002` and `name.field`
  are unaffected; only `.IDENT` in label / branch context
  triggers the rewrite. Two integration tests pin the
  uniqueness and dollar-vs-dot disambiguation.

**§X follow-up — Mesen trace-log docs:**
- New "Debugger-assisted workflows" section in
  `docs/nes-reference.md` walking through the Mesen / FCEUX
  log workflows alongside the new `debug_port:` attribute.

**Misc:**
- `future-work.md` updated to mark the shipped items out of
  the catalogue and reshuffle the priority ranking. Remaining
  niche follow-ups (signedness on Cmp16, struct save fields,
  inline-asm format specifiers) documented inline so future
  passes know the design.

All 757 tests pass. Clippy clean. 46/46 emulator goldens match.
This commit is contained in:
Claude 2026-04-18 20:49:06 +00:00
parent e0b268eea9
commit 7b4570eee5
No known key found for this signature in database
28 changed files with 841 additions and 58 deletions

View file

@ -50,6 +50,8 @@ Open any `.nes` file in an NES emulator ([Mesen](https://www.mesen.ca/), [FCEUX]
| `auto_sprite_flicker.ne` | `game { sprite_flicker: true }` | The `game` attribute equivalent of calling `cycle_sprites` at the top of every `on frame` handler. Same 12-sprite layout as `sprite_flicker_demo.ne`, minus the explicit call — the IR lowerer injects the op automatically when the flag is set, so it's byte-identical to a hand-rolled version without the per-site boilerplate. |
| `fade_demo.ne` | `fade_out(n)`, `fade_in(n)` | Blocking fade helpers that walk brightness 4 → 0 and 0 → 4 with `n` frames per step. The runtime splices `__fade_out` / `__fade_in` plus a callable `__wait_frame_rt` helper when the builtin is used; fade use also forces `__set_palette_brightness` to be linked in since the fade body JSRs into it. |
| `sprite_0_split_demo.ne` | `sprite_0_split(x, y)` | Mid-frame scroll change driven by the PPU's sprite-0 hit flag (`$2002` bit 6), so the effect works on any mapper — NROM, UxROM, MMC1 — not just MMC3 via `on_scanline(N)`. Two-phase busy-wait (wait for clear, then wait for set) guarantees the hit we're responding to came from the current frame. Requires a sprite in OAM slot 0 that overlaps opaque background pixels; this demo uses a full smiley background so every frame's sprite-0 hit fires deterministically. |
| `i16_demo.ne` | `i16` signed 16-bit type | Negative literals fold to wide two's complement (`-10``$FFF6`), so `var vy: i16 = -10` stores the right bytes instead of the zero-extended `$00F6`. Comparisons currently use the unsigned 16-bit compare path (matching existing `i8` behaviour) — fine for positive ranges, wrong for negative compares. The companion `i16_negative_literal_sign_extends_to_wide_store` integration test guards the literal-fold path. |
| `sram_demo.ne` | `save { var ... }` | Battery-backed save block. The analyzer allocates `high_score` and `coins` at `$6000+` (cartridge SRAM window) instead of main RAM, and the linker flips iNES header byte-6 bit-1 so emulators (FCEUX, Mesen, Nestopia) load and persist the region from a `.sav` file alongside the ROM. SRAM is uninitialized at first power-on; production games should reserve a magic-byte sentinel and validate it before trusting the rest of the data — the compiler doesn't auto-initialize and emits W0111 if you try. |
## Emulator Controls

48
examples/i16_demo.ne Normal file
View file

@ -0,0 +1,48 @@
// i16 demo — exercises the signed 16-bit type with negative
// literals, signed velocity, and round-tripping through wide
// arithmetic.
//
// Position is stored as `i16` so negative deltas (the ball moving
// left or up) can be represented directly instead of having to
// shadow them with a sign byte. The lowering folds `-N` literals
// into wide two's-complement constants, so `vy = -10` lands as
// `$FFF6` (=-10) rather than the zero-extended `$00F6` (=246).
game "i16 Demo" {
mapper: NROM
}
var px: i16 = 64
var py: i16 = 80
var vx: i16 = 1
var vy: i16 = -1
var frame: u8 = 0
on frame {
frame += 1
// Bounce off the visible-area edges. Comparisons against the
// small positive bounds use the unsigned 16-bit compare path
// (matching the existing i8 behaviour) — for purely positive
// ranges this matches signed semantics.
px += vx
py += vy
// Reverse vx every 100 frames so we exercise both the
// positive-velocity and negative-velocity arithmetic paths.
if frame == 100 {
frame = 0
vx = vx + vx // double-then-negate would be cleaner once
vx = vx - vx // we add unary `-` on a runtime expression
vx = 1
vy = -1
}
// Clamp position into u8 range for `draw`. The cast truncates
// the high byte; for our 0..255 motion that's the right thing.
var dx: u8 = px as u8
var dy: u8 = py as u8
draw Ball at: (dx, dy)
}
start Main

BIN
examples/i16_demo.nes Normal file

Binary file not shown.

48
examples/sram_demo.ne Normal file
View file

@ -0,0 +1,48 @@
// SRAM demo — a `save { var ... }` block declares battery-backed
// state in the iNES `$6000-$7FFF` SRAM window. The compiler:
// * allocates `high_score`, `coins`, and `initials` at $6000+
// instead of main RAM,
// * sets the iNES header byte-6 bit-1 (battery flag), and
// * emits absolute-mode loads/stores that target SRAM directly.
//
// Real cartridge boards persist this region to a `.sav` file; in
// most emulators (FCEUX, Mesen, Nestopia) the file lives next to
// the ROM. SRAM is uninitialized at first power-on, so production
// games should reserve a magic-byte sentinel and validate it
// before trusting the rest of the data — this demo skips the
// validation step for brevity.
game "SRAM Demo" {
mapper: NROM
}
// Save vars don't take initializers — SRAM is preserved across
// power cycles, so an init expression would either silently
// never run or clobber the player's saved data on every boot.
// Production games guard their save data with a magic-byte
// sentinel: check a known signature on boot and only seed
// defaults if it's missing.
save {
var high_score: u16
var coins: u8
}
var px: u8 = 64
on frame {
// Bump the in-SRAM coin counter every frame; when it wraps
// past 256 we treat that as "scored a power-up" and bump
// the high score.
coins += 1
if coins == 0 {
high_score += 1
}
// Move a sprite so the demo has visible output for the
// emulator golden — frame 180 captures the sprite at
// x = 64 + 180 (mod 256) = 244.
px += 1
draw Ball at: (px, 100)
}
start Main

BIN
examples/sram_demo.nes Normal file

Binary file not shown.