full refactor on linker and verifier to become more JVM-like

This commit is contained in:
bQUARKz 2026-02-09 20:54:20 +00:00
parent e486c1980e
commit c6411fd53c
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
17 changed files with 701 additions and 237 deletions

View File

@ -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,
}
}

View File

@ -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<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);
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 }
}

View File

@ -18,6 +18,7 @@ pub mod abi;
pub mod readwrite;
pub mod asm;
pub mod disasm;
pub mod layout;
mod model;

View File

@ -71,8 +71,14 @@ pub fn emit_fragments(module: &ir_vm::Module) -> Result<EmitFragments> {
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<EmitFragments> {
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));
}

View File

@ -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<u32> = 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::<u32>(), len_str.parse::<u32>()) {
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 {

View File

@ -73,11 +73,19 @@ pub fn build_from_graph(graph: &ResolvedGraph, target: BuildTarget) -> Result<Bu
// 2) Ensure that file declares fn frame(): void (no params)
// We validate at the bytecode metadata level using function names and signature slots.
if let Some(root_compiled) = compiled_modules.get(&root_step.project_id) {
// Instrumentação: listar nomes de funções e metas do módulo root
if let Some(di) = &root_compiled.debug_info {
// Suprimir logs verbosos na versão final.
let _ = di; // no-op
}
// Find function index by name "frame" (tolerate qualified names ending with ":frame")
let mut found_valid = false;
if let Some(di) = &root_compiled.debug_info {
for (idx, name) in &di.function_names {
let is_frame_name = name == "frame" || name.ends_with(":frame");
// Names in debug_info may be enriched as "name@offset+len". Strip the annotation for comparison.
let base_name = name.split('@').next().unwrap_or(name.as_str());
let is_frame_name = base_name == "frame" || base_name.ends_with(":frame");
if is_frame_name {
// Check signature: 0 params, 0 return slots
if let Some(meta) = root_compiled.function_metas.get(*idx as usize) {

View File

@ -206,6 +206,7 @@ pub fn compile_project(
_ => 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<String> = 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<crate::ir_vm::types::ConstId> = 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<String> = 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,
})
}

View File

@ -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<String> = 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:<ServiceName>"
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) {

View File

@ -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

View File

@ -33,15 +33,14 @@ pub struct Lowerer<'a> {
struct_constructors: HashMap<String, HashMap<String, NodeId>>,
type_constants: HashMap<String, HashMap<String, NodeId>>,
current_type_context: Option<String>,
// Campos de structs definidos pelo usuário
user_struct_field_offsets: HashMap<String, HashMap<String, u32>>,
user_struct_field_types: HashMap<String, HashMap<String, Type>>,
// Slot base do parâmetro `self` durante o lowering de métodos
method_self_slot: Option<u32>,
contract_registry: ContractRegistry,
diagnostics: Vec<Diagnostic>,
max_slots_used: u32,
current_span: Option<Span>,
import_bindings: HashMap<String, (String, String)>,
}
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<Function, ()> {
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<u32> = 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.<lvl>(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]

View File

@ -19,6 +19,8 @@ pub struct TypeChecker<'a> {
struct_constructors: HashMap<String, HashMap<String, PbsType>>,
struct_constants: HashMap<String, HashMap<String, PbsType>>,
struct_methods: HashMap<String, HashMap<String, PbsType>>,
// Mapa global de fields por struct: StructName -> { field_name -> PbsType }
struct_fields: HashMap<String, HashMap<String, PbsType>>,
diagnostics: Vec<Diagnostic>,
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 {

View File

@ -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,
}
}
}

View File

@ -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(())
}

View File

@ -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<u16, VerifierError> {
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 });

View File

@ -1,6 +1,5 @@
{
"name": "canonical",
"version": "0.1.0",
"script_fe": "pbs",
"entry": "src/main/modules/main.pbs"
"script_fe": "pbs"
}

View File

@ -61,5 +61,6 @@ fn frame(): void {
if Pad.b().pressed {
Log.debug("B Pressed");
Gfx.clear(Color.RED);
}
}