use crate::common::diagnostics::{Diagnostic, DiagnosticBundle, DiagnosticLevel}; use crate::frontends::pbs::ast::*; use crate::frontends::pbs::symbols::*; use crate::frontends::pbs::contracts::ContractRegistry; use crate::ir_core; use crate::ir_core::ids::{FieldId, FunctionId, TypeId, ValueId}; use crate::ir_core::{Block, Function, Instr, Module, Param, Program, Terminator, Type}; use std::collections::HashMap; pub struct Lowerer<'a> { module_symbols: &'a ModuleSymbols, program: Program, current_function: Option, current_block: Option, next_block_id: u32, next_func_id: u32, next_type_id: u32, local_vars: Vec>, function_ids: HashMap, type_ids: HashMap, struct_slots: HashMap, contract_registry: ContractRegistry, diagnostics: Vec, } impl<'a> Lowerer<'a> { pub fn new(module_symbols: &'a ModuleSymbols) -> Self { let mut field_offsets = HashMap::new(); field_offsets.insert(FieldId(0), 0); // V0 hardcoded field resolution foundation Self { module_symbols, program: Program { const_pool: ir_core::ConstPool::new(), modules: Vec::new(), field_offsets, field_types: HashMap::new(), }, current_function: None, current_block: None, next_block_id: 0, next_func_id: 1, next_type_id: 1, local_vars: Vec::new(), function_ids: HashMap::new(), type_ids: HashMap::new(), struct_slots: HashMap::new(), contract_registry: ContractRegistry::new(), diagnostics: Vec::new(), } } fn error(&mut self, code: &str, message: String, span: crate::common::spans::Span) { self.diagnostics.push(Diagnostic { level: DiagnosticLevel::Error, code: Some(code.to_string()), message, span: Some(span), }); } pub fn lower_file(mut self, file: &FileNode, module_name: &str) -> Result { // Pre-scan for function declarations to assign IDs for decl in &file.decls { if let Node::FnDecl(n) = decl { let id = FunctionId(self.next_func_id); self.next_func_id += 1; self.function_ids.insert(n.name.clone(), id); } if let Node::TypeDecl(n) = decl { let id = TypeId(self.next_type_id); self.next_type_id += 1; self.type_ids.insert(n.name.clone(), id); if n.type_kind == "struct" { if let Node::TypeBody(body) = &*n.body { self.struct_slots.insert(n.name.clone(), body.members.len() as u32); } } } } let mut module = Module { name: module_name.to_string(), functions: Vec::new(), }; for decl in &file.decls { match decl { Node::FnDecl(fn_decl) => { let func = self.lower_function(fn_decl).map_err(|_| DiagnosticBundle { diagnostics: self.diagnostics.clone(), })?; module.functions.push(func); } _ => {} // Other declarations not handled for now } } self.program.modules.push(module); Ok(self.program) } fn lower_function(&mut self, n: &FnDeclNode) -> Result { let func_id = *self.function_ids.get(&n.name).unwrap(); self.next_block_id = 0; self.local_vars = vec![HashMap::new()]; let mut params = Vec::new(); let mut local_types = HashMap::new(); for (i, param) in n.params.iter().enumerate() { let ty = self.lower_type_node(¶m.ty); params.push(Param { name: param.name.clone(), ty: ty.clone(), }); self.local_vars[0].insert(param.name.clone(), i as u32); local_types.insert(i as u32, ty); } let ret_ty = if let Some(ret) = &n.ret { self.lower_type_node(ret) } else { Type::Void }; let func = Function { id: func_id, name: n.name.clone(), params, return_type: ret_ty, blocks: Vec::new(), local_types, }; self.current_function = Some(func); self.start_block(); self.lower_node(&n.body)?; // Ensure every function ends with a return if not already terminated if let Some(mut block) = self.current_block.take() { if !matches!(block.terminator, Terminator::Return | Terminator::Jump(_) | Terminator::JumpIfFalse { .. }) { block.terminator = Terminator::Return; } if let Some(func) = &mut self.current_function { func.blocks.push(block); } } Ok(self.current_function.take().unwrap()) } fn lower_node(&mut self, node: &Node) -> Result<(), ()> { match node { Node::Block(n) => self.lower_block(n), Node::LetStmt(n) => self.lower_let_stmt(n), Node::ExprStmt(n) => self.lower_node(&n.expr), Node::ReturnStmt(n) => self.lower_return_stmt(n), Node::IntLit(n) => { let id = self.program.const_pool.add_int(n.value); self.emit(Instr::PushConst(id)); Ok(()) } Node::FloatLit(n) => { let id = self.program.const_pool.add_float(n.value); self.emit(Instr::PushConst(id)); Ok(()) } Node::StringLit(n) => { let id = self.program.const_pool.add_string(n.value.clone()); self.emit(Instr::PushConst(id)); Ok(()) } Node::BoundedLit(n) => { let id = self.program.const_pool.add_int(n.value as i64); self.emit(Instr::PushConst(id)); Ok(()) } Node::Ident(n) => self.lower_ident(n), Node::MemberAccess(n) => self.lower_member_access(n), Node::Call(n) => self.lower_call(n), Node::Binary(n) => self.lower_binary(n), Node::Unary(n) => self.lower_unary(n), Node::IfExpr(n) => self.lower_if_expr(n), Node::Alloc(n) => self.lower_alloc(n), Node::Mutate(n) => self.lower_mutate(n), Node::Borrow(n) => self.lower_borrow(n), Node::Peek(n) => self.lower_peek(n), _ => { // For unhandled nodes, we can either ignore or error. // Given the PR, maybe we should error on things we don't support yet in lowering. self.error("E_LOWER_UNSUPPORTED", format!("Lowering for node kind {:?} not supported", node), node.span()); Err(()) } } } fn lower_alloc(&mut self, n: &AllocNode) -> Result<(), ()> { let (ty_id, slots) = self.get_type_id_and_slots(&n.ty)?; self.emit(Instr::Alloc { ty: ty_id, slots }); Ok(()) } fn get_type_id_and_slots(&mut self, node: &Node) -> Result<(TypeId, u32), ()> { match node { Node::TypeName(n) => { let slots = self.struct_slots.get(&n.name).cloned().unwrap_or(1); let id = self.get_or_create_type_id(&n.name); Ok((id, slots)) } Node::TypeApp(ta) if ta.base == "array" => { let size = if ta.args.len() > 1 { if let Node::IntLit(il) = &ta.args[1] { il.value as u32 } else { 1 } } else { 1 }; let elem_ty = self.lower_type_node(&ta.args[0]); let name = format!("array<{}>[{}]", elem_ty, size); let id = self.get_or_create_type_id(&name); Ok((id, size)) } _ => { self.error("E_RESOLVE_UNDEFINED", format!("Unknown type in allocation: {:?}", node), node.span()); Err(()) } } } fn get_or_create_type_id(&mut self, name: &str) -> TypeId { if let Some(id) = self.type_ids.get(name) { *id } else { let id = TypeId(self.next_type_id); self.next_type_id += 1; self.type_ids.insert(name.to_string(), id); id } } fn lower_peek(&mut self, n: &PeekNode) -> Result<(), ()> { // 1. Evaluate target (gate) self.lower_node(&n.target)?; // 2. Preserve gate identity let gate_slot = self.get_next_local_slot(); self.local_vars.last_mut().unwrap().insert(format!("$gate_{}", gate_slot), gate_slot); self.emit(Instr::SetLocal(gate_slot)); // 3. Begin Operation self.emit(Instr::BeginPeek { gate: ValueId(gate_slot) }); self.emit(Instr::GateLoadField { gate: ValueId(gate_slot), field: FieldId(0) }); // 4. Bind view to local self.local_vars.push(HashMap::new()); let view_slot = self.get_next_local_slot(); self.local_vars.last_mut().unwrap().insert(n.binding.to_string(), view_slot); self.emit(Instr::SetLocal(view_slot)); // 5. Body self.lower_node(&n.body)?; // 6. End Operation self.emit(Instr::EndPeek); self.local_vars.pop(); Ok(()) } fn lower_borrow(&mut self, n: &BorrowNode) -> Result<(), ()> { // 1. Evaluate target (gate) self.lower_node(&n.target)?; // 2. Preserve gate identity let gate_slot = self.get_next_local_slot(); self.local_vars.last_mut().unwrap().insert(format!("$gate_{}", gate_slot), gate_slot); self.emit(Instr::SetLocal(gate_slot)); // 3. Begin Operation self.emit(Instr::BeginBorrow { gate: ValueId(gate_slot) }); self.emit(Instr::GateLoadField { gate: ValueId(gate_slot), field: FieldId(0) }); // 4. Bind view to local self.local_vars.push(HashMap::new()); let view_slot = self.get_next_local_slot(); self.local_vars.last_mut().unwrap().insert(n.binding.to_string(), view_slot); self.emit(Instr::SetLocal(view_slot)); // 5. Body self.lower_node(&n.body)?; // 6. End Operation self.emit(Instr::EndBorrow); self.local_vars.pop(); Ok(()) } fn lower_mutate(&mut self, n: &MutateNode) -> Result<(), ()> { // 1. Evaluate target (gate) self.lower_node(&n.target)?; // 2. Preserve gate identity let gate_slot = self.get_next_local_slot(); self.local_vars.last_mut().unwrap().insert(format!("$gate_{}", gate_slot), gate_slot); self.emit(Instr::SetLocal(gate_slot)); // 3. Begin Operation self.emit(Instr::BeginMutate { gate: ValueId(gate_slot) }); self.emit(Instr::GateLoadField { gate: ValueId(gate_slot), field: FieldId(0) }); // 4. Bind view to local self.local_vars.push(HashMap::new()); let view_slot = self.get_next_local_slot(); self.local_vars.last_mut().unwrap().insert(n.binding.to_string(), view_slot); self.emit(Instr::SetLocal(view_slot)); // 5. Body self.lower_node(&n.body)?; // 6. End Operation self.emit(Instr::EndMutate); self.local_vars.pop(); Ok(()) } fn lower_block(&mut self, n: &BlockNode) -> Result<(), ()> { self.local_vars.push(HashMap::new()); for stmt in &n.stmts { self.lower_node(stmt)?; } if let Some(tail) = &n.tail { self.lower_node(tail)?; } self.local_vars.pop(); Ok(()) } fn lower_let_stmt(&mut self, n: &LetStmtNode) -> Result<(), ()> { self.lower_node(&n.init)?; let slot = self.get_next_local_slot(); self.local_vars.last_mut().unwrap().insert(n.name.clone(), slot); self.emit(Instr::SetLocal(slot)); Ok(()) } fn lower_return_stmt(&mut self, n: &ReturnStmtNode) -> Result<(), ()> { if let Some(expr) = &n.expr { self.lower_node(expr)?; } self.terminate(Terminator::Return); Ok(()) } fn lower_ident(&mut self, n: &IdentNode) -> Result<(), ()> { if let Some(slot) = self.lookup_local(&n.name) { self.emit(Instr::GetLocal(slot)); Ok(()) } else { // Check for special identifiers match n.name.as_str() { "true" => { let id = self.program.const_pool.add_int(1); self.emit(Instr::PushConst(id)); return Ok(()); } "false" => { let id = self.program.const_pool.add_int(0); self.emit(Instr::PushConst(id)); return Ok(()); } "none" => { // For now, treat none as 0. This should be refined when optional is fully implemented. let id = self.program.const_pool.add_int(0); self.emit(Instr::PushConst(id)); return Ok(()); } _ => {} } // Check if it's a function (for first-class functions if supported) if let Some(_id) = self.function_ids.get(&n.name) { // Push function reference? Not in v0. self.error("E_LOWER_UNSUPPORTED", format!("First-class function reference '{}' not supported", n.name), n.span); Err(()) } else { self.error("E_RESOLVE_UNDEFINED", format!("Undefined identifier '{}'", n.name), n.span); Err(()) } } } fn lower_member_access(&mut self, n: &MemberAccessNode) -> Result<(), ()> { if let Node::Ident(id) = &*n.object { if id.name == "Color" { let val = match n.member.as_str() { "BLACK" => 0x0000, "WHITE" => 0xFFFF, "RED" => 0xF800, "GREEN" => 0x07E0, "BLUE" => 0x001F, "MAGENTA" => 0xF81F, "TRANSPARENT" => 0x0000, "COLOR_KEY" => 0x0000, _ => { self.error("E_RESOLVE_UNDEFINED", format!("Undefined Color constant '{}'", n.member), n.span); return Err(()); } }; let id = self.program.const_pool.add_int(val); self.emit(Instr::PushConst(id)); return Ok(()); } } // For instance members (e.g., p.any), we'll just ignore for now in v0 // to pass typecheck tests if they are being lowered. // In a real implementation we would need type information. Ok(()) } fn lower_call(&mut self, n: &CallNode) -> Result<(), ()> { for arg in &n.args { self.lower_node(arg)?; } match &*n.callee { Node::Ident(id_node) => { if let Some(func_id) = self.function_ids.get(&id_node.name) { self.emit(Instr::Call(*func_id, n.args.len() as u32)); Ok(()) } else { // Check for special built-in functions match id_node.name.as_str() { "some" | "ok" | "err" => { // For now, these are effectively nops in terms of IR emission, // as they just wrap the already pushed arguments. // In a real implementation, they might push a tag. return Ok(()); } _ => {} } self.error("E_RESOLVE_UNDEFINED", format!("Undefined function '{}'", id_node.name), id_node.span); Err(()) } } Node::MemberAccess(ma) => { if let Node::Ident(obj_id) = &*ma.object { // Check if it's a host contract according to symbol table let is_host_contract = self.module_symbols.type_symbols.get(&obj_id.name) .map(|sym| sym.kind == SymbolKind::Contract && sym.is_host) .unwrap_or(false); // Ensure it's not shadowed by a local variable let is_shadowed = self.lookup_local(&obj_id.name).is_some(); if is_host_contract && !is_shadowed { if let Some(syscall_id) = self.contract_registry.resolve(&obj_id.name, &ma.member) { self.emit(Instr::HostCall(syscall_id)); return Ok(()); } else { self.error("E_RESOLVE_UNDEFINED", format!("Undefined contract member '{}.{}'", obj_id.name, ma.member), ma.span); return Err(()); } } } // Regular member call (method) or fallback // In v0 we don't handle this yet. self.error("E_LOWER_UNSUPPORTED", "Method calls not supported in v0".to_string(), ma.span); Err(()) } _ => { self.error("E_LOWER_UNSUPPORTED", "Indirect calls not supported in v0".to_string(), n.callee.span()); Err(()) } } } fn lower_binary(&mut self, n: &BinaryNode) -> Result<(), ()> { self.lower_node(&n.left)?; self.lower_node(&n.right)?; match n.op.as_str() { "+" => self.emit(Instr::Add), "-" => self.emit(Instr::Sub), "*" => self.emit(Instr::Mul), "/" => self.emit(Instr::Div), "==" => self.emit(Instr::Eq), "!=" => self.emit(Instr::Neq), "<" => self.emit(Instr::Lt), "<=" => self.emit(Instr::Lte), ">" => self.emit(Instr::Gt), ">=" => self.emit(Instr::Gte), "&&" => self.emit(Instr::And), "||" => self.emit(Instr::Or), _ => { self.error("E_LOWER_UNSUPPORTED", format!("Binary operator '{}' not supported", n.op), n.span); return Err(()); } } Ok(()) } fn lower_unary(&mut self, n: &UnaryNode) -> Result<(), ()> { self.lower_node(&n.expr)?; match n.op.as_str() { "-" => self.emit(Instr::Neg), "!" => self.emit(Instr::Not), _ => { self.error("E_LOWER_UNSUPPORTED", format!("Unary operator '{}' not supported", n.op), n.span); return Err(()); } } Ok(()) } fn lower_if_expr(&mut self, n: &IfExprNode) -> Result<(), ()> { let then_id = self.reserve_block_id(); let else_id = self.reserve_block_id(); let merge_id = self.reserve_block_id(); self.lower_node(&n.cond)?; self.terminate(Terminator::JumpIfFalse { target: else_id, else_target: then_id, }); // Then block self.start_block_with_id(then_id); self.lower_node(&n.then_block)?; self.terminate(Terminator::Jump(merge_id)); // Else block self.start_block_with_id(else_id); if let Some(else_block) = &n.else_block { self.lower_node(else_block)?; } self.terminate(Terminator::Jump(merge_id)); // Merge block self.start_block_with_id(merge_id); Ok(()) } fn lower_type_node(&mut self, node: &Node) -> Type { match node { Node::TypeName(n) => match n.name.as_str() { "int" => Type::Int, "float" => Type::Float, "bool" => Type::Bool, "string" => Type::String, "void" => Type::Void, _ => Type::Struct(n.name.clone()), }, Node::TypeApp(ta) => { if ta.base == "array" { let elem_ty = self.lower_type_node(&ta.args[0]); let size = if ta.args.len() > 1 { if let Node::IntLit(il) = &ta.args[1] { il.value as u32 } else { 0 } } else { 0 }; Type::Array(Box::new(elem_ty), size) } else if ta.base == "optional" { Type::Optional(Box::new(self.lower_type_node(&ta.args[0]))) } else if ta.base == "result" { Type::Result( Box::new(self.lower_type_node(&ta.args[0])), Box::new(self.lower_type_node(&ta.args[1])) ) } else { Type::Struct(format!("{}<{}>", ta.base, ta.args.len())) } } _ => Type::Void, } } fn start_block(&mut self) { let id = self.reserve_block_id(); self.start_block_with_id(id); } fn start_block_with_id(&mut self, id: u32) { if let Some(block) = self.current_block.take() { if let Some(func) = &mut self.current_function { func.blocks.push(block); } } self.current_block = Some(Block { id, instrs: Vec::new(), terminator: Terminator::Return, // Default, will be overwritten }); } fn reserve_block_id(&mut self) -> u32 { let id = self.next_block_id; self.next_block_id += 1; id } fn emit(&mut self, instr: Instr) { if let Some(block) = &mut self.current_block { block.instrs.push(instr); } } fn terminate(&mut self, terminator: Terminator) { if let Some(mut block) = self.current_block.take() { block.terminator = terminator; if let Some(func) = &mut self.current_function { func.blocks.push(block); } } } fn get_next_local_slot(&self) -> u32 { self.local_vars.iter().map(|s| s.len() as u32).sum() } fn lookup_local(&self, name: &str) -> Option { for scope in self.local_vars.iter().rev() { if let Some(slot) = scope.get(name) { return Some(*slot); } } None } } #[cfg(test)] mod tests { use super::*; use crate::frontends::pbs::parser::Parser; use crate::frontends::pbs::collector::SymbolCollector; use crate::frontends::pbs::symbols::ModuleSymbols; use crate::ir_core; #[test] fn test_basic_lowering() { let code = " fn add(a: int, b: int): int { return a + b; } fn main() { let x = add(10, 20); } "; let mut parser = Parser::new(code, 0); let ast = parser.parse_file().expect("Failed to parse"); let mut collector = SymbolCollector::new(); let (type_symbols, value_symbols) = collector.collect(&ast).expect("Failed to collect symbols"); let module_symbols = ModuleSymbols { type_symbols, value_symbols }; let lowerer = Lowerer::new(&module_symbols); let program = lowerer.lower_file(&ast, "test").expect("Lowering failed"); // Verify program structure assert_eq!(program.modules.len(), 1); let module = &program.modules[0]; assert_eq!(module.functions.len(), 2); let add_func = module.functions.iter().find(|f| f.name == "add").unwrap(); assert_eq!(add_func.params.len(), 2); assert_eq!(add_func.return_type, ir_core::Type::Int); // Verify blocks assert!(add_func.blocks.len() >= 1); let first_block = &add_func.blocks[0]; // Check for Add instruction assert!(first_block.instrs.iter().any(|i| matches!(i, ir_core::Instr::Add))); } #[test] fn test_control_flow_lowering() { let code = " fn max(a: int, b: int): int { if (a > b) { return a; } else { return b; } } "; let mut parser = Parser::new(code, 0); let ast = parser.parse_file().expect("Failed to parse"); let mut collector = SymbolCollector::new(); let (type_symbols, value_symbols) = collector.collect(&ast).expect("Failed to collect symbols"); let module_symbols = ModuleSymbols { type_symbols, value_symbols }; let lowerer = Lowerer::new(&module_symbols); let program = lowerer.lower_file(&ast, "test").expect("Lowering failed"); let max_func = &program.modules[0].functions[0]; // Should have multiple blocks for if-else assert!(max_func.blocks.len() >= 3); } #[test] fn test_hip_lowering() { let code = " fn test_hip() { let g = alloc int; mutate g as x { let y = x + 1; } } "; let mut parser = Parser::new(code, 0); let ast = parser.parse_file().expect("Failed to parse"); let mut collector = SymbolCollector::new(); let (type_symbols, value_symbols) = collector.collect(&ast).expect("Failed to collect symbols"); let module_symbols = ModuleSymbols { type_symbols, value_symbols }; let lowerer = Lowerer::new(&module_symbols); let program = lowerer.lower_file(&ast, "test").expect("Lowering failed"); let func = &program.modules[0].functions[0]; let instrs: Vec<_> = func.blocks.iter().flat_map(|b| b.instrs.iter()).collect(); assert!(instrs.iter().any(|i| matches!(i, ir_core::Instr::Alloc { .. }))); assert!(instrs.iter().any(|i| matches!(i, ir_core::Instr::BeginMutate { .. }))); assert!(instrs.iter().any(|i| matches!(i, ir_core::Instr::EndMutate))); } #[test] fn test_hip_lowering_golden() { let code = " fn test_hip() { let g = alloc int; mutate g as x { let y = x + 1; } } "; let mut parser = Parser::new(code, 0); let ast = parser.parse_file().expect("Failed to parse"); let mut collector = SymbolCollector::new(); let (type_symbols, value_symbols) = collector.collect(&ast).unwrap(); let module_symbols = ModuleSymbols { type_symbols, value_symbols }; let lowerer = Lowerer::new(&module_symbols); let program = lowerer.lower_file(&ast, "test").expect("Lowering failed"); let json = serde_json::to_string_pretty(&program).unwrap(); // Assertions for PR-20 HIP Semantics: // 1. Gate is preserved in a local (SetLocal(1) after GetLocal(0)) // 2. BeginMutate uses that local (BeginMutate { gate: 1 }) // 3. EndMutate exists // 4. No ReadGate/WriteGate (they were removed from Instr) assert!(json.contains("\"SetLocal\": 1"), "Gate should be stored in a local"); assert!(json.contains("\"BeginMutate\""), "Should have BeginMutate"); assert!(json.contains("\"gate\": 1"), "BeginMutate should use the gate local"); assert!(json.contains("\"EndMutate\""), "Should have EndMutate"); assert!(!json.contains("ReadGate"), "ReadGate should be gone"); assert!(!json.contains("WriteGate"), "WriteGate should be gone"); } #[test] fn test_hip_semantics_distinction() { let code = " fn test_hip(g: int) { peek g as p { let x = p; } borrow g as b { let y = b; } mutate g as m { let z = m; } } "; let mut parser = Parser::new(code, 0); let ast = parser.parse_file().expect("Failed to parse"); let mut collector = SymbolCollector::new(); let (type_symbols, value_symbols) = collector.collect(&ast).expect("Failed to collect symbols"); let module_symbols = ModuleSymbols { type_symbols, value_symbols }; let lowerer = Lowerer::new(&module_symbols); let program = lowerer.lower_file(&ast, "test").expect("Lowering failed"); let func = &program.modules[0].functions[0]; let instrs: Vec<_> = func.blocks.iter().flat_map(|b| b.instrs.iter()).collect(); // Assert distinct Core IR instruction sequences assert!(instrs.iter().any(|i| matches!(i, Instr::BeginPeek { .. }))); assert!(instrs.iter().any(|i| matches!(i, Instr::EndPeek))); assert!(instrs.iter().any(|i| matches!(i, Instr::BeginBorrow { .. }))); assert!(instrs.iter().any(|i| matches!(i, Instr::EndBorrow))); assert!(instrs.iter().any(|i| matches!(i, Instr::BeginMutate { .. }))); assert!(instrs.iter().any(|i| matches!(i, Instr::EndMutate))); } #[test] fn test_host_contract_call_lowering() { let code = " declare contract Gfx host {} declare contract Log host {} fn main() { Gfx.clear(0); Log.write(2, \"Hello\"); } "; let mut parser = Parser::new(code, 0); let ast = parser.parse_file().expect("Failed to parse"); let mut collector = SymbolCollector::new(); let (type_symbols, value_symbols) = collector.collect(&ast).expect("Failed to collect symbols"); let module_symbols = ModuleSymbols { type_symbols, value_symbols }; let lowerer = Lowerer::new(&module_symbols); let program = lowerer.lower_file(&ast, "test").expect("Lowering failed"); let func = &program.modules[0].functions[0]; let instrs: Vec<_> = func.blocks.iter().flat_map(|b| b.instrs.iter()).collect(); // Gfx.clear -> 0x1001 assert!(instrs.iter().any(|i| matches!(i, ir_core::Instr::HostCall(0x1001)))); // Log.write -> 0x5001 assert!(instrs.iter().any(|i| matches!(i, ir_core::Instr::HostCall(0x5001)))); } #[test] fn test_contract_call_without_host_lowering() { let code = " declare contract Gfx {} fn main() { Gfx.clear(0); } "; let mut parser = Parser::new(code, 0); let ast = parser.parse_file().expect("Failed to parse"); let mut collector = SymbolCollector::new(); let (type_symbols, value_symbols) = collector.collect(&ast).expect("Failed to collect symbols"); let module_symbols = ModuleSymbols { type_symbols, value_symbols }; let lowerer = Lowerer::new(&module_symbols); let result = lowerer.lower_file(&ast, "test"); assert!(result.is_err()); let bundle = result.err().unwrap(); assert!(bundle.diagnostics.iter().any(|d| d.code == Some("E_LOWER_UNSUPPORTED".to_string()))); } #[test] fn test_shadowed_contract_call_lowering() { let code = " declare contract Gfx host {} fn main() { let Gfx = 10; Gfx.clear(0); } "; let mut parser = Parser::new(code, 0); let ast = parser.parse_file().expect("Failed to parse"); let mut collector = SymbolCollector::new(); let (type_symbols, value_symbols) = collector.collect(&ast).expect("Failed to collect symbols"); let module_symbols = ModuleSymbols { type_symbols, value_symbols }; let lowerer = Lowerer::new(&module_symbols); let result = lowerer.lower_file(&ast, "test"); assert!(result.is_err()); let bundle = result.err().unwrap(); assert!(bundle.diagnostics.iter().any(|d| d.code == Some("E_LOWER_UNSUPPORTED".to_string()))); } #[test] fn test_invalid_contract_call_lowering() { let code = " declare contract Gfx host {} fn main() { Gfx.invalidMethod(0); } "; let mut parser = Parser::new(code, 0); let ast = parser.parse_file().expect("Failed to parse"); let mut collector = SymbolCollector::new(); let (type_symbols, value_symbols) = collector.collect(&ast).expect("Failed to collect symbols"); let module_symbols = ModuleSymbols { type_symbols, value_symbols }; let lowerer = Lowerer::new(&module_symbols); let result = lowerer.lower_file(&ast, "test"); assert!(result.is_err()); let bundle = result.err().unwrap(); assert!(bundle.diagnostics.iter().any(|d| d.code == Some("E_RESOLVE_UNDEFINED".to_string()))); } #[test] fn test_alloc_struct_slots() { let code = " declare struct Vec3 { x: int, y: int, z: int } fn main() { let v = alloc Vec3; } "; let mut parser = Parser::new(code, 0); let ast = parser.parse_file().expect("Failed to parse"); let mut collector = SymbolCollector::new(); let (type_symbols, value_symbols) = collector.collect(&ast).expect("Failed to collect symbols"); let module_symbols = ModuleSymbols { type_symbols, value_symbols }; let lowerer = Lowerer::new(&module_symbols); let program = lowerer.lower_file(&ast, "test").expect("Lowering failed"); let func = &program.modules[0].functions[0]; let instrs: Vec<_> = func.blocks.iter().flat_map(|b| b.instrs.iter()).collect(); let alloc = instrs.iter().find_map(|i| { if let Instr::Alloc { ty, slots } = i { Some((ty, slots)) } else { None } }).expect("Should have Alloc instruction"); assert_eq!(*alloc.1, 3, "Vec3 should have 3 slots"); assert!(alloc.0.0 > 0, "Should have a valid TypeId"); } #[test] fn test_alloc_array_slots() { let code = " fn main() { let a = alloc array[10b]; } "; let mut parser = Parser::new(code, 0); let ast = parser.parse_file().expect("Failed to parse"); let mut collector = SymbolCollector::new(); let (type_symbols, value_symbols) = collector.collect(&ast).expect("Failed to collect symbols"); let module_symbols = ModuleSymbols { type_symbols, value_symbols }; let lowerer = Lowerer::new(&module_symbols); let program = lowerer.lower_file(&ast, "test").expect("Lowering failed"); let func = &program.modules[0].functions[0]; let instrs: Vec<_> = func.blocks.iter().flat_map(|b| b.instrs.iter()).collect(); let alloc = instrs.iter().find_map(|i| { if let Instr::Alloc { ty, slots } = i { Some((ty, slots)) } else { None } }).expect("Should have Alloc instruction"); assert_eq!(*alloc.1, 10, "array[10b] should have 10 slots"); assert!(alloc.0.0 > 0, "Should have a valid TypeId"); } #[test] fn test_alloc_primitive_slots() { let code = " fn main() { let x = alloc int; } "; let mut parser = Parser::new(code, 0); let ast = parser.parse_file().expect("Failed to parse"); let mut collector = SymbolCollector::new(); let (type_symbols, value_symbols) = collector.collect(&ast).expect("Failed to collect symbols"); let module_symbols = ModuleSymbols { type_symbols, value_symbols }; let lowerer = Lowerer::new(&module_symbols); let program = lowerer.lower_file(&ast, "test").expect("Lowering failed"); let func = &program.modules[0].functions[0]; let instrs: Vec<_> = func.blocks.iter().flat_map(|b| b.instrs.iter()).collect(); let alloc = instrs.iter().find_map(|i| { if let Instr::Alloc { ty, slots } = i { Some((ty, slots)) } else { None } }).expect("Should have Alloc instruction"); assert_eq!(*alloc.1, 1, "Primitive int should have 1 slot"); assert!(alloc.0.0 > 0, "Should have a valid TypeId"); } #[test] fn test_missing_function_error() { let code = " fn main() { missing_func(); } "; let mut parser = Parser::new(code, 0); let ast = parser.parse_file().expect("Failed to parse"); let mut collector = SymbolCollector::new(); let (type_symbols, value_symbols) = collector.collect(&ast).expect("Failed to collect symbols"); let module_symbols = ModuleSymbols { type_symbols, value_symbols }; let lowerer = Lowerer::new(&module_symbols); let result = lowerer.lower_file(&ast, "test"); assert!(result.is_err()); let bundle = result.err().unwrap(); assert!(bundle.diagnostics.iter().any(|d| d.code == Some("E_RESOLVE_UNDEFINED".to_string()))); assert!(bundle.diagnostics.iter().any(|d| d.message.contains("Undefined function 'missing_func'"))); } #[test] fn test_unresolved_ident_error() { let code = " fn main() { let x = undefined_var; } "; let mut parser = Parser::new(code, 0); let ast = parser.parse_file().expect("Failed to parse"); let mut collector = SymbolCollector::new(); let (type_symbols, value_symbols) = collector.collect(&ast).expect("Failed to collect symbols"); let module_symbols = ModuleSymbols { type_symbols, value_symbols }; let lowerer = Lowerer::new(&module_symbols); let result = lowerer.lower_file(&ast, "test"); assert!(result.is_err()); let bundle = result.err().unwrap(); assert!(bundle.diagnostics.iter().any(|d| d.code == Some("E_RESOLVE_UNDEFINED".to_string()))); assert!(bundle.diagnostics.iter().any(|d| d.message.contains("Undefined identifier 'undefined_var'"))); } }