Skip to main content

hermes_ast/
context.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8// The GC arena is the single sanctioned location for encapsulated unsafe in
9// this crate (see spec §1).
10#![allow(unsafe_code)]
11
12//! Garbage-collected Storage structures for AST nodes.
13
14use std::cell::Cell;
15use std::cell::RefCell;
16use std::cell::UnsafeCell;
17use std::ffi::c_void;
18use std::hash::Hash;
19use std::hash::Hasher;
20use std::ops::Deref;
21use std::pin::Pin;
22use std::ptr::NonNull;
23use std::sync::atomic::AtomicU32;
24use std::sync::atomic::Ordering;
25
26use hermes_atom_table::AtomBytes;
27use hermes_atom_table::AtomTable;
28
29use hermes_support::deque::Deque;
30use crate::node::Node;
31use crate::NodeId;
32use crate::node_child::NodeList;
33use crate::visitor::Visitor;
34use hermes_support::HeapSize;
35
36/// ID which indicates a `StorageEntry` is free.
37const FREE_ENTRY: u32 = 0;
38
39/// Recover a pointer to the struct which contains `field`, where `offset` is
40/// the byte offset of that field inside the containing struct (the C
41/// `container_of` idiom).
42///
43/// The arithmetic is deliberately in *bytes*: `field` is a typed pointer, so
44/// the plain `offset`/`sub` methods would step by `size_of::<Field>()` and
45/// land somewhere far outside the object whenever the field is not at offset
46/// zero. Nothing about a `repr(Rust)` struct guarantees a particular field
47/// order, so byte stride is the only correct stride here.
48///
49/// # Safety
50///
51/// `field` must point to the field of a live `Outer` whose byte offset is
52/// `offset` (i.e. `offset` came from `core::mem::offset_of!(Outer, <field>)`).
53#[inline]
54unsafe fn container_of<Outer, Field>(field: *const Field, offset: usize) -> *const Outer {
55    // SAFETY: by the contract above, `field` is `offset` bytes into a live
56    // `Outer`, so stepping back `offset` *bytes* stays inside that same
57    // allocation and yields its base address.
58    unsafe { field.byte_sub(offset).cast::<Outer>() }
59}
60
61/// A single entry in the heap.
62#[derive(Debug)]
63struct StorageEntry<'ctx> {
64    /// ID of the context to which this entry belongs.
65    /// Top bit is used as a mark bit, and flips meaning every time a GC happens.
66    /// If this field is `0`, then this entry is free.
67    ctx_id_markbit: Cell<u32>,
68
69    /// Refcount of how many [`NodeRc`] point to this node.
70    /// Entry may only be freed if this number is `0` and no other entries reference this entry
71    /// directly.
72    count: Cell<u32>,
73
74    /// Actual node stored in this entry.
75    inner: Node<'ctx>,
76}
77
78impl<'ctx> StorageEntry<'ctx> {
79    /// Recover the `StorageEntry` which contains `node`.
80    ///
81    /// # Safety
82    ///
83    /// `node` must be a node allocated in a `Context` (i.e. it must be the
84    /// `inner` field of a live `StorageEntry`), which is true of every node
85    /// reference handed out by the arena.
86    unsafe fn from_node<'a>(node: &'a Node<'a>) -> &'a StorageEntry<'a> {
87        let inner_offset = core::mem::offset_of!(StorageEntry, inner);
88        // SAFETY: by the contract above `node` is the `inner` field of a live
89        // `StorageEntry`, so `container_of` yields that entry, which outlives
90        // `'a` (entries never move once pushed into the deque).
91        unsafe { &*container_of::<StorageEntry<'a>, Node<'a>>(node, inner_offset) }
92    }
93
94    #[inline]
95    fn set_markbit(&self, bit: bool) {
96        let id = self.ctx_id_markbit.get();
97        if bit {
98            self.ctx_id_markbit.set(id | 1 << 31);
99        } else {
100            self.ctx_id_markbit.set(id & !(1 << 31));
101        }
102    }
103
104    #[inline]
105    fn markbit(&self) -> bool {
106        (self.ctx_id_markbit.get() >> 31) != 0
107    }
108
109    fn is_free(&self) -> bool {
110        self.ctx_id_markbit.get() == FREE_ENTRY
111    }
112}
113
114/// A single entry in the NodeList storage.
115/// These are also immutable from the user's perspective, like `Node`s,
116/// but they are temporarily mutated here during construction only, in order to append elements.
117#[derive(Debug)]
118pub(crate) struct NodeListElement<'ctx> {
119    /// ID of the context to which this entry belongs.
120    /// Top bit is used as a mark bit, and flips meaning every time a GC happens.
121    /// If this field is `0`, then this entry is free.
122    ctx_id_markbit: Cell<u32>,
123
124    /// Actual node stored in this entry.
125    /// Must not be null, because empty lists are represented as null pointers in the [`NodeList`].
126    pub inner: *const Node<'ctx>,
127
128    /// Pointer to the next element in the NodeList.
129    /// Stored in a `Cell` to allow for simple appends.
130    pub next: Cell<*const NodeListElement<'ctx>>,
131}
132
133impl<'ctx> NodeListElement<'ctx> {
134    #[inline]
135    fn set_markbit(&self, bit: bool) {
136        let id = self.ctx_id_markbit.get();
137        if bit {
138            self.ctx_id_markbit.set(id | 1 << 31);
139        } else {
140            self.ctx_id_markbit.set(id & !(1 << 31));
141        }
142    }
143
144    #[inline]
145    fn markbit(&self) -> bool {
146        (self.ctx_id_markbit.get() >> 31) != 0
147    }
148
149    fn is_free(&self) -> bool {
150        self.ctx_id_markbit.get() == FREE_ENTRY
151    }
152}
153
154/// Dereference a `NodeListElement`, returning its node and the next pointer.
155/// The single sanctioned list-deref (see node_child::NodeListIter).
156pub(crate) fn list_elem_parts<'gc>(
157    ptr: *const NodeListElement<'gc>,
158) -> (&'gc Node<'gc>, *const NodeListElement<'gc>) {
159    let elem = unsafe { &*ptr };
160    debug_assert!(!elem.inner.is_null(), "NodeList node must not be null");
161    (unsafe { &*elem.inner }, elem.next.get())
162}
163
164/// Structure pointed to by `Context` and `NodeRc` to facilitate panicking if there are
165/// outstanding `NodeRc` when the `Context` is dropped.
166#[derive(Debug)]
167struct NodeRcCounter {
168    /// ID of the context owning the counter.
169    ctx_id: u32,
170
171    /// Number of [`NodeRc`]s allocated in this `Context`.
172    /// Must be `0` when `Context` is dropped.
173    count: Cell<usize>,
174}
175
176/// The storage for AST nodes.
177///
178/// Can be used to allocate and free nodes.
179/// Nodes allocated in one `Context` must not be referenced by another `Context`'s AST.
180#[derive(Debug)]
181pub struct Context<'ast> {
182    /// Unique number used to identify this context.
183    id: u32,
184
185    /// List of all the nodes stored in this context.
186    /// Each element is a "chunk" of nodes.
187    /// None of the chunks are ever resized after allocation.
188    nodes: UnsafeCell<Deque<StorageEntry<'ast>>>,
189
190    /// Free list for AST nodes.
191    free_nodes: UnsafeCell<Vec<NonNull<StorageEntry<'ast>>>>,
192
193    /// Every `NodeListElement` allocated in this context.
194    /// These store the links in the linked lists.
195    list_elements: UnsafeCell<Deque<NodeListElement<'ast>>>,
196
197    /// Free list for `NodeListElement`s.
198    free_list_elements: UnsafeCell<Vec<NonNull<NodeListElement<'ast>>>>,
199
200    /// `NodeRc` count stored in a `Box` to ensure that `NodeRc`s can also point to it
201    /// and decrement the count on drop.
202    /// Placed separately to guard against `Context` moving, though relying on that behavior is
203    /// technically unsafe.
204    noderc_count: Pin<Box<NodeRcCounter>>,
205
206    /// All identifiers are kept here.
207    pub atom_table: AtomTable,
208
209    /// `true` if `1` indicates an entry is marked, `false` if `0` indicates an entry is marked.
210    /// Flipped every time GC occurs.
211    markbit_marked: bool,
212
213    /// Whether strict mode has been forced.
214    strict_mode: bool,
215
216    /// Is 'eval()' is enabled. Port of `Context::enableEval_`
217    /// (Context.h:227-228); getter/setter at Context.h:407-412. Read by
218    /// `SemanticResolver::visit(CallExpressionNode *)` (SemanticResolver.cpp:
219    /// 1134) to decide between the `DirectEval` warning + `registerLocalEval`
220    /// and the `EvalDisabled` warning. Default `true`, matching the C++
221    /// member initializer (hermesc only turns it off for
222    /// `-enable-eval=false`).
223    enable_eval: bool,
224
225    /// Whether to parse Flow type syntax. Mirrors C++ `Context::getParseFlow()`.
226    parse_flow: bool,
227
228    /// Whether to parse the Flow ambiguous-expression grammar (type-args on
229    /// call/new, `as`, typed arrows, type-casts). Mirrors C++
230    /// `Context::getParseFlowAmbiguous()` (= `parseFlow_ == ParseFlowSetting::ALL`).
231    parse_flow_ambiguous: bool,
232
233    /// Whether to parse Flow `component`/`hook` syntax. Mirrors C++
234    /// `Context::getParseFlowComponentSyntax()`.
235    parse_flow_component_syntax: bool,
236
237    /// Whether to parse Flow `record` declarations/expressions. Mirrors C++
238    /// `Context::getParseFlowRecords()`.
239    parse_flow_records: bool,
240
241    /// Whether to parse Flow `match` expressions/statements. Mirrors C++
242    /// `Context::getParseFlowMatch()`.
243    parse_flow_match: bool,
244
245    /// Whether to parse TypeScript type syntax. Mirrors C++
246    /// `Context::getParseTS()`.
247    parse_ts: bool,
248
249    /// Whether to parse JSX syntax. Mirrors C++ `Context::getParseJSX()`.
250    /// Defaults to off; the TS `<Type>expr` assertion grammar is only enabled
251    /// when JSX is *disabled* (C++ JSParserImpl.cpp:4164).
252    parse_jsx: bool,
253
254    /// Whether to warn about undefined variables in strict mode functions.
255    pub warn_undefined: bool,
256
257    /// Even if lazily compiling, eagerly compile any functions under this size
258    /// in bytes. Port of `Context::preemptiveFunctionCompilationThreshold_`
259    /// (Context.h:236); getter/setter at Context.h:516-521. Default `0`
260    /// (= no threshold, consistent with the C++ initializer).
261    preemptive_function_compilation_threshold: u32,
262
263    /// Monotonic counter for `NodeId` assignment. Starts at `1` (`0` is
264    /// `NodeId::UNASSIGNED`); `alloc` stamps the current value onto every
265    /// node it establishes, then advances it. Never reset, never reused.
266    next_node_id: Cell<u32>,
267
268    /// Ids of nodes freed since the last `take_freed_node_ids()`. Appended to
269    /// by both node-freeing paths: `gc()`'s sweep and `AllocationScope::drop`.
270    /// Consumers (sema side tables) drain this to prune dead entries keyed
271    /// by `NodeId` (see doc/superpowers/specs/2026-07-26-sema-untyped-design.md §3.1).
272    freed_node_ids: RefCell<Vec<NodeId>>,
273}
274
275impl Default for Context<'_> {
276    fn default() -> Self {
277        Self::new()
278    }
279}
280
281impl<'ast> Context<'ast> {
282    /// Allocate a new `Context` with a new ID.
283    pub fn new() -> Self {
284        static NEXT_ID: AtomicU32 = AtomicU32::new(FREE_ENTRY + 1);
285        let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
286        Self {
287            id,
288            nodes: Default::default(),
289            free_nodes: Default::default(),
290            list_elements: Default::default(),
291            free_list_elements: Default::default(),
292            noderc_count: Pin::new(Box::new(NodeRcCounter {
293                ctx_id: id,
294                count: Cell::new(0),
295            })),
296            atom_table: Default::default(),
297            markbit_marked: true,
298            strict_mode: false,
299            enable_eval: true,
300            parse_flow: false,
301            parse_flow_ambiguous: false,
302            parse_flow_component_syntax: false,
303            parse_flow_records: false,
304            parse_flow_match: false,
305            parse_ts: false,
306            parse_jsx: false,
307            warn_undefined: false,
308            preemptive_function_compilation_threshold: 0,
309            next_node_id: Cell::new(1),
310            freed_node_ids: RefCell::new(Vec::new()),
311        }
312    }
313
314    /// Acquire a [`GCLock`] on this `Context`.
315    /// This is just a more ergonomic way to call `GCLock::new`.
316    pub fn lock<'ctx>(&'ctx mut self) -> GCLock<'ast, 'ctx> {
317        GCLock::new(self)
318    }
319
320    /// Allocate a new `Node` in this `Context`.
321    pub(crate) fn alloc<'s>(&'s self, n: Node<'_>) -> &'s Node<'s> {
322        let free = unsafe { &mut *self.free_nodes.get() };
323        let nodes: &mut Deque<StorageEntry<'ast>> = unsafe { &mut *self.nodes.get() };
324        let node = unsafe { std::mem::transmute::<Node<'_>, Node<'_>>(n) };
325        let entry: &StorageEntry<'ast> = if let Some(mut entry) = free.pop() {
326            let entry: &mut StorageEntry<'ast> = unsafe { entry.as_mut() };
327            debug_assert!(
328                entry.ctx_id_markbit.get() == FREE_ENTRY,
329                "Incorrect context ID"
330            );
331            debug_assert!(entry.count.get() == 0, "Freed entry has pointers to it");
332            entry.ctx_id_markbit.set(self.id);
333            entry.set_markbit(!self.markbit_marked);
334            entry.inner = node;
335            entry
336        } else {
337            let entry: &StorageEntry = nodes.push(StorageEntry {
338                ctx_id_markbit: Cell::new(self.id),
339                count: Cell::new(0),
340                inner: node,
341            });
342            entry.set_markbit(!self.markbit_marked);
343            entry
344        };
345        // Stamp a fresh, never-reused id unconditionally — both the
346        // free-list-reuse and fresh-push arms land here.
347        let id = self.next_node_id.get();
348        self.next_node_id.set(id.checked_add(1).expect("NodeId overflow"));
349        entry.inner.metadata().id.set(NodeId(id));
350        // Transmute here to handle the fact that Cell<> is invariant over its type,
351        // meaning the lifetime doesn't automatically narrow from `'ast` to `'s`.
352        unsafe { std::mem::transmute(&entry.inner) }
353    }
354
355    /// Allocate a list element in the context with the provided previous element if it exists.
356    /// `prev` will be updated to point to `node` as its next element.
357    pub(crate) fn append_list_element<'a>(
358        &'a self,
359        prev: Option<&'a NodeListElement<'a>>,
360        node: &'a Node<'a>,
361    ) -> &'a NodeListElement<'a> {
362        let elements: &mut Deque<NodeListElement<'ast>> = unsafe { &mut *self.list_elements.get() };
363        let free = unsafe { &mut *self.free_list_elements.get() };
364        // Transmutation is safe here, because `Node`s can only be allocated through
365        // this path and only one GCLock can be made available at a time per thread.
366        let node: &'ast Node<'ast> = unsafe { std::mem::transmute(node) };
367        let prev: Option<&'ast NodeListElement<'ast>> = unsafe { std::mem::transmute(prev) };
368        let entry = if let Some(mut entry) = free.pop() {
369            let entry: &mut NodeListElement<'ast> = unsafe { entry.as_mut() };
370            debug_assert!(
371                entry.ctx_id_markbit.get() == FREE_ENTRY,
372                "Incorrect context ID"
373            );
374            entry.ctx_id_markbit.set(self.id);
375            entry.set_markbit(!self.markbit_marked);
376            entry.inner = node;
377            entry.next.set(std::ptr::null());
378            if let Some(prev) = prev {
379                prev.next.set(entry as *const _);
380            }
381            entry
382        } else {
383            let entry = elements.push(NodeListElement {
384                ctx_id_markbit: Cell::new(self.id),
385                inner: node,
386                next: Cell::new(std::ptr::null()),
387            });
388            entry.set_markbit(!self.markbit_marked);
389            if let Some(prev) = prev {
390                prev.next.set(entry as *const _);
391            }
392            entry
393        };
394        debug_assert!(!entry.is_free(), "Entry must not be free");
395        // Transmute here to handle the fact that Cell<> is invariant over its type,
396        // meaning the lifetime doesn't automatically narrow from `'ast` to `'s`.
397        unsafe { std::mem::transmute(entry) }
398    }
399
400    /// Return the atom table.
401    pub fn atom_table(&self) -> &AtomTable {
402        &self.atom_table
403    }
404
405    /// Add a byte-string to the identifier table.
406    #[inline]
407    pub fn atom_bytes<V: Into<Vec<u8>> + AsRef<[u8]>>(&self, value: V) -> AtomBytes {
408        self.atom_table.atom_bytes(value)
409    }
410
411    /// Obtain the contents of an atom from the atom table.
412    #[inline]
413    pub fn bytes(&self, ident: AtomBytes) -> &[u8] {
414        self.atom_table.bytes(ident)
415    }
416
417    /// Return true if strict mode has been forced globally.
418    pub fn strict_mode(&self) -> bool {
419        self.strict_mode
420    }
421
422    /// Enable strict mode. Note that it cannot be unset.
423    pub fn enable_strict_mode(&mut self) {
424        self.strict_mode = true;
425    }
426
427    /// Return true if `eval()` is enabled. Mirrors C++
428    /// `Context::getEnableEval()` (Context.h:407-409).
429    pub fn enable_eval(&self) -> bool {
430        self.enable_eval
431    }
432
433    /// Enable or disable `eval()`. Mirrors C++
434    /// `Context::setEnableEval()` (Context.h:410-412).
435    pub fn set_enable_eval(&mut self, v: bool) {
436        self.enable_eval = v;
437    }
438
439    /// Return true if Flow type parsing is enabled.
440    /// Mirrors C++ `Context::getParseFlow()`.
441    pub fn parse_flow(&self) -> bool {
442        self.parse_flow
443    }
444
445    /// Enable or disable Flow type parsing.
446    /// Mirrors C++ `Context::setParseFlow()`.
447    pub fn set_parse_flow(&mut self, v: bool) {
448        self.parse_flow = v;
449    }
450
451    /// Return true if the Flow ambiguous-expression grammar is enabled.
452    /// Mirrors C++ `Context::getParseFlowAmbiguous()`.
453    pub fn parse_flow_ambiguous(&self) -> bool {
454        self.parse_flow_ambiguous
455    }
456
457    /// Enable or disable the Flow ambiguous-expression grammar.
458    pub fn set_parse_flow_ambiguous(&mut self, v: bool) {
459        self.parse_flow_ambiguous = v;
460    }
461
462    /// Return true if Flow `component`/`hook` syntax is enabled.
463    /// Mirrors C++ `Context::getParseFlowComponentSyntax()`.
464    pub fn parse_flow_component_syntax(&self) -> bool {
465        self.parse_flow_component_syntax
466    }
467
468    /// Enable or disable Flow `component`/`hook` syntax.
469    pub fn set_parse_flow_component_syntax(&mut self, v: bool) {
470        self.parse_flow_component_syntax = v;
471    }
472
473    /// Return true if Flow `record` declarations/expressions are enabled.
474    /// Mirrors C++ `Context::getParseFlowRecords()`.
475    pub fn parse_flow_records(&self) -> bool {
476        self.parse_flow_records
477    }
478
479    /// Enable or disable Flow `record` declarations/expressions.
480    pub fn set_parse_flow_records(&mut self, v: bool) {
481        self.parse_flow_records = v;
482    }
483
484    /// Return true if Flow `match` expressions/statements are enabled.
485    /// Mirrors C++ `Context::getParseFlowMatch()`.
486    pub fn parse_flow_match(&self) -> bool {
487        self.parse_flow_match
488    }
489
490    /// Enable or disable Flow `match` expressions/statements.
491    pub fn set_parse_flow_match(&mut self, v: bool) {
492        self.parse_flow_match = v;
493    }
494
495    /// Return true if TypeScript type parsing is enabled.
496    /// Mirrors C++ `Context::getParseTS()`.
497    pub fn parse_ts(&self) -> bool {
498        self.parse_ts
499    }
500
501    /// Enable or disable TypeScript type parsing.
502    /// Mirrors C++ `Context::setParseTS()`.
503    pub fn set_parse_ts(&mut self, v: bool) {
504        self.parse_ts = v;
505    }
506
507    /// Return true if JSX parsing is enabled. Mirrors C++
508    /// `Context::getParseJSX()`.
509    pub fn parse_jsx(&self) -> bool {
510        self.parse_jsx
511    }
512
513    /// Enable or disable JSX parsing. Mirrors C++ `Context::setParseJSX()`.
514    /// Currently only read by the TS `<Type>` cast gate; the setter is wired
515    /// when the JSX phase lands.
516    pub fn set_parse_jsx(&mut self, v: bool) {
517        self.parse_jsx = v;
518    }
519
520    /// Return the preemptive-function-compilation threshold (bytes). Port of
521    /// `Context::getPreemptiveFunctionCompilationThreshold()` (Context.h:516-518).
522    pub fn preemptive_function_compilation_threshold(&self) -> u32 {
523        self.preemptive_function_compilation_threshold
524    }
525
526    /// Set the preemptive-function-compilation threshold (bytes). Port of
527    /// `Context::setPreemptiveFunctionCompilationThreshold()` (Context.h:520-522).
528    pub fn set_preemptive_function_compilation_threshold(&mut self, byte_count: u32) {
529        self.preemptive_function_compilation_threshold = byte_count;
530    }
531
532    /// Mark and sweep the arena: everything reachable from a live [`NodeRc`]
533    /// survives, the rest is returned to the free lists. Requires `&mut self`,
534    /// so no [`GCLock`] — and therefore no `&Node` — can be outstanding.
535    pub fn gc(&mut self) {
536        let nodes = unsafe { &mut *self.nodes.get() };
537        let free_nodes = unsafe { &mut *self.free_nodes.get() };
538
539        let list_elements = unsafe { &mut *self.list_elements.get() };
540        let free_list_elements = unsafe { &mut *self.free_list_elements.get() };
541
542        {
543            // Begin by collecting all the roots: entries with non-zero refcount.
544            let mut roots: Vec<&StorageEntry> = vec![];
545            for entry in nodes.iter() {
546                if entry.is_free() {
547                    continue;
548                }
549                debug_assert!(
550                    entry.markbit() != self.markbit_marked,
551                    "Entry marked before start of GC: \
552                        {:?}\nentry.markbit()={}\nmarkbit_marked={}",
553                    &entry,
554                    entry.markbit(),
555                    self.markbit_marked,
556                );
557                if entry.count.get() > 0 {
558                    // Transmuting the lifetime here because we have to store the roots from
559                    // across accesses to `nodes`, meaning we must translate
560                    // from `'ast` to the lifetime of this scope.
561                    roots.push(unsafe {
562                        std::mem::transmute::<&StorageEntry<'_>, &StorageEntry<'_>>(entry)
563                    });
564                }
565            }
566
567            struct Marker {
568                markbit_marked: bool,
569            }
570
571            impl<'gc> Visitor<'gc> for Marker {
572                fn visit_node(&mut self, node: &'gc Node<'gc>) {
573                    let entry = unsafe { StorageEntry::from_node(node) };
574                    if entry.markbit() == self.markbit_marked {
575                        // Stop visiting early if we've already marked this part,
576                        // because we must have also marked all the children.
577                        return;
578                    }
579                    entry.set_markbit(self.markbit_marked);
580                    let mark = self.markbit_marked;
581                    node.mark_lists(&mut |list: &NodeList<'gc>| {
582                        // Mark each list element's storage bit.
583                        let mut p = list.head;
584                        while !p.is_null() {
585                            let elem = unsafe { &*p };
586                            elem.set_markbit(mark);
587                            p = elem.next.get();
588                        }
589                    });
590                    node.visit_children(self);
591                }
592            }
593
594            // Use a visitor to mark every node reachable from roots.
595            // Marking happens while holding `&mut self`, so no GCLock
596            // re-entrancy is needed.
597            let mut marker = Marker {
598                markbit_marked: self.markbit_marked,
599            };
600            for root in roots {
601                marker.visit_node(&root.inner);
602            }
603        }
604
605        // Borrow once: every node this sweep frees appends its id here so
606        // sema side tables (keyed by NodeId) can prune the dead entries.
607        let mut freed_node_ids = self.freed_node_ids.borrow_mut();
608        for entry in nodes.iter_mut() {
609            if entry.is_free() {
610                // Skip free entries.
611                continue;
612            }
613            if entry.count.get() > 0 {
614                // Keep referenced entries alive.
615                continue;
616            }
617            if entry.markbit() == self.markbit_marked {
618                // Keep marked entries alive.
619                continue;
620            }
621            // Passed all checks, this entry is free.
622            freed_node_ids.push(entry.inner.metadata().id.get());
623            entry.ctx_id_markbit.set(FREE_ENTRY);
624            free_nodes.push(unsafe { NonNull::new_unchecked(entry as *mut StorageEntry) });
625        }
626
627        for element in list_elements.iter_mut() {
628            if element.is_free() {
629                // Skip free entries.
630                continue;
631            }
632            if element.markbit() == self.markbit_marked {
633                // Keep marked entries alive.
634                continue;
635            }
636            // Passed all checks, this element is free.
637            element.ctx_id_markbit.set(FREE_ENTRY);
638            free_list_elements
639                .push(unsafe { NonNull::new_unchecked(element as *mut NodeListElement) });
640        }
641
642        self.markbit_marked = !self.markbit_marked;
643    }
644
645    /// Drain and return the ids of every node freed (by `gc()` or by an
646    /// `AllocationScope` truncation) since the last call. Consumers use this
647    /// to prune dead entries out of side tables keyed by `NodeId`.
648    pub fn take_freed_node_ids(&mut self) -> Vec<NodeId> {
649        std::mem::take(&mut *self.freed_node_ids.borrow_mut())
650    }
651
652    /// Returns the number of node slots which have been allocated.
653    /// Includes nodes currently in use as well as nodes in the free list.
654    pub fn num_nodes(&self) -> usize {
655        let nodes = unsafe { &*self.nodes.get() };
656        nodes.len()
657    }
658
659    /// Returns the number of list-element slots which have been allocated.
660    /// Includes elements currently in use as well as elements in the free
661    /// list.
662    pub fn num_list_elements(&self) -> usize {
663        let list_elements = unsafe { &*self.list_elements.get() };
664        list_elements.len()
665    }
666
667    /// Returns the number of node slots currently in the free list (i.e.
668    /// allocated but unused, reclaimed by GC).
669    pub fn num_free_nodes(&self) -> usize {
670        let free_nodes = unsafe { &*self.free_nodes.get() };
671        free_nodes.len()
672    }
673
674    /// Returns the approximate size of just the AST storages in bytes.
675    /// Includes the allocated nodes, lists, as well as free lists for both.
676    pub fn storage_size(&self) -> usize {
677        let nodes = unsafe { &*self.nodes.get() };
678        let free_nodes = unsafe { &*self.free_nodes.get() };
679        let list_elements = unsafe { &*self.list_elements.get() };
680        let free_list_elements = unsafe { &*self.free_list_elements.get() };
681        let mut result = 0;
682        result += nodes.heap_size();
683        result += free_nodes.heap_size();
684        result += list_elements.heap_size();
685        result += free_list_elements.heap_size();
686        result
687    }
688
689    /// Leak everything an outstanding [`NodeRc`] still touches after this
690    /// `Context` is gone, and return the leaked node storage.
691    ///
692    /// Called only from the `Drop` guard's failure path. A `NodeRc` that
693    /// outlives its `Context` is a caller bug which the guard reports by
694    /// panicking, but the report is worthless if the handle's own `drop` —
695    /// which runs during the unwind, or after a `catch_unwind` — writes into
696    /// freed memory. A handle reaches exactly two places: the `count` cell in
697    /// its `StorageEntry` (inside the node deque) and the `NodeRcCounter`
698    /// box (also read by `NodeRc::node` for its context-id check). Both are
699    /// leaked here, so those accesses stay valid for the life of the process.
700    /// Nothing else in the arena is reachable from a `NodeRc` once the
701    /// `Context` is gone, so the rest is freed normally.
702    fn leak_noderc_targets<'s>(&'s mut self) -> &'s Deque<StorageEntry<'ast>> {
703        // SAFETY: `drop` holds `&mut self`, so no `GCLock` and no other
704        // borrow of the deque exists; the field is left holding an empty
705        // deque for the drop glue to dispose of.
706        let nodes = std::mem::take(unsafe { &mut *self.nodes.get() });
707        // The `StorageEntry`s live in the deque's chunks, which this moves
708        // (as a `Vec<Vec<_>>` header) but does not reallocate, so entry
709        // addresses — the ones the outstanding handles hold — are unchanged.
710        let leaked_nodes: &'s Deque<StorageEntry<'ast>> = Box::leak(Box::new(nodes));
711
712        // Replace the counter with a fresh box and forget the old one, so the
713        // address every outstanding handle holds stays allocated. `ctx_id` is
714        // preserved in the leaked copy, which keeps `NodeRc::node`'s
715        // "allocated in context N" assertion honest afterwards.
716        let fresh = Pin::new(Box::new(NodeRcCounter {
717            ctx_id: self.id,
718            count: Cell::new(0),
719        }));
720        std::mem::forget(std::mem::replace(&mut self.noderc_count, fresh));
721
722        leaked_nodes
723    }
724}
725
726impl HeapSize for Context<'_> {
727    /// Returns the heap size of the AST storages only.
728    /// Atom-table memory is intentionally excluded: the `AtomTable` is
729    /// externally owned and accounted for separately.
730    fn heap_size(&self) -> usize {
731        let nodes = unsafe { &*self.nodes.get() };
732        let free_nodes = unsafe { &*self.free_nodes.get() };
733        let list_elements = unsafe { &*self.list_elements.get() };
734        let free_list_elements = unsafe { &*self.free_list_elements.get() };
735        let mut result = 0;
736        result += nodes.heap_size();
737        result += free_nodes.heap_size();
738        result += list_elements.heap_size();
739        result += free_list_elements.heap_size();
740        result += std::mem::size_of::<NodeRcCounter>();
741        result
742    }
743}
744
745impl Drop for Context<'_> {
746    /// Ensure that there are no outstanding `NodeRc`s into this `Context` which will be
747    /// invalidated once it is dropped.
748    ///
749    /// # Panics
750    ///
751    /// Will panic if there are any `NodeRc`s stored when this `Context` is dropped.
752    ///
753    /// The panic is the *only* effect: before panicking, the node storage and
754    /// the `NodeRc` counter are leaked (`Context::leak_noderc_targets`), so
755    /// the outstanding handles — which are dropped during the ensuing
756    /// unwind, or later still if the panic is caught — decrement refcounts in
757    /// memory that is still valid. Leaking the arena is the price of keeping a
758    /// caller's bug a panic instead of a use-after-free.
759    fn drop(&mut self) {
760        if self.noderc_count.count.get() > 0 {
761            // Do this first: everything below can panic, and after the leak
762            // no unwind path can free what the outstanding `NodeRc`s touch.
763            let leaked_nodes = self.leak_noderc_targets();
764            #[cfg(debug_assertions)]
765            {
766                // In debug mode, provide more information on which node was leaked.
767                for entry in leaked_nodes.iter() {
768                    assert!(
769                        entry.count.get() == 0,
770                        "NodeRc must not outlive Context: {:#?}\n",
771                        &entry.inner
772                    );
773                }
774            }
775            #[cfg(not(debug_assertions))]
776            let _ = leaked_nodes;
777            // In release mode, just panic immediately.
778            panic!("NodeRc must not outlive Context");
779        }
780    }
781}
782
783thread_local! {
784    /// Whether there exists a `GCLock` on the current thread.
785    static GCLOCK_IN_USE: Cell<bool> = const { Cell::new(false) };
786}
787
788/// A way to view the [`Context`].
789///
790/// Provides the user the ability to create new nodes and dereference [`NodeRc`].
791///
792/// **At most one is allowed to be active in any thread at any time.**
793/// This is to ensure no `&Node` can be shared between `Context`s.
794pub struct GCLock<'ast, 'ctx> {
795    ctx: &'ctx mut Context<'ast>,
796}
797
798impl Drop for GCLock<'_, '_> {
799    fn drop(&mut self) {
800        GCLOCK_IN_USE.with(|flag| {
801            flag.set(false);
802        });
803    }
804}
805
806impl<'ast, 'ctx> GCLock<'ast, 'ctx> {
807    /// # Panics
808    ///
809    /// Will panic if there is already an active `GCLock` on this thread.
810    pub fn new(ctx: &'ctx mut Context<'ast>) -> Self {
811        GCLOCK_IN_USE.with(|flag| {
812            if flag.get() {
813                panic!("Attempt to create multiple GCLocks in a single thread");
814            }
815            flag.set(true);
816        });
817        GCLock { ctx }
818    }
819
820    /// Allocate a node in the `ctx`.
821    #[inline]
822    pub fn alloc<'s>(&'s self, n: Node<'s>) -> &'s Node<'s> {
823        self.ctx.alloc(n)
824    }
825
826    /// Append `node` to the `prev` element if provided, else create the element as the first
827    /// element in the `NodeList`.
828    #[inline]
829    pub(crate) fn append_list_element<'s>(
830        &'s self,
831        prev: Option<&'s NodeListElement<'s>>,
832        n: &'s Node<'s>,
833    ) -> &'s NodeListElement<'s> {
834        self.ctx.append_list_element(prev, n)
835    }
836
837    /// Return a reference to the owning Context.
838    pub fn ctx(&self) -> &Context<'ast> {
839        self.ctx
840    }
841
842    /// Add a byte-string to the identifier table.
843    #[inline]
844    pub fn atom_bytes<V: Into<Vec<u8>> + AsRef<[u8]>>(&self, value: V) -> AtomBytes {
845        self.ctx.atom_bytes(value)
846    }
847
848    /// Obtain the contents of an atom from the atom table.
849    #[inline]
850    pub fn bytes(&self, ident: AtomBytes) -> &[u8] {
851        self.ctx.bytes(ident)
852    }
853}
854
855/// RAII allocation scope over the arena: everything allocated (nodes AND
856/// list elements) between construction and drop is reclaimed at drop, with
857/// bump-allocator save/restore semantics. Port of the C++ `AllocationScope`
858/// (hermes/Support/Allocator.h:500-521) as used by the parser's PreParse
859/// pass (JSParserImpl.cpp:548, 7523).
860///
861/// See [`GCLock::alloc_scope`] for the safety contract.
862pub struct AllocationScope<'gcl, 'ast, 'ctx> {
863    lock: &'gcl GCLock<'ast, 'ctx>,
864    nodes_watermark: usize,
865    list_elements_watermark: usize,
866}
867
868impl Drop for AllocationScope<'_, '_, '_> {
869    fn drop(&mut self) {
870        let ctx: &Context<'_> = self.lock.ctx;
871        let nodes = unsafe { &mut *ctx.nodes.get() };
872        #[cfg(debug_assertions)]
873        for entry in nodes.iter_from(self.nodes_watermark) {
874            // A NodeRc into the suffix would dangle after truncation.
875            debug_assert!(
876                entry.count.get() == 0,
877                "NodeRc points into a truncated AllocationScope suffix"
878            );
879            // gc() cannot run under a GCLock, so no suffix entry can be
880            // free (free-list pops reuse only pre-watermark slots).
881            debug_assert!(!entry.is_free(), "free entry in scope suffix");
882        }
883        // Log every reclaimed node's id (the debug asserts above already
884        // guarantee no suffix entry is free) so sema side tables can prune
885        // dead entries, same as the gc() sweep does.
886        let mut freed_node_ids = ctx.freed_node_ids.borrow_mut();
887        for entry in nodes.iter_from(self.nodes_watermark) {
888            freed_node_ids.push(entry.inner.metadata().id.get());
889        }
890        nodes.truncate(self.nodes_watermark);
891        let list_elements = unsafe { &mut *ctx.list_elements.get() };
892        list_elements.truncate(self.list_elements_watermark);
893    }
894}
895
896impl<'ast, 'ctx> GCLock<'ast, 'ctx> {
897    /// Open an allocation scope: everything allocated between this call and
898    /// the returned guard's drop is reclaimed at drop (nodes and list
899    /// elements). Mirrors the C++ `AllocationScope` discipline the PreParse
900    /// pass uses (JSParserImpl.cpp:516-560).
901    ///
902    /// # Safety
903    ///
904    /// The caller must guarantee that when the guard drops:
905    /// - no `&Node`, `NodeList`, `&NodeListElement`, or interior reference into an allocation
906    ///   made after this call survives — the storage is freed and any such
907    ///   reference dangles; and
908    /// - no `NodeRc` points into those allocations (debug-asserted).
909    ///
910    /// If the `Context` ran `gc()` before this pass, in-scope allocations
911    /// may be served from the free list at pre-watermark positions; those
912    /// escape reclamation harmlessly (unreferenced until the next `gc()`).
913    pub unsafe fn alloc_scope<'s>(&'s self) -> AllocationScope<'s, 'ast, 'ctx> {
914        let nodes = unsafe { &*self.ctx.nodes.get() };
915        let list_elements = unsafe { &*self.ctx.list_elements.get() };
916        AllocationScope {
917            lock: self,
918            nodes_watermark: nodes.len(),
919            list_elements_watermark: list_elements.len(),
920        }
921    }
922}
923
924/// A wrapper around Node&, with "shallow" hashing and equality, suitable for
925/// hash tables.
926#[derive(Debug, Copy, Clone)]
927pub struct NodePtr<'gc>(pub &'gc Node<'gc>);
928
929impl<'gc> NodePtr<'gc> {
930    /// Wrap a node reference so it can be used as a hash-table key.
931    pub fn from_node(node: &'gc Node<'gc>) -> Self {
932        Self(node)
933    }
934}
935
936impl<'gc> PartialEq for NodePtr<'gc> {
937    fn eq(&self, other: &Self) -> bool {
938        std::ptr::eq(self.0, other.0)
939    }
940}
941
942impl Eq for NodePtr<'_> {}
943
944impl Hash for NodePtr<'_> {
945    fn hash<H: Hasher>(&self, state: &mut H) {
946        (self.0 as *const Node).hash(state)
947    }
948}
949
950impl<'gc> Deref for NodePtr<'gc> {
951    type Target = Node<'gc>;
952    fn deref(&self) -> &'gc Self::Target {
953        self.0
954    }
955}
956
957impl<'gc> AsRef<Node<'gc>> for NodePtr<'gc> {
958    fn as_ref(&self) -> &'gc Node<'gc> {
959        self.0
960    }
961}
962
963impl<'gc> From<&'gc Node<'gc>> for NodePtr<'gc> {
964    fn from(node: &'gc Node<'gc>) -> Self {
965        NodePtr(node)
966    }
967}
968
969/// Reference counted pointer to a [`Node`] in any [`Context`].
970///
971/// It can be used to keep references to `Node`s outside of the lifetime of a [`GCLock`],
972/// but the only way to derefence and inspect the `Node` is to use a `GCLock`.
973///
974/// A `NodeRc` must not outlive its `Context`: dropping a `Context` while one
975/// is alive panics (see [`Context`]'s `Drop`). Should that happen anyway, the
976/// handle itself stays safe to drop and to clone — the guard leaks the storage
977/// it points at rather than freeing it — but it can no longer be dereferenced,
978/// since [`NodeRc::node`] needs a `GCLock` on the context it came from.
979#[derive(Debug, Eq)]
980pub struct NodeRc {
981    /// The `NodeRcCounter` counting for the `Context` to which this belongs.
982    counter: NonNull<NodeRcCounter>,
983
984    /// Pointer to the `StorageEntry` containing the `Node`.
985    /// Stored as `c_void` to avoid specifying lifetimes, as dereferencing is checked manually.
986    entry: NonNull<c_void>,
987}
988
989impl Hash for NodeRc {
990    fn hash<H: Hasher>(&self, state: &mut H) {
991        self.entry.hash(state)
992    }
993}
994
995impl PartialEq for NodeRc {
996    fn eq(&self, other: &Self) -> bool {
997        self.entry == other.entry
998    }
999}
1000
1001impl Drop for NodeRc {
1002    fn drop(&mut self) {
1003        let entry = unsafe { self.entry().as_mut() };
1004        let c = entry.count.get();
1005        debug_assert!(c > 0);
1006        entry.count.set(c - 1);
1007
1008        let noderc_count = unsafe { self.counter.as_mut() };
1009        let c = noderc_count.count.get();
1010        debug_assert!(c > 0);
1011        noderc_count.count.set(c - 1);
1012    }
1013}
1014
1015impl Clone for NodeRc {
1016    /// Cloning a `NodeRc` increments refcounts on the entry and the context.
1017    fn clone(&self) -> Self {
1018        let mut cloned = NodeRc { ..*self };
1019
1020        let entry = unsafe { cloned.entry().as_mut() };
1021        let c = entry.count.get();
1022        entry.count.set(c + 1);
1023
1024        let noderc_count = unsafe { cloned.counter.as_mut() };
1025        let c = noderc_count.count.get();
1026        noderc_count.count.set(c + 1);
1027
1028        cloned
1029    }
1030}
1031
1032impl NodeRc {
1033    /// Turn a node reference into a `NodeRc` for storage outside `GCLock`.
1034    pub fn from_node<'gc>(gc: &'gc GCLock, node: &'gc Node<'gc>) -> NodeRc {
1035        // SAFETY: `node` was handed out by the arena, so it is the `inner`
1036        // field of a live `StorageEntry` — the contract of
1037        // `StorageEntry::from_node`.
1038        unsafe { Self::from_entry(gc, StorageEntry::from_node(node)) }
1039    }
1040
1041    /// Return the actual `Node` that `self` points to.
1042    ///
1043    /// # Panics
1044    ///
1045    /// Will panic if `gc` is not for the same context as this `NodeRc` was created in.
1046    pub fn node<'gc>(&'_ self, gc: &'gc GCLock<'_, '_>) -> &'gc Node<'_> {
1047        unsafe {
1048            assert_eq!(
1049                self.counter.as_ref().ctx_id,
1050                gc.ctx.id,
1051                "Attempt to derefence NodeRc allocated context {} in context {}",
1052                self.counter.as_ref().ctx_id,
1053                gc.ctx.id
1054            );
1055            &self.entry().as_ref().inner
1056        }
1057    }
1058
1059    /// Get the pointer to the `StorageEntry`.
1060    unsafe fn entry(&self) -> NonNull<StorageEntry<'_>> {
1061        let outer = self.entry.as_ptr() as *mut StorageEntry;
1062        NonNull::new_unchecked(outer)
1063    }
1064
1065    unsafe fn from_entry(gc: &GCLock, entry: &StorageEntry<'_>) -> NodeRc {
1066        let c = entry.count.get();
1067        entry.count.set(c + 1);
1068
1069        let c = gc.ctx.noderc_count.count.get();
1070        gc.ctx.noderc_count.count.set(c + 1);
1071
1072        NodeRc {
1073            counter: NonNull::new_unchecked(gc.ctx.noderc_count.as_ref().get_ref()
1074                as *const NodeRcCounter
1075                as *mut NodeRcCounter),
1076            entry: NonNull::new_unchecked(entry as *const StorageEntry as *mut c_void),
1077        }
1078    }
1079}
1080
1081#[cfg(test)]
1082mod tests {
1083    use super::*;
1084    use crate::node::*;
1085    use crate::node_child::NodeMetadata;
1086    use std::cell::Cell;
1087    use std::cell::RefCell;
1088    use std::panic::AssertUnwindSafe;
1089
1090    fn dummy_range() -> hermes_support::location::SMRange {
1091        let l = hermes_support::location::SMLoc {
1092            source: hermes_support::location::SourceId::from_index(0),
1093            offset: 0,
1094        };
1095        hermes_support::location::SMRange { start: l, end: l }
1096    }
1097
1098    fn num<'gc>(gc: &'gc GCLock, v: f64) -> &'gc Node<'gc> {
1099        gc.alloc(Node::NumericLiteral(NumericLiteral {
1100            metadata: NodeMetadata::new(dummy_range()),
1101            value: Cell::new(v),
1102        }))
1103    }
1104
1105    #[test]
1106    fn alloc_and_deep_match() {
1107        let mut ctx = Context::new();
1108        let gc = GCLock::new(&mut ctx);
1109        let l = num(&gc, 1.0);
1110        let r = num(&gc, 2.0);
1111        let op = gc.atom_bytes("+".as_bytes());
1112        let bin = gc.alloc(Node::BinaryExpression(BinaryExpression {
1113            metadata: NodeMetadata::new(dummy_range()),
1114            left: l,
1115            right: r,
1116            operator: Cell::new(op),
1117        }));
1118        // Deep, one-level match through &Node.
1119        if let Node::BinaryExpression(b) = bin {
1120            assert!(matches!(b.left, Node::NumericLiteral(n) if n.value.get() == 1.0));
1121        } else {
1122            panic!()
1123        }
1124    }
1125
1126    #[test]
1127    fn cell_mutation_in_place() {
1128        let mut ctx = Context::new();
1129        let gc = GCLock::new(&mut ctx);
1130        let n = num(&gc, 3.0);
1131        if let Node::NumericLiteral(x) = n {
1132            x.value.set(9.0);
1133        }
1134        assert!(matches!(n, Node::NumericLiteral(x) if x.value.get() == 9.0));
1135    }
1136
1137    #[test]
1138    #[should_panic(expected = "multiple GCLocks")]
1139    fn single_gclock_per_thread() {
1140        let mut a = Context::new();
1141        let mut b = Context::new();
1142        let _g1 = GCLock::new(&mut a);
1143        let _g2 = GCLock::new(&mut b); // must panic
1144    }
1145
1146    #[test]
1147    fn from_iter_roundtrip() {
1148        let mut ctx = Context::new();
1149        let gc = GCLock::new(&mut ctx);
1150
1151        // Empty list has zero elements.
1152        let empty = NodeList::empty();
1153        assert_eq!(empty.iter().count(), 0);
1154
1155        // Build three nodes and collect into a NodeList.
1156        let a = num(&gc, 1.0);
1157        let b = num(&gc, 2.0);
1158        let c = num(&gc, 3.0);
1159        let list = NodeList::from_iter(&gc, [a, b, c]);
1160        assert_eq!(list.iter().count(), 3);
1161
1162        // Verify values come back in the original order.
1163        let values: Vec<f64> = list
1164            .iter()
1165            .map(|n| {
1166                if let Node::NumericLiteral(nl) = n {
1167                    nl.value.get()
1168                } else {
1169                    panic!("expected NumericLiteral")
1170                }
1171            })
1172            .collect();
1173        assert_eq!(values, vec![1.0, 2.0, 3.0]);
1174    }
1175
1176    #[test]
1177    fn noderc_roundtrip() {
1178        let mut ctx = Context::new();
1179        let rc = {
1180            // First GCLock scope: allocate a node and wrap it in a NodeRc.
1181            let gc = GCLock::new(&mut ctx);
1182            let n = num(&gc, 42.0);
1183            NodeRc::from_node(&gc, n)
1184            // `gc` drops here, releasing the GCLock.
1185        };
1186
1187        // Re-acquire the lock and verify the node is still reachable.
1188        let gc2 = GCLock::new(&mut ctx);
1189        let node = rc.node(&gc2);
1190        assert!(matches!(node, Node::NumericLiteral(nl) if nl.value.get() == 42.0));
1191        // Drop rc while the lock is held so the Context doesn't panic on drop.
1192        drop(rc);
1193    }
1194
1195    /// The `StorageEntry` recovered from a node reference must be exactly the
1196    /// entry the allocation produced — for the node itself and for the
1197    /// `NodeRc` built from it.
1198    #[test]
1199    fn storage_entry_recovery_matches_allocation() {
1200        let mut ctx = Context::new();
1201        let rc = {
1202            let gc = GCLock::new(&mut ctx);
1203            let n = num(&gc, 7.0);
1204
1205            // The entry the arena actually allocated: the last one pushed.
1206            let nodes = unsafe { &*gc.ctx().nodes.get() };
1207            let allocated = nodes.iter().last().expect("one entry") as *const StorageEntry as usize;
1208
1209            let entry = unsafe { StorageEntry::from_node(n) };
1210            assert_eq!(
1211                entry as *const StorageEntry as usize, allocated,
1212                "StorageEntry::from_node must recover the allocated entry"
1213            );
1214            assert!(
1215                std::ptr::eq(&entry.inner, n),
1216                "recovered entry holds the node"
1217            );
1218            assert_eq!(entry.ctx_id_markbit.get() & !(1 << 31), gc.ctx().id);
1219
1220            let rc = NodeRc::from_node(&gc, n);
1221            assert_eq!(
1222                rc.entry.as_ptr() as usize,
1223                allocated,
1224                "NodeRc::from_node must point at the allocated entry"
1225            );
1226            assert_eq!(entry.count.get(), 1, "the NodeRc took the entry's refcount");
1227            rc
1228        };
1229        let gc2 = GCLock::new(&mut ctx);
1230        assert!(matches!(rc.node(&gc2), Node::NumericLiteral(n) if n.value.get() == 7.0));
1231        drop(rc);
1232    }
1233
1234    /// `container_of` must step back in *bytes*. `StorageEntry` is
1235    /// `repr(Rust)` and happens to place `inner` at offset 0 today, which
1236    /// hides the difference; this stand-in forces a non-zero offset, which is
1237    /// exactly what a field reorder would produce.
1238    #[test]
1239    fn container_of_is_byte_stride() {
1240        #[repr(C)]
1241        struct Outer {
1242            ctx_id_markbit: Cell<u32>,
1243            count: Cell<u32>,
1244            inner: [u64; 4],
1245        }
1246        let outer = Outer {
1247            ctx_id_markbit: Cell::new(1),
1248            count: Cell::new(0),
1249            inner: [7; 4],
1250        };
1251        let offset = core::mem::offset_of!(Outer, inner);
1252        assert_ne!(offset, 0, "the stand-in must exercise a non-zero offset");
1253        let recovered = unsafe { container_of::<Outer, [u64; 4]>(&outer.inner, offset) };
1254        assert_eq!(
1255            recovered as usize, &outer as *const Outer as usize,
1256            "container_of must step back in bytes, not in units of the field type"
1257        );
1258    }
1259
1260    /// Build a `NodeRc`, park it in `escaped`, then drop its `Context` out
1261    /// from under it — which panics on the way out, by design.
1262    fn orphan_noderc(escaped: &RefCell<Option<NodeRc>>) {
1263        let mut ctx = Context::new();
1264        {
1265            let gc = GCLock::new(&mut ctx);
1266            *escaped.borrow_mut() = Some(NodeRc::from_node(&gc, num(&gc, 5.0)));
1267        }
1268        drop(ctx); // panics: a NodeRc is still alive
1269    }
1270
1271    /// The documented guard: dropping a `Context` with a live `NodeRc` panics.
1272    /// `escaped` is a local of *this* function, so the orphaned handle is
1273    /// dropped by the unwind the guard starts — which is the first place the
1274    /// old code went off the rails (SIGSEGV, since a deque chunk is large
1275    /// enough to be unmapped on free).
1276    #[test]
1277    #[should_panic(expected = "NodeRc must not outlive Context")]
1278    fn noderc_outliving_context_panics() {
1279        let escaped = RefCell::new(None);
1280        orphan_noderc(&escaped);
1281    }
1282
1283    /// ...and the panic is survivable, which is what a panic-catching host
1284    /// (test harness, server) depends on: here the handle outlives the caught
1285    /// panic and is cloned and dropped afterwards, touching both the entry's
1286    /// refcount and the counter's. Pre-fix those were accesses to freed
1287    /// memory.
1288    #[test]
1289    fn noderc_outliving_context_is_survivable() {
1290        // Declared outside the closure, so the handle survives the unwind.
1291        let escaped: RefCell<Option<NodeRc>> = RefCell::new(None);
1292        let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
1293            orphan_noderc(&escaped);
1294        }));
1295        assert!(result.is_err(), "the guard must still panic");
1296
1297        let rc = escaped
1298            .borrow_mut()
1299            .take()
1300            .expect("handle outlived the panic");
1301        let cloned = rc.clone(); // touches the leaked entry + counter
1302        drop(cloned);
1303        drop(rc);
1304
1305        // Churn the allocator the way a panic-catching host would: pre-fix
1306        // the refcount decrements above landed in freed memory.
1307        let mut v: Vec<Vec<u64>> = (0..512u64).map(|i| vec![i; 64]).collect();
1308        v.truncate(0);
1309        drop(v);
1310    }
1311
1312    #[test]
1313    fn alloc_scope_truncates_nodes_and_lists() {
1314        let mut ctx = Context::new();
1315        let gc = GCLock::new(&mut ctx);
1316        let base_nodes = gc.ctx().num_nodes();
1317        let base_elems = gc.ctx().num_list_elements();
1318
1319        // Pre-scope survivor.
1320        let survivor = num(&gc, 99.0);
1321        {
1322            let _scope = unsafe { gc.alloc_scope() };
1323            for _ in 0..100 {
1324                num(&gc, 0.0);
1325            }
1326            // A NodeList inside the scope allocates list elements.
1327            let a = num(&gc, 1.0);
1328            let _list = NodeList::from_iter(&gc, [a]);
1329            assert_eq!(gc.ctx().num_nodes(), base_nodes + 102);
1330            assert!(gc.ctx().num_list_elements() > base_elems);
1331        }
1332        // Scope drop reclaimed everything allocated inside it.
1333        assert_eq!(gc.ctx().num_nodes(), base_nodes + 1);
1334        assert_eq!(gc.ctx().num_list_elements(), base_elems);
1335        // The pre-scope survivor is untouched.
1336        assert!(matches!(survivor, Node::NumericLiteral(n) if n.value.get() == 99.0));
1337    }
1338
1339    #[test]
1340    fn alloc_scope_nests() {
1341        let mut ctx = Context::new();
1342        let gc = GCLock::new(&mut ctx);
1343        let base = gc.ctx().num_nodes();
1344        {
1345            let _outer = unsafe { gc.alloc_scope() };
1346            num(&gc, 1.0); // 1 outer allocation
1347            {
1348                let _inner = unsafe { gc.alloc_scope() };
1349                for _ in 0..50 {
1350                    num(&gc, 0.0);
1351                }
1352            }
1353            assert_eq!(gc.ctx().num_nodes(), base + 1, "inner scope reclaimed");
1354            // Outer keeps allocating after the inner truncate (bump reuse).
1355            for _ in 0..10 {
1356                num(&gc, 0.0);
1357            }
1358            assert_eq!(gc.ctx().num_nodes(), base + 11);
1359        }
1360        assert_eq!(gc.ctx().num_nodes(), base);
1361    }
1362}