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}