use crate::ast::*; use crate::error::CompileError; use crate::opcodes::OpCode; use crate::token::Span; use crate::ui_decl::{UiDeclaration, UiElement}; use crate::validator::VType; use crate::vm::ScriptVM; /// Where a named variable lives in the VM #[derive(Debug, Clone, Copy)] enum VarLoc { Local(u16, VType), Param(u16), StateScalar(u16, VType), InputBuffer(u8), OutputBuffer(u8), StateArray(u16, VType), // VType is the element type SampleSlot(u8), BuiltinSampleRate, BuiltinBufferSize, } struct Compiler { code: Vec, constants_f32: Vec, constants_i32: Vec, vars: Vec<(String, VarLoc)>, next_local: u16, scope_stack: Vec, // local count at scope entry draw_context: bool, // true when compiling a draw {} block } impl Compiler { fn new() -> Self { Self { code: Vec::new(), constants_f32: Vec::new(), constants_i32: Vec::new(), vars: Vec::new(), next_local: 0, scope_stack: Vec::new(), draw_context: false, } } fn emit(&mut self, op: OpCode) { self.code.push(op as u8); } fn emit_u8(&mut self, v: u8) { self.code.push(v); } fn emit_u16(&mut self, v: u16) { self.code.extend_from_slice(&v.to_le_bytes()); } fn emit_u32(&mut self, v: u32) { self.code.extend_from_slice(&v.to_le_bytes()); } /// Returns index into constants_f32 fn add_const_f32(&mut self, v: f32) -> u16 { // Reuse existing constant if possible for (i, &c) in self.constants_f32.iter().enumerate() { if c.to_bits() == v.to_bits() { return i as u16; } } let idx = self.constants_f32.len() as u16; self.constants_f32.push(v); idx } /// Returns index into constants_i32 fn add_const_i32(&mut self, v: i32) -> u16 { for (i, &c) in self.constants_i32.iter().enumerate() { if c == v { return i as u16; } } let idx = self.constants_i32.len() as u16; self.constants_i32.push(v); idx } fn push_scope(&mut self) { self.scope_stack.push(self.next_local); } fn pop_scope(&mut self) { let prev = self.scope_stack.pop().unwrap(); // Remove variables defined in this scope self.vars.retain(|(_, loc)| { if let VarLoc::Local(idx, _) = loc { *idx < prev } else { true } }); self.next_local = prev; } fn alloc_local(&mut self, name: String, ty: VType) -> u16 { let idx = self.next_local; self.next_local += 1; self.vars.push((name, VarLoc::Local(idx, ty))); idx } fn lookup(&self, name: &str) -> Option { self.vars.iter().rev().find(|(n, _)| n == name).map(|(_, l)| *l) } /// Emit a placeholder u32 and return the offset where it was written fn emit_jump_placeholder(&mut self, op: OpCode) -> usize { self.emit(op); let pos = self.code.len(); self.emit_u32(0); pos } /// Patch a previously emitted u32 placeholder fn patch_jump(&mut self, placeholder_pos: usize) { let target = self.code.len() as u32; let bytes = target.to_le_bytes(); self.code[placeholder_pos] = bytes[0]; self.code[placeholder_pos + 1] = bytes[1]; self.code[placeholder_pos + 2] = bytes[2]; self.code[placeholder_pos + 3] = bytes[3]; } fn compile_script(&mut self, script: &Script) -> Result<(), CompileError> { // Register built-in variables self.vars.push(("sample_rate".into(), VarLoc::BuiltinSampleRate)); self.vars.push(("buffer_size".into(), VarLoc::BuiltinBufferSize)); // Register inputs for (i, input) in script.inputs.iter().enumerate() { match input.signal { SignalKind::Audio | SignalKind::Cv => { self.vars.push((input.name.clone(), VarLoc::InputBuffer(i as u8))); } SignalKind::Midi => {} } } // Register outputs for (i, output) in script.outputs.iter().enumerate() { match output.signal { SignalKind::Audio | SignalKind::Cv => { self.vars.push((output.name.clone(), VarLoc::OutputBuffer(i as u8))); } SignalKind::Midi => {} } } self.register_params_and_state(script, true); // Compile process block for stmt in &script.process { self.compile_stmt(stmt)?; } self.emit(OpCode::Halt); Ok(()) } /// Compile the draw block into separate bytecode (for the DrawVM) fn compile_draw(&mut self, script: &Script) -> Result<(), CompileError> { self.draw_context = true; self.register_params_and_state(script, false); // Compile draw block if let Some(draw) = &script.draw { for stmt in draw { self.compile_stmt(stmt)?; } } self.emit(OpCode::Halt); Ok(()) } /// Register params and state variables. If `include_samples` is true, also registers sample slots. fn register_params_and_state(&mut self, script: &Script, include_samples: bool) { for (i, param) in script.params.iter().enumerate() { self.vars.push((param.name.clone(), VarLoc::Param(i as u16))); } let mut scalar_idx: u16 = 0; let mut array_idx: u16 = 0; let mut sample_idx: u8 = 0; for state in &script.state { match &state.ty { StateType::F32 => { self.vars.push((state.name.clone(), VarLoc::StateScalar(scalar_idx, VType::F32))); scalar_idx += 1; } StateType::Int => { self.vars.push((state.name.clone(), VarLoc::StateScalar(scalar_idx, VType::Int))); scalar_idx += 1; } StateType::Bool => { self.vars.push((state.name.clone(), VarLoc::StateScalar(scalar_idx, VType::Bool))); scalar_idx += 1; } StateType::ArrayF32(_) => { self.vars.push((state.name.clone(), VarLoc::StateArray(array_idx, VType::F32))); array_idx += 1; } StateType::ArrayInt(_) => { self.vars.push((state.name.clone(), VarLoc::StateArray(array_idx, VType::Int))); array_idx += 1; } StateType::Sample if include_samples => { self.vars.push((state.name.clone(), VarLoc::SampleSlot(sample_idx))); sample_idx += 1; } StateType::Sample => {} } } } fn compile_stmt(&mut self, stmt: &Stmt) -> Result<(), CompileError> { match stmt { Stmt::Let { name, init, .. } => { let ty = self.infer_type(init)?; self.compile_expr(init)?; let _idx = self.alloc_local(name.clone(), ty); self.emit(OpCode::StoreLocal); self.emit_u16(_idx); } Stmt::Assign { target, value, span } => { match target { LValue::Ident(name, _) => { let loc = self.lookup(name).ok_or_else(|| { CompileError::new(format!("Undefined variable: {}", name), *span) })?; self.compile_expr(value)?; match loc { VarLoc::Local(idx, _) => { self.emit(OpCode::StoreLocal); self.emit_u16(idx); } VarLoc::StateScalar(idx, _) => { self.emit(OpCode::StoreState); self.emit_u16(idx); } VarLoc::Param(idx) if self.draw_context => { self.emit(OpCode::StoreParam); self.emit_u16(idx); } _ => { return Err(CompileError::new( format!("Cannot assign to {}", name), *span, )); } } } LValue::Index(name, idx_expr, s) => { let loc = self.lookup(name).ok_or_else(|| { CompileError::new(format!("Undefined variable: {}", name), *s) })?; match loc { VarLoc::OutputBuffer(port) => { // StoreOutput: pops value then index self.compile_expr(idx_expr)?; self.compile_expr(value)?; self.emit(OpCode::StoreOutput); self.emit_u8(port); } VarLoc::StateArray(arr_id, _) => { // StoreStateArray: pops value then index self.compile_expr(idx_expr)?; self.compile_expr(value)?; self.emit(OpCode::StoreStateArray); self.emit_u16(arr_id); } _ => { return Err(CompileError::new( format!("Cannot index-assign to {}", name), *s, )); } } } } } Stmt::If { cond, then_block, else_block, .. } => { self.compile_expr(cond)?; if let Some(else_b) = else_block { // JumpIfFalse -> else let else_jump = self.emit_jump_placeholder(OpCode::JumpIfFalse); self.push_scope(); self.compile_block(then_block)?; self.pop_scope(); // Jump -> end (skip else) let end_jump = self.emit_jump_placeholder(OpCode::Jump); self.patch_jump(else_jump); self.push_scope(); self.compile_block(else_b)?; self.pop_scope(); self.patch_jump(end_jump); } else { let end_jump = self.emit_jump_placeholder(OpCode::JumpIfFalse); self.push_scope(); self.compile_block(then_block)?; self.pop_scope(); self.patch_jump(end_jump); } } Stmt::For { var, end, body, span: _ } => { // Allocate loop variable as local self.push_scope(); let loop_var = self.alloc_local(var.clone(), VType::Int); // Initialize loop var to 0 let zero_idx = self.add_const_i32(0); self.emit(OpCode::PushI32); self.emit_u16(zero_idx); self.emit(OpCode::StoreLocal); self.emit_u16(loop_var); // Loop start: check condition (i < end) let loop_start = self.code.len(); self.emit(OpCode::LoadLocal); self.emit_u16(loop_var); self.compile_expr(end)?; self.emit(OpCode::LtI); let exit_jump = self.emit_jump_placeholder(OpCode::JumpIfFalse); // Body self.compile_block(body)?; // Increment loop var self.emit(OpCode::LoadLocal); self.emit_u16(loop_var); let one_idx = self.add_const_i32(1); self.emit(OpCode::PushI32); self.emit_u16(one_idx); self.emit(OpCode::AddI); self.emit(OpCode::StoreLocal); self.emit_u16(loop_var); // Jump back to loop start self.emit(OpCode::Jump); self.emit_u32(loop_start as u32); // Patch exit self.patch_jump(exit_jump); self.pop_scope(); } Stmt::ExprStmt(expr) => { let is_void = self.is_void_call(expr); self.compile_expr(expr)?; if !is_void { self.emit(OpCode::Pop); } } } Ok(()) } fn compile_block(&mut self, block: &[Stmt]) -> Result<(), CompileError> { for stmt in block { self.compile_stmt(stmt)?; } Ok(()) } fn compile_expr(&mut self, expr: &Expr) -> Result<(), CompileError> { match expr { Expr::FloatLit(v, _) => { let idx = self.add_const_f32(*v); self.emit(OpCode::PushF32); self.emit_u16(idx); } Expr::IntLit(v, _) => { let idx = self.add_const_i32(*v); self.emit(OpCode::PushI32); self.emit_u16(idx); } Expr::BoolLit(v, _) => { self.emit(OpCode::PushBool); self.emit_u8(if *v { 1 } else { 0 }); } Expr::Ident(name, span) => { let loc = self.lookup(name).ok_or_else(|| { CompileError::new(format!("Undefined variable: {}", name), *span) })?; match loc { VarLoc::Local(idx, _) => { self.emit(OpCode::LoadLocal); self.emit_u16(idx); } VarLoc::Param(idx) => { self.emit(OpCode::LoadParam); self.emit_u16(idx); } VarLoc::StateScalar(idx, _) => { self.emit(OpCode::LoadState); self.emit_u16(idx); } VarLoc::BuiltinSampleRate => { self.emit(OpCode::LoadSampleRate); } VarLoc::BuiltinBufferSize => { self.emit(OpCode::LoadBufferSize); } // Arrays/buffers/samples used bare (for len(), etc.) — handled by call codegen _ => {} } } Expr::BinOp(left, op, right, _span) => { let lt = self.infer_type(left)?; let rt = self.infer_type(right)?; self.compile_expr(left)?; self.compile_expr(right)?; match op { BinOp::Add => { if lt == VType::F32 || rt == VType::F32 { self.emit(OpCode::AddF); } else { self.emit(OpCode::AddI); } } BinOp::Sub => { if lt == VType::F32 || rt == VType::F32 { self.emit(OpCode::SubF); } else { self.emit(OpCode::SubI); } } BinOp::Mul => { if lt == VType::F32 || rt == VType::F32 { self.emit(OpCode::MulF); } else { self.emit(OpCode::MulI); } } BinOp::Div => { if lt == VType::F32 || rt == VType::F32 { self.emit(OpCode::DivF); } else { self.emit(OpCode::DivI); } } BinOp::Mod => { if lt == VType::F32 || rt == VType::F32 { self.emit(OpCode::ModF); } else { self.emit(OpCode::ModI); } } BinOp::Eq => { if lt == VType::F32 || rt == VType::F32 { self.emit(OpCode::EqF); } else if lt == VType::Int || rt == VType::Int { self.emit(OpCode::EqI); } else { // bool comparison: treat as int self.emit(OpCode::EqI); } } BinOp::Ne => { if lt == VType::F32 || rt == VType::F32 { self.emit(OpCode::NeF); } else { self.emit(OpCode::NeI); } } BinOp::Lt => { if lt == VType::F32 || rt == VType::F32 { self.emit(OpCode::LtF); } else { self.emit(OpCode::LtI); } } BinOp::Gt => { if lt == VType::F32 || rt == VType::F32 { self.emit(OpCode::GtF); } else { self.emit(OpCode::GtI); } } BinOp::Le => { if lt == VType::F32 || rt == VType::F32 { self.emit(OpCode::LeF); } else { self.emit(OpCode::LeI); } } BinOp::Ge => { if lt == VType::F32 || rt == VType::F32 { self.emit(OpCode::GeF); } else { self.emit(OpCode::GeI); } } BinOp::And => self.emit(OpCode::And), BinOp::Or => self.emit(OpCode::Or), } } Expr::UnaryOp(op, inner, _) => { let ty = self.infer_type(inner)?; self.compile_expr(inner)?; match op { UnaryOp::Neg => { if ty == VType::F32 { self.emit(OpCode::NegF); } else { self.emit(OpCode::NegI); } } UnaryOp::Not => self.emit(OpCode::Not), } } Expr::Cast(kind, inner, _) => { self.compile_expr(inner)?; match kind { CastKind::ToInt => self.emit(OpCode::F32ToI32), CastKind::ToFloat => self.emit(OpCode::I32ToF32), } } Expr::Index(base, idx, span) => { // base must be an Ident referencing an array/buffer if let Expr::Ident(name, _) = base.as_ref() { let loc = self.lookup(name).ok_or_else(|| { CompileError::new(format!("Undefined variable: {}", name), *span) })?; match loc { VarLoc::InputBuffer(port) => { self.compile_expr(idx)?; self.emit(OpCode::LoadInput); self.emit_u8(port); } VarLoc::OutputBuffer(port) => { self.compile_expr(idx)?; self.emit(OpCode::LoadInput); // Reading from output buffer — use same port but from outputs // Actually outputs aren't readable in the VM. This would be // an error in practice, but the validator should catch it. // For now, treat as input read (will read zeros). self.emit_u8(port); } VarLoc::StateArray(arr_id, _) => { self.compile_expr(idx)?; self.emit(OpCode::LoadStateArray); self.emit_u16(arr_id); } _ => { return Err(CompileError::new( format!("Cannot index variable: {}", name), *span, )); } } } else { return Err(CompileError::new("Index base must be an identifier", *span)); } } Expr::Call(name, args, span) => { self.compile_call(name, args, *span)?; } } Ok(()) } /// Returns true if the expression is a call to a void function (no return value). fn is_void_call(&self, expr: &Expr) -> bool { if let Expr::Call(name, _, _) = expr { matches!(name.as_str(), "fill_circle" | "stroke_circle" | "stroke_arc" | "line" | "fill_rect" | "stroke_rect" ) } else { false } } fn compile_call(&mut self, name: &str, args: &[Expr], span: Span) -> Result<(), CompileError> { match name { // 1-arg math → push arg, emit opcode "sin" => { self.compile_expr(&args[0])?; self.emit(OpCode::Sin); } "cos" => { self.compile_expr(&args[0])?; self.emit(OpCode::Cos); } "tan" => { self.compile_expr(&args[0])?; self.emit(OpCode::Tan); } "asin" => { self.compile_expr(&args[0])?; self.emit(OpCode::Asin); } "acos" => { self.compile_expr(&args[0])?; self.emit(OpCode::Acos); } "atan" => { self.compile_expr(&args[0])?; self.emit(OpCode::Atan); } "exp" => { self.compile_expr(&args[0])?; self.emit(OpCode::Exp); } "log" => { self.compile_expr(&args[0])?; self.emit(OpCode::Log); } "log2" => { self.compile_expr(&args[0])?; self.emit(OpCode::Log2); } "sqrt" => { self.compile_expr(&args[0])?; self.emit(OpCode::Sqrt); } "floor" => { self.compile_expr(&args[0])?; self.emit(OpCode::Floor); } "ceil" => { self.compile_expr(&args[0])?; self.emit(OpCode::Ceil); } "round" => { self.compile_expr(&args[0])?; self.emit(OpCode::Round); } "trunc" => { self.compile_expr(&args[0])?; self.emit(OpCode::Trunc); } "fract" => { self.compile_expr(&args[0])?; self.emit(OpCode::Fract); } "abs" => { self.compile_expr(&args[0])?; self.emit(OpCode::Abs); } "sign" => { self.compile_expr(&args[0])?; self.emit(OpCode::Sign); } // 2-arg math "atan2" => { self.compile_expr(&args[0])?; self.compile_expr(&args[1])?; self.emit(OpCode::Atan2); } "pow" => { self.compile_expr(&args[0])?; self.compile_expr(&args[1])?; self.emit(OpCode::Pow); } "min" => { self.compile_expr(&args[0])?; self.compile_expr(&args[1])?; self.emit(OpCode::Min); } "max" => { self.compile_expr(&args[0])?; self.compile_expr(&args[1])?; self.emit(OpCode::Max); } // 3-arg math "clamp" => { self.compile_expr(&args[0])?; self.compile_expr(&args[1])?; self.compile_expr(&args[2])?; self.emit(OpCode::Clamp); } "mix" => { self.compile_expr(&args[0])?; self.compile_expr(&args[1])?; self.compile_expr(&args[2])?; self.emit(OpCode::Mix); } "smoothstep" => { self.compile_expr(&args[0])?; self.compile_expr(&args[1])?; self.compile_expr(&args[2])?; self.emit(OpCode::Smoothstep); } // cv_or(value, default) — if value is NaN, use default "cv_or" => { // Compile: push value, check IsNan, if true use default else keep value // Strategy: push value, dup-like via local, IsNan, branch // Simpler: push value, push value again, IsNan, JumpIfFalse skip, Pop, push default, skip: // But we don't have Dup. Use a temp local instead. let temp = self.next_local; self.next_local += 1; self.compile_expr(&args[0])?; // Store to temp self.emit(OpCode::StoreLocal); self.emit_u16(temp); // Load and check NaN self.emit(OpCode::LoadLocal); self.emit_u16(temp); self.emit(OpCode::IsNan); let skip_default = self.emit_jump_placeholder(OpCode::JumpIfFalse); // NaN path: use default self.compile_expr(&args[1])?; let skip_end = self.emit_jump_placeholder(OpCode::Jump); // Not NaN path: use original value self.patch_jump(skip_default); self.emit(OpCode::LoadLocal); self.emit_u16(temp); self.patch_jump(skip_end); self.next_local -= 1; // release temp } // len(array) -> int "len" => { // Arg must be an ident referencing a state array or input/output buffer if let Expr::Ident(arr_name, s) = &args[0] { let loc = self.lookup(arr_name).ok_or_else(|| { CompileError::new(format!("Undefined variable: {}", arr_name), *s) })?; match loc { VarLoc::StateArray(arr_id, _) => { self.emit(OpCode::ArrayLen); self.emit_u16(arr_id); } VarLoc::InputBuffer(_) | VarLoc::OutputBuffer(_) => { // Buffer length is buffer_size (for CV) or buffer_size*2 (for audio) // We emit LoadBufferSize — scripts use buffer_size for iteration self.emit(OpCode::LoadBufferSize); } _ => { return Err(CompileError::new("len() argument must be an array", span)); } } } else { return Err(CompileError::new("len() argument must be an identifier", span)); } } // sample_len(sample) -> int "sample_len" => { if let Expr::Ident(sname, s) = &args[0] { let loc = self.lookup(sname).ok_or_else(|| { CompileError::new(format!("Undefined: {}", sname), *s) })?; if let VarLoc::SampleSlot(slot) = loc { self.emit(OpCode::SampleLen); self.emit_u8(slot); } else { return Err(CompileError::new("sample_len() requires a sample", span)); } } else { return Err(CompileError::new("sample_len() requires an identifier", span)); } } // sample_read(sample, index) -> f32 "sample_read" => { if let Expr::Ident(sname, s) = &args[0] { let loc = self.lookup(sname).ok_or_else(|| { CompileError::new(format!("Undefined: {}", sname), *s) })?; if let VarLoc::SampleSlot(slot) = loc { self.compile_expr(&args[1])?; self.emit(OpCode::SampleRead); self.emit_u8(slot); } else { return Err(CompileError::new("sample_read() requires a sample", span)); } } else { return Err(CompileError::new("sample_read() requires an identifier", span)); } } // sample_rate_of(sample) -> int "sample_rate_of" => { if let Expr::Ident(sname, s) = &args[0] { let loc = self.lookup(sname).ok_or_else(|| { CompileError::new(format!("Undefined: {}", sname), *s) })?; if let VarLoc::SampleSlot(slot) = loc { self.emit(OpCode::SampleRateOf); self.emit_u8(slot); } else { return Err(CompileError::new("sample_rate_of() requires a sample", span)); } } else { return Err(CompileError::new("sample_rate_of() requires an identifier", span)); } } // Draw builtins (only valid in draw context) "fill_circle" | "stroke_circle" | "stroke_arc" | "line" | "fill_rect" | "stroke_rect" | "mouse_x" | "mouse_y" | "mouse_down" if self.draw_context => { match name { "fill_circle" => { // fill_circle(cx, cy, r, color) for arg in args { self.compile_expr(arg)?; } self.emit(OpCode::DrawFillCircle); } "stroke_circle" => { // stroke_circle(cx, cy, r, color, width) for arg in args { self.compile_expr(arg)?; } self.emit(OpCode::DrawStrokeCircle); } "stroke_arc" => { // stroke_arc(cx, cy, r, start_deg, end_deg, color, width) for arg in args { self.compile_expr(arg)?; } self.emit(OpCode::DrawStrokeArc); } "line" => { // line(x1, y1, x2, y2, color, width) for arg in args { self.compile_expr(arg)?; } self.emit(OpCode::DrawLine); } "fill_rect" => { // fill_rect(x, y, w, h, color) for arg in args { self.compile_expr(arg)?; } self.emit(OpCode::DrawFillRect); } "stroke_rect" => { // stroke_rect(x, y, w, h, color, width) for arg in args { self.compile_expr(arg)?; } self.emit(OpCode::DrawStrokeRect); } "mouse_x" => { self.emit(OpCode::MouseX); } "mouse_y" => { self.emit(OpCode::MouseY); } "mouse_down" => { self.emit(OpCode::MouseDown); } _ => unreachable!(), } } _ => { return Err(CompileError::new(format!("Unknown function: {}", name), span)); } } Ok(()) } /// Infer the type of an expression (mirrors validator logic, needed for selecting typed opcodes) fn infer_type(&self, expr: &Expr) -> Result { match expr { Expr::FloatLit(_, _) => Ok(VType::F32), Expr::IntLit(_, _) => Ok(VType::Int), Expr::BoolLit(_, _) => Ok(VType::Bool), Expr::Ident(name, span) => { let loc = self.lookup(name).ok_or_else(|| { CompileError::new(format!("Undefined variable: {}", name), *span) })?; match loc { VarLoc::Local(_, ty) => Ok(ty), VarLoc::Param(_) => Ok(VType::F32), VarLoc::StateScalar(_, ty) => Ok(ty), VarLoc::InputBuffer(_) => Ok(VType::ArrayF32), VarLoc::OutputBuffer(_) => Ok(VType::ArrayF32), VarLoc::StateArray(_, elem_ty) => { if elem_ty == VType::Int { Ok(VType::ArrayInt) } else { Ok(VType::ArrayF32) } } VarLoc::SampleSlot(_) => Ok(VType::Sample), VarLoc::BuiltinSampleRate => Ok(VType::Int), VarLoc::BuiltinBufferSize => Ok(VType::Int), } } Expr::BinOp(left, op, right, _) => { let lt = self.infer_type(left)?; let rt = self.infer_type(right)?; match op { BinOp::And | BinOp::Or | BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Gt | BinOp::Le | BinOp::Ge => Ok(VType::Bool), _ => { if lt == VType::F32 || rt == VType::F32 { Ok(VType::F32) } else { Ok(VType::Int) } } } } Expr::UnaryOp(op, inner, _) => { match op { UnaryOp::Neg => self.infer_type(inner), UnaryOp::Not => Ok(VType::Bool), } } Expr::Cast(kind, _, _) => match kind { CastKind::ToInt => Ok(VType::Int), CastKind::ToFloat => Ok(VType::F32), }, Expr::Index(base, _, _) => { let base_ty = self.infer_type(base)?; match base_ty { VType::ArrayF32 => Ok(VType::F32), VType::ArrayInt => Ok(VType::Int), _ => Ok(VType::F32), // fallback } } Expr::Call(name, _, _) => { match name.as_str() { "len" | "sample_len" | "sample_rate_of" => Ok(VType::Int), "isnan" => Ok(VType::Bool), _ => Ok(VType::F32), // all math functions return f32 } } } } } /// Compile a validated AST into bytecode VM and UI declaration pub fn compile(script: &Script) -> Result<(ScriptVM, UiDeclaration, Option), CompileError> { let mut compiler = Compiler::new(); compiler.compile_script(script)?; // Collect state layout info let mut num_state_scalars = 0usize; let mut state_array_sizes = Vec::new(); let mut num_sample_slots = 0usize; for state in &script.state { match &state.ty { StateType::F32 | StateType::Int | StateType::Bool => { num_state_scalars += 1; } StateType::ArrayF32(sz) => state_array_sizes.push(*sz), StateType::ArrayInt(sz) => state_array_sizes.push(*sz), StateType::Sample => num_sample_slots += 1, } } let param_defaults: Vec = script.params.iter().map(|p| p.default).collect(); let vm = ScriptVM::new( compiler.code, compiler.constants_f32, compiler.constants_i32, script.params.len(), ¶m_defaults, num_state_scalars, &state_array_sizes, num_sample_slots, ); // Build UI declaration let ui_decl = if let Some(elements) = &script.ui { UiDeclaration { elements: elements.clone() } } else { // Auto-generate: sample pickers first, then all params let mut elements = Vec::new(); for state in &script.state { if state.ty == StateType::Sample { elements.push(UiElement::Sample(state.name.clone())); } } for param in &script.params { elements.push(UiElement::Param(param.name.clone())); } UiDeclaration { elements } }; // Compile draw block if present let draw_vm = if script.draw.is_some() { let mut draw_compiler = Compiler::new(); draw_compiler.compile_draw(script)?; Some(crate::vm::DrawVM::new( draw_compiler.code, draw_compiler.constants_f32, draw_compiler.constants_i32, script.params.len(), ¶m_defaults, num_state_scalars, &state_array_sizes, )) } else { None }; Ok((vm, ui_decl, draw_vm)) } #[cfg(test)] mod tests { use super::*; use crate::lexer::Lexer; use crate::parser::Parser; use crate::validator; fn compile_source(src: &str) -> Result<(ScriptVM, UiDeclaration), CompileError> { let mut lexer = Lexer::new(src); let tokens = lexer.tokenize()?; let mut parser = Parser::new(&tokens); let script = parser.parse()?; let validated = validator::validate(&script)?; let (vm, ui, _draw_vm) = compile(validated)?; Ok((vm, ui)) } #[test] fn test_passthrough() { let src = r#" name "Pass" category effect inputs { audio_in: audio } outputs { audio_out: audio } process { for i in 0..buffer_size { audio_out[i] = audio_in[i]; } } "#; let (mut vm, _) = compile_source(src).unwrap(); let input = vec![1.0f32, 2.0, 3.0, 4.0]; let mut output = vec![0.0f32; 4]; let inputs: Vec<&[f32]> = vec![&input]; let mut out_slice: Vec<&mut [f32]> = vec![&mut output]; vm.execute(&inputs, &mut out_slice, 44100, 4).unwrap(); assert_eq!(output, vec![1.0, 2.0, 3.0, 4.0]); } #[test] fn test_gain() { let src = r#" name "Gain" category effect inputs { audio_in: audio } outputs { audio_out: audio } params { gain: 0.5 [0.0, 1.0] "" } process { for i in 0..buffer_size { audio_out[i] = audio_in[i] * gain; } } "#; let (mut vm, _) = compile_source(src).unwrap(); let input = vec![1.0f32, 2.0, 3.0, 4.0]; let mut output = vec![0.0f32; 4]; let inputs: Vec<&[f32]> = vec![&input]; let mut out_slice: Vec<&mut [f32]> = vec![&mut output]; vm.execute(&inputs, &mut out_slice, 44100, 4).unwrap(); assert_eq!(output, vec![0.5, 1.0, 1.5, 2.0]); } #[test] fn test_state_array() { let src = r#" name "Delay" category effect inputs { audio_in: audio } outputs { audio_out: audio } state { buf: [8]f32 } process { for i in 0..buffer_size { audio_out[i] = buf[i]; buf[i] = audio_in[i]; } } "#; let (mut vm, _) = compile_source(src).unwrap(); // First call: output should be zeros (state initialized to 0), state gets input let input = vec![10.0f32, 20.0, 30.0, 40.0]; let mut output = vec![0.0f32; 4]; { let inputs: Vec<&[f32]> = vec![&input]; let mut out_slice: Vec<&mut [f32]> = vec![&mut output]; vm.execute(&inputs, &mut out_slice, 44100, 4).unwrap(); } assert_eq!(output, vec![0.0, 0.0, 0.0, 0.0]); // Second call: output should be previous input let input2 = vec![50.0f32, 60.0, 70.0, 80.0]; let mut output2 = vec![0.0f32; 4]; { let inputs: Vec<&[f32]> = vec![&input2]; let mut out_slice: Vec<&mut [f32]> = vec![&mut output2]; vm.execute(&inputs, &mut out_slice, 44100, 4).unwrap(); } assert_eq!(output2, vec![10.0, 20.0, 30.0, 40.0]); } #[test] fn test_if_else() { let src = r#" name "Gate" category effect inputs { audio_in: audio } outputs { audio_out: audio } params { threshold: 0.5 [0.0, 1.0] "" } process { for i in 0..buffer_size { if audio_in[i] >= threshold { audio_out[i] = audio_in[i]; } else { audio_out[i] = 0.0; } } } "#; let (mut vm, _) = compile_source(src).unwrap(); let input = vec![0.2f32, 0.8, 0.1, 0.9]; let mut output = vec![0.0f32; 4]; let inputs: Vec<&[f32]> = vec![&input]; let mut out_slice: Vec<&mut [f32]> = vec![&mut output]; vm.execute(&inputs, &mut out_slice, 44100, 4).unwrap(); assert_eq!(output, vec![0.0, 0.8, 0.0, 0.9]); } #[test] fn test_auto_ui() { let src = r#" name "Test" category utility params { gain: 1.0 [0.0, 2.0] "dB" } state { clip: sample } outputs { out: audio } process {} "#; let (_, ui) = compile_source(src).unwrap(); // Auto-generated: sample first, then params assert_eq!(ui.elements.len(), 2); assert!(matches!(&ui.elements[0], UiElement::Sample(n) if n == "clip")); assert!(matches!(&ui.elements[1], UiElement::Param(n) if n == "gain")); } #[test] fn test_draw_block() { let src = r#" name "Knob" category utility params { volume: 0.75 [0.0, 1.0] "" } outputs { out: audio } ui { canvas [80, 80] } draw { let cx = 40.0; let cy = 40.0; fill_circle(cx, cy, 35.0, 0x333333FF); let angle = volume * 270.0 - 135.0; stroke_arc(cx, cy, 30.0, -135.0, angle, 0x4488FFFF, 3.0); } process { for i in 0..buffer_size { out[i] = 0.0; } } "#; let mut lexer = Lexer::new(src); let tokens = lexer.tokenize().unwrap(); let mut parser = Parser::new(&tokens); let script = parser.parse().unwrap(); let validated = validator::validate(&script).unwrap(); let (_vm, _ui, draw_vm) = compile(validated).unwrap(); // Draw VM should exist let mut dvm = draw_vm.expect("draw_vm should be Some"); assert!(dvm.has_bytecode()); // Execute should succeed without stack errors dvm.execute().unwrap(); // Should have produced draw commands assert_eq!(dvm.draw_commands.len(), 2); // fill_circle + stroke_arc } }