mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 00:45:38 +00:00
Remove the legacy AST codegen — IR path is canonical now
The `--use-ast` path through `src/codegen/mod.rs` was a strictly
inferior subset of the IR codegen. Building every example with
`--use-ast` through the jsnes harness:
- `arrays_and_functions` — fully black (array init + function
return values + OAM-in-loop all broken)
- `structs_enums_for` — fully black (struct literal is a no-op,
all fields stay at 0)
- `inline_asm_demo` — fully black
- `bitwise_ops`, `loop_break_continue` — below sprite floors
(static `next_oam_slot` bug B)
- `match_demo` — panics at compile time with
`branch offset 153 out of range` (AST's if/else-chain
desugaring of `match` emits short branches that can't reach
the far arms in a multi-arm match)
Six of fourteen examples are non-functional under `--use-ast`.
The other eight happen to fall inside the subset AST handles
(no arrays, no structs, no function return values, no
multi-sprite loops, no long match chains).
`docs/future-work.md` already listed "Once working, delete the
AST-based codegen entirely" as the intended direction. It's
working, so this commit does the deletion.
What's removed:
- The `CodeGen` struct, its impl block, and every helper in
`src/codegen/mod.rs` (the AST codegen body) — ~1150 lines.
The file is now a module header that re-exports `IrCodeGen`.
- `src/codegen/tests.rs` — 15 AST-specific instruction-pattern
tests. Every feature they covered has an equivalent test in
`src/codegen/ir_codegen.rs::{tests,more_tests}` already.
- The `--use-ast` CLI flag and its branch in `src/main.rs`.
- `compile_with_ir_codegen` in `tests/integration_test.rs` —
`compile()` now does what it did, so they merged. All 40
integration tests go through the IR path.
- Outdated sections in `docs/future-work.md` that described the
IR codegen as "not yet implemented" and listed AST codegen
gaps as priority work.
What's kept:
- `src/codegen/ir_codegen.rs` — the real codegen.
- `src/codegen/peephole.rs` — post-codegen cleanup pass, now
run unconditionally from `main.rs`.
Test plan:
- `cargo test --release` — 313 unit + 37 integration tests pass
(was 328 + 37; the 15 dropped are the deleted AST-specific
tests).
- `cargo fmt --check` clean.
- `cargo clippy --release --all-targets -- -D warnings` clean.
- `node tests/emulator/run_examples.mjs` — 14/14 ROMs render
above their per-example nonBlack floors.
- The one tightening: `sprite_resolution_uses_tile_index` was
asserting on the old static-slot encoding
(`A9 01 8D 01 02`). Updated to the cursor-based form
(`A9 01 99 01 02`, i.e. STA AbsoluteY).
Net diff: 1581 deletions, 62 insertions.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
This commit is contained in:
parent
54acb9ee38
commit
a757336681
6 changed files with 61 additions and 1580 deletions
|
|
@ -7,64 +7,9 @@ in the NEScript compiler. Items are organized by priority and area.
|
||||||
|
|
||||||
## 1. IR-Based Code Generation
|
## 1. IR-Based Code Generation
|
||||||
|
|
||||||
**Status**: The IR pipeline (lowering + optimization) runs during compilation but
|
**Status**: Complete. The AST → IR lowering, optimizer, and
|
||||||
its output is discarded. Code generation still works from the AST directly
|
`src/codegen/ir_codegen.rs` all work end-to-end; the legacy AST
|
||||||
(`src/codegen/mod.rs`). This means IR-level optimizations have no effect on the
|
codegen has been removed. See "Recently completed" below.
|
||||||
final ROM.
|
|
||||||
|
|
||||||
**What exists**:
|
|
||||||
- `src/ir/mod.rs`: Complete IR type definitions (`IrProgram`, `IrFunction`,
|
|
||||||
`IrBasicBlock`, `IrOp`, `IrTerminator`)
|
|
||||||
- `src/ir/lowering.rs`: AST → IR translation for all statement and expression types
|
|
||||||
- `src/optimizer/mod.rs`: Constant folding, dead code elimination, strength
|
|
||||||
reduction, ZP promotion analysis, function inlining — all operating on IR
|
|
||||||
|
|
||||||
**What's needed**:
|
|
||||||
- A new `src/codegen/ir_codegen.rs` that walks `IrProgram` and emits 6502
|
|
||||||
`Instruction` sequences from `IrOp`/`IrTerminator` instead of from AST nodes
|
|
||||||
- Register allocation strategy for IR temps → A/X/Y/zero-page spill slots
|
|
||||||
- Replace the `CodeGen::generate(&program)` call in `main.rs` with
|
|
||||||
`ir_codegen::generate(&ir_program, &analysis)`
|
|
||||||
- Once working, delete the AST-based codegen entirely
|
|
||||||
|
|
||||||
**IR lowering issues to fix first** (found during code review):
|
|
||||||
- `ButtonRead` emits `ReadInput` with no destination temp, then uses uninitialized
|
|
||||||
temp in the `And` mask operation (`src/ir/lowering.rs:534`). The fix: `ReadInput`
|
|
||||||
should store the input byte into a temp, or the lowering should emit
|
|
||||||
`LoadVar(t, input_var_id)` after `ReadInput`.
|
|
||||||
- Logical AND/OR use raw `VarId(self.next_var_id)` for temp storage without
|
|
||||||
registering it (`src/ir/lowering.rs:603,637`). Should use `IrTemp` instead.
|
|
||||||
- Break/continue create unreachable blocks that contain subsequent dead statements
|
|
||||||
(`src/ir/lowering.rs:259`). Should either skip lowering after a terminator, or
|
|
||||||
the dead code elimination pass should handle it.
|
|
||||||
|
|
||||||
**Impact**: Enables all optimizer passes to actually affect output quality.
|
|
||||||
Currently the optimizer is validated by tests but its results are thrown away.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Codegen Gaps (AST-Based)
|
|
||||||
|
|
||||||
These features are parsed and analyzed but produce no 6502 output:
|
|
||||||
|
|
||||||
| Feature | Location | Status |
|
|
||||||
|---------|----------|--------|
|
|
||||||
| Function calls | `codegen:148` | `Statement::Call` is a no-op |
|
|
||||||
| Return values | `codegen:148` | `Statement::Return` is a no-op |
|
|
||||||
| State transitions | `codegen:148` | `Statement::Transition` is a no-op |
|
|
||||||
| Array indexing | `codegen:199-200` | `LValue::ArrayIndex` assignment is a no-op |
|
|
||||||
| Array expressions | `codegen:417` | `Expr::ArrayIndex`, `Expr::ArrayLiteral` are no-ops |
|
|
||||||
| Function call expressions | `codegen:417` | `Expr::Call` returns nothing |
|
|
||||||
| Scroll | `codegen:151-152` | `Statement::Scroll` is a no-op |
|
|
||||||
| Load background | `codegen:154-155` | `Statement::LoadBackground` is a no-op |
|
|
||||||
| Set palette | `codegen:154-155` | `Statement::SetPalette` is a no-op |
|
|
||||||
| Multiply/divide/modulo | `codegen:450-452` | `BinOp::Mul/Div/Mod` only emit left operand |
|
|
||||||
| Dynamic shifts | `ir/lowering:575` | Shift amount is hardcoded to 1 |
|
|
||||||
|
|
||||||
**Priority fixes for a working multi-state game**:
|
|
||||||
1. **Function calls**: JSR to function label, pass args via zero-page, return via A
|
|
||||||
2. **State transitions**: Write state ID to a zero-page variable, jump to dispatcher
|
|
||||||
3. **Array indexing**: Use X register for index, LDA absolute,X for loads
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -373,8 +318,10 @@ These items were documented as future work but have since been implemented:
|
||||||
- **IR-based codegen** — `src/codegen/ir_codegen.rs` walks `IrProgram` and
|
- **IR-based codegen** — `src/codegen/ir_codegen.rs` walks `IrProgram` and
|
||||||
emits 6502 for every IR op: load/store, arithmetic, comparisons, arrays,
|
emits 6502 for every IR op: load/store, arithmetic, comparisons, arrays,
|
||||||
calls, draws, input (P1 and P2), scroll, debug.log/assert, state
|
calls, draws, input (P1 and P2), scroll, debug.log/assert, state
|
||||||
dispatch, multi-OAM slot allocation, transitions + on_enter handlers.
|
dispatch, runtime OAM cursor for looped draws, transitions + on_enter
|
||||||
Now the default; `--use-ast` falls back to the legacy AST-based codegen.
|
handlers. It's the only codegen — the legacy AST-based path and the
|
||||||
|
`--use-ast` flag were removed once the IR pipeline was proven correct
|
||||||
|
by the jsnes emulator smoke test.
|
||||||
- **IR lowering bug fixes** — `ReadInput` now has a destination temp,
|
- **IR lowering bug fixes** — `ReadInput` now has a destination temp,
|
||||||
`ButtonRead` uses the proper input temp, logical AND/OR use a new
|
`ButtonRead` uses the proper input temp, logical AND/OR use a new
|
||||||
`emit_move` helper instead of the buggy raw VarId temp storage
|
`emit_move` helper instead of the buggy raw VarId temp storage
|
||||||
|
|
@ -490,23 +437,18 @@ These items were documented as future work but have since been implemented:
|
||||||
|
|
||||||
For someone picking up this codebase, the recommended order of work:
|
For someone picking up this codebase, the recommended order of work:
|
||||||
|
|
||||||
1. **Delete AST codegen** — IR codegen is now the default and beats
|
1. **u16 / array / nested struct fields** — current structs only
|
||||||
the AST codegen in 5/7 examples. Once confidence is high, remove
|
|
||||||
`--use-ast` and `src/codegen/mod.rs`'s AST-specific code.
|
|
||||||
2. **Struct literals** — `pos = Vec2 { x: 100, y: 50 }` as an
|
|
||||||
expression. Currently fields must be assigned individually.
|
|
||||||
3. **u16 / array / nested struct fields** — current structs only
|
|
||||||
allow u8/i8/bool fields. The layout machinery is ready for more
|
allow u8/i8/bool fields. The layout machinery is ready for more
|
||||||
types but the codegen only handles 1-byte loads/stores.
|
types but the codegen only handles 1-byte loads/stores.
|
||||||
4. **Audio driver** — the `play`/`start_music`/`stop_music`
|
2. **Audio driver** — the `play`/`start_music`/`stop_music`
|
||||||
statements parse but don't generate any code. A minimal NSF-style
|
statements parse but don't generate any code. A minimal NSF-style
|
||||||
driver running in NMI would unblock game projects.
|
driver running in NMI would unblock game projects.
|
||||||
5. **Multi-scanline on_scanline reload** — the current codegen
|
3. **Multi-scanline on_scanline reload** — the current codegen
|
||||||
supports one scanline per state but not a chain of handlers at
|
supports one scanline per state but not a chain of handlers at
|
||||||
different scanlines in the same frame.
|
different scanlines in the same frame.
|
||||||
6. **Register allocator** — proper A/X/Y allocation to replace
|
4. **Register allocator** — proper A/X/Y allocation to replace
|
||||||
zero-page spills used by the current IR codegen. Partially
|
zero-page spills used by the current IR codegen. Partially
|
||||||
mitigated by peephole passes but still wasteful in some cases.
|
mitigated by peephole passes but still wasteful in some cases.
|
||||||
7. **Match statement** — `match x { 0 => ... , 1 => ... }` would be
|
5. **Match statement** — `match x { 0 => ... , 1 => ... }` would be
|
||||||
useful for state dispatch and enum-driven logic.
|
useful for state dispatch and enum-driven logic.
|
||||||
8. **Text / HUD layer** — font sheet + layout system for scores.
|
8. **Text / HUD layer** — font sheet + layout system for scores.
|
||||||
|
|
|
||||||
|
|
@ -945,7 +945,6 @@ nescript build game.ne --dump-ir
|
||||||
| `--dump-ir` | Dump the lowered IR program (after optimization) to stdout |
|
| `--dump-ir` | Dump the lowered IR program (after optimization) to stdout |
|
||||||
| `--memory-map` | Dump a memory map of variable allocations to stdout |
|
| `--memory-map` | Dump a memory map of variable allocations to stdout |
|
||||||
| `--call-graph` | Dump a call graph (which handler/function calls which) to stdout |
|
| `--call-graph` | Dump a call graph (which handler/function calls which) to stdout |
|
||||||
| `--use-ast` | Use the legacy AST-based codegen (default is the IR codegen) |
|
|
||||||
|
|
||||||
### Check
|
### Check
|
||||||
|
|
||||||
|
|
|
||||||
1169
src/codegen/mod.rs
1169
src/codegen/mod.rs
File diff suppressed because it is too large
Load diff
|
|
@ -1,278 +0,0 @@
|
||||||
use super::*;
|
|
||||||
use crate::analyzer;
|
|
||||||
use crate::asm::{AddressingMode as AM, Opcode::*};
|
|
||||||
use crate::parser;
|
|
||||||
|
|
||||||
fn compile_to_instructions(src: &str) -> Vec<Instruction> {
|
|
||||||
let (prog, diags) = parser::parse(src);
|
|
||||||
assert!(diags.is_empty(), "parse errors: {diags:?}");
|
|
||||||
let prog = prog.unwrap();
|
|
||||||
let analysis = analyzer::analyze(&prog);
|
|
||||||
assert!(
|
|
||||||
analysis.diagnostics.iter().all(|d| !d.is_error()),
|
|
||||||
"analysis errors: {:?}",
|
|
||||||
analysis.diagnostics
|
|
||||||
);
|
|
||||||
|
|
||||||
let codegen = CodeGen::new(&analysis.var_allocations, &prog.constants);
|
|
||||||
codegen.generate(&prog)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_instruction(instructions: &[Instruction], opcode: crate::asm::Opcode, mode: &AM) -> bool {
|
|
||||||
instructions
|
|
||||||
.iter()
|
|
||||||
.any(|i| i.opcode == opcode && i.mode == *mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn codegen_var_init() {
|
|
||||||
let src = r#"
|
|
||||||
game "Test" { mapper: NROM }
|
|
||||||
var px: u8 = 128
|
|
||||||
on frame { wait_frame }
|
|
||||||
start Main
|
|
||||||
"#;
|
|
||||||
let insts = compile_to_instructions(src);
|
|
||||||
// Should have LDA #128 and STA to zero page
|
|
||||||
assert!(has_instruction(&insts, LDA, &AM::Immediate(128)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn codegen_plus_assign() {
|
|
||||||
let src = r#"
|
|
||||||
game "Test" { mapper: NROM }
|
|
||||||
var px: u8 = 0
|
|
||||||
on frame { px += 2 }
|
|
||||||
start Main
|
|
||||||
"#;
|
|
||||||
let insts = compile_to_instructions(src);
|
|
||||||
// Should have CLC, ADC #2
|
|
||||||
assert!(has_instruction(&insts, CLC, &AM::Implied));
|
|
||||||
assert!(has_instruction(&insts, ADC, &AM::Immediate(2)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn codegen_minus_assign() {
|
|
||||||
let src = r#"
|
|
||||||
game "Test" { mapper: NROM }
|
|
||||||
var px: u8 = 100
|
|
||||||
on frame { px -= 1 }
|
|
||||||
start Main
|
|
||||||
"#;
|
|
||||||
let insts = compile_to_instructions(src);
|
|
||||||
assert!(has_instruction(&insts, SEC, &AM::Implied));
|
|
||||||
assert!(has_instruction(&insts, SBC, &AM::Immediate(1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn codegen_button_check() {
|
|
||||||
let src = r#"
|
|
||||||
game "Test" { mapper: NROM }
|
|
||||||
var px: u8 = 0
|
|
||||||
on frame {
|
|
||||||
if button.right { px += 1 }
|
|
||||||
}
|
|
||||||
start Main
|
|
||||||
"#;
|
|
||||||
let insts = compile_to_instructions(src);
|
|
||||||
// Should read controller input and AND with right button mask (0x01)
|
|
||||||
assert!(has_instruction(&insts, AND, &AM::Immediate(0x01)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn codegen_draw_sprite() {
|
|
||||||
let src = r#"
|
|
||||||
game "Test" { mapper: NROM }
|
|
||||||
var px: u8 = 64
|
|
||||||
var py: u8 = 64
|
|
||||||
on frame {
|
|
||||||
draw Smiley at: (px, py)
|
|
||||||
}
|
|
||||||
start Main
|
|
||||||
"#;
|
|
||||||
let insts = compile_to_instructions(src);
|
|
||||||
// Should write to OAM buffer at $0200-$0203
|
|
||||||
assert!(has_instruction(&insts, STA, &AM::Absolute(0x0200))); // Y
|
|
||||||
assert!(has_instruction(&insts, STA, &AM::Absolute(0x0201))); // tile
|
|
||||||
assert!(has_instruction(&insts, STA, &AM::Absolute(0x0202))); // attr
|
|
||||||
assert!(has_instruction(&insts, STA, &AM::Absolute(0x0203))); // X
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn codegen_const_usage() {
|
|
||||||
let src = r#"
|
|
||||||
game "Test" { mapper: NROM }
|
|
||||||
const SPEED: u8 = 2
|
|
||||||
var px: u8 = 0
|
|
||||||
on frame { px += SPEED }
|
|
||||||
start Main
|
|
||||||
"#;
|
|
||||||
let insts = compile_to_instructions(src);
|
|
||||||
// Constant should be inlined as immediate
|
|
||||||
assert!(has_instruction(&insts, ADC, &AM::Immediate(2)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn codegen_main_loop_structure() {
|
|
||||||
let src = r#"
|
|
||||||
game "Test" { mapper: NROM }
|
|
||||||
on frame { wait_frame }
|
|
||||||
start Main
|
|
||||||
"#;
|
|
||||||
let insts = compile_to_instructions(src);
|
|
||||||
// Should have JMP back to loop start
|
|
||||||
let has_jmp = insts.iter().any(|i| {
|
|
||||||
i.opcode == JMP && matches!(&i.mode, AM::Label(l) if l.starts_with("__main_loop"))
|
|
||||||
});
|
|
||||||
assert!(has_jmp, "should have JMP to main loop");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn codegen_comparison() {
|
|
||||||
let src = r#"
|
|
||||||
game "Test" { mapper: NROM }
|
|
||||||
var x: u8 = 0
|
|
||||||
on frame {
|
|
||||||
if x == 10 { x = 0 }
|
|
||||||
}
|
|
||||||
start Main
|
|
||||||
"#;
|
|
||||||
let insts = compile_to_instructions(src);
|
|
||||||
assert!(has_instruction(&insts, CMP, &AM::ZeroPage(0x02)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn codegen_array_index_read() {
|
|
||||||
let src = r#"
|
|
||||||
game "Test" { mapper: NROM }
|
|
||||||
var arr: u8[4] = [1, 2, 3, 4]
|
|
||||||
var idx: u8 = 0
|
|
||||||
var result: u8 = 0
|
|
||||||
on frame {
|
|
||||||
result = arr[idx]
|
|
||||||
}
|
|
||||||
start Main
|
|
||||||
"#;
|
|
||||||
let insts = compile_to_instructions(src);
|
|
||||||
// Reading arr[idx] should use TAX + LDA,X
|
|
||||||
assert!(has_instruction(&insts, TAX, &AM::Implied));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn codegen_array_index_write() {
|
|
||||||
let src = r#"
|
|
||||||
game "Test" { mapper: NROM }
|
|
||||||
var arr: u8[4] = [1, 2, 3, 4]
|
|
||||||
var idx: u8 = 0
|
|
||||||
on frame {
|
|
||||||
arr[idx] = 42
|
|
||||||
}
|
|
||||||
start Main
|
|
||||||
"#;
|
|
||||||
let insts = compile_to_instructions(src);
|
|
||||||
// Writing arr[idx] = val should use TAX + STA,X
|
|
||||||
assert!(has_instruction(&insts, TAX, &AM::Implied));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn codegen_scroll() {
|
|
||||||
let src = r#"
|
|
||||||
game "Test" { mapper: NROM }
|
|
||||||
var sx: u8 = 0
|
|
||||||
var sy: u8 = 0
|
|
||||||
on frame {
|
|
||||||
scroll(sx, sy)
|
|
||||||
}
|
|
||||||
start Main
|
|
||||||
"#;
|
|
||||||
let insts = compile_to_instructions(src);
|
|
||||||
// scroll(x, y) should write to $2005 twice
|
|
||||||
let count_2005 = insts
|
|
||||||
.iter()
|
|
||||||
.filter(|i| i.opcode == STA && i.mode == AM::Absolute(0x2005))
|
|
||||||
.count();
|
|
||||||
assert_eq!(count_2005, 2, "scroll should write to $2005 twice");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn codegen_multiply() {
|
|
||||||
let src = r#"
|
|
||||||
game "Test" { mapper: NROM }
|
|
||||||
var a: u8 = 3
|
|
||||||
var b: u8 = 5
|
|
||||||
var result: u8 = 0
|
|
||||||
on frame {
|
|
||||||
result = a * b
|
|
||||||
}
|
|
||||||
start Main
|
|
||||||
"#;
|
|
||||||
let insts = compile_to_instructions(src);
|
|
||||||
// a * b should generate JSR __multiply
|
|
||||||
let has_jsr_multiply = insts
|
|
||||||
.iter()
|
|
||||||
.any(|i| i.opcode == JSR && matches!(&i.mode, AM::Label(l) if l == "__multiply"));
|
|
||||||
assert!(has_jsr_multiply, "multiply should generate JSR __multiply");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn codegen_shift_left() {
|
|
||||||
let src = r#"
|
|
||||||
game "Test" { mapper: NROM }
|
|
||||||
var x: u8 = 1
|
|
||||||
var result: u8 = 0
|
|
||||||
on frame {
|
|
||||||
result = x << 1
|
|
||||||
}
|
|
||||||
start Main
|
|
||||||
"#;
|
|
||||||
let insts = compile_to_instructions(src);
|
|
||||||
// x << 1 should generate ASL A
|
|
||||||
assert!(
|
|
||||||
has_instruction(&insts, ASL, &AM::Accumulator),
|
|
||||||
"shift left should generate ASL A"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn codegen_debug_log_stripped_in_release() {
|
|
||||||
// Without --debug, debug.log should not emit any $4800 writes
|
|
||||||
let src = r#"
|
|
||||||
game "T" { mapper: NROM }
|
|
||||||
var x: u8 = 42
|
|
||||||
on frame {
|
|
||||||
debug.log(x)
|
|
||||||
}
|
|
||||||
start Main
|
|
||||||
"#;
|
|
||||||
let insts = compile_to_instructions(src);
|
|
||||||
// Should NOT write to $4800
|
|
||||||
let has_debug = insts
|
|
||||||
.iter()
|
|
||||||
.any(|i| i.opcode == STA && i.mode == AM::Absolute(0x4800));
|
|
||||||
assert!(!has_debug, "debug.log should be stripped in release mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn codegen_debug_log_emits_in_debug_mode() {
|
|
||||||
use crate::analyzer;
|
|
||||||
use crate::parser;
|
|
||||||
|
|
||||||
let src = r#"
|
|
||||||
game "T" { mapper: NROM }
|
|
||||||
var x: u8 = 42
|
|
||||||
on frame {
|
|
||||||
debug.log(x)
|
|
||||||
}
|
|
||||||
start Main
|
|
||||||
"#;
|
|
||||||
let (prog, _) = parser::parse(src);
|
|
||||||
let prog = prog.unwrap();
|
|
||||||
let analysis = analyzer::analyze(&prog);
|
|
||||||
let codegen = CodeGen::new(&analysis.var_allocations, &prog.constants).with_debug(true);
|
|
||||||
let insts = codegen.generate(&prog);
|
|
||||||
// Should write to $4800 at least once
|
|
||||||
let has_debug = insts
|
|
||||||
.iter()
|
|
||||||
.any(|i| i.opcode == STA && i.mode == AM::Absolute(0x4800));
|
|
||||||
assert!(has_debug, "debug.log should write to $4800 in debug mode");
|
|
||||||
}
|
|
||||||
35
src/main.rs
35
src/main.rs
|
|
@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use nescript::analyzer;
|
use nescript::analyzer;
|
||||||
use nescript::assets;
|
use nescript::assets;
|
||||||
use nescript::codegen::{CodeGen, IrCodeGen};
|
use nescript::codegen::IrCodeGen;
|
||||||
use nescript::errors::render_diagnostics;
|
use nescript::errors::render_diagnostics;
|
||||||
use nescript::ir;
|
use nescript::ir;
|
||||||
use nescript::linker::Linker;
|
use nescript::linker::Linker;
|
||||||
|
|
@ -41,11 +41,6 @@ enum Cli {
|
||||||
/// Dump a call graph showing which functions call which.
|
/// Dump a call graph showing which functions call which.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
call_graph: bool,
|
call_graph: bool,
|
||||||
|
|
||||||
/// Use the legacy AST-based codegen. The default is the IR-based
|
|
||||||
/// codegen, which runs the optimizer passes before emitting 6502.
|
|
||||||
#[arg(long)]
|
|
||||||
use_ast: bool,
|
|
||||||
},
|
},
|
||||||
/// Type-check a source file without building
|
/// Type-check a source file without building
|
||||||
Check {
|
Check {
|
||||||
|
|
@ -66,7 +61,6 @@ fn main() {
|
||||||
dump_ir,
|
dump_ir,
|
||||||
memory_map,
|
memory_map,
|
||||||
call_graph,
|
call_graph,
|
||||||
use_ast,
|
|
||||||
} => {
|
} => {
|
||||||
let output = output.unwrap_or_else(|| input.with_extension("nes"));
|
let output = output.unwrap_or_else(|| input.with_extension("nes"));
|
||||||
match compile(
|
match compile(
|
||||||
|
|
@ -77,7 +71,6 @@ fn main() {
|
||||||
dump_ir,
|
dump_ir,
|
||||||
memory_map,
|
memory_map,
|
||||||
call_graph,
|
call_graph,
|
||||||
use_ast,
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Ok(rom) => {
|
Ok(rom) => {
|
||||||
|
|
@ -226,7 +219,6 @@ struct CompileOptions {
|
||||||
dump_ir: bool,
|
dump_ir: bool,
|
||||||
memory_map: bool,
|
memory_map: bool,
|
||||||
call_graph: bool,
|
call_graph: bool,
|
||||||
use_ast: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile(input: &PathBuf, opts: &CompileOptions) -> Result<Vec<u8>, ()> {
|
fn compile(input: &PathBuf, opts: &CompileOptions) -> Result<Vec<u8>, ()> {
|
||||||
|
|
@ -235,7 +227,6 @@ fn compile(input: &PathBuf, opts: &CompileOptions) -> Result<Vec<u8>, ()> {
|
||||||
let dump_ir = opts.dump_ir;
|
let dump_ir = opts.dump_ir;
|
||||||
let memory_map = opts.memory_map;
|
let memory_map = opts.memory_map;
|
||||||
let call_graph = opts.call_graph;
|
let call_graph = opts.call_graph;
|
||||||
let use_ast = opts.use_ast;
|
|
||||||
let raw_source = std::fs::read_to_string(input).map_err(|e| {
|
let raw_source = std::fs::read_to_string(input).map_err(|e| {
|
||||||
eprintln!("error: failed to read {}: {e}", input.display());
|
eprintln!("error: failed to read {}: {e}", input.display());
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -296,24 +287,14 @@ fn compile(input: &PathBuf, opts: &CompileOptions) -> Result<Vec<u8>, ()> {
|
||||||
eprintln!("error: {e}");
|
eprintln!("error: {e}");
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Code generation: IR-based is the default. `--use-ast` switches to
|
// IR-based code generation. Lower → optimize → emit 6502.
|
||||||
// the legacy AST-based codegen for comparison and fallback.
|
let mut instructions = IrCodeGen::new(&analysis.var_allocations, &ir_program)
|
||||||
let mut instructions = if use_ast {
|
.with_sprites(&sprites)
|
||||||
CodeGen::new(&analysis.var_allocations, &program.constants)
|
.with_debug(debug)
|
||||||
.with_sprites(&sprites)
|
.generate(&ir_program);
|
||||||
.with_enums(&program.enums)
|
|
||||||
.with_debug(debug)
|
|
||||||
.generate(&program)
|
|
||||||
} else {
|
|
||||||
IrCodeGen::new(&analysis.var_allocations, &ir_program)
|
|
||||||
.with_sprites(&sprites)
|
|
||||||
.with_debug(debug)
|
|
||||||
.generate(&ir_program)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Peephole optimization: cheap pass that removes redundant
|
// Peephole pass: cleans up the IR codegen's temp-heavy output —
|
||||||
// store-then-load pairs over IR temp slots. Biggest win for the
|
// dead stores, redundant loads, short-branch folds, etc.
|
||||||
// IR codegen, but safe for the AST codegen too.
|
|
||||||
nescript::codegen::peephole::optimize(&mut instructions);
|
nescript::codegen::peephole::optimize(&mut instructions);
|
||||||
|
|
||||||
if asm_dump {
|
if asm_dump {
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,17 @@ use std::path::Path;
|
||||||
|
|
||||||
use nescript::analyzer;
|
use nescript::analyzer;
|
||||||
use nescript::assets;
|
use nescript::assets;
|
||||||
use nescript::codegen::CodeGen;
|
use nescript::codegen::IrCodeGen;
|
||||||
use nescript::ir;
|
use nescript::ir;
|
||||||
use nescript::linker::Linker;
|
use nescript::linker::Linker;
|
||||||
use nescript::optimizer;
|
use nescript::optimizer;
|
||||||
use nescript::rom;
|
use nescript::rom;
|
||||||
|
|
||||||
/// Compile a `NEScript` source string into a .nes ROM.
|
/// Compile a `NEScript` source string into a .nes ROM. Runs the full
|
||||||
|
/// IR pipeline: parse → analyze → IR lower → optimize → IR codegen
|
||||||
|
/// → peephole → link. This is what the `nescript build` CLI does
|
||||||
|
/// (minus file IO and the dump flags), so these integration tests
|
||||||
|
/// exercise the same path end users hit.
|
||||||
fn compile(source: &str) -> Vec<u8> {
|
fn compile(source: &str) -> Vec<u8> {
|
||||||
let (program, diags) = nescript::parser::parse(source);
|
let (program, diags) = nescript::parser::parse(source);
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -24,16 +28,15 @@ fn compile(source: &str) -> Vec<u8> {
|
||||||
analysis.diagnostics
|
analysis.diagnostics
|
||||||
);
|
);
|
||||||
|
|
||||||
// Run IR lowering and optimization (validates the pipeline works)
|
|
||||||
let mut ir_program = ir::lower(&program, &analysis);
|
let mut ir_program = ir::lower(&program, &analysis);
|
||||||
optimizer::optimize(&mut ir_program);
|
optimizer::optimize(&mut ir_program);
|
||||||
|
|
||||||
let sprites = assets::resolve_sprites(&program, Path::new("."))
|
let sprites = assets::resolve_sprites(&program, Path::new("."))
|
||||||
.expect("sprite resolution should succeed");
|
.expect("sprite resolution should succeed");
|
||||||
|
|
||||||
let codegen =
|
let codegen = IrCodeGen::new(&analysis.var_allocations, &ir_program).with_sprites(&sprites);
|
||||||
CodeGen::new(&analysis.var_allocations, &program.constants).with_sprites(&sprites);
|
let mut instructions = codegen.generate(&ir_program);
|
||||||
let instructions = codegen.generate(&program);
|
nescript::codegen::peephole::optimize(&mut instructions);
|
||||||
|
|
||||||
let linker = Linker::new(program.game.mirroring);
|
let linker = Linker::new(program.game.mirroring);
|
||||||
linker.link_with_assets(&instructions, &sprites)
|
linker.link_with_assets(&instructions, &sprites)
|
||||||
|
|
@ -643,9 +646,9 @@ fn compile_with_mapper(source: &str) -> Vec<u8> {
|
||||||
let sprites = assets::resolve_sprites(&program, Path::new("."))
|
let sprites = assets::resolve_sprites(&program, Path::new("."))
|
||||||
.expect("sprite resolution should succeed");
|
.expect("sprite resolution should succeed");
|
||||||
|
|
||||||
let codegen = nescript::codegen::CodeGen::new(&analysis.var_allocations, &program.constants)
|
let codegen = IrCodeGen::new(&analysis.var_allocations, &ir_program).with_sprites(&sprites);
|
||||||
.with_sprites(&sprites);
|
let mut instructions = codegen.generate(&ir_program);
|
||||||
let instructions = codegen.generate(&program);
|
nescript::codegen::peephole::optimize(&mut instructions);
|
||||||
|
|
||||||
let linker = Linker::with_mapper(program.game.mirroring, program.game.mapper);
|
let linker = Linker::with_mapper(program.game.mirroring, program.game.mapper);
|
||||||
linker.link_with_assets(&instructions, &sprites)
|
linker.link_with_assets(&instructions, &sprites)
|
||||||
|
|
@ -703,13 +706,15 @@ fn sprite_resolution_uses_tile_index() {
|
||||||
"tile 0 should still contain the default smiley",
|
"tile 0 should still contain the default smiley",
|
||||||
);
|
);
|
||||||
|
|
||||||
// In PRG ROM, look for `LDA #$01 ; STA $0201` which writes the Player's
|
// In PRG ROM, look for `LDA #$01 ; STA $0201,Y` which writes
|
||||||
// tile index (1) into the tile-index byte of the first OAM slot.
|
// the Player's tile index (1) into the tile-index byte of the
|
||||||
|
// current OAM slot (the slot is computed at runtime via the
|
||||||
|
// OAM cursor in Y). The STA AbsoluteY opcode is $99.
|
||||||
let prg = &rom_data[16..16 + 16384];
|
let prg = &rom_data[16..16 + 16384];
|
||||||
let pattern = [0xA9u8, 0x01, 0x8D, 0x01, 0x02];
|
let pattern = [0xA9u8, 0x01, 0x99, 0x01, 0x02];
|
||||||
assert!(
|
assert!(
|
||||||
prg.windows(pattern.len()).any(|w| w == pattern),
|
prg.windows(pattern.len()).any(|w| w == pattern),
|
||||||
"PRG ROM should contain LDA #$01 ; STA $0201 for draw Player",
|
"PRG ROM should contain LDA #$01 ; STA $0201,Y for draw Player",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -746,38 +751,11 @@ fn program_with_mmc1() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── IR Codegen Tests ──
|
// ── IR Codegen Tests ──
|
||||||
|
//
|
||||||
/// Compile a program using the IR-based codegen path instead of the
|
// These tests exercise specific end-to-end IR codegen behavior.
|
||||||
/// AST-based codegen. Validates the full IR pipeline produces a valid ROM.
|
// They all use the top-level `compile()` helper now that it runs
|
||||||
fn compile_with_ir_codegen(source: &str) -> Vec<u8> {
|
// the full IR pipeline — there's no longer a separate legacy path
|
||||||
use nescript::codegen::IrCodeGen;
|
// to compare against.
|
||||||
|
|
||||||
let (program, diags) = nescript::parser::parse(source);
|
|
||||||
assert!(
|
|
||||||
diags.is_empty(),
|
|
||||||
"unexpected parse errors: {diags:?}\nsource:\n{source}"
|
|
||||||
);
|
|
||||||
let program = program.expect("parse should succeed");
|
|
||||||
|
|
||||||
let analysis = analyzer::analyze(&program);
|
|
||||||
assert!(
|
|
||||||
analysis.diagnostics.iter().all(|d| !d.is_error()),
|
|
||||||
"unexpected analysis errors: {:?}",
|
|
||||||
analysis.diagnostics
|
|
||||||
);
|
|
||||||
|
|
||||||
// Lower to IR and run the optimizer
|
|
||||||
let mut ir_program = ir::lower(&program, &analysis);
|
|
||||||
optimizer::optimize(&mut ir_program);
|
|
||||||
|
|
||||||
// IR-based codegen
|
|
||||||
let codegen = IrCodeGen::new(&analysis.var_allocations, &ir_program);
|
|
||||||
let instructions = codegen.generate(&ir_program);
|
|
||||||
|
|
||||||
// Link into a ROM
|
|
||||||
let linker = Linker::new(program.game.mirroring);
|
|
||||||
linker.link(&instructions)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ir_codegen_minimal_rom() {
|
fn ir_codegen_minimal_rom() {
|
||||||
|
|
@ -787,7 +765,7 @@ fn ir_codegen_minimal_rom() {
|
||||||
on frame { wait_frame }
|
on frame { wait_frame }
|
||||||
start Main
|
start Main
|
||||||
"#;
|
"#;
|
||||||
let rom_data = compile_with_ir_codegen(source);
|
let rom_data = compile(source);
|
||||||
let info = rom::validate_ines(&rom_data).expect("should be valid iNES");
|
let info = rom::validate_ines(&rom_data).expect("should be valid iNES");
|
||||||
assert_eq!(info.mapper, 0);
|
assert_eq!(info.mapper, 0);
|
||||||
assert_eq!(rom_data.len(), 16 + 16384 + 8192);
|
assert_eq!(rom_data.len(), 16 + 16384 + 8192);
|
||||||
|
|
@ -807,7 +785,7 @@ fn ir_codegen_full_pipeline() {
|
||||||
}
|
}
|
||||||
start Main
|
start Main
|
||||||
"#;
|
"#;
|
||||||
let rom_data = compile_with_ir_codegen(source);
|
let rom_data = compile(source);
|
||||||
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -831,7 +809,7 @@ fn ir_codegen_multi_state_dispatch() {
|
||||||
}
|
}
|
||||||
start Title
|
start Title
|
||||||
"#;
|
"#;
|
||||||
let rom_data = compile_with_ir_codegen(source);
|
let rom_data = compile(source);
|
||||||
let info = rom::validate_ines(&rom_data).expect("should be valid iNES");
|
let info = rom::validate_ines(&rom_data).expect("should be valid iNES");
|
||||||
assert_eq!(info.mapper, 0);
|
assert_eq!(info.mapper, 0);
|
||||||
}
|
}
|
||||||
|
|
@ -851,7 +829,7 @@ fn ir_codegen_multi_oam() {
|
||||||
}
|
}
|
||||||
start Main
|
start Main
|
||||||
"#;
|
"#;
|
||||||
let rom_data = compile_with_ir_codegen(source);
|
let rom_data = compile(source);
|
||||||
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue