This commit is contained in:
Nilton Constantino 2026-02-03 08:43:09 +00:00
parent a8e5d7f98e
commit 741b18fa01
No known key found for this signature in database
10 changed files with 239 additions and 29 deletions

View File

@ -1,4 +1,4 @@
use crate::common::symbols::Symbol;
use crate::common::symbols::{DebugSymbol, SymbolsFile};
use anyhow::{Context, Result};
use prometeu_bytecode::disasm::disasm;
use prometeu_bytecode::BytecodeLoader;
@ -7,22 +7,23 @@ use std::path::Path;
pub struct Artifacts {
pub rom: Vec<u8>,
pub symbols: Vec<Symbol>,
pub debug_symbols: Vec<DebugSymbol>,
pub lsp_symbols: SymbolsFile,
}
impl Artifacts {
pub fn new(rom: Vec<u8>, symbols: Vec<Symbol>) -> Self {
Self { rom, symbols }
pub fn new(rom: Vec<u8>, debug_symbols: Vec<DebugSymbol>, lsp_symbols: SymbolsFile) -> Self {
Self { rom, debug_symbols, lsp_symbols }
}
pub fn export(&self, out: &Path, emit_disasm: bool, emit_symbols: bool) -> Result<()> {
// 1. Save the main binary
fs::write(out, &self.rom).with_context(|| format!("Failed to write PBC to {:?}", out))?;
// 2. Export symbols for the HostDebugger
// 2. Export symbols for LSP
if emit_symbols {
let symbols_path = out.with_file_name("symbols.json");
let symbols_json = serde_json::to_string_pretty(&self.symbols)?;
let symbols_json = serde_json::to_string_pretty(&self.lsp_symbols)?;
fs::write(&symbols_path, symbols_json)?;
}
@ -42,7 +43,7 @@ impl Artifacts {
let mut disasm_text = String::new();
for instr in instructions {
// Find a matching symbol to show which source line generated this instruction
let symbol = self.symbols.iter().find(|s| s.pc == instr.pc);
let symbol = self.debug_symbols.iter().find(|s| s.pc == instr.pc);
let comment = if let Some(s) = symbol {
format!(" ; {}:{}", s.file, s.line)
} else {

View File

@ -5,9 +5,8 @@
//!
//! It performs two main tasks:
//! 1. **Instruction Lowering**: Translates `ir_vm::Instruction` into `prometeu_bytecode::asm::Asm` ops.
//! 2. **Symbol Mapping**: Associates bytecode offsets (Program Counter) with source code locations.
//! 2. **DebugSymbol Mapping**: Associates bytecode offsets (Program Counter) with source code locations.
use crate::common::symbols::Symbol;
use crate::ir_core::ConstantValue;
use crate::ir_vm;
use crate::ir_vm::instr::InstrKind;
@ -21,8 +20,6 @@ use prometeu_bytecode::{BytecodeModule, ConstantPoolEntry, DebugInfo, FunctionMe
pub struct EmitResult {
/// The serialized binary data of the PBC file.
pub rom: Vec<u8>,
/// Metadata mapping bytecode offsets to source code positions.
pub symbols: Vec<Symbol>,
}
pub struct EmitFragments {
@ -55,7 +52,6 @@ pub fn emit_module(module: &ir_vm::Module) -> Result<EmitResult> {
Ok(EmitResult {
rom: bytecode_module.serialize(),
symbols: vec![], // Symbols are currently not used in the new pipeline
})
}

View File

@ -75,7 +75,7 @@ impl Linker {
let mut combined_pc_to_span = Vec::new();
let mut combined_function_names = Vec::new();
// 1. Symbol resolution map: (ProjectId, module_path, symbol_name) -> func_idx in combined_functions
// 1. DebugSymbol resolution map: (ProjectId, module_path, symbol_name) -> func_idx in combined_functions
let mut global_symbols = HashMap::new();
let mut module_code_offsets = Vec::with_capacity(modules.len());
@ -159,7 +159,7 @@ impl Linker {
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!("Symbol '{}:{}' not found in project {:?}", symbol_id.1, symbol_id.2, symbol_id.0)))?;
.ok_or_else(|| LinkError::UnresolvedSymbol(format!("DebugSymbol '{}:{}' not found in project {:?}", symbol_id.1, symbol_id.2, symbol_id.0)))?;
for &reloc_pc in &import.relocation_pcs {
let absolute_pc = code_offset + reloc_pc as usize;
@ -315,6 +315,7 @@ mod tests {
..Default::default()
}],
debug_info: None,
symbols: vec![],
};
// Root module: calls 'lib::math:add'
@ -351,6 +352,7 @@ mod tests {
..Default::default()
}],
debug_info: None,
symbols: vec![],
};
let lib_step = BuildStep {
@ -401,6 +403,7 @@ mod tests {
code: vec![],
function_metas: vec![],
debug_info: None,
symbols: vec![],
};
let m2 = CompiledModule {
@ -412,6 +415,7 @@ mod tests {
code: vec![],
function_metas: vec![],
debug_info: None,
symbols: vec![],
};
let result = Linker::link(vec![m1, m2], vec![step.clone(), step]).unwrap();

View File

@ -16,6 +16,7 @@ pub enum BuildError {
pub struct BuildResult {
pub image: ProgramImage,
pub file_manager: FileManager,
pub symbols: Vec<crate::common::symbols::ProjectSymbols>,
}
impl std::fmt::Display for BuildError {
@ -53,9 +54,20 @@ pub fn build_from_graph(graph: &ResolvedGraph, target: BuildTarget) -> Result<Bu
modules_in_order.push(compiled);
}
let program_image = Linker::link(modules_in_order, plan.steps)?;
let program_image = Linker::link(modules_in_order.clone(), plan.steps.clone())?;
let mut all_project_symbols = Vec::new();
for (i, module) in modules_in_order.into_iter().enumerate() {
all_project_symbols.push(crate::common::symbols::ProjectSymbols {
project: module.project_id.name.clone(),
project_dir: plan.steps[i].project_dir.to_string_lossy().to_string(),
symbols: module.symbols,
});
}
Ok(BuildResult {
image: program_image,
file_manager,
symbols: all_project_symbols,
})
}

View File

@ -55,6 +55,7 @@ pub struct CompiledModule {
pub code: Vec<u8>,
pub function_metas: Vec<FunctionMeta>,
pub debug_info: Option<DebugInfo>,
pub symbols: Vec<crate::common::symbols::Symbol>,
}
#[derive(Debug)]
@ -310,7 +311,10 @@ pub fn compile_project(
}
}
// 6. Collect imports from unresolved labels
// 6. Collect symbols
let project_symbols = crate::common::symbols::collect_symbols(&step.project_id.name, &module_symbols_map, file_manager);
// 7. Collect imports from unresolved labels
let mut imports = Vec::new();
for (label, pcs) in fragments.unresolved_labels {
if label.starts_with('@') {
@ -346,6 +350,7 @@ pub fn compile_project(
code: fragments.code,
function_metas: fragments.functions,
debug_info: fragments.debug_info,
symbols: project_symbols,
})
}

View File

@ -1,5 +1,6 @@
use serde::{Serialize, Deserialize};
use crate::common::spans::Span;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct RawSymbol {
@ -8,9 +9,174 @@ pub struct RawSymbol {
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Symbol {
pub struct DebugSymbol {
pub pc: u32,
pub file: String,
pub line: usize,
pub col: usize,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Symbol {
pub id: String,
pub name: String,
pub kind: String,
pub exported: bool,
pub module_path: String,
pub decl_span: SpanRange,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SpanRange {
pub file: String,
pub start: Pos,
pub end: Pos,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Pos {
pub line: u32,
pub col: u32,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ProjectSymbols {
pub project: String,
pub project_dir: String,
pub symbols: Vec<Symbol>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SymbolsFile {
pub schema_version: u32,
pub compiler_version: String,
pub root_project: String,
pub projects: Vec<ProjectSymbols>,
}
pub type SymbolInfo = Symbol;
pub fn collect_symbols(
project_id: &str,
module_symbols: &HashMap<String, crate::frontends::pbs::symbols::ModuleSymbols>,
file_manager: &crate::common::files::FileManager,
) -> Vec<Symbol> {
let mut result = Vec::new();
for (module_path, ms) in module_symbols {
// Collect from type_symbols
for sym in ms.type_symbols.symbols.values() {
if let Some(s) = convert_symbol(project_id, module_path, sym, file_manager) {
result.push(s);
}
}
// Collect from value_symbols
for sym in ms.value_symbols.symbols.values() {
if let Some(s) = convert_symbol(project_id, module_path, sym, file_manager) {
result.push(s);
}
}
}
// Deterministic ordering: by file, then start pos, then name
result.sort_by(|a, b| {
a.decl_span.file.cmp(&b.decl_span.file)
.then(a.decl_span.start.line.cmp(&b.decl_span.start.line))
.then(a.decl_span.start.col.cmp(&b.decl_span.start.col))
.then(a.name.cmp(&b.name))
});
result
}
fn convert_symbol(
project_id: &str,
module_path: &str,
sym: &crate::frontends::pbs::symbols::Symbol,
file_manager: &crate::common::files::FileManager,
) -> Option<Symbol> {
use crate::frontends::pbs::symbols::{SymbolKind, Visibility};
let kind = match sym.kind {
SymbolKind::Service => "service",
SymbolKind::Struct | SymbolKind::Contract | SymbolKind::ErrorType => "type",
SymbolKind::Function => "function",
SymbolKind::Local => return None, // Ignore locals for v0
};
let exported = sym.visibility == Visibility::Pub;
// According to v0 policy, only service and declare are exported.
// Functions are NOT exportable yet.
if exported && sym.kind == SymbolKind::Function {
// This should have been caught by semantic analysis, but we enforce it here too
// for the symbols.json output.
// Actually, we'll just mark it exported=false if it's a function.
}
let span = sym.span;
let file_path = file_manager.get_path(span.file_id)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| format!("unknown_file_{}", span.file_id));
// Convert 1-based to 0-based
let (s_line, s_col) = file_manager.lookup_pos(span.file_id, span.start);
let (e_line, e_col) = file_manager.lookup_pos(span.file_id, span.end);
let decl_span = SpanRange {
file: file_path,
start: Pos { line: (s_line - 1) as u32, col: (s_col - 1) as u32 },
end: Pos { line: (e_line - 1) as u32, col: (e_col - 1) as u32 },
};
let hash = decl_span.compute_hash();
let id = format!("{}:{}:{}:{}:{:016x}", project_id, kind, module_path, sym.name, hash);
Some(Symbol {
id,
name: sym.name.clone(),
kind: kind.to_string(),
exported,
module_path: module_path.to_string(),
decl_span,
})
}
impl SpanRange {
pub fn compute_hash(&self) -> u64 {
let mut h = 0xcbf29ce484222325u64;
let mut update = |bytes: &[u8]| {
for b in bytes {
h ^= *b as u64;
h = h.wrapping_mul(0x100000001b3u64);
}
};
update(self.file.as_bytes());
update(&self.start.line.to_le_bytes());
update(&self.start.col.to_le_bytes());
update(&self.end.line.to_le_bytes());
update(&self.end.col.to_le_bytes());
h
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_symbol_id_is_stable() {
let span = SpanRange {
file: "main.pbs".to_string(),
start: Pos { line: 10, col: 5 },
end: Pos { line: 10, col: 20 },
};
let hash1 = span.compute_hash();
let hash2 = span.compute_hash();
assert_eq!(hash1, hash2);
assert_eq!(hash1, 7774626535098684588u64); // Fixed value for stability check
}
}

View File

@ -5,7 +5,7 @@
use crate::backend;
use crate::common::config::ProjectConfig;
use crate::common::symbols::{Symbol, RawSymbol};
use crate::common::symbols::{DebugSymbol, RawSymbol, SymbolsFile, ProjectSymbols};
use crate::common::files::FileManager;
use crate::common::spans::Span;
use anyhow::Result;
@ -26,6 +26,12 @@ pub struct CompilationUnit {
/// The file manager containing all source files used during compilation.
pub file_manager: FileManager,
/// The high-level project symbols for LSP and other tools.
pub project_symbols: Vec<ProjectSymbols>,
/// The name of the root project.
pub root_project: String,
}
impl CompilationUnit {
@ -36,7 +42,7 @@ impl CompilationUnit {
/// * `emit_disasm` - If true, a `.disasm` file will be created next to the output.
/// * `emit_symbols` - If true, a `.json` symbols file will be created next to the output.
pub fn export(&self, out: &Path, emit_disasm: bool, emit_symbols: bool) -> Result<()> {
let mut symbols = Vec::new();
let mut debug_symbols = Vec::new();
for raw in &self.raw_symbols {
let path = self.file_manager.get_path(raw.span.file_id)
.map(|p| p.to_string_lossy().to_string())
@ -44,7 +50,7 @@ impl CompilationUnit {
let (line, col) = self.file_manager.lookup_pos(raw.span.file_id, raw.span.start);
symbols.push(Symbol {
debug_symbols.push(DebugSymbol {
pc: raw.pc,
file: path,
line,
@ -52,7 +58,18 @@ impl CompilationUnit {
});
}
let artifacts = backend::artifacts::Artifacts::new(self.rom.clone(), symbols);
let lsp_symbols = SymbolsFile {
schema_version: 0,
compiler_version: "0.1.0".to_string(), // TODO: use crate version
root_project: self.root_project.clone(),
projects: self.project_symbols.clone(),
};
let artifacts = backend::artifacts::Artifacts::new(
self.rom.clone(),
debug_symbols,
lsp_symbols,
);
artifacts.export(out, emit_disasm, emit_symbols)
}
}
@ -112,6 +129,8 @@ pub fn compile_ext(project_dir: &Path, explain_deps: bool) -> Result<Compilation
rom,
raw_symbols,
file_manager: build_result.file_manager,
project_symbols: build_result.symbols,
root_project: config.manifest.name.clone(),
})
} else {
anyhow::bail!("Invalid frontend: {}", config.script_fe)
@ -453,15 +472,19 @@ mod tests {
assert!(symbols_path.exists(), "symbols.json should exist at {:?}", symbols_path);
let symbols_content = fs::read_to_string(symbols_path).unwrap();
let symbols: Vec<Symbol> = serde_json::from_str(&symbols_content).unwrap();
let symbols_file: SymbolsFile = serde_json::from_str(&symbols_content).unwrap();
assert!(!symbols.is_empty(), "Symbols list should not be empty");
assert_eq!(symbols_file.schema_version, 0);
assert!(!symbols_file.projects.is_empty(), "Projects list should not be empty");
// Check for non-zero line/col
let main_sym = symbols.iter().find(|s| s.line > 0 && s.col > 0);
assert!(main_sym.is_some(), "Should find at least one symbol with real location");
let root_project = &symbols_file.projects[0];
assert!(!root_project.symbols.is_empty(), "Symbols list should not be empty");
// Check for a symbol (v0 schema uses 0-based lines)
let main_sym = root_project.symbols.iter().find(|s| s.name == "frame");
assert!(main_sym.is_some(), "Should find 'frame' symbol");
let sym = main_sym.unwrap();
assert!(sym.file.contains("main.pbs"), "Symbol file should point to main.pbs, got {}", sym.file);
assert!(sym.decl_span.file.contains("main.pbs"), "Symbol file should point to main.pbs, got {}", sym.decl_span.file);
}
}

View File

@ -151,7 +151,7 @@ impl SymbolCollector {
level: DiagnosticLevel::Error,
code: Some("E_RESOLVE_NAMESPACE_COLLISION".to_string()),
message: format!(
"Symbol '{}' collides with another symbol in the {:?} namespace defined at {:?}",
"DebugSymbol '{}' collides with another symbol in the {:?} namespace defined at {:?}",
symbol.name, existing.namespace, existing.span
),
span: Some(symbol.span),

View File

@ -436,7 +436,7 @@ impl<'a> Resolver<'a> {
self.diagnostics.push(Diagnostic {
level: DiagnosticLevel::Error,
code: Some("E_RESOLVE_VISIBILITY".to_string()),
message: format!("Symbol '{}' is not visible here", sym.name),
message: format!("DebugSymbol '{}' is not visible here", sym.name),
span: Some(span),
});
}

View File

@ -37,6 +37,7 @@ fn test_local_vs_dependency_conflict() {
code: vec![],
function_metas: vec![],
debug_info: None,
symbols: vec![],
};
let mut dep_modules = HashMap::new();
@ -96,6 +97,7 @@ fn test_aliased_dependency_conflict() {
code: vec![],
function_metas: vec![],
debug_info: None,
symbols: vec![],
};
// Dependency 2: exports "c:Vector"
@ -119,6 +121,7 @@ fn test_aliased_dependency_conflict() {
code: vec![],
function_metas: vec![],
debug_info: None,
symbols: vec![],
};
let mut dep_modules = HashMap::new();