Skip to main content

praxis_runtime/
descriptor.rs

1//! Type descriptors: the vtable-equivalent for runtime objects (§11.4).
2//!
3//! Every GC object carries a pointer to a [`TypeDescriptor`] that centralizes
4//! all payload-aware operations — tracing, dropping, formatting, equality, and
5//! hashing. The compiler generates one descriptor per type and emits code that
6//! reaches these function pointers through the object header. The point of the
7//! design (§11.4) is that there are no scattered type switches in generated or
8//! runtime code: every operation routes through a descriptor.
9
10use std::collections::hash_map::DefaultHasher;
11use std::fmt;
12use std::hash::{Hash, Hasher};
13
14/// The closed set of built-in runtime types (§11.4).
15///
16/// This enum *is* the type-id registry: a descriptor's [`TypeId`] is derived
17/// from its variant, so two built-ins cannot be labelled with the same id.
18/// Uniqueness reduces to enum-discriminant uniqueness, which rustc already
19/// enforces.
20#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
21#[repr(u32)]
22pub enum BuiltinTypeId {
23    Unit = 0,
24    Bool,
25    Int,
26    Byte,
27    Char,
28    Float,
29    Text,
30    Vec,
31    Deque,
32    Grid,
33    Map,
34    Set,
35    Counter,
36    MinHeap,
37    MaxHeap,
38    BitSet,
39    Tuple,
40    Record,
41    Enum,
42    Closure,
43    VarCell,
44    /// `Range` (§4.11, ADR-059). New variants are appended, so no existing id
45    /// ever moves.
46    Range,
47}
48
49impl BuiltinTypeId {
50    /// Number of built-in types. Kept honest by [`BUILTINS`]'s array length and
51    /// by `builtins_are_indexed_by_their_id`.
52    pub const COUNT: usize = 22;
53
54    /// Total inverse of the discriminant. A `match` rather than a `transmute`,
55    /// so an out-of-range word yields `None` instead of an invalid enum value.
56    pub const fn from_u32(v: u32) -> Option<BuiltinTypeId> {
57        use BuiltinTypeId::*;
58        Some(match v {
59            0 => Unit,
60            1 => Bool,
61            2 => Int,
62            3 => Byte,
63            4 => Char,
64            5 => Float,
65            6 => Text,
66            7 => Vec,
67            8 => Deque,
68            9 => Grid,
69            10 => Map,
70            11 => Set,
71            12 => Counter,
72            13 => MinHeap,
73            14 => MaxHeap,
74            15 => BitSet,
75            16 => Tuple,
76            17 => Record,
77            18 => Enum,
78            19 => Closure,
79            20 => VarCell,
80            21 => Range,
81            _ => return None,
82        })
83    }
84
85    /// This built-in's descriptor. The inverse of
86    /// [`TypeDescriptor::as_builtin`].
87    pub fn descriptor(self) -> &'static TypeDescriptor {
88        BUILTINS[self as usize]
89    }
90}
91
92/// An opaque, interned identifier for a type. Equality on `TypeId` *is* type
93/// identity for descriptor-table lookups.
94///
95/// The inner word is **private**: the only producers are
96/// [`TypeDescriptor::builtin`] (which derives it from a [`BuiltinTypeId`]) and
97/// the test-only escape hatch, so a hand-written integer literal cannot
98/// impersonate a built-in.
99#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
100pub struct TypeId(u32);
101
102impl TypeId {
103    #[inline]
104    pub const fn to_u32(self) -> u32 {
105        self.0
106    }
107
108    /// The built-in this id names, or `None` for a non-built-in (today: only
109    /// the test descriptors, which live at the top of the `u32` range).
110    #[inline]
111    pub const fn as_builtin(self) -> Option<BuiltinTypeId> {
112        BuiltinTypeId::from_u32(self.0)
113    }
114}
115
116/// The tracer a descriptor's `trace` function receives during GC. The collector
117/// supplies a concrete implementation whose own `trace` method enqueues child
118/// references onto the mark worklist (ADR-011).
119pub trait Tracer {
120    /// Mark a `GcRef` as reachable and arrange for it to be traced.
121    fn trace(&mut self, reference: crate::GcRef);
122}
123
124/// A hashing sink used by structural hash descriptors (§5.5). Concrete
125/// implementations feed bytes into a hash state; [`StructHasher`] is the
126/// built-in implementation used by the scalar and collection descriptors.
127pub trait DynamicHasher {
128    fn write_bytes(&mut self, bytes: &[u8]);
129    fn finish(&self) -> u64;
130}
131
132/// The built-in [`DynamicHasher`] backed by [`DefaultHasher`]. Used by every
133/// descriptor's `hash` callback.
134pub struct StructHasher(DefaultHasher);
135
136impl StructHasher {
137    pub fn new() -> Self {
138        StructHasher(DefaultHasher::new())
139    }
140}
141
142impl Default for StructHasher {
143    fn default() -> Self {
144        Self::new()
145    }
146}
147
148impl DynamicHasher for StructHasher {
149    fn write_bytes(&mut self, bytes: &[u8]) {
150        // `Hasher::write` consumes the bytes into the hash state.
151        self.0.write(bytes);
152    }
153
154    fn finish(&self) -> u64 {
155        self.0.finish()
156    }
157}
158
159/// Convenience: feed any `Hash` value into a [`DynamicHasher`] byte-wise.
160pub(crate) fn hash_value<H: DynamicHasher + ?Sized, T: Hash + ?Sized>(hasher: &mut H, value: &T) {
161    // Route through a shim Hasher so we don't re-implement Hash for each scalar.
162    struct HasherShim<'a, H: ?Sized>(&'a mut H);
163    impl<H: DynamicHasher + ?Sized> Hasher for HasherShim<'_, H> {
164        #[inline]
165        fn write(&mut self, bytes: &[u8]) {
166            self.0.write_bytes(bytes);
167        }
168        #[inline]
169        fn finish(&self) -> u64 {
170            self.0.finish()
171        }
172    }
173    value.hash(&mut HasherShim(hasher));
174}
175
176/// `trace` callback shape: receive a pointer to the object payload (the bytes
177/// after the header) plus a tracer, and report any `GcRef`s stored inside.
178///
179/// # Safety
180/// The `payload` pointer must point at a value of the descriptor's type for the
181/// duration of the call.
182pub type TraceFn = unsafe fn(payload: *mut u8, tracer: &mut dyn Tracer);
183
184/// `drop_value` callback shape: release Rust-owned resources held in the
185/// payload (e.g. the backing `Vec<GcRef>` of a `Vec[T]`). Invoked during sweep
186/// (§12.5).
187///
188/// # Safety
189/// `payload` must point at a value of the descriptor's type, and afterwards the
190/// memory is no longer valid.
191pub type DropFn = unsafe fn(payload: *mut u8);
192
193/// Which of the two renderings a `format` callback is producing.
194///
195/// Exactly one type reads this — `Text` — and it is one bit rather than a second
196/// callback per descriptor because of *nesting*: a `Vec[Text]` renders its
197/// elements through the element descriptor's `format`, so whatever distinguishes
198/// the two renderings has to travel down that recursion. A `format_debug` field
199/// beside `format` would not: `vec_format` would have to know which of the two
200/// it was itself running as in order to pick the right one for its elements, and
201/// that knowledge is exactly this enum.
202#[derive(Clone, Copy, PartialEq, Eq, Debug)]
203pub enum FormatStyle {
204    /// The program's own rendering: `out(v)`, `"{v}"` interpolation, `to_text()`
205    /// and `praxis run`'s result line. A `Text` is its characters, because that
206    /// is what a program printing a string means (§16.1, and §8.1 for the
207    /// interpolation that shares the callback).
208    Display,
209    /// The **debugger's** rendering: a locals row, a TUI pane cell, `p EXPR`. A
210    /// `Text` is a quoted literal here, because a display that gives each value
211    /// one line and no other context cannot afford a value that renders as zero
212    /// characters, as a newline, or as something an adjacent `"` could have
213    /// ended.
214    Debug,
215}
216
217/// The writer a `format` callback appends to, carrying the [`FormatStyle`] it is
218/// rendering under.
219///
220/// A wrapper rather than an extra parameter, so that a container passing its
221/// writer to an element descriptor passes the style with it and cannot forget
222/// to.
223pub struct FormatSink<'a> {
224    out: &'a mut dyn fmt::Write,
225    style: FormatStyle,
226}
227
228impl<'a> FormatSink<'a> {
229    /// A sink in the program's own rendering.
230    pub fn display(out: &'a mut dyn fmt::Write) -> FormatSink<'a> {
231        FormatSink {
232            out,
233            style: FormatStyle::Display,
234        }
235    }
236
237    /// A sink in the debugger's rendering.
238    pub fn debug(out: &'a mut dyn fmt::Write) -> FormatSink<'a> {
239        FormatSink {
240            out,
241            style: FormatStyle::Debug,
242        }
243    }
244
245    /// The style this sink renders under.
246    #[must_use]
247    pub fn style(&self) -> FormatStyle {
248        self.style
249    }
250
251    /// A sink over `out` in a style read off another one.
252    ///
253    /// For the callbacks that render a part into a scratch `String` before
254    /// placing it — `map_format` orders its entries by key and has to have them
255    /// rendered to place them (ADR-138 decision 4). Such a callback holds its
256    /// [`style`](Self::style) across the buffer and rebuilds a sink around it,
257    /// which is what keeps a `Map[Text, Text]` quoting its keys and values in
258    /// the debugger, exactly as a `Vec[Text]` does without needing a buffer at
259    /// all.
260    ///
261    /// Every scratch buffer is a place the style could be dropped, and dropping
262    /// it is silent — the value still renders, just in the other rendering. The
263    /// style is `Copy` so that carrying it across is the easy thing to write.
264    pub fn styled(out: &'a mut dyn fmt::Write, style: FormatStyle) -> FormatSink<'a> {
265        FormatSink { out, style }
266    }
267}
268
269impl fmt::Write for FormatSink<'_> {
270    fn write_str(&mut self, s: &str) -> fmt::Result {
271        self.out.write_str(s)
272    }
273}
274
275/// `format` callback shape: append the user-visible representation of the value
276/// to the given writer, in that writer's [`FormatStyle`].
277///
278/// # Safety
279/// `payload` must point at a value of the descriptor's type.
280pub type FormatFn = unsafe fn(payload: *const u8, out: &mut FormatSink<'_>);
281
282/// `equals` callback shape: structural equality between two values of the same
283/// descriptor. `None` on the descriptor means the type is not equatable.
284///
285/// # Safety
286/// Both pointers must point at values of the descriptor's type.
287pub type EqualsFn = unsafe fn(a: *const u8, b: *const u8) -> bool;
288
289/// `hash` callback shape: feed the value's structural identity into a hasher.
290/// `None` on the descriptor means the type is not hashable.
291///
292/// # Safety
293/// `payload` must point at a value of the descriptor's type.
294pub type HashFn = unsafe fn(payload: *const u8, hasher: &mut dyn DynamicHasher);
295
296/// `owned_bytes` callback shape: how many bytes *outside* the object's
297/// `[header|payload]` block this value owns — the `Box<str>` behind a `Text`,
298/// the `Vec`'s buffer behind a `Vec[T]`, the `HashMap`'s table behind a
299/// `Map[K,V]`.
300///
301/// `None` on the descriptor means "nothing beyond the payload", which is the
302/// truth for every scalar and the reason this is opt-in rather than a required
303/// constructor argument.
304///
305/// The collector's pacing counter reads it at allocation. Without it a 1 MiB
306/// `Text` would charge the same 40 bytes as an `Int`, and a text-heavy program
307/// would under-report its own pressure by essentially its whole footprint.
308///
309/// # Safety
310/// `payload` must point at a value of the descriptor's type.
311pub type OwnedBytesFn = unsafe fn(payload: *const u8) -> usize;
312
313/// `compare` callback shape: total ordering between two values of the same
314/// descriptor. `None` on the descriptor means the type has no container order.
315///
316/// This is the ordering a **container** imposes — a heap's `Ord`, a sort, the
317/// sequence a `Map` or `Set` prints and iterates in — and it is total, including
318/// over `Float` NaN (which sorts last and equals itself). The source-level `<`
319/// on a `Float` keeps IEEE semantics and is a different operation; see ADR-045.
320///
321/// Populated on every type a `Map` key or `Set` member can be: `Int`, `Byte`,
322/// `Char`, `Float`, `Text`, `Bool`, `Unit`, `Range`, and tuples, records and
323/// enums recursing through their element types. `None` on the eleven that can
324/// never be one — the nine collections, `Closure` and `VarCell` (ADR-138
325/// decision 1). That is deliberately a *different* set from
326/// `praxis_hir::capability::supports_ord`, which is the source language's `<`
327/// and `sorted()`: a tuple has a container order and no `<`, and
328/// `(1, 2) < (1, 3)` is still `Y006` (ADR-138 decision 3).
329///
330/// # Safety
331/// Both pointers must point at values of the descriptor's type.
332pub type CompareFn = unsafe fn(a: *const u8, b: *const u8) -> std::cmp::Ordering;
333
334/// Centralized table of operations on a value's payload (§11.4).
335///
336/// Exact Rust types may evolve, but all payload-aware operations must live here
337/// rather than in scattered type switches.
338///
339/// `id`, `size` and `align` are private and *derived*: a built-in descriptor is
340/// constructible only through [`TypeDescriptor::builtin`], which takes the
341/// [`BuiltinTypeId`] the id comes from and the payload type the layout comes
342/// from. "A descriptor whose id names a different type" and "a descriptor whose
343/// size disagrees with its payload" are therefore unrepresentable.
344#[derive(Clone, Copy)]
345pub struct TypeDescriptor {
346    id: TypeId,
347    pub name: &'static str,
348    size: usize,
349    align: usize,
350    pub trace: TraceFn,
351    pub drop_value: DropFn,
352    pub format: FormatFn,
353    pub equals: Option<EqualsFn>,
354    pub hash: Option<HashFn>,
355    pub compare: Option<CompareFn>,
356    /// Bytes this value owns outside its allocation block, for GC pacing.
357    /// `None` means none — the scalar case, and the default. Set with
358    /// [`TypeDescriptor::with_owned_bytes`].
359    pub owned_bytes: Option<OwnedBytesFn>,
360}
361
362impl TypeDescriptor {
363    /// The only constructor for a built-in descriptor. `id` is derived from
364    /// `builtin`; `size`/`align` are derived from the payload type `P`.
365    ///
366    /// Built-in descriptors must be declared as `static`, never `const`: a
367    /// `const` reference is a promoted rvalue with no guaranteed unique
368    /// address, and descriptor *pointer* identity is what the runtime compares.
369    #[allow(clippy::too_many_arguments)]
370    pub const fn builtin<P>(
371        builtin: BuiltinTypeId,
372        name: &'static str,
373        trace: TraceFn,
374        drop_value: DropFn,
375        format: FormatFn,
376        equals: Option<EqualsFn>,
377        hash: Option<HashFn>,
378        compare: Option<CompareFn>,
379    ) -> TypeDescriptor {
380        TypeDescriptor {
381            id: TypeId(builtin as u32),
382            name,
383            size: std::mem::size_of::<P>(),
384            align: std::mem::align_of::<P>(),
385            trace,
386            drop_value,
387            format,
388            equals,
389            hash,
390            compare,
391            owned_bytes: None,
392        }
393    }
394
395    /// Test-only descriptor whose id is outside the built-in range by
396    /// construction, so a fixture can never collide with a real type.
397    #[cfg(test)]
398    #[allow(clippy::too_many_arguments)]
399    pub const fn for_test<P>(
400        n: u32,
401        name: &'static str,
402        trace: TraceFn,
403        drop_value: DropFn,
404        format: FormatFn,
405        equals: Option<EqualsFn>,
406        hash: Option<HashFn>,
407        compare: Option<CompareFn>,
408    ) -> TypeDescriptor {
409        TypeDescriptor {
410            id: TypeId(u32::MAX - n),
411            name,
412            size: std::mem::size_of::<P>(),
413            align: std::mem::align_of::<P>(),
414            trace,
415            drop_value,
416            format,
417            equals,
418            hash,
419            compare,
420            owned_bytes: None,
421        }
422    }
423
424    /// Declare that this type owns memory outside its allocation block, and how
425    /// to measure it.
426    ///
427    /// A builder rather than a constructor argument because the default —
428    /// "nothing beyond the payload" — is right for every scalar and for
429    /// `VarCell` and `Range`, and a required argument would make each of those
430    /// declarations spell out the same `None`.
431    #[must_use]
432    pub const fn with_owned_bytes(self, owned_bytes: OwnedBytesFn) -> TypeDescriptor {
433        TypeDescriptor {
434            id: self.id,
435            name: self.name,
436            size: self.size,
437            align: self.align,
438            trace: self.trace,
439            drop_value: self.drop_value,
440            format: self.format,
441            equals: self.equals,
442            hash: self.hash,
443            compare: self.compare,
444            owned_bytes: Some(owned_bytes),
445        }
446    }
447
448    /// Bytes `payload` owns outside its allocation block, or 0 if this type
449    /// owns nothing beyond its payload.
450    ///
451    /// # Safety
452    /// `payload` must point at a value of this descriptor's type.
453    #[inline]
454    pub unsafe fn owned_bytes_of(&self, payload: *const u8) -> usize {
455        match self.owned_bytes {
456            // SAFETY: forwarded from this function's contract.
457            Some(f) => unsafe { f(payload) },
458            None => 0,
459        }
460    }
461
462    /// This descriptor's type identity.
463    #[inline]
464    pub const fn id(&self) -> TypeId {
465        self.id
466    }
467
468    /// Which built-in this descriptor is, if any.
469    #[inline]
470    pub const fn as_builtin(&self) -> Option<BuiltinTypeId> {
471        self.id.as_builtin()
472    }
473
474    /// Size in bytes of this type's payload.
475    #[inline]
476    pub const fn size(&self) -> usize {
477        self.size
478    }
479
480    /// Alignment in bytes of this type's payload.
481    #[inline]
482    pub const fn align(&self) -> usize {
483        self.align
484    }
485
486    /// True iff values of this type participate in structural equality (§5.5).
487    #[inline]
488    pub fn is_equatable(&self) -> bool {
489        self.equals.is_some()
490    }
491
492    /// True iff values of this type have a structural hash (§5.5).
493    ///
494    /// Not the same as "may be a `Map` key": a `Vec` hashes and can never be a
495    /// key (ADR-057 D4). That question is
496    /// `praxis_hir::capability::supports_hash_stable`.
497    #[inline]
498    pub fn is_hashable(&self) -> bool {
499        self.hash.is_some()
500    }
501
502    /// True iff values of this type have a **container** order — the sequence a
503    /// `Map`, `Set`, `Counter` or heap puts them in (ADR-138).
504    ///
505    /// Not the source language's `<`: that is
506    /// `praxis_hir::capability::supports_ord`, and it is a strictly smaller set
507    /// on purpose. A tuple answers `true` here and is still refused by `<`.
508    #[inline]
509    pub fn is_orderable(&self) -> bool {
510        self.compare.is_some()
511    }
512}
513
514/// A descriptor together with the Rust type of the payload it describes.
515///
516/// [`TypeDescriptor::builtin`] takes the payload type `P`, derives `size`/`align`
517/// from it, and then **erases it**. An allocator that took the payload as a bare
518/// generic could therefore only compare widths at *runtime* —
519/// `gc_alloc(ctx, &scalars::INT, 0)` passes an `i32`, because Rust's default
520/// integer type is not `i64`, and aborts the process with "payload size mismatch
521/// for descriptor Int" from inside `extern "C"`. That is the non-unwinding panic
522/// across the ABI §10.4 forbids, and it cannot fire until the wrong call runs.
523///
524/// `Payload<T>` re-attaches the type. The pairing is checked once, where the
525/// handle is declared — [`Payload::new`] is a `const fn` whose assertions run
526/// during const evaluation, so a `static`/`const` handle whose `T` is not its
527/// descriptor's payload **fails to compile**. And because the allocators take
528/// the handle and the value together, the value's type is checked at every call
529/// site by ordinary type inference. Neither mistake reaches a runtime assert.
530pub struct Payload<T: Copy> {
531    descriptor: &'static TypeDescriptor,
532    /// `fn() -> T` rather than `T`: invariance is not wanted here, and this
533    /// marker leaves `Payload<T>` `Copy`/`Send`/`Sync` whatever `T` is.
534    _payload: std::marker::PhantomData<fn() -> T>,
535}
536
537// Derived impls would demand `T: Clone`/`T: Copy` bounds that the marker makes
538// unnecessary — a handle is two words of shared metadata, not a value.
539impl<T: Copy> Clone for Payload<T> {
540    fn clone(&self) -> Self {
541        *self
542    }
543}
544impl<T: Copy> Copy for Payload<T> {}
545
546impl<T: Copy> Payload<T> {
547    /// Pair `descriptor` with the payload type `T`.
548    ///
549    /// Declare the result as a `const` or `static` — that is what makes the
550    /// check a compile-time one. Called in a runtime expression the assertions
551    /// are ordinary ones, which is the situation this type exists to remove.
552    ///
553    /// # Panics
554    /// During const evaluation, if `T`'s layout is not the one `descriptor`
555    /// declares.
556    #[must_use]
557    pub const fn new(descriptor: &'static TypeDescriptor) -> Payload<T> {
558        assert!(
559            std::mem::size_of::<T>() == descriptor.size(),
560            "payload type is not this descriptor's width"
561        );
562        assert!(
563            std::mem::align_of::<T>() == descriptor.align(),
564            "payload type is not this descriptor's alignment"
565        );
566        Payload {
567            descriptor,
568            _payload: std::marker::PhantomData,
569        }
570    }
571
572    /// The descriptor this handle carries. Its *address* is the type's identity,
573    /// and the handle holds the one `static`, so that identity survives.
574    #[must_use]
575    pub const fn descriptor(self) -> &'static TypeDescriptor {
576        self.descriptor
577    }
578
579    /// Read the payload at `payload` as this handle's `T`.
580    ///
581    /// The **width is the compiler's**: it is `size_of::<T>()`, and
582    /// [`Payload::new`] proved during const evaluation that that is exactly the
583    /// descriptor's declared width. A caller therefore cannot pick a width, and
584    /// cannot pick the wrong one — a hand-written read of a one-byte `Bool`
585    /// through an `i64` consumes seven bytes of arena padding the allocator
586    /// never initialized.
587    ///
588    /// This is the read half of what `Payload<T>` already does for allocation.
589    /// It does **not** check the object's descriptor — a handle names a type but
590    /// a raw payload pointer carries no header — so callers that hold a `GcRef`
591    /// should reach for the wrapper that checks identity first.
592    ///
593    /// # Safety
594    /// `payload` must point at an initialized payload of this handle's type,
595    /// aligned for `T`.
596    #[must_use]
597    #[inline]
598    pub unsafe fn read(self, payload: *const u8) -> T {
599        // SAFETY: the caller guarantees `payload` is an initialized, aligned
600        // payload of this descriptor's type, and `Payload::new` already proved
601        // `T`'s layout is that type's layout.
602        unsafe { payload.cast::<T>().read() }
603    }
604}
605
606impl<T: Copy> fmt::Debug for Payload<T> {
607    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
608        f.debug_struct("Payload")
609            .field("descriptor", &self.descriptor.name)
610            .field("size", &std::mem::size_of::<T>())
611            .finish()
612    }
613}
614
615impl fmt::Debug for TypeDescriptor {
616    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617        f.debug_struct("TypeDescriptor")
618            .field("id", &self.id)
619            .field("name", &self.name)
620            .field("size", &self.size)
621            .field("align", &self.align)
622            .field("equatable", &self.is_equatable())
623            .field("hashable", &self.is_hashable())
624            .finish()
625    }
626}
627
628/// Every built-in descriptor, indexed by its [`BuiltinTypeId`] discriminant.
629///
630/// This is the registry `BuiltinTypeId::descriptor` reads and the array
631/// `builtins_are_indexed_by_their_id` walks; adding a variant without adding an
632/// entry here is a compile error on the array length.
633pub static BUILTINS: [&TypeDescriptor; BuiltinTypeId::COUNT] = [
634    &crate::scalars::UNIT,
635    &crate::scalars::BOOL,
636    &crate::scalars::INT,
637    &crate::scalars::BYTE,
638    &crate::scalars::CHAR,
639    &crate::scalars::FLOAT,
640    &crate::text::TEXT,
641    &crate::collections::VEC,
642    &crate::collections::DEQUE,
643    &crate::collections::GRID,
644    &crate::maps::MAP,
645    &crate::maps::SET,
646    &crate::maps::COUNTER,
647    &crate::heaps::MIN_HEAP,
648    &crate::heaps::MAX_HEAP,
649    &crate::bitset::BITSET,
650    &crate::tuples::TUPLE,
651    &crate::records::RECORD,
652    &crate::enums::ENUM,
653    &crate::closures::CLOSURE,
654    &crate::var_cell::VAR_CELL,
655    &crate::range::RANGE,
656];
657
658/// [`BUILTINS`] as raw addresses, for
659/// [`RuntimeContext::descriptors`](crate::RuntimeContext::descriptors) to hold
660/// by value (ADR-116).
661///
662/// **Derived rather than written out, which is the point.** Generated code
663/// proves a value's type by loading slot `id` of that array and comparing it
664/// against the header's descriptor word (ADR-102), so a slot holding a
665/// neighbour's descriptor would be a proof of the wrong type. Mapping
666/// `BUILTINS` here leaves the registry as the one place the index-to-descriptor
667/// correspondence is stated, and `builtins_are_indexed_by_their_id` as the one
668/// gate on it.
669///
670/// A `fn` and not a `const fn`: const evaluation may not read a `static`, and
671/// `BUILTINS` is one deliberately — the addresses *are* the identities
672/// (`builtin_descriptors_have_a_stable_address`). It is called once per
673/// [`Runtime::context`](crate::Runtime::context), which is once per program
674/// run, not per call into generated code.
675#[must_use]
676pub fn builtin_descriptor_addresses() -> [*const TypeDescriptor; BuiltinTypeId::COUNT] {
677    BUILTINS.map(|d| d as *const TypeDescriptor)
678}
679
680#[cfg(test)]
681mod tests {
682    use super::*;
683
684    /// Smoke test: a descriptor can be constructed and copied, and the
685    /// `is_equatable` / `is_hashable` flags reflect the optional callbacks.
686    /// The function pointers here are dummies that must never be called — the
687    /// point is that the *type* is well-formed.
688    unsafe fn dummy_trace(_: *mut u8, _: &mut dyn Tracer) {}
689    unsafe fn dummy_drop(_: *mut u8) {}
690    unsafe fn dummy_format(_: *const u8, _: &mut FormatSink<'_>) {}
691    unsafe fn dummy_eq(a: *const u8, b: *const u8) -> bool {
692        a == b
693    }
694    unsafe fn dummy_hash(_: *const u8, _: &mut dyn DynamicHasher) {}
695
696    #[test]
697    fn descriptor_constructs_and_reports_capabilities() {
698        static EQUATABLE_ONLY: TypeDescriptor = TypeDescriptor::for_test::<i64>(
699            0,
700            "EquatableOnly",
701            dummy_trace,
702            dummy_drop,
703            dummy_format,
704            Some(dummy_eq),
705            None,
706            None,
707        );
708        assert!(EQUATABLE_ONLY.is_equatable());
709        assert!(!EQUATABLE_ONLY.is_hashable());
710        assert!(!EQUATABLE_ONLY.is_orderable());
711
712        static HASHABLE: TypeDescriptor = TypeDescriptor::for_test::<[u64; 2]>(
713            1,
714            "Key",
715            dummy_trace,
716            dummy_drop,
717            dummy_format,
718            Some(dummy_eq),
719            Some(dummy_hash),
720            None,
721        );
722        assert!(HASHABLE.is_equatable());
723        assert!(HASHABLE.is_hashable());
724        assert_eq!(HASHABLE.size(), 16);
725        assert_eq!(HASHABLE.align(), 8);
726    }
727
728    /// A test descriptor's id is outside the built-in range by construction, so
729    /// a fixture can never be mistaken for a real type.
730    #[test]
731    fn test_descriptor_ids_are_not_builtins() {
732        static PROBE: TypeDescriptor = TypeDescriptor::for_test::<u8>(
733            0,
734            "Probe",
735            dummy_trace,
736            dummy_drop,
737            dummy_format,
738            None,
739            None,
740            None,
741        );
742        assert_eq!(PROBE.as_builtin(), None);
743    }
744
745    #[test]
746    fn builtin_type_ids_are_globally_unique() {
747        let mut by_id = std::collections::BTreeMap::new();
748
749        for descriptor in BUILTINS {
750            if let Some(previous) = by_id.insert(descriptor.id(), descriptor.name) {
751                panic!(
752                    "built-in descriptors {previous} and {} share {:?}; descriptor IDs are runtime type identity",
753                    descriptor.name,
754                    descriptor.id()
755                );
756            }
757        }
758        assert_eq!(by_id.len(), BuiltinTypeId::COUNT);
759    }
760
761    /// The registry is a lookup table: `BUILTINS[b as usize]` must be the
762    /// descriptor whose id *is* `b`. Without this, `BuiltinTypeId::descriptor`
763    /// would silently return a neighbour.
764    #[test]
765    fn builtins_are_indexed_by_their_id() {
766        for (index, descriptor) in BUILTINS.iter().enumerate() {
767            assert_eq!(
768                descriptor.id().to_u32(),
769                index as u32,
770                "BUILTINS[{index}] is {} whose id is {:?}",
771                descriptor.name,
772                descriptor.id()
773            );
774            let builtin = BuiltinTypeId::from_u32(index as u32).expect("index is in range");
775            assert!(std::ptr::eq(builtin.descriptor(), *descriptor));
776        }
777        assert!(BuiltinTypeId::from_u32(BuiltinTypeId::COUNT as u32).is_none());
778    }
779
780    /// Built-in descriptors are `static`, so their address is their identity.
781    /// Two reads of the same descriptor must produce the same pointer — this is
782    /// what lets the runtime compare descriptors by pointer rather than by id.
783    #[test]
784    fn builtin_descriptors_have_a_stable_address() {
785        assert!(std::ptr::eq(&crate::scalars::INT, &crate::scalars::INT));
786        assert!(std::ptr::eq(
787            BuiltinTypeId::Int.descriptor(),
788            &crate::scalars::INT
789        ));
790        assert!(!std::ptr::eq(
791            &crate::scalars::FLOAT,
792            &crate::text::TEXT as &TypeDescriptor
793        ));
794    }
795}