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:
parent
058f7a87b6
commit
5434dda114
15 changed files with 865 additions and 13 deletions
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue