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}