stator_jse 0.1.3

Stator JavaScript engine core — parser, bytecode compiler, Maglev JIT, interpreter, GC
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
//! Cheney semi-space scavenger (minor GC) and write-barrier remembered set.
//!
//! # Design
//!
//! The scavenger implements a classic Cheney two-finger copy collector for the
//! young generation:
//!
//! 1. **Root copying** – every root pointer that falls in the from-space is
//!    replaced by a fresh copy in the to-space (or, for objects old enough, by
//!    a promotion into old-space).  A forwarding pointer is installed in the
//!    original from-space slot so that subsequent references to the same object
//!    resolve to the same copy.
//!
//! 2. **BFS scan** – a scan cursor walks the to-space from its base to its
//!    current high-water mark.  For each copied object the scavenger would
//!    update any embedded child pointers (a `Relocate` trait will provide this
//!    once the full object model is in place; the loop structure is here now).
//!
//! 3. **Semi-space swap** – after the scan, [`SemiSpace::collect`] swaps the
//!    halves: the populated to-space becomes the new from-space and the old
//!    from-space is cleared for reuse.
//!
//! # Write barrier
//!
//! When mutator code stores a young-generation pointer into an old-generation
//! object it must call the write barrier, which inserts the old-generation
//! object into the [`RememberedSet`].  During the next scavenge the remembered
//! set entries are used as additional roots so that young objects reachable
//! only through old-space references are not incorrectly collected.

use std::alloc::Layout;
use std::collections::HashSet;
use std::mem::align_of;

use crate::gc::heap::{OldSpace, SemiSpace};
use crate::objects::heap_object::HeapObject;

/// Number of scavenge cycles an object must survive before being promoted to
/// the old generation.
pub const PROMOTION_AGE: u8 = 3;

// ── RememberedSet ──────────────────────────────────────────────────────────────

/// Tracks old-generation objects that contain references into the young
/// generation ("old→young" pointers).
///
/// When mutator code assigns a young-generation pointer into a field of an
/// old-generation object, the *write barrier* must record the old object here.
/// On the next scavenge, every object in the remembered set is treated as an
/// additional GC root so that the young objects it references are copied and
/// not collected.
///
/// After a scavenge the set is cleared; all young objects that were reached
/// through it are now in the new from-space (or promoted), and the
/// old-generation objects' pointer fields should be updated to those new
/// addresses.
pub struct RememberedSet {
    /// Raw pointers to old-space objects with at least one young-space field.
    slots: HashSet<*mut HeapObject>,
}

// SAFETY: The GC runtime is single-threaded; `RememberedSet` is owned by the
// heap and is never aliased across threads.
unsafe impl Send for RememberedSet {}

impl RememberedSet {
    /// Create an empty remembered set.
    pub fn new() -> Self {
        Self {
            slots: HashSet::new(),
        }
    }

    /// Record that `old_ptr` (an old-space object) now holds a reference to a
    /// young-space object.
    ///
    /// Duplicate insertions are silently ignored.
    pub fn insert(&mut self, old_ptr: *mut HeapObject) {
        self.slots.insert(old_ptr);
    }

    /// Remove `old_ptr` from the set.
    ///
    /// Has no effect if `old_ptr` is not present.
    pub fn remove(&mut self, old_ptr: *mut HeapObject) {
        self.slots.remove(&old_ptr);
    }

    /// Iterate the raw pointers of all recorded old-space objects.
    pub fn iter(&self) -> impl Iterator<Item = *mut HeapObject> + '_ {
        self.slots.iter().copied()
    }

    /// Remove all entries.
    ///
    /// Called at the end of a scavenge cycle once all live young objects have
    /// been copied and the old-space pointers updated.
    pub fn clear(&mut self) {
        self.slots.clear();
    }

    /// Number of entries currently in the set.
    pub fn len(&self) -> usize {
        self.slots.len()
    }

    /// Returns `true` if the set contains no entries.
    pub fn is_empty(&self) -> bool {
        self.slots.is_empty()
    }
}

impl Default for RememberedSet {
    fn default() -> Self {
        Self::new()
    }
}

// ── Scavenger ─────────────────────────────────────────────────────────────────

/// Implements a Cheney-style semi-space scavenge (minor GC).
///
/// The scavenger borrows the young generation's [`SemiSpace`] and the old
/// generation's [`OldSpace`] for the duration of a single scavenge cycle.
/// After [`scavenge`][Scavenger::scavenge] returns both spaces are in a
/// consistent state and the borrows are released.
pub struct Scavenger<'heap> {
    semi_space: &'heap mut SemiSpace,
    old_space: &'heap mut OldSpace,
}

impl<'heap> Scavenger<'heap> {
    /// Create a `Scavenger` that operates on the given spaces.
    pub fn new(semi_space: &'heap mut SemiSpace, old_space: &'heap mut OldSpace) -> Self {
        Self {
            semi_space,
            old_space,
        }
    }

    /// Run a full scavenge cycle.
    ///
    /// # Phases
    ///
    /// 1. **Root copying** – every `*mut HeapObject` that falls inside the
    ///    from-space is copied to the to-space (or promoted to old-space if its
    ///    age is ≥ [`PROMOTION_AGE`]).  The original from-space slot receives a
    ///    forwarding pointer so that shared references converge on one copy.
    ///    Both the `roots` slice and the `remembered_set` contribute roots.
    ///
    /// 2. **Cheney BFS scan** – a scan cursor walks the to-space; each copied
    ///    object's size (from its `alloc_size` header field) advances the
    ///    cursor.  When a `Relocate` trait is available this phase will also
    ///    update embedded child pointers.
    ///
    /// 3. **Semi-space swap** – [`SemiSpace::collect`] makes the to-space the
    ///    new from-space and resets the old from-space.
    ///
    /// # Safety
    /// Every `*mut HeapObject` reachable via `roots` or `remembered_set` must
    /// point to a live, validly-aligned heap object in the current from-space.
    pub unsafe fn scavenge(
        &mut self,
        roots: &mut [*mut *mut HeapObject],
        remembered_set: &mut RememberedSet,
    ) {
        // ── Phase 1: copy from-space roots into to-space ──────────────────
        for slot in roots.iter_mut() {
            // SAFETY: slot is a valid pointer to a root slot.
            let ptr = unsafe { **slot };
            if !ptr.is_null() && self.semi_space.is_in_from_space(ptr) {
                // SAFETY: caller guarantees ptr is a valid live from-space object.
                let new_ptr = unsafe { self.copy_object(ptr) };
                // SAFETY: slot is a valid, dereferenceable pointer.
                unsafe { **slot = new_ptr };
            }
        }

        // Process remembered-set entries as additional roots.  Old-space
        // objects themselves are not in the from-space, so we only use them
        // to ensure liveness — the actual pointer-update pass (scanning old
        // object fields) requires the Relocate trait which is not yet
        // implemented.
        let rem_ptrs: Vec<*mut HeapObject> = remembered_set.iter().collect();
        for old_ptr in rem_ptrs {
            // Guard: if the remembered-set entry is itself in from-space
            // (should not normally happen), copy it too.
            if !old_ptr.is_null() && self.semi_space.is_in_from_space(old_ptr) {
                // SAFETY: old_ptr is a valid from-space object per precondition.
                unsafe { self.copy_object(old_ptr) };
            }
        }
        remembered_set.clear();

        // ── Phase 2: Cheney BFS scan of to-space ─────────────────────────
        //
        // Walk the range [to_base, to_base + to_space_used) advancing by
        // each object's alloc_size.  When a Relocate trait is available,
        // each object's embedded HeapObject pointer fields will be updated
        // here (calling copy_object for each child that still lives in the
        // from-space).
        let to_base = self.semi_space.to_space_base() as usize;
        let mut scan = to_base;
        loop {
            let used = self.semi_space.to_space_used();
            if scan >= to_base + used {
                break;
            }
            let obj = scan as *mut HeapObject;
            // SAFETY: `scan` is within the to-space allocation range and
            // was written there by copy_object.
            let size = unsafe { (*obj).alloc_size() as usize };
            if size == 0 {
                // Defensive stop: an uninitialized header means we've run
                // past the live area.
                break;
            }
            // TODO: call obj.relocate(&mut |field| self.copy_field(field))
            //       once the Relocate trait is implemented.
            scan += size;
        }

        // ── Phase 3: swap semi-space halves ──────────────────────────────
        self.semi_space.collect();
    }

    /// Copy `from_ptr` from the from-space to the to-space (or promote it to
    /// old-space if its scavenge age meets the promotion threshold).
    ///
    /// If `from_ptr` already has a forwarding pointer (it was copied earlier
    /// in this cycle), the existing copy's address is returned without any
    /// further work.
    ///
    /// Returns the address of the (possibly newly-created) copy.
    ///
    /// # Safety
    /// `from_ptr` must be non-null, properly aligned, and point to a live
    /// `HeapObject` in the current from-space.
    unsafe fn copy_object(&mut self, from_ptr: *mut HeapObject) -> *mut HeapObject {
        // If this object was already copied this cycle, return its forwarding
        // destination immediately — this handles shared/cyclic references.
        // SAFETY: from_ptr is a valid from-space object per caller contract.
        if unsafe { (*from_ptr).is_forwarded() } {
            return unsafe { (*from_ptr).forwarding_ptr() };
        }

        let alloc_size = unsafe { (*from_ptr).alloc_size() as usize };
        let age = unsafe { (*from_ptr).age() };

        // Build the layout for the copy destination.
        let layout = Layout::from_size_align(alloc_size, align_of::<HeapObject>())
            .expect("alloc_size and HeapObject alignment must produce a valid Layout");

        // Helper: try to bump-allocate in to-space.
        let alloc_to_space = |ss: &mut SemiSpace| -> *mut HeapObject {
            ss.bump_alloc_to_space(layout)
                .map(|p| p as *mut HeapObject)
                .unwrap_or(std::ptr::null_mut())
        };

        // Decide whether to promote or keep in the young generation.
        let dest: *mut HeapObject = if age >= PROMOTION_AGE {
            // Promote: allocate in old-space.
            match self.old_space.bump_alloc(layout) {
                Some(ptr) => ptr as *mut HeapObject,
                // Old-space full: fall back to to-space (object stays young).
                None => alloc_to_space(self.semi_space),
            }
        } else {
            // Copy into to-space.
            alloc_to_space(self.semi_space)
        };

        if dest.is_null() {
            // Allocation failed; leave the object in place as a last resort.
            return from_ptr;
        }

        // Bitwise-copy the object bytes to the new location.
        // SAFETY: both pointers are valid and non-overlapping; alloc_size bytes
        // are accessible at each address.
        unsafe {
            std::ptr::copy_nonoverlapping(from_ptr as *const u8, dest as *mut u8, alloc_size)
        };

        // Increment the survival age in the freshly-copied object.
        // SAFETY: dest is a valid, just-written HeapObject.
        unsafe { (*dest).increment_age() };

        // Install the forwarding pointer in the original location so that any
        // later reference to from_ptr is redirected to dest.
        // SAFETY: from_ptr is valid and dest is non-null and properly aligned.
        unsafe { (*from_ptr).set_forwarding_ptr(dest) };

        dest
    }
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use crate::gc::heap::{OldSpace, SemiSpace};
    use crate::objects::heap_object::HeapObject;
    use std::alloc::Layout;

    // Helper: allocate a HeapObject in `semi_space`'s from-space, zero-init
    // it, and write its alloc_size header field.
    fn alloc_in_from(semi: &mut SemiSpace) -> *mut HeapObject {
        let layout = Layout::new::<HeapObject>();
        let raw = semi.bump_alloc(layout).expect("from-space has space");
        // SAFETY: raw is valid and layout.size() bytes are accessible.
        unsafe { std::ptr::write_bytes(raw, 0, layout.size()) };
        let obj = raw as *mut HeapObject;
        // SAFETY: obj is a valid pointer to zero-initialised memory.
        unsafe { (*obj).init_alloc_size(layout.size() as u32) };
        obj
    }

    // ── RememberedSet basic API ───────────────────────────────────────────

    #[test]
    fn test_remembered_set_insert_and_clear() {
        let mut rs = RememberedSet::new();
        let mut obj = HeapObject::new_null();
        let ptr = &raw mut obj;
        assert!(rs.is_empty());
        rs.insert(ptr);
        assert_eq!(rs.len(), 1);
        rs.clear();
        assert!(rs.is_empty());
    }

    #[test]
    fn test_remembered_set_remove() {
        let mut rs = RememberedSet::new();
        let mut obj = HeapObject::new_null();
        let ptr = &raw mut obj;
        rs.insert(ptr);
        rs.remove(ptr);
        assert!(rs.is_empty());
    }

    #[test]
    fn test_remembered_set_duplicate_insert_is_idempotent() {
        let mut rs = RememberedSet::new();
        let mut obj = HeapObject::new_null();
        let ptr = &raw mut obj;
        rs.insert(ptr);
        rs.insert(ptr);
        assert_eq!(rs.len(), 1, "duplicate inserts must not grow the set");
    }

    // ── Scavenge cycle: live objects are copied ───────────────────────────

    #[test]
    fn test_scavenge_live_object_root_is_updated() {
        let mut semi = SemiSpace::new(4096);
        let mut old = OldSpace::new(65536);

        let obj_ptr = alloc_in_from(&mut semi);
        let original_addr = obj_ptr as usize;

        let mut root: *mut HeapObject = obj_ptr;
        let mut roots = [&raw mut root as *mut *mut HeapObject];
        let mut rs = RememberedSet::new();

        // SAFETY: root is a valid from-space object.
        unsafe {
            Scavenger::new(&mut semi, &mut old).scavenge(&mut roots, &mut rs);
        }

        // Root must have been updated to the new location (in the new from-space,
        // which is the old to-space).
        let new_addr = root as usize;
        assert_ne!(
            new_addr, original_addr,
            "root must point to the copy, not the original"
        );
        // New from-space must contain at least one object.
        assert!(
            semi.used() > 0,
            "live object must appear in the new from-space"
        );
    }

    // ── Dead objects are not present in new from-space ────────────────────

    #[test]
    fn test_scavenge_dead_objects_do_not_extend_new_from_space() {
        let mut semi = SemiSpace::new(4096);
        let mut old = OldSpace::new(65536);

        // Allocate two objects but only root one of them.
        let live_ptr = alloc_in_from(&mut semi);
        let _dead_ptr = alloc_in_from(&mut semi); // no root → dead

        let mut root: *mut HeapObject = live_ptr;
        let mut roots = [&raw mut root as *mut *mut HeapObject];
        let mut rs = RememberedSet::new();

        unsafe {
            Scavenger::new(&mut semi, &mut old).scavenge(&mut roots, &mut rs);
        }

        // Only the one live object should be in the new from-space.
        let obj_size = std::mem::size_of::<HeapObject>();
        assert_eq!(
            semi.used(),
            obj_size,
            "only the live object must be in the new from-space"
        );
    }

    // ── Forwarding pointers deduplicate shared references ─────────────────

    #[test]
    fn test_scavenge_two_roots_to_same_object_get_same_copy() {
        let mut semi = SemiSpace::new(4096);
        let mut old = OldSpace::new(65536);

        let obj_ptr = alloc_in_from(&mut semi);

        // Two roots pointing at the same object.
        let mut root1: *mut HeapObject = obj_ptr;
        let mut root2: *mut HeapObject = obj_ptr;
        let mut roots = [
            &raw mut root1 as *mut *mut HeapObject,
            &raw mut root2 as *mut *mut HeapObject,
        ];
        let mut rs = RememberedSet::new();

        unsafe {
            Scavenger::new(&mut semi, &mut old).scavenge(&mut roots, &mut rs);
        }

        // Both roots must converge on the single copy.
        assert_eq!(
            root1, root2,
            "two roots to the same object must both point to the single copy"
        );
        // Only one copy must exist in the new from-space.
        let obj_size = std::mem::size_of::<HeapObject>();
        assert_eq!(
            semi.used(),
            obj_size,
            "shared object must be copied exactly once"
        );
    }

    // ── Promotion after PROMOTION_AGE scavenges ───────────────────────────

    #[test]
    fn test_scavenge_promotes_old_enough_object_to_old_space() {
        let mut semi = SemiSpace::new(4096);
        let mut old = OldSpace::new(65536);

        let obj_ptr = alloc_in_from(&mut semi);

        // Artificially age the object to just below the threshold.
        // SAFETY: obj_ptr is a valid from-space HeapObject.
        for _ in 0..PROMOTION_AGE {
            unsafe { (*obj_ptr).increment_age() };
        }

        let mut root: *mut HeapObject = obj_ptr;
        let mut roots = [&raw mut root as *mut *mut HeapObject];
        let mut rs = RememberedSet::new();

        let old_used_before = old.used();

        unsafe {
            Scavenger::new(&mut semi, &mut old).scavenge(&mut roots, &mut rs);
        }

        // The object must have been promoted: old-space grew.
        assert!(
            old.used() > old_used_before,
            "promoted object must be in old-space"
        );
        // Young from-space must be empty (no copy went there).
        assert_eq!(
            semi.used(),
            0,
            "promoted object must not appear in the young from-space"
        );
    }

    // ── Age is incremented on each copy ──────────────────────────────────

    #[test]
    fn test_scavenge_increments_age_on_copy() {
        let mut semi = SemiSpace::new(4096);
        let mut old = OldSpace::new(65536);

        let obj_ptr = alloc_in_from(&mut semi);
        assert_eq!(unsafe { (*obj_ptr).age() }, 0);

        let mut root: *mut HeapObject = obj_ptr;
        let mut roots = [&raw mut root as *mut *mut HeapObject];
        let mut rs = RememberedSet::new();

        unsafe {
            Scavenger::new(&mut semi, &mut old).scavenge(&mut roots, &mut rs);
        }

        // The copy's age must be 1.
        // SAFETY: root now points to the freshly-copied object.
        assert_eq!(
            unsafe { (*root).age() },
            1,
            "copied object's age must be incremented by 1"
        );
    }

    // ── Null roots are skipped gracefully ─────────────────────────────────

    #[test]
    fn test_scavenge_null_root_is_skipped() {
        let mut semi = SemiSpace::new(4096);
        let mut old = OldSpace::new(65536);

        let mut root: *mut HeapObject = std::ptr::null_mut();
        let mut roots = [&raw mut root as *mut *mut HeapObject];
        let mut rs = RememberedSet::new();

        // Must not panic or crash.
        unsafe {
            Scavenger::new(&mut semi, &mut old).scavenge(&mut roots, &mut rs);
        }
        assert!(root.is_null(), "null root must remain null");
    }

    // ── Remembered set is cleared after scavenge ─────────────────────────

    #[test]
    fn test_scavenge_clears_remembered_set() {
        let mut semi = SemiSpace::new(4096);
        let mut old = OldSpace::new(65536);

        let mut rs = RememberedSet::new();
        // Insert a dummy old-space address (not in from-space).
        let mut dummy = HeapObject::new_null();
        rs.insert(&raw mut dummy);
        assert!(!rs.is_empty());

        let mut roots: [*mut *mut HeapObject; 0] = [];
        unsafe {
            Scavenger::new(&mut semi, &mut old).scavenge(&mut roots, &mut rs);
        }
        assert!(
            rs.is_empty(),
            "remembered set must be cleared after scavenge"
        );
    }

    // ── Multiple scavenge cycles ──────────────────────────────────────────

    #[test]
    fn test_multiple_scavenge_cycles_age_accumulates() {
        let mut semi = SemiSpace::new(4096);
        let mut old = OldSpace::new(65536);

        let first_ptr = alloc_in_from(&mut semi);
        let mut root: *mut HeapObject = first_ptr;

        let mut rs = RememberedSet::new();
        for expected_age in 1..PROMOTION_AGE {
            let mut roots = [&raw mut root as *mut *mut HeapObject];
            unsafe {
                Scavenger::new(&mut semi, &mut old).scavenge(&mut roots, &mut rs);
            }
            // Re-allocate into the new from-space so the next cycle has a fresh
            // from-space object alongside the survivor.
            assert_eq!(
                unsafe { (*root).age() },
                expected_age,
                "age after {expected_age} cycle(s) must be {expected_age}"
            );
        }
    }
}