1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-10 09:42:37 +00:00

M4+M5: Optimizer passes, type casting, bank switching, math runtime

Milestone 4 — Optimization & Polish:
- Strength reduction: multiply by power-of-2 → shift left
- Zero-page promotion analysis: rank variables by access frequency
- `as` type casting expression in parser/AST/analyzer
- `scroll(x, y)` statement
- `--asm-dump` flag for viewing generated assembly
- Extended optimizer tests (strength reduction, frequency analysis)

Milestone 5 — Bank Switching & Release:
- Mapper support: MMC1 (1), UxROM (2), MMC3 (4) in parser and ROM builder
- Bank declarations: `bank Name: prg` / `bank Name: chr`
- Linker::with_mapper for mapper-aware ROM generation
- Software multiply (8x8→16, shift-and-add algorithm)
- Software divide (8÷8→8, restoring division algorithm)
- ROM tests for mapper encoding round-trip
- Integration test for MMC1 compilation

210 tests total (18 new), all pre-commit checks pass.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
Claude 2026-04-12 00:22:11 +00:00
parent 058f7a87b6
commit 5434dda114
No known key found for this signature in database
15 changed files with 865 additions and 13 deletions

View file

@ -3,14 +3,159 @@ mod tests;
use std::collections::{HashMap, HashSet};
use crate::ir::{IrBasicBlock, IrFunction, IrOp, IrProgram, IrTemp, IrTerminator};
use crate::ir::{IrBasicBlock, IrFunction, IrOp, IrProgram, IrTemp, IrTerminator, VarId};
/// Run all optimization passes on the IR program.
pub fn optimize(program: &mut IrProgram) {
strength_reduce(program);
inline_small_functions(program);
const_fold(program);
dead_code(program);
}
// ---------------------------------------------------------------------------
// Zero-page promotion analysis
// ---------------------------------------------------------------------------
/// Analyze IR to count variable access frequency.
/// Returns a list of `(VarId, count)` sorted by frequency (highest first).
pub fn analyze_zp_candidates(program: &IrProgram) -> Vec<(VarId, u32)> {
let mut counts: HashMap<VarId, u32> = HashMap::new();
for func in &program.functions {
for block in &func.blocks {
for op in &block.ops {
match op {
IrOp::LoadVar(_, var_id) | IrOp::StoreVar(var_id, _) => {
*counts.entry(*var_id).or_insert(0) += 1;
}
IrOp::ArrayLoad(_, var_id, _) | IrOp::ArrayStore(var_id, _, _) => {
*counts.entry(*var_id).or_insert(0) += 1;
}
_ => {}
}
}
}
}
let mut result: Vec<(VarId, u32)> = counts.into_iter().collect();
result.sort_by(|a, b| b.1.cmp(&a.1));
result
}
// ---------------------------------------------------------------------------
// Function inlining
// ---------------------------------------------------------------------------
/// Inline small functions (< 8 ops) called from <= 2 sites.
/// For now, just remove empty/trivial functions (those with 0 meaningful ops).
pub fn inline_small_functions(program: &mut IrProgram) {
// Count call sites for each function
let mut call_counts: HashMap<String, u32> = HashMap::new();
for func in &program.functions {
for block in &func.blocks {
for op in &block.ops {
if let IrOp::Call(_, name, _) = op {
*call_counts.entry(name.clone()).or_insert(0) += 1;
}
}
}
}
// Find functions that are trivial (0 meaningful ops, just return)
// and have <= 2 call sites and < 8 ops
let trivial_fns: HashSet<String> = program
.functions
.iter()
.filter(|f| {
let op_count = f.op_count();
let calls = call_counts.get(&f.name).copied().unwrap_or(0);
op_count < 8 && calls <= 2 && op_count == 0
})
.map(|f| f.name.clone())
.collect();
if trivial_fns.is_empty() {
return;
}
// Remove calls to trivial functions
for func in &mut program.functions {
for block in &mut func.blocks {
block
.ops
.retain(|op| !matches!(op, IrOp::Call(_, name, _) if trivial_fns.contains(name)));
}
}
// Remove the trivial functions themselves
program.functions.retain(|f| !trivial_fns.contains(&f.name));
}
// ---------------------------------------------------------------------------
// Strength reduction
// ---------------------------------------------------------------------------
/// Replace multiply by power-of-2 with shifts, and multiply by 3 with add chain.
fn strength_reduce(program: &mut IrProgram) {
for func in &mut program.functions {
for block in &mut func.blocks {
strength_reduce_block(block);
}
}
}
fn strength_reduce_block(block: &mut IrBasicBlock) {
// First, collect known constants from LoadImm ops
let mut constants: HashMap<IrTemp, u8> = HashMap::new();
for op in &block.ops {
if let IrOp::LoadImm(t, v) = op {
constants.insert(*t, *v);
}
}
// Now scan for Mul ops where one operand is a known constant
let mut replacements: Vec<(usize, Vec<IrOp>)> = Vec::new();
for (i, op) in block.ops.iter().enumerate() {
if let IrOp::Mul(dest, a, b) = op {
// Check if b is a known constant
if let Some(&val) = constants.get(b) {
if val.is_power_of_two() && val > 1 {
let shift = val.trailing_zeros() as u8;
replacements.push((i, vec![IrOp::ShiftLeft(*dest, *a, shift)]));
} else if val == 3 {
// Mul by 3: tmp = a + a; dest = tmp + a
// We reuse dest as tmp in first step, then compute final
// Actually need a temp. Use dest for the intermediate.
replacements.push((
i,
vec![IrOp::Add(*dest, *a, *a), IrOp::Add(*dest, *dest, *a)],
));
}
continue;
}
// Check if a is a known constant (commutative)
if let Some(&val) = constants.get(a) {
if val.is_power_of_two() && val > 1 {
let shift = val.trailing_zeros() as u8;
replacements.push((i, vec![IrOp::ShiftLeft(*dest, *b, shift)]));
} else if val == 3 {
replacements.push((
i,
vec![IrOp::Add(*dest, *b, *b), IrOp::Add(*dest, *dest, *b)],
));
}
}
}
}
// Apply replacements in reverse order to maintain correct indices
for (i, new_ops) in replacements.into_iter().rev() {
block.ops.splice(i..=i, new_ops);
}
}
// ---------------------------------------------------------------------------
// Constant folding
// ---------------------------------------------------------------------------

View file

@ -158,3 +158,188 @@ fn optimize_preserves_used_ops() {
// StoreVar has no dest so it's always kept.
assert_eq!(block.ops.len(), 3, "expected 3 ops, got {:?}", block.ops);
}
// ---------------------------------------------------------------------------
// Strength reduction tests
// ---------------------------------------------------------------------------
#[test]
fn strength_reduce_power_of_2() {
// Mul(t2, t0, t1) where t1 = 4 -> ShiftLeft(t2, t0, 2)
let t0 = IrTemp(0);
let t1 = IrTemp(1);
let t2 = IrTemp(2);
let ops = vec![
IrOp::LoadImm(t0, 7),
IrOp::LoadImm(t1, 4),
IrOp::Mul(t2, t0, t1),
];
let mut prog = make_program(ops, IrTerminator::Return(Some(t2)));
strength_reduce(&mut prog);
let block = &prog.functions[0].blocks[0];
// The Mul should have been replaced with ShiftLeft
let has_shift = block
.ops
.iter()
.any(|op| matches!(op, IrOp::ShiftLeft(d, s, 2) if *d == t2 && *s == t0));
assert!(
has_shift,
"expected ShiftLeft(t2, t0, 2), got {:?}",
block.ops
);
// No Mul should remain
let has_mul = block.ops.iter().any(|op| matches!(op, IrOp::Mul(..)));
assert!(!has_mul, "Mul should have been replaced");
}
#[test]
fn strength_reduce_mul_by_3() {
// Mul(t2, t0, t1) where t1 = 3 -> Add chain
let t0 = IrTemp(0);
let t1 = IrTemp(1);
let t2 = IrTemp(2);
let ops = vec![
IrOp::LoadImm(t0, 5),
IrOp::LoadImm(t1, 3),
IrOp::Mul(t2, t0, t1),
];
let mut prog = make_program(ops, IrTerminator::Return(Some(t2)));
strength_reduce(&mut prog);
let block = &prog.functions[0].blocks[0];
// Mul should have been replaced by two Add ops
let add_count = block
.ops
.iter()
.filter(|op| matches!(op, IrOp::Add(..)))
.count();
assert_eq!(
add_count, 2,
"expected 2 Add ops for mul-by-3, got {:?}",
block.ops
);
let has_mul = block.ops.iter().any(|op| matches!(op, IrOp::Mul(..)));
assert!(!has_mul, "Mul should have been replaced");
}
#[test]
fn strength_reduce_leaves_non_power() {
// Mul by 5 should NOT be replaced
let t0 = IrTemp(0);
let t1 = IrTemp(1);
let t2 = IrTemp(2);
let ops = vec![
IrOp::LoadImm(t0, 7),
IrOp::LoadImm(t1, 5),
IrOp::Mul(t2, t0, t1),
];
let mut prog = make_program(ops, IrTerminator::Return(Some(t2)));
strength_reduce(&mut prog);
let block = &prog.functions[0].blocks[0];
let has_mul = block.ops.iter().any(|op| matches!(op, IrOp::Mul(..)));
assert!(has_mul, "Mul by 5 should not be replaced");
}
// ---------------------------------------------------------------------------
// Zero-page candidate analysis tests
// ---------------------------------------------------------------------------
#[test]
fn zp_candidates_by_frequency() {
let v0 = VarId(0);
let v1 = VarId(1);
let v2 = VarId(2);
let t0 = IrTemp(0);
let t1 = IrTemp(1);
// v0 accessed 3 times, v1 accessed 1 time, v2 accessed 2 times
let ops = vec![
IrOp::LoadVar(t0, v0),
IrOp::StoreVar(v0, t0),
IrOp::LoadVar(t1, v0),
IrOp::LoadVar(t0, v2),
IrOp::StoreVar(v2, t0),
IrOp::LoadVar(t1, v1),
];
let prog = make_program(ops, IrTerminator::Return(Some(t1)));
let candidates = analyze_zp_candidates(&prog);
assert!(!candidates.is_empty());
// First should be v0 with count 3
assert_eq!(candidates[0].0, v0);
assert_eq!(candidates[0].1, 3);
// v2 with count 2 should be before v1 with count 1
let v2_idx = candidates.iter().position(|(v, _)| *v == v2).unwrap();
let v1_idx = candidates.iter().position(|(v, _)| *v == v1).unwrap();
assert!(
v2_idx < v1_idx,
"v2 (count 2) should come before v1 (count 1)"
);
}
// ---------------------------------------------------------------------------
// Function inlining tests
// ---------------------------------------------------------------------------
#[test]
fn inline_removes_trivial() {
// Create a program with a trivial (empty) function and a main function that calls it
let t0 = IrTemp(0);
let trivial_fn = IrFunction {
name: "trivial".to_string(),
blocks: vec![IrBasicBlock {
label: "entry".to_string(),
ops: vec![],
terminator: IrTerminator::Return(None),
}],
locals: vec![],
param_count: 0,
has_return: false,
source_span: Span::new(0, 0, 0),
};
let main_fn = IrFunction {
name: "main_fn".to_string(),
blocks: vec![IrBasicBlock {
label: "entry".to_string(),
ops: vec![
IrOp::Call(None, "trivial".to_string(), vec![]),
IrOp::LoadImm(t0, 42),
],
terminator: IrTerminator::Return(Some(t0)),
}],
locals: vec![],
param_count: 0,
has_return: true,
source_span: Span::new(0, 0, 0),
};
let mut prog = IrProgram {
functions: vec![trivial_fn, main_fn],
globals: vec![],
rom_data: vec![],
};
inline_small_functions(&mut prog);
// The trivial function should be removed
assert_eq!(
prog.functions.len(),
1,
"trivial function should have been removed"
);
assert_eq!(prog.functions[0].name, "main_fn");
// The call to trivial should be removed from main_fn
let has_call = prog.functions[0].blocks[0]
.ops
.iter()
.any(|op| matches!(op, IrOp::Call(..)));
assert!(!has_call, "call to trivial function should be removed");
}