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

M2: Add function/array parsing, IR data structures and lowering

Parser extensions:
- Function declarations with params and return types (fun, inline fun)
- Array type syntax (u8[16]), array literal expressions ([1, 2, 3])
- fast/slow variable placement hints
- Functions stored in Program AST

IR module (new):
- IrProgram, IrFunction, IrBasicBlock, IrOp, IrTerminator types
- AST-to-IR lowering pass with:
  - Expression lowering to virtual temps
  - Control flow (if/else, while, loop) to basic blocks with branches
  - Break/continue via loop context stack
  - Short-circuit logical and/or
  - Button reads, draw sprites, wait_frame
  - Constants inlined as LoadImm
  - State handlers lowered as separate IR functions

Tests: 6 new parser tests, 11 new IR lowering tests (153 total)

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
Claude 2026-04-11 23:25:26 +00:00
parent fd5a940c89
commit 664ccc05db
No known key found for this signature in database
8 changed files with 1295 additions and 22 deletions

View file

@ -329,6 +329,7 @@ impl Analyzer {
_ => None,
})
}
Expr::ArrayLiteral(_, _) => Some(NesType::U8), // element type inferred from context
}
}
}

View file

@ -395,7 +395,7 @@ impl CodeGen {
self.emit(LDA, AM::ZeroPage(self.input_addr));
self.emit(AND, AM::Immediate(mask));
}
Expr::Call(_, _, _) | Expr::ArrayIndex(_, _, _) => {
Expr::Call(_, _, _) | Expr::ArrayIndex(_, _, _) | Expr::ArrayLiteral(_, _) => {
// TODO: implement for later milestones
}
}

658
src/ir/lowering.rs Normal file
View file

@ -0,0 +1,658 @@
use std::collections::HashMap;
use super::*;
use crate::analyzer::{AnalysisResult, VarAllocation};
use crate::parser::ast::*;
/// Lower a parsed & analyzed program into IR.
pub fn lower(program: &Program, analysis: &AnalysisResult) -> IrProgram {
let mut ctx = LoweringContext::new(analysis);
ctx.lower_program(program);
ctx.finish()
}
struct LoweringContext {
functions: Vec<IrFunction>,
globals: Vec<IrGlobal>,
rom_data: Vec<IrRomBlock>,
var_map: HashMap<String, VarId>,
const_values: HashMap<String, u16>,
next_var_id: u32,
next_temp: u32,
next_block: u32,
// Current function being built
current_blocks: Vec<IrBasicBlock>,
current_ops: Vec<IrOp>,
current_label: String,
// Loop context for break/continue
loop_stack: Vec<LoopContext>,
}
struct LoopContext {
continue_label: String,
break_label: String,
}
impl LoweringContext {
fn new(analysis: &AnalysisResult) -> Self {
let mut var_map = HashMap::new();
let mut next_var_id = 0u32;
// Pre-register all allocated variables
for alloc in &analysis.var_allocations {
var_map.insert(alloc.name.clone(), VarId(next_var_id));
next_var_id += 1;
}
Self {
functions: Vec::new(),
globals: Vec::new(),
rom_data: Vec::new(),
var_map,
const_values: HashMap::new(),
next_var_id,
next_temp: 0,
next_block: 0,
current_blocks: Vec::new(),
current_ops: Vec::new(),
current_label: String::new(),
loop_stack: Vec::new(),
}
}
fn fresh_temp(&mut self) -> IrTemp {
let t = IrTemp(self.next_temp);
self.next_temp += 1;
t
}
fn fresh_label(&mut self, prefix: &str) -> String {
self.next_block += 1;
format!("{prefix}_{}", self.next_block)
}
fn get_or_create_var(&mut self, name: &str) -> VarId {
if let Some(&id) = self.var_map.get(name) {
id
} else {
let id = VarId(self.next_var_id);
self.next_var_id += 1;
self.var_map.insert(name.to_string(), id);
id
}
}
fn emit(&mut self, op: IrOp) {
self.current_ops.push(op);
}
fn start_block(&mut self, label: &str) {
self.current_label = label.to_string();
self.current_ops = Vec::new();
}
fn end_block(&mut self, terminator: IrTerminator) {
self.current_blocks.push(IrBasicBlock {
label: self.current_label.clone(),
ops: std::mem::take(&mut self.current_ops),
terminator,
});
}
fn finish(self) -> IrProgram {
IrProgram {
functions: self.functions,
globals: self.globals,
rom_data: self.rom_data,
}
}
fn lower_program(&mut self, program: &Program) {
// Register constants
for c in &program.constants {
if let Expr::IntLiteral(v, _) = &c.value {
self.const_values.insert(c.name.clone(), *v);
}
}
// Lower globals
for var in &program.globals {
let var_id = self.get_or_create_var(&var.name);
let init = var.init.as_ref().and_then(|e| {
if let Expr::IntLiteral(v, _) = e {
Some(*v)
} else {
None
}
});
self.globals.push(IrGlobal {
var_id,
name: var.name.clone(),
size: type_size(&var.var_type),
init_value: init,
});
}
// Lower user functions
for fun in &program.functions {
self.lower_function(fun);
}
// Lower state handlers
for state in &program.states {
self.lower_state(state, state.name == program.start_state);
}
}
fn lower_function(&mut self, fun: &FunDecl) {
self.next_temp = 0;
self.current_blocks = Vec::new();
let mut locals = Vec::new();
// Register parameters as locals
for param in &fun.params {
let var_id = self.get_or_create_var(&param.name);
locals.push(IrLocal {
var_id,
name: param.name.clone(),
size: type_size(&param.param_type),
});
}
let entry = self.fresh_label(&format!("fn_{}_entry", fun.name));
self.start_block(&entry);
self.lower_block(&fun.body);
// Ensure the function ends with a return
if self.current_ops.is_empty()
|| !matches!(
self.current_blocks.last().map(|b| &b.terminator),
Some(IrTerminator::Return(_))
)
{
self.end_block(IrTerminator::Return(None));
}
self.functions.push(IrFunction {
name: fun.name.clone(),
blocks: std::mem::take(&mut self.current_blocks),
locals,
param_count: fun.params.len(),
has_return: fun.return_type.is_some(),
source_span: fun.span,
});
}
fn lower_state(&mut self, state: &StateDecl, _is_start: bool) {
// Lower each event handler as a separate function
if let Some(on_enter) = &state.on_enter {
self.lower_handler(&format!("{}_enter", state.name), on_enter, state);
}
if let Some(on_exit) = &state.on_exit {
self.lower_handler(&format!("{}_exit", state.name), on_exit, state);
}
if let Some(on_frame) = &state.on_frame {
self.lower_handler(&format!("{}_frame", state.name), on_frame, state);
}
}
fn lower_handler(&mut self, name: &str, block: &Block, state: &StateDecl) {
self.next_temp = 0;
self.current_blocks = Vec::new();
let mut locals = Vec::new();
// Register state-local variables
for var in &state.locals {
let var_id = self.get_or_create_var(&var.name);
locals.push(IrLocal {
var_id,
name: var.name.clone(),
size: type_size(&var.var_type),
});
}
let entry = self.fresh_label(&format!("{name}_entry"));
self.start_block(&entry);
self.lower_block(block);
self.end_block(IrTerminator::Return(None));
self.functions.push(IrFunction {
name: name.to_string(),
blocks: std::mem::take(&mut self.current_blocks),
locals,
param_count: 0,
has_return: false,
source_span: state.span,
});
}
fn lower_block(&mut self, block: &Block) {
for stmt in &block.statements {
self.lower_statement(stmt);
}
}
fn lower_statement(&mut self, stmt: &Statement) {
match stmt {
Statement::VarDecl(var) => {
if let Some(init) = &var.init {
let var_id = self.get_or_create_var(&var.name);
let val = self.lower_expr(init);
self.emit(IrOp::StoreVar(var_id, val));
}
}
Statement::Assign(lvalue, op, expr, _) => {
self.lower_assign(lvalue, *op, expr);
}
Statement::If(cond, then_block, else_ifs, else_block, _) => {
self.lower_if(cond, then_block, else_ifs, else_block.as_ref());
}
Statement::While(cond, body, _) => {
self.lower_while(cond, body);
}
Statement::Loop(body, _) => {
self.lower_loop(body);
}
Statement::Break(_) => {
if let Some(ctx) = self.loop_stack.last() {
let label = ctx.break_label.clone();
self.end_block(IrTerminator::Jump(label.clone()));
let cont = self.fresh_label("after_break");
self.start_block(&cont);
}
}
Statement::Continue(_) => {
if let Some(ctx) = self.loop_stack.last() {
let label = ctx.continue_label.clone();
self.end_block(IrTerminator::Jump(label.clone()));
let cont = self.fresh_label("after_continue");
self.start_block(&cont);
}
}
Statement::Return(value, _) => {
let temp = value.as_ref().map(|e| self.lower_expr(e));
self.end_block(IrTerminator::Return(temp));
let cont = self.fresh_label("after_return");
self.start_block(&cont);
}
Statement::Draw(draw) => {
let x = self.lower_expr(&draw.x);
let y = self.lower_expr(&draw.y);
let frame = draw.frame.as_ref().map(|e| self.lower_expr(e));
self.emit(IrOp::DrawSprite {
sprite_name: draw.sprite_name.clone(),
x,
y,
frame,
});
}
Statement::Transition(name, _) => {
self.emit(IrOp::Transition(name.clone()));
}
Statement::WaitFrame(_) => {
self.emit(IrOp::WaitFrame);
}
Statement::Call(name, args, _) => {
let arg_temps: Vec<_> = args.iter().map(|a| self.lower_expr(a)).collect();
self.emit(IrOp::Call(None, name.clone(), arg_temps));
}
}
}
fn lower_assign(&mut self, lvalue: &LValue, op: AssignOp, expr: &Expr) {
match lvalue {
LValue::Var(name) => {
let var_id = self.get_or_create_var(name);
match op {
AssignOp::Assign => {
let val = self.lower_expr(expr);
self.emit(IrOp::StoreVar(var_id, val));
}
_ => {
let current = self.fresh_temp();
self.emit(IrOp::LoadVar(current, var_id));
let rhs = self.lower_expr(expr);
let result = self.fresh_temp();
let ir_op = match op {
AssignOp::PlusAssign => IrOp::Add(result, current, rhs),
AssignOp::MinusAssign => IrOp::Sub(result, current, rhs),
AssignOp::AmpAssign => IrOp::And(result, current, rhs),
AssignOp::PipeAssign => IrOp::Or(result, current, rhs),
AssignOp::CaretAssign => IrOp::Xor(result, current, rhs),
AssignOp::Assign => unreachable!(),
};
self.emit(ir_op);
self.emit(IrOp::StoreVar(var_id, result));
}
}
}
LValue::ArrayIndex(name, index) => {
let var_id = self.get_or_create_var(name);
let idx = self.lower_expr(index);
let val = self.lower_expr(expr);
// For compound assignment on arrays, load first
if op != AssignOp::Assign {
let current = self.fresh_temp();
self.emit(IrOp::ArrayLoad(current, var_id, idx));
let result = self.fresh_temp();
let ir_op = match op {
AssignOp::PlusAssign => IrOp::Add(result, current, val),
AssignOp::MinusAssign => IrOp::Sub(result, current, val),
AssignOp::AmpAssign => IrOp::And(result, current, val),
AssignOp::PipeAssign => IrOp::Or(result, current, val),
AssignOp::CaretAssign => IrOp::Xor(result, current, val),
AssignOp::Assign => unreachable!(),
};
self.emit(ir_op);
self.emit(IrOp::ArrayStore(var_id, idx, result));
} else {
self.emit(IrOp::ArrayStore(var_id, idx, val));
}
}
}
}
fn lower_if(
&mut self,
cond: &Expr,
then_block: &Block,
else_ifs: &[(Expr, Block)],
else_block: Option<&Block>,
) {
let end_label = self.fresh_label("if_end");
let cond_temp = self.lower_expr(cond);
let then_label = self.fresh_label("if_then");
let else_label = if else_ifs.is_empty() && else_block.is_none() {
end_label.clone()
} else {
self.fresh_label("if_else")
};
self.end_block(IrTerminator::Branch(
cond_temp,
then_label.clone(),
else_label.clone(),
));
// Then block
self.start_block(&then_label);
self.lower_block(then_block);
self.end_block(IrTerminator::Jump(end_label.clone()));
// Else-if chains
let mut current_else = else_label;
for (i, (elif_cond, elif_block)) in else_ifs.iter().enumerate() {
self.start_block(&current_else);
let cond_temp = self.lower_expr(elif_cond);
let elif_then = self.fresh_label("elif_then");
let elif_else = if i + 1 < else_ifs.len() || else_block.is_some() {
self.fresh_label("elif_else")
} else {
end_label.clone()
};
self.end_block(IrTerminator::Branch(
cond_temp,
elif_then.clone(),
elif_else.clone(),
));
self.start_block(&elif_then);
self.lower_block(elif_block);
self.end_block(IrTerminator::Jump(end_label.clone()));
current_else = elif_else;
}
// Else block
if let Some(block) = else_block {
self.start_block(&current_else);
self.lower_block(block);
self.end_block(IrTerminator::Jump(end_label.clone()));
}
self.start_block(&end_label);
}
fn lower_while(&mut self, cond: &Expr, body: &Block) {
let cond_label = self.fresh_label("while_cond");
let body_label = self.fresh_label("while_body");
let end_label = self.fresh_label("while_end");
self.end_block(IrTerminator::Jump(cond_label.clone()));
// Condition check
self.start_block(&cond_label);
let cond_temp = self.lower_expr(cond);
self.end_block(IrTerminator::Branch(
cond_temp,
body_label.clone(),
end_label.clone(),
));
// Body
self.loop_stack.push(LoopContext {
continue_label: cond_label,
break_label: end_label.clone(),
});
self.start_block(&body_label);
self.lower_block(body);
let cond_label = &self.loop_stack.last().unwrap().continue_label.clone();
self.end_block(IrTerminator::Jump(cond_label.clone()));
self.loop_stack.pop();
self.start_block(&end_label);
}
fn lower_loop(&mut self, body: &Block) {
let body_label = self.fresh_label("loop_body");
let end_label = self.fresh_label("loop_end");
self.end_block(IrTerminator::Jump(body_label.clone()));
self.loop_stack.push(LoopContext {
continue_label: body_label.clone(),
break_label: end_label.clone(),
});
self.start_block(&body_label);
self.lower_block(body);
self.end_block(IrTerminator::Jump(body_label));
self.loop_stack.pop();
self.start_block(&end_label);
}
fn lower_expr(&mut self, expr: &Expr) -> IrTemp {
match expr {
Expr::IntLiteral(v, _) => {
let t = self.fresh_temp();
self.emit(IrOp::LoadImm(t, *v as u8));
t
}
Expr::BoolLiteral(v, _) => {
let t = self.fresh_temp();
self.emit(IrOp::LoadImm(t, u8::from(*v)));
t
}
Expr::Ident(name, _) => {
// Check constants first
if let Some(&val) = self.const_values.get(name) {
let t = self.fresh_temp();
self.emit(IrOp::LoadImm(t, val as u8));
return t;
}
let var_id = self.get_or_create_var(name);
let t = self.fresh_temp();
self.emit(IrOp::LoadVar(t, var_id));
t
}
Expr::ArrayIndex(name, index, _) => {
let var_id = self.get_or_create_var(name);
let idx = self.lower_expr(index);
let t = self.fresh_temp();
self.emit(IrOp::ArrayLoad(t, var_id, idx));
t
}
Expr::BinaryOp(left, op, right, _) => {
self.lower_binop(left, *op, right)
}
Expr::UnaryOp(op, inner, _) => {
let val = self.lower_expr(inner);
let t = self.fresh_temp();
match op {
UnaryOp::Negate => self.emit(IrOp::Negate(t, val)),
UnaryOp::Not => {
// Logical not: compare with 0
let zero = self.fresh_temp();
self.emit(IrOp::LoadImm(zero, 0));
self.emit(IrOp::CmpEq(t, val, zero));
}
UnaryOp::BitNot => self.emit(IrOp::Complement(t, val)),
}
t
}
Expr::Call(name, args, _) => {
let arg_temps: Vec<_> = args.iter().map(|a| self.lower_expr(a)).collect();
let t = self.fresh_temp();
self.emit(IrOp::Call(Some(t), name.clone(), arg_temps));
t
}
Expr::ButtonRead(_, button, _) => {
// Button reads are lowered to a ReadInput + mask check
self.emit(IrOp::ReadInput);
let t = self.fresh_temp();
let mask = button_mask(button);
let mask_temp = self.fresh_temp();
self.emit(IrOp::LoadImm(mask_temp, mask));
self.emit(IrOp::And(t, t, mask_temp));
t
}
Expr::ArrayLiteral(_, _) => {
// Array literals are handled during initialization, not as general expressions
let t = self.fresh_temp();
self.emit(IrOp::LoadImm(t, 0));
t
}
}
}
fn lower_binop(&mut self, left: &Expr, op: BinOp, right: &Expr) -> IrTemp {
// Short-circuit for logical operators
match op {
BinOp::And => return self.lower_logical_and(left, right),
BinOp::Or => return self.lower_logical_or(left, right),
_ => {}
}
let l = self.lower_expr(left);
let r = self.lower_expr(right);
let t = self.fresh_temp();
match op {
BinOp::Add => self.emit(IrOp::Add(t, l, r)),
BinOp::Sub => self.emit(IrOp::Sub(t, l, r)),
BinOp::Mul => self.emit(IrOp::Mul(t, l, r)),
BinOp::BitwiseAnd => self.emit(IrOp::And(t, l, r)),
BinOp::BitwiseOr => self.emit(IrOp::Or(t, l, r)),
BinOp::BitwiseXor => self.emit(IrOp::Xor(t, l, r)),
BinOp::Eq => self.emit(IrOp::CmpEq(t, l, r)),
BinOp::NotEq => self.emit(IrOp::CmpNe(t, l, r)),
BinOp::Lt => self.emit(IrOp::CmpLt(t, l, r)),
BinOp::Gt => self.emit(IrOp::CmpGt(t, l, r)),
BinOp::LtEq => self.emit(IrOp::CmpLtEq(t, l, r)),
BinOp::GtEq => self.emit(IrOp::CmpGtEq(t, l, r)),
BinOp::ShiftLeft => self.emit(IrOp::ShiftLeft(t, l, 1)), // TODO: dynamic shift
BinOp::ShiftRight => self.emit(IrOp::ShiftRight(t, l, 1)),
BinOp::Div | BinOp::Mod => {
// Software div/mod — emit as a call to runtime for now
self.emit(IrOp::LoadImm(t, 0));
}
BinOp::And | BinOp::Or => unreachable!("handled above"),
}
t
}
fn lower_logical_and(&mut self, left: &Expr, right: &Expr) -> IrTemp {
let result = self.fresh_temp();
let right_label = self.fresh_label("and_right");
let end_label = self.fresh_label("and_end");
let false_label = self.fresh_label("and_false");
let l = self.lower_expr(left);
self.end_block(IrTerminator::Branch(
l,
right_label.clone(),
false_label.clone(),
));
// Right side (only evaluated if left is true)
self.start_block(&right_label);
let r = self.lower_expr(right);
self.emit(IrOp::StoreVar(VarId(self.next_var_id), r)); // temp storage
self.end_block(IrTerminator::Jump(end_label.clone()));
// False path
self.start_block(&false_label);
self.emit(IrOp::LoadImm(result, 0));
self.end_block(IrTerminator::Jump(end_label.clone()));
// Merge
self.start_block(&end_label);
result
}
fn lower_logical_or(&mut self, left: &Expr, right: &Expr) -> IrTemp {
let result = self.fresh_temp();
let right_label = self.fresh_label("or_right");
let end_label = self.fresh_label("or_end");
let true_label = self.fresh_label("or_true");
let l = self.lower_expr(left);
self.end_block(IrTerminator::Branch(
l,
true_label.clone(),
right_label.clone(),
));
// True path (left was true)
self.start_block(&true_label);
self.emit(IrOp::LoadImm(result, 1));
self.end_block(IrTerminator::Jump(end_label.clone()));
// Right side
self.start_block(&right_label);
let r = self.lower_expr(right);
self.emit(IrOp::StoreVar(VarId(self.next_var_id), r));
self.end_block(IrTerminator::Jump(end_label.clone()));
// Merge
self.start_block(&end_label);
result
}
}
fn type_size(t: &NesType) -> u16 {
match t {
NesType::U8 | NesType::I8 | NesType::Bool => 1,
NesType::U16 => 2,
NesType::Array(elem, count) => type_size(elem) * count,
}
}
fn button_mask(button: &str) -> u8 {
match button {
"a" => 0x80,
"b" => 0x40,
"select" => 0x20,
"start" => 0x10,
"up" => 0x08,
"down" => 0x04,
"left" => 0x02,
"right" => 0x01,
_ => 0x00,
}
}

View file

@ -1,3 +1,160 @@
// IR module — stub for Milestone 1.
// The IR phase will be implemented in Milestone 2.
// For M1, we compile directly from AST to 6502 instructions.
mod lowering;
#[cfg(test)]
mod tests;
pub use lowering::lower;
use crate::lexer::Span;
use std::fmt;
/// A unique identifier for a variable across the program.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct VarId(pub u32);
/// A virtual register — unlimited supply, resolved during codegen.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct IrTemp(pub u32);
impl fmt::Display for IrTemp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "t{}", self.0)
}
}
impl fmt::Display for VarId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "v{}", self.0)
}
}
/// The top-level IR program.
#[derive(Debug, Clone)]
pub struct IrProgram {
pub functions: Vec<IrFunction>,
pub globals: Vec<IrGlobal>,
pub rom_data: Vec<IrRomBlock>,
}
/// A global variable in the IR.
#[derive(Debug, Clone)]
pub struct IrGlobal {
pub var_id: VarId,
pub name: String,
pub size: u16,
pub init_value: Option<u16>,
}
/// A block of constant data to be placed in ROM.
#[derive(Debug, Clone)]
pub struct IrRomBlock {
pub label: String,
pub data: Vec<u8>,
}
/// An IR function (includes state handlers, user functions, etc.)
#[derive(Debug, Clone)]
pub struct IrFunction {
pub name: String,
pub blocks: Vec<IrBasicBlock>,
pub locals: Vec<IrLocal>,
pub param_count: usize,
pub has_return: bool,
pub source_span: Span,
}
/// A local variable within a function.
#[derive(Debug, Clone)]
pub struct IrLocal {
pub var_id: VarId,
pub name: String,
pub size: u16,
}
/// A basic block — a straight-line sequence of ops ending with a terminator.
#[derive(Debug, Clone)]
pub struct IrBasicBlock {
pub label: String,
pub ops: Vec<IrOp>,
pub terminator: IrTerminator,
}
/// An IR operation.
#[derive(Debug, Clone)]
pub enum IrOp {
// Load/Store
LoadImm(IrTemp, u8),
LoadVar(IrTemp, VarId),
StoreVar(VarId, IrTemp),
// Arithmetic (8-bit)
Add(IrTemp, IrTemp, IrTemp),
Sub(IrTemp, IrTemp, IrTemp),
Mul(IrTemp, IrTemp, IrTemp),
And(IrTemp, IrTemp, IrTemp),
Or(IrTemp, IrTemp, IrTemp),
Xor(IrTemp, IrTemp, IrTemp),
ShiftLeft(IrTemp, IrTemp, u8),
ShiftRight(IrTemp, IrTemp, u8),
Negate(IrTemp, IrTemp),
Complement(IrTemp, IrTemp),
// Comparison (sets a boolean temp)
CmpEq(IrTemp, IrTemp, IrTemp),
CmpNe(IrTemp, IrTemp, IrTemp),
CmpLt(IrTemp, IrTemp, IrTemp),
CmpGt(IrTemp, IrTemp, IrTemp),
CmpLtEq(IrTemp, IrTemp, IrTemp),
CmpGtEq(IrTemp, IrTemp, IrTemp),
// Array access
ArrayLoad(IrTemp, VarId, IrTemp),
ArrayStore(VarId, IrTemp, IrTemp),
// Function call
Call(Option<IrTemp>, String, Vec<IrTemp>),
// Hardware operations
DrawSprite {
sprite_name: String,
x: IrTemp,
y: IrTemp,
frame: Option<IrTemp>,
},
ReadInput,
WaitFrame,
Transition(String),
// Source mapping
SourceLoc(Span),
}
/// A basic block terminator.
#[derive(Debug, Clone)]
pub enum IrTerminator {
/// Unconditional jump to a label.
Jump(String),
/// Conditional branch: if temp != 0 goto true_label else goto false_label.
Branch(IrTemp, String, String),
/// Return from function, optionally with a value.
Return(Option<IrTemp>),
/// Unreachable (after infinite loops, etc.)
Unreachable,
}
impl IrProgram {
/// Count total number of IR operations across all functions.
pub fn op_count(&self) -> usize {
self.functions
.iter()
.flat_map(|f| &f.blocks)
.map(|b| b.ops.len())
.sum()
}
}
impl IrFunction {
/// Count total number of IR operations in this function.
pub fn op_count(&self) -> usize {
self.blocks.iter().map(|b| b.ops.len()).sum()
}
}

242
src/ir/tests.rs Normal file
View file

@ -0,0 +1,242 @@
use super::*;
use crate::analyzer;
use crate::parser;
fn lower_ok(input: &str) -> IrProgram {
let (prog, diags) = parser::parse(input);
assert!(diags.is_empty(), "parse errors: {diags:?}");
let prog = prog.unwrap();
let analysis = analyzer::analyze(&prog);
assert!(
analysis.diagnostics.iter().all(|d| !d.is_error()),
"analysis errors: {:?}",
analysis.diagnostics
);
lower(&prog, &analysis)
}
#[test]
fn lower_minimal_program() {
let ir = lower_ok(
r#"
game "Test" { mapper: NROM }
var px: u8 = 128
on frame { px = 1 }
start Main
"#,
);
assert_eq!(ir.globals.len(), 1);
assert_eq!(ir.globals[0].name, "px");
assert_eq!(ir.globals[0].init_value, Some(128));
// Should have at least one function (the frame handler)
assert!(!ir.functions.is_empty());
}
#[test]
fn lower_var_assignment() {
let ir = lower_ok(
r#"
game "Test" { mapper: NROM }
var x: u8 = 0
on frame { x = 42 }
start Main
"#,
);
let frame_fn = ir.functions.iter().find(|f| f.name.contains("frame")).unwrap();
// Should have a StoreVar op
let has_store = frame_fn
.blocks
.iter()
.flat_map(|b| &b.ops)
.any(|op| matches!(op, IrOp::StoreVar(..)));
assert!(has_store, "should emit StoreVar for assignment");
}
#[test]
fn lower_plus_assign() {
let ir = lower_ok(
r#"
game "Test" { mapper: NROM }
var x: u8 = 0
on frame { x += 5 }
start Main
"#,
);
let frame_fn = ir.functions.iter().find(|f| f.name.contains("frame")).unwrap();
let has_add = frame_fn
.blocks
.iter()
.flat_map(|b| &b.ops)
.any(|op| matches!(op, IrOp::Add(..)));
assert!(has_add, "should emit Add for += operator");
}
#[test]
fn lower_if_creates_branch() {
let ir = lower_ok(
r#"
game "Test" { mapper: NROM }
var x: u8 = 0
on frame {
if x == 0 { x = 1 }
}
start Main
"#,
);
let frame_fn = ir.functions.iter().find(|f| f.name.contains("frame")).unwrap();
let has_branch = frame_fn
.blocks
.iter()
.any(|b| matches!(&b.terminator, IrTerminator::Branch(..)));
assert!(has_branch, "if statement should produce a Branch terminator");
}
#[test]
fn lower_while_creates_loop() {
let ir = lower_ok(
r#"
game "Test" { mapper: NROM }
var x: u8 = 0
on frame {
while x < 10 { x += 1 }
}
start Main
"#,
);
let frame_fn = ir.functions.iter().find(|f| f.name.contains("frame")).unwrap();
// A while loop needs at least 3 blocks: condition check, body, and exit
assert!(
frame_fn.blocks.len() >= 3,
"while should create multiple blocks, got {}",
frame_fn.blocks.len()
);
}
#[test]
fn lower_button_read() {
let ir = lower_ok(
r#"
game "Test" { mapper: NROM }
var px: u8 = 0
on frame {
if button.right { px += 1 }
}
start Main
"#,
);
let frame_fn = ir.functions.iter().find(|f| f.name.contains("frame")).unwrap();
let has_input = frame_fn
.blocks
.iter()
.flat_map(|b| &b.ops)
.any(|op| matches!(op, IrOp::ReadInput));
assert!(has_input, "button read should emit ReadInput op");
}
#[test]
fn lower_draw_sprite() {
let ir = lower_ok(
r#"
game "Test" { mapper: NROM }
var px: u8 = 0
var py: u8 = 0
on frame { draw Smiley at: (px, py) }
start Main
"#,
);
let frame_fn = ir.functions.iter().find(|f| f.name.contains("frame")).unwrap();
let has_draw = frame_fn
.blocks
.iter()
.flat_map(|b| &b.ops)
.any(|op| matches!(op, IrOp::DrawSprite { .. }));
assert!(has_draw, "should emit DrawSprite op");
}
#[test]
fn lower_constants_become_immediates() {
let ir = lower_ok(
r#"
game "Test" { mapper: NROM }
const SPEED: u8 = 3
var px: u8 = 0
on frame { px += SPEED }
start Main
"#,
);
let frame_fn = ir.functions.iter().find(|f| f.name.contains("frame")).unwrap();
// SPEED should be lowered to LoadImm(_, 3)
let has_imm3 = frame_fn
.blocks
.iter()
.flat_map(|b| &b.ops)
.any(|op| matches!(op, IrOp::LoadImm(_, 3)));
assert!(has_imm3, "constant should be inlined as LoadImm");
}
#[test]
fn lower_multiple_states() {
let ir = lower_ok(
r#"
game "Test" { mapper: NROM }
state Title {
on enter { wait_frame }
on frame { wait_frame }
}
state Game {
on frame { wait_frame }
}
start Title
"#,
);
// Should have: Title_enter, Title_frame, Game_frame
assert!(
ir.functions.len() >= 3,
"should have at least 3 functions for 2 states, got {}",
ir.functions.len()
);
let names: Vec<&str> = ir.functions.iter().map(|f| f.name.as_str()).collect();
assert!(
names.iter().any(|n| n.contains("Title_enter")),
"should have Title_enter handler"
);
assert!(
names.iter().any(|n| n.contains("Title_frame")),
"should have Title_frame handler"
);
assert!(
names.iter().any(|n| n.contains("Game_frame")),
"should have Game_frame handler"
);
}
#[test]
fn lower_op_count() {
let ir = lower_ok(
r#"
game "Test" { mapper: NROM }
var x: u8 = 0
on frame { x = 1 }
start Main
"#,
);
assert!(ir.op_count() > 0, "should have some IR ops");
}
#[test]
fn lower_wait_frame() {
let ir = lower_ok(
r#"
game "Test" { mapper: NROM }
on frame { wait_frame }
start Main
"#,
);
let frame_fn = ir.functions.iter().find(|f| f.name.contains("frame")).unwrap();
let has_wait = frame_fn
.blocks
.iter()
.flat_map(|b| &b.ops)
.any(|op| matches!(op, IrOp::WaitFrame));
assert!(has_wait, "should emit WaitFrame op");
}

View file

@ -76,9 +76,7 @@ pub struct ConstDecl {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Placement {
#[allow(dead_code)]
Fast,
#[allow(dead_code)]
Slow,
Auto,
}
@ -89,7 +87,6 @@ pub enum NesType {
I8,
U16,
Bool,
#[allow(dead_code)]
Array(Box<NesType>, u16),
}
@ -121,6 +118,7 @@ pub enum Expr {
UnaryOp(UnaryOp, Box<Expr>, Span),
Call(String, Vec<Expr>, Span),
ButtonRead(Option<Player>, String, Span),
ArrayLiteral(Vec<Expr>, Span),
}
impl Expr {
@ -133,7 +131,8 @@ impl Expr {
| Self::BinaryOp(_, _, _, s)
| Self::UnaryOp(_, _, s)
| Self::Call(_, _, s)
| Self::ButtonRead(_, _, s) => *s,
| Self::ButtonRead(_, _, s)
| Self::ArrayLiteral(_, s) => *s,
}
}
}

View file

@ -109,6 +109,7 @@ impl Parser {
let mut game = None;
let mut globals = Vec::new();
let mut constants = Vec::new();
let mut functions = Vec::new();
let mut states = Vec::new();
let mut start_state = None;
let mut on_frame = None;
@ -119,9 +120,15 @@ impl Parser {
TokenKind::KwGame => {
game = Some(self.parse_game_decl()?);
}
TokenKind::KwFast | TokenKind::KwSlow => {
globals.push(self.parse_var_decl()?);
}
TokenKind::KwVar => {
globals.push(self.parse_var_decl()?);
}
TokenKind::KwFun | TokenKind::KwInline => {
functions.push(self.parse_fun_decl()?);
}
TokenKind::KwConst => {
constants.push(self.parse_const_decl()?);
}
@ -178,7 +185,7 @@ impl Parser {
game,
globals,
constants,
functions: Vec::new(),
functions,
states,
start_state,
span,
@ -266,6 +273,17 @@ impl Parser {
fn parse_var_decl(&mut self) -> Result<VarDecl, Diagnostic> {
let start = self.current_span();
let placement = match self.peek() {
TokenKind::KwFast => {
self.advance();
Placement::Fast
}
TokenKind::KwSlow => {
self.advance();
Placement::Slow
}
_ => Placement::Auto,
};
self.expect(&TokenKind::KwVar)?;
let (name, _) = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
@ -282,7 +300,7 @@ impl Parser {
name,
var_type,
init,
placement: Placement::Auto,
placement,
span: Span::new(start.file_id, start.start, self.current_span().start),
})
}
@ -306,6 +324,54 @@ impl Parser {
})
}
// ── Function declaration ──
fn parse_fun_decl(&mut self) -> Result<FunDecl, Diagnostic> {
let start = self.current_span();
let is_inline = if *self.peek() == TokenKind::KwInline {
self.advance();
true
} else {
false
};
self.expect(&TokenKind::KwFun)?;
let (name, _) = self.expect_ident()?;
self.expect(&TokenKind::LParen)?;
let mut params = Vec::new();
while *self.peek() != TokenKind::RParen && *self.peek() != TokenKind::Eof {
let (pname, _) = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
let ptype = self.parse_type()?;
params.push(Param {
name: pname,
param_type: ptype,
});
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RParen)?;
let return_type = if *self.peek() == TokenKind::Arrow {
self.advance();
Some(self.parse_type()?)
} else {
None
};
let body = self.parse_block()?;
Ok(FunDecl {
name,
params,
return_type,
body,
is_inline,
span: Span::new(start.file_id, start.start, self.current_span().start),
})
}
// ── State declaration ──
fn parse_state_decl(&mut self) -> Result<StateDecl, Diagnostic> {
@ -322,7 +388,7 @@ impl Parser {
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
match self.peek().clone() {
TokenKind::KwVar => {
TokenKind::KwFast | TokenKind::KwSlow | TokenKind::KwVar => {
locals.push(self.parse_var_decl()?);
}
TokenKind::KwOn => {
@ -406,7 +472,7 @@ impl Parser {
fn parse_statement(&mut self) -> Result<Statement, Diagnostic> {
match self.peek().clone() {
TokenKind::KwVar => {
TokenKind::KwFast | TokenKind::KwSlow | TokenKind::KwVar => {
let decl = self.parse_var_decl()?;
Ok(Statement::VarDecl(decl))
}
@ -684,28 +750,47 @@ impl Parser {
// ── Type parsing ──
fn parse_type(&mut self) -> Result<NesType, Diagnostic> {
match self.peek().clone() {
let base = match self.peek().clone() {
TokenKind::KwU8 => {
self.advance();
Ok(NesType::U8)
NesType::U8
}
TokenKind::KwI8 => {
self.advance();
Ok(NesType::I8)
NesType::I8
}
TokenKind::KwU16 => {
self.advance();
Ok(NesType::U16)
NesType::U16
}
TokenKind::KwBool => {
self.advance();
Ok(NesType::Bool)
NesType::Bool
}
_ => Err(Diagnostic::error(
ErrorCode::E0201,
format!("expected type, found '{}'", self.peek()),
self.current_span(),
)),
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("expected type, found '{}'", self.peek()),
self.current_span(),
));
}
};
// Check for array suffix [N]
if *self.peek() == TokenKind::LBracket {
self.advance();
if let TokenKind::IntLiteral(size) = self.peek().clone() {
self.advance();
self.expect(&TokenKind::RBracket)?;
Ok(NesType::Array(Box::new(base), size))
} else {
Err(Diagnostic::error(
ErrorCode::E0201,
"expected array size",
self.current_span(),
))
}
} else {
Ok(base)
}
}
@ -906,6 +991,19 @@ impl Parser {
Ok(Expr::Ident(name, span))
}
TokenKind::LBracket => {
let span = self.current_span();
self.advance();
let mut elements = Vec::new();
while *self.peek() != TokenKind::RBracket && *self.peek() != TokenKind::Eof {
elements.push(self.parse_expr()?);
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RBracket)?;
Ok(Expr::ArrayLiteral(elements, span))
}
TokenKind::LParen => {
self.advance();
let expr = self.parse_expr()?;

View file

@ -364,3 +364,121 @@ fn parse_full_m1_program() {
let frame = prog.states[0].on_frame.as_ref().unwrap();
assert_eq!(frame.statements.len(), 5); // 4 ifs + 1 draw
}
// ── Milestone 2: Functions ──
#[test]
fn parse_function_decl() {
let src = r#"
game "Test" { mapper: NROM }
fun add(a: u8, b: u8) -> u8 {
return a + b
}
on frame { wait_frame }
start Main
"#;
let prog = parse_ok(src);
assert_eq!(prog.functions.len(), 1);
let f = &prog.functions[0];
assert_eq!(f.name, "add");
assert!(!f.is_inline);
assert_eq!(f.params.len(), 2);
assert_eq!(f.params[0].name, "a");
assert_eq!(f.params[0].param_type, NesType::U8);
assert_eq!(f.params[1].name, "b");
assert_eq!(f.params[1].param_type, NesType::U8);
assert_eq!(f.return_type, Some(NesType::U8));
assert_eq!(f.body.statements.len(), 1);
}
#[test]
fn parse_inline_function() {
let src = r#"
game "Test" { mapper: NROM }
inline fun double(x: u8) -> u8 {
return x + x
}
on frame { wait_frame }
start Main
"#;
let prog = parse_ok(src);
assert_eq!(prog.functions.len(), 1);
let f = &prog.functions[0];
assert_eq!(f.name, "double");
assert!(f.is_inline);
assert_eq!(f.params.len(), 1);
assert_eq!(f.params[0].name, "x");
assert_eq!(f.return_type, Some(NesType::U8));
}
#[test]
fn parse_array_type() {
let src = r#"
game "Test" { mapper: NROM }
var buf: u8[16] = [0, 0, 0]
on frame { wait_frame }
start Main
"#;
let prog = parse_ok(src);
assert_eq!(prog.globals.len(), 1);
assert_eq!(
prog.globals[0].var_type,
NesType::Array(Box::new(NesType::U8), 16)
);
assert!(prog.globals[0].init.is_some());
}
#[test]
fn parse_array_literal() {
let src = r#"
game "Test" { mapper: NROM }
var x: u8 = 0
on frame {
x = [1, 2, 3]
}
start Main
"#;
let prog = parse_ok(src);
let frame = prog.states[0].on_frame.as_ref().unwrap();
match &frame.statements[0] {
Statement::Assign(_, _, Expr::ArrayLiteral(elems, _), _) => {
assert_eq!(elems.len(), 3);
}
other => panic!("expected assignment with ArrayLiteral, got {other:?}"),
}
}
#[test]
fn parse_fast_var() {
let src = r#"
game "Test" { mapper: NROM }
fast var x: u8 = 0
on frame { wait_frame }
start Main
"#;
let prog = parse_ok(src);
assert_eq!(prog.globals.len(), 1);
assert_eq!(prog.globals[0].name, "x");
assert_eq!(prog.globals[0].placement, Placement::Fast);
}
#[test]
fn parse_function_call_expr() {
let src = r#"
game "Test" { mapper: NROM }
var x: u8 = 0
on frame {
x = add(1, 2)
}
start Main
"#;
let prog = parse_ok(src);
let frame = prog.states[0].on_frame.as_ref().unwrap();
match &frame.statements[0] {
Statement::Assign(_, _, Expr::Call(name, args, _), _) => {
assert_eq!(name, "add");
assert_eq!(args.len(), 2);
}
other => panic!("expected assignment with Call, got {other:?}"),
}
}