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

IR lowering: constant expression folding

Constants can now reference other constants and use arithmetic /
bitwise / comparison operators. The lowering runs \`eval_const\` on
each \`ConstDecl\` and \`VarDecl\` initializer, threading the existing
\`const_values\` map for identifier lookups.

Examples that now work:

    const BASE: u8 = 10
    const OFFSET: u8 = BASE + 5       // 15
    const MASK: u8 = FLAG_A | FLAG_B  // bitwise-or of two earlier consts
    var counter: u8 = BASE            // initializer is now 10

Enum variants are registered before constants so a const like
\`const FIRST: u8 = VariantA\` also resolves at compile time.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
Claude 2026-04-12 16:48:29 +00:00
parent 0a3e830acf
commit 8f0e0635df
No known key found for this signature in database
2 changed files with 96 additions and 16 deletions

View file

@ -87,6 +87,53 @@ impl LoweringContext {
} }
} }
/// Try to evaluate an expression at compile time, using the
/// already-registered constants as operands. Returns `None` if
/// the expression references something that isn't known at this
/// point (e.g. a runtime variable) or contains an operator we
/// don't constant-fold. The result is a u16 to keep the same
/// range as the AST integer literal type.
fn eval_const(&self, expr: &Expr) -> Option<u16> {
match expr {
Expr::IntLiteral(v, _) => Some(*v),
Expr::BoolLiteral(b, _) => Some(u16::from(*b)),
Expr::Ident(name, _) => self.const_values.get(name).copied(),
Expr::BinaryOp(lhs, op, rhs, _) => {
let l = self.eval_const(lhs)?;
let r = self.eval_const(rhs)?;
match op {
BinOp::Add => Some(l.wrapping_add(r)),
BinOp::Sub => Some(l.wrapping_sub(r)),
BinOp::Mul => Some(l.wrapping_mul(r)),
BinOp::Div if r != 0 => Some(l / r),
BinOp::Mod if r != 0 => Some(l % r),
BinOp::BitwiseAnd => Some(l & r),
BinOp::BitwiseOr => Some(l | r),
BinOp::BitwiseXor => Some(l ^ r),
BinOp::ShiftLeft => Some(l.wrapping_shl(u32::from(r))),
BinOp::ShiftRight => Some(l.wrapping_shr(u32::from(r))),
BinOp::Eq => Some(u16::from(l == r)),
BinOp::NotEq => Some(u16::from(l != r)),
BinOp::Lt => Some(u16::from(l < r)),
BinOp::Gt => Some(u16::from(l > r)),
BinOp::LtEq => Some(u16::from(l <= r)),
BinOp::GtEq => Some(u16::from(l >= r)),
_ => None,
}
}
Expr::UnaryOp(op, inner, _) => {
let v = self.eval_const(inner)?;
match op {
UnaryOp::Negate => Some(v.wrapping_neg()),
UnaryOp::BitNot => Some(!v),
UnaryOp::Not => Some(u16::from(v == 0)),
}
}
Expr::Cast(inner, _, _) => self.eval_const(inner),
_ => None,
}
}
fn emit(&mut self, op: IrOp) { fn emit(&mut self, op: IrOp) {
self.current_ops.push(op); self.current_ops.push(op);
} }
@ -119,30 +166,26 @@ impl LoweringContext {
self.state_names = program.states.iter().map(|s| s.name.clone()).collect(); self.state_names = program.states.iter().map(|s| s.name.clone()).collect();
program.start_state.clone_into(&mut self.start_state); program.start_state.clone_into(&mut self.start_state);
// Register constants // Register enum variants first so constants that reference
for c in &program.constants { // them (e.g. `const FIRST: u8 = VariantA`) can resolve.
if let Expr::IntLiteral(v, _) = &c.value {
self.const_values.insert(c.name.clone(), *v);
}
}
// Register enum variants as constants (0, 1, 2, ... per enum)
for e in &program.enums { for e in &program.enums {
for (i, (variant, _)) in e.variants.iter().enumerate() { for (i, (variant, _)) in e.variants.iter().enumerate() {
self.const_values.insert(variant.clone(), i as u16); self.const_values.insert(variant.clone(), i as u16);
} }
} }
// Lower globals // Register constants with constant-evaluation. Each const
// may reference earlier constants.
for c in &program.constants {
if let Some(v) = self.eval_const(&c.value) {
self.const_values.insert(c.name.clone(), v);
}
}
// Lower globals. Initializers can be any constant expression.
for var in &program.globals { for var in &program.globals {
let var_id = self.get_or_create_var(&var.name); let var_id = self.get_or_create_var(&var.name);
let init = var.init.as_ref().and_then(|e| { let init = var.init.as_ref().and_then(|e| self.eval_const(e));
if let Expr::IntLiteral(v, _) = e {
Some(*v)
} else {
None
}
});
self.globals.push(IrGlobal { self.globals.push(IrGlobal {
var_id, var_id,
name: var.name.clone(), name: var.name.clone(),

View file

@ -205,6 +205,43 @@ fn lower_constants_become_immediates() {
assert!(has_imm3, "constant should be inlined as LoadImm"); assert!(has_imm3, "constant should be inlined as LoadImm");
} }
#[test]
fn lower_const_expressions_constant_fold() {
// Constants may reference earlier constants and use arithmetic.
// `B` resolves to `A + 3` = 8 at lowering time.
let ir = lower_ok(
r#"
game "Test" { mapper: NROM }
const A: u8 = 5
const B: u8 = A + 3
var x: u8 = B
on frame { wait_frame }
start Main
"#,
);
let x_global = ir.globals.iter().find(|g| g.name == "x").unwrap();
assert_eq!(x_global.init_value, Some(8));
}
#[test]
fn lower_const_bit_ops() {
// Bitwise constant folding should work for things like defining
// flags or masks based on other constants.
let ir = lower_ok(
r#"
game "Test" { mapper: NROM }
const FLAG_A: u8 = 1
const FLAG_B: u8 = 2
const BOTH: u8 = FLAG_A | FLAG_B
var x: u8 = BOTH
on frame { wait_frame }
start Main
"#,
);
let x_global = ir.globals.iter().find(|g| g.name == "x").unwrap();
assert_eq!(x_global.init_value, Some(3));
}
#[test] #[test]
fn lower_multiple_states() { fn lower_multiple_states() {
let ir = lower_ok( let ir = lower_ok(