1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 00:45:38 +00:00
nescript/docs/architecture.md
Claude d42540f45e
audio: complete the subsystem — asset pipeline, user decls, tracker-style driver
The audio subsystem was a sketch: `play name` / `start_music name` /
`stop_music` parsed, lowered, and emitted a few hardcoded register
writes from a builtin name table. No user-declared effects, no
per-frame envelope, no note streams, no real engine.

This flesh-out brings audio up to the quality bar of the rest of
the compiler (sprites, palettes, bank switching, scanline IRQ,
etc.) with a full data-driven pipeline:

## Asset pipeline (new `src/assets/audio.rs`)

- `sfx Name { duty, pitch, volume }` blocks compile into per-frame
  pulse-1 envelopes. Pitch/volume arrays must match in length; each
  entry is one NMI's worth of `$4000` data.
- `music Name { duty, volume, repeat, notes }` blocks compile into
  flat `(pitch, duration)` streams for pulse 2. Pitch 0 is a rest,
  1-60 indexes a builtin period table covering C1-B5.
- `resolve_sfx` / `resolve_music` walk the program for `play` /
  `start_music` references and append builtin fallbacks for any
  name that isn't user-declared — so `play coin` still works
  without a `sfx Coin { ... }` block.
- Builtin effects (coin, jump, hit, click, cancel, shoot, step)
  and tracks (theme, battle, victory, gameover) synthesize through
  the same compile path as user decls — one data model, one driver.

## Runtime engine (`src/runtime/mod.rs`)

- `gen_audio_tick()` walks both channels every NMI: reads one
  envelope byte through `(ZP_SFX_PTR),Y` -> writes `$4000`,
  advances ptr, mutes on zero sentinel. Music decrements the note
  counter, advances to the next `(pitch, dur)` pair on zero, looks
  up the period through `(__period_table),Y`, loops on `0xFF 0xFF`.
- `gen_period_table()` emits a 60-entry equal-tempered table
  (A4 = 440 Hz, NTSC 1.789773 MHz CPU clock) with length-counter
  load bits pre-baked into each high byte.
- `gen_data_block()` emits a label + raw-bytes pseudo pair so
  user sfx/music data can be spliced into PRG with regular labels
  that the two-pass assembler resolves.
- New ZP layout: `$05/$06` music loop base, `$07` music state
  (duty/volume/loop/active), `$0C-$0F` sfx and music pointers.

## IR codegen (`src/codegen/ir_codegen.rs`)

- `with_audio(sfx, music)` registers compile-time trigger constants
  per blob name.
- `gen_play_sfx` emits: write period to `$4002`/`$4003`, load
  envelope pointer into `ZP_SFX_PTR` via SymbolLo/SymbolHi of
  `__sfx_<name>`, mark the sfx counter active.
- `gen_start_music` stamps the header byte into `ZP_MUSIC_STATE`
  with the active bit OR'd in, seeds both ptr and loop base from
  `__music_<name>`, primes the duration counter.
- `gen_stop_music` mutes pulse 2 and clears state.

## Linker (`src/linker/mod.rs`)

- New `link_with_all_assets(user_code, sprites, sfx, music)` path
  that splices driver body, period table, and each sfx/music data
  blob into PRG — all guarded on the `__audio_used` marker so
  silent programs pay zero ROM cost.

## Assembler (`src/asm/opcodes.rs`, `src/asm/mod.rs`)

- New `AddressingMode::Bytes(Vec<u8>)` variant for raw-data
  pseudo-instructions. `NOP+Bytes(v)` emits the payload verbatim,
  letting the linker splice ROM data tables into a code section
  and still have `Label` / `SymbolLo` / `SymbolHi` fixups resolve
  correctly in the same assembly pass.

## Analyzer

- `play` / `start_music` now validate the name against user decls
  and builtin tables. Unknown names emit E0505 with a helpful list
  of builtins — previously a typo would silently compile to no-op.

## Parser

- New `sfx_decl` / `music_decl` grammar with property-style
  configuration. Strict validation: duty 0-3, volume 0-15, pitch
  arrays must match volume length, music notes must come in pairs,
  pitch 0-60, duration ≥ 1.

## Tests

+170 new tests across every layer:
- `src/assets/audio.rs`: 17 tests (compile, resolve, builtins,
  shadowing, label sanitation, nested reference walks)
- `src/parser/tests.rs`: 13 tests (valid/invalid sfx + music
  declarations, property validation, play/start_music/stop_music)
- `src/analyzer/tests.rs`: 7 tests (builtin acceptance, user decl
  acceptance, unknown-name rejection)
- `src/runtime/tests.rs`: 10 tests (audio tick labels, RTS end,
  $4000 write, $4004 mute, period table assembly, A4 = 440 Hz,
  length counter bits, data block verbatim emit)
- `src/linker/tests.rs`: 4 tests (sfx/music blob placement,
  pointer resolution, elision when unused)
- `src/codegen/ir_codegen.rs`: rewrote the 4 existing audio tests
  to match the new data-driven contract
- `tests/integration_test.rs`: 4 end-to-end tests including a
  user-declared `sfx` + `music` program that verifies bytes land
  in PRG ROM at the right addresses

## Docs

- New Audio section in `docs/language-guide.md` with syntax
  reference, builtin tables, and an explanation of how the
  driver works at compile and run time.
- `docs/architecture.md` updated to reflect the real audio
  pipeline instead of the old "audio import stubs" stub.
- `docs/future-work.md` moves audio from "status: minimal" to
  "status: full subsystem" with a narrower list of follow-up work
  (triangle/noise/DMC channels, NSF/FTM imports, richer envelopes).
- `examples/audio_demo.ne` rewritten to showcase user-declared
  `sfx LongCoin`, `sfx Zap`, `music Theme`, still demonstrating
  builtin fallback via `play coin`.

Total: 424 tests passing (381 unit + 43 integration), clippy clean,
fmt clean, all 19 examples compile.

https://claude.ai/code/session_015WfaDttE3DpWn9rpyfpQd8
2026-04-13 01:10:21 +00:00

6.4 KiB

Compiler Architecture

An overview of the NEScript compiler internals for contributors and maintainers.


Pipeline

Source (.ne) --> Lexer --> Parser --> Analyzer --> IR Lowering --> Optimizer --> Codegen --> Assembler --> Linker --> ROM (.nes)

Each phase is a pure function (input to output) with no global state, making every stage independently testable.

.ne source ---------> Lexer -----> Parser -----> Analyzer ------>
                     (tokens)     (AST)       (annotated AST)

IR Lowering -----> Optimizer -----> Codegen -----> Linker -----> ROM
   (IR)          (optimized IR)   (6502 insns)               (.nes file)

Phase Summary

Phase Input Output Responsibility
Lexer Source text Token stream Tokenization, number/string literal parsing
Parser Token stream AST Syntax validation, tree construction
Analyzer AST Annotated AST Type checking, scope resolution, call graph analysis
IR Lowering Annotated AST NEScript IR Flatten expressions, expand u16 ops, desugar
Optimizer IR Optimized IR Constant folding, dead code, ZP promotion, inlining
Codegen Optimized IR 6502 instruction list Register allocation, instruction selection
Assembler 6502 instructions Byte sequences + fixups Opcode encoding, address resolution
Linker Bytes + assets .nes file Bank layout, vectors, iNES header

Modules

lexer/

Tokenizes NEScript source text into a stream of typed tokens with source spans. Handles decimal, hex, and binary integer literals, string literals, all keywords, and operators.

parser/

Recursive descent parser that converts the token stream into an AST. Defines all AST node types (expressions, statements, declarations) in ast.rs.

analyzer/

Performs semantic analysis on the AST: type checking (types.rs), scope and symbol table management (scope.rs), and call graph construction with depth analysis (call_graph.rs). Detects recursion, type mismatches, undefined references, and call depth violations.

ir/

Defines the intermediate representation and the lowering pass (lowering.rs) that translates the annotated AST into IR. Flattens nested expressions, expands 16-bit operations into 8-bit sequences, and resolves syntactic sugar.

optimizer/

Runs optimization passes over the IR: constant folding (const_fold.rs), dead code elimination (dead_code.rs), zero-page promotion analysis (zp_promote.rs), and function inlining (inliner.rs).

codegen/

Translates optimized IR into 6502 instructions. Includes register allocation for the A/X/Y registers (regalloc.rs) and instruction pattern matching for idiomatic 6502 code (patterns.rs).

asm/

The built-in assembler. Encodes 6502 instructions (encode.rs) with all addressing modes (addressing.rs), using a complete opcode table (opcodes.rs -- 56 instructions across all modes).

linker/

Assigns addresses to code and data segments, resolves fixups/relocations (fixups.rs), and handles bank allocation (banks.rs) for banked mappers.

rom/

Builds the final iNES ROM file. Generates the 16-byte iNES header (header.rs) and places the NMI/RESET/IRQ vector table (vectors.rs).

runtime/

Contains built-in runtime code that the compiler emits into every ROM: NES hardware initialization, NMI handler generation, controller read routines, OAM DMA setup, software multiply/divide, and the frame-walking audio driver (gen_audio_tick, gen_period_table, gen_data_block).

assets/

The asset pipeline. Converts PNG images to CHR tile data (chr.rs), maps RGB colors to the NES palette (palette.rs), resolves sprite/background declarations into tile-indexed CHR blocks (resolve.rs), and compiles sfx/music declarations into ROM-ready envelope and note-stream byte tables (audio.rs) — plus the builtin effect/track tables used as fallbacks when programs reference audio names without declaring them.

debug/

Debug instrumentation output. Generates source maps relating ROM addresses to source locations (source_map.rs), symbol tables compatible with Mesen (symbols.rs), and runtime check code for debug builds (checks.rs).

errors/

Error reporting infrastructure. Defines the Diagnostic struct with error codes, severity levels, source spans, labels, help text, and notes (diagnostic.rs). Renders diagnostics with color and source context for terminal output (render.rs).


Testing

Test Organization

Tests are co-located with each module in tests.rs files:

src/lexer/tests.rs        -- lexer unit tests
src/parser/tests.rs       -- parser unit tests
src/analyzer/tests.rs     -- semantic analysis tests
src/ir/tests.rs           -- IR lowering tests
src/optimizer/tests.rs    -- optimizer tests
src/codegen/tests.rs      -- code generation tests
src/asm/tests.rs          -- assembler tests
src/linker/tests.rs       -- linker tests
src/rom/tests.rs          -- ROM builder tests
src/assets/tests.rs       -- asset pipeline tests

Integration tests live in the tests/ directory:

tests/integration/        -- full pipeline tests with .ne source files
tests/error_tests/        -- tests that verify specific error codes
tests/asm_tests/          -- 6502 opcode and addressing mode tests

Running Tests

# Run all tests
cargo test

# Run tests for a specific module
cargo test --lib lexer
cargo test --lib parser
cargo test --lib analyzer

# Run integration tests only
cargo test --test '*'

# Run a specific test by name
cargo test test_name

Test Strategy

Each compiler phase is designed as a pure function, so unit tests provide isolated input and verify output without side effects. Integration tests compile complete .ne source files and verify the output ROM matches expected golden files in tests/integration/expected/.

Error tests in tests/error_tests/ contain intentionally broken programs and verify that the correct error code is produced (e.g., recursion.ne should produce E0402).