Skip to main content

praxis_runtime/
collections.rs

1//! The `Vec[T]` collection descriptor (§11.2, ADR-013).
2//!
3//! §11.2 maps `Vec[T]` to a Rust `Vec<GcRef>`. The static element type `T` is
4//! enforced by the compiler and **recorded in the collection object's payload**
5//! (§11.2: "recorded in the collection object's type descriptor"). The payload
6//! therefore carries an element descriptor alongside the items, so `trace`,
7//! `format`, and `equals` dispatch element-wise without a type switch.
8//!
9//! This is the composite type that proves nested `GcRef` tracing (ADR-013):
10//! `trace` forwards every element to the tracer.
11//!
12//! `items` is growable, so `push` mutates the vector *in place* — matching
13//! §4.2's "a `var` binding may still point to a mutable object" and §11.1's
14//! `push -> Unit` (the receiver is mutated, no new reference returned). The
15//! backing storage may reallocate internally, but the `VecPayload` object itself
16//! stays at the same GC address (non-moving collector, ADR-011), so existing
17//! `GcRef`s remain valid. Per §11.5, runtime wrappers never expose an interior
18//! pointer to the vector's backing buffer across a capacity-mutating op; they
19//! reload from the payload each call.
20//!
21//! `VecPayload.items` is a [`ReprCVec<GcRef>`](crate::repr_c_vec::ReprCVec) and
22//! not a `std::Vec<GcRef>`: same three words, same size, same growth machinery,
23//! but `#[repr(C)]`, so the length and the element pointer are at offsets a
24//! backend is allowed to bake in (ADR-118). `DequePayload` and `GridPayload` are
25//! deliberately not migrated; see the ADR.
26
27use std::fmt::Write as _;
28
29use crate::DynamicHasher;
30use crate::GcRef;
31use crate::descriptor::{BuiltinTypeId, FormatSink, Tracer, TypeDescriptor};
32use crate::repr_c_vec::ReprCVec;
33
34/// The `Vec[T]` payload: the element descriptor plus the growable items.
35///
36/// `items` grows in place (§11.1) and is `Drop`, so [`VEC`]'s `drop_value`
37/// releases its buffer on sweep (§12.5). The element descriptor is a `'static`
38/// borrow and owns nothing.
39#[repr(C)]
40pub struct VecPayload {
41    /// The descriptor for every element in `items`, or **null** when this
42    /// vector has not been told its element type. Read by
43    /// `trace`/`format`/`equals` to dispatch without a scattered type switch
44    /// (§11.4); read it through [`VecPayload::element`], not directly.
45    ///
46    /// Null is the honest encoding of "unknown", and it only survives while the
47    /// vector is empty: the first `push` adopts the pushed value's descriptor.
48    /// A vector that has been told its element type is never retagged.
49    pub element_descriptor: *const TypeDescriptor,
50    /// The elements, in order. Growable (not `Box<[T]>`) so `push` mutates in
51    /// place, and a [`ReprCVec`] rather than a `std::Vec` so the length and the
52    /// element pointer are at offsets generated code is allowed to know
53    /// (ADR-118). `std::Vec` is `#[repr(Rust)]` and hides both inside a private
54    /// `RawVec`.
55    pub items: ReprCVec<GcRef>,
56}
57
58// The offsets generated code bakes in, pinned here rather than asserted in prose
59// (ADR-118). `element_descriptor` is at 0 — it is what `same_element` and every
60// `element()` call reads — so `items` starts at 8, its element pointer is at 8
61// and its length at 16.
62const _: () = assert!(std::mem::offset_of!(VecPayload, element_descriptor) == 0);
63const _: () = assert!(std::mem::offset_of!(VecPayload, items) == 8);
64// 8 + 24 = 32, which is the block size class this payload falls in (ADR-109).
65const _: () = assert!(std::mem::size_of::<VecPayload>() == 32);
66
67/// The one site generated code may read a `Vec[T]`'s three words through
68/// (ADR-118 part 2). `v.len()` reads the length; `v[i]` reads the length for the
69/// bounds test and then the element.
70///
71/// Minted here, beside the payload whose alignment and field offset it names,
72/// for `INLINE_INTERN_SITE`'s reason: [`InlineSliceSite::new`] is `pub(crate)`,
73/// so the set of payloads generated code may walk is a list this crate wrote.
74/// **`GridPayload` is the reason that matters** — it is also a leading word
75/// followed by a growable vector, at a different offset and behind a different
76/// descriptor, and a `Grid` walked as a `Vec` would read its width as an
77/// element pointer.
78#[cfg(not(feature = "std-vec-payload"))]
79pub const INLINE_VEC_SITE: crate::repr_c_vec::InlineSliceSite =
80    crate::repr_c_vec::InlineSliceSite::new(
81        BuiltinTypeId::Vec,
82        std::mem::align_of::<VecPayload>(),
83        std::mem::offset_of!(VecPayload, items),
84        std::mem::size_of::<GcRef>(),
85    );
86
87impl VecPayload {
88    /// The element descriptor, or `None` if this vector was never told its
89    /// element type. `None` implies `items` is empty.
90    ///
91    /// [`ElementSeq::element`] is the body; this forwarder is the *public*
92    /// spelling, and it exists because [`ElementSeq`] is `pub(crate)` while one
93    /// caller is not: `praxis-codegen-cranelift`'s adversarial audit reads an
94    /// empty `Vec[Float]`'s descriptor through this method, which is the check
95    /// that codegen labelled the vector before any `push` could repair it.
96    #[must_use]
97    pub fn element(&self) -> Option<&'static TypeDescriptor> {
98        ElementSeq::element(self)
99    }
100}
101
102impl ElementSeq for VecPayload {
103    fn element_descriptor(&self) -> *const TypeDescriptor {
104        self.element_descriptor
105    }
106
107    fn items(&self) -> impl ExactSizeIterator<Item = GcRef> {
108        self.items.iter().copied()
109    }
110
111    fn extra_shape(&self) -> Option<u64> {
112        None
113    }
114}
115
116/// Whether two collections agree on their element type.
117///
118/// Descriptors are `static`, so pointer identity is the authoritative test
119/// where both sides *have* one (ADR-038).
120///
121/// **A null slot agrees with anything**, which is ADR-066 decision 5's rule
122/// applied here: a null slot is not the label `Unknown`, it is the *absence* of
123/// a label, and what answers instead is the value's own descriptor. A
124/// collection with no label has no elements — that is [`VecPayload::element`]'s
125/// documented invariant, and the constructors uphold it — so there is no
126/// element whose descriptor could disagree, and the length check every caller
127/// performs immediately afterwards is what makes the two collections equal or
128/// not. No element-wise dispatch can go wrong through a null: the side without
129/// a label contributes no elements to dispatch over.
130///
131/// **An unlearned label is never compared against a learned one**, which would
132/// treat the label as the authority it explicitly is not: a never-inserted
133/// `Map[Text, Int]`'s `values()` carries no label, and it must still equal an
134/// equally-typed empty `Vec[Int]`.
135///
136/// Two collections that *have each* been told their element type must agree,
137/// so an empty `Vec[Int]` is not an empty `Vec[Text]`.
138pub(crate) fn same_element(a: *const TypeDescriptor, b: *const TypeDescriptor) -> bool {
139    a.is_null() || b.is_null() || std::ptr::eq(a, b)
140}
141
142/// A payload's descriptor **label** slot, as an `Option`. Null means the
143/// collection was never told that type — the *absence* of a label rather than
144/// the label `Unknown`, which is ADR-066 decision 5 and the same null
145/// [`same_element`] above treats as agreeing with anything.
146///
147/// Every labelled payload in the crate reads its slot through here: the three
148/// element sequences via [`ElementSeq::element`], `Map`'s key and value, `Set`'s
149/// and `Counter`'s and both heaps' element, and [`crate::repr::instance_repr`]
150/// when it recovers a value's type arguments. The dereference is the same one
151/// each time, so the reason it is sound is stated once.
152#[inline]
153pub(crate) fn nullable(d: *const TypeDescriptor) -> Option<&'static TypeDescriptor> {
154    // SAFETY: a non-null label is always a `&'static` written by the constructor
155    // that built the payload, or by the first store that taught it its type
156    // (`adopt_or_reject`). Descriptors are `static`s and outlive every payload.
157    (!d.is_null()).then(|| unsafe { &*d })
158}
159
160// ===========================================================================
161// The element-wise descriptor callbacks, written once (§11.4).
162//
163// To a descriptor, `Vec`, `Deque` and `Grid` are the same collection: a
164// nullable element descriptor and a sequence of `GcRef`s. So trace/format/
165// equals/hash are generic over `ElementSeq` and named monomorphised
166// (`seq_trace::<VecPayload>`) at each `TypeDescriptor::builtin` call — what the
167// descriptor stores is still one direct, payload-specific function, and the
168// element rule `same_element` above documents is written once.
169//
170// Comments elsewhere in the crate, and several ADRs, name these callbacks
171// `vec_`/`deque_`/`grid_trace`, `_format`, `_equals` and `_hash`. Those nine
172// names are the `seq_*` generics below; there is no function by any of them.
173//
174// Only `drop` and `owned_bytes` remain per payload, because those *are* per
175// payload: three different backing stores to free.
176// ===========================================================================
177
178/// What the element-wise callbacks need from a collection payload: a nullable
179/// element descriptor, the elements in order, and any further shape that is
180/// part of the collection's identity.
181pub(crate) trait ElementSeq {
182    /// The descriptor for every element, or **null** when the collection has
183    /// not been told its element type. Read it through
184    /// [`element`](Self::element) rather than dereferencing it.
185    fn element_descriptor(&self) -> *const TypeDescriptor;
186
187    /// The elements, in order — row-major for a [`GridPayload`].
188    fn items(&self) -> impl ExactSizeIterator<Item = GcRef>;
189
190    /// Shape beyond the element count that is part of the collection's
191    /// identity: a [`GridPayload`]'s width, and `None` for the flat sequences,
192    /// which have none. A 2×3 `Grid` is not a 3×2 one however its cells fall,
193    /// so `equals` compares this and `hash` folds it in.
194    fn extra_shape(&self) -> Option<u64>;
195
196    /// The element descriptor, or `None` if this collection was never told its
197    /// element type. `None` implies the collection is empty.
198    #[must_use]
199    fn element(&self) -> Option<&'static TypeDescriptor> {
200        nullable(self.element_descriptor())
201    }
202}
203
204unsafe fn seq_trace<S: ElementSeq>(payload: *mut u8, tracer: &mut dyn Tracer) {
205    // SAFETY: caller guarantees `payload` points at an initialized `S`.
206    let p = unsafe { &*(payload as *const S) };
207    for item in p.items() {
208        tracer.trace(item);
209    }
210}
211
212unsafe fn seq_format<S: ElementSeq>(payload: *const u8, out: &mut FormatSink<'_>) {
213    // SAFETY: caller guarantees `payload` points at an initialized `S`.
214    let p = unsafe { &*(payload as *const S) };
215    let _ = out.write_str("[");
216    // No element descriptor means no elements to format.
217    let Some(elem_desc) = p.element() else {
218        let _ = out.write_str("]");
219        return;
220    };
221    for (i, item) in p.items().enumerate() {
222        if i > 0 {
223            let _ = out.write_str(", ");
224        }
225        // Route element formatting through the element descriptor (§11.4).
226        let elem_payload = item.payload::<u8>() as *const u8;
227        // SAFETY: the descriptor came from the schema for this slot, so the slot's
228        // payload is the type its `format` expects.
229        unsafe { (elem_desc.format)(elem_payload, out) };
230    }
231    let _ = out.write_str("]");
232}
233
234unsafe fn seq_equals<S: ElementSeq>(a: *const u8, b: *const u8) -> bool {
235    // SAFETY: caller guarantees both pointers point at initialized `S`s with
236    // compatible element descriptors.
237    let pa = unsafe { &*(a as *const S) };
238    let pb = unsafe { &*(b as *const S) };
239    // Runtime element type is part of collection identity — a `Grid`'s cell
240    // type no less than a `Vec`'s element type. Without it an empty `Vec[Int]`
241    // and an empty `Vec[Text]` compare equal (both are "zero elements") and a
242    // non-empty pair dispatches the *left* element descriptor's callback against
243    // the right's payloads.
244    if !same_element(pa.element_descriptor(), pb.element_descriptor()) {
245        return false;
246    }
247    if pa.extra_shape() != pb.extra_shape() {
248        return false;
249    }
250    if pa.items().len() != pb.items().len() {
251        return false;
252    }
253    // Element-wise equality through the element descriptor (§11.4). If the
254    // element type is not equatable, the collection is not equatable (§5.5).
255    let Some(elem) = pa.element() else {
256        // Both are element-typeless, hence both empty, hence equal.
257        return true;
258    };
259    let Some(eq) = elem.equals else {
260        return false;
261    };
262    for (x, y) in pa.items().zip(pb.items()) {
263        let xe = x.payload::<u8>() as *const u8;
264        let ye = y.payload::<u8>() as *const u8;
265        // SAFETY: both slots were just checked to carry the same descriptor, and it
266        // is the one whose `equals` this is.
267        if !unsafe { eq(xe, ye) } {
268            return false;
269        }
270    }
271    true
272}
273
274unsafe fn seq_hash<S: ElementSeq>(payload: *const u8, hasher: &mut dyn DynamicHasher) {
275    // SAFETY: caller guarantees `payload` points at an initialized `S`.
276    let p = unsafe { &*(payload as *const S) };
277    let Some(hash_elem) = p.element().and_then(|d| d.hash) else {
278        return;
279    };
280    // Length first to distinguish prefixes (standard sequence-hash practice),
281    // then the rest of the shape, so two collections holding the same cells in
282    // the same order but laid out differently do not hash alike.
283    hasher.write_bytes(&(p.items().len() as u64).to_le_bytes());
284    if let Some(shape) = p.extra_shape() {
285        hasher.write_bytes(&shape.to_le_bytes());
286    }
287    for item in p.items() {
288        let elem_payload = item.payload::<u8>() as *const u8;
289        // SAFETY: the descriptor came from the schema for this slot, so the slot's
290        // payload is the type its `hash` expects.
291        unsafe { hash_elem(elem_payload, hasher) };
292    }
293}
294
295unsafe fn vec_drop(payload: *mut u8) {
296    // SAFETY: caller guarantees `payload` points at an initialized `VecPayload`.
297    // `drop_in_place` runs `ReprCVec`'s `Drop`, which hands the three words back
298    // to a `Vec` and lets it free the buffer — one single free. The element
299    // descriptor is a static reference and is not owned.
300    unsafe { std::ptr::drop_in_place(payload as *mut VecPayload) };
301}
302
303/// Descriptor for the `Vec[T]` collection (§11.2). The per-instance element
304/// type lives in the payload, so a single descriptor serves all `Vec[T]`.
305pub static VEC: TypeDescriptor = TypeDescriptor::builtin::<VecPayload>(
306    BuiltinTypeId::Vec,
307    "Vec",
308    seq_trace::<VecPayload>,
309    vec_drop,
310    seq_format::<VecPayload>,
311    Some(seq_equals::<VecPayload>),
312    Some(seq_hash::<VecPayload>),
313    // No container order: a mutable collection can never be a `Map` key or a
314    // `Set` member (ADR-057 D4), so nothing ever has to put one in a
315    // deterministic sequence (ADR-138).
316    None,
317)
318.with_owned_bytes(vec_owned_bytes);
319
320impl VecPayload {
321    /// The heap bytes this payload owns beyond its GC block, for GC pacing —
322    /// the buffer, not the spine's three words. `capacity`, not `len`: the
323    /// buffer's real footprint is what the collector is paced against.
324    ///
325    /// **One statement of the size, with two readers** (ADR-121). The
326    /// descriptor's `owned_bytes` callback charges it once at construction;
327    /// the ABI wrapper that can *grow* this collection reads it either side of
328    /// the mutation and charges the delta through
329    /// [`Heap::charge_owned_growth`](crate::heap::Heap::charge_owned_growth),
330    /// so the pacer sees a buffer that doubled. Writing the capacity arithmetic
331    /// at the growth site instead would be a second spelling of this line, and
332    /// the two would drift the first time an element type changed width.
333    ///
334    /// **This is the statement, for every payload in the crate.** The rule is
335    /// the same for a `Deque`'s ring, a `Grid`'s cells, a `Map`'s table, a
336    /// heap's array and a `BitSet`'s words, so each of those `owned_bytes`
337    /// methods says what it multiplies and points back here for why.
338    #[must_use]
339    pub(crate) fn owned_bytes(&self) -> usize {
340        self.items.capacity() * std::mem::size_of::<GcRef>()
341    }
342}
343
344unsafe fn vec_owned_bytes(payload: *const u8) -> usize {
345    // SAFETY: caller guarantees `payload` points at an initialized VecPayload.
346    let p = unsafe { &*(payload as *const VecPayload) };
347    p.owned_bytes()
348}
349
350// ===========================================================================
351// Deque[T] (§6.1). A double-ended queue backed by Rust's `VecDeque`.
352// Mirrors `VecPayload` exactly (element descriptor + growable items) — it is an
353// `ElementSeq` like the other two, so trace/format/equals/hash are not merely
354// identical but the same bodies; only the backing store and the front/back
355// method surface differ.
356// ===========================================================================
357
358use std::collections::VecDeque;
359
360/// The `Deque[T]` payload: the element descriptor plus a growable `VecDeque`.
361/// `VecDeque` (not `Vec`) so `push_front`/`pop_front` are O(1) amortized.
362/// Both fields are `Drop`, so [`DEQUE`]'s `drop_value` releases them on sweep.
363#[repr(C)]
364pub struct DequePayload {
365    /// The descriptor for every element, or null for "not told yet" —
366    /// [`VecPayload::element_descriptor`]'s contract exactly. Read it through
367    /// [`ElementSeq::element`].
368    pub element_descriptor: *const TypeDescriptor,
369    /// The elements. A `VecDeque` so both ends are cheap to mutate.
370    pub items: VecDeque<GcRef>,
371}
372
373impl ElementSeq for DequePayload {
374    fn element_descriptor(&self) -> *const TypeDescriptor {
375        self.element_descriptor
376    }
377
378    fn items(&self) -> impl ExactSizeIterator<Item = GcRef> {
379        self.items.iter().copied()
380    }
381
382    fn extra_shape(&self) -> Option<u64> {
383        None
384    }
385}
386
387unsafe fn deque_drop(payload: *mut u8) {
388    // SAFETY: caller guarantees `payload` points at an initialized DequePayload.
389    unsafe { std::ptr::drop_in_place(payload as *mut DequePayload) };
390}
391
392/// Descriptor for the `Deque[T]` collection (§6.1). The per-instance element
393/// type lives in the payload, so a single descriptor serves all `Deque[T]`.
394pub static DEQUE: TypeDescriptor = TypeDescriptor::builtin::<DequePayload>(
395    BuiltinTypeId::Deque,
396    "Deque",
397    seq_trace::<DequePayload>,
398    deque_drop,
399    seq_format::<DequePayload>,
400    Some(seq_equals::<DequePayload>),
401    Some(seq_hash::<DequePayload>),
402    // No container order: a mutable collection can never be a `Map` key or a
403    // `Set` member (ADR-057 D4), so nothing ever has to put one in a
404    // deterministic sequence (ADR-138).
405    None,
406)
407.with_owned_bytes(deque_owned_bytes);
408
409impl DequePayload {
410    /// The ring buffer this payload owns beyond its GC block, for GC pacing —
411    /// `capacity`, not `len`.
412    ///
413    /// One statement of the size, with two readers (ADR-121):
414    /// [`VecPayload::owned_bytes`] is that statement.
415    #[must_use]
416    pub(crate) fn owned_bytes(&self) -> usize {
417        self.items.capacity() * std::mem::size_of::<GcRef>()
418    }
419}
420
421unsafe fn deque_owned_bytes(payload: *const u8) -> usize {
422    // SAFETY: caller guarantees `payload` points at an initialized DequePayload.
423    let p = unsafe { &*(payload as *const DequePayload) };
424    p.owned_bytes()
425}
426
427/// A validated `Vec` length: a non-negative item count the runtime can actually
428/// allocate.
429///
430/// The third of ADR-041 decision 1's validated newtypes, beside [`GridExtent`]
431/// and `BitIndex`, and it exists for the same reason they do: `Vec(n, fill)`
432/// (ADR-146) takes a user-supplied `Int` to a `vec![fill; n]`, so `n = -1` would
433/// cast to `usize::MAX` and ask the host for 147 exabytes — an OOM abort raised
434/// inside an `extern "C"` function, which the program that caused it never gets
435/// to see. `praxis_vec_filled` cannot reach the allocation without one of these,
436/// so the guard is not something a caller can forget.
437#[derive(Clone, Copy, Debug, PartialEq, Eq)]
438pub struct VecExtent {
439    len: usize,
440}
441
442impl VecExtent {
443    /// The longest `Vec` the runtime will construct at a stroke: 2^28 items,
444    /// which is [`GridExtent::MAX_CELLS`] and is 2 GiB of `GcRef` storage before
445    /// a single element object exists.
446    ///
447    /// The same number as a grid's for the same reason ADR-041 decision 2 gave:
448    /// a cell of one and an item of the other are the same eight bytes, and a
449    /// count that merely fits in a `usize` is still an allocation no host can
450    /// serve. `push` is not bounded by this and does not need to be — it grows
451    /// one item at a time, so there is no single multiplication to overflow.
452    pub const MAX_ITEMS: usize = GridExtent::MAX_CELLS;
453
454    /// The extent `len` names, or `None` if it is negative or exceeds
455    /// [`MAX_ITEMS`](Self::MAX_ITEMS). Zero is legal and names the empty `Vec`.
456    #[must_use]
457    pub const fn new(len: i64) -> Option<VecExtent> {
458        if len < 0 {
459            return None;
460        }
461        // Now non-negative, so the cast is exact on a 64-bit host.
462        let len = len as usize;
463        if len > Self::MAX_ITEMS {
464            return None;
465        }
466        Some(VecExtent { len })
467    }
468
469    /// The item count, proven allocatable.
470    #[inline]
471    #[must_use]
472    pub const fn len(self) -> usize {
473        self.len
474    }
475
476    /// Whether the extent names the empty `Vec`. Present because clippy asks
477    /// any type with a `len` for it, and it is the honest answer.
478    #[inline]
479    #[must_use]
480    pub const fn is_empty(self) -> bool {
481        self.len == 0
482    }
483}
484
485// ===========================================================================
486// Grid[T] (§7.5 `grid`, §7.8 type derivation). Row-major storage with a known
487// width, so the synthesized type is the spec-faithful `Grid[T]`.
488// ===========================================================================
489
490/// A validated grid shape: a non-negative width and height whose product is a
491/// cell count the runtime can actually allocate.
492///
493/// This is the *only* route from a user-supplied `Int` pair to a cell count.
494/// Reaching `vec![unit; (w as usize) * (h as usize)]` directly would let
495/// `w = -1` become `usize::MAX` and the product overflow — either an allocation
496/// the host cannot serve (an OOM abort) or a capacity-overflow panic, both
497/// crossing `extern "C"`. Neither is expressible here: `GridExtent` holds
498/// `usize`s, and the multiplication it proves is the one `cells()` returns.
499#[derive(Clone, Copy, Debug, PartialEq, Eq)]
500pub struct GridExtent {
501    width: usize,
502    height: usize,
503    cells: usize,
504}
505
506impl GridExtent {
507    /// The largest grid the runtime will construct: 2^28 cells, which is 2 GiB
508    /// of `GcRef` storage before a single cell object exists.
509    ///
510    /// A cap, not a `checked_mul`, because a product that merely *fits* in a
511    /// `usize` is still an allocation no host can serve — `Grid[Int](2^40, 2)`
512    /// multiplies cleanly and then aborts the process. The number is a judgement
513    /// about what a Praxis program plausibly asks for; a program that wants more
514    /// gets a fault it can see rather than a SIGKILL it cannot.
515    pub const MAX_CELLS: usize = 1 << 28;
516
517    /// The extent `width × height` names, or `None` if either side is negative
518    /// or the grid would exceed [`MAX_CELLS`](Self::MAX_CELLS).
519    #[must_use]
520    pub const fn new(width: i64, height: i64) -> Option<GridExtent> {
521        if width < 0 || height < 0 {
522            return None;
523        }
524        // Both are now non-negative, so the casts are exact on a 64-bit host.
525        let (width, height) = (width as usize, height as usize);
526        let Some(cells) = width.checked_mul(height) else {
527            return None;
528        };
529        if cells > Self::MAX_CELLS {
530            return None;
531        }
532        Some(GridExtent {
533            width,
534            height,
535            cells,
536        })
537    }
538
539    /// The column count.
540    #[inline]
541    #[must_use]
542    pub const fn width(self) -> usize {
543        self.width
544    }
545
546    /// The row count.
547    #[inline]
548    #[must_use]
549    pub const fn height(self) -> usize {
550        self.height
551    }
552
553    /// The total cell count — `width * height`, proven not to overflow.
554    #[inline]
555    #[must_use]
556    pub const fn cells(self) -> usize {
557        self.cells
558    }
559}
560
561/// The `Grid[T]` payload: a row-major sequence of `GcRef`s plus the fixed
562/// column count (width). `items.len() == width * height`. Mirrors `VecPayload`
563/// but carries rectangular shape so indexing and neighbourhood walks are cheap.
564#[repr(C)]
565pub struct GridPayload {
566    /// The descriptor for every cell in `items`, or null for "not told yet" —
567    /// [`VecPayload::element_descriptor`]'s contract exactly. Read it through
568    /// [`ElementSeq::element`].
569    pub element_descriptor: *const TypeDescriptor,
570    /// Row-major cells: `items[row * width + col]`.
571    pub items: Vec<GcRef>,
572    /// The number of columns (all rows share this width).
573    pub width: usize,
574}
575
576impl ElementSeq for GridPayload {
577    fn element_descriptor(&self) -> *const TypeDescriptor {
578        self.element_descriptor
579    }
580
581    fn items(&self) -> impl ExactSizeIterator<Item = GcRef> {
582        self.items.iter().copied()
583    }
584
585    /// The width, which is the shape a flat sequence does not have: the same
586    /// cells in the same order are a different `Grid` at a different width, so
587    /// `equals` and `hash` must see it.
588    fn extra_shape(&self) -> Option<u64> {
589        Some(self.width as u64)
590    }
591}
592
593unsafe fn grid_drop(payload: *mut u8) {
594    // SAFETY: caller guarantees `payload` points at an initialized GridPayload.
595    unsafe { std::ptr::drop_in_place(payload as *mut GridPayload) };
596}
597
598/// Descriptor for the `Grid[T]` collection (§7.8). Element-wise, like Vec —
599/// literally so: the callbacks are the shared `ElementSeq` ones, and the width
600/// reaches them as `extra_shape`. It is equatable and hashable, but not a
601/// `Map` key: that requires immutability too (ADR-057 D4).
602pub static GRID: TypeDescriptor = TypeDescriptor::builtin::<GridPayload>(
603    BuiltinTypeId::Grid,
604    "Grid",
605    seq_trace::<GridPayload>,
606    grid_drop,
607    seq_format::<GridPayload>,
608    Some(seq_equals::<GridPayload>),
609    Some(seq_hash::<GridPayload>),
610    // No container order: a mutable collection can never be a `Map` key or a
611    // `Set` member (ADR-057 D4), so nothing ever has to put one in a
612    // deterministic sequence (ADR-138).
613    None,
614)
615.with_owned_bytes(grid_owned_bytes);
616
617impl GridPayload {
618    /// The row-major cell buffer this payload owns beyond its GC block, for GC
619    /// pacing — `capacity`, not `cells()`. A grid is built at its full extent,
620    /// but what the allocator was asked for is still the vector's.
621    ///
622    /// One statement of the size, with two readers (ADR-121):
623    /// [`VecPayload::owned_bytes`] is that statement.
624    #[must_use]
625    pub(crate) fn owned_bytes(&self) -> usize {
626        self.items.capacity() * std::mem::size_of::<GcRef>()
627    }
628}
629
630unsafe fn grid_owned_bytes(payload: *const u8) -> usize {
631    // SAFETY: caller guarantees `payload` points at an initialized GridPayload.
632    let p = unsafe { &*(payload as *const GridPayload) };
633    p.owned_bytes()
634}
635
636#[cfg(test)]
637mod tests {
638    // The Vec descriptor is exercised end-to-end through the Heap in heap.rs
639    // (allocation, tracing, collection of nested references). Here we only
640    // sanity-check the descriptor is well-formed.
641    use super::*;
642
643    /// ADR-041 decision 1's guarantee, for the third newtype: the only route
644    /// from a source `Int` to a `Vec` allocation size refuses a negative length
645    /// and one past the cap — the two that would otherwise end the process.
646    /// Zero is not one of them: the empty `Vec` is a `Vec`.
647    #[test]
648    fn a_vec_extent_refuses_a_negative_or_absurd_length() {
649        assert!(VecExtent::new(-1).is_none());
650        assert!(VecExtent::new(i64::MIN).is_none());
651        assert!(VecExtent::new(VecExtent::MAX_ITEMS as i64 + 1).is_none());
652        assert!(VecExtent::new(i64::MAX).is_none());
653
654        assert_eq!(VecExtent::new(0).expect("zero is a length").len(), 0);
655        assert!(VecExtent::new(0).expect("zero is a length").is_empty());
656        assert_eq!(VecExtent::new(7).expect("seven is a length").len(), 7);
657        assert_eq!(
658            VecExtent::new(VecExtent::MAX_ITEMS as i64)
659                .expect("the cap itself is allowed")
660                .len(),
661            VecExtent::MAX_ITEMS
662        );
663    }
664
665    /// The two caps are one number, stated once. ADR-041 decision 2 says a
666    /// `Vec` item and a `Grid` cell are the same eight bytes, so a change to one
667    /// bound that left the other behind would be a judgement made twice and
668    /// agreed with once.
669    #[test]
670    fn a_vecs_cap_is_a_grids_cap() {
671        assert_eq!(VecExtent::MAX_ITEMS, GridExtent::MAX_CELLS);
672    }
673
674    #[test]
675    fn vec_descriptor_reports_capabilities() {
676        assert!(VEC.is_equatable());
677        assert!(VEC.is_hashable());
678        assert_eq!(VEC.name, "Vec");
679    }
680
681    #[test]
682    fn empty_vectors_with_different_element_types_are_not_equal() {
683        let rt = crate::Runtime::new();
684        let ints = rt.alloc_vec(&crate::scalars::INT, Vec::new());
685        let floats = rt.alloc_vec(&crate::scalars::FLOAT, Vec::new());
686
687        assert!(
688            !ints.equals(&floats),
689            "a collection's element descriptor is part of its runtime type identity"
690        );
691    }
692
693    /// The one thing a `Grid` adds to the shared element-wise callbacks: its
694    /// width is part of its identity, and it reaches `seq_equals` as
695    /// [`ElementSeq::extra_shape`]. Same cells, same order, different rectangle.
696    #[test]
697    fn grids_that_differ_only_in_width_are_not_equal() {
698        let rt = crate::Runtime::new();
699        let cells: Vec<GcRef> = (0..6_i64).map(|v| rt.alloc_int(v)).collect();
700        let two_wide = rt.alloc_grid(&crate::scalars::INT, cells.clone(), 2);
701        let three_wide = rt.alloc_grid(&crate::scalars::INT, cells.clone(), 3);
702        let also_two_wide = rt.alloc_grid(&crate::scalars::INT, cells, 2);
703
704        assert!(
705            !two_wide.equals(&three_wide),
706            "a 3×2 grid is not a 2×3 one however its cells fall"
707        );
708        assert!(
709            two_wide.equals(&also_two_wide),
710            "the shape check must not reject two grids that agree on it"
711        );
712    }
713
714    // ADR-118. The three layout properties generated code rests on, asserted
715    // against a real heap-allocated payload rather than a standalone
716    // `ReprCVec`.
717
718    #[test]
719    fn a_vec_payload_is_thirty_two_bytes_with_the_items_at_offset_eight() {
720        // Also a `const _` above; repeated as a test because the number is what
721        // decides the block's size class (ADR-109) and a silent change to it
722        // would move every `Vec` to a different page pool.
723        assert_eq!(std::mem::size_of::<VecPayload>(), 32);
724        assert_eq!(std::mem::offset_of!(VecPayload, element_descriptor), 0);
725        assert_eq!(std::mem::offset_of!(VecPayload, items), 8);
726    }
727
728    // Only without `std-vec-payload`: under that feature the payload holds a
729    // `std::Vec`, whose field order is exactly the thing nothing is allowed to
730    // assume — so this test failing there is the toggle working, not broken.
731    #[cfg(not(feature = "std-vec-payload"))]
732    #[test]
733    fn a_backend_can_read_the_length_and_the_elements_out_of_a_live_payload() {
734        // Everything below is a load at a constant displacement from the payload
735        // pointer generated code already holds. `praxis_vec_len` is the word at
736        // 16; `praxis_vec_get` is a bounds compare against it and a load through
737        // the word at 8.
738        let rt = crate::Runtime::new();
739        let elements: Vec<GcRef> = (0..7_i64).map(|v| rt.alloc_int(v)).collect();
740        let vec_ref = rt.alloc_vec(&crate::scalars::INT, elements);
741
742        let base = vec_ref.payload::<u8>().cast_const();
743        // SAFETY: `vec_ref` is a live `Vec`, so `base` addresses an initialized
744        // `VecPayload` whose layout the `const _` assertions above pin: the
745        // element pointer at 8 and the length at 16.
746        let (items_ptr, len) = unsafe {
747            (
748                base.add(8).cast::<*const GcRef>().read(),
749                base.add(16).cast::<usize>().read(),
750            )
751        };
752        assert_eq!(len, 7);
753        for i in 0..len {
754            // SAFETY: `i < len`, and `items_ptr` is the live element buffer.
755            let element = unsafe { *items_ptr.add(i) };
756            assert!(std::ptr::eq(element.descriptor(), &crate::scalars::INT));
757            // SAFETY: the descriptor check above proves the payload is an `Int`.
758            assert_eq!(unsafe { *element.payload::<i64>() }, i as i64);
759        }
760    }
761
762    /// The same three words, reached the way generated code reaches them:
763    /// through [`INLINE_VEC_SITE`], from the **object** base, with the header
764    /// size folded in by the site rather than added at the emit site.
765    ///
766    /// A second test rather than an edit of the one above because the two pin
767    /// different things: that one pins `VecPayload`'s field order, this one pins
768    /// the arithmetic the site performs on top of it.
769    #[cfg(not(feature = "std-vec-payload"))]
770    #[test]
771    fn the_inline_vec_site_addresses_a_live_vec_from_its_object_base() {
772        let rt = crate::Runtime::new();
773        let elements: Vec<GcRef> = (0..5_i64).map(|v| rt.alloc_int(v)).collect();
774        let vec_ref = rt.alloc_vec(&crate::scalars::INT, elements);
775
776        assert!(
777            std::ptr::eq(INLINE_VEC_SITE.type_id().descriptor(), vec_ref.descriptor()),
778            "the site names the descriptor the proof compares against, and it \
779             must be the one a live `Vec` carries"
780        );
781
782        let base = vec_ref.as_ptr().cast::<u8>().cast_const();
783        // SAFETY: `vec_ref` is a live `Vec` whose descriptor was just checked
784        // against the site's, so the site's two displacements address the
785        // element pointer and the length of an initialized `ReprCVec`.
786        let (items, len) = unsafe {
787            (
788                base.add(INLINE_VEC_SITE.elements_offset())
789                    .cast::<*const GcRef>()
790                    .read(),
791                base.add(INLINE_VEC_SITE.len_offset())
792                    .cast::<usize>()
793                    .read(),
794            )
795        };
796        assert_eq!(len, 5);
797        assert_eq!(INLINE_VEC_SITE.element_shift(), 3, "a GcRef is eight bytes");
798        for i in 0..len {
799            // SAFETY: `i < len` and `items` is the live element buffer.
800            let element = unsafe { *items.add(i) };
801            // SAFETY: every element was allocated as an `Int` above.
802            assert_eq!(unsafe { *element.payload::<i64>() }, i as i64);
803        }
804    }
805
806    #[test]
807    fn a_vec_that_reallocates_across_a_collection_keeps_every_element() {
808        // The failure this is aimed at: a payload left holding the address of a
809        // buffer `RawVec` has already grown away from. `seq_trace` walks
810        // `items` on every mark, so a stale pointer here is a wild read inside
811        // the collector rather than a wrong answer.
812        let rt = crate::Runtime::new();
813        let mut scope = crate::roots::RootScope::new();
814        let vec_ref = rt.alloc_vec(&crate::scalars::INT, Vec::new());
815        scope.root(vec_ref);
816
817        // Outside the intern range (ADR-100), so every element is a real block
818        // the sweep can reclaim rather than an immortal it cannot.
819        const BASE: i64 = 1_000_000;
820        for i in 0..512_i64 {
821            let element = rt.alloc_int(BASE + i);
822            // Growing the buffer is a Rust `malloc`, not a GC allocation, so
823            // nothing collects between the `alloc_int` and the `push` and the
824            // element needs no root of its own.
825            //
826            // SAFETY: `vec_ref` is rooted in `scope` and the collector does not
827            // move objects (ADR-011), so the payload address is stable and the
828            // only live reference to it is this one.
829            unsafe { &mut *vec_ref.payload::<VecPayload>() }
830                .items
831                .push(element);
832        }
833
834        // Garbage the sweep must take, so the collection is a real one.
835        for i in 0..64_i64 {
836            let _ = rt.alloc_int(BASE + 100_000 + i);
837        }
838        rt.collect_with(&scope);
839
840        // SAFETY: `vec_ref` was rooted across the collection.
841        let p = unsafe { &*vec_ref.payload::<VecPayload>() };
842        assert_eq!(p.items.len(), 512);
843        assert!(p.items.capacity() >= 512);
844        for (i, item) in p.items.iter().enumerate() {
845            // SAFETY: every element was traced through `items` and survived.
846            assert_eq!(unsafe { *item.payload::<i64>() }, BASE + i as i64);
847        }
848    }
849
850    #[test]
851    fn the_owned_bytes_callback_charges_the_pacer_for_the_whole_buffer() {
852        let rt = crate::Runtime::new();
853        let elements: Vec<GcRef> = (0..10_i64).map(|v| rt.alloc_int(v)).collect();
854        let vec_ref = rt.alloc_vec(&crate::scalars::INT, elements);
855
856        // SAFETY: `vec_ref` is a live `Vec`.
857        let p = unsafe { &*vec_ref.payload::<VecPayload>() };
858        let expected = p.items.capacity() * std::mem::size_of::<GcRef>();
859        // SAFETY: same payload, and `vec_owned_bytes` is `VEC`'s own callback.
860        let reported = unsafe { vec_owned_bytes(vec_ref.payload::<u8>() as *const u8) };
861        assert_eq!(reported, expected);
862        assert!(reported >= 10 * std::mem::size_of::<GcRef>());
863    }
864}