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:
parent
fd5a940c89
commit
664ccc05db
8 changed files with 1295 additions and 22 deletions
|
|
@ -329,6 +329,7 @@ impl Analyzer {
|
|||
_ => None,
|
||||
})
|
||||
}
|
||||
Expr::ArrayLiteral(_, _) => Some(NesType::U8), // element type inferred from context
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
658
src/ir/lowering.rs
Normal 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(¶m.name);
|
||||
locals.push(IrLocal {
|
||||
var_id,
|
||||
name: param.name.clone(),
|
||||
size: type_size(¶m.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(¤t_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(¤t_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,
|
||||
}
|
||||
}
|
||||
163
src/ir/mod.rs
163
src/ir/mod.rs
|
|
@ -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
242
src/ir/tests.rs
Normal 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");
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()?;
|
||||
|
|
|
|||
|
|
@ -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:?}"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue