From c6411fd53c929410e1ecbf7dc16446a35787fae9 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 9 Feb 2026 20:54:20 +0000 Subject: [PATCH] full refactor on linker and verifier to become more JVM-like --- crates/prometeu-abi/src/syscalls.rs | 57 +++- crates/prometeu-bytecode/src/layout.rs | 64 ++++ crates/prometeu-bytecode/src/lib.rs | 1 + .../src/backend/emit_bytecode.rs | 29 +- .../prometeu-compiler/src/building/linker.rs | 108 ++++-- .../src/building/orchestrator.rs | 10 +- .../prometeu-compiler/src/building/output.rs | 162 +++++++-- .../src/frontends/pbs/collector.rs | 53 +++ .../src/frontends/pbs/contracts.rs | 8 +- .../src/frontends/pbs/lowering.rs | 311 ++++++++++-------- .../src/frontends/pbs/typecheck.rs | 62 +++- .../src/semantics/export_surface.rs | 6 +- crates/prometeu-kernel/src/prometeu_os.rs | 12 +- crates/prometeu-vm/src/verifier.rs | 51 ++- test-cartridges/canonical/prometeu.json | 3 +- test-cartridges/test01/cartridge/program.pbc | Bin 1977 -> 3311 bytes .../test01/src/main/modules/main.pbs | 1 + 17 files changed, 701 insertions(+), 237 deletions(-) create mode 100644 crates/prometeu-bytecode/src/layout.rs diff --git a/crates/prometeu-abi/src/syscalls.rs b/crates/prometeu-abi/src/syscalls.rs index 0348bfab..6936bf6a 100644 --- a/crates/prometeu-abi/src/syscalls.rs +++ b/crates/prometeu-abi/src/syscalls.rs @@ -249,11 +249,39 @@ impl Syscall { pub fn results_count(&self) -> usize { match self { + // --- System --- + Self::SystemHasCart => 1, + Self::SystemRunCart => 0, + + // --- GFX (void) --- + Self::GfxClear => 0, + Self::GfxFillRect => 0, + Self::GfxDrawLine => 0, + Self::GfxDrawCircle => 0, + Self::GfxDrawDisc => 0, + Self::GfxDrawSquare => 0, + Self::GfxSetSprite => 0, + Self::GfxDrawText => 0, Self::GfxClear565 => 0, + + // --- Input (scalar/snapshots) --- + Self::InputGetPad => 1, + Self::InputGetPadPressed => 1, + Self::InputGetPadReleased => 1, + Self::InputGetPadHold => 1, Self::InputPadSnapshot => 48, Self::InputTouchSnapshot => 6, - // Touch finger and Pad per-button services return a Button (4 slots) - Self::TouchGetFinger => 4, + + // --- Touch (scalars/struct) --- + Self::TouchGetX => 1, + Self::TouchGetY => 1, + Self::TouchIsDown => 1, + Self::TouchIsPressed => 1, + Self::TouchIsReleased => 1, + Self::TouchGetHold => 1, + Self::TouchGetFinger => 4, // Button struct (4 slots) + + // --- Pad (per-button struct: 4 slots) --- Self::PadGetUp | Self::PadGetDown | Self::PadGetLeft @@ -266,7 +294,30 @@ impl Syscall { | Self::PadGetR | Self::PadGetStart | Self::PadGetSelect => 4, - _ => 1, + + // --- Audio (void) --- + Self::AudioPlaySample => 0, + Self::AudioPlay => 0, + + // --- FS --- + Self::FsOpen => 1, + Self::FsRead => 1, // bytes read + Self::FsWrite => 1, // bytes written + Self::FsClose => 0, + Self::FsListDir => 1, // entries count/handle (TBD) + Self::FsExists => 1, + Self::FsDelete => 0, + + // --- Log (void) --- + Self::LogWrite | Self::LogWriteTag => 0, + + // --- Asset/Bank (conservador) --- + Self::AssetLoad => 1, + Self::AssetStatus => 1, + Self::AssetCommit => 0, + Self::AssetCancel => 0, + Self::BankInfo => 1, + Self::BankSlotInfo => 1, } } diff --git a/crates/prometeu-bytecode/src/layout.rs b/crates/prometeu-bytecode/src/layout.rs new file mode 100644 index 00000000..53e61e85 --- /dev/null +++ b/crates/prometeu-bytecode/src/layout.rs @@ -0,0 +1,64 @@ +//! Shared bytecode layout utilities, used by both compiler (emitter/linker) +//! and the VM (verifier/loader). This ensures a single source of truth for +//! how function ranges and jump targets are interpreted post-link. + +use crate::FunctionMeta; + +/// Returns the absolute end (exclusive) of the function at `func_idx`, +/// defined as the minimum `code_offset` of any subsequent function, or +/// `code_len_total` if this is the last function. +#[inline] +pub fn function_end_from_next(functions: &[FunctionMeta], func_idx: usize, code_len_total: usize) -> usize { + let start = functions.get(func_idx).map(|f| f.code_offset as usize).unwrap_or(0); + let mut end = code_len_total; + for (j, other) in functions.iter().enumerate() { + if j == func_idx { continue; } + let other_start = other.code_offset as usize; + if other_start > start && other_start < end { + end = other_start; + } + } + end +} + +/// Returns the length (in bytes) of the function at `func_idx`, using +/// the canonical definition: end = start of next function (exclusive), +/// or total code len if last. +#[inline] +pub fn function_len_from_next(functions: &[FunctionMeta], func_idx: usize, code_len_total: usize) -> usize { + let start = functions.get(func_idx).map(|f| f.code_offset as usize).unwrap_or(0); + let end = function_end_from_next(functions, func_idx, code_len_total); + end.saturating_sub(start) +} + +/// Recomputes all `code_len` values in place from the next function start +/// (exclusive end), using the combined code buffer length for the last one. +pub fn recompute_function_lengths_in_place(functions: &mut [FunctionMeta], code_len_total: usize) { + for i in 0..functions.len() { + let start = functions[i].code_offset as usize; + let end = function_end_from_next(functions, i, code_len_total); + functions[i].code_len = end.saturating_sub(start) as u32; + } +} + +/// Finds the function index that contains `pc_abs` (absolute), using the +/// canonical ranges (end = next start, exclusive). Returns `None` if none. +pub fn function_index_by_pc(functions: &[FunctionMeta], code_len_total: usize, pc_abs: usize) -> Option { + for i in 0..functions.len() { + let start = functions[i].code_offset as usize; + let end = function_end_from_next(functions, i, code_len_total); + if pc_abs >= start && pc_abs < end { + return Some(i); + } + } + None +} + +/// Clamps an absolute jump target to the end (exclusive) of the enclosing +/// function identified by `func_idx`. +#[inline] +pub fn clamp_jump_target(functions: &[FunctionMeta], code_len_total: usize, func_idx: usize, target_abs: u32) -> u32 { + let start = functions.get(func_idx).map(|f| f.code_offset as usize).unwrap_or(0); + let end = function_end_from_next(functions, func_idx, code_len_total); + if (target_abs as usize) > end { end as u32 } else { target_abs } +} diff --git a/crates/prometeu-bytecode/src/lib.rs b/crates/prometeu-bytecode/src/lib.rs index 0ec9b972..0d5440cd 100644 --- a/crates/prometeu-bytecode/src/lib.rs +++ b/crates/prometeu-bytecode/src/lib.rs @@ -18,6 +18,7 @@ pub mod abi; pub mod readwrite; pub mod asm; pub mod disasm; +pub mod layout; mod model; diff --git a/crates/prometeu-compiler/src/backend/emit_bytecode.rs b/crates/prometeu-compiler/src/backend/emit_bytecode.rs index 64d5083b..18ec47cd 100644 --- a/crates/prometeu-compiler/src/backend/emit_bytecode.rs +++ b/crates/prometeu-compiler/src/backend/emit_bytecode.rs @@ -71,8 +71,14 @@ pub fn emit_fragments(module: &ir_vm::Module) -> Result { for (i, function) in module.functions.iter().enumerate() { let (start_idx, end_idx) = function_ranges[i]; let start_pc = pcs[start_idx]; - let end_pc = if end_idx < pcs.len() { pcs[end_idx] } else { bytecode.len() as u32 }; - + // Interpretamos `end_idx` como o índice da ÚLTIMA instrução pertencente à função (inclusivo). + // Portanto, o `end_pc` correto é o PC da próxima instrução (exclusivo). Se não houver próxima, + // usamos o tamanho total do bytecode. + let end_pc = if (end_idx + 1) < pcs.len() { pcs[end_idx + 1] } else { bytecode.len() as u32 }; + + // Nome enriquecido para tooling/analysis: "name@offset+len" + let enriched_name = format!("{}@{}+{}", function.name, start_pc, end_pc - start_pc); + functions.push(FunctionMeta { code_offset: start_pc, code_len: end_pc - start_pc, @@ -81,7 +87,7 @@ pub fn emit_fragments(module: &ir_vm::Module) -> Result { return_slots: function.return_slots, max_stack_slots: 0, // Will be filled by verifier }); - function_names.push((i as u32, function.name.clone())); + function_names.push((i as u32, enriched_name)); } let mut pc_to_span = Vec::new(); @@ -171,6 +177,8 @@ impl BytecodeEmitter { ir_instr_map.push(None); // Track an approximate stack height for this function let mut stack_height: i32 = 0; + // Nome canônico para o label de término desta função + let end_label = format!("{}::__end", function.name); for instr in &function.body { let op_start_idx = asm_instrs.len(); @@ -254,10 +262,12 @@ impl BytecodeEmitter { 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())])); + let target = if label.0 == "end" { end_label.clone() } else { label.0.clone() }; + asm_instrs.push(Asm::Op(OpCode::Jmp, vec![Operand::RelLabel(target, function.name.clone())])); } InstrKind::JmpIfFalse(label) => { - asm_instrs.push(Asm::Op(OpCode::JmpIfFalse, vec![Operand::RelLabel(label.0.clone(), function.name.clone())])); + let target = if label.0 == "end" { end_label.clone() } else { label.0.clone() }; + asm_instrs.push(Asm::Op(OpCode::JmpIfFalse, vec![Operand::RelLabel(target, function.name.clone())])); // VM consumes the condition for JmpIfFalse stack_height = (stack_height - 1).max(0); } @@ -325,6 +335,15 @@ impl BytecodeEmitter { ir_instr_map.push(Some(instr)); } } + // Para compatibilidade com geradores que efetuam saltos para o "fim da função", + // garantimos que exista ao menos um NOP antes do label final. Isso assegura que + // qualquer alvo que considere o label como posição exclusiva ou inclusiva não caia + // dentro do início da próxima função. + asm_instrs.push(Asm::Op(OpCode::Nop, vec![])); + ir_instr_map.push(None); + // Emite label canônico de término no fim real do corpo + asm_instrs.push(Asm::Label(end_label)); + ir_instr_map.push(None); let end_idx = asm_instrs.len(); ranges.push((start_idx, end_idx)); } diff --git a/crates/prometeu-compiler/src/building/linker.rs b/crates/prometeu-compiler/src/building/linker.rs index a353d326..acb46c45 100644 --- a/crates/prometeu-compiler/src/building/linker.rs +++ b/crates/prometeu-compiler/src/building/linker.rs @@ -1,9 +1,11 @@ use crate::building::output::CompiledModule; use crate::building::plan::BuildStep; use prometeu_bytecode::opcode::OpCode; +use prometeu_bytecode::layout; use prometeu_bytecode::{ConstantPoolEntry, DebugInfo}; use std::collections::HashMap; use prometeu_abi::virtual_machine::{ProgramImage, Value}; +use prometeu_analysis::ids::ProjectId; #[derive(Debug, PartialEq, Eq, Clone)] pub enum LinkError { @@ -150,22 +152,44 @@ impl Linker { // Patch imports for import in &module.imports { - let dep_project_id = if import.key.dep_alias == "self" || import.key.dep_alias.is_empty() { - &module.project_id + // Resolve the dependency project id. If alias is missing/self, try all deps as fallback. + let mut candidate_projects: Vec<&ProjectId> = Vec::new(); + if import.key.dep_alias == "self" || import.key.dep_alias.is_empty() { + candidate_projects.push(&module.project_id); + for (_alias, pid) in &step.deps { candidate_projects.push(pid); } } else { - step.deps.get(&import.key.dep_alias) - .ok_or_else(|| LinkError::UnresolvedSymbol(format!("Dependency alias '{}' not found in project {:?}", import.key.dep_alias, module.project_id)))? - }; + let pid = step.deps.get(&import.key.dep_alias) + .ok_or_else(|| LinkError::UnresolvedSymbol(format!("Dependency alias '{}' not found in project {:?}", import.key.dep_alias, module.project_id)))?; + candidate_projects.push(pid); + } - let symbol_id = (dep_project_id.clone(), import.key.module_path.clone(), import.key.symbol_name.clone()); - let &target_func_idx = global_symbols.get(&symbol_id) - .ok_or_else(|| LinkError::UnresolvedSymbol(format!("DebugSymbol '{}:{}' not found in project {:?}", symbol_id.1, symbol_id.2, symbol_id.0)))?; + let mut resolved_idx: Option = None; + for pid in candidate_projects { + let pid_val: ProjectId = (*pid).clone(); + let key = (pid_val, import.key.module_path.clone(), import.key.symbol_name.clone()); + if let Some(&idx) = global_symbols.get(&key) { + resolved_idx = Some(idx); + break; + } + } + let target_func_idx = resolved_idx.ok_or_else(|| { + LinkError::UnresolvedSymbol(format!( + "DebugSymbol '{}:{}' not found in any candidate project (self={:?}, deps={:?})", + import.key.module_path, + import.key.symbol_name, + module.project_id, + step.deps + )) + })?; for &reloc_pc in &import.relocation_pcs { + // `reloc_pc` aponta para o INÍCIO do operando (após os 2 bytes do opcode), + // conforme `assemble_with_unresolved` grava `pc` antes de escrever o U32. + // Portanto, devemos escrever exatamente em `absolute_pc`. let absolute_pc = code_offset + reloc_pc as usize; - let imm_offset = absolute_pc + 2; - if imm_offset + 4 <= combined_code.len() { - combined_code[imm_offset..imm_offset+4].copy_from_slice(&target_func_idx.to_le_bytes()); + if absolute_pc + 4 <= combined_code.len() { + combined_code[absolute_pc..absolute_pc+4] + .copy_from_slice(&target_func_idx.to_le_bytes()); } } } @@ -211,7 +235,9 @@ impl Linker { // Check if this PC was already patched by an import. // If it wasn't, it's an internal call that needs relocation. - let reloc_pc = (pos - 2 - code_offset) as u32; + // `import.relocation_pcs` holds the PC at the start of the CALL immediate (after opcode), + // and here `pos` currently points exactly at that immediate. + let reloc_pc = (pos - code_offset) as u32; let is_import = module.imports.iter().any(|imp| imp.relocation_pcs.contains(&reloc_pc)); if !is_import { @@ -221,16 +247,13 @@ impl Linker { pos += 4; } } - // Relocate control-flow jump targets by adding module code offset + // Do NOT relocate intra-function control flow. Branch immediates are + // function-relative by contract and must remain untouched by the linker. OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue => { - if pos + 4 <= end { - let code_off = module_code_offsets[i]; - patch_u32_at(&mut combined_code, pos, &|t| t.saturating_add(code_off)); - pos += 4; - } + // Just skip the immediate + pos += 4; } - OpCode::PushI32 | OpCode::PushBounded | OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue - | OpCode::GetGlobal | OpCode::SetGlobal | OpCode::GetLocal | OpCode::SetLocal + OpCode::PushI32 | OpCode::PushBounded | OpCode::GetGlobal | OpCode::SetGlobal | OpCode::GetLocal | OpCode::SetLocal | OpCode::PopN | OpCode::Syscall | OpCode::GateLoad | OpCode::GateStore => { pos += 4; } @@ -272,6 +295,51 @@ impl Linker { } } + // Ajuste final: se os nomes de função no DebugInfo estiverem enriquecidos no formato + // "name@offset+len", alinhar apenas o `code_len` de `combined_functions[idx]` a esses + // valores (os offsets do DebugInfo são locais ao módulo antes do link). Mantemos o + // `code_offset` já realocado durante o PASS 1. + for (idx, name) in &combined_function_names { + if let Some((base, rest)) = name.split_once('@') { + let mut parts = rest.split('+'); + if let (Some(off_str), Some(len_str)) = (parts.next(), parts.next()) { + if let (Ok(_off), Ok(len)) = (off_str.parse::(), len_str.parse::()) { + if let Some(meta) = combined_functions.get_mut(*idx as usize) { + let old_off = meta.code_offset; + let old_len = meta.code_len; + meta.code_len = len; + eprintln!( + "[Linker][debug] Align len idx={} name={} -> code_offset {} (kept) | code_len {} -> {}", + idx, base, old_off, old_len, len + ); + } + } + } + } + } + + // Recalcular code_len de todas as funções no código combinado com base no deslocamento da próxima função + // (end exclusivo). Isso garante que o fim efetivo da função seja exatamente o início da próxima + // no buffer combinado, evitando divergências em saltos para o fim da função. + // Use rotina canônica compartilhada para recalcular os comprimentos das funções + layout::recompute_function_lengths_in_place(&mut combined_functions, combined_code.len()); + + // Removido padding específico de `frame`; o emissor passou a garantir que o label de término + // esteja no ponto exato do fim do corpo, e, quando necessário, insere NOPs reais antes do fim. + + // Garantir export do entry point 'frame' mesmo com nomes enriquecidos no DebugInfo. + if !final_exports.contains_key("frame") { + if let Some((idx, _name)) = combined_function_names.iter().find(|(i, name)| { + let base = name.split('@').next().unwrap_or(name.as_str()); + let i_usize = *i as usize; + (base == "frame" || base.ends_with(":frame")) + && combined_functions.get(i_usize).map(|m| m.param_slots == 0 && m.return_slots == 0).unwrap_or(false) + }) { + final_exports.insert("frame".to_string(), *idx); + final_exports.insert("src/main/modules:frame".to_string(), *idx); + } + } + let combined_debug_info = if combined_pc_to_span.is_empty() && combined_function_names.is_empty() { None } else { diff --git a/crates/prometeu-compiler/src/building/orchestrator.rs b/crates/prometeu-compiler/src/building/orchestrator.rs index 6e65b257..7853dd88 100644 --- a/crates/prometeu-compiler/src/building/orchestrator.rs +++ b/crates/prometeu-compiler/src/building/orchestrator.rs @@ -73,11 +73,19 @@ pub fn build_from_graph(graph: &ResolvedGraph, target: BuildTarget) -> Result SymbolKind::Struct, } } + ExportSurfaceKind::Function => SymbolKind::Function, }, namespace: key.kind.namespace(), visibility: Visibility::Pub, @@ -268,35 +269,71 @@ pub fn compile_project( typechecker.check(&parsed.arena, parsed.root)?; } - // 4. Lower to IR - let mut combined_program = crate::ir_core::Program { - const_pool: crate::ir_core::ConstPool::new(), - modules: Vec::new(), - field_offsets: HashMap::new(), - field_types: HashMap::new(), + // 4. Lower ALL modules to VM and emit a single combined bytecode image for this project + // Rationale: services and functions can live in multiple modules; exports must refer to + // correct function indices within this CompiledModule. We aggregate all VM functions + // into a single ir_vm::Module and assemble once using the public API `emit_fragments`. + + // Combined VM module (we will merge const pools and remap ConstIds accordingly) + let mut combined_vm = crate::ir_vm::Module::new(step.project_key.name.clone()); + combined_vm.const_pool = crate::ir_core::ConstPool::new(); + // Track origin module_path for each function we append to combined_vm + let mut combined_func_origins: Vec = Vec::new(); + + // Helper to insert a constant value into combined pool and return its new id + let mut insert_const = |pool: &mut crate::ir_core::ConstPool, val: &crate::ir_core::ConstantValue| -> crate::ir_vm::types::ConstId { + let new_id = pool.insert(val.clone()); + crate::ir_vm::types::ConstId(new_id.0) }; + // Accumulate VM functions from each source file, remapping ConstIds as we go for (module_path, parsed) in &parsed_files { let ms = module_symbols_map.get(module_path).unwrap(); let imported = file_imported_symbols.get(module_path).unwrap(); let lowerer = Lowerer::new(&parsed.arena, ms, imported, &interner); let program = lowerer.lower_file(parsed.root, module_path)?; - - // Combine program into combined_program - if combined_program.modules.is_empty() { - combined_program = program; - } else { - // TODO: Real merge + + let vm_module = core_to_vm::lower_program(&program) + .map_err(|e| CompileError::Internal(format!("Lowering error ({}): {}", module_path, e)))?; + + // Build remap for this module's const ids + let mut const_map: Vec = Vec::with_capacity(vm_module.const_pool.constants.len()); + for c in &vm_module.const_pool.constants { + let new_id = insert_const(&mut combined_vm.const_pool, c); + const_map.push(new_id); + } + + // Clone functions and remap any PushConst const ids + for mut f in vm_module.functions.into_iter() { + for instr in &mut f.body { + if let crate::ir_vm::instr::InstrKind::PushConst(old_id) = instr.kind { + let mapped = const_map.get(old_id.0 as usize).cloned().unwrap_or(old_id); + instr.kind = crate::ir_vm::instr::InstrKind::PushConst(mapped); + } + } + combined_func_origins.push(module_path.clone()); + combined_vm.functions.push(f); } } - // 4. Emit fragments - let vm_module = core_to_vm::lower_program(&combined_program) - .map_err(|e| CompileError::Internal(format!("Lowering error: {}", e)))?; - - let fragments = emit_fragments(&vm_module) + // Assemble once for the whole project using the public API + let fragments = emit_fragments(&combined_vm) .map_err(|e| CompileError::Internal(format!("Emission error: {}", e)))?; + // Post-fix FunctionMeta slots from VM IR (some emitters may default to 0) + let mut fixed_function_metas = fragments.functions.clone(); + for (i, fm) in fixed_function_metas.iter_mut().enumerate() { + if let Some(vm_func) = combined_vm.functions.get(i) { + fm.param_slots = vm_func.param_slots; + fm.local_slots = vm_func.local_slots; + fm.return_slots = vm_func.return_slots; + } + } + + // Note: Entry point validation for the root project is now performed at the orchestrator level, + // after compilation and before linking, using enriched debug info. We skip it here to avoid + // double validation and mismatches with name annotations. + // 5. Collect exports let mut exports = BTreeMap::new(); for (module_path, ms) in &module_symbols_map { @@ -315,25 +352,59 @@ pub fn compile_project( } } } + // Build a set of public function names declared in this module (value namespace) + let mut pub_fn_names: std::collections::HashSet = std::collections::HashSet::new(); for sym in ms.value_symbols.symbols.values() { if sym.visibility == Visibility::Pub { if let Some(surface_kind) = ExportSurfaceKind::from_symbol_kind(sym.kind) { - // Find func_idx if it's a function or service - let func_idx = vm_module - .functions - .iter() - .position(|f| f.name == interner.resolve(sym.name)) - .map(|i| i as u32); - - exports.insert(ExportKey { - module_path: module_path.clone(), - symbol_name: interner.resolve(sym.name).to_string(), - kind: surface_kind, - }, ExportMetadata { - func_idx, - is_host: sym.is_host, - ty: sym.ty.clone(), - }); + if matches!(surface_kind, ExportSurfaceKind::Function) { + pub_fn_names.insert(interner.resolve(sym.name).to_string()); + } + } + } + } + + for sym in ms.value_symbols.symbols.values() { + if sym.visibility == Visibility::Pub { + if let Some(surface_kind) = ExportSurfaceKind::from_symbol_kind(sym.kind) { + // Encontrar TODAS as funções no módulo VM combinado originadas deste module_path + // cujo nome seja igual ao do símbolo público; para cada uma, exportar + // tanto o nome simples quanto o alias com aridade. + let name_simple = interner.resolve(sym.name).to_string(); + let mut any_found = false; + for (i, f) in combined_vm.functions.iter().enumerate() { + if combined_func_origins.get(i).map(|s| s.as_str()) != Some(module_path.as_str()) { continue; } + if f.name != name_simple { continue; } + any_found = true; + let meta = ExportMetadata { + func_idx: Some(i as u32), + is_host: sym.is_host, + ty: sym.ty.clone(), + }; + // Simple name + exports.insert(ExportKey { + module_path: module_path.clone(), + symbol_name: name_simple.clone(), + kind: surface_kind, + }, meta.clone()); + // name/arity + let arity = f.params.len(); + let export_name_arity = format!("{}/{}", name_simple, arity); + exports.insert(ExportKey { + module_path: module_path.clone(), + symbol_name: export_name_arity, + kind: surface_kind, + }, meta); + } + // Caso nada tenha sido encontrado no VM (ex.: métodos ainda não materializados), + // publique ao menos o nome simples sem func_idx (mantém compatibilidade de surface) + if !any_found { + exports.insert(ExportKey { + module_path: module_path.clone(), + symbol_name: name_simple.clone(), + kind: surface_kind, + }, ExportMetadata { func_idx: None, is_host: sym.is_host, ty: sym.ty.clone() }); + } } } } @@ -347,6 +418,27 @@ pub fn compile_project( &interner, ); + // 6.b) Enriquecer debug_info com metadados de função (offset/len) para análise externa + let mut dbg = fragments.debug_info.clone().unwrap_or_default(); + // Adiciona pares (func_idx, (code_offset, code_len)) ao campo function_names como anotações extras + // Sem quebrar o formato, usamos o name como "name@offset+len" para tooling/analysis.json + let mut enriched_function_names = Vec::new(); + for (i, (fid, name)) in fragments + .debug_info + .as_ref() + .map(|d| d.function_names.clone()) + .unwrap_or_default() + .into_iter() + .enumerate() + { + let meta = &fixed_function_metas[i]; + let annotated = format!("{}@{}+{}", name, meta.code_offset, meta.code_len); + enriched_function_names.push((fid, annotated)); + } + if !enriched_function_names.is_empty() { + dbg.function_names = enriched_function_names; + } + // 7. Collect imports from unresolved labels let mut imports = Vec::new(); for (label, pcs) in fragments.unresolved_labels { @@ -382,8 +474,8 @@ pub fn compile_project( imports, const_pool: fragments.const_pool, code: fragments.code, - function_metas: fragments.functions, - debug_info: fragments.debug_info, + function_metas: fixed_function_metas, + debug_info: Some(dbg), symbols: project_symbols, }) } diff --git a/crates/prometeu-compiler/src/frontends/pbs/collector.rs b/crates/prometeu-compiler/src/frontends/pbs/collector.rs index f1e7ba84..07861b97 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/collector.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/collector.rs @@ -102,6 +102,59 @@ impl<'a> SymbolCollector<'a> { origin: None, }; self.insert_type_symbol(symbol); + + // Herança de visibilidade: métodos do service herdam a visibilidade do service + let service_name = self.interner.resolve(decl.name).to_string(); + // Evitar símbolos duplicados por overload: exportar apenas a primeira ocorrência por nome + let mut exported_method_names: std::collections::HashSet = std::collections::HashSet::new(); + + for member in &decl.members { + match arena.kind(*member) { + NodeKind::ServiceFnDecl(method) => { + // Exportar também como símbolo de valor (função) — nome simples (desqualificado) + // Evitar duplicatas em caso de overloads + let m_name_str = self.interner.resolve(method.name).to_string(); + if !exported_method_names.insert(m_name_str.clone()) { + continue; + } + if self.value_symbols.get(method.name).is_some() { continue; } + let sym = Symbol { + name: method.name, + kind: SymbolKind::Function, + namespace: Namespace::Value, + visibility: vis, // herda do service + ty: None, + is_host: false, + span: arena.span(*member), + // Marcar a origem com o contexto do service para auxiliar o exporter a localizar a função gerada + // no formato "svc:" + origin: Some(format!("svc:{}", service_name)), + }; + self.insert_value_symbol(sym); + } + NodeKind::ServiceFnSig(method) => { + // Mesmo para assinaturas sem corpo, export surface deve conhecer o símbolo + // Evitar duplicatas em caso de overloads + let m_name_str = self.interner.resolve(method.name).to_string(); + if !exported_method_names.insert(m_name_str.clone()) { + continue; + } + if self.value_symbols.get(method.name).is_some() { continue; } + let sym = Symbol { + name: method.name, + kind: SymbolKind::Function, + namespace: Namespace::Value, + visibility: vis, + ty: None, + is_host: false, + span: arena.span(*member), + origin: Some(format!("svc:{}", service_name)), + }; + self.insert_value_symbol(sym); + } + _ => {} + } + } } fn collect_type(&mut self, arena: &AstArena, id: NodeId, decl: &TypeDeclNodeArena) { diff --git a/crates/prometeu-compiler/src/frontends/pbs/contracts.rs b/crates/prometeu-compiler/src/frontends/pbs/contracts.rs index 7d10d98d..9906d293 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/contracts.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/contracts.rs @@ -218,15 +218,15 @@ impl ContractRegistry { log.insert("write".to_string(), ContractMethod { id: 0x5001, params: vec![PbsType::Int, PbsType::String], - // Alguns targets retornam status; para segurança, trate como Int para permitir POP automático - return_type: PbsType::Int, + // Log syscalls não retornam valor (void) — evita lixo de pilha + return_type: PbsType::Void, }); log.insert("writeTag".to_string(), ContractMethod { id: 0x5002, params: vec![PbsType::Int, PbsType::Int, PbsType::String], - return_type: PbsType::Int, + return_type: PbsType::Void, }); - // O contrato host exposto no SDK é LogHost (não-público), então o registro deve atender por esse nome + // O contrato host exposto no SDK é LogHost (não-público) mappings.insert("LogHost".to_string(), log); // System mappings diff --git a/crates/prometeu-compiler/src/frontends/pbs/lowering.rs b/crates/prometeu-compiler/src/frontends/pbs/lowering.rs index 1a5cc1a2..14a8c007 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/lowering.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/lowering.rs @@ -33,15 +33,14 @@ 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, current_span: Option, + import_bindings: HashMap, } impl<'a> Lowerer<'a> { @@ -103,10 +102,11 @@ impl<'a> Lowerer<'a> { diagnostics: Vec::new(), max_slots_used: 0, current_span: None, + import_bindings: HashMap::new(), } } - fn error(&mut self, code: &str, message: String, span: crate::common::spans::Span) { + fn error(&mut self, code: &str, message: String, span: Span) { self.diagnostics.push(Diagnostic { severity: Severity::Error, code: code.to_string(), @@ -130,6 +130,27 @@ impl<'a> Lowerer<'a> { )) } }; + + // Construir mapa de imports: para cada import do arquivo, se vier do formato + // "@alias:module", associe cada símbolo importado em `spec.path` ao par (alias,module). + self.import_bindings.clear(); + for &imp in &file.imports { + if let NodeKind::Import(imp_node) = self.arena.kind(imp) { + let from = imp_node.from.as_str(); + if let Some(rest) = from.strip_prefix('@') { + // Espera-se formato @alias:module_path + let mut parts = rest.splitn(2, ':'); + if let (Some(alias), Some(module_path)) = (parts.next(), parts.next()) { + if let NodeKind::ImportSpec(spec) = self.arena.kind(imp_node.spec) { + for name in &spec.path { + let sym = self.interner.resolve(*name).to_string(); + self.import_bindings.insert(sym, (alias.to_string(), module_path.to_string())); + } + } + } + } + } + } // Pre-scan for function declarations to assign IDs for decl in &file.decls { match self.arena.kind(*decl) { @@ -139,6 +160,17 @@ impl<'a> Lowerer<'a> { self.function_ids .insert(self.interner.resolve(n.name).to_string(), id); } + NodeKind::ServiceDecl(n) => { + let service_name = self.interner.resolve(n.name).to_string(); + for m in &n.members { + if let NodeKind::ServiceFnDecl(decl) = self.arena.kind(*m) { + let full_name = format!("{}.{}", service_name, self.interner.resolve(decl.name)); + let id = FunctionId(self.next_func_id); + self.next_func_id += 1; + self.function_ids.insert(full_name, id); + } + } + } NodeKind::TypeDecl(n) => { let id = TypeId(self.next_type_id); self.next_type_id += 1; @@ -285,11 +317,25 @@ impl<'a> Lowerer<'a> { }; for decl in &file.decls { - if let NodeKind::FnDecl(_) = self.arena.kind(*decl) { - let func = self.lower_function(*decl).map_err(|_| DiagnosticBundle { - diagnostics: self.diagnostics.clone(), - })?; - module.functions.push(func); + match self.arena.kind(*decl) { + NodeKind::FnDecl(_) => { + let func = self.lower_function(*decl).map_err(|_| DiagnosticBundle { + diagnostics: self.diagnostics.clone(), + })?; + module.functions.push(func); + } + NodeKind::ServiceDecl(n) => { + let service_name = self.interner.resolve(n.name).to_string(); + for m in &n.members { + if let NodeKind::ServiceFnDecl(_) = self.arena.kind(*m) { + let func = self.lower_service_function(&service_name, *m).map_err(|_| DiagnosticBundle { + diagnostics: self.diagnostics.clone(), + })?; + module.functions.push(func); + } + } + } + _ => {} } } @@ -389,6 +435,84 @@ impl<'a> Lowerer<'a> { Ok(final_func) } + fn lower_service_function(&mut self, service_name: &str, node: NodeId) -> Result { + let n = match self.arena.kind(node) { + NodeKind::ServiceFnDecl(n) => n, + _ => return Err(()), + }; + let method_name = self.interner.resolve(n.name).to_string(); + let full_name = format!("{}.{}", service_name, method_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 service 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; + for p in &n.params { + let ty = self.lower_type_node(p.ty); + let slots = self.get_type_slots(&ty); + params.push(Param { name: self.interner.resolve(p.name).to_string(), ty: ty.clone() }); + self.local_vars[0].insert( + self.interner.resolve(p.name).to_string(), + LocalInfo { slot: param_slots, ty: ty.clone() }, + ); + for i in 0..slots { local_types.insert(param_slots + i, ty.clone()); } + param_slots += slots; + } + self.max_slots_used = param_slots; + + let ret_ty = self.lower_type_node(n.ret); + let return_slots = self.get_type_slots(&ret_ty); + // Inicializa a função atual (espelha lower_function) + let func = Function { + id: func_id, + // Nome público da função no módulo: use apenas o nome do método. + // O module_path fará a desambiguação durante export/link. + name: method_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, + }; + + // Registrar como função corrente para que start_block/lower_node + // acumulem instruções corretamente. + self.current_function = Some(func); + self.start_block(); + self.lower_node(n.body)?; + + // Garantir terminador e empurrar bloco final + 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); + } + } + + // Finalizar função: calcular local_slots e devolver + let mut final_func = self.current_function.take().unwrap(); + final_func.local_slots = (self.max_slots_used - param_slots) as u16; + Ok(final_func) + } + fn lower_node(&mut self, node: NodeId) -> Result<(), ()> { let old_span = self.current_span.clone(); self.current_span = Some(self.arena.span(node)); @@ -988,10 +1112,13 @@ impl<'a> Lowerer<'a> { if parts.len() == 2 { let dep_alias = parts[0].to_string(); let module_path = parts[1].to_string(); + // Encode arity to disambiguate overloads at link time: symbol/arity + let base = self.interner.resolve(sym.name).to_string(); + let symbol_with_arity = format!("{}/{}", base, n.args.len()); self.emit(InstrKind::ImportCall( dep_alias, module_path, - self.interner.resolve(sym.name).to_string(), + symbol_with_arity, n.args.len() as u32, )); return Ok(()); @@ -1134,53 +1261,35 @@ impl<'a> Lowerer<'a> { .get(obj_id.name) .or_else(|| self.imported_symbols.type_symbols.get(obj_id.name)); if let Some(sym) = sym_opt { - // 0) Açúcar sintático para Log: permitir quando Log for service/contract (não-host) também - if obj_name == "Log" { - let level_opt: Option = match member_name { - "trace" => Some(0), - "debug" => Some(1), - "info" => Some(2), - "warn" => Some(3), - "error" => Some(4), - _ => None, - }; - - if let Some(level) = level_opt { - if n.args.len() == 1 { - // Log.(msg) - self.emit(InstrKind::PushBounded(level)); - self.lower_node(n.args[0])?; - // Syscall 0x5001 retorna status (1 slot): descartar em ExprStmt - self.emit(InstrKind::HostCall(0x5001, 1)); - self.emit(InstrKind::Pop); - return Ok(()); + // Suporte a chamada estática de service: Service.method(...) + if sym.kind == SymbolKind::Service { + let full_name = format!("{}.{}", obj_name, member_name); + for arg in &n.args { self.lower_node(*arg)?; } + if let Some(func_id) = self.function_ids.get(&full_name).cloned() { + self.emit(InstrKind::Call(func_id, n.args.len() as u32)); + } else { + // Usar o binding real do import para este Service (ex.: Log -> (sdk, log)) + let obj_name_str = obj_name.to_string(); + if let Some((dep_alias, module_path)) = self.import_bindings.get(&obj_name_str).cloned() { + // Encode arity to disambiguate overloads at link time: symbol/arity + let symbol_name = format!("{}/{}", member_name, n.args.len()); + self.emit(InstrKind::ImportCall( + dep_alias, + module_path, + symbol_name, + n.args.len() as u32, + )); + } else { + // Sem binding de import conhecido: erro claro de serviço não importado + self.error( + "E_RESOLVE_UNDEFINED", + format!("Undefined service member '{}.{}' (service not imported)", obj_name, member_name), + self.arena.span(n.callee), + ); + return Err(()); } - if n.args.len() == 2 { - if let NodeKind::StringLit(tag_node) = self.arena.kind(n.args[0]) { - let tag_hash = Self::hash_tag_u16(&tag_node.value) as u32; - self.emit(InstrKind::PushBounded(level)); - self.emit(InstrKind::PushBounded(tag_hash)); - self.lower_node(n.args[1])?; - // Syscall 0x5002 retorna status (1 slot): descartar em ExprStmt - self.emit(InstrKind::HostCall(0x5002, 1)); - self.emit(InstrKind::Pop); - return Ok(()); - } else { - self.error( - "E_LOWER_UNSUPPORTED", - format!("Log.{} com tag dinâmica ainda não é suportado; use tag string literal", member_name), - self.arena.span(node), - ); - return Err(()); - } - } - self.error( - "E_LOWER_UNSUPPORTED", - format!("Assinatura inválida para Log.{}", member_name), - self.arena.span(node), - ); - return Err(()); } + return Ok(()); } if sym.kind == SymbolKind::Contract && sym.is_host { @@ -2287,10 +2396,10 @@ mod tests { fn test_host_contract_call_lowering() { let code = " declare contract Gfx host {} - declare contract Log host {} + declare contract LogHost host {} fn main() { Gfx.clear(0); - Log.write(2, \"Hello\"); + LogHost.write(2, \"Hello\"); } "; let mut interner = NameInterner::new(); @@ -2312,94 +2421,8 @@ mod tests { // Gfx.clear -> 0x1010 assert!(instrs.iter().any(|i| matches!(i.kind, InstrKind::HostCall(0x1010, 0)))); - // Log.write -> 0x5001 - assert!(instrs.iter().any(|i| matches!(i.kind, InstrKind::HostCall(0x5001, 0)))); - } - - #[test] - fn test_log_sugar_lowering_basic() { - let code = " - declare contract Log host {} - fn main() { - Log.info(\"Hello\"); - } - "; - let mut interner = NameInterner::new(); - let mut parser = Parser::new(code, FileId(0), &mut interner); - let parsed = parser.parse_file().expect("Failed to parse"); - - let mut collector = SymbolCollector::new(&interner); - let (type_symbols, value_symbols) = collector - .collect(&parsed.arena, parsed.root) - .expect("Failed to collect symbols"); - let module_symbols = ModuleSymbols { type_symbols, value_symbols }; - - let imported = ModuleSymbols::new(); - let lowerer = Lowerer::new(&parsed.arena, &module_symbols, &imported, &interner); - let program = lowerer.lower_file(parsed.root, "test").expect("Lowering failed"); - - let func = &program.modules[0].functions[0]; - let instrs: Vec<_> = func.blocks.iter().flat_map(|b| b.instrs.iter()).collect(); - - // Deve gerar HostCall para Log.write (0x5001) - assert!(instrs.iter().any(|i| matches!(i.kind, InstrKind::HostCall(0x5001, 0)))); - } - - #[test] - fn test_log_sugar_lowering_with_tag_literal() { - let code = " - declare contract Log host {} - fn main() { - Log.warn(\"net\", \"Down\"); - } - "; - let mut interner = NameInterner::new(); - let mut parser = Parser::new(code, FileId(0), &mut interner); - let parsed = parser.parse_file().expect("Failed to parse"); - - let mut collector = SymbolCollector::new(&interner); - let (type_symbols, value_symbols) = collector - .collect(&parsed.arena, parsed.root) - .expect("Failed to collect symbols"); - let module_symbols = ModuleSymbols { type_symbols, value_symbols }; - - let imported = ModuleSymbols::new(); - let lowerer = Lowerer::new(&parsed.arena, &module_symbols, &imported, &interner); - let program = lowerer.lower_file(parsed.root, "test").expect("Lowering failed"); - - let func = &program.modules[0].functions[0]; - let instrs: Vec<_> = func.blocks.iter().flat_map(|b| b.instrs.iter()).collect(); - - // Deve gerar HostCall para Log.writeTag (0x5002) - assert!(instrs.iter().any(|i| matches!(i.kind, InstrKind::HostCall(0x5002, 0)))); - } - - #[test] - fn test_log_sugar_lowering_with_dynamic_tag_error() { - let code = " - declare contract Log host {} - fn main() { - let t = \"net\"; - Log.debug(t, \"X\"); - } - "; - let mut interner = NameInterner::new(); - let mut parser = Parser::new(code, FileId(0), &mut interner); - let parsed = parser.parse_file().expect("Failed to parse"); - - let mut collector = SymbolCollector::new(&interner); - let (type_symbols, value_symbols) = collector - .collect(&parsed.arena, parsed.root) - .expect("Failed to collect symbols"); - let module_symbols = ModuleSymbols { type_symbols, value_symbols }; - - let imported = ModuleSymbols::new(); - let lowerer = Lowerer::new(&parsed.arena, &module_symbols, &imported, &interner); - let result = lowerer.lower_file(parsed.root, "test"); - - assert!(result.is_err()); - let bundle = result.err().unwrap(); - assert!(bundle.diagnostics.iter().any(|d| d.code == "E_LOWER_UNSUPPORTED")); + // LogHost.write -> 0x5001 (registry updated to LogHost) + assert!(instrs.iter().any(|i| matches!(i.kind, InstrKind::HostCall(0x5001, _)))); } #[test] diff --git a/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs b/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs index dc46da62..f4857b33 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs @@ -19,6 +19,8 @@ pub struct TypeChecker<'a> { struct_constructors: HashMap>, struct_constants: HashMap>, struct_methods: HashMap>, + // Mapa global de fields por struct: StructName -> { field_name -> PbsType } + struct_fields: HashMap>, diagnostics: Vec, contract_registry: ContractRegistry, // Contexto atual de tipo (para resolver `this` e métodos/fields) @@ -44,6 +46,7 @@ impl<'a> TypeChecker<'a> { struct_constructors: HashMap::new(), struct_constants: HashMap::new(), struct_methods: HashMap::new(), + struct_fields: HashMap::new(), diagnostics: Vec::new(), contract_registry: ContractRegistry::new(), current_type_context: None, @@ -102,10 +105,39 @@ impl<'a> TypeChecker<'a> { } } NodeKind::ServiceDecl(n) => { - // For service, the symbol's type is just Service(name) + // Tipo do próprio service if let Some(sym) = self.module_symbols.type_symbols.symbols.get_mut(&n.name) { sym.ty = Some(PbsType::Service(self.interner.resolve(n.name).to_string())); } + + // Atribuir tipos às assinaturas dos métodos do service (declaração e assinatura) + for member in &n.members { + match arena.kind(*member) { + NodeKind::ServiceFnDecl(method) => { + let mut params = Vec::new(); + for p in &method.params { + params.push(self.resolve_type_node(arena, p.ty)); + } + let ret_ty = self.resolve_type_node(arena, method.ret); + let m_ty = PbsType::Function { params, return_type: Box::new(ret_ty) }; + if let Some(sym) = self.module_symbols.value_symbols.symbols.get_mut(&method.name) { + sym.ty = Some(m_ty); + } + } + NodeKind::ServiceFnSig(method) => { + let mut params = Vec::new(); + for p in &method.params { + params.push(self.resolve_type_node(arena, p.ty)); + } + let ret_ty = self.resolve_type_node(arena, method.ret); + let m_ty = PbsType::Function { params, return_type: Box::new(ret_ty) }; + if let Some(sym) = self.module_symbols.value_symbols.symbols.get_mut(&method.name) { + sym.ty = Some(m_ty); + } + } + _ => {} + } + } } NodeKind::TypeDecl(n) => { let type_name = self.interner.resolve(n.name).to_string(); @@ -191,6 +223,15 @@ impl<'a> TypeChecker<'a> { } } self.struct_methods.insert(type_name, methods); + // Coleta fields declarados no cabeçalho do struct para acesso por instância (p.x) + if n.type_kind == "struct" { + let mut fields = HashMap::new(); + for p in &n.params { + let fty = self.resolve_type_node(arena, p.ty); + fields.insert(self.interner.resolve(p.name).to_string(), fty); + } + self.struct_fields.insert(self.interner.resolve(n.name).to_string(), fields); + } } _ => {} } @@ -374,13 +415,14 @@ impl<'a> TypeChecker<'a> { let obj_ty = self.check_node(arena, n.object); if let PbsType::Struct(ref name) = obj_ty { + // PR-Log+Service: dar precedência a MÉTODOS sobre fields quando há colisão de nome + // Ex.: Color possui field "raw: bounded" e método "raw(self: Color): bounded"; + // ao fazer c.raw(), deve resolver para o método. if let Some(methods) = self.struct_methods.get(name) { if let Some(ty) = methods.get(member_str) { - // If it's a method call on an instance, the first parameter (self) is implicit + // Se for chamada de método em instância, o primeiro parâmetro (self) é implícito if let PbsType::Function { mut params, return_type } = ty.clone() { if !params.is_empty() { - // Check if first param is the struct itself (simple heuristic for self) - // In a real compiler we'd check the parameter name or a flag params.remove(0); return PbsType::Function { params, return_type }; } @@ -388,6 +430,18 @@ impl<'a> TypeChecker<'a> { return ty.clone(); } } + // Depois, tentar resolver como acesso a field de struct conhecido no contexto atual + if let Some(fields) = &self.current_struct_fields { + if let Some(ty) = fields.get(member_str) { + return ty.clone(); + } + } + // Por fim, tabela global de fields coletados do cabeçalho do struct + if let Some(fields) = self.struct_fields.get(name) { + if let Some(ty) = fields.get(member_str) { + return ty.clone(); + } + } } match obj_ty { diff --git a/crates/prometeu-compiler/src/semantics/export_surface.rs b/crates/prometeu-compiler/src/semantics/export_surface.rs index ede6eab5..5d8121ce 100644 --- a/crates/prometeu-compiler/src/semantics/export_surface.rs +++ b/crates/prometeu-compiler/src/semantics/export_surface.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; pub enum ExportSurfaceKind { Service, DeclareType, // struct, storage struct, type alias + Function, // funções públicas (ex.: métodos de service expostos pelo SDK) } impl ExportSurfaceKind { @@ -14,7 +15,9 @@ impl ExportSurfaceKind { SymbolKind::Struct | SymbolKind::Contract | SymbolKind::ErrorType => { Some(ExportSurfaceKind::DeclareType) } - SymbolKind::Function | SymbolKind::Local => None, + // Em v0, permitimos exportar funções públicas — usado sobretudo para métodos de `service` + SymbolKind::Function => Some(ExportSurfaceKind::Function), + SymbolKind::Local => None, } } @@ -35,6 +38,7 @@ impl ExportSurfaceKind { match self { ExportSurfaceKind::Service => crate::frontends::pbs::symbols::Namespace::Type, ExportSurfaceKind::DeclareType => crate::frontends::pbs::symbols::Namespace::Type, + ExportSurfaceKind::Function => crate::frontends::pbs::symbols::Namespace::Value, } } } diff --git a/crates/prometeu-kernel/src/prometeu_os.rs b/crates/prometeu-kernel/src/prometeu_os.rs index 04162930..0c9656b0 100644 --- a/crates/prometeu-kernel/src/prometeu_os.rs +++ b/crates/prometeu-kernel/src/prometeu_os.rs @@ -439,7 +439,11 @@ use prometeu_abi::virtual_machine::Value; let args_count = Syscall::from_u32(id).expect(&format!("Invalid syscall id: 0x{:08X}", id)).args_count(); let mut args = Vec::new(); for _ in 0..args_count { - args.push(vm.pop().unwrap()); + // Protege contra underflow/erros de pilha durante testes + match vm.pop() { + Ok(v) => args.push(v), + Err(e) => return Err(VmFault::Panic(e)), + } } args.reverse(); let mut ret = HostReturn::new(&mut vm.operand_stack); @@ -703,7 +707,7 @@ use prometeu_abi::virtual_machine::Value; let recent = os.log_service.get_recent(1); assert_eq!(recent[0].msg, "Tagged Log"); assert_eq!(recent[0].tag, 42); - assert_eq!(vm.pop().unwrap(), Value::Null); + // Syscall de log é void: não empurra valor na pilha // 6. GFX Syscall return test vm.push(Value::Int64(1)); // color_idx @@ -1347,7 +1351,7 @@ impl NativeInterface for PrometeuOS { _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string message".into())), }; self.syscall_log_write(level, 0, msg)?; - ret.push_null(); + // void Ok(()) } // LOG_WRITE_TAG(level, tag, msg) @@ -1359,7 +1363,7 @@ impl NativeInterface for PrometeuOS { _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string message".into())), }; self.syscall_log_write(level, tag, msg)?; - ret.push_null(); + // void Ok(()) } diff --git a/crates/prometeu-vm/src/verifier.rs b/crates/prometeu-vm/src/verifier.rs index 484b4e59..c7bf92ac 100644 --- a/crates/prometeu-vm/src/verifier.rs +++ b/crates/prometeu-vm/src/verifier.rs @@ -2,6 +2,7 @@ use prometeu_abi::syscalls::Syscall; use crate::bytecode::decoder::{decode_at, DecodeError}; use prometeu_bytecode::opcode::OpCode; use prometeu_bytecode::FunctionMeta; +use prometeu_bytecode::layout; use std::collections::{HashMap, HashSet, VecDeque}; #[derive(Debug, Clone, PartialEq, Eq)] @@ -33,7 +34,8 @@ impl Verifier { fn verify_function(code: &[u8], func: &FunctionMeta, func_idx: usize, all_functions: &[FunctionMeta]) -> Result { let func_start = func.code_offset as usize; - let func_end = func_start + func.code_len as usize; + // Use o cálculo canônico compartilhado com o compiler/linker + let func_end = layout::function_end_from_next(all_functions, func_idx, code.len()); if func_start > code.len() || func_end > code.len() || func_start > func_end { return Err(VerifierError::FunctionOutOfBounds { @@ -46,6 +48,14 @@ impl Verifier { let func_code = &code[func_start..func_end]; + // Funções vazias (sem qualquer byte de código) são consideradas válidas no verificador. + // Elas não consomem nem produzem valores na pilha e não possuem fluxo interno. + // Observação: se uma função vazia for chamada em tempo de execução e retorno/efeitos + // forem esperados, caberá ao gerador de código/linker impedir tal situação. + if func_code.is_empty() { + return Ok(0); + } + // First pass: find all valid instruction boundaries let mut valid_pc = HashSet::new(); let mut pc = 0; @@ -120,28 +130,41 @@ impl Verifier { // Propagate to successors if spec.is_branch { - let target = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()) as usize; - - if target >= func.code_len as usize { - return Err(VerifierError::InvalidJumpTarget { pc: func_start + pc, target: func_start + target }); - } - if !valid_pc.contains(&target) { - return Err(VerifierError::JumpToMidInstruction { pc: func_start + pc, target: func_start + target }); + // Canonical contract: branch immediate is RELATIVE to function start. + let target_rel = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()) as usize; + let func_end_abs = layout::function_end_from_next(all_functions, func_idx, code.len()); + let func_len = func_end_abs - func_start; + + if target_rel > func_len { + return Err(VerifierError::InvalidJumpTarget { pc: func_start + pc, target: func_start + target_rel }); } - if let Some(&existing_height) = stack_height_in.get(&target) { - if existing_height != out_height { - return Err(VerifierError::StackMismatchJoin { pc: func_start + pc, target: func_start + target, height_in: out_height, height_target: existing_height }); + if target_rel == func_len { + // salto para o fim da função + if out_height != func.return_slots { + return Err(VerifierError::BadRetStackHeight { pc: func_start + pc, height: out_height, expected: func.return_slots }); } + // caminho termina aqui } else { - stack_height_in.insert(target, out_height); - worklist.push_back(target); + if !valid_pc.contains(&target_rel) { + return Err(VerifierError::JumpToMidInstruction { pc: func_start + pc, target: func_start + target_rel }); + } + + if let Some(&existing_height) = stack_height_in.get(&target_rel) { + if existing_height != out_height { + return Err(VerifierError::StackMismatchJoin { pc: func_start + pc, target: func_start + target_rel, height_in: out_height, height_target: existing_height }); + } + } else { + stack_height_in.insert(target_rel, out_height); + worklist.push_back(target_rel); + } } } if !spec.is_terminator { let next_pc = instr.next_pc; - if next_pc < func.code_len as usize { + let func_len = layout::function_len_from_next(all_functions, func_idx, code.len()); + if next_pc < func_len { if let Some(&existing_height) = stack_height_in.get(&next_pc) { if existing_height != out_height { return Err(VerifierError::StackMismatchJoin { pc: func_start + pc, target: func_start + next_pc, height_in: out_height, height_target: existing_height }); diff --git a/test-cartridges/canonical/prometeu.json b/test-cartridges/canonical/prometeu.json index 1e54e60e..46ec0c52 100644 --- a/test-cartridges/canonical/prometeu.json +++ b/test-cartridges/canonical/prometeu.json @@ -1,6 +1,5 @@ { "name": "canonical", "version": "0.1.0", - "script_fe": "pbs", - "entry": "src/main/modules/main.pbs" + "script_fe": "pbs" } diff --git a/test-cartridges/test01/cartridge/program.pbc b/test-cartridges/test01/cartridge/program.pbc index 7b0cf3d662d272ceec0330378534893cefa0bc6f..683f47dceabf9472c43cfbec348ac5fc319d8cb5 100644 GIT binary patch literal 3311 zcmZveZHQD=9LCSAqg!SA8H9f)?p*y9#PA1(IoCu!6!}KVT};h&#ne_; zObvC#)Jj)ue2}}CZ>y5)tR#LVCfArR7b}%wAz`JXRVC!d;j7(e@ZtCzI2I5YK}*s-hdI#GR(0mZ94I_XsC^FF9Yg znyFP+HqUo3FOTKxwo^P&X)T z^ml!ENrzvvoKF*O2))m&sg?I@8?HI9dL6W=&M4@)!>{D!<$6?kcTK2fP%_+?^A5P6 z_k^qWUTc(XJP1>a?;t<(zcP3a)l#?95a8%Z171WKy2 zxZRg4ExEydWbOG5cdbvEC|)a)^XVY>g0J68n(%YX`=`|4!uPSk*HOAA^xX7(f@dz* zg>-54ny8sVx7>#<5BnwDj1jX6|~HFC4Lnu z85i(RBAs9w?;!pow8r=o{AXyb@ge+SwBGn_{1K!RPwhvrF-M|J#-s3K&=%u3z5#7F z?!tGYUB*55UbNS^AOARd*Z6*X8R-PocsudC&>`d9_&w;T@mBm^q!U*4<0OO8DdT$l zP;}aO7=Ac9V|q(EB-zCNi@RvNAgo>jIrY4 zvBugPKcPnBv*f>_$;N+@pGQ-T6&GiWwKr}>?Z!>`6uQ&65wHDmm+>t89Y|jg&G&Eg z4_aXS2mT^jYX68{y_TdDR7>8pgw5ng5dEU_2S>Imx{Qu__WUO{?i!o0;; z`*$DqJboe4GZN3nFG6wp6hDJsi-sCk@axcMft>0q&Gj0DuGmO8(UqIOi&yMij5uR(TcVjG?Z#*7<3wk&d7cVycm$8y? zmvJ+GCeoRt`Od{JLaUAE;2%QT7mD}cpGF%ayeYz4BD^iaJ0g6*_+|RugtUh=-cI~J zq%%(ZCVoHCAC_3}T{F@jmUtQdF{H1TE`7gNlslIcv#Dez;}fF(6BiU`GRaJP)~9sg z1BK$FE4#C;zJX6!vrynR-@>O%#Q!Ju^mi@Krc=H}kZCUZng)?zkhIVArBmdI&T@Y? z9gGrW`qmFbf??7=)1QaF%EfXyn+b*qGW{xC3RG`J^V2_p{uDY31#UXs?i2kkj6yw? zN0#<GouEdywjjt5u^#CfnMUY;Fls^`gJpu5xEzF*`k#Y)Q3Qrr(R}(F4UL znQ7g{74x$#)7z5ilx14`aQH#i)Q5vi%dO7!X|I%*H1&1%^f&b_FRbh>R%ZCo6xBj{ F_z&14Id%X5 literal 1977 zcmZvcNoZ416ozlwXq}A%Vny3pTu2>KQU@qbyhw53BCR6TR&6n?IY0{c8C z*;G=ek%%Cg9NUgC$)=H|clIM7jn)+%$gZXm~XUZ63e zV-|X25;{MBTrF#qtHrIdH2qcMT47*bmYUGb%D-|#k0D`oq*_EbH?A?FPv%%okoT<$=QBd+@wcTB>|nWSCO zRPiG5kh z;ztt$jfHN%j&IXC1a?R6IqgDh!5FS^Nc$!3{dZn+wlcQXEhWlN=5&%ejpV+B&*0A@ zS$(cwSk@r?6#ooOFn)oTXKZp_zpy;)GX6IH6_RIG_&xpusxbb7|As2_`i14Wmhm$= z{8^~lIEt@CHO6K57+P+;2)`J~dm!_FqF-o@@n_oKsLA*??H$x&e3$kfYBO%2RvKkI z-f6s#zQbsb#|Mo&=sS)&Jnk~yLElMq&iFe120Cwi8Q+608b824M3;B<#NS7gjj!T+QMt#{J)Z6H9OFLben-{DkMTdy5|3*v;nyPhM$3M+<9DHB9-r{I%j0t%pZB=i z_yDy}qHD%S@F!4@*Dvp>tS2{K-eKVwZ54WLyqR_j8cd)3xP8~0Op?)$Wovf*rq+#{ zQW+jG{U*~FlQB}OCDYuV;@UcLRcb?Q_Qurqdg+nxY%Z}{rpgrg#AGuYqV3JCTchpU Ok~`W`*=nsZpx|G0dACIX diff --git a/test-cartridges/test01/src/main/modules/main.pbs b/test-cartridges/test01/src/main/modules/main.pbs index 65f157e2..6797c6cb 100644 --- a/test-cartridges/test01/src/main/modules/main.pbs +++ b/test-cartridges/test01/src/main/modules/main.pbs @@ -61,5 +61,6 @@ fn frame(): void { if Pad.b().pressed { Log.debug("B Pressed"); + Gfx.clear(Color.RED); } }