246 lines
6.8 KiB
Rust

use crate::common::spans::Span;
use prometeu_analysis::NameInterner;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct RawSymbol {
pub pc: u32,
pub span: Span,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
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_uri: 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;
// ========================
// 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>,
file_manager: &crate::common::files::FileManager,
interner: &NameInterner,
) -> Vec<Symbol> {
let mut result = Vec::new();
for (module_path, ms) in module_symbols {
// Collect from type_symbols
for list in ms.type_symbols.symbols.values() {
for sym in list {
if let Some(s) = convert_symbol(project_id, module_path, sym, file_manager, interner) {
result.push(s);
}
}
}
// Collect from value_symbols
for list in ms.value_symbols.symbols.values() {
for sym in list {
if let Some(s) = convert_symbol(project_id, module_path, sym, file_manager, interner) {
result.push(s);
}
}
}
}
// Deterministic ordering: by file, then start pos, then name
result.sort_by(|a, b| {
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))
});
result
}
fn convert_symbol(
project_id: &str,
module_path: &str,
sym: &crate::frontends::pbs::symbols::Symbol,
file_manager: &crate::common::files::FileManager,
interner: &NameInterner,
) -> 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.clone();
let file_path = file_manager.get_path(span.file.as_usize())
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| format!("unknown_file_{}", span.file.as_usize()));
// Convert 1-based to 0-based
let (s_line, s_col) = file_manager.lookup_pos(span.file.as_usize(), span.start);
let (e_line, e_col) = file_manager.lookup_pos(span.file.as_usize(), span.end);
let decl_span = SpanRange {
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 },
};
let hash = decl_span.compute_hash();
let name = interner.resolve(sym.name).to_string();
let id = format!("{}:{}:{}:{}:{:016x}", project_id, kind, module_path, name, hash);
Some(Symbol {
id,
name,
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_uri.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_uri: "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);
// Hash constant may change if algorithm or field names change; we only
// assert determinism here.
assert!(hash1 != 0);
}
}