Skip to main content

gc/
runtime.rs

1use crate::header::{
2    GcId, GcListKind, GcObjectHeader, GcObjectType, GcPhase, RefCountHeader, RefCountId,
3};
4use crate::list::{GcList, GcListIter};
5use crate::malloc::{MallocState, DEFAULT_GC_THRESHOLD};
6use std::any::Any;
7
8/// Child-mark callback mode used during the three GC phases.
9/// Matches QuickJS `gc_decref_child`, `gc_scan_incref_child`, `gc_scan_incref_child2`.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum MarkFunc {
12    Decref,
13    ScanIncref,
14    ScanIncref2,
15}
16
17/// Trait for objects stored in the GC heap. Implementors expose graph edges via `trace`.
18pub trait GcObject: Any {
19    fn trace(&self, visit: &mut dyn FnMut(GcId));
20    fn on_free(&mut self, _rt: &mut GcRuntime) {}
21}
22
23struct GcObjectEntry {
24    header: GcObjectHeader,
25    object: Option<Box<dyn GcObject>>,
26}
27
28struct RefCountEntry {
29    header: RefCountHeader,
30    payload: Box<dyn FnOnce(&mut GcRuntime)>,
31}
32
33/// QuickJS-style runtime heap: reference counting + three-phase cycle removal.
34pub struct GcRuntime {
35    objects: Vec<Option<GcObjectEntry>>,
36    free_slots: Vec<GcId>,
37    ref_counts: Vec<Option<RefCountEntry>>,
38    ref_count_free_slots: Vec<RefCountId>,
39
40    gc_obj_list: GcList,
41    gc_zero_ref_count_list: GcList,
42    tmp_obj_list: GcList,
43    gc_phase: GcPhase,
44    malloc_state: MallocState,
45    malloc_gc_threshold: usize,
46}
47
48impl GcRuntime {
49    pub fn new() -> Self {
50        GcRuntime {
51            objects: Vec::new(),
52            free_slots: Vec::new(),
53            ref_counts: Vec::new(),
54            ref_count_free_slots: Vec::new(),
55            gc_obj_list: GcList::new(),
56            gc_zero_ref_count_list: GcList::new(),
57            tmp_obj_list: GcList::new(),
58            gc_phase: GcPhase::None,
59            malloc_state: MallocState::new(),
60            malloc_gc_threshold: DEFAULT_GC_THRESHOLD,
61        }
62    }
63
64    pub fn malloc_state(&self) -> &MallocState {
65        &self.malloc_state
66    }
67
68    pub fn malloc_state_mut(&mut self) -> &mut MallocState {
69        &mut self.malloc_state
70    }
71
72    pub fn gc_threshold(&self) -> usize {
73        self.malloc_gc_threshold
74    }
75
76    /// `threshold == usize::MAX` disables automatic GC, matching `JS_SetGCThreshold(rt, -1)`.
77    pub fn set_gc_threshold(&mut self, threshold: usize) {
78        self.malloc_gc_threshold = threshold;
79    }
80
81    pub fn gc_phase(&self) -> GcPhase {
82        self.gc_phase
83    }
84
85    pub fn gc_object_count(&self) -> usize {
86        GcListIter {
87            rt: self,
88            current: self.gc_obj_list.head,
89        }
90        .count()
91    }
92
93    pub(crate) fn header(&self, id: GcId) -> &GcObjectHeader {
94        &self.objects[id].as_ref().expect("invalid GcId").header
95    }
96
97    pub(crate) fn header_mut(&mut self, id: GcId) -> &mut GcObjectHeader {
98        &mut self.objects[id].as_mut().expect("invalid GcId").header
99    }
100
101    fn list_ptr(&mut self, kind: GcListKind) -> *mut GcList {
102        (match kind {
103            GcListKind::GcObj => &mut self.gc_obj_list,
104            GcListKind::Tmp => &mut self.tmp_obj_list,
105            GcListKind::ZeroRef => &mut self.gc_zero_ref_count_list,
106        }) as *mut GcList
107    }
108
109    fn list_push_back(&mut self, kind: GcListKind, id: GcId) {
110        let list_ptr = self.list_ptr(kind);
111        let tail = unsafe { (*list_ptr).tail };
112
113        {
114            let header = self.header_mut(id);
115            assert!(
116                header.list_kind.is_none(),
117                "object already belongs to a GC list: {:?}",
118                header.list_kind
119            );
120            header.list_kind = Some(kind);
121            header.list_prev = tail;
122            header.list_next = None;
123        }
124
125        if let Some(tail_id) = tail {
126            self.header_mut(tail_id).list_next = Some(id);
127        } else {
128            unsafe {
129                (*list_ptr).head = Some(id);
130            }
131        }
132        unsafe {
133            (*list_ptr).tail = Some(id);
134        }
135    }
136
137    fn list_remove(&mut self, kind: GcListKind, id: GcId) {
138        assert_eq!(self.header(id).list_kind, Some(kind), "object is not on the expected GC list");
139
140        let list_ptr = self.list_ptr(kind);
141        let (prev, next) = {
142            let header = self.header(id);
143            (header.list_prev, header.list_next)
144        };
145
146        match prev {
147            Some(p) => self.header_mut(p).list_next = next,
148            None => unsafe {
149                (*list_ptr).head = next;
150            },
151        }
152
153        match next {
154            Some(n) => self.header_mut(n).list_prev = prev,
155            None => unsafe {
156                (*list_ptr).tail = prev;
157            },
158        }
159
160        let header = self.header_mut(id);
161        header.list_kind = None;
162        header.list_prev = None;
163        header.list_next = None;
164    }
165
166    fn list_remove_current(&mut self, id: GcId) {
167        let kind = self
168            .header(id)
169            .list_kind
170            .expect("object is not on a GC list");
171        self.list_remove(kind, id);
172    }
173
174    fn list_move(&mut self, from: GcListKind, to: GcListKind, id: GcId) {
175        self.list_remove(from, id);
176        self.list_push_back(to, id);
177    }
178
179    fn list_move_current_to(&mut self, to: GcListKind, id: GcId) {
180        let from = self
181            .header(id)
182            .list_kind
183            .expect("object is not on a GC list");
184        self.list_move(from, to, id);
185    }
186
187    fn alloc_slot(&mut self, entry: GcObjectEntry) -> GcId {
188        let id = if let Some(id) = self.free_slots.pop() {
189            self.objects[id] = Some(entry);
190            id
191        } else {
192            let id = self.objects.len();
193            self.objects.push(Some(entry));
194            id
195        };
196        self.malloc_state
197            .record_alloc(std::mem::size_of::<GcObjectEntry>());
198        id
199    }
200
201    fn free_slot(&mut self, id: GcId) {
202        self.malloc_state
203            .record_free(std::mem::size_of::<GcObjectEntry>());
204        self.objects[id] = None;
205        self.free_slots.push(id);
206    }
207
208    /// Register a new GC object with `ref_count = 1` on `gc_obj_list`.
209    /// Matches QuickJS `add_gc_object`.
210    pub fn add_gc_object(&mut self, object: Box<dyn GcObject>, gc_obj_type: GcObjectType) -> GcId {
211        let id = self.alloc_slot(GcObjectEntry {
212            header: GcObjectHeader::new(gc_obj_type, 1),
213            object: Some(object),
214        });
215        self.list_push_back(GcListKind::GcObj, id);
216        id
217    }
218
219    /// Increment refcount. Matches QuickJS `js_dup` for GC objects.
220    pub fn dup_gc(&mut self, id: GcId) -> GcId {
221        self.header_mut(id).ref_count += 1;
222        id
223    }
224
225    /// Decrement refcount and free when it reaches zero.
226    /// Matches QuickJS `JS_FreeValueRT` for GC objects.
227    pub fn free_gc(&mut self, id: GcId) {
228        if !self.object_exists(id) {
229            return;
230        }
231        let ref_count = {
232            let header = self.header_mut(id);
233            header.ref_count -= 1;
234            header.ref_count
235        };
236
237        if ref_count > 0 {
238            return;
239        }
240
241        if self.gc_phase != GcPhase::RemoveCycles {
242            self.list_move(GcListKind::GcObj, GcListKind::ZeroRef, id);
243            if self.gc_phase == GcPhase::None {
244                self.free_zero_refcount();
245            }
246        }
247    }
248
249    /// Allocate a simple refcounted payload (strings, etc.). Not cycle-collected.
250    pub fn add_ref_counted<F>(&mut self, on_free: F) -> RefCountId
251    where
252        F: FnOnce(&mut GcRuntime) + 'static,
253    {
254        let entry = RefCountEntry {
255            header: RefCountHeader::new(1),
256            payload: Box::new(on_free),
257        };
258        let id = if let Some(id) = self.ref_count_free_slots.pop() {
259            self.ref_counts[id] = Some(entry);
260            id
261        } else {
262            let id = self.ref_counts.len();
263            self.ref_counts.push(Some(entry));
264            id
265        };
266        self.malloc_state
267            .record_alloc(std::mem::size_of::<RefCountEntry>());
268        id
269    }
270
271    pub fn dup_ref_counted(&mut self, id: RefCountId) -> RefCountId {
272        self.ref_counts[id]
273            .as_mut()
274            .expect("invalid RefCountId")
275            .header
276            .ref_count += 1;
277        id
278    }
279
280    pub fn free_ref_counted(&mut self, id: RefCountId) {
281        let ref_count = {
282            let entry = self.ref_counts[id].as_mut().expect("invalid RefCountId");
283            entry.header.ref_count -= 1;
284            entry.header.ref_count
285        };
286
287        if ref_count <= 0 {
288            let entry = self.ref_counts[id].take().expect("double free");
289            self.malloc_state
290                .record_free(std::mem::size_of::<RefCountEntry>());
291            (entry.payload)(self);
292            self.ref_count_free_slots.push(id);
293        }
294    }
295
296    /// Mark a GC object header during traversal. Matches `JS_MarkValue` for object tags.
297    pub fn mark_gc_header(&mut self, id: GcId, mark_func: MarkFunc) {
298        match mark_func {
299            MarkFunc::Decref => self.gc_decref_child(id),
300            MarkFunc::ScanIncref => self.gc_scan_incref_child(id),
301            MarkFunc::ScanIncref2 => self.gc_scan_incref_child2(id),
302        }
303    }
304
305    /// Traverse children of a GC object. Matches QuickJS `mark_children`.
306    pub fn mark_children(&mut self, id: GcId, mark_func: MarkFunc) {
307        // Move the box out temporarily so `trace` can call back into the runtime without
308        // materializing a child-id buffer.
309        let object = self.objects[id]
310            .as_mut()
311            .expect("invalid GcId")
312            .object
313            .take()
314            .expect("object already finalized");
315        object.trace(&mut |child| {
316            self.mark_gc_header(child, mark_func);
317        });
318        self.objects[id]
319            .as_mut()
320            .expect("object freed while tracing")
321            .object = Some(object);
322    }
323
324    /// Maybe run GC when tracked malloc exceeds threshold. Matches `js_trigger_gc`.
325    pub fn trigger_gc(&mut self, alloc_size: usize) {
326        let force_gc =
327            self.malloc_state.malloc_size.saturating_add(alloc_size) > self.malloc_gc_threshold;
328        if force_gc {
329            self.run_gc();
330            self.malloc_gc_threshold =
331                self.malloc_state.malloc_size + (self.malloc_state.malloc_size >> 1);
332        }
333    }
334
335    /// Run the three-phase cycle collector. Matches `JS_RunGC`.
336    pub fn run_gc(&mut self) {
337        self.gc_decref();
338        self.gc_scan();
339        self.gc_free_cycles();
340    }
341
342    /// Return false if the object has been freed during cycle collection.
343    /// Matches `JS_IsLiveObject`.
344    pub fn is_live_object(&self, id: GcId) -> bool {
345        self.objects
346            .get(id)
347            .and_then(|o| o.as_ref())
348            .map_or(false, |e| !e.header.free_mark)
349    }
350
351    pub fn ref_count(&self, id: GcId) -> i32 {
352        self.header(id).ref_count
353    }
354
355    pub fn object_exists(&self, id: GcId) -> bool {
356        self.objects.get(id).and_then(|o| o.as_ref()).is_some()
357    }
358
359    pub fn object_downcast<T: GcObject>(&self, id: GcId) -> Option<&T> {
360        let entry = self.objects.get(id)?.as_ref()?;
361        (entry.object.as_ref()?.as_ref() as &dyn Any).downcast_ref()
362    }
363
364    pub fn object_downcast_mut<T: GcObject>(&mut self, id: GcId) -> Option<&mut T> {
365        let entry = self.objects.get_mut(id)?.as_mut()?;
366        (entry.object.as_mut()?.as_mut() as &mut dyn Any).downcast_mut()
367    }
368
369    // --- Phase 1: trial deletion ---
370
371    fn gc_decref_child(&mut self, id: GcId) {
372        assert!(self.header(id).ref_count > 0);
373        self.header_mut(id).ref_count -= 1;
374        if self.header(id).ref_count == 0 && self.header(id).mark == 1 {
375            self.list_move(GcListKind::GcObj, GcListKind::Tmp, id);
376        }
377    }
378
379    fn gc_decref(&mut self) {
380        assert!(
381            self.tmp_obj_list.is_empty(),
382            "temporary GC list must be empty before trial deletion"
383        );
384        let mut current = self.gc_obj_list.head;
385        while let Some(id) = current {
386            let next = self.header(id).list_next;
387            assert_eq!(self.header(id).mark, 0);
388            self.mark_children(id, MarkFunc::Decref);
389            self.header_mut(id).mark = 1;
390            if self.header(id).ref_count == 0 {
391                self.list_move(GcListKind::GcObj, GcListKind::Tmp, id);
392            }
393            current = next;
394        }
395    }
396
397    // --- Phase 2: restore live refs ---
398
399    fn gc_scan_incref_child(&mut self, id: GcId) {
400        self.header_mut(id).ref_count += 1;
401        if self.header(id).ref_count == 1 {
402            self.list_move(GcListKind::Tmp, GcListKind::GcObj, id);
403            self.header_mut(id).mark = 0;
404        }
405    }
406
407    fn gc_scan_incref_child2(&mut self, id: GcId) {
408        self.header_mut(id).ref_count += 1;
409    }
410
411    fn gc_scan(&mut self) {
412        let mut current = self.gc_obj_list.head;
413        while let Some(id) = current {
414            assert!(self.header(id).ref_count > 0);
415            self.header_mut(id).mark = 0;
416            self.mark_children(id, MarkFunc::ScanIncref);
417            current = self.header(id).list_next;
418        }
419
420        let mut current = self.tmp_obj_list.head;
421        while let Some(id) = current {
422            let next = self.header(id).list_next;
423            self.mark_children(id, MarkFunc::ScanIncref2);
424            current = next;
425        }
426    }
427
428    // --- Phase 3: free cyclic garbage ---
429
430    fn gc_free_cycles(&mut self) {
431        self.gc_phase = GcPhase::RemoveCycles;
432
433        loop {
434            let id = self.tmp_obj_list.head;
435            if id.is_none() {
436                break;
437            }
438            let id = id.unwrap();
439            let gc_obj_type = self.header(id).gc_obj_type;
440            match gc_obj_type {
441                GcObjectType::MonkeyObject | GcObjectType::FunctionBytecode => {
442                    self.free_gc_object(id);
443                }
444                _ => {
445                    self.list_move(GcListKind::Tmp, GcListKind::ZeroRef, id);
446                }
447            }
448        }
449
450        self.gc_phase = GcPhase::None;
451
452        while let Some(id) = self.gc_zero_ref_count_list.head {
453            let ty = self.header(id).gc_obj_type;
454            assert!(
455                ty == GcObjectType::MonkeyObject || ty == GcObjectType::FunctionBytecode,
456                "unexpected deferred type: {:?}",
457                ty
458            );
459            self.list_remove_current(id);
460            self.free_slot(id);
461        }
462    }
463
464    fn free_zero_refcount(&mut self) {
465        self.gc_phase = GcPhase::Decref;
466        loop {
467            let id = self.gc_zero_ref_count_list.head;
468            if id.is_none() {
469                break;
470            }
471            let id = id.unwrap();
472            assert_eq!(self.header(id).ref_count, 0);
473            self.free_gc_object(id);
474        }
475        self.gc_phase = GcPhase::None;
476    }
477
478    fn free_gc_object(&mut self, id: GcId) {
479        match self.header(id).gc_obj_type {
480            GcObjectType::MonkeyObject | GcObjectType::FunctionBytecode => {
481                self.free_heap_object(id)
482            }
483            other => panic!("free_gc_object: unsupported type {:?}", other),
484        }
485    }
486
487    fn free_heap_object(&mut self, id: GcId) {
488        self.header_mut(id).free_mark = true;
489
490        // Process outgoing edges as `trace` reports them; GC freeing should not allocate
491        // a child snapshot.
492        let mut object = self.objects[id]
493            .as_mut()
494            .expect("invalid GcId")
495            .object
496            .take()
497            .expect("object already finalized");
498        object.trace(&mut |child| {
499            self.free_gc(child);
500        });
501        object.on_free(self);
502        drop(object);
503
504        let defer_free = self.gc_phase == GcPhase::RemoveCycles && self.header(id).ref_count != 0;
505        if !defer_free {
506            self.list_remove_current(id);
507            self.free_slot(id);
508        } else {
509            self.list_move_current_to(GcListKind::ZeroRef, id);
510        }
511    }
512}
513
514impl Default for GcRuntime {
515    fn default() -> Self {
516        Self::new()
517    }
518}