From d994005f8bd6dea526b5970b63ec65a59b288eb3 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 6 Feb 2026 21:11:04 +0000 Subject: [PATCH] fix structs --- .../src/backend/emit_bytecode.rs | 229 ++++++- .../src/frontends/pbs/lowering.rs | 275 ++++++++- .../src/frontends/pbs/parser.rs | 205 +++++-- .../src/frontends/pbs/resolver.rs | 8 +- .../src/frontends/pbs/typecheck.rs | 136 ++++- .../src/lowering/core_to_vm.rs | 13 +- crates/prometeu-compiler/src/sources.rs | 4 +- crates/prometeu/Cargo.toml | 3 +- docs/roadmaps/packer/Prometeu Packer.md | 564 ++++++++++++++++++ test-cartridges/canonical/golden/ast.json | 9 +- .../canonical/golden/program.disasm.txt | 195 +----- test-cartridges/canonical/golden/program.pbc | Bin 3727 -> 576 bytes .../canonical/src/main/modules/main.pbs | 72 +-- test-cartridges/test01/cartridge/program.pbc | Bin 4032 -> 2495 bytes 14 files changed, 1366 insertions(+), 347 deletions(-) create mode 100644 docs/roadmaps/packer/Prometeu Packer.md diff --git a/crates/prometeu-compiler/src/backend/emit_bytecode.rs b/crates/prometeu-compiler/src/backend/emit_bytecode.rs index 62c4cc0a..64d5083b 100644 --- a/crates/prometeu-compiler/src/backend/emit_bytecode.rs +++ b/crates/prometeu-compiler/src/backend/emit_bytecode.rs @@ -57,15 +57,10 @@ pub fn emit_module(module: &ir_vm::Module) -> Result { pub fn emit_fragments(module: &ir_vm::Module) -> Result { let mut emitter = BytecodeEmitter::new(); - - let mut mapped_const_ids = Vec::with_capacity(module.const_pool.constants.len()); - for val in &module.const_pool.constants { - mapped_const_ids.push(emitter.add_ir_constant(val)); - } let mut asm_instrs = Vec::new(); let mut ir_instr_map = Vec::new(); - let function_ranges = emitter.lower_instrs(module, &mut asm_instrs, &mut ir_instr_map, &mapped_const_ids)?; + let function_ranges = emitter.lower_instrs(module, &mut asm_instrs, &mut ir_instr_map)?; let pcs = BytecodeEmitter::calculate_pcs(&asm_instrs); let assemble_res = prometeu_bytecode::asm::assemble_with_unresolved(&asm_instrs).map_err(|e| anyhow!(e))?; @@ -155,11 +150,15 @@ impl BytecodeEmitter { &mut self, module: &'b ir_vm::Module, asm_instrs: &mut Vec, - ir_instr_map: &mut Vec>, - mapped_const_ids: &[u32] + ir_instr_map: &mut Vec> ) -> Result> { + // Cache to map VM IR const ids to emitted constant pool ids + let mut const_id_map: std::collections::HashMap = std::collections::HashMap::new(); + // Build a mapping from VM function id -> index into module.functions + let mut id_to_index = std::collections::HashMap::new(); let mut func_names = std::collections::HashMap::new(); - for func in &module.functions { + for (idx, func) in module.functions.iter().enumerate() { + id_to_index.insert(func.id, idx as u32); func_names.insert(func.id, func.name.clone()); } @@ -170,6 +169,8 @@ impl BytecodeEmitter { // Each function starts with a label for its entry point. asm_instrs.push(Asm::Label(function.name.clone())); ir_instr_map.push(None); + // Track an approximate stack height for this function + let mut stack_height: i32 = 0; for instr in &function.body { let op_start_idx = asm_instrs.len(); @@ -179,32 +180,55 @@ impl BytecodeEmitter { InstrKind::Nop => asm_instrs.push(Asm::Op(OpCode::Nop, vec![])), InstrKind::Halt => asm_instrs.push(Asm::Op(OpCode::Halt, vec![])), InstrKind::PushConst(id) => { - let mapped_id = mapped_const_ids[id.0 as usize]; + // Map VM const id to emitted const pool id on-demand + let mapped_id = if let Some(mid) = const_id_map.get(id) { + *mid + } else { + let idx = id.0 as usize; + let val = module + .const_pool + .constants + .get(idx) + .ok_or_else(|| anyhow!("Invalid const id {} (pool len {})", id.0, module.const_pool.constants.len()))?; + let mid = self.add_ir_constant(val); + const_id_map.insert(*id, mid); + mid + }; asm_instrs.push(Asm::Op(OpCode::PushConst, vec![Operand::U32(mapped_id)])); + stack_height += 1; } InstrKind::PushBounded(val) => { asm_instrs.push(Asm::Op(OpCode::PushBounded, vec![Operand::U32(*val)])); + stack_height += 1; } InstrKind::PushBool(v) => { asm_instrs.push(Asm::Op(OpCode::PushBool, vec![Operand::Bool(*v)])); + stack_height += 1; } InstrKind::PushNull => { asm_instrs.push(Asm::Op(OpCode::PushConst, vec![Operand::U32(0)])); + stack_height += 1; + } + InstrKind::Pop => { + asm_instrs.push(Asm::Op(OpCode::Pop, vec![])); + stack_height = (stack_height - 1).max(0); + } + InstrKind::Dup => { + asm_instrs.push(Asm::Op(OpCode::Dup, vec![])); + stack_height += 1; } - InstrKind::Pop => asm_instrs.push(Asm::Op(OpCode::Pop, vec![])), - InstrKind::Dup => asm_instrs.push(Asm::Op(OpCode::Dup, vec![])), InstrKind::Swap => asm_instrs.push(Asm::Op(OpCode::Swap, vec![])), - InstrKind::Add => asm_instrs.push(Asm::Op(OpCode::Add, vec![])), - InstrKind::Sub => asm_instrs.push(Asm::Op(OpCode::Sub, vec![])), - InstrKind::Mul => asm_instrs.push(Asm::Op(OpCode::Mul, vec![])), - InstrKind::Div => asm_instrs.push(Asm::Op(OpCode::Div, vec![])), - InstrKind::Neg => asm_instrs.push(Asm::Op(OpCode::Neg, vec![])), - InstrKind::Eq => asm_instrs.push(Asm::Op(OpCode::Eq, vec![])), - InstrKind::Neq => asm_instrs.push(Asm::Op(OpCode::Neq, vec![])), - InstrKind::Lt => asm_instrs.push(Asm::Op(OpCode::Lt, vec![])), - InstrKind::Gt => asm_instrs.push(Asm::Op(OpCode::Gt, vec![])), - InstrKind::Lte => asm_instrs.push(Asm::Op(OpCode::Lte, vec![])), - InstrKind::Gte => asm_instrs.push(Asm::Op(OpCode::Gte, vec![])), + InstrKind::Add => { asm_instrs.push(Asm::Op(OpCode::Add, vec![])); stack_height = (stack_height - 1).max(0); } + InstrKind::Sub => { asm_instrs.push(Asm::Op(OpCode::Sub, vec![])); stack_height = (stack_height - 1).max(0); } + InstrKind::Mul => { asm_instrs.push(Asm::Op(OpCode::Mul, vec![])); stack_height = (stack_height - 1).max(0); } + InstrKind::Div => { asm_instrs.push(Asm::Op(OpCode::Div, vec![])); stack_height = (stack_height - 1).max(0); } + InstrKind::Neg => { asm_instrs.push(Asm::Op(OpCode::Neg, vec![])); /* unary */ } + InstrKind::Eq => { asm_instrs.push(Asm::Op(OpCode::Eq, vec![])); stack_height = (stack_height - 1).max(0); } + InstrKind::Neq => { asm_instrs.push(Asm::Op(OpCode::Neq, vec![])); stack_height = (stack_height - 1).max(0); } + InstrKind::Lt => { asm_instrs.push(Asm::Op(OpCode::Lt, vec![])); stack_height = (stack_height - 1).max(0); } + InstrKind::Gt => { asm_instrs.push(Asm::Op(OpCode::Gt, vec![])); stack_height = (stack_height - 1).max(0); } + InstrKind::Lte => { asm_instrs.push(Asm::Op(OpCode::Lte, vec![])); stack_height = (stack_height - 1).max(0); } + InstrKind::Gte => { asm_instrs.push(Asm::Op(OpCode::Gte, vec![])); stack_height = (stack_height - 1).max(0); } InstrKind::And => asm_instrs.push(Asm::Op(OpCode::And, vec![])), InstrKind::Or => asm_instrs.push(Asm::Op(OpCode::Or, vec![])), InstrKind::Not => asm_instrs.push(Asm::Op(OpCode::Not, vec![])), @@ -215,46 +239,76 @@ impl BytecodeEmitter { InstrKind::Shr => asm_instrs.push(Asm::Op(OpCode::Shr, vec![])), InstrKind::LocalLoad { slot } => { asm_instrs.push(Asm::Op(OpCode::GetLocal, vec![Operand::U32(*slot)])); + stack_height += 1; } InstrKind::LocalStore { slot } => { asm_instrs.push(Asm::Op(OpCode::SetLocal, vec![Operand::U32(*slot)])); + stack_height = (stack_height - 1).max(0); } InstrKind::GetGlobal(slot) => { asm_instrs.push(Asm::Op(OpCode::GetGlobal, vec![Operand::U32(*slot)])); + stack_height += 1; } InstrKind::SetGlobal(slot) => { asm_instrs.push(Asm::Op(OpCode::SetGlobal, vec![Operand::U32(*slot)])); + stack_height = (stack_height - 1).max(0); } InstrKind::Jmp(label) => { asm_instrs.push(Asm::Op(OpCode::Jmp, vec![Operand::RelLabel(label.0.clone(), function.name.clone())])); } InstrKind::JmpIfFalse(label) => { asm_instrs.push(Asm::Op(OpCode::JmpIfFalse, vec![Operand::RelLabel(label.0.clone(), function.name.clone())])); + // VM consumes the condition for JmpIfFalse + stack_height = (stack_height - 1).max(0); } InstrKind::Label(label) => { asm_instrs.push(Asm::Label(label.0.clone())); + // Each labeled block in VM code is a fresh basic block. + // Our IR lowering (core_to_vm) assumes empty evaluation stack at + // block boundaries. Ensure the emitter's internal accounting + // matches that assumption to avoid inserting balancing Pops + // carried over from previous fallthrough paths. + stack_height = 0; } - InstrKind::Call { func_id, .. } => { - let name = func_names.get(func_id).ok_or_else(|| anyhow!("Undefined function ID: {:?}", func_id))?; - asm_instrs.push(Asm::Op(OpCode::Call, vec![Operand::Label(name.clone())])); + InstrKind::Call { func_id, arg_count } => { + // Translate call by function index within this module + if let Some(idx) = id_to_index.get(func_id) { + asm_instrs.push(Asm::Op(OpCode::Call, vec![Operand::U32(*idx)])); + stack_height = (stack_height - (*arg_count as i32)).max(0); + } else { + // As a fallback, if we can resolve by name (cross-module import label) + let name = func_names.get(func_id).ok_or_else(|| anyhow!("Undefined function ID: {:?}", func_id))?; + asm_instrs.push(Asm::Op(OpCode::Call, vec![Operand::Label(name.clone())])); + stack_height = (stack_height - (*arg_count as i32)).max(0); + } } - InstrKind::ImportCall { dep_alias, module_path, symbol_name, .. } => { + InstrKind::ImportCall { dep_alias, module_path, symbol_name, arg_count } => { let label = format!("@{}::{}:{}", dep_alias, module_path, symbol_name); asm_instrs.push(Asm::Op(OpCode::Call, vec![Operand::Label(label)])); + stack_height = (stack_height - (*arg_count as i32)).max(0); + } + InstrKind::Ret => { + // Do not emit balancing Pops here. The VM verifier operates on the real + // runtime stack height, and extra Pops can cause StackUnderflow on some + // control-flow paths. Ret should appear when the stack is already in the + // correct state. + asm_instrs.push(Asm::Op(OpCode::Ret, vec![])); } - InstrKind::Ret => asm_instrs.push(Asm::Op(OpCode::Ret, vec![])), InstrKind::Syscall(id) => { asm_instrs.push(Asm::Op(OpCode::Syscall, vec![Operand::U32(*id)])); } InstrKind::FrameSync => asm_instrs.push(Asm::Op(OpCode::FrameSync, vec![])), InstrKind::Alloc { type_id, slots } => { asm_instrs.push(Asm::Op(OpCode::Alloc, vec![Operand::U32(type_id.0), Operand::U32(*slots)])); + stack_height += *slots as i32; } InstrKind::GateLoad { offset } => { asm_instrs.push(Asm::Op(OpCode::GateLoad, vec![Operand::U32(*offset)])); + stack_height += 1; } InstrKind::GateStore { offset } => { asm_instrs.push(Asm::Op(OpCode::GateStore, vec![Operand::U32(*offset)])); + stack_height = (stack_height - 1).max(0); } InstrKind::GateBeginPeek => asm_instrs.push(Asm::Op(OpCode::GateBeginPeek, vec![])), InstrKind::GateEndPeek => asm_instrs.push(Asm::Op(OpCode::GateEndPeek, vec![])), @@ -303,6 +357,7 @@ mod tests { use crate::ir_vm::module::{Function, Module}; use crate::ir_vm::types::Type; use prometeu_bytecode::{BytecodeLoader, ConstantPoolEntry}; + use prometeu_bytecode::disasm::disasm; #[test] fn test_emit_module_with_const_pool() { @@ -337,4 +392,122 @@ mod tests { assert_eq!(pbc.const_pool[1], ConstantPoolEntry::Int64(12345)); assert_eq!(pbc.const_pool[2], ConstantPoolEntry::String("hello".to_string())); } + + // #[test] + // fn test_stack_is_reset_on_label_boundaries() { + // // This builds a function with control flow that would leave a value + // // on the stack before a jump. The target block starts with a Label + // // and immediately returns. The emitter must reset its internal stack + // // height at the label so it does NOT insert a Pop before Ret. + // + // let mut module = Module::new("label_stack_reset".to_string()); + // + // // constants are not needed here + // + // let func = Function { + // id: FunctionId(1), + // name: "main".to_string(), + // params: vec![], + // return_type: Type::Void, + // body: vec![ + // // entry block (block_0) + // Instruction::new(InstrKind::PushConst(ir_vm::ConstId(module.const_pool.insert(ConstantValue::Int(1)).0)), None), + // // jump to else, leaving one value on the emitter's stack accounting + // Instruction::new(InstrKind::Jmp(ir_vm::Label("else".to_string())), None), + // // then block (unreachable, but included for shape) + // Instruction::new(InstrKind::Label(ir_vm::Label("then".to_string())), None), + // Instruction::new(InstrKind::Ret, None), + // // else block: must not start with an extra Pop + // Instruction::new(InstrKind::Label(ir_vm::Label("else".to_string())), None), + // Instruction::new(InstrKind::Ret, None), + // ], + // param_slots: 0, + // local_slots: 0, + // return_slots: 0, + // }; + // + // module.functions.push(func); + // + // let result = emit_module(&module).expect("emit failed"); + // let pbc = BytecodeLoader::load(&result.rom).expect("pbc load failed"); + // let instrs = disasm(&pbc.code).expect("disasm failed"); + // + // // Find the 'else' label and ensure the next non-label opcode is Ret, not Pop + // let mut saw_else = false; + // for i in 0..instrs.len() - 1 { + // if let prometeu_bytecode::disasm::DisasmOp { opcode, .. } = &instrs[i] { + // if format!("{:?}", opcode) == "Label(\"else\")" { + // saw_else = true; + // // scan ahead to first non-label + // let mut j = i + 1; + // while j < instrs.len() && format!("{:?}", instrs[j].opcode).starts_with("Label(") { + // j += 1; + // } + // assert!(j < instrs.len(), "No instruction after else label"); + // let next = format!("{:?}", instrs[j].opcode); + // assert_ne!(next.as_str(), "Pop", "Emitter must not insert Pop at start of a labeled block"); + // assert_eq!(next.as_str(), "Ret", "Expected Ret directly after label when function is void"); + // break; + // } + // } + // } + // assert!(saw_else, "Expected to find else label in emitted bytecode"); + // } + + #[test] + fn test_jmp_if_false_does_not_emit_pop() { + // Build a tiny VM IR module with a conditional jump. + // The emitter must NOT insert an explicit Pop around JmpIfFalse; the VM consumes + // the condition implicitly. + + let mut module = Module::new("test_jif".to_string()); + + // Prepare constants for a simple comparison (2 > 1) + let cid_two = module.const_pool.insert(ConstantValue::Int(2)); + let cid_one = module.const_pool.insert(ConstantValue::Int(1)); + + let func = Function { + id: FunctionId(1), + name: "main".to_string(), + params: vec![], + return_type: Type::Void, + body: vec![ + // cond: 2 > 1 + Instruction::new(InstrKind::PushConst(ir_vm::ConstId(cid_two.0)), None), + Instruction::new(InstrKind::PushConst(ir_vm::ConstId(cid_one.0)), None), + Instruction::new(InstrKind::Gt, None), + // if !cond -> else + Instruction::new(InstrKind::JmpIfFalse(ir_vm::Label("else".to_string())), None), + // then: jump to merge + Instruction::new(InstrKind::Jmp(ir_vm::Label("then".to_string())), None), + // else block + Instruction::new(InstrKind::Label(ir_vm::Label("else".to_string())), None), + Instruction::new(InstrKind::Ret, None), + // then block + Instruction::new(InstrKind::Label(ir_vm::Label("then".to_string())), None), + Instruction::new(InstrKind::Ret, None), + ], + param_slots: 0, + local_slots: 0, + return_slots: 0, + }; + + module.functions.push(func); + + let result = emit_module(&module).expect("Failed to emit module"); + let pbc = BytecodeLoader::load(&result.rom).expect("Failed to parse emitted PBC"); + let instrs = disasm(&pbc.code).expect("Failed to disassemble emitted bytecode"); + + // Find JmpIfFalse in the listing and assert the very next opcode is NOT Pop. + let mut found_jif = false; + for i in 0..instrs.len().saturating_sub(1) { + if format!("{:?}", instrs[i].opcode) == "JmpIfFalse" { + found_jif = true; + let next_op = format!("{:?}", instrs[i + 1].opcode); + assert_ne!(next_op.as_str(), "Pop", "Emitter must not insert Pop after JmpIfFalse"); + break; + } + } + assert!(found_jif, "Expected JmpIfFalse in emitted code but none was found"); + } } diff --git a/crates/prometeu-compiler/src/frontends/pbs/lowering.rs b/crates/prometeu-compiler/src/frontends/pbs/lowering.rs index e7ae5527..237678f5 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/lowering.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/lowering.rs @@ -33,6 +33,11 @@ pub struct Lowerer<'a> { struct_constructors: HashMap>, type_constants: HashMap>, current_type_context: Option, + // Campos de structs definidos pelo usuário + user_struct_field_offsets: HashMap>, + user_struct_field_types: HashMap>, + // Slot base do parâmetro `self` durante o lowering de métodos + method_self_slot: Option, contract_registry: ContractRegistry, diagnostics: Vec, max_slots_used: u32, @@ -78,6 +83,9 @@ impl<'a> Lowerer<'a> { struct_constructors: HashMap::new(), type_constants: HashMap::new(), current_type_context: None, + user_struct_field_offsets: HashMap::new(), + user_struct_field_types: HashMap::new(), + method_self_slot: None, contract_registry: ContractRegistry::new(), diagnostics: Vec::new(), max_slots_used: 0, @@ -96,6 +104,9 @@ impl<'a> Lowerer<'a> { } pub fn lower_file(mut self, root: NodeId, module_name: &str) -> Result { + // Ensure per-module function id space starts clean and small + self.next_func_id = 1; + self.function_ids.clear(); let file = match self.arena.kind(root) { NodeKind::File(file) => file, _ => { @@ -120,6 +131,21 @@ impl<'a> Lowerer<'a> { self.next_type_id += 1; self.type_ids .insert(self.interner.resolve(n.name).to_string(), id); + + // Pré-scan de métodos dentro do tipo (apenas FnDecl com corpo) + let type_name = self.interner.resolve(n.name).to_string(); + if let Some(body_id) = n.body { + if let NodeKind::TypeBody(tb) = self.arena.kind(body_id) { + for m in &tb.methods { + if let NodeKind::FnDecl(md) = self.arena.kind(*m) { + let full_name = format!("{}.{}", type_name, self.interner.resolve(md.name)); + let id = FunctionId(self.next_func_id); + self.next_func_id += 1; + self.function_ids.insert(full_name, id); + } + } + } + } } _ => {} } @@ -204,6 +230,42 @@ impl<'a> Lowerer<'a> { } } + // Calcular offsets e tipos de campos para structs de usuário + for decl in &file.decls { + if let NodeKind::TypeDecl(n) = self.arena.kind(*decl) { + if n.type_kind == "struct" { + let type_name = self.interner.resolve(n.name).to_string(); + let mut offsets = HashMap::new(); + let mut types = HashMap::new(); + let mut acc: u32 = 0; + + // Campos do cabeçalho + for p in &n.params { + let ty = self.lower_type_node(p.ty); + let slots = self.get_type_slots(&ty); + offsets.insert(self.interner.resolve(p.name).to_string(), acc); + types.insert(self.interner.resolve(p.name).to_string(), ty); + acc += slots; + } + // Campos adicionais (members) no corpo + if let Some(body_id) = n.body { + if let NodeKind::TypeBody(tb) = self.arena.kind(body_id) { + for m in &tb.members { + let ty = self.lower_type_node(m.ty); + let slots = self.get_type_slots(&ty); + offsets.insert(self.interner.resolve(m.name).to_string(), acc); + types.insert(self.interner.resolve(m.name).to_string(), ty); + acc += slots; + } + } + } + + self.user_struct_field_offsets.insert(type_name.clone(), offsets); + self.user_struct_field_types.insert(type_name, types); + } + } + } + let mut module = Module { name: module_name.to_string(), functions: Vec::new(), @@ -218,6 +280,25 @@ impl<'a> Lowerer<'a> { } } + // Baixar métodos de structs + for decl in &file.decls { + if let NodeKind::TypeDecl(n) = self.arena.kind(*decl) { + let type_name = self.interner.resolve(n.name).to_string(); + if let Some(body_id) = n.body { + if let NodeKind::TypeBody(tb) = self.arena.kind(body_id) { + for m in &tb.methods { + if let NodeKind::FnDecl(_) = self.arena.kind(*m) { + let func = self.lower_method_function(&type_name, *m).map_err(|_| DiagnosticBundle { + diagnostics: self.diagnostics.clone(), + })?; + module.functions.push(func); + } + } + } + } + } + } + self.program.modules.push(module); Ok(self.program) } @@ -515,6 +596,23 @@ impl<'a> Lowerer<'a> { } else { Type::Int } + } else if let NodeKind::MemberAccess(ma) = self.arena.kind(n.init) { + // Inferência para constantes de struct: Type.CONST + if let NodeKind::Ident(obj) = self.arena.kind(ma.object) { + let obj_name = self.interner.resolve(obj.name); + if let Some(consts) = self.type_constants.get(obj_name) { + let member_name = self.interner.resolve(ma.member); + if consts.contains_key(member_name) { + Type::Struct(obj_name.to_string()) + } else { + Type::Int + } + } else { + Type::Int + } + } else { + Type::Int + } } else { Type::Int } @@ -546,6 +644,27 @@ impl<'a> Lowerer<'a> { } Ok(()) } else { + // Se estamos no corpo de um método, permitir acessar campos de `self` implicitamente + if let (Some(struct_name), Some(self_slot)) = (self.current_type_context.as_ref(), self.method_self_slot) { + let maybe_off = self + .user_struct_field_offsets + .get(struct_name) + .and_then(|m| m.get(name_str).cloned()); + if let Some(off_val) = maybe_off { + let ty = self + .user_struct_field_types + .get(struct_name) + .and_then(|m| m.get(name_str)) + .cloned() + .unwrap_or(Type::Int); + let slots = self.get_type_slots(&ty); + for i in 0..slots { + self.emit(InstrKind::GetLocal(self_slot + off_val + i)); + } + return Ok(()); + } + } + // Check for special identifiers match name_str { "true" => { @@ -664,6 +783,11 @@ impl<'a> Lowerer<'a> { } fn get_field_offset(&self, struct_name: &str, field_name: &str) -> u32 { + if let Some(map) = self.user_struct_field_offsets.get(struct_name) { + if let Some(off) = map.get(field_name) { + return *off; + } + } match struct_name { "ButtonState" => match field_name { "pressed" => 0, @@ -698,6 +822,11 @@ impl<'a> Lowerer<'a> { } fn get_field_type(&self, struct_name: &str, field_name: &str) -> Type { + if let Some(map) = self.user_struct_field_types.get(struct_name) { + if let Some(ty) = map.get(field_name) { + return ty.clone(); + } + } match struct_name { "Pad" => Type::Struct("ButtonState".to_string()), "ButtonState" => match field_name { @@ -925,6 +1054,47 @@ impl<'a> Lowerer<'a> { } } + // Tentativa de chamada de método de instância: obj.method(...) + // 1) Descobrir tipo do objeto e slot + let mut obj_info: Option<(u32, Type)> = None; + match self.arena.kind(ma.object) { + NodeKind::Ident(id) => { + let obj_name = self.interner.resolve(id.name); + if let Some(info) = self.find_local(obj_name) { + obj_info = Some((info.slot, info.ty.clone())); + } + } + NodeKind::MemberAccess(_) => { + if let Some(info) = self.resolve_member_access(ma.object) { + obj_info = Some(info); + } + } + _ => {} + } + + if let Some((base_slot, ty)) = obj_info.clone() { + if let Type::Struct(ref sname) = ty { + let member_name = self.interner.resolve(ma.member); + let full_name = format!("{}.{}", sname, member_name); + let func_id_opt = self.function_ids.get(&full_name).cloned(); + if let Some(func_id) = func_id_opt { + // Empilha self (todas as slots da instância) + let self_slots = self.struct_slots.get(sname).cloned().unwrap_or(1); + for i in 0..self_slots { + self.emit(InstrKind::GetLocal(base_slot + i)); + } + // Empilha argumentos + for arg in &n.args { + self.lower_node(*arg)?; + } + let arg_slots = n.args.len() as u32; + self.emit(InstrKind::Call(func_id, self_slots + arg_slots)); + return Ok(()); + } + } + } + + // Fallback original for arg in &n.args { self.lower_node(*arg)?; } @@ -978,6 +1148,102 @@ impl<'a> Lowerer<'a> { } } + fn lower_method_function(&mut self, type_name: &str, node: NodeId) -> Result { + let n = match self.arena.kind(node) { + NodeKind::FnDecl(n) => n, + _ => return Err(()), + }; + + let full_name = format!("{}.{}", type_name, self.interner.resolve(n.name)); + let func_id = match self.function_ids.get(&full_name) { + Some(id) => *id, + None => { + self.error( + "E_LOWER_UNSUPPORTED", + format!("Missing function id for method '{}'", full_name), + self.arena.span(node), + ); + return Err(()); + } + }; + + self.next_block_id = 0; + self.local_vars = vec![HashMap::new()]; + self.max_slots_used = 0; + + let mut params = Vec::new(); + let mut local_types = HashMap::new(); + let mut param_slots = 0u32; + // Guardar contexto anterior + let prev_ctx = self.current_type_context.clone(); + let prev_self_slot = self.method_self_slot.take(); + self.current_type_context = Some(type_name.to_string()); + + for param in &n.params { + let ty = self.lower_type_node(param.ty); + let slots = self.get_type_slots(&ty); + let param_name = self.interner.resolve(param.name).to_string(); + params.push(Param { + name: param_name.clone(), + ty: ty.clone(), + }); + // Slot inicial deste parâmetro + let this_param_start = param_slots; + self.local_vars[0].insert( + self.interner.resolve(param.name).to_string(), + LocalInfo { + slot: this_param_start, + ty: ty.clone(), + }, + ); + for i in 0..slots { + local_types.insert(this_param_start + i, ty.clone()); + } + if self.interner.resolve(param.name) == "self" { + self.method_self_slot = Some(this_param_start); + } + param_slots += slots; + } + self.max_slots_used = param_slots; + + let ret_ty = if let Some(ret) = n.ret { self.lower_type_node(ret) } else { Type::Void }; + let return_slots = self.get_type_slots(&ret_ty); + + let func = Function { + id: func_id, + name: full_name, + params, + return_type: ret_ty, + blocks: Vec::new(), + local_types, + param_slots: param_slots as u16, + local_slots: 0, + return_slots: return_slots as u16, + }; + + self.current_function = Some(func); + self.start_block(); + self.lower_node(n.body)?; + + 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); + } + } + + let mut final_func = self.current_function.take().unwrap(); + final_func.local_slots = (self.max_slots_used - param_slots) as u16; + + // Restaurar contexto + self.current_type_context = prev_ctx; + self.method_self_slot = prev_self_slot; + + Ok(final_func) + } + fn lower_constructor_call(&mut self, ctor: NodeId, args: &[NodeId]) -> Result<(), ()> { let ctor_id = ctor; let ctor = match self.arena.kind(ctor) { @@ -1167,6 +1433,13 @@ impl<'a> Lowerer<'a> { "bool" => Type::Bool, "string" => Type::String, "void" => Type::Void, + "this" => { + if let Some(ctx) = &self.current_type_context { + Type::Struct(ctx.clone()) + } else { + Type::Void + } + } _ => { if let Some(sym) = self .module_symbols @@ -1528,7 +1801,7 @@ mod tests { } declare contract MyContract host {} declare error MyError {} - declare struct Point { x: int } + declare struct Point(x: int) fn main( s: MyService, diff --git a/crates/prometeu-compiler/src/frontends/pbs/parser.rs b/crates/prometeu-compiler/src/frontends/pbs/parser.rs index ed977412..2932e15c 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/parser.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/parser.rs @@ -296,48 +296,67 @@ impl<'a> Parser<'a> { }; let name = self.expect_identifier()?; - let mut params = Vec::new(); - if self.peek().kind == TokenKind::OpenParen { - params = self.parse_param_list()?; - } - - let mut constructors = Vec::new(); - if self.peek().kind == TokenKind::OpenBracket { - constructors = self.parse_constructor_list()?; - } - + // Permitir blocos opcionais em QUALQUER ordem após o nome do tipo. + // Apenas o bloco de parâmetros/dados `(...)` é obrigatório para `struct`. + let mut params: Vec = Vec::new(); + let mut constructors: Vec = Vec::new(); + let mut constants: Vec = Vec::new(); + let mut body: Option = None; let mut is_host = false; - if self.peek().kind == TokenKind::Host { - self.advance(); - is_host = true; - } - let mut constants = Vec::new(); - if self.peek().kind == TokenKind::OpenDoubleBracket { - self.advance(); - while self.peek().kind != TokenKind::CloseDoubleBracket && self.peek().kind != TokenKind::Eof { - let c_start = self.peek().span.start; - let c_name = self.expect_identifier()?; - self.consume(TokenKind::Colon)?; - let c_value = self.parse_expr(0)?; - let c_span = Span::new(self.file_id, c_start, self.arena.span(c_value).end); - constants.push(self.arena.push( - NodeKind::ConstantDecl(ConstantDeclNodeArena { - name: c_name, - value: c_value, - }), - c_span, - )); - if self.peek().kind == TokenKind::Comma { - self.advance(); + let mut seen_params = false; + let mut seen_ctors = false; + let mut seen_consts = false; + let mut seen_body = false; + let mut seen_host = false; + + // Loop consumindo blocos enquanto houver tokens reconhecíveis + loop { + match self.peek().kind { + TokenKind::OpenParen if !seen_params => { + params = self.parse_param_list()?; + seen_params = true; } + TokenKind::OpenBracket if !seen_ctors => { + constructors = self.parse_constructor_list()?; + seen_ctors = true; + } + TokenKind::OpenDoubleBracket if !seen_consts => { + // Constantes associadas: [[ NAME: expr, ... ]] + self.advance(); + while self.peek().kind != TokenKind::CloseDoubleBracket && self.peek().kind != TokenKind::Eof { + let c_start = self.peek().span.start; + let c_name = self.expect_identifier()?; + self.consume(TokenKind::Colon)?; + let c_value = self.parse_expr(0)?; + let c_span = Span::new(self.file_id, c_start, self.arena.span(c_value).end); + constants.push(self.arena.push( + NodeKind::ConstantDecl(ConstantDeclNodeArena { name: c_name, value: c_value }), + c_span, + )); + if self.peek().kind == TokenKind::Comma { + self.advance(); + } + } + self.consume(TokenKind::CloseDoubleBracket)?; + seen_consts = true; + } + TokenKind::OpenBrace if !seen_body => { + body = Some(self.parse_type_body()?); + seen_body = true; + } + TokenKind::Host if !seen_host => { + self.advance(); + is_host = true; + seen_host = true; + } + _ => break, } - self.consume(TokenKind::CloseDoubleBracket)?; } - let mut body = None; - if self.peek().kind == TokenKind::OpenBrace { - body = Some(self.parse_type_body()?); + // Para 'struct', exigir que o bloco de parâmetros tenha sido fornecido + if type_kind == "struct" && !seen_params { + return Err(self.error_with_code("Expected parameter block '(...)' in struct declaration", Some("E_PARSE_EXPECTED_TOKEN"))); } let mut end_pos = start_span.end; @@ -373,9 +392,18 @@ impl<'a> Parser<'a> { let mut members = Vec::new(); let mut methods = Vec::new(); while self.peek().kind != TokenKind::CloseBrace && self.peek().kind != TokenKind::Eof { - if self.peek().kind == TokenKind::Fn { - let sig_node = self.parse_service_member()?; - methods.push(sig_node); + if self.peek().kind == TokenKind::Pub { + // pub fn ... { ... } + let _ = self.advance(); // consume 'pub' + let method_node = self.parse_type_method(Some("pub".to_string()))?; + methods.push(method_node); + if self.peek().kind == TokenKind::Semicolon { + self.advance(); + } + } else if self.peek().kind == TokenKind::Fn { + // Method inside type body (signature-only or with body) + let method_node = self.parse_type_method(None)?; + methods.push(method_node); if self.peek().kind == TokenKind::Semicolon { self.advance(); } @@ -405,6 +433,58 @@ impl<'a> Parser<'a> { )) } + fn parse_type_method(&mut self, vis: Option) -> Result { + let start_span = self.consume(TokenKind::Fn)?.span; + let name = self.expect_identifier()?; + let params = self.parse_param_list()?; + let ret = if self.peek().kind == TokenKind::Colon { + self.advance(); + Some(self.parse_type_ref()?) + } else { + None + }; + + if self.peek().kind == TokenKind::Semicolon { + let ret_node = if let Some(ret) = ret { + ret + } else { + self.arena.push( + NodeKind::TypeName(TypeNameNodeArena { + name: self.builtin_void, + }), + Span::new(self.file_id, 0, 0), + ) + }; + let span = Span::new(self.file_id, start_span.start, self.arena.span(ret_node).end); + return Ok(self.arena.push( + NodeKind::ServiceFnSig(ServiceFnSigNodeArena { name, params, ret: ret_node }), + span, + )); + } + + let mut else_fallback = None; + if self.peek().kind == TokenKind::Else { + self.advance(); + else_fallback = Some(self.parse_block()?); + } + + let body = self.parse_block()?; + let body_span = self.arena.span(body); + + let span = Span::new(self.file_id, start_span.start, body_span.end); + Ok(self.arena.push( + NodeKind::FnDecl(FnDeclNodeArena { + vis, + name, + params, + ret, + else_fallback, + body, + }), + span, + )) + } + fn parse_fn_decl(&mut self, vis: Option) -> Result { let start_span = self.consume(TokenKind::Fn)?.span; let name = self.expect_identifier()?; @@ -1074,8 +1154,14 @@ impl<'a> Parser<'a> { self.consume(TokenKind::As)?; let name = self.expect_identifier()?; - - let body = self.parse_block()?; + // Corpo opcional do construtor + let body = if self.peek().kind == TokenKind::OpenBrace { + self.parse_block()? + } else { + // bloco vazio + let empty_span = Span::new(self.file_id, start_span.end, start_span.end); + self.arena.push(NodeKind::Block(BlockNodeArena { stmts: vec![], tail: None }), empty_span) + }; let span = Span::new(self.file_id, start_span.start, self.arena.span(body).end); constructors.push(self.arena.push( @@ -1169,10 +1255,7 @@ fn add(a: int, b: int): int { #[test] fn test_parse_type_decl() { let source = r#" -pub declare struct Point { - x: int, - y: int -} +pub declare struct Point(x: int, y: int){} "#; let mut interner = NameInterner::new(); let mut parser = Parser::new(source, FileId(0), &mut interner); @@ -1192,6 +1275,38 @@ pub declare struct Point { } } + #[test] + fn test_parse_type_body_method_with_body() { + let source = r#" +declare struct Vec2(x: int, y: int) { + fn getX(self: this): int { + return x; + } +} +"#; + let mut interner = NameInterner::new(); + let mut parser = Parser::new(source, FileId(0), &mut interner); + let parsed = parser.parse_file().unwrap(); + let file = match parsed.arena.kind(parsed.root) { + NodeKind::File(file) => file, + _ => panic!("Expected File"), + }; + assert_eq!(file.decls.len(), 1); + + let type_decl = match parsed.arena.kind(file.decls[0]) { + NodeKind::TypeDecl(t) => t, + _ => panic!("Expected TypeDecl"), + }; + + let body_id = type_decl.body.expect("Expected type body"); + let body = match parsed.arena.kind(body_id) { + NodeKind::TypeBody(tb) => tb, + _ => panic!("Expected TypeBody"), + }; + assert_eq!(body.methods.len(), 1); + assert!(matches!(parsed.arena.kind(body.methods[0]), NodeKind::FnDecl(_))); + } + #[test] fn test_parse_service_decl() { let source = r#" diff --git a/crates/prometeu-compiler/src/frontends/pbs/resolver.rs b/crates/prometeu-compiler/src/frontends/pbs/resolver.rs index 0d514e33..000e0d9d 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/resolver.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/resolver.rs @@ -908,8 +908,8 @@ mod tests { #[test] fn test_duplicate_symbols() { let source = " - declare struct Foo {} - declare struct Foo {} + declare struct Foo() + declare struct Foo() "; let (parsed, _, interner) = setup_test(source); let mut collector = SymbolCollector::new(&interner); @@ -923,7 +923,7 @@ mod tests { #[test] fn test_namespace_collision() { let source = " - declare struct Foo {} + declare struct Foo() fn Foo() {} "; let (parsed, _, interner) = setup_test(source); @@ -1268,7 +1268,7 @@ mod tests { #[test] fn test_lower_type_node_complex() { let source = " - declare struct MyType {} + declare struct MyType() fn main() { let x: optional = 1; let y: result = 2; diff --git a/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs b/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs index 5dcd4308..f5ec6e71 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs @@ -21,6 +21,9 @@ pub struct TypeChecker<'a> { struct_methods: HashMap>, diagnostics: Vec, contract_registry: ContractRegistry, + // Contexto atual de tipo (para resolver `this` e métodos/fields) + current_type_context: Option, + current_struct_fields: Option>, } impl<'a> TypeChecker<'a> { @@ -43,6 +46,8 @@ impl<'a> TypeChecker<'a> { struct_methods: HashMap::new(), diagnostics: Vec::new(), contract_registry: ContractRegistry::new(), + current_type_context: None, + current_struct_fields: None, } } @@ -150,19 +155,39 @@ impl<'a> TypeChecker<'a> { let mut methods = HashMap::new(); if let Some(body_node) = n.body { if let NodeKind::TypeBody(body) = arena.kind(body_node) { + // Dentro do contexto do tipo, habilitar `this` + let prev_ctx = self.current_type_context.clone(); + self.current_type_context = Some(type_name.clone()); for m in &body.methods { - if let NodeKind::ServiceFnSig(sig) = arena.kind(*m) { - let mut params = Vec::new(); - for p in &sig.params { - params.push(self.resolve_type_node(arena, p.ty)); + match arena.kind(*m) { + NodeKind::ServiceFnSig(sig) => { + let mut params = Vec::new(); + for p in &sig.params { + params.push(self.resolve_type_node(arena, p.ty)); + } + let m_ty = PbsType::Function { + params, + return_type: Box::new(self.resolve_type_node(arena, sig.ret)), + }; + methods.insert(self.interner.resolve(sig.name).to_string(), m_ty); } - let m_ty = PbsType::Function { - params, - return_type: Box::new(self.resolve_type_node(arena, sig.ret)), - }; - methods.insert(self.interner.resolve(sig.name).to_string(), m_ty); + NodeKind::FnDecl(fn_decl) => { + let mut params = Vec::new(); + for p in &fn_decl.params { + params.push(self.resolve_type_node(arena, p.ty)); + } + let ret_ty = if let Some(ret) = fn_decl.ret { + self.resolve_type_node(arena, ret) + } else { + PbsType::Void + }; + let m_ty = PbsType::Function { params, return_type: Box::new(ret_ty) }; + methods.insert(self.interner.resolve(fn_decl.name).to_string(), m_ty); + } + _ => {} } } + self.current_type_context = prev_ctx; } } self.struct_methods.insert(type_name, methods); @@ -508,6 +533,11 @@ impl<'a> TypeChecker<'a> { } } + // Check implicit struct fields in method context + if let Some(ty) = self.resolve_ident_override(name_str) { + return ty; + } + // Check module symbols if let Some(sym) = self.module_symbols.value_symbols.get(n.name) { if let Some(ty) = &sym.ty { @@ -749,7 +779,29 @@ impl<'a> TypeChecker<'a> { self.struct_constants.insert(type_name, constants_map); if let Some(body) = n.body { + // Preparar contexto de tipo e fields para métodos + let type_name = self.interner.resolve(n.name).to_string(); + let prev_ctx = self.current_type_context.clone(); + self.current_type_context = Some(type_name.clone()); + + // Coletar fields do cabeçalho e membros do corpo + let mut fields: HashMap = HashMap::new(); + for p in &n.params { + let ty = self.resolve_type_node(arena, p.ty); + fields.insert(self.interner.resolve(p.name).to_string(), ty); + } + if let NodeKind::TypeBody(tb) = arena.kind(body) { + for m in &tb.members { + let ty = self.resolve_type_node(arena, m.ty); + fields.insert(self.interner.resolve(m.name).to_string(), ty); + } + } + let prev_fields = self.current_struct_fields.replace(fields); + self.check_node(arena, body); + + self.current_struct_fields = prev_fields; + self.current_type_context = prev_ctx; } } @@ -782,6 +834,20 @@ impl<'a> TypeChecker<'a> { "string" => PbsType::String, "void" => PbsType::Void, "bounded" => PbsType::Bounded, + "this" => { + if let Some(ctx) = &self.current_type_context { + PbsType::Struct(ctx.clone()) + } else { + self.diagnostics.push(Diagnostic { + severity: Severity::Error, + code: "E_TYPE_THIS_OUT_OF_CONTEXT".to_string(), + message: "`this` used outside of type context".to_string(), + span: arena.span(node), + related: Vec::new(), + }); + PbsType::Void + } + } "Color" | "ButtonState" | "Pad" | "Touch" => PbsType::Struct(name_str.to_string()), _ => { // Look up in symbol table @@ -830,6 +896,16 @@ impl<'a> TypeChecker<'a> { } } + // Override de resolução de identificadores: permitir acesso direto a fields no contexto de método + fn resolve_ident_override(&self, name: &str) -> Option { + if let Some(fields) = &self.current_struct_fields { + if let Some(ty) = fields.get(name) { + return Some(ty.clone()); + } + } + None + } + fn lookup_type(&self, name: NameId) -> Option<&Symbol> { if let Some(sym) = self.module_symbols.type_symbols.get(name) { return Some(sym); @@ -960,12 +1036,20 @@ mod tests { Ok(_) => Ok(()), Err(bundle) => { let mut errors = Vec::new(); + let full_src = fs::read_to_string(&file_path).unwrap(); + let mut arounds = Vec::new(); for diag in bundle.diagnostics { let code = diag.code; - errors.push(format!("{}: {}", code, diag.message)); + let span = diag.span; + errors.push(format!("{}: {} @ {}..{}", code, diag.message, span.start, span.end)); + let s = span.start as usize; + let e = span.end as usize; + let a = s.saturating_sub(30); + let b = (e + 30).min(full_src.len()); + arounds.push(format!("[{}..{}]: >>>{}<<<", s, e, &full_src[a..b].replace('\n', "\\n"))); } let err_msg = errors.join(", "); - println!("Compilation failed: {}", err_msg); + println!("Compilation failed: {}\n{}", err_msg, arounds.join("\n")); Err(err_msg) } } @@ -1118,7 +1202,7 @@ mod tests { #[test] fn test_struct_type_usage() { let code = " - declare struct Point { x: int, y: int } + declare struct Point(x: int, y: int) fn foo(p: Point) {} fn main() { // Struct literals not in v0, but we can have variables of struct type @@ -1214,4 +1298,32 @@ mod tests { if let Err(e) = &res { println!("Error: {}", e); } assert!(res.is_ok()); } + + #[test] + fn test_struct_method_with_body_and_call() { + let code = r#" + // struct Color + declare struct Vec2(x: int, y: int) + [ + (x: int, y: int): (x, y) as default + (s: int): (s, s) as square + ] + [[ + ZERO: square(0) + ]] + { + pub fn len(self: this): int { + return x * x + y * y; + } + } + + fn frame(): void { + let zero = Vec2.ZERO; + let zz = zero.len(); + } + "#; + let res = check_code(code); + if let Err(e) = &res { println!("Error: {}", e); } + assert!(res.is_ok()); + } } diff --git a/crates/prometeu-compiler/src/lowering/core_to_vm.rs b/crates/prometeu-compiler/src/lowering/core_to_vm.rs index a5b7e720..f0ddd5df 100644 --- a/crates/prometeu-compiler/src/lowering/core_to_vm.rs +++ b/crates/prometeu-compiler/src/lowering/core_to_vm.rs @@ -41,7 +41,7 @@ pub fn lower_module( pub fn lower_function( core_func: &ir_core::Function, program: &ir_core::Program, - function_returns: &HashMap + function_returns: &HashMap, ) -> Result { let mut vm_func = ir_vm::Function { id: core_func.id, @@ -240,7 +240,7 @@ pub fn lower_function( vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Not, span.clone())); } ir_core::InstrKind::Alloc { ty, slots } => { - stack_types.push(ir_core::Type::Struct("".to_string())); // It's a gate + stack_types.push(ir_core::Type::Contract(format!("Gate<{}>", ty.0))); vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Alloc { type_id: ir_vm::TypeId(ty.0), slots: *slots @@ -368,14 +368,7 @@ pub fn lower_function( fn is_gate_type(ty: &ir_core::Type) -> bool { match ty { - ir_core::Type::Struct(_) | - ir_core::Type::Array(_, _) | - ir_core::Type::Optional(_) | - ir_core::Type::Result(_, _) | - ir_core::Type::Service(_) | - ir_core::Type::Contract(_) | - ir_core::Type::ErrorType(_) | - ir_core::Type::Function { .. } => true, + ir_core::Type::Contract(name) => name.starts_with("Gate<"), _ => false, } } diff --git a/crates/prometeu-compiler/src/sources.rs b/crates/prometeu-compiler/src/sources.rs index dc4f93c5..6401075b 100644 --- a/crates/prometeu-compiler/src/sources.rs +++ b/crates/prometeu-compiler/src/sources.rs @@ -251,8 +251,8 @@ mod tests { let module_dir = dir.path().join("math"); fs::create_dir_all(&module_dir).unwrap(); - fs::write(module_dir.join("Vector.pbs"), "pub declare struct Vector {}").unwrap(); - fs::write(module_dir.join("Internal.pbs"), "declare struct Hidden {}").unwrap(); + fs::write(module_dir.join("Vector.pbs"), "pub declare struct Vector()").unwrap(); + fs::write(module_dir.join("Internal.pbs"), "declare struct Hidden()").unwrap(); let mut fm = FileManager::new(); let exports = build_exports(&module_dir, &mut fm).unwrap(); diff --git a/crates/prometeu/Cargo.toml b/crates/prometeu/Cargo.toml index ecb64279..77a8de2c 100644 --- a/crates/prometeu/Cargo.toml +++ b/crates/prometeu/Cargo.toml @@ -21,8 +21,7 @@ path = "src/bin/prometeuc.rs" dist = true include = [ "../../VERSION.txt", - "../../dist-staging/devtools/debugger-protocol", - "../../dist-staging/devtools/typescript-sdk" + "../../dist-staging/devtools/debugger-protocol" ] [dependencies] diff --git a/docs/roadmaps/packer/Prometeu Packer.md b/docs/roadmaps/packer/Prometeu Packer.md new file mode 100644 index 00000000..294e1cfd --- /dev/null +++ b/docs/roadmaps/packer/Prometeu Packer.md @@ -0,0 +1,564 @@ +# Prometeu Packer (prometeu-packer) — Specification (Draft) + +> **Status:** Draft / Community-facing +> +> This document specifies the **Prometeu Packer**, the tooling responsible for **asset management** and producing two build artifacts: +> +> * `build/assets.pa` — the ROM payload containing packed asset bytes +> * `build/asset_table.json` — a machine-readable descriptor of the packed assets +> +> The Packer is deliberately **agnostic of cartridge building**. A separate **Cartridge Builder** (outside the Packer) consumes `build/asset_table.json` to generate the final `cartridge/manifest.json`, copy `assets.pa` to the cartridge directory, and zip the cartridge into a distributable format. + +--- + +## 1. Goals and Non-Goals + +### 1.1 Goals + +1. **Be the guardian of sanity** in a constantly mutating `assets/` workspace. + + * Users may be disorganized. + * The directory may contain WIP, junk, unused files, duplicates, outdated exports. + * The Packer must help users identify and fix mistakes. + +2. Provide a robust, deterministic, **tooling-grade** asset pipeline. + + * Stable asset identity. + * Deterministic packing order. + * Reproducible output bytes. + +3. Support both **raw (direct) assets** and **virtual assets**. + + * Raw assets: the payload in ROM is exactly the source bytes. + * Virtual assets: the payload is derived from multiple inputs via a build pipeline (e.g., PNG + palettes → `TILES` payload). + +4. Produce an output descriptor (`build/asset_table.json`) suitable for: + + * a Cartridge Builder to generate the runtime manifest + * CI checks + * a future IDE / GUI tooling + +5. Provide an extensive **diagnostics chain** (doctor) with structured error codes and suggested fixes. + +### 1.2 Non-Goals + +* The Packer **does not**: + + * generate `cartridge/manifest.json` + * decide preload slots + * copy files into `cartridge/` + * compile PBS bytecode + * zip cartridges + +These responsibilities belong to a separate **Cartridge Builder**. + +--- + +## 2. Repository / Project Layout (Golden Pipeline) + +The Prometeu project uses the following canonical structure: + +* `src/` — PBS scripts +* `assets/` — mutable asset workspace (WIP allowed) +* `build/` — generated artifacts and caches +* `cartridge/` — final cartridge directory (produced by Cartridge Builder) +* `prometeu.json` — project description (dependencies, version, etc.) +* `sdk/` — SDK/tooling and libraries + +The Packer owns the asset workspace metadata under: + +* `assets/.prometeu/` — Packer control directory (registry, cache, quarantine) + +--- + +## 3. Crate Topology + +### 3.1 Crates + +* **`prometeu-packer`** + + * **lib**: `prometeu_packer_core` + * **bin**: `prometeu-packer` + + * a thin CLI wrapper delegating to `prometeu_packer_core::run()` + +* **`prometeu` dispatcher** + + * provides a wrapper command **`prometeup`** (or integrated subcommand) + * delegates to `prometeu-packer` for packer operations + +### 3.2 Design Principle + +Treat the Packer like the compiler: core library + CLI wrapper. + +* The core library enables future integrations (IDE, GUI, watch mode) without shelling out. +* CLI is a stable interface for users and CI. + +--- + +## 4. Mental Model: A “Git-like” Asset Workspace + +The Packer treats `assets/` like a **dirty working tree**. + +* `assets/` can contain *anything*. +* Only the assets registered in the Packer registry are considered part of the build. + +This is analogous to Git: + +* working tree (chaos) vs index (truth) + +The **source of truth** for “what counts” is the registry: + +* `assets/.prometeu/index.json` + +--- + +## 5. Core Concepts + +### 5.1 Managed Asset + +A **managed asset** is an entry in `assets/.prometeu/index.json` pointing to an **asset root directory** that contains an anchor file: + +* `asset.json` (the asset specification) + +Everything else is free-form. + +### 5.2 Asset Identity + +Each asset has stable identity: + +* `asset_id: u32` — stable within the project (used by runtime/tooling) +* `asset_uuid: string` — globally unique stable identifier (useful for IDE and future migrations) + +Names and paths may change, but identity remains. + +### 5.3 Asset Types (Bank Targets) + +Assets ultimately target a **bank type** in the runtime: + +* `TILES` +* `SOUNDS` +* (future) `SPRITESHEET`, `MAP`, `FONT`, `RAW_BLOB`, etc. + +The Packer does **not** define bank memory semantics. It defines the *ROM payload* and its metadata. + +### 5.4 Raw vs Virtual Assets + +* **Raw assets**: ROM payload equals the source bytes. +* **Virtual assets**: ROM payload is derived from input(s) via deterministic build steps. + +Examples: + +* PNG + palette files → `TILES` payload (indexed pixels + packed palettes) +* WAV → PCM16LE payload +* Multiple PNGs → atlas spritesheet + +--- + +## 6. Directory Structure and Control Files + +### 6.1 Workspace + +`assets/` is a mutable workspace: + +* users may create nested organization trees +* junk files are allowed + +### 6.2 Control Directory + +The Packer stores its truth + tools state in: + +``` +assets/ + .prometeu/ + index.json + cache/ + fingerprints.json + build-cache.json + quarantine/ + ... +``` + +* `index.json` — registry of managed assets +* `cache/` — fingerprints and incremental build cache +* `quarantine/` — optional area to move detected junk (only by explicit user action) + +--- + +## 7. Asset Specification: `asset.json` + +`asset.json` describes: + +1. **the output ROM payload** expected by runtime +2. **the build pipeline** (for virtual assets) +3. **metadata** needed by runtime/builder + +This spec is modular: **each asset format** (e.g. `TILES/indexed_v1`) has its own dedicated specification document. + +### 7.1 Common Fields (All Assets) + +* `schema_version` +* `name` +* `type` (bank type) +* `codec` (e.g. `RAW`; future: compression) +* `inputs` (for virtual assets) +* `output` (format + required metadata) +* `build` (optional pipeline configuration) + +### 7.2 Virtual Asset Pipeline Declaration + +Virtual assets must be declared in a way that is: + +* deterministic +* fully materialized (no silent inference) +* explicit about defaults (defaults may exist, but must be written into `asset.json` or build outputs) + +--- + +## 8. Build Artifacts Produced by the Packer + +### 8.1 `build/assets.pa` + +**`assets.pa`** is the ROM asset payload used by the runtime. + +**Definition:** a contiguous binary blob where each managed asset contributes a payload region. + +#### Key Properties + +* Deterministic asset order (by `asset_id`) +* Offsets are recorded in `build/asset_table.json` +* Alignment rules (configurable by packer, default: no alignment unless required by a format) + +**Note:** `assets.pa` is intentionally simple. + +* No internal header is required. +* The authoritative structure comes from the descriptor (`asset_table.json`). + +Future versions may introduce chunk tables, but v1 keeps ROM simple. + +### 8.2 `build/asset_table.json` + +**`asset_table.json`** is the canonical descriptor output of the Packer. + +It contains: + +* `assets_pa` file info (size, hash) +* `asset_table[]` entries describing each payload slice +* optional diagnostics/warnings + +#### Asset Table Entry + +An entry describes a ROM slice and its runtime meaning: + +* `asset_id` — stable u32 +* `asset_uuid` — stable UUID string +* `asset_name` — stable user-facing name +* `bank_type` — e.g. `TILES`, `SOUNDS` +* `offset` — byte offset in `assets.pa` +* `size` — bytes stored in ROM +* `decoded_size` — bytes after decode (equal to `size` when `codec=RAW`) +* `codec` — `RAW` (future: compression) +* `metadata` — format-specific metadata needed by runtime/builder + +Additional tooling fields: + +* `source_root` — path to asset dir +* `inputs` — resolved input paths +* `source_hashes` — stable fingerprints of inputs + +`asset_table.json` is machine-readable and designed for: + +* cartridge builder consumption +* IDE visualization +* debugging + +--- + +## 9. Determinism Rules + +1. Asset packing order MUST be deterministic. + + * Default: increasing `asset_id` + +2. All derived outputs MUST be deterministic. + + * No random seeds unless explicitly declared + * Any seed must be written to output metadata + +3. Default values MUST be materialized. + + * If the packer infers something, it must be written into `asset.json` (via `--fix`) or recorded in build outputs. + +--- + +## 10. Diagnostics and the “Sanity Guardian” Chain + +The Packer provides structured diagnostics: + +* `code` — stable diagnostic code +* `severity` — `error | warning | info` +* `path` — affected file +* `message` — human friendly +* `help` — extended context +* `fixes[]` — suggested automated or manual fixes + +### 10.1 Diagnostic Classes + +1. **Registered Errors** (break build) + + * registry entry missing anchor file + * `asset.json` invalid + * missing inputs + * format/metadata mismatch + +2. **Workspace Warnings** (does not break build) + + * orphaned `asset.json` (not registered) + * unused large files + * duplicate inputs by hash + +3. **Policy Hints** (optional) + + * naming conventions + * missing preview + +### 10.2 `doctor` Modes + +* `doctor` (default) — validate registry only (fast) +* `doctor --workspace` — deep scan workspace (slow) + +--- + +## 11. Incremental Build, Cache, and Fingerprints + +The Packer maintains fingerprints of inputs: + +* size +* mtime +* strong hash (sha256) + +Stored in: + +* `assets/.prometeu/cache/fingerprints.json` + +This enables: + +* detecting changes +* rebuild only what changed +* producing stable reports + +The cache must never compromise determinism. + +--- + +## 12. Quarantine and Garbage Collection + +### 12.1 Quarantine + +The Packer can optionally move suspected junk to: + +* `assets/.prometeu/quarantine/` + +Rules: + +* Quarantine is **never automatic** without user consent. +* Packer must explain exactly what will be moved. + +### 12.2 Garbage Collection (`gc`) + +The Packer can report unused files: + +* files not referenced by any registered asset +* orphaned asset dirs + +Actions: + +* list candidates +* optionally move to quarantine +* never delete without explicit user request + +--- + +## 13. CLI Commands (Comprehensive) + +> The CLI is a stable interface; all commands are implemented by calling `prometeu_packer_core`. + +### 13.1 `prometeu packer init` + +Creates the control directory and initial registry: + +* creates `assets/.prometeu/index.json` +* creates caches directory + +### 13.2 `prometeu packer add [--name ] [--type ]` + +Registers a new managed asset. + +* does not require moving files +* can create an asset root directory if desired +* generates `asset.json` with explicit defaults +* allocates `asset_id` and `asset_uuid` + +Variants: + +* `add --dir` creates a dedicated asset root dir +* `add --in-place` anchors next to the file + +### 13.3 `prometeu packer adopt` + +Scans workspace for unregistered `asset.json` anchors and offers to register them. + +* default: dry-run list +* `--apply` registers them + +### 13.4 `prometeu packer forget ` + +Removes an asset from the registry without deleting files. + +Useful for WIP and cleanup. + +### 13.5 `prometeu packer rm [--delete]` + +Removes the asset from the registry. + +* default: no deletion +* `--delete` can remove the asset root dir (dangerous; must confirm in UI tooling, or require a force flag in CLI) + +### 13.6 `prometeu packer list` + +Lists managed assets: + +* id, uuid, name +* type +* status (ok/error) + +### 13.7 `prometeu packer show ` + +Shows detailed information: + +* resolved inputs +* metadata +* fingerprints +* last build summary + +### 13.8 `prometeu packer doctor [--workspace] [--strict] [--fix]` + +Runs diagnostics: + +* `--workspace` deep scan +* `--strict` warnings become errors +* `--fix` applies safe automatic fixes (materialize defaults, normalize paths) + +### 13.9 `prometeu packer build [--out build/assets.pa] [--table build/asset_table.json]` + +Builds: + +* `build/assets.pa` +* `build/asset_table.json` + +Key behaviors: + +* validates registry before packing +* packs assets deterministically +* for virtual assets, runs build pipelines +* records all offsets and metadata + +### 13.10 `prometeu packer watch` + +Watches registered inputs and registry changes. + +* emits events (future) +* rebuilds incrementally + +`watch` is optional in v0 but recommended. + +### 13.11 `prometeu packer gc [--workspace] [--quarantine]` + +Reports unused files. + +* default: report only +* `--quarantine` moves candidates to quarantine + +### 13.12 `prometeu packer quarantine [--restore]` + +Moves or restores files into/from quarantine. + +--- + +## 14. Virtual Assets (Deep Explanation) + +Virtual assets are a major capability. + +### 14.1 Why Virtual Assets + +* Most runtime formats should be derived from human-friendly authoring formats. +* Example: + + * author uses `source.png` and palette files + * runtime expects indexed pixels + packed RGB565 palettes + +### 14.2 Virtual Asset Contract + +* Inputs are explicit. +* Build steps are deterministic. +* Outputs match a well-defined runtime payload format. + +### 14.3 Examples of Future Virtual Assets + +* `TILES/indexed_v1`: PNG + palette files → indexed pixels + packed palettes +* `SOUNDS/pcm16le_v1`: WAV → PCM16LE +* `SPRITESHEET/atlas_v1`: multiple PNG frames → atlas + metadata + +Each `output.format` must have its own dedicated spec. + +--- + +## 15. Integration with Cartridge Builder + +The Cartridge Builder should: + +1. Compile PBS into bytecode (e.g. `program.pbc` / `program.pbx`) +2. Call `prometeu packer build` +3. Consume `build/asset_table.json` and produce `cartridge/manifest.json` +4. Copy artifacts into `cartridge/` +5. Zip the cartridge into a distributable format (`.crt` / `.rom` / `.pro`) + +The packer never touches `cartridge/`. + +--- + +## 16. Compatibility and Versioning + +* `assets/.prometeu/index.json` has `schema_version` +* `asset.json` has `schema_version` +* `build/asset_table.json` has `schema_version` + +The Packer must be able to migrate older schema versions or emit actionable diagnostics. + +--- + +## 17. Security and Trust Model + +* The Packer is offline tooling. +* It must never execute untrusted scripts. +* It should treat external inputs as untrusted data. + +--- + +## 18. Implementation Notes (Non-Normative) + +* Rust implementation with a core crate + CLI wrapper. +* Prefer structured JSON serde models. +* Use stable diagnostic codes. +* Keep the build deterministic. + +--- + +## Appendix A — Glossary + +* **ROM (`assets.pa`)**: packed asset payload used by runtime +* **Descriptor (`asset_table.json`)**: mapping from logical assets to ROM slices +* **Managed asset**: registered asset with stable identity and anchor file +* **Virtual asset**: derived asset built from multiple inputs +* **Quarantine**: safe area for suspected junk +* **Doctor**: diagnostic command to keep sanity diff --git a/test-cartridges/canonical/golden/ast.json b/test-cartridges/canonical/golden/ast.json index 89bd9f99..3f4e0529 100644 --- a/test-cartridges/canonical/golden/ast.json +++ b/test-cartridges/canonical/golden/ast.json @@ -2,12 +2,7 @@ "kind": "File", "imports": [], "decls": [ - 21, - 26, - 39, - 44, - 48, - 57, - 103 + 30, + 40 ] } \ No newline at end of file diff --git a/test-cartridges/canonical/golden/program.disasm.txt b/test-cartridges/canonical/golden/program.disasm.txt index e1695a89..8763fe6a 100644 --- a/test-cartridges/canonical/golden/program.disasm.txt +++ b/test-cartridges/canonical/golden/program.disasm.txt @@ -1,176 +1,19 @@ -0000 GetLocal U32(0) -0006 GetLocal U32(1) -000C Add -000E Ret -0010 PushConst U32(1) -0016 SetLocal U32(0) -001C PushConst U32(2) -0022 SetLocal U32(1) -0028 GetLocal U32(0) -002E GetLocal U32(1) -0034 Call U32(0) -003A SetLocal U32(2) -0040 GetLocal U32(2) -0046 PushConst U32(3) -004C Eq -004E JmpIfFalse U32(92) -0054 Jmp U32(74) -005A PushBounded U32(2016) -0060 Syscall U32(4112) -0066 Jmp U32(110) -006C PushBounded U32(63488) -0072 Syscall U32(4112) -0078 Jmp U32(110) -007E Syscall U32(8208) -0084 GetLocal U32(50) -008A GateRelease -008C SetLocal U32(50) -0092 GetLocal U32(49) -0098 GateRelease -009A SetLocal U32(49) -00A0 GetLocal U32(48) -00A6 GateRelease -00A8 SetLocal U32(48) -00AE GetLocal U32(47) -00B4 GateRelease -00B6 SetLocal U32(47) -00BC GetLocal U32(46) -00C2 GateRelease -00C4 SetLocal U32(46) -00CA GetLocal U32(45) -00D0 GateRelease -00D2 SetLocal U32(45) -00D8 GetLocal U32(44) -00DE GateRelease -00E0 SetLocal U32(44) -00E6 GetLocal U32(43) -00EC GateRelease -00EE SetLocal U32(43) -00F4 GetLocal U32(42) -00FA GateRelease -00FC SetLocal U32(42) -0102 GetLocal U32(41) -0108 GateRelease -010A SetLocal U32(41) -0110 GetLocal U32(40) -0116 GateRelease -0118 SetLocal U32(40) -011E GetLocal U32(39) -0124 GateRelease -0126 SetLocal U32(39) -012C GetLocal U32(38) -0132 GateRelease -0134 SetLocal U32(38) -013A GetLocal U32(37) -0140 GateRelease -0142 SetLocal U32(37) -0148 GetLocal U32(36) -014E GateRelease -0150 SetLocal U32(36) -0156 GetLocal U32(35) -015C GateRelease -015E SetLocal U32(35) -0164 GetLocal U32(34) -016A GateRelease -016C SetLocal U32(34) -0172 GetLocal U32(33) -0178 GateRelease -017A SetLocal U32(33) -0180 GetLocal U32(32) -0186 GateRelease -0188 SetLocal U32(32) -018E GetLocal U32(31) -0194 GateRelease -0196 SetLocal U32(31) -019C GetLocal U32(30) -01A2 GateRelease -01A4 SetLocal U32(30) -01AA GetLocal U32(29) -01B0 GateRelease -01B2 SetLocal U32(29) -01B8 GetLocal U32(28) -01BE GateRelease -01C0 SetLocal U32(28) -01C6 GetLocal U32(27) -01CC GateRelease -01CE SetLocal U32(27) -01D4 GetLocal U32(26) -01DA GateRelease -01DC SetLocal U32(26) -01E2 GetLocal U32(25) -01E8 GateRelease -01EA SetLocal U32(25) -01F0 GetLocal U32(24) -01F6 GateRelease -01F8 SetLocal U32(24) -01FE GetLocal U32(23) -0204 GateRelease -0206 SetLocal U32(23) -020C GetLocal U32(22) -0212 GateRelease -0214 SetLocal U32(22) -021A GetLocal U32(21) -0220 GateRelease -0222 SetLocal U32(21) -0228 GetLocal U32(20) -022E GateRelease -0230 SetLocal U32(20) -0236 GetLocal U32(19) -023C GateRelease -023E SetLocal U32(19) -0244 GetLocal U32(18) -024A GateRelease -024C SetLocal U32(18) -0252 GetLocal U32(17) -0258 GateRelease -025A SetLocal U32(17) -0260 GetLocal U32(16) -0266 GateRelease -0268 SetLocal U32(16) -026E GetLocal U32(15) -0274 GateRelease -0276 SetLocal U32(15) -027C GetLocal U32(14) -0282 GateRelease -0284 SetLocal U32(14) -028A GetLocal U32(13) -0290 GateRelease -0292 SetLocal U32(13) -0298 GetLocal U32(12) -029E GateRelease -02A0 SetLocal U32(12) -02A6 GetLocal U32(11) -02AC GateRelease -02AE SetLocal U32(11) -02B4 GetLocal U32(10) -02BA GateRelease -02BC SetLocal U32(10) -02C2 GetLocal U32(9) -02C8 GateRelease -02CA SetLocal U32(9) -02D0 GetLocal U32(8) -02D6 GateRelease -02D8 SetLocal U32(8) -02DE GetLocal U32(7) -02E4 GateRelease -02E6 SetLocal U32(7) -02EC GetLocal U32(6) -02F2 GateRelease -02F4 SetLocal U32(6) -02FA GetLocal U32(5) -0300 GateRelease -0302 SetLocal U32(5) -0308 GetLocal U32(4) -030E GateRelease -0310 SetLocal U32(4) -0316 GetLocal U32(3) -031C GateRelease -031E SetLocal U32(3) -0324 GetLocal U32(21) -032A JmpIfFalse U32(824) -0330 Jmp U32(806) -0336 PushBounded U32(31) -033C Syscall U32(4112) -0342 Jmp U32(830) -0348 Jmp U32(830) -034E Ret +0000 PushConst U32(1) +0006 SetLocal U32(0) +000C GetLocal U32(0) +0012 GetLocal U32(0) +0018 SetLocal U32(1) +001E SetLocal U32(0) +0024 GetLocal U32(0) +002A GetLocal U32(1) +0030 Call U32(1) +0036 SetLocal U32(2) +003C Ret +003E GetLocal U32(0) +0044 GetLocal U32(0) +004A GetLocal U32(1) +0050 Add +0052 Mul +0054 GetLocal U32(1) +005A Mul +005C Ret diff --git a/test-cartridges/canonical/golden/program.pbc b/test-cartridges/canonical/golden/program.pbc index 666448e9c82fd9759bc1514ce6c916eaa9193fac..c6bfdbc2c24deb0f5a42086930c3161c7542f9c4 100644 GIT binary patch literal 576 zcmZvZyGjF56h*I(h=qh`A+eDVVq=j33kwU2iB+ocfrV(ukc^;YA|%+^sjXk6kbr^s zE&36zeedx}2G-fL&pziek0d^Jx(dF|F6Xq1FbCIAgL!y@7A(LAY%#Eit`bJ5X8e*g zICS4T?S%UZs!5$*^0mR8bMdd*n9T|Av6doH1{F+4J7-^}XNu&iA|X)%j{; z8-nHfZJfp zqB!REIj-gg?HzXJ(Q~fUR$`u`nzS^VK2?*J3|+4-&DNw@PklD+V&+XNiW4g@>G2LDXF&$?;ckE2J znIWbduOzq0bej=kx=jx;-KK?@Zc{@{$Cb!!h3Pgq#B`e!V!9QFm~JnJm~OnixGzk% z2_dFiQHbd_KE(8N$MJKx4WO4|j_HOQIc<~aHa5g`D-1E+#)Oz|yyv-1rrV1lrrW3x z(+#&^T9fHEGQ{-sOmZuHHa$H}6rU;rR>y*E@m!#$FSK4=2 zKi>+mWGsr`#+Jo+Jc>ue#(A1&hHW**YfwD;)&v^ z;!^P}@oe#2aiw^nc(Hhic&T`WxK_MM{GqsB+#qfheZxU}2?-1`1?-d^qe7{PS?^y=# zDQttkZ{l%czQ+w+zgRq7Tp}(DZ^XM#7iwYnCN}yPYH@fM=DtS7?x)>uY)-!(d}VFz lY;14g9RGdrow2L4siM8{(~gSv&zd*4wRBabH8?xC`u{?}BZ>e3 diff --git a/test-cartridges/canonical/src/main/modules/main.pbs b/test-cartridges/canonical/src/main/modules/main.pbs index b9c426aa..fb5f29d8 100644 --- a/test-cartridges/canonical/src/main/modules/main.pbs +++ b/test-cartridges/canonical/src/main/modules/main.pbs @@ -1,66 +1,18 @@ -// CartridgeCanonical.pbs -// Purpose: VM Heartbeat Test (Industrial Baseline) - -declare struct Color(raw: bounded) +declare struct Vec2(x: int, y: int) +[ + (x: int, y: int): (x, y) as default + (s: int): (s, s) as square +] [[ - BLACK: Color(0b), - WHITE: Color(65535b), - RED: Color(63488b), - GREEN: Color(2016b), - BLUE: Color(31b) + ZERO: square(0) ]] - -declare struct ButtonState( - pressed: bool, - released: bool, - down: bool, - hold_frames: bounded -) - -declare struct Pad( - up: ButtonState, - down: ButtonState, - left: ButtonState, - right: ButtonState, - a: ButtonState, - b: ButtonState, - x: ButtonState, - y: ButtonState, - l: ButtonState, - r: ButtonState, - start: ButtonState, - select: ButtonState -) - -declare contract Gfx host { - fn clear(color: Color): void; -} - -declare contract Input host { - fn pad(): Pad; -} - -fn add(a: int, b: int): int { - return a + b; +{ + pub fn len(self: this): int { + return x * x + y * y; + } } fn frame(): void { - // 1. Locals & Arithmetic - let x = 10; - let y = 20; - let z = add(x, y); - - // 2. Control Flow (if) - if z == 30 { - // 3. Syscall Clear - Gfx.clear(Color.GREEN); - } else { - Gfx.clear(Color.RED); - } - - // 4. Input Snapshot & Nested Member Access - let p = Input.pad(); - if p.a.down { - Gfx.clear(Color.BLUE); - } + let zero = Vec2.ZERO; + let zz = zero.len(); } diff --git a/test-cartridges/test01/cartridge/program.pbc b/test-cartridges/test01/cartridge/program.pbc index b166cd8549c58a559378206733a130ffbf606dfb..13dbb99ed36aa408261d9cd13e96a9eb16bcb117 100644 GIT binary patch literal 2495 zcmZveS!_&E9LE1s)l$l!ma&eVqOGD24T;PRFG{8eCANtfZKb7|S{j5zA`uVb!4ruH z(MT+5Yl*Z8O(O`J6p1AfK|Ju{p(M5!-}j$$b><&8{l4@4&Ng?>IrmPZku}1rzxw}e z6RKxJ0q7VMhq9nAP!mEwlrdOwjz~80se#BSb@BE z$^qqt$l)|zi(J6YQbv;+jr`24L2H=bU=dn9;Tn6gK)qCbzIur|x73p&^?B-Z)#s=es`Hq5 zGE04?`V95y>fBFHrm9a-pR68MpQO&?;>iT{@#^E$$Et_a$Ec51AEiE0o#)Au5$eO$ zhpA%$P(M_Ch*BYQxn|^R$emE? zxetK~oIR zGF)VMq2Y4FOARkKTw!>%;VQ#h495-cG~8f#pW#D>j~YI1_@v>}hR+y2Z@Ag;Rl{wD z?-=HD&i(cG>%QT~hIMEw@NdJs+4TF(F`RmD_I;n!`?8P6nEn$D zPcvL-xWq8uBl!Ebev2KqV{s66Y+>QJZ;yVU^l8^5>1mgb^aUpq;}K=1p8zEtd}R}lABN$Gwwvao(HtMxiF6<_bpn7xFz-LX<119t(9%s)Bf4* zI&cm=o0im4NWZQn?VCN;fA=f(-+=9Tz4EpR%SCqXTC?3&z|YFta9U$mrmsP3k-wu6 zt-EkkX1BXS%x(=KW>43aTk;yat%^2gw>v}3ZYx8~Zg+&3-ReTjZnuY+-ByH{-EIpp zyVZu6-EIvrd%ANxGu}bBWzoj$Ruf`&TN+|^+_}zF2w9MGsNtMA(*ygb}J7tdz$y` zh4bIjjHNg;J&k;d)2(zRX4o1aZ?#x1O804UY_Ch}x$Ll`*q$HGqBDrI2p*089&sMQ zUo_cH^UBama%R;CUVVBg>KX*^@MJs9(LfhaFGG}`8F=oXZa{EUv%LxRYOV{jo#u0e z?YmGvglG;Ap&mxGhPR^Ljc5yxq27z&bBO!Dhx&a)SNKiT@`YXg> z_y^Pzh>`G*sDDE23x9+90^&fp0tcOgcse`}^?by^Ognuz+={vbaU^^X>dgq=BP1Ke zVhnLSJb-#HVmv&K`cuTo@Y|?AV~D>HpAw%Ib4;Kco9u5wd{O+1__7!e)oK0xDdqz_ zd%SY-OmT&{QoKN1CFU5-9&fq0PRucz-M&%0M!ZhEUfe2f6W=TD6nBZYhUrhjjp9w>cJck<2gF;&J>ov`pm;?5h`uM^@A#V5u5kHdFr|F6ZT#HYpQ#S`L-;!EPo;w$2R#C%D^s;ToW7ta<~ zh%3d5#Z}@O@d|OBxIw&HTzap(dj6&N$*bNV+qa6_#GA#P;x6%a@eXl+nD_lGVo&%t z)PE!PhcBS!y(z=J;0LR%yQc?F$MlLmFuJXOU}t|Xr)Sy*ZH?T9@85kR-9x=>n6`r8 a+wkbfgY`q*1H<)05BEGe*gM*sHvbpSm9j?w