use crate::error::Result;
use crate::symbols::{Symbol, Visibility};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct Scope {
pub id: usize,
pub kind: ScopeKind,
pub parent: Option<usize>,
pub children: Vec<usize>,
pub symbols: HashMap<String, Symbol>,
pub name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScopeKind {
Global,
Class,
Procedure,
Property,
Block,
Type,
Enum,
Reference,
}
#[derive(Debug, Clone)]
pub struct ScopeManager {
scopes: HashMap<usize, Scope>,
current_scope: usize,
global_scope: usize,
next_scope_id: usize,
module_scopes: Vec<usize>,
reference_scopes: Vec<usize>,
}
impl ScopeManager {
pub fn new() -> Self {
let mut manager = Self {
scopes: HashMap::new(),
current_scope: 0,
global_scope: 0,
next_scope_id: 1,
module_scopes: Vec::new(),
reference_scopes: Vec::new(),
};
let global = Scope {
id: 0,
kind: ScopeKind::Global,
parent: None,
children: Vec::new(),
symbols: HashMap::new(),
name: "global".to_string(),
};
manager.scopes.insert(0, global);
manager
}
pub fn push_scope(&mut self, kind: ScopeKind, name: String) -> usize {
self.create_scope(kind, name)
}
pub fn push_module_scope(&mut self, kind: ScopeKind, name: String) -> usize {
let scope_id = self.create_scope(kind, name);
self.module_scopes.push(scope_id);
scope_id
}
pub fn push_reference_scope(&mut self, name: String) -> usize {
let scope_id = self.create_scope(ScopeKind::Reference, name);
self.reference_scopes.push(scope_id);
scope_id
}
fn create_scope(&mut self, kind: ScopeKind, name: String) -> usize {
let scope_id = self.next_scope_id;
self.next_scope_id += 1;
let scope = Scope {
id: scope_id,
kind,
parent: Some(self.current_scope),
children: Vec::new(),
symbols: HashMap::new(),
name,
};
if let Some(current) = self.scopes.get_mut(&self.current_scope) {
current.children.push(scope_id);
}
self.scopes.insert(scope_id, scope);
self.current_scope = scope_id;
scope_id
}
pub fn pop_scope(&mut self) -> Result<()> {
let current = self.scopes.get(&self.current_scope).ok_or_else(|| {
crate::error::SemanticError::InvalidScope {
message: format!("Current scope {} not found", self.current_scope),
}
})?;
if let Some(parent) = current.parent {
self.current_scope = parent;
Ok(())
} else {
Err(crate::error::SemanticError::InvalidScope {
message: "Cannot pop global scope".to_string(),
})
}
}
pub fn current_scope_id(&self) -> usize {
self.current_scope
}
pub fn set_current_scope(&mut self, scope_id: usize) {
self.current_scope = scope_id;
}
pub fn get_scope(&self, scope_id: usize) -> Option<&Scope> {
self.scopes.get(&scope_id)
}
pub fn get_scope_mut(&mut self, scope_id: usize) -> Option<&mut Scope> {
self.scopes.get_mut(&scope_id)
}
pub fn add_symbol(&mut self, symbol: Symbol) -> Result<()> {
let scope = self.scopes.get_mut(&self.current_scope).ok_or_else(|| {
crate::error::SemanticError::InvalidScope {
message: format!("Current scope {} not found", self.current_scope),
}
})?;
if let Some(existing) = scope.symbols.get(&symbol.name) {
return Err(crate::error::SemanticError::DuplicateSymbol {
name: symbol.name.clone(),
location: symbol.location.clone(),
previous_location: existing.location.clone(),
});
}
scope.symbols.insert(symbol.name.clone(), symbol);
Ok(())
}
pub fn lookup(&self, name: &str) -> Option<&Symbol> {
let mut current = Some(self.current_scope);
while let Some(scope_id) = current {
if let Some(scope) = self.scopes.get(&scope_id) {
if let Some(symbol) = scope.symbols.get(name) {
return Some(symbol);
}
current = scope.parent;
} else {
break;
}
}
for module_scope_id in &self.module_scopes {
if let Some(symbol) = self
.scopes
.get(module_scope_id)
.and_then(|scope| scope.symbols.get(name))
&& self.can_access(symbol)
{
return Some(symbol);
}
}
for reference_scope_id in &self.reference_scopes {
if let Some(symbol) = self
.scopes
.get(reference_scope_id)
.and_then(|scope| scope.symbols.get(name))
&& self.can_access(symbol)
{
return Some(symbol);
}
}
None
}
pub fn lookup_in_scope(&self, scope_id: usize, name: &str) -> Option<&Symbol> {
self.scopes.get(&scope_id)?.symbols.get(name)
}
pub fn can_access(&self, symbol: &Symbol) -> bool {
match symbol.visibility {
Visibility::Public | Visibility::Global => true,
Visibility::Friend => {
true
}
Visibility::Private => {
self.is_in_same_module(symbol.scope_id)
}
}
}
fn is_in_same_module(&self, scope_id: usize) -> bool {
let current_module = self.find_module_scope(self.current_scope);
let other_module = self.find_module_scope(scope_id);
current_module == other_module
}
fn find_module_scope(&self, mut scope_id: usize) -> usize {
while let Some(scope) = self.scopes.get(&scope_id) {
match scope.kind {
ScopeKind::Global | ScopeKind::Class => return scope_id,
_ => {
if let Some(parent) = scope.parent {
scope_id = parent;
} else {
return scope_id;
}
}
}
}
scope_id
}
pub fn global_scope_id(&self) -> usize {
self.global_scope
}
pub fn get_scopes_by_kind(&self, kind: ScopeKind) -> Vec<&Scope> {
self.scopes.values().filter(|s| s.kind == kind).collect()
}
pub fn all_scopes(&self) -> Vec<&Scope> {
self.scopes.values().collect()
}
}
impl Default for ScopeManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::SourceLocation;
use crate::symbols::SymbolKind;
use crate::types::{TypeInfo, VBType};
fn symbol(name: &str, visibility: Visibility, scope_id: usize) -> Symbol {
Symbol {
name: name.to_string(),
kind: SymbolKind::Variable,
type_info: TypeInfo::new(VBType::Long),
visibility,
location: SourceLocation {
file: "test.bas".to_string(),
line: 1,
column: 1,
},
scope_id,
attributes: HashMap::new(),
}
}
#[test]
fn lookup_skips_private_symbols_from_other_modules() {
let mut manager = ScopeManager::new();
let module_a = manager.push_module_scope(ScopeKind::Global, "ModuleA".to_string());
manager
.add_symbol(symbol("Secret", Visibility::Private, module_a))
.unwrap();
manager
.add_symbol(symbol("Visible", Visibility::Public, module_a))
.unwrap();
manager.pop_scope().unwrap();
let _module_b = manager.push_module_scope(ScopeKind::Global, "ModuleB".to_string());
assert!(manager.lookup("Secret").is_none());
assert!(manager.lookup("Visible").is_some());
}
#[test]
fn lookup_finds_private_symbol_in_same_module() {
let mut manager = ScopeManager::new();
let module_a = manager.push_module_scope(ScopeKind::Global, "ModuleA".to_string());
manager
.add_symbol(symbol("Secret", Visibility::Private, module_a))
.unwrap();
assert!(manager.lookup("Secret").is_some());
}
#[test]
fn lookup_searches_module_scopes_in_push_order() {
let mut manager = ScopeManager::new();
let first = manager.push_module_scope(ScopeKind::Global, "First".to_string());
manager
.add_symbol(symbol("Name", Visibility::Public, first))
.unwrap();
manager.pop_scope().unwrap();
let second = manager.push_module_scope(ScopeKind::Global, "Second".to_string());
manager
.add_symbol(symbol("Name", Visibility::Public, second))
.unwrap();
manager.pop_scope().unwrap();
let found = manager.lookup("Name").expect("Symbol should resolve");
assert_eq!(found.scope_id, first);
}
#[test]
fn lookup_prefers_modules_over_references() {
let mut manager = ScopeManager::new();
let module = manager.push_module_scope(ScopeKind::Global, "Module".to_string());
manager
.add_symbol(symbol("Name", Visibility::Public, module))
.unwrap();
manager.pop_scope().unwrap();
let reference = manager.push_reference_scope("OLE Automation".to_string());
manager
.add_symbol(symbol("Name", Visibility::Public, reference))
.unwrap();
manager.pop_scope().unwrap();
let found = manager.lookup("Name").expect("Symbol should resolve");
assert_eq!(found.scope_id, module);
}
#[test]
fn all_scopes_returns_every_scope() {
let mut manager = ScopeManager::new();
let first = manager.push_scope(ScopeKind::Class, "First".to_string());
let second = manager.push_scope(ScopeKind::Procedure, "Second".to_string());
let mut ids: Vec<usize> = manager.all_scopes().iter().map(|scope| scope.id).collect();
ids.sort_unstable();
assert_eq!(ids, vec![0, first, second]);
}
#[test]
fn lookup_searches_reference_scopes_in_push_order() {
let mut manager = ScopeManager::new();
let first = manager.push_reference_scope("first".to_string());
manager
.add_symbol(symbol("Name", Visibility::Public, first))
.unwrap();
manager.pop_scope().unwrap();
let second = manager.push_reference_scope("second".to_string());
manager
.add_symbol(symbol("Name", Visibility::Public, second))
.unwrap();
manager.pop_scope().unwrap();
let found = manager.lookup("Name").expect("Symbol should resolve");
assert_eq!(found.scope_id, first);
}
}