Skip to main content

sqry_core/graph/unified/storage/
arena.rs

1//! `NodeArena`: Contiguous node storage with generational indices.
2//!
3//! This module implements `NodeArena`, the primary node storage for the
4//! unified graph architecture. It provides:
5//!
6//! - **O(1) access**: Direct indexing into contiguous storage
7//! - **Generational safety**: Stale IDs return `None` instead of wrong data
8//! - **Free list reuse**: Deleted slots are recycled efficiently
9//!
10//! # Design
11//!
12//! The arena uses a slot-based design:
13//! - Each slot is either occupied (with data) or vacant (with next-free pointer)
14//! - Occupied slots store `NodeEntry` with generation counter
15//! - Vacant slots form a singly-linked free list
16//! - Generation counter increments on each reuse to detect stale IDs
17
18use std::fmt;
19
20use serde::{Deserialize, Serialize};
21
22use super::super::file::id::FileId;
23use super::super::node::id::{GenerationOverflowError, NodeId};
24use super::super::node::kind::NodeKind;
25use super::super::string::id::StringId;
26use crate::graph::body_hash::BodyHash128;
27
28/// Error type for arena allocation operations.
29///
30/// This enum covers all possible failure modes during node allocation,
31/// providing graceful error handling instead of panics.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum ArenaError {
34    /// Generation counter would overflow, risking stale ID collision.
35    GenerationOverflow(GenerationOverflowError),
36
37    /// Free list is corrupted: an occupied slot was found in the free list.
38    ///
39    /// This indicates a serious internal consistency error, likely caused by
40    /// concurrent modification without proper synchronization, memory corruption,
41    /// or a bug in the arena implementation.
42    FreeListCorruption {
43        /// Index of the corrupted slot.
44        index: u32,
45    },
46
47    /// Arena capacity exceeded: cannot allocate more than `u32::MAX` nodes.
48    CapacityExceeded,
49}
50
51impl fmt::Display for ArenaError {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            ArenaError::GenerationOverflow(e) => write!(f, "{e}"),
55            ArenaError::FreeListCorruption { index } => {
56                write!(
57                    f,
58                    "free list corruption: occupied slot found at index {index}"
59                )
60            }
61            ArenaError::CapacityExceeded => {
62                write!(
63                    f,
64                    "arena capacity exceeded: cannot allocate more than {} nodes",
65                    u32::MAX
66                )
67            }
68        }
69    }
70}
71
72impl std::error::Error for ArenaError {
73    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
74        match self {
75            ArenaError::GenerationOverflow(e) => Some(e),
76            _ => None,
77        }
78    }
79}
80
81impl From<GenerationOverflowError> for ArenaError {
82    fn from(e: GenerationOverflowError) -> Self {
83        ArenaError::GenerationOverflow(e)
84    }
85}
86
87/// State of a slot in the arena.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub enum SlotState<T> {
90    /// Slot contains an occupied entry.
91    Occupied(T),
92    /// Slot is vacant, contains next free index (or None if end of list).
93    Vacant {
94        /// Index of the next free slot in the free list, or None if end.
95        next_free: Option<u32>,
96    },
97}
98
99/// A slot in the arena with generation tracking.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct Slot<T> {
102    /// Current generation for this slot.
103    generation: u64,
104    /// Slot state (occupied or vacant).
105    state: SlotState<T>,
106}
107
108impl<T> Slot<T> {
109    /// Creates a new vacant slot with the given generation and next free.
110    #[allow(dead_code)] // Used by Compaction (Step 15) + V13→V14 arena translation
111    pub(crate) fn new_vacant(generation: u64, next_free: Option<u32>) -> Self {
112        Self {
113            generation,
114            state: SlotState::Vacant { next_free },
115        }
116    }
117
118    /// Creates a new occupied slot with the given generation and data.
119    pub(crate) fn new_occupied(generation: u64, data: T) -> Self {
120        Self {
121            generation,
122            state: SlotState::Occupied(data),
123        }
124    }
125
126    /// Returns true if this slot is occupied.
127    #[inline]
128    pub fn is_occupied(&self) -> bool {
129        matches!(self.state, SlotState::Occupied(_))
130    }
131
132    /// Returns true if this slot is vacant.
133    #[inline]
134    pub fn is_vacant(&self) -> bool {
135        matches!(self.state, SlotState::Vacant { .. })
136    }
137
138    /// Returns the generation of this slot.
139    #[inline]
140    pub fn generation(&self) -> u64 {
141        self.generation
142    }
143
144    /// Returns a reference to the occupied data, if any.
145    pub fn get(&self) -> Option<&T> {
146        match &self.state {
147            SlotState::Occupied(data) => Some(data),
148            SlotState::Vacant { .. } => None,
149        }
150    }
151
152    /// Returns a reference to the slot state (occupied or vacant).
153    #[inline]
154    pub fn state(&self) -> &SlotState<T> {
155        &self.state
156    }
157
158    /// Returns a mutable reference to the occupied data, if any.
159    pub fn get_mut(&mut self) -> Option<&mut T> {
160        match &mut self.state {
161            SlotState::Occupied(data) => Some(data),
162            SlotState::Vacant { .. } => None,
163        }
164    }
165}
166
167/// A node entry stored in the arena.
168///
169/// This is the actual data stored for each node in the graph.
170#[allow(clippy::struct_excessive_bools)] // persisted graph row keeps compact flag fields for compatibility
171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
172pub struct NodeEntry {
173    /// The kind of code entity this node represents.
174    pub kind: NodeKind,
175    /// Interned name of the symbol.
176    pub name: StringId,
177    /// File containing this node.
178    pub file: FileId,
179    /// Start byte offset in the file.
180    pub start_byte: u32,
181    /// End byte offset in the file.
182    pub end_byte: u32,
183    /// Start line (1-indexed).
184    pub start_line: u32,
185    /// Start column (0-indexed).
186    pub start_column: u32,
187    /// End line (1-indexed).
188    pub end_line: u32,
189    /// End column (0-indexed).
190    pub end_column: u32,
191    /// Optional signature/type information.
192    pub signature: Option<StringId>,
193    /// Optional docstring.
194    pub doc: Option<StringId>,
195    /// Optional qualified name (e.g., module.Class.method).
196    pub qualified_name: Option<StringId>,
197    /// Optional visibility modifier (public, private, protected, etc.).
198    pub visibility: Option<StringId>,
199    /// Whether this is an async function/method.
200    pub is_async: bool,
201    /// Whether this is a static member.
202    pub is_static: bool,
203    /// 128-bit body hash for duplicate detection.
204    ///
205    /// Computed from the raw body bytes of hashable symbol kinds:
206    /// Function, Method, Class, Struct, Enum, Interface, Trait, Module.
207    ///
208    /// CRITICAL: This field MUST remain BEFORE `is_unsafe` for
209    /// postcard serialization compatibility with older snapshots.
210    /// Uses `#[serde(default)]` to read older V2 snapshots that lack this field.
211    /// DO NOT use `skip_serializing_if` - postcard is positional and skipping corrupts the stream.
212    #[serde(default)]
213    pub body_hash: Option<BodyHash128>,
214    /// Whether this is an unsafe function (Rust, C, etc.).
215    ///
216    /// For FFI functions, this indicates whether the function is declared with
217    /// `unsafe` modifier (Haskell), `unsafe` keyword (Rust), or similar safety
218    /// markers in other languages.
219    ///
220    /// CRITICAL: This field is placed AFTER `body_hash` to maintain postcard
221    /// serialization compatibility. Adding fields before `body_hash` would
222    /// corrupt existing snapshots due to positional deserialization.
223    /// Uses `#[serde(default)]` to read older snapshots that lack this field.
224    #[serde(default)]
225    pub is_unsafe: bool,
226    /// Whether this node is a real source-defined declaration (as opposed to a
227    /// call/FFI/callee/reference stub created during edge construction).
228    ///
229    /// Set to `true` only at creation sites that KNOW they are materializing a
230    /// genuine declaration (typed declaration helpers, classpath bytecode
231    /// emitter); every other path defaults to `false`. Monotonic: once `true`
232    /// it is never cleared (OR-in on metadata update + unification merge).
233    ///
234    /// CRITICAL: This field is placed AFTER `is_unsafe` as the LAST field to
235    /// maintain postcard serialization compatibility. Adding fields before it
236    /// would corrupt existing snapshots due to positional deserialization.
237    /// Uses `#[serde(default)]` to read older snapshots that lack this field
238    /// (decodes to `false`, preserving pre-V16 behavior byte-for-byte).
239    #[serde(default)]
240    pub is_definition: bool,
241}
242
243impl NodeEntry {
244    /// Creates a new node entry with minimal required fields.
245    #[must_use]
246    pub fn new(kind: NodeKind, name: StringId, file: FileId) -> Self {
247        Self {
248            kind,
249            name,
250            file,
251            start_byte: 0,
252            end_byte: 0,
253            start_line: 0,
254            start_column: 0,
255            end_line: 0,
256            end_column: 0,
257            signature: None,
258            doc: None,
259            qualified_name: None,
260            visibility: None,
261            is_async: false,
262            is_static: false,
263            is_unsafe: false,
264            body_hash: None,
265            is_definition: false,
266        }
267    }
268
269    /// Sets the byte range for this node.
270    #[must_use]
271    pub fn with_byte_range(mut self, start: u32, end: u32) -> Self {
272        self.start_byte = start;
273        self.end_byte = end;
274        self
275    }
276
277    /// Sets the line/column range for this node.
278    #[must_use]
279    pub fn with_location(
280        mut self,
281        start_line: u32,
282        start_column: u32,
283        end_line: u32,
284        end_column: u32,
285    ) -> Self {
286        self.start_line = start_line;
287        self.start_column = start_column;
288        self.end_line = end_line;
289        self.end_column = end_column;
290        self
291    }
292
293    /// Whether this arena entry is a Phase 4c-prime cross-file
294    /// unification loser (an "inert" merged duplicate).
295    ///
296    /// Phase 4c-prime merges cross-file stubs that share a qualified
297    /// name and a call-compatible kind into a single canonical winner,
298    /// folding metadata + flags into the winner and calling
299    /// [`crate::graph::unified::build::unification::merge_node_into`] on
300    /// the loser. The loser's arena slot stays `Occupied` (so
301    /// `NodeArena::slot_count()` stays stable for CSR `row_ptr` sizing),
302    /// but its `name` is cleared to [`StringId::INVALID`] and its
303    /// `qualified_name` to `None`. That sentinel is what this method
304    /// detects.
305    ///
306    /// Callers that iterate `NodeArena::iter()` for publish-visible
307    /// purposes (symbol listings, document-symbol enumeration,
308    /// find_by_pattern-style fanout) MUST skip entries where
309    /// `is_unified_loser()` returns `true`. Callers working on rebuild
310    /// internals (Gate 0d residue checks, CSR compaction) must continue
311    /// to include them.
312    #[inline]
313    #[must_use]
314    pub fn is_unified_loser(&self) -> bool {
315        self.name == StringId::INVALID
316    }
317
318    /// Whether this node is a real source-defined declaration.
319    ///
320    /// See the [`is_definition`](Self::is_definition) field for the precise
321    /// contract (default `false`, opt-in `true` at declaration sites,
322    /// monotonic).
323    #[inline]
324    #[must_use]
325    pub fn is_definition(&self) -> bool {
326        self.is_definition
327    }
328
329    /// Whether this entry is a synthetic placeholder according to its name shape.
330    ///
331    /// `C_SUPPRESS` introduced an explicit `NodeFlags::SYNTHETIC` flag
332    /// stored in [`crate::graph::unified::storage::metadata::NodeMetadataStore`];
333    /// see
334    /// [`crate::graph::unified::storage::metadata::NodeMetadataStore::is_synthetic`]
335    /// for the authoritative O(1) lookup that publish-visible callers
336    /// (`find_by_pattern`, MCP tool surfaces, CLI `search --exact`) use.
337    ///
338    /// This helper is the **fallback name-shape check** that runs alongside
339    /// the metadata bit so the filter remains correct in three boundary cases:
340    ///
341    /// 1. **V10 snapshots written before the synthetic bit existed** —
342    ///    pre-C_SUPPRESS Go indexes have no metadata entries flagging
343    ///    these placeholders, but their names still follow the canonical
344    ///    `<...>` (angle-bracket-wrapped pseudo-identifier) and
345    ///    `<ident>@<digit>` (per-binding-site offset suffix) shapes the
346    ///    Go plugin emits via
347    ///    `process_field_access_unified` and
348    ///    `sqry-lang-go/src/relations/local_scopes.rs`.
349    /// 2. **Cross-file unification losers** that retained their original
350    ///    synthetic name but lost their metadata entry during the
351    ///    rebuild's metadata retention pass.
352    /// 3. **Future plugins** that emit similarly-shaped placeholders
353    ///    without remembering to flip the bit — the name shape is a
354    ///    structural invariant Go identifiers cannot satisfy
355    ///    (Go identifiers are `[a-zA-Z_][a-zA-Z0-9_]*`, so `<` and `@`
356    ///    can never appear in a real Go symbol name).
357    ///
358    /// Callers that want the canonical "is this node user-visible"
359    /// answer should OR this against the metadata-store check.
360    /// `find_by_pattern` and the MCP analysis surfaces wire both checks
361    /// together via a single helper so the filter stays in lockstep.
362    ///
363    /// `display_name` is the simple-name string the interner returns
364    /// for `NodeEntry.name`; passing the qualified name instead is
365    /// also valid because the synthetic shape appears in both
366    /// (the Go plugin builds the qualified name from the same shape).
367    #[inline]
368    #[must_use]
369    pub fn is_synthetic_placeholder_name(display_name: &str) -> bool {
370        is_synthetic_placeholder_name_shape(display_name)
371    }
372
373    /// Sets the signature for this node.
374    #[must_use]
375    pub fn with_signature(mut self, signature: StringId) -> Self {
376        self.signature = Some(signature);
377        self
378    }
379
380    /// Sets the docstring for this node.
381    #[must_use]
382    pub fn with_doc(mut self, doc: StringId) -> Self {
383        self.doc = Some(doc);
384        self
385    }
386
387    /// Sets the qualified name for this node.
388    #[must_use]
389    pub fn with_qualified_name(mut self, qualified_name: StringId) -> Self {
390        self.qualified_name = Some(qualified_name);
391        self
392    }
393
394    /// Sets the visibility modifier for this node.
395    #[must_use]
396    pub fn with_visibility(mut self, visibility: StringId) -> Self {
397        self.visibility = Some(visibility);
398        self
399    }
400
401    /// Sets the async flag for this node.
402    #[must_use]
403    pub fn with_async(mut self, is_async: bool) -> Self {
404        self.is_async = is_async;
405        self
406    }
407
408    /// Sets the static flag for this node.
409    #[must_use]
410    pub fn with_static(mut self, is_static: bool) -> Self {
411        self.is_static = is_static;
412        self
413    }
414
415    /// Sets the unsafe flag for this node.
416    #[must_use]
417    pub fn with_unsafe(mut self, is_unsafe: bool) -> Self {
418        self.is_unsafe = is_unsafe;
419        self
420    }
421
422    /// Sets the definition flag for this node.
423    ///
424    /// See [`is_definition`](Self::is_definition) (the field) for the contract.
425    /// Used by direct-construction sinks (e.g. the classpath bytecode emitter)
426    /// to mark genuine decoded declarations.
427    #[must_use]
428    pub fn with_definition(mut self, is_definition: bool) -> Self {
429        self.is_definition = is_definition;
430        self
431    }
432
433    /// Sets the body hash for this node.
434    ///
435    /// The body hash is a 128-bit hash computed from the raw body bytes
436    /// of the symbol. Used for duplicate code detection.
437    #[must_use]
438    pub fn with_body_hash(mut self, hash: BodyHash128) -> Self {
439        self.body_hash = Some(hash);
440        self
441    }
442}
443
444/// Contiguous node storage with generational indices.
445///
446/// `NodeArena` provides O(1) access to nodes by `NodeId`, with stale
447/// reference detection via generation counters.
448///
449/// # Free List Management
450///
451/// When a node is removed, its slot is added to the free list for reuse.
452/// This avoids memory fragmentation and keeps indices stable.
453///
454/// # Example
455///
456/// ```rust,ignore
457/// let mut arena = NodeArena::new();
458/// let id = arena.alloc(NodeEntry::new(NodeKind::Function, name, file))?;
459///
460/// let node = arena.get(id).expect("node exists");
461/// assert_eq!(node.kind, NodeKind::Function);
462///
463/// arena.remove(id)?;
464/// assert!(arena.get(id).is_none()); // Stale ID returns None
465/// ```
466#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct NodeArena {
468    /// Slots for node storage.
469    slots: Vec<Slot<NodeEntry>>,
470    /// Head of the free list (index of first free slot).
471    free_head: Option<u32>,
472    /// Number of currently occupied slots.
473    len: usize,
474}
475
476impl NodeArena {
477    /// Creates a new empty arena.
478    #[must_use]
479    pub fn new() -> Self {
480        Self {
481            slots: Vec::new(),
482            free_head: None,
483            len: 0,
484        }
485    }
486
487    /// Creates a new arena with the specified initial capacity.
488    #[must_use]
489    pub fn with_capacity(capacity: usize) -> Self {
490        Self {
491            slots: Vec::with_capacity(capacity),
492            free_head: None,
493            len: 0,
494        }
495    }
496
497    /// Reconstructs an arena from its raw component parts.
498    ///
499    /// Used by the V13→V14 snapshot upconvert to rebuild the live arena from
500    /// translated slots while preserving slot indices, generations, and the
501    /// free list exactly (so existing `NodeId` references in edges stay
502    /// valid). The caller is responsible for the slot/free-list invariants;
503    /// this is a thin structural constructor, not a validating one.
504    #[must_use]
505    pub(crate) fn from_raw_parts(
506        slots: Vec<Slot<NodeEntry>>,
507        free_head: Option<u32>,
508        len: usize,
509    ) -> Self {
510        Self {
511            slots,
512            free_head,
513            len,
514        }
515    }
516
517    /// Returns the number of occupied slots.
518    #[inline]
519    #[must_use]
520    pub fn len(&self) -> usize {
521        self.len
522    }
523
524    /// Returns true if the arena is empty.
525    #[inline]
526    #[must_use]
527    pub fn is_empty(&self) -> bool {
528        self.len == 0
529    }
530
531    /// Returns the allocated capacity (slots that can be stored without reallocation).
532    #[inline]
533    #[must_use]
534    pub fn capacity(&self) -> usize {
535        self.slots.capacity()
536    }
537
538    /// Returns the total number of slots (occupied + vacant).
539    ///
540    /// This is different from `len()` which counts only occupied slots,
541    /// and `capacity()` which counts pre-allocated slots.
542    #[inline]
543    #[must_use]
544    pub fn slot_count(&self) -> usize {
545        self.slots.len()
546    }
547
548    /// Allocates a new node and returns its `NodeId`.
549    ///
550    /// If there are free slots available, one is reused. Otherwise, a new
551    /// slot is appended to the storage.
552    ///
553    /// # Errors
554    ///
555    /// Returns [`ArenaError`] if:
556    /// - Generation counter would overflow (`GenerationOverflow`)
557    /// - Free list is corrupted (`FreeListCorruption`)
558    /// - Arena capacity exceeded (`CapacityExceeded`)
559    pub fn alloc(&mut self, entry: NodeEntry) -> Result<NodeId, ArenaError> {
560        self.len += 1;
561
562        if let Some(free_idx) = self.free_head {
563            // Check bounds before accessing slot - corrupted free list could point out of bounds
564            let slot_index = free_idx as usize;
565            if slot_index >= self.slots.len() {
566                self.len -= 1; // Rollback
567                return Err(ArenaError::FreeListCorruption { index: free_idx });
568            }
569
570            // Reuse a free slot (bounds now guaranteed valid)
571            let slot = &mut self.slots[slot_index];
572
573            // Update free list head
574            self.free_head = match &slot.state {
575                SlotState::Vacant { next_free } => *next_free,
576                SlotState::Occupied(_) => {
577                    // Free list corruption detected - return error instead of panicking
578                    self.len -= 1; // Rollback
579                    return Err(ArenaError::FreeListCorruption { index: free_idx });
580                }
581            };
582
583            // Increment generation for reuse detection (respect MAX_GENERATION limit)
584            // Use NodeId::try_increment_generation which checks against MAX_GENERATION, not u64::MAX
585            let temp_id = NodeId::new(free_idx, slot.generation);
586            let new_generation = temp_id.try_increment_generation().map_err(|mut e| {
587                self.len -= 1; // Rollback
588                e.index = free_idx; // Ensure index is correct
589                ArenaError::GenerationOverflow(e)
590            })?;
591
592            slot.generation = new_generation;
593            slot.state = SlotState::Occupied(entry);
594
595            Ok(NodeId::new(free_idx, new_generation))
596        } else {
597            // Append a new slot
598            let index = self.slots.len();
599
600            // Check capacity before allocation using checked conversion
601            let Ok(index_u32) = u32::try_from(index) else {
602                self.len -= 1; // Rollback
603                return Err(ArenaError::CapacityExceeded);
604            };
605
606            let slot = Slot::new_occupied(1, entry);
607            self.slots.push(slot);
608
609            Ok(NodeId::new(index_u32, 1))
610        }
611    }
612
613    /// Returns a reference to the node with the given ID.
614    ///
615    /// Returns `None` if the ID is invalid or stale (generation mismatch).
616    #[must_use]
617    pub fn get(&self, id: NodeId) -> Option<&NodeEntry> {
618        if id.is_invalid() {
619            return None;
620        }
621
622        let index = id.index() as usize;
623        let slot = self.slots.get(index)?;
624
625        // Check generation match
626        if slot.generation != id.generation() {
627            return None;
628        }
629
630        slot.get()
631    }
632
633    /// Returns a mutable reference to the node with the given ID.
634    ///
635    /// Returns `None` if the ID is invalid or stale.
636    #[must_use]
637    pub fn get_mut(&mut self, id: NodeId) -> Option<&mut NodeEntry> {
638        if id.is_invalid() {
639            return None;
640        }
641
642        let index = id.index() as usize;
643        let slot = self.slots.get_mut(index)?;
644
645        // Check generation match
646        if slot.generation != id.generation() {
647            return None;
648        }
649
650        slot.get_mut()
651    }
652
653    /// Checks if the given ID is valid (not stale).
654    #[must_use]
655    pub fn contains(&self, id: NodeId) -> bool {
656        self.get(id).is_some()
657    }
658
659    /// Removes the node with the given ID.
660    ///
661    /// Returns the removed entry, or `None` if the ID was invalid/stale.
662    /// This method is idempotent: calling it twice with the same ID will
663    /// return `None` on the second call without corrupting the arena.
664    pub fn remove(&mut self, id: NodeId) -> Option<NodeEntry> {
665        if id.is_invalid() {
666            return None;
667        }
668
669        let index = id.index() as usize;
670        let slot = self.slots.get_mut(index)?;
671
672        // Check generation match
673        if slot.generation != id.generation() {
674            return None;
675        }
676
677        // Only proceed if slot is occupied - this makes remove idempotent
678        // and prevents double-decrement of len / double-enqueue to free list
679        if let SlotState::Occupied(_) = &slot.state {
680            let old_state = std::mem::replace(
681                &mut slot.state,
682                SlotState::Vacant {
683                    next_free: self.free_head,
684                },
685            );
686
687            // Update free list and len only when actually removing
688            self.free_head = Some(id.index());
689            self.len -= 1;
690
691            if let SlotState::Occupied(entry) = old_state {
692                return Some(entry);
693            }
694        }
695
696        None
697    }
698
699    /// Iterates over all occupied entries with their IDs.
700    pub fn iter(&self) -> impl Iterator<Item = (NodeId, &NodeEntry)> {
701        self.slots.iter().enumerate().filter_map(|(index, slot)| {
702            if let SlotState::Occupied(entry) = &slot.state {
703                let index_u32 = u32::try_from(index).ok()?;
704                Some((NodeId::new(index_u32, slot.generation), entry))
705            } else {
706                None
707            }
708        })
709    }
710
711    /// Iterates over all occupied entries with mutable access.
712    pub fn iter_mut(&mut self) -> impl Iterator<Item = (NodeId, &mut NodeEntry)> {
713        self.slots
714            .iter_mut()
715            .enumerate()
716            .filter_map(|(index, slot)| {
717                let generation = slot.generation;
718                if let SlotState::Occupied(entry) = &mut slot.state {
719                    let index_u32 = u32::try_from(index).ok()?;
720                    Some((NodeId::new(index_u32, generation), entry))
721                } else {
722                    None
723                }
724            })
725    }
726
727    /// Clears all nodes from the arena.
728    ///
729    /// This resets the arena to an empty state, but retains allocated capacity.
730    pub fn clear(&mut self) {
731        self.slots.clear();
732        self.free_head = None;
733        self.len = 0;
734    }
735
736    /// Reserves capacity for at least `additional` more nodes.
737    pub fn reserve(&mut self, additional: usize) {
738        self.slots.reserve(additional);
739    }
740
741    /// Returns the slot at the given index, if any.
742    ///
743    /// This is a low-level method for internal use. Prefer `get()` for normal access.
744    #[must_use]
745    pub fn slot(&self, index: u32) -> Option<&Slot<NodeEntry>> {
746        self.slots.get(index as usize)
747    }
748
749    /// Pre-allocates a contiguous range of occupied slots for parallel commit.
750    ///
751    /// Returns the start index of the allocated range. All slots are initialized
752    /// as `Occupied` with clones of the `placeholder` entry at generation 1.
753    /// The free list is **not** touched, preserving its invariants.
754    ///
755    /// A `count` of zero is a no-op and returns the current slot count as the
756    /// start index.
757    ///
758    /// # Errors
759    ///
760    /// Returns [`ArenaError::CapacityExceeded`] if the resulting slot count
761    /// would exceed `u32::MAX`.
762    pub fn alloc_range(&mut self, count: u32, placeholder: &NodeEntry) -> Result<u32, ArenaError> {
763        if count == 0 {
764            // No-op: return current slot count as start index (safe because
765            // slot_count <= u32::MAX is maintained as an invariant).
766            return Ok(u32::try_from(self.slots.len())
767                .unwrap_or_else(|_| unreachable!("slot_count invariant violated")));
768        }
769
770        let start = self.slots.len();
771        let new_total = start
772            .checked_add(count as usize)
773            .ok_or(ArenaError::CapacityExceeded)?;
774
775        // Verify new total fits in u32 (arena index space).
776        if new_total > u32::MAX as usize + 1 {
777            return Err(ArenaError::CapacityExceeded);
778        }
779
780        let start_u32 = u32::try_from(start).map_err(|_| ArenaError::CapacityExceeded)?;
781
782        // Reserve and fill in one go.
783        self.slots.reserve(count as usize);
784        for _ in 0..count {
785            self.slots.push(Slot::new_occupied(1, placeholder.clone()));
786        }
787
788        // Increment occupied count immediately — arena state is always consistent.
789        self.len += count as usize;
790
791        Ok(start_u32)
792    }
793
794    /// Returns a mutable sub-slice of the internal slot storage.
795    ///
796    /// This is intended for parallel writes after [`alloc_range`](Self::alloc_range):
797    /// each thread can write to its own non-overlapping portion of the slice.
798    ///
799    /// # Panics
800    ///
801    /// Panics if `start + count` exceeds the slot count.
802    #[must_use]
803    pub fn bulk_slice_mut(&mut self, start: u32, count: u32) -> &mut [Slot<NodeEntry>] {
804        let begin = start as usize;
805        let end = begin + count as usize;
806        &mut self.slots[begin..end]
807    }
808
809    /// Truncates the slot storage to `saved_slot_count` slots.
810    ///
811    /// Used to roll back a failed bulk allocation. Any slots beyond
812    /// `saved_slot_count` are dropped, and `len` is adjusted to reflect
813    /// only the occupied slots that remain.
814    ///
815    /// # Panics
816    ///
817    /// Panics if `saved_slot_count` is greater than the current slot count
818    /// (cannot grow via truncation).
819    pub fn truncate_to(&mut self, saved_slot_count: usize) {
820        assert!(
821            saved_slot_count <= self.slots.len(),
822            "truncate_to({saved_slot_count}) exceeds current slot count ({})",
823            self.slots.len(),
824        );
825
826        // Count occupied slots that will be dropped.
827        let dropped_occupied = self.slots[saved_slot_count..]
828            .iter()
829            .filter(|s| s.is_occupied())
830            .count();
831
832        self.slots.truncate(saved_slot_count);
833        self.len -= dropped_occupied;
834
835        // Rebuild free list: remove any entries that pointed into the truncated
836        // region. We walk the chain and keep only links to retained slots.
837        self.rebuild_free_list_after_truncate(saved_slot_count);
838    }
839
840    /// Rebuilds the free list after a truncation, dropping any links to slots
841    /// at or beyond `cutoff`.
842    ///
843    /// Scans retained slots for vacant entries and re-links them into a fresh
844    /// free list. This is O(cutoff) but `truncate_to` is a rare rollback path.
845    fn rebuild_free_list_after_truncate(&mut self, cutoff: usize) {
846        // Simple approach: scan retained slots for vacant entries.
847        // This avoids issues with interleaved chains (e.g., head -> 6 -> 0)
848        // where truncated indices appear mid-chain.
849        let mut new_head: Option<u32> = None;
850        // Walk backwards so the resulting list is in ascending index order
851        // (head = lowest free index), matching the pattern of fresh arenas.
852        for i in (0..cutoff).rev() {
853            if self.slots[i].is_vacant() {
854                let i_u32 = u32::try_from(i)
855                    .unwrap_or_else(|_| unreachable!("free-list index exceeds u32 invariant"));
856                self.slots[i].state = SlotState::Vacant {
857                    next_free: new_head,
858                };
859                new_head = Some(i_u32);
860            }
861        }
862        self.free_head = new_head;
863    }
864
865    /// Returns a read-only view of all slots (occupied and vacant).
866    ///
867    /// Useful for post-build passes that need to scan every slot without
868    /// the overhead of generation checks.
869    #[must_use]
870    pub fn slots(&self) -> &[Slot<NodeEntry>] {
871        &self.slots
872    }
873}
874
875impl Default for NodeArena {
876    fn default() -> Self {
877        Self::new()
878    }
879}
880
881impl fmt::Display for NodeArena {
882    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
883        write!(
884            f,
885            "NodeArena(len={}, capacity={})",
886            self.len,
887            self.capacity()
888        )
889    }
890}
891
892impl crate::graph::unified::memory::GraphMemorySize for NodeArena {
893    fn heap_bytes(&self) -> usize {
894        self.slots.capacity() * std::mem::size_of::<Slot<NodeEntry>>()
895    }
896}
897
898/// Returns `true` for the canonical synthetic-placeholder name shapes
899/// emitted by language plugins for binding-plane scaffolding.
900///
901/// See [`NodeEntry::is_synthetic_placeholder_name`] for the full
902/// rationale. Patterns recognised:
903///
904/// - **Angle-bracket pseudo-identifiers**: `<...>` — used for
905///   `<field:operand.field>` (Go field-access fallback) and similar
906///   shadows. Real source-language identifiers cannot start with `<`.
907/// - **Per-binding-site offset suffix**: `<ident>@<digits>` — used
908///   for the Go local-scope resolver's per-use Variable nodes. Real
909///   Go identifiers cannot contain `@` (Go's identifier grammar is
910///   `[a-zA-Z_][a-zA-Z0-9_]*`).
911fn is_synthetic_placeholder_name_shape(name: &str) -> bool {
912    if name.starts_with('<') {
913        return true;
914    }
915    // `ident@<digits>` shape: split on '@' and require digits after.
916    if let Some((_, rest)) = name.rsplit_once('@')
917        && !rest.is_empty()
918        && rest.bytes().all(|b| b.is_ascii_digit())
919    {
920        return true;
921    }
922    false
923}
924
925#[cfg(test)]
926mod tests {
927    use super::*;
928
929    fn test_file() -> FileId {
930        FileId::new(1)
931    }
932
933    fn test_name() -> StringId {
934        StringId::new(1)
935    }
936
937    fn test_entry(kind: NodeKind) -> NodeEntry {
938        NodeEntry::new(kind, test_name(), test_file())
939    }
940
941    #[test]
942    fn test_arena_new() {
943        let arena = NodeArena::new();
944        assert_eq!(arena.len(), 0);
945        assert!(arena.is_empty());
946        assert_eq!(arena.capacity(), 0);
947    }
948
949    #[test]
950    fn synthetic_placeholder_name_shape_recognises_known_synthetics() {
951        // Go field-access fallback (process_field_access_unified)
952        assert!(is_synthetic_placeholder_name_shape("<field:s.NeedTags>"));
953        assert!(is_synthetic_placeholder_name_shape(
954            "<field:selector.NeedTags>"
955        ));
956        // Go type-assertion placeholder
957        assert!(is_synthetic_placeholder_name_shape("<type:main.Foo>"));
958        // Go local-scope per-binding-site Variable (graph_builder + local_scopes)
959        assert!(is_synthetic_placeholder_name_shape("NeedTags@469"));
960        assert!(is_synthetic_placeholder_name_shape("NeedTags@508"));
961        assert!(is_synthetic_placeholder_name_shape("foo@0"));
962        // The same check as exposed on NodeEntry
963        assert!(NodeEntry::is_synthetic_placeholder_name("<field:x.y>"));
964        assert!(NodeEntry::is_synthetic_placeholder_name("x@123"));
965    }
966
967    #[test]
968    fn synthetic_placeholder_name_shape_passes_real_identifiers() {
969        // Real Go identifiers (and qualified names) must NOT be flagged.
970        assert!(!is_synthetic_placeholder_name_shape("NeedTags"));
971        assert!(!is_synthetic_placeholder_name_shape(
972            "main.SelectorSource.NeedTags"
973        ));
974        assert!(!is_synthetic_placeholder_name_shape("main.useSelector"));
975        assert!(!is_synthetic_placeholder_name_shape("foo"));
976        assert!(!is_synthetic_placeholder_name_shape(""));
977        // `@` followed by non-digits — does not match the binding-site shape.
978        assert!(!is_synthetic_placeholder_name_shape("foo@bar"));
979        // `@` with empty suffix — degenerate, not synthetic.
980        assert!(!is_synthetic_placeholder_name_shape("foo@"));
981    }
982
983    #[test]
984    fn test_arena_with_capacity() {
985        let arena = NodeArena::with_capacity(100);
986        assert_eq!(arena.len(), 0);
987        assert!(arena.capacity() >= 100);
988    }
989
990    #[test]
991    fn test_alloc_and_get() {
992        let mut arena = NodeArena::new();
993        let entry = test_entry(NodeKind::Function);
994
995        let id = arena.alloc(entry).unwrap();
996        assert!(!id.is_invalid());
997        assert_eq!(arena.len(), 1);
998
999        let node = arena.get(id).unwrap();
1000        assert_eq!(node.kind, NodeKind::Function);
1001    }
1002
1003    #[test]
1004    fn test_alloc_multiple() {
1005        let mut arena = NodeArena::new();
1006
1007        let id1 = arena.alloc(test_entry(NodeKind::Function)).unwrap();
1008        let id2 = arena.alloc(test_entry(NodeKind::Class)).unwrap();
1009        let id3 = arena.alloc(test_entry(NodeKind::Method)).unwrap();
1010
1011        assert_eq!(arena.len(), 3);
1012        assert_ne!(id1, id2);
1013        assert_ne!(id2, id3);
1014
1015        assert_eq!(arena.get(id1).unwrap().kind, NodeKind::Function);
1016        assert_eq!(arena.get(id2).unwrap().kind, NodeKind::Class);
1017        assert_eq!(arena.get(id3).unwrap().kind, NodeKind::Method);
1018    }
1019
1020    #[test]
1021    fn test_get_mut() {
1022        let mut arena = NodeArena::new();
1023        let id = arena.alloc(test_entry(NodeKind::Function)).unwrap();
1024
1025        {
1026            let node = arena.get_mut(id).unwrap();
1027            node.start_line = 42;
1028        }
1029
1030        assert_eq!(arena.get(id).unwrap().start_line, 42);
1031    }
1032
1033    #[test]
1034    fn test_contains() {
1035        let mut arena = NodeArena::new();
1036        let id = arena.alloc(test_entry(NodeKind::Function)).unwrap();
1037
1038        assert!(arena.contains(id));
1039        assert!(!arena.contains(NodeId::INVALID));
1040    }
1041
1042    #[test]
1043    fn test_remove() {
1044        let mut arena = NodeArena::new();
1045        let id = arena.alloc(test_entry(NodeKind::Function)).unwrap();
1046
1047        assert_eq!(arena.len(), 1);
1048        assert!(arena.contains(id));
1049
1050        let removed = arena.remove(id).unwrap();
1051        assert_eq!(removed.kind, NodeKind::Function);
1052        assert_eq!(arena.len(), 0);
1053        assert!(!arena.contains(id));
1054    }
1055
1056    #[test]
1057    fn test_stale_id_after_remove() {
1058        let mut arena = NodeArena::new();
1059        let id1 = arena.alloc(test_entry(NodeKind::Function)).unwrap();
1060
1061        // Remove and reallocate
1062        arena.remove(id1);
1063        let id2 = arena.alloc(test_entry(NodeKind::Class)).unwrap();
1064
1065        // Old ID should be stale
1066        assert!(arena.get(id1).is_none());
1067        // New ID should work
1068        assert_eq!(arena.get(id2).unwrap().kind, NodeKind::Class);
1069        // They should have the same index but different generations
1070        assert_eq!(id1.index(), id2.index());
1071        assert_ne!(id1.generation(), id2.generation());
1072    }
1073
1074    #[test]
1075    fn test_remove_idempotent() {
1076        let mut arena = NodeArena::new();
1077        let id1 = arena.alloc(test_entry(NodeKind::Function)).unwrap();
1078        let id2 = arena.alloc(test_entry(NodeKind::Class)).unwrap();
1079
1080        // First remove should succeed
1081        assert!(arena.remove(id1).is_some());
1082        assert_eq!(arena.len(), 1);
1083
1084        // Second remove of same ID should be idempotent (no side effects)
1085        assert!(arena.remove(id1).is_none());
1086        assert_eq!(arena.len(), 1); // len unchanged
1087
1088        // Third remove should still be safe
1089        assert!(arena.remove(id1).is_none());
1090        assert_eq!(arena.len(), 1); // len still unchanged
1091
1092        // Other entries should be unaffected
1093        assert!(arena.contains(id2));
1094
1095        // New allocation should work correctly
1096        let id3 = arena.alloc(test_entry(NodeKind::Method)).unwrap();
1097        assert_eq!(id3.index(), id1.index()); // Reuses the removed slot
1098        assert_eq!(arena.len(), 2);
1099    }
1100
1101    #[test]
1102    fn test_free_list_reuse() {
1103        let mut arena = NodeArena::new();
1104
1105        // Allocate 3 nodes
1106        let id1 = arena.alloc(test_entry(NodeKind::Function)).unwrap();
1107        let id2 = arena.alloc(test_entry(NodeKind::Class)).unwrap();
1108        let id3 = arena.alloc(test_entry(NodeKind::Method)).unwrap();
1109
1110        // Remove middle node
1111        arena.remove(id2);
1112        assert_eq!(arena.len(), 2);
1113        assert_eq!(arena.slot_count(), 3);
1114
1115        // Allocate new node - should reuse id2's slot
1116        let id4 = arena.alloc(test_entry(NodeKind::Variable)).unwrap();
1117        assert_eq!(id4.index(), id2.index());
1118        assert_eq!(arena.len(), 3);
1119        assert_eq!(arena.slot_count(), 3); // No growth - slot reused
1120
1121        // Old id2 is stale
1122        assert!(arena.get(id2).is_none());
1123        // New id4 works
1124        assert_eq!(arena.get(id4).unwrap().kind, NodeKind::Variable);
1125        // Other IDs still valid
1126        assert!(arena.contains(id1));
1127        assert!(arena.contains(id3));
1128    }
1129
1130    #[test]
1131    fn test_invalid_id() {
1132        let arena = NodeArena::new();
1133        assert!(arena.get(NodeId::INVALID).is_none());
1134    }
1135
1136    #[test]
1137    fn test_out_of_bounds_id() {
1138        let arena = NodeArena::new();
1139        let fake_id = NodeId::new(999, 1);
1140        assert!(arena.get(fake_id).is_none());
1141    }
1142
1143    #[test]
1144    fn test_wrong_generation() {
1145        let mut arena = NodeArena::new();
1146        let id = arena.alloc(test_entry(NodeKind::Function)).unwrap();
1147
1148        // Create ID with wrong generation
1149        let wrong_gen_id = NodeId::new(id.index(), id.generation() + 1);
1150        assert!(arena.get(wrong_gen_id).is_none());
1151    }
1152
1153    #[test]
1154    fn test_iter() {
1155        let mut arena = NodeArena::new();
1156        arena.alloc(test_entry(NodeKind::Function)).unwrap();
1157        arena.alloc(test_entry(NodeKind::Class)).unwrap();
1158        arena.alloc(test_entry(NodeKind::Method)).unwrap();
1159
1160        let entries: Vec<_> = arena.iter().collect();
1161        assert_eq!(entries.len(), 3);
1162
1163        let kinds: Vec<_> = entries.iter().map(|(_, e)| e.kind).collect();
1164        assert!(kinds.contains(&NodeKind::Function));
1165        assert!(kinds.contains(&NodeKind::Class));
1166        assert!(kinds.contains(&NodeKind::Method));
1167    }
1168
1169    #[test]
1170    fn test_iter_with_removed() {
1171        let mut arena = NodeArena::new();
1172        let id1 = arena.alloc(test_entry(NodeKind::Function)).unwrap();
1173        let id2 = arena.alloc(test_entry(NodeKind::Class)).unwrap();
1174        let id3 = arena.alloc(test_entry(NodeKind::Method)).unwrap();
1175
1176        arena.remove(id2);
1177
1178        let entries: Vec<_> = arena.iter().collect();
1179        assert_eq!(entries.len(), 2);
1180
1181        let collected_ids: Vec<_> = entries.iter().map(|(id, _)| *id).collect();
1182        assert!(collected_ids.contains(&id1));
1183        assert!(!collected_ids.contains(&id2));
1184        assert!(collected_ids.contains(&id3));
1185    }
1186
1187    #[test]
1188    fn test_iter_mut() {
1189        let mut arena = NodeArena::new();
1190        arena.alloc(test_entry(NodeKind::Function)).unwrap();
1191        arena.alloc(test_entry(NodeKind::Class)).unwrap();
1192
1193        for (_, entry) in arena.iter_mut() {
1194            entry.start_line = 100;
1195        }
1196
1197        for (_, entry) in arena.iter() {
1198            assert_eq!(entry.start_line, 100);
1199        }
1200    }
1201
1202    #[test]
1203    fn test_clear() {
1204        let mut arena = NodeArena::new();
1205        arena.alloc(test_entry(NodeKind::Function)).unwrap();
1206        arena.alloc(test_entry(NodeKind::Class)).unwrap();
1207
1208        assert_eq!(arena.len(), 2);
1209        arena.clear();
1210        assert_eq!(arena.len(), 0);
1211        assert!(arena.is_empty());
1212    }
1213
1214    #[test]
1215    fn test_reserve() {
1216        let mut arena = NodeArena::new();
1217        arena.reserve(1000);
1218        assert!(arena.capacity() >= 1000);
1219    }
1220
1221    #[test]
1222    fn test_display() {
1223        let mut arena = NodeArena::new();
1224        arena.alloc(test_entry(NodeKind::Function)).unwrap();
1225
1226        let display = format!("{arena}");
1227        assert!(display.contains("NodeArena"));
1228        assert!(display.contains("len=1"));
1229    }
1230
1231    #[test]
1232    fn test_node_entry_builder() {
1233        let entry = NodeEntry::new(NodeKind::Function, test_name(), test_file())
1234            .with_byte_range(10, 100)
1235            .with_location(1, 0, 5, 10)
1236            .with_signature(StringId::new(2))
1237            .with_doc(StringId::new(3))
1238            .with_qualified_name(StringId::new(4));
1239
1240        assert_eq!(entry.kind, NodeKind::Function);
1241        assert_eq!(entry.start_byte, 10);
1242        assert_eq!(entry.end_byte, 100);
1243        assert_eq!(entry.start_line, 1);
1244        assert_eq!(entry.start_column, 0);
1245        assert_eq!(entry.end_line, 5);
1246        assert_eq!(entry.end_column, 10);
1247        assert_eq!(entry.signature, Some(StringId::new(2)));
1248        assert_eq!(entry.doc, Some(StringId::new(3)));
1249        assert_eq!(entry.qualified_name, Some(StringId::new(4)));
1250    }
1251
1252    #[test]
1253    fn test_slot_state() {
1254        let occupied: Slot<i32> = Slot::new_occupied(1, 42);
1255        assert!(occupied.is_occupied());
1256        assert!(!occupied.is_vacant());
1257        assert_eq!(occupied.get(), Some(&42));
1258
1259        let vacant: Slot<i32> = Slot::new_vacant(2, Some(5));
1260        assert!(!vacant.is_occupied());
1261        assert!(vacant.is_vacant());
1262        assert_eq!(vacant.get(), None);
1263    }
1264
1265    #[test]
1266    fn test_slot_generation() {
1267        let slot: Slot<i32> = Slot::new_occupied(42, 100);
1268        assert_eq!(slot.generation(), 42);
1269    }
1270
1271    #[test]
1272    fn test_slot_state_accessor() {
1273        let occupied: Slot<i32> = Slot::new_occupied(1, 42);
1274        assert!(matches!(occupied.state(), SlotState::Occupied(42)));
1275
1276        let vacant: Slot<i32> = Slot::new_vacant(1, Some(3));
1277        assert!(matches!(
1278            vacant.state(),
1279            SlotState::Vacant { next_free: Some(3) }
1280        ));
1281    }
1282
1283    // ---- Bulk API tests ----
1284
1285    #[test]
1286    fn test_alloc_range_basic() {
1287        let mut arena = NodeArena::new();
1288        let placeholder = test_entry(NodeKind::Function);
1289
1290        let start = arena.alloc_range(5, &placeholder).unwrap();
1291        assert_eq!(start, 0);
1292        assert_eq!(arena.len(), 5);
1293        assert_eq!(arena.slot_count(), 5);
1294
1295        // All slots should be occupied with generation 1.
1296        for i in 0..5u32 {
1297            let id = NodeId::new(i, 1);
1298            let entry = arena.get(id).expect("slot should be occupied");
1299            assert_eq!(entry.kind, NodeKind::Function);
1300        }
1301    }
1302
1303    #[test]
1304    fn test_alloc_range_after_existing() {
1305        let mut arena = NodeArena::new();
1306
1307        // Allocate one node via normal path.
1308        let id0 = arena.alloc(test_entry(NodeKind::Class)).unwrap();
1309        assert_eq!(id0.index(), 0);
1310        assert_eq!(arena.len(), 1);
1311
1312        // Bulk allocate 3 more.
1313        let placeholder = test_entry(NodeKind::Method);
1314        let start = arena.alloc_range(3, &placeholder).unwrap();
1315        assert_eq!(start, 1);
1316        assert_eq!(arena.len(), 4);
1317        assert_eq!(arena.slot_count(), 4);
1318
1319        // Original node intact.
1320        assert_eq!(arena.get(id0).unwrap().kind, NodeKind::Class);
1321
1322        // Bulk-allocated nodes accessible.
1323        for i in 1..4u32 {
1324            let id = NodeId::new(i, 1);
1325            assert_eq!(arena.get(id).unwrap().kind, NodeKind::Method);
1326        }
1327    }
1328
1329    #[test]
1330    fn test_alloc_range_zero_is_noop() {
1331        let mut arena = NodeArena::new();
1332
1333        // Allocate one normally first.
1334        arena.alloc(test_entry(NodeKind::Function)).unwrap();
1335        let len_before = arena.len();
1336        let slot_count_before = arena.slot_count();
1337
1338        let start = arena.alloc_range(0, &test_entry(NodeKind::Class)).unwrap();
1339        #[allow(clippy::cast_possible_truncation)]
1340        // Arena slot count fits in u32 for graph storage indices
1341        let expected_start = slot_count_before as u32;
1342        assert_eq!(start, expected_start);
1343        assert_eq!(arena.len(), len_before);
1344        assert_eq!(arena.slot_count(), slot_count_before);
1345    }
1346
1347    #[test]
1348    fn test_bulk_slice_mut() {
1349        let mut arena = NodeArena::new();
1350        let placeholder = test_entry(NodeKind::Function);
1351        let start = arena.alloc_range(3, &placeholder).unwrap();
1352
1353        // Overwrite slot 1 via bulk_slice_mut.
1354        {
1355            let slice = arena.bulk_slice_mut(start, 3);
1356            assert_eq!(slice.len(), 3);
1357
1358            // Replace the middle slot with a Class entry.
1359            slice[1] = Slot::new_occupied(1, test_entry(NodeKind::Class));
1360        }
1361
1362        // Verify the overwrite via get().
1363        let id1 = NodeId::new(1, 1);
1364        assert_eq!(arena.get(id1).unwrap().kind, NodeKind::Class);
1365
1366        // Other slots unchanged.
1367        let id0 = NodeId::new(0, 1);
1368        assert_eq!(arena.get(id0).unwrap().kind, NodeKind::Function);
1369        let id2 = NodeId::new(2, 1);
1370        assert_eq!(arena.get(id2).unwrap().kind, NodeKind::Function);
1371    }
1372
1373    #[test]
1374    fn test_truncate_to() {
1375        let mut arena = NodeArena::new();
1376        let placeholder = test_entry(NodeKind::Function);
1377
1378        // Allocate initial range.
1379        arena.alloc_range(3, &placeholder).unwrap();
1380        assert_eq!(arena.len(), 3);
1381        assert_eq!(arena.slot_count(), 3);
1382
1383        // Save state.
1384        let saved = arena.slot_count();
1385
1386        // Allocate more.
1387        arena.alloc_range(4, &placeholder).unwrap();
1388        assert_eq!(arena.len(), 7);
1389        assert_eq!(arena.slot_count(), 7);
1390
1391        // Truncate back.
1392        arena.truncate_to(saved);
1393        assert_eq!(arena.len(), 3);
1394        assert_eq!(arena.slot_count(), 3);
1395
1396        // Original slots still accessible.
1397        for i in 0..3u32 {
1398            let id = NodeId::new(i, 1);
1399            assert!(arena.get(id).is_some());
1400        }
1401    }
1402
1403    #[test]
1404    fn test_alloc_range_with_free_list() {
1405        let mut arena = NodeArena::new();
1406
1407        // Allocate two nodes, then remove the first to create a free list entry.
1408        let id0 = arena.alloc(test_entry(NodeKind::Function)).unwrap();
1409        let _id1 = arena.alloc(test_entry(NodeKind::Class)).unwrap();
1410        arena.remove(id0);
1411
1412        // State: len=1, slot_count=2, free_head=Some(0)
1413        assert_eq!(arena.len(), 1);
1414        assert_eq!(arena.slot_count(), 2);
1415
1416        // alloc_range should append AFTER slot_count, not reuse free list.
1417        let start = arena.alloc_range(3, &test_entry(NodeKind::Method)).unwrap();
1418        assert_eq!(start, 2); // Appended after existing 2 slots
1419        assert_eq!(arena.len(), 4); // 1 existing + 3 new
1420        assert_eq!(arena.slot_count(), 5); // 2 existing + 3 new
1421
1422        // Free list should still be intact — slot 0 still vacant.
1423        let slot0 = arena.slot(0).unwrap();
1424        assert!(slot0.is_vacant());
1425
1426        // Bulk-allocated nodes accessible.
1427        for i in 2..5u32 {
1428            let id = NodeId::new(i, 1);
1429            assert_eq!(arena.get(id).unwrap().kind, NodeKind::Method);
1430        }
1431    }
1432
1433    #[test]
1434    fn test_truncate_to_clamps_free_head() {
1435        let mut arena = NodeArena::new();
1436
1437        // Allocate 5 nodes.
1438        let mut ids = Vec::new();
1439        for _ in 0..5 {
1440            ids.push(arena.alloc(test_entry(NodeKind::Function)).unwrap());
1441        }
1442
1443        // Save state at 5 slots.
1444        let saved = arena.slot_count();
1445
1446        // Allocate 3 more, then remove one of them to put it on the free list.
1447        let extra_ids: Vec<_> = (0..3)
1448            .map(|_| arena.alloc(test_entry(NodeKind::Class)).unwrap())
1449            .collect();
1450        arena.remove(extra_ids[1]); // free_head now points to index 6
1451
1452        // Truncate back — free_head should be clamped since index 6 is gone.
1453        arena.truncate_to(saved);
1454        assert_eq!(arena.slot_count(), 5);
1455        assert_eq!(arena.len(), 5);
1456
1457        // Arena should still be usable: new alloc appends (no dangling free list).
1458        let new_id = arena.alloc(test_entry(NodeKind::Variable)).unwrap();
1459        assert_eq!(new_id.index(), 5); // Appended, not reusing dangling slot
1460        assert_eq!(arena.get(new_id).unwrap().kind, NodeKind::Variable);
1461    }
1462
1463    #[test]
1464    fn test_truncate_to_preserves_retained_free_list() {
1465        let mut arena = NodeArena::new();
1466
1467        // Allocate 8 nodes: indices 0..7
1468        let mut ids = Vec::new();
1469        for _ in 0..8 {
1470            ids.push(arena.alloc(test_entry(NodeKind::Function)).unwrap());
1471        }
1472        assert_eq!(arena.len(), 8);
1473
1474        // Remove indices 2 and 6 — both on free list.
1475        // Free list: 6 -> 2 -> None (LIFO)
1476        arena.remove(ids[2]);
1477        arena.remove(ids[6]);
1478        assert_eq!(arena.len(), 6);
1479
1480        // Truncate to 5 slots — removes indices 5,6,7.
1481        // Index 6 (vacant) is in truncated region, index 2 (vacant) is retained.
1482        // Free list chain was 6 -> 2. After truncate, only index 2 should survive.
1483        arena.truncate_to(5);
1484
1485        // Should have 4 occupied (indices 0,1,3,4) + 1 vacant (index 2) = 5 slots.
1486        // len should be 4 (we had 6 occupied, truncated 3 slots: 5 occupied + 1 vacant
1487        // in truncated region = indices 5(occ), 6(vac), 7(occ) => dropped 2 occupied).
1488        assert_eq!(arena.slot_count(), 5);
1489        assert_eq!(arena.len(), 4);
1490
1491        // Free list should contain only index 2.
1492        // Verify by allocating — should reuse index 2.
1493        let reused = arena.alloc(test_entry(NodeKind::Variable)).unwrap();
1494        assert_eq!(reused.index(), 2);
1495        assert_eq!(arena.get(reused).unwrap().kind, NodeKind::Variable);
1496        assert_eq!(arena.len(), 5);
1497
1498        // Next alloc should append (no more free slots).
1499        let appended = arena.alloc(test_entry(NodeKind::Class)).unwrap();
1500        assert_eq!(appended.index(), 5);
1501        assert_eq!(arena.len(), 6);
1502    }
1503
1504    #[test]
1505    fn test_slots_read_access() {
1506        let mut arena = NodeArena::new();
1507        let placeholder = test_entry(NodeKind::Variable);
1508        arena.alloc_range(4, &placeholder).unwrap();
1509
1510        let slots = arena.slots();
1511        assert_eq!(slots.len(), 4);
1512
1513        for slot in slots {
1514            assert!(slot.is_occupied());
1515            assert_eq!(slot.generation(), 1);
1516            assert_eq!(slot.get().unwrap().kind, NodeKind::Variable);
1517        }
1518    }
1519
1520    #[test]
1521    fn test_multiple_remove_reuse_cycle() {
1522        let mut arena = NodeArena::new();
1523
1524        // Allocate and remove multiple times to test free list chaining
1525        let mut ids = Vec::new();
1526        for _ in 0..5 {
1527            ids.push(arena.alloc(test_entry(NodeKind::Function)).unwrap());
1528        }
1529
1530        // Remove in reverse order (builds free list: 4 -> 3 -> 2 -> 1 -> 0)
1531        for &id in ids.iter().rev() {
1532            arena.remove(id);
1533        }
1534
1535        // Reallocate - should use free list in LIFO order
1536        let new_ids: Vec<_> = (0..5)
1537            .map(|_| arena.alloc(test_entry(NodeKind::Class)).unwrap())
1538            .collect();
1539
1540        // All old IDs should be stale
1541        for &old_id in &ids {
1542            assert!(arena.get(old_id).is_none());
1543        }
1544
1545        // All new IDs should work
1546        for &new_id in &new_ids {
1547            assert!(arena.get(new_id).is_some());
1548        }
1549    }
1550
1551    #[test]
1552    fn test_generation_overflow_at_max_generation() {
1553        // Test that allocation fails when generation reaches MAX_GENERATION
1554        let mut arena = NodeArena::new();
1555
1556        // Allocate a node
1557        let id = arena.alloc(test_entry(NodeKind::Function)).unwrap();
1558        assert_eq!(id.index(), 0);
1559
1560        // Remove it to put on free list
1561        arena.remove(id);
1562
1563        // Manually set the slot's generation to MAX_GENERATION
1564        // This simulates what would happen after ~9.2 quintillion allocations
1565        arena.slots[0].generation = NodeId::MAX_GENERATION;
1566
1567        // Now allocation should fail because incrementing MAX_GENERATION would overflow
1568        let result = arena.alloc(test_entry(NodeKind::Class));
1569        assert!(result.is_err());
1570
1571        let err = result.unwrap_err();
1572        match err {
1573            ArenaError::GenerationOverflow(inner) => {
1574                assert_eq!(inner.index, 0);
1575                assert_eq!(inner.generation, NodeId::MAX_GENERATION);
1576            }
1577            _ => panic!("expected GenerationOverflow, got {err:?}"),
1578        }
1579    }
1580
1581    #[test]
1582    fn test_free_list_corruption_returns_error() {
1583        // Test that free list corruption returns an error instead of panicking
1584        let mut arena = NodeArena::new();
1585
1586        // Allocate and remove a node to put it on free list
1587        let id = arena.alloc(test_entry(NodeKind::Function)).unwrap();
1588        arena.remove(id);
1589
1590        // Corrupt the free list by manually marking the vacant slot as occupied
1591        arena.slots[0].state = SlotState::Occupied(test_entry(NodeKind::Class));
1592
1593        // Allocation should fail gracefully with FreeListCorruption error
1594        let result = arena.alloc(test_entry(NodeKind::Method));
1595        assert!(result.is_err());
1596
1597        let err = result.unwrap_err();
1598        match err {
1599            ArenaError::FreeListCorruption { index } => {
1600                assert_eq!(index, 0);
1601            }
1602            _ => panic!("expected FreeListCorruption, got {err:?}"),
1603        }
1604
1605        // Verify the arena is still in a consistent state (len was rolled back)
1606        assert_eq!(arena.len(), 0);
1607    }
1608
1609    #[test]
1610    fn test_arena_error_display() {
1611        let gen_err = ArenaError::GenerationOverflow(GenerationOverflowError {
1612            index: 42,
1613            generation: 100,
1614        });
1615        let display = format!("{gen_err}");
1616        assert!(display.contains("42"));
1617        assert!(display.contains("100"));
1618
1619        let corruption_err = ArenaError::FreeListCorruption { index: 5 };
1620        let display = format!("{corruption_err}");
1621        assert!(display.contains("free list corruption"));
1622        assert!(display.contains('5'));
1623
1624        let capacity_err = ArenaError::CapacityExceeded;
1625        let display = format!("{capacity_err}");
1626        assert!(display.contains("capacity exceeded"));
1627    }
1628
1629    #[test]
1630    fn test_arena_error_source_generation_overflow() {
1631        use std::error::Error;
1632
1633        let inner = GenerationOverflowError {
1634            index: 0,
1635            generation: NodeId::MAX_GENERATION,
1636        };
1637        let err = ArenaError::GenerationOverflow(inner);
1638        // source() returns Some for GenerationOverflow
1639        assert!(err.source().is_some());
1640    }
1641
1642    #[test]
1643    fn test_arena_error_source_none_for_other_variants() {
1644        use std::error::Error;
1645
1646        let corruption = ArenaError::FreeListCorruption { index: 0 };
1647        assert!(corruption.source().is_none());
1648
1649        let capacity = ArenaError::CapacityExceeded;
1650        assert!(capacity.source().is_none());
1651    }
1652
1653    #[test]
1654    fn test_arena_from_generation_overflow_error() {
1655        let inner = GenerationOverflowError {
1656            index: 10,
1657            generation: 99,
1658        };
1659        let err: ArenaError = inner.into();
1660        assert!(matches!(err, ArenaError::GenerationOverflow(_)));
1661    }
1662
1663    #[test]
1664    fn test_arena_error_clone_and_eq() {
1665        let err = ArenaError::CapacityExceeded;
1666        let cloned = err.clone();
1667        assert_eq!(err, cloned);
1668
1669        let err2 = ArenaError::FreeListCorruption { index: 3 };
1670        let cloned2 = err2.clone();
1671        assert_eq!(err2, cloned2);
1672    }
1673
1674    #[test]
1675    fn test_node_entry_with_visibility() {
1676        let entry = NodeEntry::new(NodeKind::Function, test_name(), test_file())
1677            .with_visibility(StringId::new(5));
1678
1679        assert_eq!(entry.visibility, Some(StringId::new(5)));
1680    }
1681
1682    #[test]
1683    fn test_node_entry_with_async_and_static() {
1684        let entry = NodeEntry::new(NodeKind::Method, test_name(), test_file())
1685            .with_async(true)
1686            .with_static(true);
1687
1688        assert!(entry.is_async);
1689        assert!(entry.is_static);
1690    }
1691
1692    #[test]
1693    fn test_node_entry_with_unsafe_flag() {
1694        let entry = NodeEntry::new(NodeKind::Function, test_name(), test_file()).with_unsafe(true);
1695
1696        assert!(entry.is_unsafe);
1697    }
1698
1699    #[test]
1700    fn test_node_entry_with_body_hash() {
1701        use crate::graph::body_hash::BodyHash128;
1702        let hash = BodyHash128::from_u128(0u128);
1703        let entry =
1704            NodeEntry::new(NodeKind::Function, test_name(), test_file()).with_body_hash(hash);
1705
1706        assert!(entry.body_hash.is_some());
1707    }
1708
1709    #[test]
1710    fn test_node_entry_defaults() {
1711        let entry = NodeEntry::new(NodeKind::Function, test_name(), test_file());
1712        assert_eq!(entry.start_byte, 0);
1713        assert_eq!(entry.end_byte, 0);
1714        assert_eq!(entry.start_line, 0);
1715        assert_eq!(entry.start_column, 0);
1716        assert_eq!(entry.end_line, 0);
1717        assert_eq!(entry.end_column, 0);
1718        assert!(entry.signature.is_none());
1719        assert!(entry.doc.is_none());
1720        assert!(entry.qualified_name.is_none());
1721        assert!(entry.visibility.is_none());
1722        assert!(!entry.is_async);
1723        assert!(!entry.is_static);
1724        assert!(!entry.is_unsafe);
1725        assert!(entry.body_hash.is_none());
1726    }
1727
1728    #[test]
1729    fn test_arena_default_impl() {
1730        let arena = NodeArena::default();
1731        assert_eq!(arena.len(), 0);
1732        assert!(arena.is_empty());
1733    }
1734
1735    #[test]
1736    fn test_get_invalid_id() {
1737        let mut arena = NodeArena::new();
1738        arena.alloc(test_entry(NodeKind::Function)).unwrap();
1739        // get with INVALID should return None
1740        assert!(arena.get(NodeId::INVALID).is_none());
1741    }
1742
1743    #[test]
1744    fn test_get_mut_invalid_id() {
1745        let mut arena = NodeArena::new();
1746        arena.alloc(test_entry(NodeKind::Function)).unwrap();
1747        assert!(arena.get_mut(NodeId::INVALID).is_none());
1748    }
1749
1750    #[test]
1751    fn test_get_mut_wrong_generation() {
1752        let mut arena = NodeArena::new();
1753        let id = arena.alloc(test_entry(NodeKind::Function)).unwrap();
1754        let wrong_gen_id = NodeId::new(id.index(), id.generation() + 1);
1755        assert!(arena.get_mut(wrong_gen_id).is_none());
1756    }
1757
1758    #[test]
1759    fn test_remove_invalid_id() {
1760        let mut arena = NodeArena::new();
1761        assert!(arena.remove(NodeId::INVALID).is_none());
1762    }
1763
1764    #[test]
1765    fn test_slot_get_mut_occupied() {
1766        let mut slot: Slot<i32> = Slot::new_occupied(1, 42);
1767        let val = slot.get_mut().unwrap();
1768        *val = 99;
1769        assert_eq!(slot.get(), Some(&99));
1770    }
1771
1772    #[test]
1773    fn test_slot_get_mut_vacant() {
1774        let mut slot: Slot<i32> = Slot::new_vacant(1, None);
1775        assert!(slot.get_mut().is_none());
1776    }
1777
1778    #[test]
1779    fn test_truncate_to_zero_occupied_dropped() {
1780        let mut arena = NodeArena::new();
1781        let placeholder = test_entry(NodeKind::Function);
1782        arena.alloc_range(5, &placeholder).unwrap();
1783        // Truncate to 0 — all 5 occupied slots are dropped
1784        arena.truncate_to(0);
1785        assert_eq!(arena.len(), 0);
1786        assert_eq!(arena.slot_count(), 0);
1787    }
1788
1789    #[test]
1790    fn test_slot_count_vs_len() {
1791        let mut arena = NodeArena::new();
1792        let id0 = arena.alloc(test_entry(NodeKind::Function)).unwrap();
1793        arena.alloc(test_entry(NodeKind::Class)).unwrap();
1794        arena.remove(id0);
1795        // slot_count is total slots (including vacant), len is occupied only
1796        assert_eq!(arena.slot_count(), 2);
1797        assert_eq!(arena.len(), 1);
1798    }
1799}