use rustc_hash::FxHashMap;
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct ScopeFrameId(pub u32);
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum DeclKind {
Undeclared,
Function,
FunctionDecl,
Variable,
Label,
Typedef,
EnumConstant,
}
#[derive(Clone)]
pub struct LazyScopeTable {
inner: Arc<RwLock<LazyInner>>,
}
struct LazyInner {
parent: Vec<Option<ScopeFrameId>>,
frames: Vec<FxHashMap<Arc<str>, DeclKind>>,
cache: FxHashMap<Arc<str>, FxHashMap<ScopeFrameId, DeclKind>>,
}
impl LazyScopeTable {
#[must_use]
pub fn new() -> Self {
Self {
inner: Arc::new(RwLock::new(LazyInner {
parent: vec![None],
frames: vec![FxHashMap::default()],
cache: FxHashMap::default(),
})),
}
}
#[must_use]
pub fn push_frame(&self, parent: ScopeFrameId) -> ScopeFrameId {
let mut inner = self.write_inner();
let id = ScopeFrameId(inner.frames.len() as u32);
inner.parent.push(Some(parent));
inner.frames.push(FxHashMap::default());
id
}
pub fn declare(&self, frame: ScopeFrameId, name: &str, kind: DeclKind) -> bool {
let mut inner = self.write_inner();
let frame_idx = frame.0 as usize;
if frame_idx >= inner.frames.len() {
return false;
}
inner.frames[frame_idx].insert(Arc::from(name), kind);
inner.cache.remove(name);
true
}
#[must_use]
pub fn lookup(&self, frame: ScopeFrameId, name: &str) -> DeclKind {
{
let inner = self.read_inner();
if let Some(kind) = inner.cache.get(name).and_then(|cache| cache.get(&frame)) {
return *kind;
}
}
let kind = self.walk_chain(frame, name);
let mut inner = self.write_inner();
inner
.cache
.entry(Arc::from(name))
.or_default()
.insert(frame, kind);
kind
}
fn walk_chain(&self, start: ScopeFrameId, name: &str) -> DeclKind {
let inner = self.read_inner();
let mut current = Some(start);
while let Some(frame) = current {
let frame_idx = frame.0 as usize;
if frame_idx >= inner.frames.len() {
break;
}
if let Some(&kind) = inner.frames[frame_idx].get(name) {
return kind;
}
current = inner.parent[frame_idx];
}
DeclKind::Undeclared
}
pub fn invalidate(&self) {
let mut inner = self.write_inner();
inner.cache.clear();
}
#[must_use]
pub fn frame_count(&self) -> usize {
self.read_inner().frames.len()
}
#[must_use]
pub fn cache_size(&self) -> usize {
self.read_inner().cache.values().map(FxHashMap::len).sum()
}
#[must_use]
pub fn root() -> ScopeFrameId {
ScopeFrameId(0)
}
fn read_inner(&self) -> RwLockReadGuard<'_, LazyInner> {
self.inner.read().unwrap_or_else(|error| {
panic!(
"C semantic lazy scope table read lock was poisoned: {error}. Fix: discard the translation-unit semantic cache after a panic; continuing could misclassify declarations."
)
})
}
fn write_inner(&self) -> RwLockWriteGuard<'_, LazyInner> {
self.inner.write().unwrap_or_else(|error| {
panic!(
"C semantic lazy scope table write lock was poisoned: {error}. Fix: discard the translation-unit semantic cache after a panic; continuing could corrupt declaration resolution."
)
})
}
}
impl Default for LazyScopeTable {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for LazyScopeTable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let inner = self.read_inner();
f.debug_struct("LazyScopeTable")
.field("frame_count", &inner.frames.len())
.field(
"cache_size",
&inner.cache.values().map(FxHashMap::len).sum::<usize>(),
)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_table_undeclared() {
let table = LazyScopeTable::new();
assert_eq!(
table.lookup(LazyScopeTable::root(), "x"),
DeclKind::Undeclared
);
assert_eq!(table.cache_size(), 1, "miss is also cached");
}
#[test]
fn root_declaration_resolves_from_root() {
let table = LazyScopeTable::new();
table.declare(LazyScopeTable::root(), "main", DeclKind::Function);
assert_eq!(
table.lookup(LazyScopeTable::root(), "main"),
DeclKind::Function
);
}
#[test]
fn child_frame_inherits_parent_declaration() {
let table = LazyScopeTable::new();
table.declare(LazyScopeTable::root(), "x", DeclKind::Variable);
let child = table.push_frame(LazyScopeTable::root());
assert_eq!(table.lookup(child, "x"), DeclKind::Variable);
}
#[test]
fn inner_frame_shadows_outer() {
let table = LazyScopeTable::new();
table.declare(LazyScopeTable::root(), "x", DeclKind::Function);
let child = table.push_frame(LazyScopeTable::root());
table.declare(child, "x", DeclKind::Variable);
assert_eq!(table.lookup(child, "x"), DeclKind::Variable);
assert_eq!(
table.lookup(LazyScopeTable::root(), "x"),
DeclKind::Function
);
}
#[test]
fn second_lookup_hits_cache() {
let table = LazyScopeTable::new();
table.declare(LazyScopeTable::root(), "x", DeclKind::Variable);
assert_eq!(
table.lookup(LazyScopeTable::root(), "x"),
DeclKind::Variable
);
let initial = table.cache_size();
assert_eq!(
table.lookup(LazyScopeTable::root(), "x"),
DeclKind::Variable
);
assert_eq!(
table.cache_size(),
initial,
"second lookup must not grow cache"
);
}
#[test]
fn declare_invalidates_cached_entries_for_name() {
let table = LazyScopeTable::new();
table.declare(LazyScopeTable::root(), "x", DeclKind::Function);
let child = table.push_frame(LazyScopeTable::root());
assert_eq!(table.lookup(child, "x"), DeclKind::Function);
table.declare(child, "x", DeclKind::Variable);
assert_eq!(table.lookup(child, "x"), DeclKind::Variable);
}
#[test]
fn deep_chain_walks_to_root() {
let table = LazyScopeTable::new();
table.declare(LazyScopeTable::root(), "x", DeclKind::EnumConstant);
let mut current = LazyScopeTable::root();
for _ in 0..10 {
current = table.push_frame(current);
}
assert_eq!(table.lookup(current, "x"), DeclKind::EnumConstant);
}
#[test]
fn invalidate_clears_cache() {
let table = LazyScopeTable::new();
table.declare(LazyScopeTable::root(), "x", DeclKind::Variable);
assert_eq!(
table.lookup(LazyScopeTable::root(), "x"),
DeclKind::Variable
);
assert!(table.cache_size() > 0);
table.invalidate();
assert_eq!(table.cache_size(), 0);
}
#[test]
fn poisoned_scope_table_lock_is_not_silently_recovered() {
let table = LazyScopeTable::new();
let poisoned = table.clone();
let _ = std::thread::spawn(move || {
let _guard = poisoned.write_inner();
panic!("poison lazy scope table");
})
.join();
let panic = std::panic::catch_unwind(|| {
let _ = table.lookup(LazyScopeTable::root(), "x");
})
.expect_err("poisoned semantic scope table must panic instead of recovering");
let message = panic
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| panic.downcast_ref::<&'static str>().copied())
.unwrap_or("<non-string panic>");
assert!(message.contains("read lock was poisoned"), "{message}");
}
#[test]
fn send_and_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<LazyScopeTable>();
assert_sync::<LazyScopeTable>();
}
}