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    /// Obtain the contents of an atom as a string, substituting U+FFFD for
418    /// anything unrepresentable. See [`AtomTable::bytes_str_lossy`].
419    #[inline]
420    pub fn bytes_str_lossy(&self, ident: AtomBytes) -> &str {
421        self.atom_table.bytes_str_lossy(ident)
422    }
423
424    /// Obtain the contents of an atom as a string, or `None` if it holds an
425    /// unpaired surrogate, which has no UTF-8 form. See
426    /// [`AtomTable::try_bytes_str`].
427    #[inline]
428    pub fn try_bytes_str(&self, ident: AtomBytes) -> Option<&str> {
429        self.atom_table.try_bytes_str(ident)
430    }
431
432    /// Return true if strict mode has been forced globally.
433    pub fn strict_mode(&self) -> bool {
434        self.strict_mode
435    }
436
437    /// Enable strict mode. Note that it cannot be unset.
438    pub fn enable_strict_mode(&mut self) {
439        self.strict_mode = true;
440    }
441
442    /// Return true if `eval()` is enabled. Mirrors C++
443    /// `Context::getEnableEval()` (Context.h:407-409).
444    pub fn enable_eval(&self) -> bool {
445        self.enable_eval
446    }
447
448    /// Enable or disable `eval()`. Mirrors C++
449    /// `Context::setEnableEval()` (Context.h:410-412).
450    pub fn set_enable_eval(&mut self, v: bool) {
451        self.enable_eval = v;
452    }
453
454    /// Return true if Flow type parsing is enabled.
455    /// Mirrors C++ `Context::getParseFlow()`.
456    pub fn parse_flow(&self) -> bool {
457        self.parse_flow
458    }
459
460    /// Enable or disable Flow type parsing.
461    /// Mirrors C++ `Context::setParseFlow()`.
462    pub fn set_parse_flow(&mut self, v: bool) {
463        self.parse_flow = v;
464    }
465
466    /// Return true if the Flow ambiguous-expression grammar is enabled.
467    /// Mirrors C++ `Context::getParseFlowAmbiguous()`.
468    pub fn parse_flow_ambiguous(&self) -> bool {
469        self.parse_flow_ambiguous
470    }
471
472    /// Enable or disable the Flow ambiguous-expression grammar.
473    pub fn set_parse_flow_ambiguous(&mut self, v: bool) {
474        self.parse_flow_ambiguous = v;
475    }
476
477    /// Return true if Flow `component`/`hook` syntax is enabled.
478    /// Mirrors C++ `Context::getParseFlowComponentSyntax()`.
479    pub fn parse_flow_component_syntax(&self) -> bool {
480        self.parse_flow_component_syntax
481    }
482
483    /// Enable or disable Flow `component`/`hook` syntax.
484    pub fn set_parse_flow_component_syntax(&mut self, v: bool) {
485        self.parse_flow_component_syntax = v;
486    }
487
488    /// Return true if Flow `record` declarations/expressions are enabled.
489    /// Mirrors C++ `Context::getParseFlowRecords()`.
490    pub fn parse_flow_records(&self) -> bool {
491        self.parse_flow_records
492    }
493
494    /// Enable or disable Flow `record` declarations/expressions.
495    pub fn set_parse_flow_records(&mut self, v: bool) {
496        self.parse_flow_records = v;
497    }
498
499    /// Return true if Flow `match` expressions/statements are enabled.
500    /// Mirrors C++ `Context::getParseFlowMatch()`.
501    pub fn parse_flow_match(&self) -> bool {
502        self.parse_flow_match
503    }
504
505    /// Enable or disable Flow `match` expressions/statements.
506    pub fn set_parse_flow_match(&mut self, v: bool) {
507        self.parse_flow_match = v;
508    }
509
510    /// Return true if TypeScript type parsing is enabled.
511    /// Mirrors C++ `Context::getParseTS()`.
512    pub fn parse_ts(&self) -> bool {
513        self.parse_ts
514    }
515
516    /// Enable or disable TypeScript type parsing.
517    /// Mirrors C++ `Context::setParseTS()`.
518    pub fn set_parse_ts(&mut self, v: bool) {
519        self.parse_ts = v;
520    }
521
522    /// Return true if JSX parsing is enabled. Mirrors C++
523    /// `Context::getParseJSX()`.
524    pub fn parse_jsx(&self) -> bool {
525        self.parse_jsx
526    }
527
528    /// Enable or disable JSX parsing. Mirrors C++ `Context::setParseJSX()`.
529    /// Currently only read by the TS `<Type>` cast gate; the setter is wired
530    /// when the JSX phase lands.
531    pub fn set_parse_jsx(&mut self, v: bool) {
532        self.parse_jsx = v;
533    }
534
535    /// Return the preemptive-function-compilation threshold (bytes). Port of
536    /// `Context::getPreemptiveFunctionCompilationThreshold()` (Context.h:516-518).
537    pub fn preemptive_function_compilation_threshold(&self) -> u32 {
538        self.preemptive_function_compilation_threshold
539    }
540
541    /// Set the preemptive-function-compilation threshold (bytes). Port of
542    /// `Context::setPreemptiveFunctionCompilationThreshold()` (Context.h:520-522).
543    pub fn set_preemptive_function_compilation_threshold(&mut self, byte_count: u32) {
544        self.preemptive_function_compilation_threshold = byte_count;
545    }
546
547    /// Mark and sweep the arena: everything reachable from a live [`NodeRc`]
548    /// survives, the rest is returned to the free lists. Requires `&mut self`,
549    /// so no [`GCLock`] — and therefore no `&Node` — can be outstanding.
550    pub fn gc(&mut self) {
551        let nodes = unsafe { &mut *self.nodes.get() };
552        let free_nodes = unsafe { &mut *self.free_nodes.get() };
553
554        let list_elements = unsafe { &mut *self.list_elements.get() };
555        let free_list_elements = unsafe { &mut *self.free_list_elements.get() };
556
557        {
558            // Begin by collecting all the roots: entries with non-zero refcount.
559            let mut roots: Vec<&StorageEntry> = vec![];
560            for entry in nodes.iter() {
561                if entry.is_free() {
562                    continue;
563                }
564                debug_assert!(
565                    entry.markbit() != self.markbit_marked,
566                    "Entry marked before start of GC: \
567                        {:?}\nentry.markbit()={}\nmarkbit_marked={}",
568                    &entry,
569                    entry.markbit(),
570                    self.markbit_marked,
571                );
572                if entry.count.get() > 0 {
573                    // Transmuting the lifetime here because we have to store the roots from
574                    // across accesses to `nodes`, meaning we must translate
575                    // from `'ast` to the lifetime of this scope.
576                    roots.push(unsafe {
577                        std::mem::transmute::<&StorageEntry<'_>, &StorageEntry<'_>>(entry)
578                    });
579                }
580            }
581
582            struct Marker {
583                markbit_marked: bool,
584            }
585
586            impl<'gc> Visitor<'gc> for Marker {
587                fn visit_node(&mut self, node: &'gc Node<'gc>) {
588                    let entry = unsafe { StorageEntry::from_node(node) };
589                    if entry.markbit() == self.markbit_marked {
590                        // Stop visiting early if we've already marked this part,
591                        // because we must have also marked all the children.
592                        return;
593                    }
594                    entry.set_markbit(self.markbit_marked);
595                    let mark = self.markbit_marked;
596                    node.mark_lists(&mut |list: &NodeList<'gc>| {
597                        // Mark each list element's storage bit.
598                        let mut p = list.head;
599                        while !p.is_null() {
600                            let elem = unsafe { &*p };
601                            elem.set_markbit(mark);
602                            p = elem.next.get();
603                        }
604                    });
605                    node.visit_children(self);
606                }
607            }
608
609            // Use a visitor to mark every node reachable from roots.
610            // Marking happens while holding `&mut self`, so no GCLock
611            // re-entrancy is needed.
612            let mut marker = Marker {
613                markbit_marked: self.markbit_marked,
614            };
615            for root in roots {
616                marker.visit_node(&root.inner);
617            }
618        }
619
620        // Borrow once: every node this sweep frees appends its id here so
621        // sema side tables (keyed by NodeId) can prune the dead entries.
622        let mut freed_node_ids = self.freed_node_ids.borrow_mut();
623        for entry in nodes.iter_mut() {
624            if entry.is_free() {
625                // Skip free entries.
626                continue;
627            }
628            if entry.count.get() > 0 {
629                // Keep referenced entries alive.
630                continue;
631            }
632            if entry.markbit() == self.markbit_marked {
633                // Keep marked entries alive.
634                continue;
635            }
636            // Passed all checks, this entry is free.
637            freed_node_ids.push(entry.inner.metadata().id.get());
638            entry.ctx_id_markbit.set(FREE_ENTRY);
639            free_nodes.push(unsafe { NonNull::new_unchecked(entry as *mut StorageEntry) });
640        }
641
642        for element in list_elements.iter_mut() {
643            if element.is_free() {
644                // Skip free entries.
645                continue;
646            }
647            if element.markbit() == self.markbit_marked {
648                // Keep marked entries alive.
649                continue;
650            }
651            // Passed all checks, this element is free.
652            element.ctx_id_markbit.set(FREE_ENTRY);
653            free_list_elements
654                .push(unsafe { NonNull::new_unchecked(element as *mut NodeListElement) });
655        }
656
657        self.markbit_marked = !self.markbit_marked;
658    }
659
660    /// Drain and return the ids of every node freed (by `gc()` or by an
661    /// `AllocationScope` truncation) since the last call. Consumers use this
662    /// to prune dead entries out of side tables keyed by `NodeId`.
663    pub fn take_freed_node_ids(&mut self) -> Vec<NodeId> {
664        std::mem::take(&mut *self.freed_node_ids.borrow_mut())
665    }
666
667    /// Returns the number of node slots which have been allocated.
668    /// Includes nodes currently in use as well as nodes in the free list.
669    pub fn num_nodes(&self) -> usize {
670        let nodes = unsafe { &*self.nodes.get() };
671        nodes.len()
672    }
673
674    /// Returns the number of list-element slots which have been allocated.
675    /// Includes elements currently in use as well as elements in the free
676    /// list.
677    pub fn num_list_elements(&self) -> usize {
678        let list_elements = unsafe { &*self.list_elements.get() };
679        list_elements.len()
680    }
681
682    /// Returns the number of node slots currently in the free list (i.e.
683    /// allocated but unused, reclaimed by GC).
684    pub fn num_free_nodes(&self) -> usize {
685        let free_nodes = unsafe { &*self.free_nodes.get() };
686        free_nodes.len()
687    }
688
689    /// Returns the approximate size of just the AST storages in bytes.
690    /// Includes the allocated nodes, lists, as well as free lists for both.
691    pub fn storage_size(&self) -> usize {
692        let nodes = unsafe { &*self.nodes.get() };
693        let free_nodes = unsafe { &*self.free_nodes.get() };
694        let list_elements = unsafe { &*self.list_elements.get() };
695        let free_list_elements = unsafe { &*self.free_list_elements.get() };
696        let mut result = 0;
697        result += nodes.heap_size();
698        result += free_nodes.heap_size();
699        result += list_elements.heap_size();
700        result += free_list_elements.heap_size();
701        result
702    }
703
704    /// Leak everything an outstanding [`NodeRc`] still touches after this
705    /// `Context` is gone, and return the leaked node storage.
706    ///
707    /// Called only from the `Drop` guard's failure path. A `NodeRc` that
708    /// outlives its `Context` is a caller bug which the guard reports by
709    /// panicking, but the report is worthless if the handle's own `drop` —
710    /// which runs during the unwind, or after a `catch_unwind` — writes into
711    /// freed memory. A handle reaches exactly two places: the `count` cell in
712    /// its `StorageEntry` (inside the node deque) and the `NodeRcCounter`
713    /// box (also read by `NodeRc::node` for its context-id check). Both are
714    /// leaked here, so those accesses stay valid for the life of the process.
715    /// Nothing else in the arena is reachable from a `NodeRc` once the
716    /// `Context` is gone, so the rest is freed normally.
717    fn leak_noderc_targets<'s>(&'s mut self) -> &'s Deque<StorageEntry<'ast>> {
718        // SAFETY: `drop` holds `&mut self`, so no `GCLock` and no other
719        // borrow of the deque exists; the field is left holding an empty
720        // deque for the drop glue to dispose of.
721        let nodes = std::mem::take(unsafe { &mut *self.nodes.get() });
722        // The `StorageEntry`s live in the deque's chunks, which this moves
723        // (as a `Vec<Vec<_>>` header) but does not reallocate, so entry
724        // addresses — the ones the outstanding handles hold — are unchanged.
725        let leaked_nodes: &'s Deque<StorageEntry<'ast>> = Box::leak(Box::new(nodes));
726
727        // Replace the counter with a fresh box and forget the old one, so the
728        // address every outstanding handle holds stays allocated. `ctx_id` is
729        // preserved in the leaked copy, which keeps `NodeRc::node`'s
730        // "allocated in context N" assertion honest afterwards.
731        let fresh = Pin::new(Box::new(NodeRcCounter {
732            ctx_id: self.id,
733            count: Cell::new(0),
734        }));
735        std::mem::forget(std::mem::replace(&mut self.noderc_count, fresh));
736
737        leaked_nodes
738    }
739}
740
741impl HeapSize for Context<'_> {
742    /// Returns the heap size of the AST storages only.
743    /// Atom-table memory is intentionally excluded: the `AtomTable` is
744    /// externally owned and accounted for separately.
745    fn heap_size(&self) -> usize {
746        let nodes = unsafe { &*self.nodes.get() };
747        let free_nodes = unsafe { &*self.free_nodes.get() };
748        let list_elements = unsafe { &*self.list_elements.get() };
749        let free_list_elements = unsafe { &*self.free_list_elements.get() };
750        let mut result = 0;
751        result += nodes.heap_size();
752        result += free_nodes.heap_size();
753        result += list_elements.heap_size();
754        result += free_list_elements.heap_size();
755        result += std::mem::size_of::<NodeRcCounter>();
756        result
757    }
758}
759
760impl Drop for Context<'_> {
761    /// Ensure that there are no outstanding `NodeRc`s into this `Context` which will be
762    /// invalidated once it is dropped.
763    ///
764    /// # Panics
765    ///
766    /// Will panic if there are any `NodeRc`s stored when this `Context` is dropped.
767    ///
768    /// The panic is the *only* effect: before panicking, the node storage and
769    /// the `NodeRc` counter are leaked (`Context::leak_noderc_targets`), so
770    /// the outstanding handles — which are dropped during the ensuing
771    /// unwind, or later still if the panic is caught — decrement refcounts in
772    /// memory that is still valid. Leaking the arena is the price of keeping a
773    /// caller's bug a panic instead of a use-after-free.
774    fn drop(&mut self) {
775        if self.noderc_count.count.get() > 0 {
776            // Do this first: everything below can panic, and after the leak
777            // no unwind path can free what the outstanding `NodeRc`s touch.
778            let leaked_nodes = self.leak_noderc_targets();
779            #[cfg(debug_assertions)]
780            {
781                // In debug mode, provide more information on which node was leaked.
782                for entry in leaked_nodes.iter() {
783                    assert!(
784                        entry.count.get() == 0,
785                        "NodeRc must not outlive Context: {:#?}\n",
786                        &entry.inner
787                    );
788                }
789            }
790            #[cfg(not(debug_assertions))]
791            let _ = leaked_nodes;
792            // In release mode, just panic immediately.
793            panic!("NodeRc must not outlive Context");
794        }
795    }
796}
797
798thread_local! {
799    /// Whether there exists a `GCLock` on the current thread.
800    static GCLOCK_IN_USE: Cell<bool> = const { Cell::new(false) };
801}
802
803/// A way to view the [`Context`].
804///
805/// Provides the user the ability to create new nodes and dereference [`NodeRc`].
806///
807/// **At most one is allowed to be active in any thread at any time.**
808/// This is to ensure no `&Node` can be shared between `Context`s.
809pub struct GCLock<'ast, 'ctx> {
810    ctx: &'ctx mut Context<'ast>,
811}
812
813impl Drop for GCLock<'_, '_> {
814    fn drop(&mut self) {
815        GCLOCK_IN_USE.with(|flag| {
816            flag.set(false);
817        });
818    }
819}
820
821impl<'ast, 'ctx> GCLock<'ast, 'ctx> {
822    /// # Panics
823    ///
824    /// Will panic if there is already an active `GCLock` on this thread.
825    pub fn new(ctx: &'ctx mut Context<'ast>) -> Self {
826        GCLOCK_IN_USE.with(|flag| {
827            if flag.get() {
828                panic!("Attempt to create multiple GCLocks in a single thread");
829            }
830            flag.set(true);
831        });
832        GCLock { ctx }
833    }
834
835    /// Allocate a node in the `ctx`.
836    #[inline]
837    pub fn alloc<'s>(&'s self, n: Node<'s>) -> &'s Node<'s> {
838        self.ctx.alloc(n)
839    }
840
841    /// Append `node` to the `prev` element if provided, else create the element as the first
842    /// element in the `NodeList`.
843    #[inline]
844    pub(crate) fn append_list_element<'s>(
845        &'s self,
846        prev: Option<&'s NodeListElement<'s>>,
847        n: &'s Node<'s>,
848    ) -> &'s NodeListElement<'s> {
849        self.ctx.append_list_element(prev, n)
850    }
851
852    /// Return a reference to the owning Context.
853    pub fn ctx(&self) -> &Context<'ast> {
854        self.ctx
855    }
856
857    /// Add a byte-string to the identifier table.
858    #[inline]
859    pub fn atom_bytes<V: Into<Vec<u8>> + AsRef<[u8]>>(&self, value: V) -> AtomBytes {
860        self.ctx.atom_bytes(value)
861    }
862
863    /// Obtain the contents of an atom from the atom table.
864    #[inline]
865    pub fn bytes(&self, ident: AtomBytes) -> &[u8] {
866        self.ctx.bytes(ident)
867    }
868
869    /// Obtain the contents of an atom as a string, substituting U+FFFD for
870    /// anything unrepresentable. This is the usual way to print an
871    /// identifier's name: `gc.bytes_str_lossy(id.name.get())`. See
872    /// [`AtomTable::bytes_str_lossy`].
873    #[inline]
874    pub fn bytes_str_lossy(&self, ident: AtomBytes) -> &str {
875        self.ctx.bytes_str_lossy(ident)
876    }
877
878    /// Obtain the contents of an atom as a string, or `None` if it holds an
879    /// unpaired surrogate — a legal JS string value with no UTF-8 form. This
880    /// is the right accessor for string-literal values, where substituting
881    /// U+FFFD would silently corrupt the program's data. Note a surrogate
882    /// *pair*, which is how an astral character is stored, comes back as
883    /// `Some`. See [`AtomTable::try_bytes_str`].
884    #[inline]
885    pub fn try_bytes_str(&self, ident: AtomBytes) -> Option<&str> {
886        self.ctx.try_bytes_str(ident)
887    }
888}
889
890/// RAII allocation scope over the arena: everything allocated (nodes AND
891/// list elements) between construction and drop is reclaimed at drop, with
892/// bump-allocator save/restore semantics. Port of the C++ `AllocationScope`
893/// (hermes/Support/Allocator.h:500-521) as used by the parser's PreParse
894/// pass (JSParserImpl.cpp:548, 7523).
895///
896/// See [`GCLock::alloc_scope`] for the safety contract.
897pub struct AllocationScope<'gcl, 'ast, 'ctx> {
898    lock: &'gcl GCLock<'ast, 'ctx>,
899    nodes_watermark: usize,
900    list_elements_watermark: usize,
901}
902
903impl Drop for AllocationScope<'_, '_, '_> {
904    fn drop(&mut self) {
905        let ctx: &Context<'_> = self.lock.ctx;
906        let nodes = unsafe { &mut *ctx.nodes.get() };
907        #[cfg(debug_assertions)]
908        for entry in nodes.iter_from(self.nodes_watermark) {
909            // A NodeRc into the suffix would dangle after truncation.
910            debug_assert!(
911                entry.count.get() == 0,
912                "NodeRc points into a truncated AllocationScope suffix"
913            );
914            // gc() cannot run under a GCLock, so no suffix entry can be
915            // free (free-list pops reuse only pre-watermark slots).
916            debug_assert!(!entry.is_free(), "free entry in scope suffix");
917        }
918        // Log every reclaimed node's id (the debug asserts above already
919        // guarantee no suffix entry is free) so sema side tables can prune
920        // dead entries, same as the gc() sweep does.
921        let mut freed_node_ids = ctx.freed_node_ids.borrow_mut();
922        for entry in nodes.iter_from(self.nodes_watermark) {
923            freed_node_ids.push(entry.inner.metadata().id.get());
924        }
925        nodes.truncate(self.nodes_watermark);
926        let list_elements = unsafe { &mut *ctx.list_elements.get() };
927        list_elements.truncate(self.list_elements_watermark);
928    }
929}
930
931impl<'ast, 'ctx> GCLock<'ast, 'ctx> {
932    /// Open an allocation scope: everything allocated between this call and
933    /// the returned guard's drop is reclaimed at drop (nodes and list
934    /// elements). Mirrors the C++ `AllocationScope` discipline the PreParse
935    /// pass uses (JSParserImpl.cpp:516-560).
936    ///
937    /// # Safety
938    ///
939    /// The caller must guarantee that when the guard drops:
940    /// - no `&Node`, `NodeList`, `&NodeListElement`, or interior reference into an allocation
941    ///   made after this call survives — the storage is freed and any such
942    ///   reference dangles; and
943    /// - no `NodeRc` points into those allocations (debug-asserted).
944    ///
945    /// If the `Context` ran `gc()` before this pass, in-scope allocations
946    /// may be served from the free list at pre-watermark positions; those
947    /// escape reclamation harmlessly (unreferenced until the next `gc()`).
948    pub unsafe fn alloc_scope<'s>(&'s self) -> AllocationScope<'s, 'ast, 'ctx> {
949        let nodes = unsafe { &*self.ctx.nodes.get() };
950        let list_elements = unsafe { &*self.ctx.list_elements.get() };
951        AllocationScope {
952            lock: self,
953            nodes_watermark: nodes.len(),
954            list_elements_watermark: list_elements.len(),
955        }
956    }
957}
958
959/// A wrapper around Node&, with "shallow" hashing and equality, suitable for
960/// hash tables.
961#[derive(Debug, Copy, Clone)]
962pub struct NodePtr<'gc>(pub &'gc Node<'gc>);
963
964impl<'gc> NodePtr<'gc> {
965    /// Wrap a node reference so it can be used as a hash-table key.
966    pub fn from_node(node: &'gc Node<'gc>) -> Self {
967        Self(node)
968    }
969}
970
971impl<'gc> PartialEq for NodePtr<'gc> {
972    fn eq(&self, other: &Self) -> bool {
973        std::ptr::eq(self.0, other.0)
974    }
975}
976
977impl Eq for NodePtr<'_> {}
978
979impl Hash for NodePtr<'_> {
980    fn hash<H: Hasher>(&self, state: &mut H) {
981        (self.0 as *const Node).hash(state)
982    }
983}
984
985impl<'gc> Deref for NodePtr<'gc> {
986    type Target = Node<'gc>;
987    fn deref(&self) -> &'gc Self::Target {
988        self.0
989    }
990}
991
992impl<'gc> AsRef<Node<'gc>> for NodePtr<'gc> {
993    fn as_ref(&self) -> &'gc Node<'gc> {
994        self.0
995    }
996}
997
998impl<'gc> From<&'gc Node<'gc>> for NodePtr<'gc> {
999    fn from(node: &'gc Node<'gc>) -> Self {
1000        NodePtr(node)
1001    }
1002}
1003
1004/// Reference counted pointer to a [`Node`] in any [`Context`].
1005///
1006/// It can be used to keep references to `Node`s outside of the lifetime of a [`GCLock`],
1007/// but the only way to derefence and inspect the `Node` is to use a `GCLock`.
1008///
1009/// A `NodeRc` must not outlive its `Context`: dropping a `Context` while one
1010/// is alive panics (see [`Context`]'s `Drop`). Should that happen anyway, the
1011/// handle itself stays safe to drop and to clone — the guard leaks the storage
1012/// it points at rather than freeing it — but it can no longer be dereferenced,
1013/// since [`NodeRc::node`] needs a `GCLock` on the context it came from.
1014#[derive(Debug, Eq)]
1015pub struct NodeRc {
1016    /// The `NodeRcCounter` counting for the `Context` to which this belongs.
1017    counter: NonNull<NodeRcCounter>,
1018
1019    /// Pointer to the `StorageEntry` containing the `Node`.
1020    /// Stored as `c_void` to avoid specifying lifetimes, as dereferencing is checked manually.
1021    entry: NonNull<c_void>,
1022}
1023
1024impl Hash for NodeRc {
1025    fn hash<H: Hasher>(&self, state: &mut H) {
1026        self.entry.hash(state)
1027    }
1028}
1029
1030impl PartialEq for NodeRc {
1031    fn eq(&self, other: &Self) -> bool {
1032        self.entry == other.entry
1033    }
1034}
1035
1036impl Drop for NodeRc {
1037    fn drop(&mut self) {
1038        let entry = unsafe { self.entry().as_mut() };
1039        let c = entry.count.get();
1040        debug_assert!(c > 0);
1041        entry.count.set(c - 1);
1042
1043        let noderc_count = unsafe { self.counter.as_mut() };
1044        let c = noderc_count.count.get();
1045        debug_assert!(c > 0);
1046        noderc_count.count.set(c - 1);
1047    }
1048}
1049
1050impl Clone for NodeRc {
1051    /// Cloning a `NodeRc` increments refcounts on the entry and the context.
1052    fn clone(&self) -> Self {
1053        let mut cloned = NodeRc { ..*self };
1054
1055        let entry = unsafe { cloned.entry().as_mut() };
1056        let c = entry.count.get();
1057        entry.count.set(c + 1);
1058
1059        let noderc_count = unsafe { cloned.counter.as_mut() };
1060        let c = noderc_count.count.get();
1061        noderc_count.count.set(c + 1);
1062
1063        cloned
1064    }
1065}
1066
1067impl NodeRc {
1068    /// Turn a node reference into a `NodeRc` for storage outside `GCLock`.
1069    pub fn from_node<'gc>(gc: &'gc GCLock, node: &'gc Node<'gc>) -> NodeRc {
1070        // SAFETY: `node` was handed out by the arena, so it is the `inner`
1071        // field of a live `StorageEntry` — the contract of
1072        // `StorageEntry::from_node`.
1073        unsafe { Self::from_entry(gc, StorageEntry::from_node(node)) }
1074    }
1075
1076    /// Return the actual `Node` that `self` points to.
1077    ///
1078    /// # Panics
1079    ///
1080    /// Will panic if `gc` is not for the same context as this `NodeRc` was created in.
1081    pub fn node<'gc>(&'_ self, gc: &'gc GCLock<'_, '_>) -> &'gc Node<'_> {
1082        unsafe {
1083            assert_eq!(
1084                self.counter.as_ref().ctx_id,
1085                gc.ctx.id,
1086                "Attempt to derefence NodeRc allocated context {} in context {}",
1087                self.counter.as_ref().ctx_id,
1088                gc.ctx.id
1089            );
1090            &self.entry().as_ref().inner
1091        }
1092    }
1093
1094    /// Get the pointer to the `StorageEntry`.
1095    unsafe fn entry(&self) -> NonNull<StorageEntry<'_>> {
1096        let outer = self.entry.as_ptr() as *mut StorageEntry;
1097        NonNull::new_unchecked(outer)
1098    }
1099
1100    unsafe fn from_entry(gc: &GCLock, entry: &StorageEntry<'_>) -> NodeRc {
1101        let c = entry.count.get();
1102        entry.count.set(c + 1);
1103
1104        let c = gc.ctx.noderc_count.count.get();
1105        gc.ctx.noderc_count.count.set(c + 1);
1106
1107        NodeRc {
1108            counter: NonNull::new_unchecked(gc.ctx.noderc_count.as_ref().get_ref()
1109                as *const NodeRcCounter
1110                as *mut NodeRcCounter),
1111            entry: NonNull::new_unchecked(entry as *const StorageEntry as *mut c_void),
1112        }
1113    }
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118    use super::*;
1119    use crate::node::*;
1120    use crate::node_child::NodeMetadata;
1121    use std::cell::Cell;
1122    use std::cell::RefCell;
1123    use std::panic::AssertUnwindSafe;
1124
1125    fn dummy_range() -> hermes_support::location::SMRange {
1126        let l = hermes_support::location::SMLoc {
1127            source: hermes_support::location::SourceId::from_index(0),
1128            offset: 0,
1129        };
1130        hermes_support::location::SMRange { start: l, end: l }
1131    }
1132
1133    fn num<'gc>(gc: &'gc GCLock, v: f64) -> &'gc Node<'gc> {
1134        gc.alloc(Node::NumericLiteral(NumericLiteral {
1135            metadata: NodeMetadata::new(dummy_range()),
1136            value: Cell::new(v),
1137        }))
1138    }
1139
1140    #[test]
1141    fn alloc_and_deep_match() {
1142        let mut ctx = Context::new();
1143        let gc = GCLock::new(&mut ctx);
1144        let l = num(&gc, 1.0);
1145        let r = num(&gc, 2.0);
1146        let op = gc.atom_bytes("+".as_bytes());
1147        let bin = gc.alloc(Node::BinaryExpression(BinaryExpression {
1148            metadata: NodeMetadata::new(dummy_range()),
1149            left: l,
1150            right: r,
1151            operator: Cell::new(op),
1152        }));
1153        // Deep, one-level match through &Node.
1154        if let Node::BinaryExpression(b) = bin {
1155            assert!(matches!(b.left, Node::NumericLiteral(n) if n.value.get() == 1.0));
1156        } else {
1157            panic!()
1158        }
1159    }
1160
1161    #[test]
1162    fn cell_mutation_in_place() {
1163        let mut ctx = Context::new();
1164        let gc = GCLock::new(&mut ctx);
1165        let n = num(&gc, 3.0);
1166        if let Node::NumericLiteral(x) = n {
1167            x.value.set(9.0);
1168        }
1169        assert!(matches!(n, Node::NumericLiteral(x) if x.value.get() == 9.0));
1170    }
1171
1172    #[test]
1173    #[should_panic(expected = "multiple GCLocks")]
1174    fn single_gclock_per_thread() {
1175        let mut a = Context::new();
1176        let mut b = Context::new();
1177        let _g1 = GCLock::new(&mut a);
1178        let _g2 = GCLock::new(&mut b); // must panic
1179    }
1180
1181    #[test]
1182    fn from_iter_roundtrip() {
1183        let mut ctx = Context::new();
1184        let gc = GCLock::new(&mut ctx);
1185
1186        // Empty list has zero elements.
1187        let empty = NodeList::empty();
1188        assert_eq!(empty.iter().count(), 0);
1189
1190        // Build three nodes and collect into a NodeList.
1191        let a = num(&gc, 1.0);
1192        let b = num(&gc, 2.0);
1193        let c = num(&gc, 3.0);
1194        let list = NodeList::from_iter(&gc, [a, b, c]);
1195        assert_eq!(list.iter().count(), 3);
1196
1197        // Verify values come back in the original order.
1198        let values: Vec<f64> = list
1199            .iter()
1200            .map(|n| {
1201                if let Node::NumericLiteral(nl) = n {
1202                    nl.value.get()
1203                } else {
1204                    panic!("expected NumericLiteral")
1205                }
1206            })
1207            .collect();
1208        assert_eq!(values, vec![1.0, 2.0, 3.0]);
1209    }
1210
1211    #[test]
1212    fn noderc_roundtrip() {
1213        let mut ctx = Context::new();
1214        let rc = {
1215            // First GCLock scope: allocate a node and wrap it in a NodeRc.
1216            let gc = GCLock::new(&mut ctx);
1217            let n = num(&gc, 42.0);
1218            NodeRc::from_node(&gc, n)
1219            // `gc` drops here, releasing the GCLock.
1220        };
1221
1222        // Re-acquire the lock and verify the node is still reachable.
1223        let gc2 = GCLock::new(&mut ctx);
1224        let node = rc.node(&gc2);
1225        assert!(matches!(node, Node::NumericLiteral(nl) if nl.value.get() == 42.0));
1226        // Drop rc while the lock is held so the Context doesn't panic on drop.
1227        drop(rc);
1228    }
1229
1230    /// The `StorageEntry` recovered from a node reference must be exactly the
1231    /// entry the allocation produced — for the node itself and for the
1232    /// `NodeRc` built from it.
1233    #[test]
1234    fn storage_entry_recovery_matches_allocation() {
1235        let mut ctx = Context::new();
1236        let rc = {
1237            let gc = GCLock::new(&mut ctx);
1238            let n = num(&gc, 7.0);
1239
1240            // The entry the arena actually allocated: the last one pushed.
1241            let nodes = unsafe { &*gc.ctx().nodes.get() };
1242            let allocated = nodes.iter().last().expect("one entry") as *const StorageEntry as usize;
1243
1244            let entry = unsafe { StorageEntry::from_node(n) };
1245            assert_eq!(
1246                entry as *const StorageEntry as usize, allocated,
1247                "StorageEntry::from_node must recover the allocated entry"
1248            );
1249            assert!(
1250                std::ptr::eq(&entry.inner, n),
1251                "recovered entry holds the node"
1252            );
1253            assert_eq!(entry.ctx_id_markbit.get() & !(1 << 31), gc.ctx().id);
1254
1255            let rc = NodeRc::from_node(&gc, n);
1256            assert_eq!(
1257                rc.entry.as_ptr() as usize,
1258                allocated,
1259                "NodeRc::from_node must point at the allocated entry"
1260            );
1261            assert_eq!(entry.count.get(), 1, "the NodeRc took the entry's refcount");
1262            rc
1263        };
1264        let gc2 = GCLock::new(&mut ctx);
1265        assert!(matches!(rc.node(&gc2), Node::NumericLiteral(n) if n.value.get() == 7.0));
1266        drop(rc);
1267    }
1268
1269    /// `container_of` must step back in *bytes*. `StorageEntry` is
1270    /// `repr(Rust)` and happens to place `inner` at offset 0 today, which
1271    /// hides the difference; this stand-in forces a non-zero offset, which is
1272    /// exactly what a field reorder would produce.
1273    #[test]
1274    fn container_of_is_byte_stride() {
1275        #[repr(C)]
1276        struct Outer {
1277            ctx_id_markbit: Cell<u32>,
1278            count: Cell<u32>,
1279            inner: [u64; 4],
1280        }
1281        let outer = Outer {
1282            ctx_id_markbit: Cell::new(1),
1283            count: Cell::new(0),
1284            inner: [7; 4],
1285        };
1286        let offset = core::mem::offset_of!(Outer, inner);
1287        assert_ne!(offset, 0, "the stand-in must exercise a non-zero offset");
1288        let recovered = unsafe { container_of::<Outer, [u64; 4]>(&outer.inner, offset) };
1289        assert_eq!(
1290            recovered as usize, &outer as *const Outer as usize,
1291            "container_of must step back in bytes, not in units of the field type"
1292        );
1293    }
1294
1295    /// Build a `NodeRc`, park it in `escaped`, then drop its `Context` out
1296    /// from under it — which panics on the way out, by design.
1297    fn orphan_noderc(escaped: &RefCell<Option<NodeRc>>) {
1298        let mut ctx = Context::new();
1299        {
1300            let gc = GCLock::new(&mut ctx);
1301            *escaped.borrow_mut() = Some(NodeRc::from_node(&gc, num(&gc, 5.0)));
1302        }
1303        drop(ctx); // panics: a NodeRc is still alive
1304    }
1305
1306    /// The documented guard: dropping a `Context` with a live `NodeRc` panics.
1307    /// `escaped` is a local of *this* function, so the orphaned handle is
1308    /// dropped by the unwind the guard starts — which is the first place the
1309    /// old code went off the rails (SIGSEGV, since a deque chunk is large
1310    /// enough to be unmapped on free).
1311    #[test]
1312    #[should_panic(expected = "NodeRc must not outlive Context")]
1313    fn noderc_outliving_context_panics() {
1314        let escaped = RefCell::new(None);
1315        orphan_noderc(&escaped);
1316    }
1317
1318    /// ...and the panic is survivable, which is what a panic-catching host
1319    /// (test harness, server) depends on: here the handle outlives the caught
1320    /// panic and is cloned and dropped afterwards, touching both the entry's
1321    /// refcount and the counter's. Pre-fix those were accesses to freed
1322    /// memory.
1323    #[test]
1324    fn noderc_outliving_context_is_survivable() {
1325        // Declared outside the closure, so the handle survives the unwind.
1326        let escaped: RefCell<Option<NodeRc>> = RefCell::new(None);
1327        let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
1328            orphan_noderc(&escaped);
1329        }));
1330        assert!(result.is_err(), "the guard must still panic");
1331
1332        let rc = escaped
1333            .borrow_mut()
1334            .take()
1335            .expect("handle outlived the panic");
1336        let cloned = rc.clone(); // touches the leaked entry + counter
1337        drop(cloned);
1338        drop(rc);
1339
1340        // Churn the allocator the way a panic-catching host would: pre-fix
1341        // the refcount decrements above landed in freed memory.
1342        let mut v: Vec<Vec<u64>> = (0..512u64).map(|i| vec![i; 64]).collect();
1343        v.truncate(0);
1344        drop(v);
1345    }
1346
1347    #[test]
1348    fn alloc_scope_truncates_nodes_and_lists() {
1349        let mut ctx = Context::new();
1350        let gc = GCLock::new(&mut ctx);
1351        let base_nodes = gc.ctx().num_nodes();
1352        let base_elems = gc.ctx().num_list_elements();
1353
1354        // Pre-scope survivor.
1355        let survivor = num(&gc, 99.0);
1356        {
1357            let _scope = unsafe { gc.alloc_scope() };
1358            for _ in 0..100 {
1359                num(&gc, 0.0);
1360            }
1361            // A NodeList inside the scope allocates list elements.
1362            let a = num(&gc, 1.0);
1363            let _list = NodeList::from_iter(&gc, [a]);
1364            assert_eq!(gc.ctx().num_nodes(), base_nodes + 102);
1365            assert!(gc.ctx().num_list_elements() > base_elems);
1366        }
1367        // Scope drop reclaimed everything allocated inside it.
1368        assert_eq!(gc.ctx().num_nodes(), base_nodes + 1);
1369        assert_eq!(gc.ctx().num_list_elements(), base_elems);
1370        // The pre-scope survivor is untouched.
1371        assert!(matches!(survivor, Node::NumericLiteral(n) if n.value.get() == 99.0));
1372    }
1373
1374    #[test]
1375    fn alloc_scope_nests() {
1376        let mut ctx = Context::new();
1377        let gc = GCLock::new(&mut ctx);
1378        let base = gc.ctx().num_nodes();
1379        {
1380            let _outer = unsafe { gc.alloc_scope() };
1381            num(&gc, 1.0); // 1 outer allocation
1382            {
1383                let _inner = unsafe { gc.alloc_scope() };
1384                for _ in 0..50 {
1385                    num(&gc, 0.0);
1386                }
1387            }
1388            assert_eq!(gc.ctx().num_nodes(), base + 1, "inner scope reclaimed");
1389            // Outer keeps allocating after the inner truncate (bump reuse).
1390            for _ in 0..10 {
1391                num(&gc, 0.0);
1392            }
1393            assert_eq!(gc.ctx().num_nodes(), base + 11);
1394        }
1395        assert_eq!(gc.ctx().num_nodes(), base);
1396    }
1397}