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}