use crate::models::unified_ast::{BytePos, Location, QualifiedName, RelativeLocation, Span};
use dashmap::DashMap;
use std::path::Path;
use std::sync::Arc;
use tracing::debug;
#[derive(Debug)]
pub struct SymbolTable {
symbols: DashMap<QualifiedName, Location>,
span_index: DashMap<std::path::PathBuf, Vec<(BytePos, QualifiedName)>>,
}
impl Default for SymbolTable {
fn default() -> Self {
Self::new()
}
}
impl SymbolTable {
#[must_use]
pub fn new() -> Self {
Self {
symbols: DashMap::new(),
span_index: DashMap::new(),
}
}
pub fn insert(&self, qualified_name: QualifiedName, location: Location) {
debug!("Inserting symbol: {} at {:?}", qualified_name, location);
self.symbols
.insert(qualified_name.clone(), location.clone());
let mut entry = self
.span_index
.entry(location.file_path.clone())
.or_default();
entry.push((location.span.start, qualified_name));
entry.sort_by_key(|(pos, _)| *pos);
}
#[must_use]
pub fn resolve_relative(&self, rel: &RelativeLocation, file: &Path) -> Option<Location> {
match rel {
RelativeLocation::Function { name, module } => {
let qname = self.build_qualified_name(file, module.as_deref(), name)?;
self.symbols.get(&qname).map(|entry| entry.clone())
}
RelativeLocation::Span { start, end } => Some(Location {
file_path: file.to_owned(),
span: Span {
start: BytePos(*start),
end: BytePos(*end),
},
}),
RelativeLocation::Symbol { qualified_name } => {
let qname: QualifiedName = qualified_name.parse().ok()?;
self.symbols.get(&qname).map(|entry| entry.clone())
}
}
}
#[must_use]
pub fn symbol_at_location(&self, location: &Location) -> Option<QualifiedName> {
if let Some(spans) = self.span_index.get(&location.file_path) {
let pos = location.span.start;
match spans.binary_search_by_key(&pos, |(start_pos, _)| *start_pos) {
Ok(index) => Some(spans[index].1.clone()),
Err(index) => {
if index > 0 {
Some(spans[index - 1].1.clone())
} else {
None
}
}
}
} else {
None
}
}
#[must_use]
pub fn symbols_in_span(&self, location: &Location) -> Vec<QualifiedName> {
if let Some(spans) = self.span_index.get(&location.file_path) {
let start_idx = match spans.binary_search_by_key(&location.span.start, |(pos, _)| *pos)
{
Ok(idx) => idx,
Err(idx) => idx.saturating_sub(1), };
let mut result = Vec::new();
for i in start_idx..spans.len() {
let (pos, qname) = &spans[i];
if *pos > location.span.end {
break; }
if location.span.contains(*pos) {
result.push(qname.clone());
}
}
result
} else {
Vec::new()
}
}
#[must_use]
pub fn get_location(&self, qualified_name: &QualifiedName) -> Option<Location> {
self.symbols.get(qualified_name).map(|entry| entry.clone())
}
#[must_use]
pub fn all_symbols(&self) -> Vec<(QualifiedName, Location)> {
self.symbols
.iter()
.map(|entry| (entry.key().clone(), entry.value().clone()))
.collect()
}
pub fn clear(&self) {
self.symbols.clear();
self.span_index.clear();
}
#[must_use]
pub fn len(&self) -> usize {
self.symbols.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.symbols.is_empty()
}
fn build_qualified_name(
&self,
file: &Path,
module: Option<&str>,
name: &str,
) -> Option<QualifiedName> {
let module_path = match module {
Some(explicit_module) => self.parse_explicit_module(explicit_module),
None => self.infer_module_from_file_path(file),
};
Some(QualifiedName::new(module_path, name.to_string()))
}
fn parse_explicit_module(&self, module: &str) -> Vec<String> {
module.split("::").map(std::string::ToString::to_string).collect()
}
fn infer_module_from_file_path(&self, file: &Path) -> Vec<String> {
let mut module_path = Vec::new();
if let Some(stem_str) = self.extract_significant_file_stem(file) {
module_path.push(stem_str);
}
self.add_parent_directories_to_module_path(file, &mut module_path);
module_path
}
fn extract_significant_file_stem(&self, file: &Path) -> Option<String> {
file.file_stem()
.and_then(|stem| stem.to_str())
.filter(|&stem_str| !matches!(stem_str, "mod" | "lib" | "main"))
.map(std::string::ToString::to_string)
}
fn add_parent_directories_to_module_path(&self, file: &Path, module_path: &mut Vec<String>) {
let mut current = file.parent();
while let Some(parent) = current {
if let Some(dir_name) = self.extract_directory_name(parent) {
if dir_name == "src" {
break;
}
module_path.insert(0, dir_name);
}
current = parent.parent();
}
}
fn extract_directory_name(&self, path: &Path) -> Option<String> {
path.file_name()
.and_then(|name| name.to_str())
.map(std::string::ToString::to_string)
}
}
pub struct SymbolTableBuilder {
table: Arc<SymbolTable>,
}
impl SymbolTableBuilder {
#[must_use]
pub fn new() -> Self {
Self {
table: Arc::new(SymbolTable::new()),
}
}
pub fn add_symbol(&self, qualified_name: QualifiedName, location: Location) {
self.table.insert(qualified_name, location);
}
#[must_use]
pub fn build(self) -> Arc<SymbolTable> {
self.table
}
}
impl Default for SymbolTableBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_symbol_table_insertion_and_lookup() {
let table = SymbolTable::new();
let qname = QualifiedName::new(
vec!["std".to_string(), "collections".to_string()],
"HashMap".to_string(),
);
let location = Location::new(PathBuf::from("src/lib.rs"), 100, 200);
table.insert(qname.clone(), location.clone());
assert_eq!(table.get_location(&qname), Some(location));
assert_eq!(table.len(), 1);
}
#[test]
fn test_relative_location_resolution() {
let table = SymbolTable::new();
let file_path = PathBuf::from("src/lib.rs");
let rel_span = RelativeLocation::Span {
start: 100,
end: 200,
};
let resolved = table.resolve_relative(&rel_span, &file_path).unwrap();
assert_eq!(resolved.file_path, file_path);
assert_eq!(resolved.span.start.0, 100);
assert_eq!(resolved.span.end.0, 200);
}
#[test]
fn test_qualified_name_parsing() {
let qname = QualifiedName::from_string("std::collections::HashMap").unwrap();
assert_eq!(qname.module_path, vec!["std", "collections"]);
assert_eq!(qname.name, "HashMap");
assert_eq!(qname.to_string(), "std::collections::HashMap");
}
#[test]
fn test_symbol_table_builder() {
let builder = SymbolTableBuilder::new();
let qname = QualifiedName::new(vec!["test".to_string()], "function".to_string());
let location = Location::new(PathBuf::from("test.rs"), 0, 10);
builder.add_symbol(qname.clone(), location.clone());
let table = builder.build();
assert_eq!(table.get_location(&qname), Some(location));
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}