Skip to main content

candela_vm/
gc.rs

1// This file is derived from keel (https://github.com/horacehoff/keel),
2// Copyright 2026 Horace Hoff, licensed under the Apache License, Version 2.0.
3// It has been modified by the candela authors. See the NOTICE file.
4//! The garbage collector.
5//!
6//! Every heap value lives in one of three pools: arrays (which also hold
7//! structs, enums, function values and closure cells), maps, and strings too
8//! long to sit inside a [`Data`]. One collection covers all three, because the
9//! trace crosses between them: a list can hold a map, a map can hold a list,
10//! and either can hold a pooled string. A slot the trace does not reach goes on
11//! its pool's free list, and the next allocation in that pool reuses it.
12//!
13//! A collection is a cycle of three phases, and the collector can stop between
14//! any two steps of it and carry on later:
15//!
16//! - Idle: nothing to do. A cycle begins when a pool that has reached its
17//!   threshold is down to its reserve of free slots: enough for the program
18//!   to allocate from while the cycle marks, so the pool does not have to
19//!   grow for it. A cycle also begins at the next allocation once the program
20//!   has allocated more bytes since the last cycle than its byte threshold,
21//!   so a program that allocates few but large lists does not grow its heap
22//!   far past what it keeps. While the cycle runs, every allocation, and
23//!   every [`DEBT_BYTES`] allocated, pays for a share of its work, done a
24//!   slice at a time, so the program never stops for more than one slice.
25//! - Mark: beginning a cycle records how long each pool is, marks every slot
26//!   already on a free list, and marks what the roots hold. Marking then walks
27//!   the objects it has reached, a chunk of at most [`CHUNK`] entries at a time,
28//!   until nothing is left to walk.
29//! - Sweep: walks each pool's marks up to the length recorded at the start and
30//!   puts every unmarked slot on the free list. Then the cycle is over.
31//!
32//! Work is counted in units: one per chunk walked, one per 64 slots swept and
33//! one per slot freed. A step is given a budget in these units and stops once
34//! it has spent it.
35//!
36//! A host can do the same work while its program is idle, between frames, by
37//! calling [`GcState::collect`] with a budget of its own. That drives the same
38//! cycle the allocations drive: a cycle an allocation began is carried on by
39//! the host, and the other way round. A host that collects this way is taken
40//! to pace the collector, and the thresholds at which an allocation begins a
41//! cycle rise to leave the work to it.
42//!
43//! The program keeps running between steps, so marking is
44//! snapshot-at-the-beginning: every object reachable when the cycle began is
45//! marked before the sweep starts, whatever the program does to the heap in
46//! the meantime. Three rules keep that true:
47//!
48//! - A store that overwrites or removes a value from a list, struct, cell or
49//!   map while marking runs marks the value it takes out first (the deletion
50//!   barrier, [`shade_overwritten`]). A value the program read out of the heap
51//!   and kept only in a register is still marked that way.
52//! - A list or map whose entries move in place (a removal, a sort, a reverse)
53//!   after marking has walked part of it is walked again from the start
54//!   ([`rescan_list`], [`rescan_map`]), since an entry could have moved from
55//!   the part not yet walked into the part already walked.
56//! - Interning a string hands out a slot found by its text, which the program
57//!   may have held no reference to; the slot is marked as it is handed out.
58//!
59//! Registers and the recursion stack take no barrier. They are read once, when
60//! the cycle begins, and anything they receive later was either reachable then
61//! or allocated since.
62//!
63//! Building with the `gc-torture` feature runs a cycle on every allocation, a
64//! unit of work at a time, and checks at the end of every mark phase that a
65//! fresh trace from the roots finds nothing unmarked.
66
67use crate::data::Data;
68use crate::data::PoolString;
69use crate::vm::CandelaMap;
70use crate::vm::MapPool;
71use crate::vm::ObjectPool;
72use crate::vm::RegisterFile;
73use crate::vm::StringPool;
74
75/// The fewest slots a pool holds before its first collection, and the least a
76/// collection sets the next one's threshold to.
77pub const MIN_GC_THRESHOLD: u32 = 256;
78
79/// The most elements a freed array or map keeps room for. A freed slot holds
80/// on to its buffer so the next value written there, usually a struct or a
81/// short list, needs no allocation; a buffer larger than this goes back to the
82/// allocator instead of sitting in a dead slot until something reuses it.
83const KEEP_FREED_CAPACITY: usize = 64;
84
85/// Collect on every allocation, a unit at a time, and check every mark phase
86/// against a fresh trace. See the module documentation.
87const TORTURE: bool = cfg!(feature = "gc-torture");
88
89/// The least a threshold falls to: under torture every allocation starts a
90/// cycle.
91const THRESHOLD_FLOOR: u32 = if TORTURE { 0 } else { MIN_GC_THRESHOLD };
92
93/// Units of collector work each allocation pays for while a cycle runs. A
94/// cycle with `n` units of work finishes within `n / ALLOC_DEBT` allocations.
95const ALLOC_DEBT: u32 = 16;
96
97/// The units one allocation-driven step does: the allocations' debt is paid
98/// once it reaches this, which bounds how long one allocation stops the
99/// program.
100const SLICE: u32 = 512;
101
102/// The least the byte threshold is: the bytes a program allocates before its
103/// first collection by bytes, and the floor under every later threshold. A
104/// program whose heap stays under it is paced by its object count alone.
105pub const MIN_BYTE_THRESHOLD: usize = 8 << 20;
106
107/// Bytes allocated while a cycle runs that pay for the same share of its work
108/// as one allocation does, [`ALLOC_DEBT`] units. A program building large
109/// lists allocates few objects, and this keeps a cycle it is in from lasting
110/// for as many bytes as the whole heap holds.
111pub const DEBT_BYTES: usize = 8 << 10;
112
113/// What one list element costs in bytes.
114pub const LIST_ENTRY_BYTES: usize = size_of::<Data>();
115
116/// What one map entry costs in bytes: the key, the value, the hash the entry
117/// keeps and the index slot that finds it.
118pub const MAP_ENTRY_BYTES: usize = 2 * size_of::<Data>() + 2 * size_of::<usize>();
119
120/// The most entries of one list or map a single unit of marking walks. A large
121/// list is walked a chunk at a time, so no single step has to walk all of it.
122pub const CHUNK: u32 = 64;
123
124/// One mark bit per pool slot.
125///
126/// A bit per slot rather than a `bool` keeps the marks of a large pool in an
127/// eighth of the memory, so clearing them and walking them for the free slots
128/// touch an eighth of the cache lines.
129#[derive(Default)]
130pub struct MarkBits(Vec<u64>);
131
132impl MarkBits {
133    /// Clears every mark and sizes the set for `len` slots.
134    pub fn reset(&mut self, len: usize) {
135        self.0.clear();
136        self.0.resize(len.div_ceil(64), 0);
137    }
138    /// Whether slot `i` is marked.
139    #[inline(always)]
140    #[must_use]
141    pub fn contains(&self, i: usize) -> bool {
142        unsafe { self.0.get_unchecked(i / 64) & (1u64 << (i % 64)) != 0 }
143    }
144    /// Marks slot `i` and reports whether it was already marked.
145    #[inline(always)]
146    pub fn insert(&mut self, i: usize) -> bool {
147        let word = unsafe { self.0.get_unchecked_mut(i / 64) };
148        let bit = 1u64 << (i % 64);
149        let was = *word & bit != 0;
150        *word |= bit;
151        was
152    }
153    /// The unmarked slots among the 64 that word `w` covers, below `limit`, as
154    /// set bits.
155    #[inline(always)]
156    fn unmarked_in_word(&self, w: usize, limit: usize) -> u64 {
157        let free = !unsafe { *self.0.get_unchecked(w) };
158        let first = w * 64;
159        if limit - first >= 64 {
160            free
161        } else {
162            free & ((1u64 << (limit - first)) - 1)
163        }
164    }
165}
166
167/// The threshold the next collection of a pool runs at: `factor` times what
168/// survived this one. A pool that doubles its live set between collections
169/// pays for each collection with as many fresh allocations as the trace
170/// visited, and a pool whose live set stays flat stops growing instead of
171/// doubling on every collection whether anything survived or not.
172#[inline(always)]
173fn next_threshold(pool_len: usize, freed: usize, factor: usize) -> u32 {
174    if TORTURE {
175        return 0;
176    }
177    let live = pool_len - freed;
178    (live.saturating_mul(factor).min(u32::MAX as usize) as u32).max(MIN_GC_THRESHOLD)
179}
180
181/// How far past its live set a pool grows before an allocation begins a cycle,
182/// for a program nobody else collects for.
183const GROWTH: usize = 2;
184
185/// The same, once the host collects between frames: the allocations leave room
186/// for the host to do the work first, and begin a cycle themselves only when
187/// it falls behind.
188const HOST_PACED_GROWTH: usize = 4;
189
190/// The share of a pool its free slots have to fall below before an idle
191/// collection begins a cycle for it: a pool with more free than this has room
192/// to allocate in without growing.
193const IDLE_FREE_SHARE: usize = 8;
194
195/// What the collector has done over a set of pools, and how large they are.
196///
197/// Work is counted in the units a budget is given in, never in time: the count
198/// is the same on every machine and every run, and a host that wants time
199/// measures its own calls.
200#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
201pub struct GcStats {
202    /// How many collection cycles have begun.
203    pub cycles: u64,
204    /// Every unit of collection work done, across all cycles.
205    pub units: u64,
206    /// The most units one step did: the longest the collector held up the
207    /// program at once, whether an allocation or the host asked for the step.
208    pub largest_slice: u32,
209    /// The object pool: lists, structs, enum values, functions and closure
210    /// cells.
211    pub arrays: PoolStats,
212    /// The map pool.
213    pub maps: PoolStats,
214    /// The pool of strings too long to sit inside a value.
215    pub strings: PoolStats,
216}
217
218/// How large one pool is.
219#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
220pub struct PoolStats {
221    /// Every slot the pool holds, live or free.
222    pub len: usize,
223    /// The slots on its free list, waiting for an allocation to reuse them.
224    pub free: usize,
225}
226
227/// An object the trace has reached and not yet walked to its end: the list or
228/// map, and the entry the next chunk starts at.
229#[derive(Clone, Copy)]
230struct Grey {
231    id: u32,
232    from: u32,
233    map: bool,
234}
235
236/// Where a cycle stands.
237#[derive(Clone, Copy, PartialEq, Eq)]
238enum Phase {
239    Idle,
240    Mark,
241    /// Sweeping the pool named, arrays first, then maps, then strings.
242    Sweep(Swept),
243}
244
245#[derive(Clone, Copy, PartialEq, Eq)]
246enum Swept {
247    Arrays,
248    Maps,
249    Strings,
250}
251
252/// The marks of the two object pools and the objects still to walk.
253///
254/// Every slot at or past a pool's limit was allocated after the cycle began,
255/// and counts as marked without a bit: nothing a cycle allocates is freed by
256/// it. The string pool keeps its own marks, see [`StringPool::shade`].
257#[derive(Default)]
258struct Tracer {
259    array_marks: MarkBits,
260    map_marks: MarkBits,
261    array_limit: u32,
262    map_limit: u32,
263    work: Vec<Grey>,
264    /// The bytes the lists, maps and pooled strings marked so far hold.
265    live_bytes: usize,
266}
267
268impl Tracer {
269    /// Marks what `d` refers to, queueing a list or map it marks for its
270    /// entries to be walked.
271    #[inline(always)]
272    fn shade(&mut self, strings: &mut StringPool, d: Data) {
273        if d.is_map() {
274            let id = d.as_map() as u32;
275            if id < self.map_limit && !self.map_marks.insert(id as usize) {
276                self.work.push(Grey {
277                    id,
278                    from: 0,
279                    map: true,
280                });
281            }
282        } else if d.is_array() || d.is_struct() || d.is_enum() {
283            let id = d.as_array() as u32;
284            if id < self.array_limit && !self.array_marks.insert(id as usize) {
285                self.work.push(Grey {
286                    id,
287                    from: 0,
288                    map: false,
289                });
290            }
291        } else if d.is_large_str() {
292            self.live_bytes += strings.shade_counted(d.get_str_pool_id());
293        }
294    }
295
296    /// Walks the next chunk of `g`, queueing the rest of it when it is longer.
297    ///
298    /// Each entry is tested on its own, never read as a verdict on its
299    /// neighbours: a list or map typed `any`, which is what a parsed json
300    /// document is made of, mixes scalars and objects freely.
301    #[inline(always)]
302    fn walk(&mut self, objs: &ObjectPool, maps: &MapPool, strings: &mut StringPool, g: Grey) {
303        let from = g.from as usize;
304        if g.map {
305            let map = &maps[g.id as usize];
306            if from == 0 {
307                self.live_bytes += map.capacity() * MAP_ENTRY_BYTES;
308            }
309            let end = map.len().min(from + CHUNK as usize);
310            if let Some(entries) = map.get_range(from..end) {
311                for (k, v) in entries {
312                    self.shade(strings, *k);
313                    self.shade(strings, *v);
314                }
315            }
316            if end < map.len() {
317                self.work.push(Grey {
318                    from: end as u32,
319                    ..g
320                });
321            }
322        } else {
323            let list = &objs[g.id as usize];
324            if from == 0 {
325                self.live_bytes += list.capacity() * LIST_ENTRY_BYTES;
326            }
327            let end = list.len().min(from + CHUNK as usize);
328            if from < end {
329                for &e in unsafe { list.get_unchecked(from..end) } {
330                    self.shade(strings, e);
331                }
332            }
333            if end < list.len() {
334                self.work.push(Grey {
335                    from: end as u32,
336                    ..g
337                });
338            }
339        }
340    }
341}
342
343/// A pool's free slots, kept in two lists so that the common allocation asks
344/// the collector nothing: it takes a slot from `ready`, and only when that is
345/// empty does it go to the collector, which decides from there.
346struct FreeSlots<'a> {
347    /// Slots an allocation takes without asking the collector.
348    ready: &'a mut Vec<u32>,
349    /// The rest: the reserve that feeds the next cycle, and every free slot
350    /// while a cycle runs, so each allocation then pays its share.
351    held: &'a mut Vec<u32>,
352}
353
354impl FreeSlots<'_> {
355    /// A free slot, ready or held.
356    fn take(&mut self) -> Option<u32> {
357        self.ready.pop().or_else(|| self.held.pop())
358    }
359    /// Holds every free slot back, so each allocation goes to the collector.
360    /// The lists trade places rather than copy when one is empty, which is the
361    /// usual case, since a pool's free list can run to most of the pool.
362    fn hold_all(&mut self) {
363        if self.held.is_empty() {
364            std::mem::swap(self.ready, self.held);
365        } else {
366            self.held.append(self.ready);
367        }
368    }
369    /// Makes every free slot ready except `reserve` of them.
370    fn release_above(&mut self, reserve: usize) {
371        if self.ready.is_empty() {
372            std::mem::swap(self.ready, self.held);
373            let keep = reserve.min(self.ready.len());
374            let from = self.ready.len() - keep;
375            self.held.extend(self.ready.drain(from..));
376        } else if self.held.len() > reserve {
377            self.ready.extend(self.held.drain(reserve..));
378        }
379    }
380}
381
382/// What the collector knows about the pools between two allocations.
383///
384/// This lives with the pools rather than with one run of the interpreter
385/// because the pools do: a host that calls into a resident program many times
386/// keeps every object the earlier calls left behind, and a collector that
387/// forgot its free lists and started its thresholds over on each call would
388/// run a full mark and sweep on the first few allocations of every call once
389/// the pools had grown past the starting threshold, and would never reuse a
390/// slot the previous call had freed. A cycle in progress carries over between
391/// calls the same way.
392pub struct GcState {
393    /// Object-pool slots (arrays, structs, enums and function values) an
394    /// allocation takes without asking the collector.
395    pub(crate) free_arrays: Vec<u32>,
396    /// Map-pool slots an allocation takes without asking the collector.
397    pub(crate) free_maps: Vec<u32>,
398    /// String-pool slots an allocation takes without asking the collector.
399    pub(crate) free_strings: Vec<u32>,
400    /// Object-pool length from which a cycle may begin.
401    array_threshold: u32,
402    /// Map-pool length from which a cycle may begin.
403    map_threshold: u32,
404    /// String-pool length from which a cycle may begin.
405    string_threshold: u32,
406    phase: Phase,
407    tracer: Tracer,
408    /// The next mark word the sweep reads in the pool it is sweeping.
409    sweep_word: usize,
410    /// The free slots below the limit of the pool being swept: those already
411    /// free when the cycle began, plus those the sweep has freed so far. The
412    /// program reuses freed slots while the sweep runs, so the free list's
413    /// length says nothing about what survived.
414    swept_free: usize,
415    /// Each pool's free slots when the cycle began: arrays, maps, strings.
416    free_at_begin: [usize; 3],
417    /// Units of work the allocations of the cycle in progress have paid for
418    /// and no step has done yet.
419    debt: u32,
420    /// How many free slots each pool keeps back for the next cycle to
421    /// allocate from while it marks. Set from how much the last cycle marked.
422    reserve: usize,
423    /// Units the cycle in progress has spent marking.
424    mark_units: usize,
425    /// How many cycles have begun.
426    cycles: u64,
427    /// Every unit of work done, across all cycles.
428    units: u64,
429    /// The most units one step has spent.
430    largest_slice: u32,
431    /// Each pool's free slots held back from the ready lists above: arrays,
432    /// maps, strings. See [`FreeSlots`].
433    held: [Vec<u32>; 3],
434    /// Each pool's occupied slots when the last cycle swept it: arrays, maps,
435    /// strings. An idle collection measures growth from here.
436    occupied_after: [usize; 3],
437    /// Whether the host has collected, which raises the thresholds.
438    host_paced: bool,
439    /// Bytes allocated since the last cycle began, as far as the allocations
440    /// that report them say: list and map buffers and pooled strings.
441    bytes: usize,
442    /// The bytes past which an allocation begins a cycle.
443    byte_threshold: usize,
444    /// The count of `bytes` at which [`note_bytes`](Self::note_bytes) next
445    /// has something to do: the threshold while no cycle runs, the next debt
446    /// step while one does.
447    byte_trigger: usize,
448    /// The bytes the last finished mark phase found live.
449    live_bytes: usize,
450}
451
452impl Default for GcState {
453    fn default() -> Self {
454        Self {
455            free_arrays: Vec::new(),
456            free_maps: Vec::new(),
457            free_strings: Vec::new(),
458            array_threshold: THRESHOLD_FLOOR,
459            map_threshold: THRESHOLD_FLOOR,
460            string_threshold: THRESHOLD_FLOOR,
461            phase: Phase::Idle,
462            tracer: Tracer::default(),
463            sweep_word: 0,
464            swept_free: 0,
465            free_at_begin: [0; 3],
466            debt: 0,
467            reserve: 0,
468            mark_units: 0,
469            cycles: 0,
470            units: 0,
471            largest_slice: 0,
472            held: [Vec::new(), Vec::new(), Vec::new()],
473            occupied_after: [0; 3],
474            host_paced: false,
475            bytes: 0,
476            byte_threshold: MIN_BYTE_THRESHOLD,
477            byte_trigger: if TORTURE {
478                usize::MAX
479            } else {
480                MIN_BYTE_THRESHOLD
481            },
482            live_bytes: 0,
483        }
484    }
485}
486
487impl GcState {
488    /// Whether this build runs a cycle on every allocation, a unit of work at
489    /// a time, which is what the `gc-torture` feature turns on.
490    pub const TORTURE: bool = TORTURE;
491
492    /// How many collections have begun over these pools.
493    #[must_use]
494    pub const fn cycles(&self) -> u64 {
495        self.cycles
496    }
497
498    /// What the collector has done over `objs`, `maps` and `strings`, the
499    /// pools this state collects.
500    #[must_use]
501    pub fn stats(&self, objs: &ObjectPool, maps: &MapPool, strings: &StringPool) -> GcStats {
502        GcStats {
503            cycles: self.cycles,
504            units: self.units,
505            largest_slice: self.largest_slice,
506            arrays: PoolStats {
507                len: objs.len(),
508                free: self.free_len(Swept::Arrays),
509            },
510            maps: PoolStats {
511                len: maps.len(),
512                free: self.free_len(Swept::Maps),
513            },
514            strings: PoolStats {
515                len: strings.len(),
516                free: self.free_len(Swept::Strings),
517            },
518        }
519    }
520
521    /// Collection work for a host to do while its program is idle: carries
522    /// the cycle in progress forward by about `budget` units, beginning one
523    /// first if the pools have grown enough since the last to be worth it.
524    /// Answers whether no collection work is left.
525    ///
526    /// `registers` are the roots. The host calls this between calls into the
527    /// program, when the registers are the only roots there are.
528    ///
529    /// Beginning a cycle reads every root at once, whatever the budget; after
530    /// that a step stops within one chunk or one mark word of the budget. A
531    /// cycle begins only when some pool has grown by half its live set (and by
532    /// at least [`MIN_GC_THRESHOLD`] slots) since the last one and has fewer
533    /// than an eighth of its slots free, or the program has allocated half as
534    /// many bytes as the last cycle found live (and at least
535    /// [`MIN_BYTE_THRESHOLD`]); otherwise the call does nothing.
536    pub fn collect(
537        &mut self,
538        objs: &mut ObjectPool,
539        maps: &mut MapPool,
540        strings: &mut StringPool,
541        registers: &[Data],
542        budget: u32,
543    ) -> bool {
544        self.host_paced = true;
545        if !self.active() && !self.worth_collecting(objs.len(), maps.len(), strings.len()) {
546            return true;
547        }
548        if budget == 0 {
549            return false;
550        }
551        let roots = [registers, &[][..]];
552        let mut spent = 0;
553        if !self.active() {
554            spent = self.begin(objs, maps, strings, roots);
555        }
556        spent += self.advance(objs, maps, strings, roots, budget);
557        // Work the host did is work the allocations no longer owe.
558        self.debt = self.debt.saturating_sub(spent);
559        self.record(spent);
560        !self.active()
561    }
562
563    /// Whether an idle collection should begin a cycle: some pool has grown
564    /// by half its live set since the last cycle and has few free slots left,
565    /// or the program has allocated half as many bytes as the last cycle found
566    /// live (and at least [`MIN_BYTE_THRESHOLD`]).
567    fn worth_collecting(&self, arrays: usize, maps: usize, strings: usize) -> bool {
568        if self.bytes >= (self.live_bytes / 2).max(MIN_BYTE_THRESHOLD) {
569            return true;
570        }
571        let lens = [arrays, maps, strings];
572        [Swept::Arrays, Swept::Maps, Swept::Strings]
573            .into_iter()
574            .enumerate()
575            .any(|(i, pool)| {
576                let free = self.free_len(pool);
577                let occupied = lens[i].saturating_sub(free);
578                let live = self.occupied_after[i];
579                let grown = occupied.saturating_sub(live);
580                grown >= (live / 2).max(MIN_GC_THRESHOLD as usize)
581                    && free < lens[i] / IDLE_FREE_SHARE
582            })
583    }
584
585    /// How many free slots `pool` has, ready and held.
586    fn free_len(&self, pool: Swept) -> usize {
587        let (ready, held) = match pool {
588            Swept::Arrays => (&self.free_arrays, &self.held[0]),
589            Swept::Maps => (&self.free_maps, &self.held[1]),
590            Swept::Strings => (&self.free_strings, &self.held[2]),
591        };
592        ready.len() + held.len()
593    }
594
595    /// The factor [`next_threshold`] grows a pool's live set by.
596    const fn growth(&self) -> usize {
597        if self.host_paced {
598            HOST_PACED_GROWTH
599        } else {
600            GROWTH
601        }
602    }
603
604    /// Whether marking is under way, which is when a store has to mark the
605    /// value it overwrites.
606    #[inline(always)]
607    #[must_use]
608    pub(crate) fn marking(&self) -> bool {
609        self.phase == Phase::Mark
610    }
611
612    /// The free slots of `pool`, ready and held.
613    fn free(&mut self, pool: Swept) -> FreeSlots<'_> {
614        let [arrays, maps, strings] = &mut self.held;
615        match pool {
616            Swept::Arrays => FreeSlots {
617                ready: &mut self.free_arrays,
618                held: arrays,
619            },
620            Swept::Maps => FreeSlots {
621                ready: &mut self.free_maps,
622                held: maps,
623            },
624            Swept::Strings => FreeSlots {
625                ready: &mut self.free_strings,
626                held: strings,
627            },
628        }
629    }
630
631    /// Whether a cycle is in progress.
632    #[inline(always)]
633    #[must_use]
634    pub(crate) fn active(&self) -> bool {
635        self.phase != Phase::Idle
636    }
637
638    /// Begins a cycle: records each pool's length, holds back and marks every
639    /// free slot, and marks what the roots hold. Answers the units it spent,
640    /// which it spends whatever the budget, since the roots have to be read
641    /// all at once. The caller adds them to the slice it records.
642    fn begin(
643        &mut self,
644        objs: &ObjectPool,
645        maps: &MapPool,
646        strings: &mut StringPool,
647        roots: [&[Data]; 2],
648    ) -> u32 {
649        debug_assert!(!self.active());
650        self.cycles += 1;
651        self.mark_units = 0;
652        self.debt = 0;
653        self.bytes = 0;
654        self.byte_trigger = if TORTURE { usize::MAX } else { DEBT_BYTES };
655        self.tracer.live_bytes = 0;
656        self.free(Swept::Arrays).hold_all();
657        self.free(Swept::Maps).hold_all();
658        self.free(Swept::Strings).hold_all();
659        self.free_at_begin = [self.held[0].len(), self.held[1].len(), self.held[2].len()];
660        let tracer = &mut self.tracer;
661        tracer.array_limit = objs.len() as u32;
662        tracer.map_limit = maps.len() as u32;
663        tracer.array_marks.reset(objs.len());
664        tracer.map_marks.reset(maps.len());
665        strings.begin_marking();
666        // A free slot is garbage already, and marking it keeps the sweep from
667        // putting it on the free list a second time.
668        for &id in &self.held[0] {
669            tracer.array_marks.insert(id as usize);
670        }
671        for &id in &self.held[1] {
672            tracer.map_marks.insert(id as usize);
673        }
674        for &id in &self.held[2] {
675            strings.shade(id as usize);
676        }
677        let mut read = self.free_at_begin.iter().sum::<usize>();
678        for root in roots {
679            read += root.len();
680            for &d in root {
681                tracer.shade(strings, d);
682            }
683        }
684        self.phase = Phase::Mark;
685        // One unit per 64 values read, and one per 64 mark words cleared.
686        let cleared = (objs.len() + maps.len() + strings.len()) / 64;
687        ((read + cleared) / 64 + 1).min(u32::MAX as usize) as u32
688    }
689
690    /// Carries the cycle forward by about `budget` units, and answers the
691    /// units spent. Marking stops at the budget; sweeping can pass it by up to
692    /// one mark word's worth of freed slots. A cycle that has not begun has
693    /// nothing to do. The caller records the slice.
694    fn advance(
695        &mut self,
696        objs: &mut ObjectPool,
697        maps: &mut MapPool,
698        strings: &mut StringPool,
699        roots: [&[Data]; 2],
700        budget: u32,
701    ) -> u32 {
702        let mut spent = 0u32;
703        while spent < budget {
704            match self.phase {
705                Phase::Idle => break,
706                Phase::Mark => {
707                    let before = spent;
708                    while spent < budget {
709                        let Some(g) = self.tracer.work.pop() else {
710                            break;
711                        };
712                        self.tracer.walk(objs, maps, strings, g);
713                        spent += 1;
714                    }
715                    self.mark_units += (spent - before) as usize;
716                    if self.tracer.work.is_empty() {
717                        if TORTURE {
718                            self.verify(objs, maps, strings, roots);
719                        }
720                        // The next cycle marks about as much as this one did,
721                        // and the program allocates from the reserve while it
722                        // does.
723                        self.reserve = (self.mark_units + SLICE as usize) / ALLOC_DEBT as usize;
724                        self.phase = Phase::Sweep(Swept::Arrays);
725                        self.sweep_word = 0;
726                        self.swept_free = self.free_at_begin[0];
727                    }
728                }
729                Phase::Sweep(pool) => {
730                    spent += self.sweep(pool, objs, maps, strings, budget - spent);
731                }
732            }
733        }
734        spent
735    }
736
737    /// Adds a step of `spent` units to the totals.
738    fn record(&mut self, spent: u32) {
739        self.units += u64::from(spent);
740        self.largest_slice = self.largest_slice.max(spent);
741    }
742
743    /// Sweeps `pool` for at most about `budget` units, moving on to the next
744    /// pool, or ending the cycle, once it is done. Answers the units spent.
745    fn sweep(
746        &mut self,
747        pool: Swept,
748        objs: &mut ObjectPool,
749        maps: &mut MapPool,
750        strings: &mut StringPool,
751        budget: u32,
752    ) -> u32 {
753        let mut spent = 0u32;
754        let (limit, marks, free) = match pool {
755            Swept::Arrays => (
756                self.tracer.array_limit as usize,
757                &self.tracer.array_marks,
758                &mut self.held[0],
759            ),
760            Swept::Maps => (
761                self.tracer.map_limit as usize,
762                &self.tracer.map_marks,
763                &mut self.held[1],
764            ),
765            Swept::Strings => (strings.mark_limit(), strings.marks(), &mut self.held[2]),
766        };
767        let words = limit.div_ceil(64);
768        // The string marks are borrowed from the pool the freed slots are
769        // emptied in, so the sweep gathers the slots first and empties them
770        // after.
771        let freed_from = free.len();
772        while self.sweep_word < words && spent < budget {
773            let w = self.sweep_word;
774            let mut unmarked = marks.unmarked_in_word(w, limit);
775            while unmarked != 0 {
776                free.push((w * 64) as u32 + unmarked.trailing_zeros());
777                unmarked &= unmarked - 1;
778                spent += 1;
779            }
780            spent += 1;
781            self.sweep_word += 1;
782        }
783        self.swept_free += free.len() - freed_from;
784        let done = self.sweep_word >= words;
785        match pool {
786            Swept::Arrays => {
787                for &id in &self.held[0][freed_from..] {
788                    let slot = objs.get_mut(id as usize);
789                    if slot.capacity() > KEEP_FREED_CAPACITY {
790                        *slot = Vec::new();
791                    }
792                }
793                if done {
794                    self.array_threshold = next_threshold(limit, self.swept_free, self.growth());
795                    self.occupied_after[0] = objs.len() - self.free_len(pool);
796                    self.phase = Phase::Sweep(Swept::Maps);
797                    self.swept_free = self.free_at_begin[1];
798                }
799            }
800            Swept::Maps => {
801                for &id in &self.held[1][freed_from..] {
802                    let slot = maps.get_mut(id as usize);
803                    if slot.capacity() > KEEP_FREED_CAPACITY {
804                        *slot = CandelaMap::default();
805                    }
806                }
807                if done {
808                    self.map_threshold = next_threshold(limit, self.swept_free, self.growth());
809                    self.occupied_after[1] = maps.len() - self.free_len(pool);
810                    self.phase = Phase::Sweep(Swept::Strings);
811                    self.swept_free = self.free_at_begin[2];
812                }
813            }
814            Swept::Strings => {
815                // A freed slot gives up its text. Interning finds a string by
816                // comparing text across the whole pool, so a freed slot that
817                // kept its text would be handed out as a live string and then
818                // overwritten by the next allocation that reuses the slot.
819                for &id in &self.held[2][freed_from..] {
820                    strings.release(id as usize);
821                }
822                if done {
823                    self.string_threshold = next_threshold(limit, self.swept_free, self.growth());
824                    self.occupied_after[2] = strings.len() - self.free_len(pool);
825                    strings.end_marking();
826                    self.finish();
827                }
828            }
829        }
830        if done {
831            self.sweep_word = 0;
832        }
833        spent
834    }
835
836    /// Ends the cycle: every free slot above each pool's reserve becomes ready
837    /// for allocations to take without asking the collector.
838    fn finish(&mut self) {
839        self.phase = Phase::Idle;
840        self.live_bytes = self.tracer.live_bytes;
841        if !TORTURE {
842            self.byte_threshold = self
843                .live_bytes
844                .saturating_mul(self.growth())
845                .max(MIN_BYTE_THRESHOLD);
846            self.byte_trigger = self.byte_threshold;
847            let reserve = self.reserve;
848            self.free(Swept::Arrays).release_above(reserve);
849            self.free(Swept::Maps).release_above(reserve);
850            self.free(Swept::Strings).release_above(reserve);
851        }
852    }
853
854    /// What an allocation does when its pool has no ready slot. With a cycle
855    /// running it adds ALLOC_DEBT units to what the program owes, and a step
856    /// pays the debt off once it reaches SLICE. With none running, a pool at
857    /// its threshold begins one, and so does any pool once the bytes allocated
858    /// since the last cycle pass the byte threshold; otherwise the pool makes
859    /// its reserve ready, since it has room to grow before it needs a cycle.
860    #[inline(never)]
861    fn on_empty(
862        &mut self,
863        pool: Swept,
864        objs: &mut ObjectPool,
865        maps: &mut MapPool,
866        strings: &mut StringPool,
867        roots: [&[Data]; 2],
868    ) {
869        if TORTURE {
870            let mut spent = 0;
871            if !self.active() {
872                spent = self.begin(objs, maps, strings, roots);
873            }
874            spent += self.advance(objs, maps, strings, roots, 1);
875            self.record(spent);
876            return;
877        }
878        if self.active() {
879            self.debt += ALLOC_DEBT;
880            if self.debt >= SLICE {
881                // Bytes can run the debt up by more than a slice at once, and
882                // what is past one slice waits for the next allocation.
883                let budget = self.debt.min(SLICE);
884                self.debt -= budget;
885                let spent = self.advance(objs, maps, strings, roots, budget);
886                self.record(spent);
887            }
888            return;
889        }
890        let (len, threshold) = match pool {
891            Swept::Arrays => (objs.len(), self.array_threshold),
892            Swept::Maps => (maps.len(), self.map_threshold),
893            Swept::Strings => (strings.len(), self.string_threshold),
894        };
895        if len >= threshold as usize || self.bytes >= self.byte_threshold {
896            let spent = self.begin(objs, maps, strings, roots);
897            self.record(spent);
898        } else {
899            self.free(pool).release_above(0);
900        }
901    }
902
903    /// Counts `n` bytes the program has just allocated: a list or map buffer
904    /// that grew, or a pooled string. Past the byte threshold the next
905    /// allocation begins a cycle; while one runs, every [`DEBT_BYTES`] add to
906    /// what the allocations owe it.
907    #[inline(always)]
908    pub(crate) fn note_bytes(&mut self, n: usize) {
909        self.bytes += n;
910        if self.bytes >= self.byte_trigger {
911            self.bytes_due();
912        }
913    }
914
915    /// [`note_bytes`](Self::note_bytes) once the count reaches its trigger.
916    #[cold]
917    #[inline(never)]
918    fn bytes_due(&mut self) {
919        if self.active() {
920            let steps = (self.bytes - self.byte_trigger) / DEBT_BYTES + 1;
921            let owed = u32::try_from(steps)
922                .unwrap_or(u32::MAX)
923                .saturating_mul(ALLOC_DEBT);
924            self.debt = self.debt.saturating_add(owed);
925            self.byte_trigger = self.bytes.saturating_add(DEBT_BYTES);
926            // The debt is paid at the next allocation, where the roots are at
927            // hand. Every free slot is held while a cycle runs, so it asks.
928            return;
929        }
930        // A cycle has to begin where the roots are at hand, which is an
931        // allocation. Holding every free slot back makes the next one ask the
932        // collector, whichever pool it is in, and it begins the cycle.
933        self.byte_trigger = usize::MAX;
934        self.free(Swept::Arrays).hold_all();
935        self.free(Swept::Maps).hold_all();
936        self.free(Swept::Strings).hold_all();
937    }
938
939    /// Checks a finished mark phase against a fresh trace from `roots`: every
940    /// object the trace reaches that the cycle is responsible for has to be
941    /// marked, or the sweep would free something the program can still read.
942    #[cold]
943    #[inline(never)]
944    fn verify(&self, objs: &ObjectPool, maps: &MapPool, strings: &StringPool, roots: [&[Data]; 2]) {
945        let t = &self.tracer;
946        let mut seen_arrays = vec![false; objs.len()];
947        let mut seen_maps = vec![false; maps.len()];
948        let mut stack: Vec<Data> = roots.iter().flat_map(|r| r.iter().copied()).collect();
949        while let Some(d) = stack.pop() {
950            if d.is_map() {
951                let id = d.as_map();
952                if std::mem::replace(&mut seen_maps[id], true) {
953                    continue;
954                }
955                assert!(
956                    id >= t.map_limit as usize || t.map_marks.contains(id),
957                    "gc-torture: map {id} is reachable but unmarked at the end of marking"
958                );
959                for (k, v) in &maps[id] {
960                    stack.push(*k);
961                    stack.push(*v);
962                }
963            } else if d.is_array() || d.is_struct() || d.is_enum() {
964                let id = d.as_array();
965                if std::mem::replace(&mut seen_arrays[id], true) {
966                    continue;
967                }
968                assert!(
969                    id >= t.array_limit as usize || t.array_marks.contains(id),
970                    "gc-torture: object {id} is reachable but unmarked at the end of marking"
971                );
972                stack.extend_from_slice(&objs[id]);
973            } else if d.is_large_str() {
974                let id = d.get_str_pool_id();
975                assert!(
976                    id >= strings.mark_limit() || strings.marks().contains(id),
977                    "gc-torture: string {id} is reachable but unmarked at the end of marking"
978                );
979            }
980        }
981    }
982}
983
984/// Allocates an empty array in the object pool, reusing a freed slot when
985/// there is one.
986#[inline(always)]
987pub fn alloc_array(
988    objs: &mut ObjectPool,
989    maps: &mut MapPool,
990    strings: &mut StringPool,
991    registers: &RegisterFile,
992    recursion_stack: &RegisterFile,
993    gc: &mut GcState,
994) -> u32 {
995    if let Some(id) = gc.free_arrays.pop() {
996        objs[id as usize].clear();
997        return id;
998    }
999    alloc_array_slow(objs, maps, strings, registers, recursion_stack, gc)
1000}
1001
1002/// [`alloc_array`] when no slot is ready.
1003#[inline(never)]
1004fn alloc_array_slow(
1005    objs: &mut ObjectPool,
1006    maps: &mut MapPool,
1007    strings: &mut StringPool,
1008    registers: &RegisterFile,
1009    recursion_stack: &RegisterFile,
1010    gc: &mut GcState,
1011) -> u32 {
1012    let roots = [&registers.0[..], &recursion_stack.0[..]];
1013    gc.on_empty(Swept::Arrays, objs, maps, strings, roots);
1014    if let Some(id) = gc.free(Swept::Arrays).take() {
1015        objs[id as usize].clear();
1016        return id;
1017    }
1018    let id = objs.len() as u32;
1019    objs.push(Vec::new());
1020    id
1021}
1022
1023/// Allocates an empty map in the map pool, reusing a freed slot when there is
1024/// one.
1025#[inline(always)]
1026pub fn alloc_map(
1027    objs: &mut ObjectPool,
1028    maps: &mut MapPool,
1029    strings: &mut StringPool,
1030    registers: &RegisterFile,
1031    recursion_stack: &RegisterFile,
1032    gc: &mut GcState,
1033) -> u32 {
1034    if let Some(id) = gc.free_maps.pop() {
1035        maps[id as usize].clear();
1036        return id;
1037    }
1038    let roots = [&registers.0[..], &recursion_stack.0[..]];
1039    gc.on_empty(Swept::Maps, objs, maps, strings, roots);
1040    if let Some(id) = gc.free(Swept::Maps).take() {
1041        maps[id as usize].clear();
1042        return id;
1043    }
1044    let id = maps.len() as u32;
1045    maps.push(CandelaMap::default());
1046    id
1047}
1048
1049/// Stores `s` in the string pool, reusing a freed slot when there is one, and
1050/// answers the slot. The caller has checked that `s` is too long to inline.
1051#[inline(always)]
1052pub fn alloc_string<S: PoolString>(
1053    s: S,
1054    objs: &mut ObjectPool,
1055    maps: &mut MapPool,
1056    strings: &mut StringPool,
1057    registers: &RegisterFile,
1058    recursion_stack: &RegisterFile,
1059    gc: &mut GcState,
1060) -> u64 {
1061    gc.note_bytes(s.str_len());
1062    let slot = match gc.free_strings.pop() {
1063        Some(id) => Some(id),
1064        None => {
1065            let roots = [&registers.0[..], &recursion_stack.0[..]];
1066            gc.on_empty(Swept::Strings, objs, maps, strings, roots);
1067            gc.free(Swept::Strings).take()
1068        }
1069    };
1070    if let Some(id) = slot {
1071        s.move_to_slot(strings.get_mut(id as usize));
1072        u64::from(id)
1073    } else {
1074        let id = strings.len() as u64;
1075        s.push_to_pool(strings);
1076        id
1077    }
1078}
1079
1080/// The pools and collector a value built outside the instruction stream
1081/// allocates through, with the roots a collection it starts reads.
1082///
1083/// Every allocation here can start or carry on a collection, so whatever the
1084/// builder holds has to be reachable from `registers` or `recursion_stack`
1085/// before it allocates again. A nested value is built top-down to keep it so:
1086/// the outermost container sits on `recursion_stack` while it is filled, and
1087/// each entry goes into its parent before anything inside it is allocated.
1088pub(crate) struct Heap<'a> {
1089    pub objs: &'a mut ObjectPool,
1090    pub maps: &'a mut MapPool,
1091    pub strings: &'a mut StringPool,
1092    pub gc: &'a mut GcState,
1093    pub registers: &'a RegisterFile,
1094    pub recursion_stack: &'a mut RegisterFile,
1095}
1096
1097impl Heap<'_> {
1098    /// A new empty list.
1099    pub fn array(&mut self) -> Data {
1100        Data::array(alloc_array(
1101            self.objs,
1102            self.maps,
1103            self.strings,
1104            self.registers,
1105            self.recursion_stack,
1106            self.gc,
1107        ))
1108    }
1109
1110    /// A new empty map.
1111    pub fn map(&mut self) -> Data {
1112        Data::map(alloc_map(
1113            self.objs,
1114            self.maps,
1115            self.strings,
1116            self.registers,
1117            self.recursion_stack,
1118            self.gc,
1119        ))
1120    }
1121
1122    /// `s` as a value, sharing the pool slot of an equal string, since a map
1123    /// finds a string key by its slot.
1124    pub fn string(&mut self, s: &str) -> Data {
1125        Data::interned(s, self.strings).unwrap_or_else(|| {
1126            Data::string(
1127                s,
1128                self.objs,
1129                self.maps,
1130                self.strings,
1131                self.registers,
1132                self.recursion_stack,
1133                self.gc,
1134            )
1135        })
1136    }
1137}
1138
1139/// The deletion barrier: marks `old`, a value a store is about to overwrite or
1140/// remove from the heap while marking runs. Out of line, so a store pays one
1141/// test of [`GcState::marking`] when no cycle is marking.
1142#[cold]
1143#[inline(never)]
1144pub fn shade_overwritten(gc: &mut GcState, strings: &mut StringPool, old: Data) {
1145    gc.tracer.shade(strings, old);
1146}
1147
1148/// Walks list `id` again from its first entry, if marking has already reached
1149/// it. Called after its entries moved in place.
1150#[cold]
1151#[inline(never)]
1152pub fn rescan_list(gc: &mut GcState, id: usize) {
1153    let t = &mut gc.tracer;
1154    if id < t.array_limit as usize && t.array_marks.contains(id) {
1155        t.work.push(Grey {
1156            id: id as u32,
1157            from: 0,
1158            map: false,
1159        });
1160    }
1161}
1162
1163/// Walks map `id` again from its first entry, if marking has already reached
1164/// it. Called after its entries moved in place.
1165#[cold]
1166#[inline(never)]
1167pub fn rescan_map(gc: &mut GcState, id: usize) {
1168    let t = &mut gc.tracer;
1169    if id < t.map_limit as usize && t.map_marks.contains(id) {
1170        t.work.push(Grey {
1171            id: id as u32,
1172            from: 0,
1173            map: true,
1174        });
1175    }
1176}