Skip to main content

cove_runtime/
heap.rs

1//! The per-task mark-and-sweep collector.
2//!
3//! The Language Card says memory is managed by a precise, non-moving
4//! mark-and-sweep collector, and ADR 0011 narrows that to a heap per task over
5//! the values a task owns. This module is that heap.
6//!
7//! ADR 0008 gives each spawned task a thread and an [`crate::interp::Interpreter`]
8//! of its own, so a heap belongs to one interpreter and is reached only from
9//! the thread running it. That is what makes "per task" more than a
10//! convention: a task's objects are unreachable from any other thread, so a
11//! collection needs no safepoint from anyone else and takes no lock.
12//!
13//! # What the heap owns
14//!
15//! `Rc` reclaims a value the moment nothing points at it, which is correct for
16//! every Cove value that is built once and never altered — a string, an array,
17//! a map, a set, a closure, a struct, an enum case. None of those can be made
18//! to point back at something that points at them, because each is built from
19//! values that already exist. The one exception is
20//! [`crate::value::VectorStorage`]: a vector's elements are behind a
21//! `RefCell`, so `v.push(v)` is a cycle, and `Rc` alone will never free it.
22//!
23//! The heap therefore tracks exactly the objects that can form a cycle. `Rc`
24//! remains the allocation handle and still reclaims everything acyclic on its
25//! own; the collector exists for what `Rc` cannot do.
26//!
27//! The heap holds a [`Weak`] handle to each object rather than a strong one.
28//! That is not an optimisation: `freeze()` consumes *uniquely owned* vector
29//! storage and asks `Rc::strong_count` whether the caller holds the only
30//! handle, so a heap holding a strong reference would make `freeze()` fail on
31//! every vector. A `Weak` keeps the collector out of the language's own
32//! uniqueness rule, and it costs nothing — a cycle keeps itself alive, so a
33//! `Weak` to a member of one always upgrades.
34//!
35//! # Roots, and why reference counts are part of them
36//!
37//! ADR 0011 is explicit that the roots are the interpreter's own structures
38//! rather than a machine stack, so there are no stack maps here. `Roots` is a
39//! trait naming something that can *drive* a walk of one task's roots, and
40//! `SlotRoots` is its one implementor now: every binding the interpreter
41//! creates is a [`crate::interp`] `Place`, whose slot is an
42//! `Rc<RefCell<Value>>` registered in a `SlotRoots` list with the same
43//! push-and-truncate discipline the environment chain already has; the
44//! collector borrows each cell as it walks, so what it sees is what the slot
45//! holds now rather than what it held when the binding was made.
46//!
47//! Before ADR 0034 there was a second implementor: the predecessor VM had no
48//! such cells, and put every binding into one contiguous `Vec<Value>`
49//! instead, precisely so a call on that backend would allocate nothing. A
50//! collector that demanded the interpreter's shape would have made that VM
51//! build a cell per binding and give back what the arrangement bought; one
52//! that demanded the VM's would have made the interpreter snapshot its
53//! bindings into a vector, which is both a copy and a lie, since the
54//! snapshot would be of the values as they were rather than as they are. A
55//! walk rather than a structure is what let the collector work over both
56//! shapes without asking either one to give back what it was for.
57//!
58//! The linear-memory backend does not implement this trait at all. Its heap
59//! belongs to the run rather than to a task, and it finds its roots from a
60//! frame's static reference map instead — `crate::vm::mem`'s own `Roots`
61//! describes that on its own terms. Adding that backend cost this module
62//! nothing, which is the walk-not-a-list design paying for itself a second
63//! time: the point was never only the two shapes that existed when it was
64//! written.
65//!
66//! A walk is asked for twice, once to count and once to mark, so an
67//! implementation must be re-walkable and must yield the same values both
68//! times. Nothing here consumes it.
69//!
70//! The walk covers what the program has named. It does not cover a value the
71//! interpreter is holding in a Rust local while it is mid-evaluation — the
72//! left operand of a `+` whose right operand is still being evaluated. Those
73//! are the values ADR 0011 calls "values being evaluated," and a tree walker
74//! has no list of them by construction. The linear-memory backend has no
75//! such gap to close: a value it is computing already lives in a frame slot
76//! the static reference map already covers, and an object allocated before
77//! its fields are written is zeroed rather than left holding whatever
78//! preceded it, so there is nothing mid-evaluation for its collector to
79//! miss the way this one can.
80//!
81//! The collector finds them exactly, without scanning anything: it counts the
82//! references it *can* see. For every shared allocation it walks — a vector, an
83//! array, a map, a struct, a closure, a trait object, a task, a task scope —
84//! it sums the references reachable from the walked roots and from the
85//! objects it manages, and compares that with `Rc::strong_count`. A shortfall
86//! is a reference held somewhere the collector cannot read — a Rust local it
87//! has not walked — so that allocation, and everything it holds, is a root.
88//!
89//! This is the rule that makes a safepoint safe rather than merely
90//! well-chosen. A value in a Rust local is itself a reference, so a value the
91//! collector cannot see is a value whose count does not add up; there is no
92//! arrangement of locals the interpreter can reach a safepoint in that hides
93//! one. What the interpreter has to get right is therefore narrower than
94//! "have everything on a stack": it has to not walk anything twice, because a
95//! reference counted twice is a shortfall concealed. The linear-memory
96//! backend answers the same demand — a safepoint must not reach a value its
97//! collector cannot find — without this counting trick: a static reference
98//! map leaves nothing held off to the side to count.
99//!
100//! Counting the containers as well as the objects is what makes this sound
101//! rather than merely plausible. An array can hold the only reference to a
102//! vector while being held itself by a garbage cycle *and* by a temporary; if
103//! only the vector were counted, every reference to it would look accounted
104//! for — by the garbage — and the sweep would empty something the program can
105//! still reach.
106//!
107//! This is precise in the sense ADR 0001 asks for: no word is guessed to be a
108//! pointer, and no integer is ever mistaken for one. It is also the invariant
109//! that makes every other awkward case safe. A slot the interpreter has
110//! mutably borrowed cannot be read, so its references go unseen, so whatever
111//! it holds is treated as a root.
112//!
113//! # What it does not do
114//!
115//! No finalizers, no compaction, no generations, no concurrent or incremental
116//! collection, no weak references in the language. Each is out of scope in ADR
117//! 0001 and remains so in ADR 0011.
118//!
119//! # `Shared`
120//!
121//! ADR 0011 says a `Shared<T>` cell owns its contents rather than any task's
122//! heap, and collects them with the cell. That is what happens, and it needs
123//! no collector at all.
124//!
125//! A [`crate::shared::SharedCell`] holds a [`crate::task::Transfer`], not a
126//! [`Value`], and `Transfer::of` refuses a `Vector`. A cell therefore cannot
127//! hold a collectable object, so there is nothing in one for a heap to own and
128//! no way for a cycle among a task's objects to run through one. The `Arc`
129//! frees the contents with the cell, which is exactly what the ADR asks for. Each `lock` materialises
130//! a fresh `Value` for the locking task, and *that* copy is an ordinary value
131//! in that task's heap, collected there like any other.
132//!
133//! So the collector treats a `Shared` as a leaf: it never takes the cell's
134//! lock. That is not only unnecessary, it is required. `lock` holds the mutex
135//! for the whole of the closure it is given, that closure runs Cove code, and
136//! Cove code reaches safepoints — so a collector that locked a cell would
137//! sooner or later wait for a lock the collecting thread already holds.
138//!
139//! One thing this does not reach: a cell may hold *another* cell, including
140//! itself, and that is an `Arc` cycle no heap here can see. Cells are
141//! reachable from every task that was given one and outlive all of them, so
142//! collecting cycles among them would need a collector that stops every
143//! thread — which ADR 0011 rules out under "no concurrent collection". It is
144//! a real leak, and the ADR now says so under "What this leaves uncollected".
145
146use std::cell::RefCell;
147use std::collections::{HashMap, HashSet};
148use std::mem::size_of;
149use std::rc::{Rc, Weak};
150use std::time::Duration;
151
152use crate::task::{Task, TaskScope};
153use crate::value::{MapKey, Repr, Value, VectorStorage};
154use crate::wallclock::Instant;
155
156/// The fewest objects a task may allocate between two collections.
157///
158/// A collection costs a walk of the live set, so collecting after every
159/// allocation would make the collector the program. This floor is what a small
160/// program pays: it allocates fewer than this many vectors and is never
161/// collected at all.
162const MIN_ALLOCATIONS_BETWEEN_COLLECTIONS: u64 = 64;
163
164/// How much the object count may grow past the live set before the next
165/// collection.
166///
167/// Doubling makes the total collection work over a run proportional to total
168/// allocation rather than to allocation times live size, which is the standard
169/// reason to size the next collection from the last one's survivors.
170const GROWTH_FACTOR: u64 = 2;
171
172/// One task's roots, as something the collector can walk.
173///
174/// # Why a walk and not a list
175///
176/// `Interpreter` is this trait's one implementor now, giving every binding
177/// an `Rc<RefCell<Value>>` of its own and handing the collector the cells,
178/// so a collection reads what a binding holds *now* rather than a snapshot
179/// of what it held when the binding was made. Before ADR 0034 there was a
180/// second: the predecessor VM gave every binding a slot of one contiguous
181/// `Vec<Value>` instead, which was the whole reason a call on that backend
182/// allocated nothing. A collector that demanded the interpreter's shape
183/// would have made that VM build a cell per binding and give back what the
184/// arrangement bought; one that demanded the VM's would have made the
185/// interpreter snapshot its bindings into a vector, which is both a copy and
186/// a lie, since the snapshot would be of the values as they were rather than
187/// as they are.
188///
189/// So neither shape was asked for; what is asked for is a walk: an
190/// implementation calls `visit` once per reference it holds, and how it
191/// finds them is its own business. An enum of the shapes was the alternative
192/// and it was not taken, because it would put every backend's root
193/// representation in this module and make adding another an edit here
194/// rather than there — which is exactly what let the linear-memory backend
195/// add a third kind of root, in `crate::vm::mem`'s own `Roots`, without
196/// touching this file at all.
197///
198/// # What an implementation owes
199///
200/// [`Heap::collect`] walks the roots **twice** — once to count the references
201/// it can see, once to mark from them — so a walk must be repeatable and must
202/// yield the same references both times. Nothing consumes it.
203///
204/// It must also yield each reference **exactly once**. Yielding one twice is
205/// not conservative: the collector's soundness rests on comparing the
206/// references it can see against `Rc::strong_count`, so a reference counted
207/// twice makes a live allocation's count add up when it does not, and the
208/// sweep then empties something a backend temporary can still reach. Both
209/// implementations here therefore de-duplicate what they know can alias:
210/// [`SlotRoots`] by slot address, because a `var` parameter binds the
211/// caller's slot and one cell can be registered by two environments.
212///
213/// Yielding *too few* is safe in the same accounting, and is the reason the
214/// VM need not walk its constant pool: an unseen reference is a shortfall,
215/// and a shortfall is a root.
216pub(crate) trait Roots {
217    /// Calls `visit` once for every value this task holds directly.
218    fn walk(&self, visit: &mut dyn FnMut(&Value));
219}
220
221/// A slot the collector starts from: one binding's storage.
222///
223/// This is [`crate::interp`]'s `Place` slot, registered here so a collection
224/// can read it. The interpreter is the only writer, and it keeps the list in
225/// step with the environment chain: a binding pushes, and leaving a block or a
226/// call truncates back to the length it recorded on entry.
227type Slot = Rc<RefCell<Value>>;
228
229/// Every binding one interpreter currently holds, innermost last.
230///
231/// The list is shared by every environment on one thread, and its
232/// push-and-truncate discipline mirrors that thread's environment chain
233/// exactly. There is one list per interpreter and one interpreter per task, so
234/// this *is* a task's roots — nothing has to be sliced out of a larger set.
235#[derive(Default)]
236pub(crate) struct SlotRoots {
237    slots: Vec<Slot>,
238}
239
240impl SlotRoots {
241    /// An empty root set.
242    pub(crate) fn new() -> SlotRoots {
243        SlotRoots::default()
244    }
245
246    /// How many slots are registered. A caller records this before entering a
247    /// scope and hands it back to [`SlotRoots::truncate`] on the way out.
248    pub(crate) fn len(&self) -> usize {
249        self.slots.len()
250    }
251
252    /// Registers one binding's slot.
253    pub(crate) fn push(&mut self, slot: Slot) {
254        self.slots.push(slot);
255    }
256
257    /// Drops every slot registered after `len`, which is what leaving a block
258    /// or a call does.
259    pub(crate) fn truncate(&mut self, len: usize) {
260        self.slots.truncate(len);
261    }
262}
263
264impl Roots for SlotRoots {
265    /// Borrows each registered slot and yields what it holds.
266    ///
267    /// One cell may be registered twice — a `var` parameter binds the caller's
268    /// slot, so the caller's environment and the callee's both name it — and
269    /// yielding it twice would count one reference as two. The addresses
270    /// already seen are what stops that.
271    ///
272    /// A slot that cannot be borrowed is one the interpreter is writing
273    /// through. It is skipped rather than waited for, which is not a hole:
274    /// its references go uncounted, so whatever it holds is short by one and
275    /// is a root under the rule this module's documentation gives.
276    fn walk(&self, visit: &mut dyn FnMut(&Value)) {
277        let mut walked: HashSet<usize> = HashSet::new();
278        for slot in &self.slots {
279            if !walked.insert(Rc::as_ptr(slot) as usize) {
280                continue;
281            }
282            if let Ok(value) = slot.try_borrow() {
283                visit(&value);
284            }
285        }
286    }
287}
288
289/// What one collection did.
290#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
291pub struct Collection {
292    /// Objects allocated since the previous collection.
293    pub allocated: u64,
294    /// Objects the sweep reclaimed.
295    pub freed_objects: u64,
296    /// Bytes held by what the sweep reclaimed, counting each shared
297    /// allocation once and not following an edge into an object that
298    /// survived. A value the reclaimed objects shared with a survivor is
299    /// counted here even though it was not released, so this is an upper
300    /// bound; [`Collection::live_bytes`] is the measured figure.
301    pub freed_bytes: u64,
302    /// Objects still live after the sweep.
303    pub live_objects: u64,
304    /// Bytes the live set holds: every string, array, map, set, struct, enum
305    /// case and closure the live objects and the roots reach, with each
306    /// shared allocation counted once.
307    pub live_bytes: u64,
308    /// How long the program was stopped for this collection.
309    pub pause: Duration,
310}
311
312/// What a run's heaps did in total.
313///
314/// `cove run --stats` prints this, and the trace's `heap_summary` event
315/// carries it.
316#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
317pub struct HeapStats {
318    /// Collectable objects allocated over the whole run.
319    pub allocated_objects: u64,
320    /// Bytes those allocations asked for, at the size each object was born.
321    pub allocated_bytes: u64,
322    /// How many collections ran.
323    pub collections: u64,
324    /// Objects those collections reclaimed.
325    pub freed_objects: u64,
326    /// Bytes live at the most recent collection.
327    pub live_bytes: u64,
328    /// Objects live at the most recent collection.
329    pub live_objects: u64,
330    /// The largest live set any collection measured.
331    pub peak_bytes: u64,
332    /// Total time the program was stopped for collection.
333    pub pause: Duration,
334}
335
336impl HeapStats {
337    /// Folds one heap's totals into these, which is what happens when a task's
338    /// thread ends and its heap is retired into the run's.
339    ///
340    /// The live figures are not merged: a live set is a present fact about one
341    /// heap rather than a total over a run. At the end of a run every task's
342    /// heap has been swept and gone with its thread, so what is live is the
343    /// entry's own and [`crate::interp::Interpreter::heap_stats`] reads it
344    /// from there.
345    pub fn merge(&mut self, other: &HeapStats) {
346        self.allocated_objects += other.allocated_objects;
347        self.allocated_bytes += other.allocated_bytes;
348        self.collections += other.collections;
349        self.freed_objects += other.freed_objects;
350        self.peak_bytes = self.peak_bytes.max(other.peak_bytes);
351        self.pause += other.pause;
352    }
353}
354
355/// One task's heap.
356///
357/// A task owns the objects it allocates. The Language Card's task-safety rule
358/// is what makes that a language rule rather than an approximation: a vector
359/// may not cross a task boundary, so no two tasks ever hold the same one, and
360/// a task can collect without waiting for any other task to reach a safepoint.
361pub(crate) struct Heap {
362    /// Every object this task has allocated and not yet lost, keyed by the
363    /// address the object was allocated at. The address is stable while the
364    /// `Weak` lives, so it identifies the object even after `Rc` has already
365    /// reclaimed it.
366    objects: HashMap<usize, Weak<VectorStorage>>,
367    allocations_since_collection: u64,
368    next_collection_at: u64,
369    stats: HeapStats,
370}
371
372impl Heap {
373    /// An empty heap.
374    pub(crate) fn new() -> Heap {
375        Heap {
376            objects: HashMap::new(),
377            allocations_since_collection: 0,
378            next_collection_at: MIN_ALLOCATIONS_BETWEEN_COLLECTIONS,
379            stats: HeapStats::default(),
380        }
381    }
382
383    /// This heap's totals so far.
384    pub(crate) fn stats(&self) -> HeapStats {
385        self.stats
386    }
387
388    /// Whether the heap is tracking no object at all, which is what a program
389    /// that never made a vector looks like.
390    pub(crate) fn is_empty(&self) -> bool {
391        self.objects.is_empty()
392    }
393
394    /// This heap's totals so far, resetting the cumulative ones.
395    ///
396    /// A task's heap ends with its thread, and what it did is folded into the
397    /// run's totals by [`crate::runtime::Runtime::retire_heap`]. Taking the
398    /// counters rather than reading them keeps a second fold from counting the
399    /// same allocation twice. The live figures describe the heap right now and
400    /// are not counters, so they are left alone.
401    pub(crate) fn take_stats(&mut self) -> HeapStats {
402        let taken = self.stats;
403        self.stats = HeapStats {
404            live_bytes: self.stats.live_bytes,
405            live_objects: self.stats.live_objects,
406            peak_bytes: self.stats.peak_bytes,
407            ..HeapStats::default()
408        };
409        taken
410    }
411
412    /// Allocates growable vector storage owned by this heap.
413    ///
414    /// The returned `Rc` is the program's handle; the heap keeps only a
415    /// `Weak`, so the value's lifetime is still `Rc`'s to decide until a cycle
416    /// takes that decision away from it.
417    pub(crate) fn allocate(&mut self, elements: Vec<Value>) -> Rc<VectorStorage> {
418        let storage = VectorStorage::new(elements);
419        self.stats.allocated_objects += 1;
420        self.stats.allocated_bytes += object_bytes(&storage);
421        self.allocations_since_collection += 1;
422        self.objects
423            .insert(Rc::as_ptr(&storage) as usize, Rc::downgrade(&storage));
424        storage
425    }
426
427    /// Whether enough has been allocated since the last collection to be worth
428    /// another one.
429    pub(crate) fn should_collect(&self) -> bool {
430        self.allocations_since_collection >= self.next_collection_at
431    }
432
433    /// Marks from the roots and sweeps what is not marked.
434    ///
435    /// `roots` is walked twice and never consumed: see [`Roots`] for what a
436    /// walk owes, and for why the interpreter's own implementor of it is
437    /// shaped the way it is.
438    pub(crate) fn collect(&mut self, roots: &dyn Roots) -> Collection {
439        let started = Instant::now();
440
441        // An object `Rc` already reclaimed leaves a dead `Weak` behind. Drop
442        // those first so every count below is over objects that still exist.
443        self.objects.retain(|_, weak| weak.strong_count() > 0);
444
445        // Every strong count is read before anything here upgrades a handle,
446        // so the numbers describe the program's references and not the
447        // collector's.
448        let strong: HashMap<usize, usize> = self
449            .objects
450            .iter()
451            .map(|(&at, weak)| (at, weak.strong_count()))
452            .collect();
453
454        let scan = self.count_visible_references(roots);
455        let live = self.mark(roots, &scan, &strong);
456        // The scan holds a handle to everything it saw. Releasing them before
457        // the sweep keeps the collector out of the reference counts it is
458        // about to act on.
459        drop(scan);
460        let (freed_objects, freed_bytes) = self.sweep(&live.marked);
461
462        let collection = Collection {
463            allocated: self.allocations_since_collection,
464            freed_objects,
465            freed_bytes,
466            live_objects: self.objects.len() as u64,
467            live_bytes: live.bytes,
468            pause: started.elapsed(),
469        };
470
471        self.allocations_since_collection = 0;
472        self.next_collection_at =
473            (collection.live_objects * GROWTH_FACTOR).max(MIN_ALLOCATIONS_BETWEEN_COLLECTIONS);
474        self.stats.collections += 1;
475        self.stats.freed_objects += freed_objects;
476        self.stats.live_bytes = collection.live_bytes;
477        self.stats.live_objects = collection.live_objects;
478        self.stats.peak_bytes = self.stats.peak_bytes.max(collection.live_bytes);
479        self.stats.pause += collection.pause;
480        collection
481    }
482
483    /// Counts, for every shared allocation it can reach, how many references
484    /// to it the collector can see.
485    ///
486    /// It is not enough to do this for the objects the heap manages. A `Rc`
487    /// container the collector does not manage — an array, a map, a struct, a
488    /// closure, a trait object, a task — can hold the only reference to an
489    /// object while itself being held by nothing but an evaluator temporary.
490    /// Counting the container too is what catches that: its own references do
491    /// not add up either, so everything it holds is a root.
492    ///
493    /// Each allocation's contents are walked exactly once, so each physical
494    /// reference is counted exactly once and a shortfall is exactly the set of
495    /// references the collector cannot read.
496    fn count_visible_references(&self, roots: &dyn Roots) -> Scan {
497        let mut scan = Scan {
498            seen: HashMap::new(),
499        };
500        roots.walk(&mut |value| scan.count(value));
501        for weak in self.objects.values() {
502            let Some(object) = weak.upgrade() else {
503                continue;
504            };
505            // The heap's own table is not a reference, so this registers the
506            // object without sighting one.
507            scan.observe(&object);
508            if let Ok(elements) = object.elements.try_borrow() {
509                for element in elements.iter() {
510                    scan.count(element);
511                }
512            };
513        }
514        scan
515    }
516
517    /// Marks everything reachable from this task's roots and from every
518    /// object some backend temporary still holds.
519    fn mark(&self, roots: &dyn Roots, scan: &Scan, strong: &HashMap<usize, usize>) -> LiveSet {
520        let mut marker = Marker {
521            managed: &self.objects,
522            excluded: None,
523            marked: HashSet::new(),
524            walked: HashSet::new(),
525            bytes: 0,
526            work: Vec::new(),
527        };
528        roots.walk(&mut |value| marker.visit(value));
529        // Anything whose references do not add up is held from somewhere the
530        // collector cannot read — an evaluator temporary — so it is a root.
531        for (at, sighting) in &scan.seen {
532            // A managed object's count is the one snapshotted before this
533            // collection upgraded any handle; every other allocation's was
534            // read the first time it was seen, before the scan took a handle
535            // of its own.
536            let held = strong.get(at).copied().unwrap_or(sighting.strong);
537            if held > sighting.sighted {
538                marker.visit(&sighting.held);
539            }
540        }
541        // An object whose elements are borrowed right now cannot be read, and
542        // so cannot be swept either: clearing it is exactly what the borrow
543        // would forbid.
544        for (at, weak) in &self.objects {
545            if marker.marked.contains(at) {
546                continue;
547            }
548            if let Some(object) = weak.upgrade() {
549                if object.elements.try_borrow().is_err() {
550                    marker.enqueue(object);
551                }
552            };
553        }
554        marker.drain();
555        LiveSet {
556            marked: marker.marked,
557            bytes: marker.bytes,
558        }
559    }
560
561    /// Reclaims every object the mark phase did not reach.
562    ///
563    /// The two phases matter. Clearing every doomed object's elements while
564    /// the heap still holds a strong handle to all of them breaks the cycles
565    /// without any object's `Drop` running inside another's, so a long chain
566    /// is torn down iteratively rather than by a recursive drop that would
567    /// exhaust the native stack. Dropping the handles afterwards is what
568    /// actually frees the storage.
569    fn sweep(&mut self, marked: &HashSet<usize>) -> (u64, u64) {
570        let mut doomed: Vec<Rc<VectorStorage>> = Vec::new();
571        for (at, weak) in &self.objects {
572            if marked.contains(at) {
573                continue;
574            }
575            if let Some(object) = weak.upgrade() {
576                doomed.push(object);
577            }
578        }
579        if doomed.is_empty() {
580            return (0, 0);
581        }
582
583        let freed_bytes = {
584            let mut accounting = Marker {
585                managed: &self.objects,
586                excluded: Some(marked),
587                marked: HashSet::new(),
588                walked: HashSet::new(),
589                bytes: 0,
590                work: Vec::new(),
591            };
592            for object in &doomed {
593                accounting.enqueue(object.clone());
594            }
595            accounting.drain();
596            accounting.bytes
597        };
598
599        self.objects.retain(|at, _| marked.contains(at));
600        for object in &doomed {
601            if let Ok(mut elements) = object.elements.try_borrow_mut() {
602                elements.clear();
603            }
604        }
605        let freed_objects = doomed.len() as u64;
606        drop(doomed);
607        (freed_objects, freed_bytes)
608    }
609}
610
611impl Default for Heap {
612    fn default() -> Heap {
613        Heap::new()
614    }
615}
616
617/// What one mark phase found: which objects are live, and how much storage the
618/// values it reached hold.
619struct LiveSet {
620    marked: HashSet<usize>,
621    bytes: u64,
622}
623
624/// One shared allocation the collector saw, and how completely it saw it.
625struct Sighting {
626    /// A handle to walk it again in the mark phase, taken the first time it
627    /// was seen.
628    held: Value,
629    /// References to it the collector could read.
630    sighted: usize,
631    /// References to it that exist, read before the handle above was taken.
632    strong: usize,
633}
634
635/// Counts the references to every shared allocation the collector can reach.
636struct Scan {
637    seen: HashMap<usize, Sighting>,
638}
639
640impl Scan {
641    /// Records one reference to the allocation at `at`, and reports whether
642    /// this was the first time the collector saw it — which is when its
643    /// contents still need walking.
644    fn sight(&mut self, at: usize, strong: usize, held: impl FnOnce() -> Value) -> bool {
645        match self.seen.get_mut(&at) {
646            Some(sighting) => {
647                sighting.sighted += 1;
648                false
649            }
650            None => {
651                self.seen.insert(
652                    at,
653                    Sighting {
654                        held: held(),
655                        sighted: 1,
656                        strong,
657                    },
658                );
659                true
660            }
661        }
662    }
663
664    /// Registers a managed object without counting a reference to it: the
665    /// heap's own table is a `Weak`, not a reference the program holds.
666    fn observe(&mut self, object: &Rc<VectorStorage>) {
667        let at = Rc::as_ptr(object) as usize;
668        if let std::collections::hash_map::Entry::Vacant(slot) = self.seen.entry(at) {
669            slot.insert(Sighting {
670                held: Value(Repr::Vector(object.clone())),
671                sighted: 0,
672                // A managed object's real count is snapshotted by
673                // `Heap::collect` before anything upgrades a handle; this
674                // one is never used for a managed object.
675                strong: 0,
676            });
677        }
678    }
679
680    /// Counts one reference for every shared allocation `value` names, and
681    /// walks the contents of each the first time it is seen.
682    ///
683    /// A managed object's contents are not walked here: the heap's table
684    /// enumerates every object, so walking from a reference as well would
685    /// count its outgoing references twice.
686    fn count(&mut self, value: &Value) {
687        match value {
688            Value(Repr::Vector(storage)) => {
689                self.sight(
690                    Rc::as_ptr(storage) as usize,
691                    Rc::strong_count(storage),
692                    || value.clone(),
693                );
694            }
695            Value(Repr::Array(items)) => {
696                if self.sight(array_addr(items), Rc::strong_count(items), || value.clone()) {
697                    for item in items.iter() {
698                        self.count(item);
699                    }
700                }
701            }
702            Value(Repr::Map(entries)) => {
703                if self.sight(
704                    Rc::as_ptr(entries) as usize,
705                    Rc::strong_count(entries),
706                    || value.clone(),
707                ) {
708                    for entry in entries.values() {
709                        self.count(entry);
710                    }
711                }
712            }
713            Value(Repr::Closure(closure)) => {
714                if self.sight(
715                    Rc::as_ptr(closure) as usize,
716                    Rc::strong_count(closure),
717                    || value.clone(),
718                ) {
719                    for (_, captured) in &closure.captures {
720                        self.count(captured);
721                    }
722                }
723            }
724            Value(Repr::Dyn(wrapped)) => {
725                if self.sight(
726                    Rc::as_ptr(wrapped) as usize,
727                    Rc::strong_count(wrapped),
728                    || value.clone(),
729                ) {
730                    self.count(&wrapped.value);
731                }
732            }
733            Value(Repr::Task(task)) => self.count_task(task),
734            Value(Repr::TaskScope(scope)) => {
735                if self.sight(Rc::as_ptr(scope) as usize, Rc::strong_count(scope), || {
736                    value.clone()
737                }) {
738                    if let Ok(tasks) = scope.tasks.try_borrow() {
739                        for task in tasks.iter() {
740                            self.count_task(task);
741                        }
742                    };
743                }
744            }
745            // A `Struct` is an `Rc` — it has been since issue #104, so that a
746            // non-mutating method call would stop copying its receiver — so
747            // two paths can reach one, and its fields are walked on the first
748            // sighting only. Walking them once per path would count every
749            // reference inside it twice, and a reference counted twice is a
750            // shortfall concealed.
751            Value(Repr::Struct(structure)) => {
752                if self.sight(
753                    Rc::as_ptr(structure) as usize,
754                    Rc::strong_count(structure),
755                    || value.clone(),
756                ) {
757                    for (_, field) in &structure.fields {
758                        self.count(field);
759                    }
760                }
761            }
762            // An `Enum` is `Box`ed, so it is owned by exactly one value and
763            // no two paths reach the same one. That is still true of the
764            // payload issue #183 moved inside the box: a `value::Payload`
765            // owns its values outright, whether they sit in the `EnumValue`
766            // or in the slice a longer one points at, so walking it yields
767            // each reference exactly once — which is the property this
768            // module's documentation says the whole accounting rests on.
769            Value(Repr::Enum(enumeration)) => {
770                for item in &enumeration.payload {
771                    self.count(item);
772                }
773            }
774            // A `Shared` is a leaf. Its cell holds a `Transfer`, which no
775            // `Vector` can be part of, so no reference to a managed object
776            // hides in one — and reading it would mean taking a lock the
777            // collecting thread may already hold.
778            Value(Repr::Shared(_)) => {}
779            // A `Set`'s elements are `MapKey`s, which no mutable handle can
780            // be, so no reference hides in one. A resource handle is a name
781            // the host resolves, so it owns no Cove object either. A bound
782            // host operation is an `Rc` — it has been since issue #114 — but
783            // of two names rather than of any `Value`, so nothing hides
784            // behind that pointer either. Every remaining case is a scalar, a
785            // string, or a range.
786            _ => {}
787        }
788    }
789
790    fn count_task(&mut self, task: &Rc<Task>) {
791        let first = self.sight(Rc::as_ptr(task) as usize, Rc::strong_count(task), || {
792            Value(Repr::Task(Rc::clone(task)))
793        });
794        if !first {
795            return;
796        }
797        // A task's body went to its own thread as a `Transfer` and is not
798        // reachable from the handle, so the value it settled with is all a
799        // handle holds.
800        if let Ok(state) = task.state.try_borrow() {
801            if let crate::task::TaskState::Settled(value) = &*state {
802                self.count(value);
803            }
804        };
805    }
806}
807
808/// Marks a live set and measures it.
809struct Marker<'h> {
810    /// The objects this heap manages. A vector allocated elsewhere — by a
811    /// host, or by a test building a value directly — is not this heap's to
812    /// mark or to free.
813    managed: &'h HashMap<usize, Weak<VectorStorage>>,
814    /// Objects to stop at, used when measuring what a sweep released: the
815    /// survivors are not part of what was freed.
816    excluded: Option<&'h HashSet<usize>>,
817    marked: HashSet<usize>,
818    walked: HashSet<usize>,
819    bytes: u64,
820    work: Vec<Rc<VectorStorage>>,
821}
822
823impl Marker<'_> {
824    /// Marks `object` and queues its contents.
825    fn enqueue(&mut self, object: Rc<VectorStorage>) {
826        let at = Rc::as_ptr(&object) as usize;
827        if self.excluded.is_some_and(|set| set.contains(&at)) {
828            return;
829        }
830        if self.marked.insert(at) {
831            self.work.push(object);
832        }
833    }
834
835    /// Walks the queue until nothing is left.
836    ///
837    /// Managed objects go through this queue rather than through recursion,
838    /// because a chain of vectors is as long as the program made it and
839    /// recursion over one would be bounded by the native stack.
840    fn drain(&mut self) {
841        while let Some(object) = self.work.pop() {
842            self.bytes += object_bytes(&object);
843            // An object whose elements are borrowed right now cannot be read,
844            // and does not need to be: its references went uncounted, so
845            // everything it holds is already a root.
846            if let Ok(elements) = object.elements.try_borrow() {
847                for element in elements.iter() {
848                    self.visit(element);
849                }
850            }
851        }
852    }
853
854    /// Marks every managed object `value` reaches, and adds the storage
855    /// `value` itself holds to the live total.
856    fn visit(&mut self, value: &Value) {
857        match value {
858            // A vector this heap does not manage — another task's, or one
859            // built outside the interpreter — is neither its to mark nor its
860            // to measure.
861            Value(Repr::Vector(storage))
862                if self.managed.contains_key(&(Rc::as_ptr(storage) as usize)) =>
863            {
864                self.enqueue(storage.clone());
865            }
866            Value(Repr::Str(text)) => {
867                if self.walked.insert(text.as_ptr() as usize) {
868                    self.bytes += text.len() as u64;
869                }
870            }
871            Value(Repr::Array(items)) => {
872                if self.walked.insert(array_addr(items)) {
873                    self.bytes += (items.len() * size_of::<Value>()) as u64;
874                    for item in items.iter() {
875                        self.visit(item);
876                    }
877                }
878            }
879            Value(Repr::Map(entries)) => {
880                if self.walked.insert(Rc::as_ptr(entries) as usize) {
881                    for (key, entry) in entries.iter() {
882                        self.bytes += key_bytes(key) + size_of::<Value>() as u64;
883                        self.visit(entry);
884                    }
885                }
886            }
887            Value(Repr::Set(items)) => {
888                if self.walked.insert(Rc::as_ptr(items) as usize) {
889                    for item in items.iter() {
890                        self.bytes += key_bytes(item);
891                    }
892                }
893            }
894            // A `Struct` is an `Rc`, so two paths can reach the same one;
895            // walk its fields on the first sighting only, exactly as every
896            // other `Rc` container above does. Without the guard, a struct
897            // reached twice has its header and its field names added to
898            // `live_bytes` twice, and a chain of structs each holding two
899            // references to the one below is walked in time exponential in
900            // the chain's length.
901            Value(Repr::Struct(structure)) => {
902                if self.walked.insert(Rc::as_ptr(structure) as usize) {
903                    self.bytes += size_of::<crate::value::StructValue>() as u64;
904                    for (name, field) in &structure.fields {
905                        self.bytes += (name.len() + size_of::<Value>()) as u64;
906                        self.visit(field);
907                    }
908                }
909            }
910            // An `Enum` is `Box`ed, so it is owned by exactly one value and
911            // no two paths reach the same one.
912            //
913            // Its payload is a `value::Payload`, which since issue #183
914            // holds the arities that occur — none and one — inside the
915            // `EnumValue` itself and allocates only from two upwards. So the
916            // slots are charged by asking the payload where they live rather
917            // than by counting them: an inline slot is already inside the
918            // `size_of::<EnumValue>()` charged above, and charging it again
919            // would say a `Some(x)` costs a `Value` more than it does.
920            Value(Repr::Enum(enumeration)) => {
921                self.bytes += size_of::<crate::value::EnumValue>() as u64;
922                if let crate::value::Payload::Many(items) = &enumeration.payload {
923                    self.bytes += (items.len() * size_of::<Value>()) as u64;
924                }
925                for item in &enumeration.payload {
926                    self.visit(item);
927                }
928            }
929            Value(Repr::Closure(closure)) => {
930                if self.walked.insert(Rc::as_ptr(closure) as usize) {
931                    self.bytes += size_of::<crate::value::Closure>() as u64;
932                    for (name, captured) in &closure.captures {
933                        self.bytes += (name.len() + size_of::<Value>()) as u64;
934                        self.visit(captured);
935                    }
936                }
937            }
938            Value(Repr::Dyn(wrapped)) => {
939                if self.walked.insert(Rc::as_ptr(wrapped) as usize) {
940                    self.bytes += size_of::<crate::value::DynValue>() as u64;
941                    self.visit(&wrapped.value);
942                }
943            }
944            Value(Repr::Task(task)) => self.visit_task(task),
945            // A `Shared`'s contents belong to the cell, not to this task, so
946            // they are neither marked nor measured here; see this module's
947            // documentation for why the lock is never taken.
948            Value(Repr::Shared(_)) => {}
949            Value(Repr::TaskScope(scope)) if self.walked.insert(Rc::as_ptr(scope) as usize) => {
950                self.bytes += size_of::<TaskScope>() as u64;
951                // A scope this thread is mid-way through mutating cannot be
952                // read, so its tasks go unsighted and the shortfall rule
953                // roots them.
954                if let Ok(tasks) = scope.tasks.try_borrow() {
955                    for task in tasks.iter() {
956                        self.visit_task(task);
957                    }
958                }
959            }
960            _ => {}
961        }
962    }
963
964    fn visit_task(&mut self, task: &Rc<Task>) {
965        if !self.walked.insert(Rc::as_ptr(task) as usize) {
966            return;
967        }
968        self.bytes += size_of::<Task>() as u64;
969        if let Ok(state) = task.state.try_borrow() {
970            if let crate::task::TaskState::Settled(value) = &*state {
971                self.visit(value);
972            }
973        }
974    }
975}
976
977/// The address of an array's shared storage.
978///
979/// `Rc<[Value]>` is a fat pointer, so it is narrowed to the address of its
980/// first element, which identifies the allocation just as well.
981fn array_addr(items: &Rc<[Value]>) -> usize {
982    Rc::as_ptr(items) as *const Value as usize
983}
984
985/// The storage one object holds for itself: its header and its element slots.
986///
987/// What each element points at is counted separately, once, however many
988/// elements point at it.
989fn object_bytes(storage: &VectorStorage) -> u64 {
990    let elements = storage
991        .elements
992        .try_borrow()
993        .map(|elements| elements.len())
994        .unwrap_or(0);
995    (size_of::<VectorStorage>() + elements * size_of::<Value>()) as u64
996}
997
998/// The storage a map key or set element holds.
999fn key_bytes(key: &MapKey) -> u64 {
1000    let own = size_of::<MapKey>() as u64;
1001    own + match key {
1002        MapKey::Str(text) => text.len() as u64,
1003        MapKey::EnumCase(type_name, case, payload) => {
1004            (type_name.len() + case.len()) as u64 + payload.iter().map(key_bytes).sum::<u64>()
1005        }
1006        MapKey::Struct(type_name, fields, _) => {
1007            type_name.len() as u64
1008                + fields
1009                    .iter()
1010                    .map(|(name, field)| name.len() as u64 + key_bytes(field))
1011                    .sum::<u64>()
1012        }
1013        MapKey::Array(items) => items.iter().map(key_bytes).sum(),
1014        MapKey::Set(items) => items.iter().map(key_bytes).sum(),
1015        MapKey::Map(entries) => entries
1016            .iter()
1017            .map(|(key, value)| key_bytes(key) + key_bytes(value))
1018            .sum(),
1019        _ => 0,
1020    }
1021}
1022
1023#[cfg(test)]
1024mod tests {
1025    use super::*;
1026    use crate::value::StructValue;
1027
1028    /// Registers `value` as a binding and returns the slot, so a test can drop
1029    /// the root later.
1030    fn root(roots: &mut SlotRoots, value: Value) -> Slot {
1031        let slot = Rc::new(RefCell::new(value));
1032        roots.push(slot.clone());
1033        slot
1034    }
1035
1036    #[test]
1037    fn an_unreachable_object_is_reclaimed() {
1038        let roots = SlotRoots::new();
1039        let mut heap = Heap::new();
1040        let storage = heap.allocate(vec![Value(Repr::Int(1))]);
1041        drop(storage);
1042        let collected = heap.collect(&roots);
1043        // `Rc` already freed it, so there was nothing left for the sweep to
1044        // free; the heap still stops tracking it.
1045        assert_eq!(collected.live_objects, 0);
1046    }
1047
1048    #[test]
1049    fn a_reachable_object_survives() {
1050        let mut roots = SlotRoots::new();
1051        let mut heap = Heap::new();
1052        let storage = heap.allocate(vec![Value(Repr::Int(1))]);
1053        let _slot = root(&mut roots, Value(Repr::Vector(storage)));
1054        let collected = heap.collect(&roots);
1055        assert_eq!(collected.live_objects, 1);
1056        assert_eq!(collected.freed_objects, 0);
1057    }
1058
1059    /// The whole reason for the collector: two objects that point at each
1060    /// other keep each other's reference count above zero forever.
1061    #[test]
1062    fn a_cycle_is_reclaimed() {
1063        let roots = SlotRoots::new();
1064        let mut heap = Heap::new();
1065        let a = heap.allocate(Vec::new());
1066        let b = heap.allocate(Vec::new());
1067        a.elements.borrow_mut().push(Value(Repr::Vector(b.clone())));
1068        b.elements.borrow_mut().push(Value(Repr::Vector(a.clone())));
1069        let weak = Rc::downgrade(&a);
1070        drop(a);
1071        drop(b);
1072
1073        assert!(weak.upgrade().is_some(), "`Rc` cannot free a cycle");
1074        let collected = heap.collect(&roots);
1075        assert_eq!(collected.freed_objects, 2);
1076        assert_eq!(collected.live_objects, 0);
1077        assert!(weak.upgrade().is_none(), "the cycle was not freed");
1078    }
1079
1080    #[test]
1081    fn a_reachable_cycle_survives() {
1082        let mut roots = SlotRoots::new();
1083        let mut heap = Heap::new();
1084        let a = heap.allocate(Vec::new());
1085        a.elements.borrow_mut().push(Value(Repr::Vector(a.clone())));
1086        let _slot = root(&mut roots, Value(Repr::Vector(a.clone())));
1087        drop(a);
1088        let collected = heap.collect(&roots);
1089        assert_eq!(collected.freed_objects, 0);
1090        assert_eq!(collected.live_objects, 1);
1091    }
1092
1093    /// A cycle whose back edge runs through a struct field is still a cycle.
1094    #[test]
1095    fn a_cycle_through_a_struct_field_is_reclaimed() {
1096        let roots = SlotRoots::new();
1097        let mut heap = Heap::new();
1098        let object = heap.allocate(Vec::new());
1099        object
1100            .elements
1101            .borrow_mut()
1102            .push(Value(Repr::Struct(Rc::new(StructValue {
1103                type_name: "test.Node".into(),
1104                fields: vec![("next".into(), Value(Repr::Vector(object.clone())))],
1105                opaque: false,
1106            }))));
1107        let weak = Rc::downgrade(&object);
1108        drop(object);
1109        assert!(weak.upgrade().is_some());
1110        assert_eq!(heap.collect(&roots).freed_objects, 1);
1111        assert!(weak.upgrade().is_none());
1112    }
1113
1114    /// An object held only by a value the collector cannot read — an
1115    /// evaluator temporary, here modelled by a plain Rust local — is a root,
1116    /// found by comparing the references the collector can see with the
1117    /// reference count.
1118    #[test]
1119    fn an_object_held_only_by_a_temporary_is_a_root() {
1120        let roots = SlotRoots::new();
1121        let mut heap = Heap::new();
1122        let held = heap.allocate(vec![Value(Repr::Int(1))]);
1123        let collected = heap.collect(&roots);
1124        assert_eq!(collected.freed_objects, 0);
1125        assert_eq!(collected.live_objects, 1);
1126        assert_eq!(held.elements.borrow().len(), 1, "its contents survived");
1127    }
1128
1129    /// The same rule, one level deeper: the temporary holds a container, and
1130    /// the object is inside it.
1131    #[test]
1132    fn an_object_inside_a_temporary_container_is_a_root() {
1133        let roots = SlotRoots::new();
1134        let mut heap = Heap::new();
1135        let inner = heap.allocate(vec![Value(Repr::Int(7))]);
1136        let weak = Rc::downgrade(&inner);
1137        let temporary = Value(Repr::Array(vec![Value(Repr::Vector(inner))].into()));
1138        let collected = heap.collect(&roots);
1139        assert_eq!(collected.freed_objects, 0);
1140        assert!(weak.upgrade().is_some());
1141        drop(temporary);
1142    }
1143
1144    /// The subtle case, and the reason the scan counts references to shared
1145    /// containers and not only to managed objects. `shared` is held by a
1146    /// garbage cycle *and* by a temporary the collector cannot read. Counting
1147    /// only the object would find every reference to it accounted for — by
1148    /// the garbage — and free the one thing the temporary can still reach.
1149    #[test]
1150    fn an_object_reached_through_a_container_a_temporary_shares_with_garbage_survives() {
1151        let roots = SlotRoots::new();
1152        let mut heap = Heap::new();
1153        let inner = heap.allocate(vec![Value(Repr::Int(7))]);
1154        let alive = Rc::downgrade(&inner);
1155        let shared: Rc<[Value]> = vec![Value(Repr::Vector(inner))].into();
1156
1157        let a = heap.allocate(Vec::new());
1158        let b = heap.allocate(Vec::new());
1159        a.elements.borrow_mut().push(Value(Repr::Vector(b.clone())));
1160        a.elements
1161            .borrow_mut()
1162            .push(Value(Repr::Array(Rc::clone(&shared))));
1163        b.elements.borrow_mut().push(Value(Repr::Vector(a.clone())));
1164        let cycle = Rc::downgrade(&a);
1165        drop(a);
1166        drop(b);
1167
1168        let collected = heap.collect(&roots);
1169        assert!(
1170            cycle.upgrade().is_none(),
1171            "the garbage cycle should have gone"
1172        );
1173        let survivor = alive
1174            .upgrade()
1175            .expect("the temporary's array still holds it");
1176        // Sweeping clears an object's elements, so this is what a wrong answer
1177        // looks like: the handle is still there and its contents are gone.
1178        assert!(
1179            survivor
1180                .elements
1181                .borrow()
1182                .first()
1183                .is_some_and(|element| element.eq_value(&Value(Repr::Int(7)))),
1184            "the sweep emptied a vector something still holds: {collected:?}"
1185        );
1186        assert_eq!(shared.len(), 1);
1187    }
1188
1189    /// The same rule again, with the container shared *twice* by the garbage
1190    /// rather than once. A `Value::Struct` is an `Rc`, so one struct value can
1191    /// be reached from two places; if the scan walked its fields once per
1192    /// path, the vector inside it would be sighted twice, its two sightings
1193    /// would account for the two references that exist — the struct's and the
1194    /// temporary's — and the shortfall that makes the temporary a root would
1195    /// not fire. The sweep would then empty a vector the temporary still
1196    /// reaches, which is the failure this whole mechanism exists to prevent.
1197    #[test]
1198    fn an_object_inside_a_struct_two_garbage_paths_share_survives() {
1199        let roots = SlotRoots::new();
1200        let mut heap = Heap::new();
1201        // The only two references to this are the struct's field and the
1202        // local, and the local is what stands for a backend temporary.
1203        let held = heap.allocate(vec![Value(Repr::Int(7))]);
1204        let structure = Rc::new(StructValue {
1205            type_name: "test.Holder".into(),
1206            fields: vec![("held".into(), Value(Repr::Vector(held.clone())))],
1207            opaque: false,
1208        });
1209
1210        // A garbage cycle whose two members both name that one struct, so the
1211        // scan reaches the struct twice without the program naming it at all.
1212        let a = heap.allocate(Vec::new());
1213        let b = heap.allocate(Vec::new());
1214        a.elements.borrow_mut().push(Value(Repr::Vector(b.clone())));
1215        b.elements.borrow_mut().push(Value(Repr::Vector(a.clone())));
1216        a.elements
1217            .borrow_mut()
1218            .push(Value(Repr::Struct(Rc::clone(&structure))));
1219        b.elements
1220            .borrow_mut()
1221            .push(Value(Repr::Struct(Rc::clone(&structure))));
1222        let cycle = Rc::downgrade(&a);
1223        drop(structure);
1224        drop(a);
1225        drop(b);
1226
1227        let collected = heap.collect(&roots);
1228        assert!(
1229            cycle.upgrade().is_none(),
1230            "the garbage cycle should have gone: {collected:?}"
1231        );
1232        assert!(
1233            held.elements
1234                .borrow()
1235                .first()
1236                .is_some_and(|element| element.eq_value(&Value(Repr::Int(7)))),
1237            "the sweep emptied a vector a temporary still holds: {collected:?}"
1238        );
1239    }
1240
1241    #[test]
1242    fn a_binding_dropped_from_the_roots_is_reclaimed() {
1243        let mut roots = SlotRoots::new();
1244        let mut heap = Heap::new();
1245        let object = heap.allocate(Vec::new());
1246        let cycle = Value(Repr::Vector(object.clone()));
1247        object.elements.borrow_mut().push(cycle);
1248        drop(object);
1249        let base = roots.len();
1250        let slot = Rc::new(RefCell::new(Value(Repr::Unit)));
1251        roots.push(slot.clone());
1252        drop(slot);
1253        roots.truncate(base);
1254        assert_eq!(heap.collect(&roots).freed_objects, 1);
1255    }
1256
1257    #[test]
1258    fn live_bytes_falls_when_a_cycle_is_reclaimed() {
1259        let roots = SlotRoots::new();
1260        let mut heap = Heap::new();
1261        let a = heap.allocate(vec![Value(Repr::Str("a fairly long string".into()))]);
1262        let b = heap.allocate(Vec::new());
1263        a.elements.borrow_mut().push(Value(Repr::Vector(b.clone())));
1264        b.elements.borrow_mut().push(Value(Repr::Vector(a.clone())));
1265
1266        let before = {
1267            // With both handles held, the cycle is rooted by the temporaries.
1268            heap.collect(&roots).live_bytes
1269        };
1270        drop(a);
1271        drop(b);
1272        let after = heap.collect(&roots).live_bytes;
1273        assert!(before > 0);
1274        assert_eq!(after, 0, "live bytes should fall to nothing: {before}");
1275    }
1276
1277    /// An object an enum case carries is reachable through the case, on both
1278    /// arms of `value::Payload`.
1279    ///
1280    /// There was no test here that built a `Value::Enum` at all, which is
1281    /// how a payload's representation could have changed under
1282    /// `Scan::count` and `Marker::visit` with nothing failing. Issue #183
1283    /// changed it — a payload of one now lives inside the `EnumValue`
1284    /// instead of in a vector beside it, and a payload of two or more in a
1285    /// boxed slice — so both arms are built here and the sweep is asked
1286    /// about each. Sweeping empties a vector's elements, so a wrong answer
1287    /// in either walker is a handle that is still there with its contents
1288    /// gone.
1289    #[test]
1290    fn an_object_an_enum_case_carries_is_reachable_through_the_case() {
1291        for arity in [1usize, 3] {
1292            let mut roots = SlotRoots::new();
1293            let mut heap = Heap::new();
1294            let held = heap.allocate(vec![Value(Repr::Int(7))]);
1295            let mut payload: Vec<Value> = vec![Value(Repr::Vector(held.clone()))];
1296            payload.resize(arity, Value(Repr::Unit));
1297            let _slot = root(
1298                &mut roots,
1299                Value(Repr::Enum(Box::new(crate::value::EnumValue {
1300                    type_name: "test.Carrier".into(),
1301                    case: "Holds".into(),
1302                    payload: payload.into(),
1303                }))),
1304            );
1305            let weak = Rc::downgrade(&held);
1306            drop(held);
1307
1308            let collected = heap.collect(&roots);
1309            let survivor = weak
1310                .upgrade()
1311                .expect("the enum case still holds the vector");
1312            assert!(
1313                survivor
1314                    .elements
1315                    .borrow()
1316                    .first()
1317                    .is_some_and(|element| element.eq_value(&Value(Repr::Int(7)))),
1318                "the sweep emptied a vector an enum case of arity {arity} \
1319                 still reaches: {collected:?}"
1320            );
1321            assert_eq!(collected.freed_objects, 0);
1322        }
1323    }
1324
1325    /// A payload of one is charged as the storage it is, and a payload of
1326    /// three as the storage *it* is.
1327    ///
1328    /// `Marker::visit` charges `size_of::<EnumValue>()` for the case and
1329    /// then, since issue #183, the payload slots only when they are a
1330    /// `Payload::Many` — because a `Payload::One`'s slot is already inside
1331    /// the `EnumValue` it just charged, and charging it again would say a
1332    /// `Some(x)` costs a `Value` more than it does. Nothing else in this
1333    /// suite reads an enum's byte contribution, and the differential suite
1334    /// strips `live_bytes` from what it compares, so this is the only place
1335    /// the arithmetic is stated.
1336    #[test]
1337    fn an_enum_case_charges_the_payload_it_actually_allocated() {
1338        let bytes_for = |arity: usize| {
1339            let mut roots = SlotRoots::new();
1340            let mut heap = Heap::new();
1341            let _slot = root(
1342                &mut roots,
1343                Value(Repr::Enum(Box::new(crate::value::EnumValue {
1344                    type_name: "test.Carrier".into(),
1345                    case: "Holds".into(),
1346                    payload: vec![Value(Repr::Int(1)); arity].into(),
1347                }))),
1348            );
1349            heap.collect(&roots).live_bytes
1350        };
1351        let case = size_of::<crate::value::EnumValue>() as u64;
1352        assert_eq!(bytes_for(0), case);
1353        assert_eq!(bytes_for(1), case);
1354        assert_eq!(bytes_for(3), case + 3 * size_of::<Value>() as u64);
1355    }
1356
1357    /// A closure is charged for its own header, its capture names, and one
1358    /// `Value` per capture — once, however many live paths reach it.
1359    ///
1360    /// Nothing in this suite built a `Value::Closure` at all, which is how
1361    /// its byte arithmetic could have gone wrong in either walker with
1362    /// nothing failing. Before ADR 0034, `tests/differential`'s `same_heap`
1363    /// did not close that gap either: it compared the interpreter against
1364    /// the predecessor VM rather than against an absolute figure, so an
1365    /// error made identically by both would have passed unseen there too.
1366    /// There is no such comparison to lean on today — the linear-memory
1367    /// backend counts bytes of a heap this one's arithmetic has nothing to
1368    /// do with — so the figure is stated here, absolutely, the way
1369    /// `an_enum_case_charges_the_payload_it_actually_allocated` states the
1370    /// enum's.
1371    ///
1372    /// Two roots rather than one because the closure arm's `walked` guard is
1373    /// what keeps a shared closure from being charged twice, and a guard
1374    /// nothing exercises is a guard that can be lost by accident.
1375    #[test]
1376    fn a_closure_charges_its_captures_once_however_many_paths_reach_it() {
1377        let mut roots = SlotRoots::new();
1378        let mut heap = Heap::new();
1379        let closure = Rc::new(crate::value::Closure {
1380            is_async: false,
1381            arity: 1,
1382            body: crate::value::ClosureBody::Tree {
1383                params: Vec::new(),
1384                block: std::sync::Arc::new(cove_syntax::ast::Block {
1385                    statements: Vec::new(),
1386                    tail: None,
1387                    span: cove_diag::Span::new(cove_diag::FileId(0), 0, 0),
1388                }),
1389                decl: None,
1390            },
1391            module: "test".into(),
1392            captures: vec![
1393                ("species".into(), Value(Repr::Int(7))),
1394                ("world".into(), Value(Repr::Unit)),
1395            ],
1396        });
1397        let _a = root(&mut roots, Value(Repr::Closure(Rc::clone(&closure))));
1398        let _b = root(&mut roots, Value(Repr::Closure(Rc::clone(&closure))));
1399        // The same reason the struct test below drops its local: an unrooted
1400        // binding is a temporary, and the shortfall rule would rightly treat
1401        // it as a third live path.
1402        drop(closure);
1403
1404        let expected = size_of::<crate::value::Closure>() as u64
1405            + ("species".len() + size_of::<Value>()) as u64
1406            + ("world".len() + size_of::<Value>()) as u64;
1407        assert_eq!(
1408            heap.collect(&roots).live_bytes,
1409            expected,
1410            "a closure reached from two roots should be charged once, for its \
1411             header and its captures"
1412        );
1413    }
1414
1415    /// An object a closure captured is reachable through the capture.
1416    ///
1417    /// `Scan::count`'s closure arm is what says so, and the sweep is what
1418    /// asks: a wrong answer there is a `Vector` handle the program still
1419    /// holds with its elements emptied out from under it. This is the shape
1420    /// `examples/life`'s `population()` has — a closure over one captured
1421    /// value, handed to `filter` — with the capture made collectable so that
1422    /// there is something for the collector to get wrong.
1423    #[test]
1424    fn an_object_a_closure_captured_is_reachable_through_the_capture() {
1425        let mut roots = SlotRoots::new();
1426        let mut heap = Heap::new();
1427        let held = heap.allocate(vec![Value(Repr::Int(7))]);
1428        let _slot = root(
1429            &mut roots,
1430            Value(Repr::Closure(Rc::new(crate::value::Closure {
1431                is_async: false,
1432                arity: 1,
1433                body: crate::value::ClosureBody::Tree {
1434                    params: Vec::new(),
1435                    block: std::sync::Arc::new(cove_syntax::ast::Block {
1436                        statements: Vec::new(),
1437                        tail: None,
1438                        span: cove_diag::Span::new(cove_diag::FileId(0), 0, 0),
1439                    }),
1440                    decl: None,
1441                },
1442                module: "test".into(),
1443                captures: vec![("held".into(), Value(Repr::Vector(held.clone())))],
1444            }))),
1445        );
1446        let weak = Rc::downgrade(&held);
1447        drop(held);
1448
1449        let collected = heap.collect(&roots);
1450        let survivor = weak.upgrade().expect("the closure still holds the vector");
1451        assert!(
1452            survivor
1453                .elements
1454                .borrow()
1455                .first()
1456                .is_some_and(|element| element.eq_value(&Value(Repr::Int(7)))),
1457            "the sweep emptied a vector a closure capture still reaches: {collected:?}"
1458        );
1459        assert_eq!(collected.freed_objects, 0);
1460    }
1461
1462    /// `Marker::visit`'s struct arm has no `walked` guard, unlike every other
1463    /// container it walks, so a struct reached from two live roots has its
1464    /// header and its field names added to `live_bytes` twice. That
1465    /// contradicts `Heap::live_bytes`'s own documented accounting: "each
1466    /// shared allocation counted once."
1467    #[test]
1468    fn a_struct_shared_by_two_live_paths_reports_its_bytes_once() {
1469        let mut roots = SlotRoots::new();
1470        let mut heap = Heap::new();
1471        let structure = Rc::new(StructValue {
1472            type_name: "test.Shared".into(),
1473            fields: vec![("value".into(), Value(Repr::Int(7)))],
1474            opaque: false,
1475        });
1476        let _a = root(&mut roots, Value(Repr::Struct(Rc::clone(&structure))));
1477        let _b = root(&mut roots, Value(Repr::Struct(Rc::clone(&structure))));
1478        // Drop the local so the only two references left are the roots' —
1479        // otherwise this binding is itself an unrooted temporary, and the
1480        // shortfall rule would (correctly) treat it as a third live path.
1481        drop(structure);
1482
1483        let collected = heap.collect(&roots);
1484        let expected =
1485            size_of::<StructValue>() as u64 + ("value".len() + size_of::<Value>()) as u64;
1486        assert_eq!(
1487            collected.live_bytes, expected,
1488            "a struct reached from two roots should be counted once, not once per root"
1489        );
1490    }
1491
1492    /// The same missing guard makes the walk exponential rather than merely
1493    /// wrong. A chain of struct values, each holding two references to the
1494    /// one below, is walked once per *path* to a struct rather than once per
1495    /// struct when nothing deduplicates it — so reaching depth `N` costs
1496    /// `2^N` visits to the bottom of the chain.
1497    ///
1498    /// Depth 30 is chosen so the two shapes are unmistakable rather than
1499    /// merely different: a walk that visits each of the 31 structs once, as
1500    /// the fix does, finishes in microseconds regardless of depth, while one
1501    /// that revisits every shared struct along every path needs on the order
1502    /// of `2^30` (roughly a billion) visits to the deepest struct alone. That
1503    /// is far past anything a slow-but-linear walk could rack up by
1504    /// accident, so a timeout firing here can only mean the exponential
1505    /// shape came back, not that the machine running the suite is loaded.
1506    ///
1507    /// The whole heap, and every `Rc` it touches, is built on a spawned
1508    /// thread and never leaves it — only the resulting byte count, a `u64`,
1509    /// crosses back over the channel — because `Rc` is not `Send` and cannot
1510    /// be moved across the boundary a wall-clock bound needs. Not joining
1511    /// that thread is deliberate: if the walk is exponential again, the
1512    /// `recv_timeout` below still reports the failure promptly, and the
1513    /// still-running thread is simply killed when the test process exits,
1514    /// rather than hanging the suite.
1515    #[test]
1516    fn a_chain_of_shared_structs_is_walked_without_exponential_blowup() {
1517        const DEPTH: usize = 30;
1518        let (tx, rx) = std::sync::mpsc::channel();
1519        std::thread::spawn(move || {
1520            let mut roots = SlotRoots::new();
1521            let mut heap = Heap::new();
1522
1523            let mut current = Value(Repr::Struct(Rc::new(StructValue {
1524                type_name: "test.Level0".into(),
1525                fields: Vec::new(),
1526                opaque: false,
1527            })));
1528            for level in 1..=DEPTH {
1529                current = Value(Repr::Struct(Rc::new(StructValue {
1530                    type_name: format!("test.Level{level}").into(),
1531                    fields: vec![("a".into(), current.clone()), ("b".into(), current)],
1532                    opaque: false,
1533                })));
1534            }
1535            let _slot = root(&mut roots, current);
1536
1537            let collected = heap.collect(&roots);
1538            // The sender is dropped along with this thread whether or not the
1539            // receiver is still listening; a failed send just means the main
1540            // thread already gave up and timed out.
1541            let _ = tx.send(collected.live_bytes);
1542        });
1543
1544        let live_bytes = rx.recv_timeout(std::time::Duration::from_secs(10)).expect(
1545            "collection did not finish in time — the struct arm is walking \
1546                 the chain exponentially again",
1547        );
1548
1549        let mut expected = size_of::<StructValue>() as u64;
1550        for _ in 1..=DEPTH {
1551            expected += size_of::<StructValue>() as u64 + 2 * (1 + size_of::<Value>()) as u64;
1552        }
1553        assert_eq!(
1554            live_bytes, expected,
1555            "each struct in the chain should be counted once, not once per path to it"
1556        );
1557    }
1558
1559    /// A closure's own bytes, its captures' names and slots, and what the
1560    /// captures reach — counted once however many live paths reach the
1561    /// closure.
1562    ///
1563    /// `Marker::visit`'s closure arm has the `walked` guard the struct arm
1564    /// was missing, so this passes today. It is here because nothing checked
1565    /// it: `Value::Closure` was reached by exactly one test in this module
1566    /// and that test asks about survival rather than about bytes, and before
1567    /// ADR 0034 the predecessor's own `vm::tests::heap`'s `same_heap` compared
1568    /// the interpreter against it rather than against an absolute, so an
1569    /// error made identically on both would have gone unseen there too.
1570    #[test]
1571    fn a_closure_shared_by_two_live_paths_reports_its_bytes_once() {
1572        let mut roots = SlotRoots::new();
1573        let mut heap = Heap::new();
1574        let closure = Rc::new(crate::value::Closure {
1575            is_async: false,
1576            arity: 1,
1577            body: crate::value::ClosureBody::Tree {
1578                params: Vec::new(),
1579                block: std::sync::Arc::new(cove_syntax::ast::Block {
1580                    statements: Vec::new(),
1581                    tail: None,
1582                    span: cove_diag::Span::new(cove_diag::FileId(0), 0, 0),
1583                }),
1584                decl: None,
1585            },
1586            module: "test".into(),
1587            captures: vec![("held".into(), Value::string("seven"))],
1588        });
1589        let _a = root(&mut roots, Value(Repr::Closure(Rc::clone(&closure))));
1590        let _b = root(&mut roots, Value(Repr::Closure(Rc::clone(&closure))));
1591        // As in the struct case above: the local would otherwise be an
1592        // unrooted temporary, and the shortfall rule would correctly make it
1593        // a third live path.
1594        drop(closure);
1595
1596        let collected = heap.collect(&roots);
1597        let expected = size_of::<crate::value::Closure>() as u64
1598            + ("held".len() + size_of::<Value>()) as u64
1599            + "seven".len() as u64;
1600        assert_eq!(
1601            collected.live_bytes, expected,
1602            "a closure reached from two roots should be counted once, not once per root"
1603        );
1604    }
1605
1606    /// An enum case is billed for the box it is, plus the payload slots it
1607    /// actually allocates — and a payload of one allocates none.
1608    ///
1609    /// Issue #183 put the arities an ordinary program builds inside the
1610    /// `EnumValue` itself, so a `Some(x)` is one allocation rather than two,
1611    /// and `Marker::visit` charges only a `Payload::Many` for its slice.
1612    /// ADR 0028 decision 2 says the prototype records whether a heap
1613    /// representation gives that win back before it becomes the default,
1614    /// which needs the present figure written down rather than assumed.
1615    #[test]
1616    fn an_enum_case_is_billed_for_the_payload_it_actually_allocates() {
1617        let one = {
1618            let mut roots = SlotRoots::new();
1619            let mut heap = Heap::new();
1620            let _slot = root(
1621                &mut roots,
1622                Value(Repr::Enum(Box::new(crate::value::EnumValue {
1623                    type_name: "Option".into(),
1624                    case: "Some".into(),
1625                    payload: crate::value::Payload::One(Value::string("seven")),
1626                }))),
1627            );
1628            heap.collect(&roots).live_bytes
1629        };
1630        assert_eq!(
1631            one,
1632            size_of::<crate::value::EnumValue>() as u64 + "seven".len() as u64,
1633            "a payload of one lives in the box that already exists"
1634        );
1635
1636        let many = {
1637            let mut roots = SlotRoots::new();
1638            let mut heap = Heap::new();
1639            let _slot = root(
1640                &mut roots,
1641                Value(Repr::Enum(Box::new(crate::value::EnumValue {
1642                    type_name: "test.Pair".into(),
1643                    case: "Both".into(),
1644                    payload: crate::value::Payload::Many(
1645                        vec![Value::string("seven"), Value::string("eight")].into(),
1646                    ),
1647                }))),
1648            );
1649            heap.collect(&roots).live_bytes
1650        };
1651        assert_eq!(
1652            many,
1653            size_of::<crate::value::EnumValue>() as u64
1654                + 2 * size_of::<Value>() as u64
1655                + ("seven".len() + "eight".len()) as u64,
1656            "a payload of two or more is a slice beside the box"
1657        );
1658    }
1659
1660    #[test]
1661    fn collections_are_spaced_by_allocation() {
1662        let mut heap = Heap::new();
1663        assert!(!heap.should_collect());
1664        for _ in 0..MIN_ALLOCATIONS_BETWEEN_COLLECTIONS {
1665            let _ = heap.allocate(Vec::new());
1666        }
1667        assert!(heap.should_collect());
1668    }
1669
1670    #[test]
1671    fn stats_accumulate_over_collections() {
1672        let roots = SlotRoots::new();
1673        let mut heap = Heap::new();
1674        let object = heap.allocate(Vec::new());
1675        let cycle = Value(Repr::Vector(object.clone()));
1676        object.elements.borrow_mut().push(cycle);
1677        drop(object);
1678        heap.collect(&roots);
1679        heap.collect(&roots);
1680        let stats = heap.stats();
1681        assert_eq!(stats.allocated_objects, 1);
1682        assert_eq!(stats.collections, 2);
1683        assert_eq!(stats.freed_objects, 1);
1684    }
1685}