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