Skip to main content

catallaxy_runtime/
mutator.rs

1//! Per-thread runtime context: identity, allocation, and the handle stack that roots values held
2//! by native code across calls back into the interpreter.
3
4use std::sync::atomic::AtomicU64;
5use std::sync::Arc;
6
7// SAFETY: a Mutator is moved to the thread it serves before that thread runs.
8unsafe impl Send for Mutator {}
9
10use catallaxy_gc::{Header, LocalHeap, ObjState, ThreadRecord};
11use num_bigint::BigInt;
12use num_traits::ToPrimitive;
13
14use crate::class::{Class, BUILTINS};
15use crate::dict::DictTable;
16use crate::object::*;
17use crate::value::{fits_small, Value};
18
19/// Copies `parts` one after another to `dst`.
20///
21/// # Safety
22/// `dst` must have room for all the parts' bytes.
23unsafe fn copy_parts<'a>(mut dst: *mut u8, parts: impl Iterator<Item = &'a str>) {
24    for p in parts {
25        std::ptr::copy_nonoverlapping(p.as_ptr(), dst, p.len());
26        dst = dst.add(p.len());
27    }
28}
29
30/// A recoverable interruption of execution.
31#[derive(Debug)]
32pub enum Trap {
33    /// A panic: unwinds the current thread.
34    Panic(String),
35    /// Non-local control flow (break/return out of a block) unwinding to an interpreter frame.
36    Unwind { frame: u64, kind: u8, value: Value },
37    /// `exit(code)`.
38    Exit(i32),
39}
40
41pub type RtResult<T> = Result<T, Trap>;
42
43pub fn panic<T>(msg: impl Into<String>) -> RtResult<T> {
44    Err(Trap::Panic(msg.into()))
45}
46
47/// Checks a request for `count` items of `size` bytes before allocating it, so that sizes the
48/// machine cannot provide raise a `MemoryError` panic (with the program's stack trace) instead
49/// of aborting the process in the allocator. Returns the size in bytes.
50pub fn check_alloc(count: usize, size: usize) -> RtResult<usize> {
51    let Some(bytes) = count.checked_mul(size) else {
52        return panic(format!("MemoryError: cannot allocate {count} items of {size} bytes"));
53    };
54    catallaxy_gc::limits::check_request(bytes).map_err(|e| Trap::Panic(format!("MemoryError: {e}")))?;
55    Ok(bytes)
56}
57
58/// An empty vector with room for exactly `n` items, or a `MemoryError` panic if that much
59/// memory cannot be had (see [`check_alloc`]).
60pub fn try_vec<T>(n: usize) -> RtResult<Vec<T>> {
61    let bytes = check_alloc(n, std::mem::size_of::<T>())?;
62    let mut v = Vec::new();
63    if v.try_reserve_exact(n).is_err() {
64        return refused(format!("cannot allocate {bytes} bytes"));
65    }
66    Ok(v)
67}
68
69/// The `MemoryError` for a request the allocator refused (`what` says which). Hands the
70/// collector's headroom back to the OS first, so that reporting the error does not fail too.
71#[cold]
72pub fn refused<T>(what: impl std::fmt::Display) -> RtResult<T> {
73    catallaxy_gc::limits::release_headroom();
74    panic(format!("MemoryError: {what} (the allocator refused)"))
75}
76
77/// Pushes `x` onto a vector a native is building, raising a `MemoryError` panic if the vector
78/// cannot grow. There is no collection and retry: the vector may hold values that nothing else
79/// roots.
80#[inline]
81pub fn try_push<T>(v: &mut Vec<T>, x: T) -> RtResult<()> {
82    if v.len() == v.capacity() {
83        grow_vec(v)?;
84    }
85    v.push(x);
86    Ok(())
87}
88
89#[cold]
90#[inline(never)]
91fn grow_vec<T>(v: &mut Vec<T>) -> RtResult<()> {
92    // `try_reserve` grows amortized (to at least twice the capacity), as `push` would.
93    if v.try_reserve(1).is_err() {
94        let bytes = v.capacity().max(4).saturating_mul(2).saturating_mul(std::mem::size_of::<T>());
95        return refused(format!("cannot allocate {bytes} bytes"));
96    }
97    Ok(())
98}
99
100/// Collects `items` into a vector, raising a `MemoryError` panic as [`try_push`] does.
101pub fn try_collect<T>(items: impl IntoIterator<Item = T>) -> RtResult<Vec<T>> {
102    let items = items.into_iter();
103    let mut v = try_vec(items.size_hint().0)?;
104    for x in items {
105        try_push(&mut v, x)?;
106    }
107    Ok(v)
108}
109
110/// An empty string with room for exactly `bytes` bytes, or a `MemoryError` panic.
111pub fn try_string(bytes: usize) -> RtResult<String> {
112    check_alloc(bytes, 1)?;
113    let mut s = String::new();
114    if s.try_reserve_exact(bytes).is_err() {
115        return refused(format!("cannot allocate {bytes} bytes"));
116    }
117    Ok(s)
118}
119
120pub struct Mutator {
121    pub tid: u16,
122    pub rec: Arc<ThreadRecord>,
123    /// The thread's allocator. Shared with the collector (which retires its pages while the
124    /// thread is stopped), so it is only ever accessed through this raw pointer.
125    heap: *mut LocalHeap,
126    /// Values rooted by native code. Traced as roots by the collector, which reads them while
127    /// this thread is stopped, so they live in a separate allocation reached by raw pointer.
128    handles: *mut Vec<Value>,
129}
130
131/// An index into the handle stack, valid inside the [`Mutator::scope`] that created it.
132#[derive(Copy, Clone)]
133pub struct Handle(usize);
134
135impl Mutator {
136    /// Creates the context for an already-registered thread.
137    pub fn new(rec: Arc<ThreadRecord>) -> Self {
138        let heap = Box::into_raw(Box::new(LocalHeap::new(rec.tid)));
139        let handles = Box::into_raw(Box::new(Vec::new()));
140        Mutator { tid: rec.tid, rec, heap, handles }
141    }
142
143    /// Whether new objects start young (see `catallaxy_gc::LocalHeap::set_young_alloc`).
144    pub fn set_young_alloc(&mut self, on: bool) {
145        unsafe { (*self.heap).set_young_alloc(on) }
146    }
147
148    /// A second context for the same thread (a fiber): same heap and thread record, its own
149    /// handle stack. Release it with [`Mutator::free_handles`].
150    pub fn sharing(other: &Mutator) -> Self {
151        Mutator {
152            tid: other.tid,
153            rec: other.rec.clone(),
154            heap: other.heap,
155            handles: Box::into_raw(Box::new(Vec::new())),
156        }
157    }
158
159    /// Frees the handle stack of a context made by [`Mutator::sharing`].
160    ///
161    /// # Safety
162    /// No root source may still reference the handles.
163    pub unsafe fn free_handles(&mut self) {
164        drop(Box::from_raw(self.handles));
165        self.handles = std::ptr::null_mut();
166    }
167
168    /// Raw pointer to the allocator, for registration with the collector.
169    pub fn heap_ptr(&self) -> *mut LocalHeap {
170        self.heap
171    }
172
173    /// Returns the thread's pages to the global pool (on thread exit, in native state).
174    pub fn release_pages(&mut self) {
175        // SAFETY: the heap pointer is valid for the mutator's lifetime; the caller is in native
176        // state so the collector is not using it concurrently... except a collection may be
177        // running, which is why callers hold the thread out of `Running` via unregistration.
178        unsafe { (*self.heap).release_pages() }
179    }
180
181    // ---- handles ------------------------------------------------------------------------
182
183    /// Runs `f` with a handle scope; handles rooted inside are released afterwards.
184    pub fn scope<R>(&mut self, f: impl FnOnce(&mut Mutator) -> R) -> R {
185        let base = self.handles_len();
186        let r = f(self);
187        // SAFETY: only this thread mutates its handles, never during a collection.
188        unsafe { (*self.handles).truncate(base) };
189        r
190    }
191    pub fn root(&mut self, v: Value) -> Handle {
192        unsafe {
193            (*self.handles).push(v);
194            Handle((*self.handles).len() - 1)
195        }
196    }
197    pub fn get(&self, h: Handle) -> Value {
198        unsafe { (&(*self.handles))[h.0] }
199    }
200    pub fn set(&mut self, h: Handle, v: Value) {
201        unsafe { (&mut (*self.handles))[h.0] = v };
202    }
203    fn handles_len(&self) -> usize {
204        unsafe { (*self.handles).len() }
205    }
206    /// The current depth of the handle stack, for [`Mutator::release_handles`]. Lets foreign
207    /// code (the C API) manage scopes explicitly.
208    pub fn handle_mark(&self) -> usize {
209        self.handles_len()
210    }
211    /// Releases every handle rooted since `mark`.
212    pub fn release_handles(&mut self, mark: usize) {
213        unsafe { (*self.handles).truncate(mark) };
214    }
215    /// Root values for the GC (called while this thread is stopped).
216    pub fn handle_values(&self) -> &[Value] {
217        unsafe { &*self.handles }
218    }
219    /// Pointer to the handle stack, for root sources that must not borrow the mutator.
220    pub fn handles_ptr(&self) -> *const Vec<Value> {
221        self.handles
222    }
223
224    // ---- allocation ---------------------------------------------------------------------
225
226    #[inline]
227    pub fn alloc_raw(&mut self, size: usize, cls: &'static Class) -> *mut Header {
228        // SAFETY: only this (running) thread touches its heap outside of collections.
229        unsafe { (*self.heap).alloc(size, &cls.gc as *const _) }
230    }
231
232    pub fn alloc_instance(&mut self, cls: &'static Class) -> Value {
233        let n = cls.nfields();
234        let h = self.alloc_raw(InstanceObj::size_for(n), cls);
235        let inst = unsafe { &*(h as *const InstanceObj) };
236        for i in 0..n {
237            inst.set(i, Value::NIL);
238        }
239        Value::from_ptr(h)
240    }
241
242    pub fn alloc_str(&mut self, s: &str) -> Value {
243        self.alloc_str_parts(&[s])
244    }
245
246    /// Allocates the concatenation of `parts`.
247    pub fn alloc_str_parts(&mut self, parts: &[&str]) -> Value {
248        let len: usize = parts.iter().map(|p| p.len()).sum();
249        let chars = (len >= StrObj::INDEX_MIN)
250            .then(|| parts.iter().map(|p| if p.is_ascii() { p.len() } else { p.chars().count() }).sum());
251        // SAFETY: the parts are UTF-8 and together `len` bytes with `chars` characters.
252        unsafe { self.alloc_str_with(len, chars, |dst| copy_parts(dst, parts.iter().copied())) }
253    }
254
255    /// Allocates the concatenation of `n` strings, `part(i)` being the `i`th, copying each once.
256    /// The character count comes from the parts' cached counts, so no byte is scanned twice.
257    pub fn alloc_concat<'a>(&mut self, n: usize, part: impl Fn(usize) -> &'a StrObj) -> Value {
258        let len: usize = (0..n).map(|i| part(i).len).sum();
259        let chars = (len >= StrObj::INDEX_MIN).then(|| (0..n).map(|i| part(i).char_count()).sum());
260        // SAFETY: as above. Allocation never collects, so the parts stay valid while copied.
261        unsafe { self.alloc_str_with(len, chars, |dst| copy_parts(dst, (0..n).map(|i| part(i).as_str()))) }
262    }
263
264    /// As [`Mutator::alloc_str`], but a string the allocator refuses raises a `MemoryError`
265    /// instead of aborting. A refusal first runs a collection and retries, so every value the
266    /// caller holds must be rooted (as interpreter registers and native arguments are); `s`
267    /// itself may point into a rooted string, since objects never move.
268    pub fn try_alloc_str(&mut self, s: &str) -> RtResult<Value> {
269        let chars = (s.len() >= StrObj::INDEX_MIN).then(|| if s.is_ascii() { s.len() } else { s.chars().count() });
270        // SAFETY: `s` is UTF-8 with `chars` characters.
271        unsafe { self.try_alloc_str_with(s.len(), chars, |dst| copy_parts(dst, std::iter::once(s))) }
272    }
273
274    /// As [`Mutator::alloc_concat`], raising a `MemoryError` as [`Mutator::try_alloc_str`] does.
275    pub fn try_alloc_concat<'a>(&mut self, n: usize, part: impl Fn(usize) -> &'a StrObj) -> RtResult<Value> {
276        let len: usize = (0..n).map(|i| part(i).len).sum();
277        let chars = (len >= StrObj::INDEX_MIN).then(|| (0..n).map(|i| part(i).char_count()).sum());
278        // SAFETY: as for `alloc_concat`; the parts are rooted, so a collection keeps them.
279        unsafe { self.try_alloc_str_with(len, chars, |dst| copy_parts(dst, (0..n).map(|i| part(i).as_str()))) }
280    }
281
282    /// As [`Mutator::try_alloc_str`], but a refusal raises the `MemoryError` without collecting
283    /// first, so the caller may hold values that nothing roots (the items of a list it is
284    /// building, say).
285    #[inline]
286    pub fn try_alloc_str_nogc(&mut self, s: &str) -> RtResult<Value> {
287        let chars = (s.len() >= StrObj::INDEX_MIN).then(|| if s.is_ascii() { s.len() } else { s.chars().count() });
288        let (size, word) = StrObj::layout(s.len(), chars);
289        if size > catallaxy_gc::MAX_SMALL {
290            check_alloc(size, 1)?;
291        }
292        // SAFETY: as for `alloc_str_with`; `s` is UTF-8 with `chars` characters.
293        unsafe {
294            let h = (*self.heap).try_alloc_uninit(size, &BUILTINS.string.gc as *const _);
295            if h.is_null() {
296                return refused(format!("cannot allocate a string of {} bytes", s.len()));
297            }
298            Ok(self.init_str(h, size, s.len(), word, |dst| copy_parts(dst, std::iter::once(s))))
299        }
300    }
301
302    /// For a native about to allocate much, holding no unrooted values yet: if the OS has
303    /// refused memory since the last collection, collects now. Until a collection has looked
304    /// again, the `try_` allocations refuse small objects that need a new heap segment, and a
305    /// native has no safepoint at which the collection it is waiting for could run.
306    #[inline]
307    pub fn collect_if_refused(&self) {
308        if catallaxy_gc::limits::memory_refused() {
309            catallaxy_gc::collect_to_free(&self.rec);
310        }
311    }
312
313    /// An empty vector with room for exactly `n` items. If the allocator refuses, collects
314    /// (garbage may hold the memory) and tries once more, then raises a `MemoryError`. For
315    /// natives that hold no unrooted values yet: their arguments are rooted.
316    pub fn try_vec_collecting<T>(&self, n: usize) -> RtResult<Vec<T>> {
317        self.collect_if_refused();
318        let bytes = check_alloc(n, std::mem::size_of::<T>())?;
319        let mut v = Vec::new();
320        if v.try_reserve_exact(n).is_err() {
321            catallaxy_gc::collect_to_free(&self.rec);
322            if v.try_reserve_exact(n).is_err() {
323                return refused(format!("cannot allocate {bytes} bytes"));
324            }
325        }
326        Ok(v)
327    }
328
329    /// As [`Mutator::alloc_str_with`], for the `try_` variants.
330    ///
331    /// # Safety
332    /// As for [`Mutator::alloc_str_with`].
333    #[inline]
334    unsafe fn try_alloc_str_with(
335        &mut self,
336        len: usize,
337        chars: Option<usize>,
338        fill: impl FnOnce(*mut u8),
339    ) -> RtResult<Value> {
340        let (size, word) = StrObj::layout(len, chars);
341        if size > catallaxy_gc::MAX_SMALL {
342            // Refuse what the machine cannot provide before the allocator hands out memory
343            // that overcommit cannot back.
344            check_alloc(size, 1)?;
345        }
346        let vtable = &BUILTINS.string.gc as *const _;
347        let mut h = (*self.heap).try_alloc_uninit(size, vtable);
348        if h.is_null() {
349            h = self.retry_str(size, len)?;
350        }
351        Ok(self.init_str(h, size, len, word, fill))
352    }
353
354    /// The allocator refused a string of `len` bytes (`size` with its header): garbage may be
355    /// holding the memory, so collects and tries once more.
356    #[cold]
357    #[inline(never)]
358    fn retry_str(&mut self, size: usize, len: usize) -> RtResult<*mut Header> {
359        catallaxy_gc::collect_to_free(&self.rec);
360        // SAFETY: as for the first attempt; the caller initializes the object.
361        let h = unsafe { (*self.heap).try_alloc_uninit(size, &BUILTINS.string.gc as *const _) };
362        if h.is_null() {
363            return refused(format!("cannot allocate a string of {len} bytes"));
364        }
365        Ok(h)
366    }
367
368    /// Allocates a frozen string of `len` bytes; `fill` writes them. `chars` (the character
369    /// count) must be known for strings of at least [`StrObj::INDEX_MIN`] bytes.
370    ///
371    /// # Safety
372    /// `fill` must write exactly `len` bytes of UTF-8 holding `chars` characters.
373    unsafe fn alloc_str_with(&mut self, len: usize, chars: Option<usize>, fill: impl FnOnce(*mut u8)) -> Value {
374        debug_assert!(len < StrObj::INDEX_MIN || chars.is_some());
375        let (size, word) = StrObj::layout(len, chars);
376        // Large strings skip zeroing: every byte is written below, before any safepoint.
377        let h = (*self.heap).alloc_uninit(size, &BUILTINS.string.gc as *const _);
378        self.init_str(h, size, len, word, fill)
379    }
380
381    /// Fills in a string object of `size` bytes just allocated at `h`.
382    ///
383    /// # Safety
384    /// As for [`Mutator::alloc_str_with`]; `size` and `word` come from `StrObj::layout`.
385    #[inline(always)]
386    unsafe fn init_str(
387        &mut self,
388        h: *mut Header,
389        size: usize,
390        len: usize,
391        word: u64,
392        fill: impl FnOnce(*mut u8),
393    ) -> Value {
394        let o = h as *mut StrObj;
395        std::ptr::addr_of_mut!((*o).len).write(len);
396        std::ptr::addr_of_mut!((*o).hash).write(AtomicU64::new(0));
397        std::ptr::addr_of_mut!((*o).chars).write(AtomicU64::new(word));
398        let data = (h as *mut u8).add(StrObj::BASE);
399        fill(data);
400        // The char index (and padding) start zeroed.
401        std::ptr::write_bytes(data.add(len), 0, size - StrObj::BASE - len);
402        (*h).set_state(ObjState::Frozen);
403        Value::from_ptr(h)
404    }
405
406    /// Allocates a big integer, or returns a small int if it fits.
407    pub fn int_from_big(&mut self, b: BigInt) -> Value {
408        if let Some(i) = b.to_i64() {
409            if fits_small(i) {
410                return Value::small_int(i);
411            }
412        }
413        let h = self.alloc_raw(std::mem::size_of::<BigIntObj>(), BUILTINS.bigint);
414        unsafe {
415            std::ptr::write(&mut (*(h as *mut BigIntObj)).value, b);
416            (*h).set_state(ObjState::Frozen);
417        }
418        Value::from_ptr(h)
419    }
420
421    #[inline]
422    pub fn int(&mut self, i: i64) -> Value {
423        if fits_small(i) {
424            Value::small_int(i)
425        } else {
426            self.int_from_big(BigInt::from(i))
427        }
428    }
429
430    /// A list holding `items`, which takes over the vector's allocation (and capacity).
431    /// Small lists copy the items into the list object instead (see [`ListObj`]).
432    pub fn alloc_list(&mut self, items: Vec<Value>) -> Value {
433        let icap = ListObj::inline_cap_for(items.len());
434        let h = self.alloc_raw(ListObj::size_for(icap), BUILTINS.list);
435        unsafe { ListObj::init(h as *mut ListObj, items, icap) };
436        Value::from_ptr(h)
437    }
438
439    /// As [`Mutator::alloc_list`], but raises a `MemoryError` if the list's buffer cannot be
440    /// had. Nothing roots `items`, so there is no collection and retry.
441    pub fn try_alloc_list(&mut self, items: Vec<Value>) -> RtResult<Value> {
442        let icap = ListObj::inline_cap_for(items.len());
443        let h = self.alloc_raw(ListObj::size_for(icap), BUILTINS.list);
444        // On failure the object is left an empty list, which nothing references.
445        match unsafe { ListObj::try_init(h as *mut ListObj, items, icap) } {
446            Ok(()) => Ok(Value::from_ptr(h)),
447            Err(layout) => refused(format!("cannot allocate a list of {} bytes", layout.size())),
448        }
449    }
450
451    /// A list holding a copy of `items`, with room for at least `cap` of them.
452    pub fn alloc_list_from_slice(&mut self, items: &[Value], cap: usize) -> Value {
453        let icap = ListObj::inline_cap_for(cap.max(items.len()));
454        let h = self.alloc_raw(ListObj::size_for(icap), BUILTINS.list);
455        unsafe { ListObj::init_from_slice(h as *mut ListObj, items, cap, icap) };
456        Value::from_ptr(h)
457    }
458
459    /// As [`Mutator::alloc_list_from_slice`], but raises a `MemoryError` if the list's buffer
460    /// cannot be had (without collecting: `items` may hold values nothing else roots).
461    pub fn try_alloc_list_from_slice(&mut self, items: &[Value], cap: usize) -> RtResult<Value> {
462        let icap = ListObj::inline_cap_for(cap.max(items.len()));
463        let h = self.alloc_raw(ListObj::size_for(icap), BUILTINS.list);
464        match unsafe { ListObj::try_init_from_slice(h as *mut ListObj, items, cap, icap) } {
465            Ok(()) => Ok(Value::from_ptr(h)),
466            Err(layout) => refused(format!("cannot allocate a list of {} bytes", layout.size())),
467        }
468    }
469
470    pub fn alloc_dict(&mut self, table: DictTable) -> Value {
471        self.alloc_table(table, BUILTINS.dict)
472    }
473
474    pub fn alloc_set(&mut self, table: DictTable) -> Value {
475        self.alloc_table(table, BUILTINS.set)
476    }
477
478    fn alloc_table(&mut self, table: DictTable, cls: &'static Class) -> Value {
479        let h = self.alloc_raw(std::mem::size_of::<DictObj>(), cls);
480        unsafe { std::ptr::write(std::ptr::addr_of_mut!((*(h as *mut DictObj)).table), table) };
481        Value::from_ptr(h)
482    }
483
484    pub fn alloc_tuple(&mut self, items: &[Value]) -> Value {
485        let h = self.alloc_raw(TupleObj::size_for(items.len()), BUILTINS.tuple);
486        unsafe {
487            let t = &mut *(h as *mut TupleObj);
488            t.len = items.len();
489            std::ptr::copy_nonoverlapping(items.as_ptr(), t.elems_mut(), items.len());
490        }
491        Value::from_ptr(h)
492    }
493
494    pub fn alloc_closure(&mut self, func: u32, caps: &[Value]) -> Value {
495        let h = self.alloc_raw(ClosureObj::size_for(caps.len()), BUILTINS.closure);
496        unsafe {
497            let c = &mut *(h as *mut ClosureObj);
498            c.func = func;
499            c.ncaps = caps.len() as u32;
500            std::ptr::copy_nonoverlapping(caps.as_ptr(), c.caps_mut(), caps.len());
501        }
502        Value::from_ptr(h)
503    }
504
505    pub fn alloc_cell(&mut self, v: Value) -> Value {
506        let h = self.alloc_raw(std::mem::size_of::<CellObj>(), BUILTINS.cell);
507        unsafe { (*(h as *mut CellObj)).value = AtomicU64::new(v.0) };
508        Value::from_ptr(h)
509    }
510
511    pub fn alloc_string_builder(&mut self, s: String) -> Value {
512        let h = self.alloc_raw(std::mem::size_of::<StringBuilderObj>(), BUILTINS.string_builder);
513        unsafe { std::ptr::write((*(h as *mut StringBuilderObj)).buf.get(), s) };
514        Value::from_ptr(h)
515    }
516
517    pub fn alloc_mutex(&mut self, v: Value) -> Value {
518        let h = self.alloc_raw(std::mem::size_of::<MutexObj>(), BUILTINS.mutex);
519        unsafe {
520            std::ptr::write(&mut (*(h as *mut MutexObj)).raw, parking_lot::ReentrantMutex::new(()));
521            (*(h as *mut MutexObj)).value = AtomicU64::new(v.0);
522            (*h).set_state(ObjState::Shared);
523        }
524        Value::from_ptr(h)
525    }
526
527    pub fn alloc_atomic(&mut self, v: Value) -> Value {
528        let h = self.alloc_raw(std::mem::size_of::<AtomicObj>(), BUILTINS.atomic);
529        unsafe {
530            (*(h as *mut AtomicObj)).value = AtomicU64::new(v.0);
531            (*h).set_state(ObjState::Shared);
532        }
533        Value::from_ptr(h)
534    }
535
536    pub fn alloc_channel(&mut self, capacity: usize) -> Value {
537        let h = self.alloc_raw(std::mem::size_of::<ChannelObj>(), BUILTINS.channel);
538        let inner = Box::new(ChannelInner {
539            queue: parking_lot::Mutex::new(ChannelState { items: Default::default(), closed: false }),
540            not_empty: parking_lot::Condvar::new(),
541            not_full: parking_lot::Condvar::new(),
542            capacity,
543            waiters: parking_lot::Mutex::new(Vec::new()),
544        });
545        unsafe {
546            std::ptr::write(&mut (*(h as *mut ChannelObj)).inner, inner);
547            (*h).set_state(ObjState::Shared);
548        }
549        Value::from_ptr(h)
550    }
551
552    pub fn alloc_thread(&mut self) -> Value {
553        let h = self.alloc_raw(std::mem::size_of::<ThreadObj>(), BUILTINS.thread);
554        let inner = std::sync::Arc::new(ThreadInner {
555            outcome: parking_lot::Mutex::new(ThreadOutcome::Running),
556            done: parking_lot::Condvar::new(),
557            handle: parking_lot::Mutex::new(None),
558            tid: AtomicU64::new(0),
559        });
560        unsafe {
561            std::ptr::write(&mut (*(h as *mut ThreadObj)).inner, inner);
562            (*h).set_state(ObjState::Shared);
563        }
564        Value::from_ptr(h)
565    }
566
567    pub fn alloc_native(&mut self, data: Box<dyn NativeData>) -> Value {
568        let h = self.alloc_raw(std::mem::size_of::<NativeObj>(), BUILTINS.native);
569        unsafe { std::ptr::write(&mut (*(h as *mut NativeObj)).data, data) };
570        Value::from_ptr(h)
571    }
572
573    /// Allocates a frozen native handle: one whose data is immutable or synchronizes itself, so
574    /// it may be stored in frozen objects and used from any thread (`NativeHandle` in the prelude).
575    pub fn alloc_native_frozen(&mut self, data: Box<dyn NativeData>) -> Value {
576        let v = self.alloc_native(data);
577        // A leaf, like a string: no children to escape.
578        unsafe { (*v.as_ptr()).set_state(ObjState::Frozen) };
579        v
580    }
581
582    pub fn alloc_range(&mut self, first: Value, last: Value, exclusive: bool) -> Value {
583        let v = self.alloc_instance(BUILTINS.range);
584        let r = unsafe { as_instance(v) };
585        r.set(0, first);
586        r.set(1, last);
587        r.set(2, Value::bool(exclusive));
588        unsafe { (*v.as_ptr()).set_state(ObjState::Frozen) };
589        v
590    }
591}
592
593/// Memory held outside GC pages (vector buffers) counts toward the collection budget.
594pub fn note_external(bytes: usize) {
595    if bytes > 0 {
596        catallaxy_gc::stats::note_allocated(bytes);
597    }
598}
599
600/// A region in which native code can root values across calls back into the interpreter (which
601/// may collect). Handles borrow the scope, so the borrow checker keeps them from escaping it;
602/// dropping the scope releases them.
603///
604/// ```ignore
605/// let scope = HandleScope::new(&mut vm.m);
606/// let list = scope.root(make_list(scope.mutator()));
607/// vm_call_that_may_collect();
608/// let still_valid = scope.get(list);
609/// ```
610pub struct HandleScope<'m> {
611    m: *mut Mutator,
612    base: usize,
613    _borrow: std::marker::PhantomData<&'m mut Mutator>,
614}
615
616/// A rooted value, valid for the lifetime of its [`HandleScope`].
617#[derive(Copy, Clone)]
618pub struct Local<'s> {
619    index: usize,
620    _scope: std::marker::PhantomData<&'s ()>,
621}
622
623impl<'m> HandleScope<'m> {
624    pub fn new(m: &'m mut Mutator) -> Self {
625        let base = m.handles_len();
626        HandleScope { m, base, _borrow: std::marker::PhantomData }
627    }
628
629    /// A scope that does not borrow the mutator, so native code can keep calling into the VM
630    /// while it holds handles. Handles still cannot outlive the scope.
631    ///
632    /// # Safety
633    /// `m` must outlive the scope and belong to the current thread.
634    pub unsafe fn detached(m: *mut Mutator) -> HandleScope<'static> {
635        let base = (*m).handles_len();
636        HandleScope { m, base, _borrow: std::marker::PhantomData }
637    }
638
639    /// Roots `v` until the scope ends.
640    pub fn root<'s>(&'s self, v: Value) -> Local<'s> {
641        // SAFETY: the scope holds the mutator's unique borrow; handles are only read by the
642        // collector while this thread is stopped.
643        let hs = unsafe { &mut *(*self.m).handles };
644        hs.push(v);
645        Local { index: hs.len() - 1, _scope: std::marker::PhantomData }
646    }
647
648    pub fn get(&self, h: Local<'_>) -> Value {
649        unsafe { (&(*(*self.m).handles))[h.index] }
650    }
651
652    pub fn set(&self, h: Local<'_>, v: Value) {
653        unsafe { (&mut (*(*self.m).handles))[h.index] = v }
654    }
655
656    /// The mutator, for allocation. Allocation never collects, so handles stay valid.
657    #[allow(clippy::mut_from_ref)]
658    pub fn mutator(&self) -> &mut Mutator {
659        unsafe { &mut *self.m }
660    }
661}
662
663impl Drop for HandleScope<'_> {
664    fn drop(&mut self) {
665        unsafe { (*(*self.m).handles).truncate(self.base) };
666    }
667}