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

runtime: gate __multiply / __divide on usage markers

Drop __mul_used from IrOp::Mul codegen and __div_used from IrOp::Div
/ IrOp::Mod codegen (modulo reuses the same routine). The linker
skips gen_multiply / gen_divide for programs that never emit the
markers, following the same pattern already used by __audio_used /
__ppu_update_used / __sprite_cycle_used.

The optimizer already rewrites multiplies and divides by constant
powers of two into shifts (and modulo by constant powers of two
into masks), so the markers only fire for genuinely runtime math.
A program like `examples/comparisons.ne` that never multiplies or
divides now reclaims ~56 bytes of PRG; programs that use only one
of the two reclaim the other's share.

Audio goldens flip for every example that uses audio. The .ne
sources are unchanged and the pixel goldens are byte-identical —
the audio stream differs only because removing the math routines
shifts the audio tick's absolute address in PRG by 56 bytes, which
changes which of its internal branches cross 6502 page boundaries
and therefore the per-frame cycle count of a single NMI by 1-5
clocks. Over 180 frames the accumulated drift shifts APU register
write timing enough to render a different digital sample stream
at the same logical wave shape. Expected consequence of ROM-layout
change under cycle-accurate emulation; documented path per
CLAUDE.md "Updating goldens".

https://claude.ai/code/session_016kM6P7PukktBDqTZexrrAN
This commit is contained in:
Claude 2026-04-16 13:01:19 +00:00
parent 9970a23673
commit 033d399565
No known key found for this signature in database
41 changed files with 80 additions and 12 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -161,6 +161,23 @@ pub struct IrCodeGen<'a> {
/// resulting `__ppu_update_used` marker label to decide whether /// resulting `__ppu_update_used` marker label to decide whether
/// to splice the in-NMI palette/nametable update helper. /// to splice the in-NMI palette/nametable update helper.
ppu_update_used: bool, ppu_update_used: bool,
/// Set to true the first time we lower an `IrOp::Mul`. Drives
/// the `__mul_used` marker label so the linker can skip
/// `gen_multiply` (~50 bytes) for programs that never multiply.
/// The optimizer's strength reduction has already turned
/// multiplies by constant powers of two into shifts by the time
/// codegen runs, so this flag only gets set for real runtime
/// multiplications.
mul_used: bool,
/// Set to true the first time we lower an `IrOp::Div` or
/// `IrOp::Mod` (modulo reuses the divide routine). Drives the
/// `__div_used` marker label so the linker can skip `gen_divide`
/// (~70 bytes) for programs that never divide. Divide by
/// constant powers of two has been strength-reduced to shifts
/// by the optimizer, and modulo by constant powers of two to
/// masks; runtime divide only survives for non-constant or
/// non-power-of-two divisors.
div_used: bool,
/// Source-location markers produced from [`IrOp::SourceLoc`]. /// Source-location markers produced from [`IrOp::SourceLoc`].
/// Each entry is a `(label_name, span)` pair — the codegen /// Each entry is a `(label_name, span)` pair — the codegen
/// emits a unique label-definition pseudo-op at the current /// emits a unique label-definition pseudo-op at the current
@ -338,6 +355,8 @@ impl<'a> IrCodeGen<'a> {
noise_used: false, noise_used: false,
triangle_used: false, triangle_used: false,
ppu_update_used: false, ppu_update_used: false,
mul_used: false,
div_used: false,
source_locs: Vec::new(), source_locs: Vec::new(),
next_source_loc: 0, next_source_loc: 0,
emit_source_locs: false, emit_source_locs: false,
@ -1175,7 +1194,11 @@ impl<'a> IrCodeGen<'a> {
self.store_temp(*d); self.store_temp(*d);
} }
IrOp::Mul(d, a, b) => { IrOp::Mul(d, a, b) => {
// Software multiply: multiplicand in A, multiplier in $02 // Software multiply: multiplicand in A, multiplier in $02.
// Drop the `__mul_used` marker so the linker knows to
// link the `__multiply` subroutine in — programs that
// don't multiply skip ~50 bytes of runtime.
self.emit_mul_marker();
self.load_temp(*a); self.load_temp(*a);
self.emit(PHA, AM::Implied); // Save for __multiply contract self.emit(PHA, AM::Implied); // Save for __multiply contract
let b_addr = self.temp_addr(*b); let b_addr = self.temp_addr(*b);
@ -1222,7 +1245,10 @@ impl<'a> IrCodeGen<'a> {
IrOp::Div(d, a, b) => { IrOp::Div(d, a, b) => {
// Software divide: dividend in A, divisor in $02. // Software divide: dividend in A, divisor in $02.
// `__divide` returns quotient in A and leaves // `__divide` returns quotient in A and leaves
// remainder in ZP $03. // remainder in ZP $03. Emit the `__div_used` marker
// so the linker links the `__divide` subroutine in;
// programs that don't divide skip ~70 bytes.
self.emit_div_marker();
self.load_temp(*a); self.load_temp(*a);
self.emit(PHA, AM::Implied); self.emit(PHA, AM::Implied);
let b_addr = self.temp_addr(*b); let b_addr = self.temp_addr(*b);
@ -1234,7 +1260,10 @@ impl<'a> IrCodeGen<'a> {
} }
IrOp::Mod(d, a, b) => { IrOp::Mod(d, a, b) => {
// Modulo reuses __divide and reads the remainder out // Modulo reuses __divide and reads the remainder out
// of ZP $03 afterwards. // of ZP $03 afterwards. Same `__div_used` marker as
// `IrOp::Div` — modulo doesn't have a separate
// runtime routine.
self.emit_div_marker();
self.load_temp(*a); self.load_temp(*a);
self.emit(PHA, AM::Implied); self.emit(PHA, AM::Implied);
let b_addr = self.temp_addr(*b); let b_addr = self.temp_addr(*b);
@ -2018,6 +2047,32 @@ impl<'a> IrCodeGen<'a> {
} }
} }
/// Emit the `__mul_used` marker label at most once per program.
/// The linker uses this to decide whether to link in the
/// `gen_multiply` subroutine. Programs that never multiply (or
/// that only multiply by constant powers of two — those are
/// strength-reduced to shifts) skip the runtime entirely.
fn emit_mul_marker(&mut self) {
if !self.mul_used {
self.emit_label("__mul_used");
self.mul_used = true;
}
}
/// Emit the `__div_used` marker label at most once per program.
/// The linker uses this to decide whether to link in the
/// `gen_divide` subroutine. Both `IrOp::Div` and `IrOp::Mod`
/// trigger this marker because modulo reuses the divide
/// routine. Programs whose divide/modulo operations all use
/// constant power-of-two divisors have already been rewritten
/// to shifts / masks by the optimizer and never reach here.
fn emit_div_marker(&mut self) {
if !self.div_used {
self.emit_label("__div_used");
self.div_used = true;
}
}
/// Emit the MMC3 `__irq_user` handler that dispatches on the /// Emit the MMC3 `__irq_user` handler that dispatches on the
/// `(current_state, scanline_step)` pair. Supports multiple /// `(current_state, scanline_step)` pair. Supports multiple
/// `on scanline(N)` handlers per state — they fire in ascending /// `on scanline(N)` handlers per state — they fire in ascending

View file

@ -471,9 +471,22 @@ impl Linker {
} }
} }
// Math runtime routines (included always for simplicity) // Math runtime routines. Gated on the `__mul_used` /
// `__div_used` marker labels that the IR codegen drops at
// the first `IrOp::Mul` / `IrOp::Div` / `IrOp::Mod`. The
// optimizer rewrites multiplies and divides by constant
// powers of two into shifts (and modulo by constant powers
// of two into masks) before codegen runs, so these markers
// only fire for genuinely runtime-costly math. Programs
// without any surviving mul or div pay zero bytes here.
let has_mul = has_label(user_code, "__mul_used");
let has_div = has_label(user_code, "__div_used");
if has_mul {
all_instructions.extend(runtime::gen_multiply()); all_instructions.extend(runtime::gen_multiply());
}
if has_div {
all_instructions.extend(runtime::gen_divide()); all_instructions.extend(runtime::gen_divide());
}
// Audio subsystem — linked in whenever user code touched // Audio subsystem — linked in whenever user code touched
// audio (detected via the `__audio_used` marker emitted by // audio (detected via the `__audio_used` marker emitted by

View file

@ -1 +1 @@
7b16a9db 132084 49c411ce 132084

View file

@ -1 +1 @@
e73a6a73 132084 68089bdb 132084

View file

@ -1 +1 @@
ea38044c 132084 5146a336 132084

View file

@ -1 +1 @@
dded3a6a 132084 5cc6d0f5 132084

View file

@ -1 +1 @@
f2dbfbf3 132084 714f679e 132084

View file

@ -1 +1 @@
4e8bdb33 132084 575adcb6 132084