use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::graph::unified::file::id::FileId;
use crate::graph::unified::node::id::NodeId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum ScopeArenaError {
#[error("stale or invalid ScopeId")]
StaleHandle,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ScopeKind {
Module = 0,
Function = 1,
Class = 2,
Namespace = 3,
Trait = 4,
Impl = 5,
}
impl ScopeKind {
#[inline]
#[must_use]
pub const fn discriminant(self) -> u8 {
self as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
pub struct ScopeId {
index: u32,
generation: u64,
}
impl ScopeId {
pub const INVALID: ScopeId = ScopeId {
index: u32::MAX,
generation: 0,
};
#[inline]
#[must_use]
pub const fn new(index: u32, generation: u64) -> Self {
Self { index, generation }
}
#[inline]
#[must_use]
pub const fn index(self) -> u32 {
self.index
}
#[inline]
#[must_use]
pub const fn generation(self) -> u64 {
self.generation
}
#[inline]
#[must_use]
pub const fn is_invalid(self) -> bool {
self.index == u32::MAX
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Scope {
pub kind: ScopeKind,
pub parent: ScopeId,
pub node: NodeId,
pub byte_span: (u32, u32),
pub file: FileId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
enum ScopeSlotState {
Occupied(Scope),
Vacant { next_free: Option<u32> },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ScopeSlot {
generation: u64,
state: ScopeSlotState,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ScopeArena {
slots: Vec<ScopeSlot>,
free_head: Option<u32>,
len: usize,
}
impl ScopeArena {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.len
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[inline]
#[must_use]
pub fn slot_count(&self) -> usize {
self.slots.len()
}
pub fn allocate(&mut self, scope: Scope) -> ScopeId {
self.len += 1;
if let Some(free_idx) = self.free_head {
let slot = &mut self.slots[free_idx as usize];
let ScopeSlotState::Vacant { next_free } = slot.state else {
unreachable!("free_head pointed at an occupied slot");
};
self.free_head = next_free;
slot.generation = slot.generation.saturating_add(1);
slot.state = ScopeSlotState::Occupied(scope);
ScopeId {
index: free_idx,
generation: slot.generation,
}
} else {
let index = self.slots.len() as u32;
self.slots.push(ScopeSlot {
generation: 1,
state: ScopeSlotState::Occupied(scope),
});
ScopeId {
index,
generation: 1,
}
}
}
pub fn free(&mut self, id: ScopeId) {
if id.is_invalid() {
return;
}
let Some(slot) = self.slots.get_mut(id.index as usize) else {
return;
};
if slot.generation != id.generation {
return;
}
if !matches!(slot.state, ScopeSlotState::Occupied(_)) {
return;
}
slot.generation = slot.generation.saturating_add(1);
slot.state = ScopeSlotState::Vacant {
next_free: self.free_head,
};
self.free_head = Some(id.index);
self.len -= 1;
}
pub fn set_parent(&mut self, id: ScopeId, parent: ScopeId) -> Result<(), ScopeArenaError> {
if id.is_invalid() {
return Err(ScopeArenaError::StaleHandle);
}
let slot = self
.slots
.get_mut(id.index as usize)
.ok_or(ScopeArenaError::StaleHandle)?;
if slot.generation != id.generation {
return Err(ScopeArenaError::StaleHandle);
}
match &mut slot.state {
ScopeSlotState::Occupied(scope) => {
scope.parent = parent;
Ok(())
}
ScopeSlotState::Vacant { .. } => Err(ScopeArenaError::StaleHandle),
}
}
#[must_use]
pub fn get(&self, id: ScopeId) -> Option<&Scope> {
if id.is_invalid() {
return None;
}
let slot = self.slots.get(id.index as usize)?;
if slot.generation != id.generation {
return None;
}
match &slot.state {
ScopeSlotState::Occupied(scope) => Some(scope),
ScopeSlotState::Vacant { .. } => None,
}
}
#[allow(dead_code)]
pub(crate) fn retain_by_node(&mut self, keep: &dyn Fn(NodeId) -> bool) {
let to_drop: Vec<ScopeId> = self
.iter()
.filter_map(|(id, scope)| (!keep(scope.node)).then_some(id))
.collect();
for id in to_drop {
self.free(id);
}
}
pub fn iter_node_ids(&self) -> impl Iterator<Item = NodeId> + '_ {
self.iter().map(|(_id, scope)| scope.node)
}
pub fn iter(&self) -> impl Iterator<Item = (ScopeId, &Scope)> + '_ {
self.slots
.iter()
.enumerate()
.filter_map(|(idx, slot)| match &slot.state {
ScopeSlotState::Occupied(scope) => {
let id = ScopeId::new(
u32::try_from(idx).expect("slot index fits u32"),
slot.generation,
);
Some((id, scope))
}
ScopeSlotState::Vacant { .. } => None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::unified::file::id::FileId;
use crate::graph::unified::node::id::NodeId;
fn sample_scope() -> Scope {
Scope {
kind: ScopeKind::Module,
parent: ScopeId::INVALID,
node: NodeId::new(0, 1),
byte_span: (0, 100),
file: FileId::new(0),
}
}
#[test]
fn allocate_assigns_incrementing_index_and_generation() {
let mut arena = ScopeArena::new();
let a = arena.allocate(sample_scope());
let b = arena.allocate(sample_scope());
assert_eq!(a.index(), 0);
assert_eq!(a.generation(), 1);
assert_eq!(b.index(), 1);
assert_eq!(b.generation(), 1);
assert_eq!(arena.len(), 2);
assert_eq!(arena.slot_count(), 2);
}
#[test]
fn get_returns_the_allocated_scope() {
let mut arena = ScopeArena::new();
let id = arena.allocate(sample_scope());
let scope = arena.get(id).expect("live scope must resolve");
assert_eq!(scope.kind, ScopeKind::Module);
assert_eq!(scope.byte_span, (0, 100));
}
#[test]
fn stale_handle_rejected_after_free_and_reallocate() {
let mut arena = ScopeArena::new();
let first = arena.allocate(sample_scope());
arena.free(first);
assert!(arena.get(first).is_none(), "freed handle must return None");
let second = arena.allocate(Scope {
kind: ScopeKind::Function,
..sample_scope()
});
assert_eq!(second.index(), first.index(), "same slot reused");
assert_ne!(
second.generation(),
first.generation(),
"generation must advance on reallocation"
);
assert!(
arena.get(first).is_none(),
"stale handle must not see the new occupant"
);
assert_eq!(arena.get(second).unwrap().kind, ScopeKind::Function);
}
#[test]
fn postcard_round_trip_preserves_arena_layout() {
let mut arena = ScopeArena::new();
arena.allocate(sample_scope());
let bytes = postcard::to_allocvec(&arena).expect("serialize");
let restored: ScopeArena = postcard::from_bytes(&bytes).expect("deserialize");
assert_eq!(restored.len(), arena.len());
assert_eq!(restored.slot_count(), arena.slot_count());
}
#[test]
fn double_free_is_noop() {
let mut arena = ScopeArena::new();
let id = arena.allocate(sample_scope());
arena.free(id);
let len_after_first_free = arena.len();
arena.free(id); assert_eq!(
arena.len(),
len_after_first_free,
"double-free must not corrupt len"
);
}
#[test]
fn free_invalid_or_out_of_bounds_id_is_noop() {
let mut arena = ScopeArena::new();
arena.free(ScopeId::INVALID);
arena.free(ScopeId::new(9999, 1));
assert!(arena.is_empty());
assert_eq!(arena.len(), 0);
assert_eq!(arena.slot_count(), 0);
}
#[test]
fn postcard_round_trip_preserves_free_list_and_generations() {
let mut arena = ScopeArena::new();
let a = arena.allocate(sample_scope());
let b = arena.allocate(sample_scope());
let c = arena.allocate(sample_scope());
arena.free(b);
let bytes = postcard::to_allocvec(&arena).expect("serialize");
let mut restored: ScopeArena = postcard::from_bytes(&bytes).expect("deserialize");
assert_eq!(restored.len(), 2);
assert_eq!(restored.slot_count(), 3);
assert!(
restored.get(a).is_some(),
"live handle a must survive round-trip"
);
assert!(
restored.get(b).is_none(),
"freed slot must stay stale across round-trip"
);
assert!(
restored.get(c).is_some(),
"live handle c must survive round-trip"
);
let d = restored.allocate(sample_scope());
assert_eq!(d.index(), b.index(), "free-list head must be persisted");
assert_ne!(
d.generation(),
b.generation(),
"generation must have advanced past the stale handle"
);
assert!(
restored.get(b).is_none(),
"stale handle b must remain stale after slot reuse"
);
assert_eq!(restored.len(), 3);
}
#[test]
fn scope_kind_discriminant_pins_wire_format() {
assert_eq!(ScopeKind::Module.discriminant(), 0);
assert_eq!(ScopeKind::Function.discriminant(), 1);
assert_eq!(ScopeKind::Class.discriminant(), 2);
assert_eq!(ScopeKind::Namespace.discriminant(), 3);
assert_eq!(ScopeKind::Trait.discriminant(), 4);
assert_eq!(ScopeKind::Impl.discriminant(), 5);
}
#[test]
fn set_parent_updates_existing_scope() {
let mut arena = ScopeArena::new();
let parent = arena.allocate(sample_scope());
let child = arena.allocate(sample_scope());
assert!(arena.set_parent(child, parent).is_ok());
assert_eq!(arena.get(child).unwrap().parent, parent);
}
#[test]
fn set_parent_rejects_stale_handle() {
let mut arena = ScopeArena::new();
let id = arena.allocate(sample_scope());
arena.free(id);
let other = arena.allocate(sample_scope());
assert!(
arena.set_parent(id, ScopeId::INVALID).is_err(),
"stale handle must be rejected"
);
assert_eq!(
arena.get(other).unwrap().parent,
ScopeId::INVALID,
"unrelated scope must be unaffected"
);
}
#[test]
fn scope_arena_iter_yields_live_slots_with_real_generation() {
let mut arena = ScopeArena::new();
let a = arena.allocate(sample_scope());
let b = arena.allocate(sample_scope());
let c = arena.allocate(sample_scope());
arena.free(b);
let pairs: Vec<(ScopeId, _)> = arena.iter().collect();
assert_eq!(pairs.len(), 2, "iter must skip the vacant slot for b");
let ids: Vec<ScopeId> = pairs.iter().map(|(id, _)| *id).collect();
assert!(ids.contains(&a), "iter must include slot a");
assert!(ids.contains(&c), "iter must include slot c");
assert!(
!ids.contains(&b),
"iter must not include freed slot b (stale id)"
);
for (id, scope_ref) in &pairs {
let looked_up = arena.get(*id).expect("iter-yielded id must be live");
assert_eq!(
std::ptr::from_ref::<Scope>(*scope_ref),
std::ptr::from_ref::<Scope>(looked_up),
"iter ref must alias arena.get ref"
);
}
}
#[test]
fn memory_budget_under_32_bytes_per_slot_at_1k_scopes() {
let mut arena = ScopeArena::new();
for idx in 0..1000 {
let scope = Scope {
kind: ScopeKind::Module,
parent: ScopeId::INVALID,
node: NodeId::new(idx, 1),
byte_span: (idx * 100, idx * 100 + 80),
file: FileId::new(0),
};
arena.allocate(scope);
}
let bytes = postcard::to_allocvec(&arena).expect("serialize");
let per_slot = bytes.len() as f64 / 1000.0;
assert!(
per_slot <= 48.0,
"scope arena postcard size {per_slot} B/slot exceeds 48-byte absolute ceiling \
(target: <=32 average, 48 hard cap per 05_TEST_PLAN.md T02)"
);
}
}