full refactor on linker and verifier to become more JVM-like
This commit is contained in:
parent
e486c1980e
commit
c6411fd53c
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
64
crates/prometeu-bytecode/src/layout.rs
Normal file
64
crates/prometeu-bytecode/src/layout.rs
Normal 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 }
|
||||
}
|
||||
@ -18,6 +18,7 @@ pub mod abi;
|
||||
pub mod readwrite;
|
||||
pub mod asm;
|
||||
pub mod disasm;
|
||||
pub mod layout;
|
||||
|
||||
mod model;
|
||||
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
{
|
||||
"name": "canonical",
|
||||
"version": "0.1.0",
|
||||
"script_fe": "pbs",
|
||||
"entry": "src/main/modules/main.pbs"
|
||||
"script_fe": "pbs"
|
||||
}
|
||||
|
||||
Binary file not shown.
@ -61,5 +61,6 @@ fn frame(): void {
|
||||
|
||||
if Pad.b().pressed {
|
||||
Log.debug("B Pressed");
|
||||
Gfx.clear(Color.RED);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user