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 548787ac8a
W0110 inline fallback warning + docs refresh
W0110: when a function marked `inline` has a body shape the IR
lowerer can't splice (conditional early return, loops, nested
control flow, empty void body), the analyzer now emits a
warning at the declaration site so the declined hint is
visible instead of silently falling back to a regular JSR.

Implementation:
  - New `W0110` error code in `src/errors/diagnostic.rs` (warning level).
  - New `pub fn can_inline_fun(return_type, body) -> bool` in
    `src/ir/lowering.rs`, extracted from the existing capture
    logic so the analyzer and the IR lowerer share the same
    eligibility rules and can never drift.
  - New `check_inline_declinability` analyzer pass called from
    the tail of `analyze_program`, mirroring the existing
    `check_sprite_scanline_budget` / `check_unreachable_states`
    passes. Emits W0110 with help + note text pointing at the
    two accepted body shapes.
  - `capture_inline_bodies` now defers to `can_inline_fun`
    instead of duplicating the match pattern, so the two sides
    stay in lockstep by construction.

Four regression tests in `src/analyzer/tests.rs` cover the
conditional-return and while-loop declines plus the two
accepted shapes (single-return expression, void sequence).

Example source cleanups: `wrap52` in `examples/war/deck.ne`
and `abs_diff` in both `examples/arrays_and_functions.ne` and
`examples/loop_break_continue.ne` drop the `inline` keyword.
All three were dead hints — the `inline` was being silently
declined before this change, so removing it is source-only;
the three ROMs are byte-identical, all 32 emulator goldens
still match.

Docs refresh
  - `docs/language-guide.md`: rewrote the Inline Functions section
    (real behaviour + W0110), added W0105/W0106/W0107/W0108/W0109/
    W0110 to the warnings table, added the `debug.sprite_overflow*`
    builtins + sprite-per-scanline mitigations section to the
    Debug Mode docs, added a `cycle_sprites` statement entry and
    cross-referenced it from `draw`.
  - `docs/nes-reference.md`: fleshed out the "NEScript Memory
    Usage" block with the full ZP + high-RAM layout, including
    the new `$07EF` / `$07FC` / `$07FD` slots for sprite cycling
    and the debug sprite-overflow telemetry.
  - `docs/future-work.md`: documented all four debug query
    builtins in the "What ships today" block; updated the open
    "OAM allocation strategy" question to reference the shipped
    `cycle_sprites` path and ask about an automatic-flicker
    game attribute as a follow-up.
  - `docs/architecture.md`: updated the `ir/` and `optimizer/`
    module summaries to describe real inline splicing (now
    in lowering, not the optimizer).
  - `README.md`: reframed the `inline` bullet from "hint" to
    "real splicing for single-return / void-body shapes";
    expanded the debug-support bullet to mention the four
    query builtins and their stripping in release builds; added
    a new bullet for the three-layer sprite-per-scanline
    mitigations; bumped the test count from 497 → 694; updated
    the war.ne entry to mention the seven compiler bugs are all
    fixed and point readers at `git log` (instead of the
    deleted COMPILER_BUGS.md).
  - `examples/README.md`: same `git log`-pointing rewrite for
    the war.ne entry.

Deletions
  - `examples/war/COMPILER_BUGS.md` is removed. All seven
    catalogued bugs are fixed; the file's historical value
    lives in `git log` now. Every source-code comment and doc
    reference to the file has been updated to either point at
    `git log` or just describe the bug in place.

Test count: 616 unit + 75 integration + 3 doctests = 694 total.
Clippy / fmt clean. 32/32 emulator goldens match.

https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
2026-04-15 23:19:07 +00:00

7.2 KiB

Compiler Architecture

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


Pipeline

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

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

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, strength reduction, inlining
Codegen Optimized IR 6502 instruction list Slot allocation, instruction selection
Peephole 6502 instructions 6502 instructions Dead-load elimination, branch folding, INC/DEC fold
Linker Instructions + assets .nes file Bank layout, vectors, iNES header

Modules

Each module has a mod.rs (implementation) and a co-located tests.rs with unit tests.

lexer/

mod.rs, token.rs, tests.rs. Tokenizes NEScript source into a stream of typed tokens with source spans. Handles decimal/hex/binary integer literals, string literals, keywords, operators, and the raw-capture mode for asm { ... } bodies.

parser/

mod.rs, ast.rs, preprocess.rs, tests.rs. Recursive descent parser that converts the token stream into an AST. ast.rs defines every AST node type (expressions, statements, declarations). preprocess.rs inlines include "path.ne" directives before parsing.

analyzer/

mod.rs, tests.rs. Performs semantic analysis on the AST: type checking, scope and symbol table management, call graph construction with depth analysis, state reachability, unused-variable detection, and dead-code-after-terminator warnings. Emits all user-facing diagnostics beyond lexer/parser syntax errors.

ir/

mod.rs (types), lowering.rs (AST → IR), tests.rs. The IR is a flat, register-agnostic representation built from virtual temps and basic blocks. Lowering flattens nested expressions, expands 16-bit operations, desugars for into while, resolves constant expressions early, and performs real inline fun splicing — functions marked inline whose bodies match one of the two splicable shapes (single return <expr> or void statement sequence) are captured before lowering begins and substituted at every call site. Bodies that don't match (conditional returns, loops, nested control flow) fall back to regular out-of-line calls and the analyzer emits W0110 at the declaration.

optimizer/

mod.rs, tests.rs. Runs passes over the IR in order: strength reduction (mul/div by powers of two, mod by powers of two, ShiftVar → ShiftLeft/Right), constant folding (arithmetic + comparisons + shifts), copy propagation, and dead code elimination. The inline fun splicing happens one phase earlier in ir/lowering.rs so the optimizer sees already-inlined call sites.

codegen/

ir_codegen.rs, peephole.rs, mod.rs. ir_codegen.rs walks the optimized IR and emits 6502 instructions; variables land in allocated addresses and IR temps land in a recycling zero-page slot pool. peephole.rs runs after codegen to clean up the temp-heavy output (dead-load elimination, branch folding, INC/DEC fold, copy propagation).

asm/

mod.rs, opcodes.rs, inline_parser.rs, tests.rs. The built-in assembler and the inline-asm parser. opcodes.rs defines the 6502 opcode table with addressing modes. inline_parser.rs parses the body of asm { ... } blocks so codegen can splice real instructions in-line.

linker/

mod.rs, tests.rs. Assigns addresses to code and data segments, resolves label/symbol fixups, lays out banks for banked mappers (MMC1/UxROM/MMC3), and emits the final iNES byte stream via rom::RomBuilder.

rom/

mod.rs, tests.rs. Builds the final iNES ROM file. Generates the 16-byte iNES header and places the NMI/RESET/IRQ vector table.

runtime/

mod.rs, tests.rs. Contains built-in runtime code emitted into every ROM: NES hardware init, NMI handler, controller reads, OAM DMA, software multiply/divide, the frame-walking audio driver (gen_audio_tick, gen_period_table, gen_data_block), and the MMC1/UxROM/MMC3 mapper init and bank-switch helpers.

assets/

mod.rs, chr.rs, palette.rs, resolve.rs, audio.rs, tests.rs. The asset pipeline. chr.rs converts PNGs to CHR tile data. palette.rs maps RGB to NES palette indices. resolve.rs resolves sprite declarations into tile-indexed CHR blocks. audio.rs compiles sfx/music declarations into ROM-ready envelope and note-stream byte tables, plus the builtin effect/track tables used when programs reference audio names they haven't declared.

errors/

mod.rs, diagnostic.rs, render.rs. Defines the Diagnostic struct (error codes, severity, spans, labels, help text). Renders diagnostics with color and source context for terminal output using ariadne.


Testing

Test Organization

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

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/asm/tests.rs          -- assembler tests
src/linker/tests.rs       -- linker tests
src/rom/tests.rs          -- ROM builder tests
src/runtime/tests.rs      -- runtime code emission tests
src/assets/tests.rs       -- asset pipeline tests

End-to-end and error-code tests live in tests/integration_test.rs, which compiles representative .ne snippets through the full pipeline and asserts on ROM/diagnostic shape. The emulator smoke test (tests/emulator/run_examples.mjs) runs every example through jsnes and byte-compares the resulting screenshot and audio hash against goldens in tests/emulator/goldens/.

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 integration_test

# Run a specific test by name
cargo test test_name

Test Strategy

Each compiler phase is a pure function, so unit tests provide isolated input and verify output without side effects. Integration tests compile complete .ne programs and either assert on shape (length, presence of specific labels) or byte-compare output against checked-in goldens. Emulator goldens catch regressions that pass type-check but corrupt the final executable image.