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