Skip to main content

catallaxy_runtime/
class.rs

1//! Class descriptors.
2//!
3//! Every heap object's header word 0 points at a [`Class`], whose first field is the GC vtable.
4//! Classes are created when a program loads and live forever. They are frozen: after
5//! [`register_class`] returns, nothing in a class changes, so any thread may read them freely.
6
7use std::sync::atomic::{AtomicPtr, AtomicUsize, Ordering};
8use std::sync::LazyLock;
9
10use catallaxy_gc::{GcVTable, Header};
11use rustc_hash::FxHashMap;
12
13use crate::object;
14use crate::symbol::Sym;
15use crate::value::Value;
16
17/// Physical layout of a class's instances.
18#[derive(Copy, Clone, Debug, PartialEq, Eq)]
19pub enum ObjKind {
20    /// Not a heap object (Int, Float, Bool, Nil).
21    Immediate,
22    /// Fixed fields inline after the header (user classes, enum variants, ranges).
23    Instance,
24    Str,
25    BigInt,
26    List,
27    Dict,
28    Set,
29    Tuple,
30    Closure,
31    Cell,
32    StringBuilder,
33    Mutex,
34    Atomic,
35    Channel,
36    Thread,
37    /// Opaque native resource (files and the like).
38    Native,
39}
40
41/// Which ownership state new instances start in, and how the type may cross threads.
42#[derive(Copy, Clone, Debug, PartialEq, Eq)]
43pub enum Capability {
44    Owned,
45    Frozen,
46    Shared,
47}
48
49#[repr(C)]
50pub struct Class {
51    pub gc: GcVTable,
52    pub kind: ObjKind,
53    pub id: u32,
54    pub name: Box<str>,
55    pub capability: Capability,
56    pub superclass: Option<&'static Class>,
57    pub field_names: Vec<Sym>,
58    /// Method name → function id, including inherited methods. Builtin classes get their
59    /// methods after registration (when the prelude loads), so the table is swapped atomically.
60    methods: AtomicPtr<FxHashMap<Sym, u32>>,
61    /// Virtual method table (function ids), for overridable methods of user classes.
62    pub vtable: Vec<u32>,
63    /// For enum variant classes: the variant's index within its enum.
64    pub variant_tag: Option<u32>,
65    /// For enum variant classes: the enum's id (shared by all its variants).
66    pub enum_id: Option<u32>,
67}
68
69impl Class {
70    #[inline(always)]
71    pub fn nfields(&self) -> usize {
72        self.field_names.len()
73    }
74
75    pub fn lookup(&self, name: Sym) -> Option<u32> {
76        self.methods().get(&name).copied()
77    }
78
79    pub fn methods(&self) -> &FxHashMap<Sym, u32> {
80        // SAFETY: method tables are leaked and never freed or mutated after publication.
81        unsafe { &*self.methods.load(Ordering::Acquire) }
82    }
83
84    /// True if `self` is `other` or inherits from it.
85    pub fn is_subclass_of(&self, other: &Class) -> bool {
86        let mut c = Some(self);
87        while let Some(k) = c {
88            if k.id == other.id {
89                return true;
90            }
91            c = k.superclass;
92        }
93        false
94    }
95
96    pub fn field_index(&self, name: Sym) -> Option<usize> {
97        self.field_names.iter().position(|&f| f == name)
98    }
99}
100
101// ---------------------------------------------------------------------------------------------
102// Registry
103
104const MAX_CLASSES: usize = 1 << 16;
105
106struct Registry {
107    slots: Box<[AtomicPtr<Class>]>,
108    len: AtomicUsize,
109    lock: parking_lot::Mutex<()>,
110}
111
112static REGISTRY: LazyLock<Registry> = LazyLock::new(|| Registry {
113    slots: (0..MAX_CLASSES).map(|_| AtomicPtr::new(std::ptr::null_mut())).collect(),
114    len: AtomicUsize::new(0),
115    lock: parking_lot::Mutex::new(()),
116});
117
118/// Fields of a class under construction. `register_class` assigns the id and the GC vtable.
119pub struct ClassSpec {
120    pub kind: ObjKind,
121    pub name: String,
122    pub capability: Capability,
123    pub superclass: Option<&'static Class>,
124    pub field_names: Vec<Sym>,
125    pub methods: FxHashMap<Sym, u32>,
126    pub vtable: Vec<u32>,
127    pub variant_tag: Option<u32>,
128    pub enum_id: Option<u32>,
129}
130
131impl ClassSpec {
132    pub fn new(kind: ObjKind, name: &str) -> Self {
133        ClassSpec {
134            kind,
135            name: name.to_string(),
136            capability: Capability::Owned,
137            superclass: None,
138            field_names: Vec::new(),
139            methods: FxHashMap::default(),
140            vtable: Vec::new(),
141            variant_tag: None,
142            enum_id: None,
143        }
144    }
145}
146
147/// Registers a class, making it immutable and globally visible. Returns its `'static` descriptor.
148pub fn register_class(spec: ClassSpec) -> &'static Class {
149    let _g = REGISTRY.lock.lock();
150    let id = REGISTRY.len.load(Ordering::Relaxed);
151    assert!(id < MAX_CLASSES, "class limit exceeded");
152    let cls: &'static Class = Box::leak(Box::new(Class {
153        gc: object::vtable_for(spec.kind),
154        kind: spec.kind,
155        id: id as u32,
156        name: spec.name.into_boxed_str(),
157        capability: spec.capability,
158        superclass: spec.superclass,
159        field_names: spec.field_names,
160        methods: AtomicPtr::new(Box::into_raw(Box::new(spec.methods))),
161        vtable: spec.vtable,
162        variant_tag: spec.variant_tag,
163        enum_id: spec.enum_id,
164    }));
165    REGISTRY.slots[id].store(cls as *const Class as *mut Class, Ordering::Release);
166    REGISTRY.len.store(id + 1, Ordering::Release);
167    cls
168}
169
170/// Replaces a class's method table (builtin classes get their methods when the prelude loads).
171/// The previous table is leaked, so concurrent readers stay valid.
172pub fn set_methods(cls: &'static Class, methods: FxHashMap<Sym, u32>) {
173    cls.methods.store(Box::into_raw(Box::new(methods)), Ordering::Release);
174}
175
176pub fn class_by_id(id: u32) -> &'static Class {
177    let p = REGISTRY.slots[id as usize].load(Ordering::Acquire);
178    assert!(!p.is_null(), "unknown class id {id}");
179    unsafe { &*p }
180}
181
182pub fn class_count() -> usize {
183    REGISTRY.len.load(Ordering::Acquire)
184}
185
186// ---------------------------------------------------------------------------------------------
187// Builtin classes. Their ids are fixed so the compiler can refer to them.
188
189pub mod ids {
190    pub const INT: u32 = 0;
191    pub const FLOAT: u32 = 1;
192    pub const BOOL: u32 = 2;
193    pub const NIL: u32 = 3;
194    pub const STRING: u32 = 4;
195    pub const BIGINT: u32 = 5;
196    pub const LIST: u32 = 6;
197    pub const DICT: u32 = 7;
198    pub const SET: u32 = 8;
199    pub const TUPLE: u32 = 9;
200    pub const CLOSURE: u32 = 10;
201    pub const CELL: u32 = 11;
202    pub const STRING_BUILDER: u32 = 12;
203    pub const MUTEX: u32 = 13;
204    pub const ATOMIC: u32 = 14;
205    pub const CHANNEL: u32 = 15;
206    pub const THREAD: u32 = 16;
207    pub const RANGE: u32 = 17;
208    pub const NATIVE: u32 = 18;
209    pub const FIRST_USER: u32 = 19;
210}
211
212pub struct Builtins {
213    pub int: &'static Class,
214    pub float: &'static Class,
215    pub bool: &'static Class,
216    pub nil: &'static Class,
217    pub string: &'static Class,
218    pub bigint: &'static Class,
219    pub list: &'static Class,
220    pub dict: &'static Class,
221    pub set: &'static Class,
222    pub tuple: &'static Class,
223    pub closure: &'static Class,
224    pub cell: &'static Class,
225    pub string_builder: &'static Class,
226    pub mutex: &'static Class,
227    pub atomic: &'static Class,
228    pub channel: &'static Class,
229    pub thread: &'static Class,
230    pub range: &'static Class,
231    pub native: &'static Class,
232}
233
234pub static BUILTINS: LazyLock<Builtins> = LazyLock::new(|| {
235    let mk = |kind: ObjKind, name: &str, cap: Capability, fields: &[&str], expect: u32| {
236        let mut spec = ClassSpec::new(kind, name);
237        spec.capability = cap;
238        spec.field_names = fields.iter().map(|f| crate::symbol::intern(f)).collect();
239        let c = register_class(spec);
240        assert_eq!(c.id, expect, "builtin class {name} registered out of order");
241        c
242    };
243    use Capability::*;
244    use ObjKind::*;
245    Builtins {
246        int: mk(Immediate, "Int", Frozen, &[], ids::INT),
247        float: mk(Immediate, "Float", Frozen, &[], ids::FLOAT),
248        bool: mk(Immediate, "Bool", Frozen, &[], ids::BOOL),
249        nil: mk(Immediate, "Nil", Frozen, &[], ids::NIL),
250        string: mk(Str, "String", Frozen, &[], ids::STRING),
251        bigint: mk(BigInt, "Int", Frozen, &[], ids::BIGINT),
252        list: mk(List, "List", Owned, &[], ids::LIST),
253        dict: mk(Dict, "Dict", Owned, &[], ids::DICT),
254        set: mk(Set, "Set", Owned, &[], ids::SET),
255        tuple: mk(Tuple, "Tuple", Owned, &[], ids::TUPLE),
256        closure: mk(Closure, "Fn", Owned, &[], ids::CLOSURE),
257        cell: mk(Cell, "Cell", Owned, &[], ids::CELL),
258        string_builder: mk(StringBuilder, "StringBuilder", Owned, &[], ids::STRING_BUILDER),
259        mutex: mk(Mutex, "Mutex", Shared, &[], ids::MUTEX),
260        atomic: mk(Atomic, "Atomic", Shared, &[], ids::ATOMIC),
261        channel: mk(Channel, "Channel", Shared, &[], ids::CHANNEL),
262        thread: mk(Thread, "Thread", Shared, &[], ids::THREAD),
263        range: mk(Instance, "Range", Frozen, &["first", "last", "exclusive"], ids::RANGE),
264        native: mk(Native, "NativeHandle", Owned, &[], ids::NATIVE),
265    }
266});
267
268/// Returns the class of any value, including immediates.
269#[inline]
270pub fn class_of(v: Value) -> &'static Class {
271    if v.is_ptr() {
272        unsafe { heap_class(v.as_ptr()) }
273    } else {
274        immediate_class(v)
275    }
276}
277
278#[inline]
279pub fn immediate_class(v: Value) -> &'static Class {
280    let b = &*BUILTINS;
281    if v.is_small_int() {
282        b.int
283    } else if v.is_float() {
284        b.float
285    } else if v.is_bool() {
286        b.bool
287    } else {
288        b.nil
289    }
290}
291
292/// The class of a heap object.
293///
294/// # Safety
295/// `h` must point to a live heap object.
296#[inline(always)]
297pub unsafe fn heap_class(h: *const Header) -> &'static Class {
298    &*((*h).vtable() as *const Class)
299}
300
301/// Class id of a value; bigints report the `Int` class id.
302#[inline]
303pub fn class_id_of(v: Value) -> u32 {
304    if v.is_ptr() {
305        let c = unsafe { heap_class(v.as_ptr()) };
306        if c.id == ids::BIGINT {
307            ids::INT
308        } else {
309            c.id
310        }
311    } else if v.is_small_int() {
312        ids::INT
313    } else if v.is_float() {
314        ids::FLOAT
315    } else if v.is_bool() {
316        ids::BOOL
317    } else {
318        ids::NIL
319    }
320}