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}