mirror of
https://github.com/imjasonh/nescript
synced 2026-07-10 17:52:51 +00:00
compiler: audio driver, u16 arithmetic, multi-scanline, slot recycling
Five language features and optimizations from the planned-work backlog: - **Minimal audio driver**: `play`/`start_music`/`stop_music` now generate APU pulse-1/pulse-2 writes from a builtin SFX/music name table, and the NMI handler gains a `JSR __audio_tick` splice (via the linker's `__audio_used` marker lookup) that ages an SFX countdown counter and mutes pulse 1 when the tone expires. Programs that never trigger audio pay zero ROM cost. - **u16 arithmetic and comparisons**: new IR ops `LoadVarHi`, `StoreVarHi`, `Add16`, `Sub16`, and six `Cmp*16` variants. The lowering context tracks variable types via the analyzer's symbol table and routes expressions through the 8-bit or 16-bit path based on operand width. Add16 emits `CLC;ADC;ADC` with carry propagating naturally into the high byte; compares dispatch high-byte-first with a short-circuit low-byte fallback. Fixes a silent miscompile where `big += 1` on a u16 var only incremented the low byte. - **Multi-scanline handlers per state**: `gen_scanline_irq` now dispatches on `(current_state, ZP_SCANLINE_STEP)` and reloads the MMC3 counter with the delta to the next scanline in the same state. `gen_scanline_reload` resets the step counter at the top of each NMI so a state with multiple handlers fires them in ascending line order. Previously only the first handler per state ever fired. - **IR temp slot recycling**: `build_use_counts` pre-scans each function to count per-temp uses; `retire_op_sources` decrements the counts after each op and pushes dead slots back onto `free_slots` for later allocation. `bitwise_ops.ne` used to crash (debug) or miscompile (release) once it hit 128 concurrent temps; with recycling the same function now uses ~4 slots instead of 136. - **INC/DEC peephole fold + improved dead-load elimination**: `fold_inc_dec` collapses `LDA addr; CLC; ADC #1; STA addr` into a single `INC addr` (and the SEC/SBC variant into `DEC addr`), saving 5 bytes and 5 cycles per increment. The fold is suppressed when the next instruction reads carry. `remove_dead_loads` now walks past INC/DEC/STX/STY (which don't touch A) to find the actual next A-use, catching more dead loads. Tests: 331 unit + 39 integration (up from 313 + 37), including new guards for audio, u16, multi-scanline, and slot recycling. https://claude.ai/code/session_01A8qk3gw2jWSzdiXBZPZSFE
This commit is contained in:
parent
9ebf58f7db
commit
9a539ea068
12 changed files with 2108 additions and 144 deletions
|
|
@ -394,11 +394,79 @@ fn collect_source_temps(op: &IrOp, used: &mut HashSet<IrTemp>) {
|
|||
IrOp::Poke(_, src) => {
|
||||
used.insert(*src);
|
||||
}
|
||||
IrOp::ReadInput(_, _)
|
||||
IrOp::StoreVarHi(_, src) => {
|
||||
used.insert(*src);
|
||||
}
|
||||
IrOp::Add16 {
|
||||
a_lo,
|
||||
a_hi,
|
||||
b_lo,
|
||||
b_hi,
|
||||
..
|
||||
}
|
||||
| IrOp::Sub16 {
|
||||
a_lo,
|
||||
a_hi,
|
||||
b_lo,
|
||||
b_hi,
|
||||
..
|
||||
}
|
||||
| IrOp::CmpEq16 {
|
||||
a_lo,
|
||||
a_hi,
|
||||
b_lo,
|
||||
b_hi,
|
||||
..
|
||||
}
|
||||
| IrOp::CmpNe16 {
|
||||
a_lo,
|
||||
a_hi,
|
||||
b_lo,
|
||||
b_hi,
|
||||
..
|
||||
}
|
||||
| IrOp::CmpLt16 {
|
||||
a_lo,
|
||||
a_hi,
|
||||
b_lo,
|
||||
b_hi,
|
||||
..
|
||||
}
|
||||
| IrOp::CmpGt16 {
|
||||
a_lo,
|
||||
a_hi,
|
||||
b_lo,
|
||||
b_hi,
|
||||
..
|
||||
}
|
||||
| IrOp::CmpLtEq16 {
|
||||
a_lo,
|
||||
a_hi,
|
||||
b_lo,
|
||||
b_hi,
|
||||
..
|
||||
}
|
||||
| IrOp::CmpGtEq16 {
|
||||
a_lo,
|
||||
a_hi,
|
||||
b_lo,
|
||||
b_hi,
|
||||
..
|
||||
} => {
|
||||
used.insert(*a_lo);
|
||||
used.insert(*a_hi);
|
||||
used.insert(*b_lo);
|
||||
used.insert(*b_hi);
|
||||
}
|
||||
IrOp::LoadVarHi(_, _)
|
||||
| IrOp::ReadInput(_, _)
|
||||
| IrOp::WaitFrame
|
||||
| IrOp::Transition(_)
|
||||
| IrOp::InlineAsm(_)
|
||||
| IrOp::Peek(_, _)
|
||||
| IrOp::PlaySfx(_)
|
||||
| IrOp::StartMusic(_)
|
||||
| IrOp::StopMusic
|
||||
| IrOp::SourceLoc(_) => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -429,6 +497,7 @@ fn op_dest(op: &IrOp) -> Option<IrTemp> {
|
|||
IrOp::ReadInput(d, _) => Some(*d),
|
||||
IrOp::Peek(d, _) => Some(*d),
|
||||
IrOp::StoreVar(_, _)
|
||||
| IrOp::StoreVarHi(_, _)
|
||||
| IrOp::ArrayStore(_, _, _)
|
||||
| IrOp::DrawSprite { .. }
|
||||
| IrOp::WaitFrame
|
||||
|
|
@ -438,7 +507,25 @@ fn op_dest(op: &IrOp) -> Option<IrTemp> {
|
|||
| IrOp::DebugAssert(_)
|
||||
| IrOp::InlineAsm(_)
|
||||
| IrOp::Poke(_, _)
|
||||
| IrOp::PlaySfx(_)
|
||||
| IrOp::StartMusic(_)
|
||||
| IrOp::StopMusic
|
||||
| IrOp::SourceLoc(_) => None,
|
||||
// 16-bit ops have two destinations; the simple single-dest
|
||||
// DCE below would incorrectly drop a 16-bit op whose low
|
||||
// dest is unused even if its high dest is live. Returning
|
||||
// `None` here preserves them unconditionally — they're
|
||||
// rare enough that the lost DCE opportunity is a good
|
||||
// trade for correctness.
|
||||
IrOp::LoadVarHi(_, _)
|
||||
| IrOp::Add16 { .. }
|
||||
| IrOp::Sub16 { .. }
|
||||
| IrOp::CmpEq16 { .. }
|
||||
| IrOp::CmpNe16 { .. }
|
||||
| IrOp::CmpLt16 { .. }
|
||||
| IrOp::CmpGt16 { .. }
|
||||
| IrOp::CmpLtEq16 { .. }
|
||||
| IrOp::CmpGtEq16 { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue