Skip to main content

praxis_runtime/
maps.rs

1//! `Map[K, V]`, `Set[T]`, and `Counter[T]` (§6.1, §11.3).
2//!
3//! All three reuse Rust's hash collections behind opaque GC objects:
4//!
5//! - `Map[K, V]`    → `HashMap<DynamicKey, GcRef>` (§11.3)
6//! - `Set[T]`       → `HashSet<DynamicKey>`       (§11.3)
7//! - `Counter[T]`   → `HashMap<DynamicKey, GcRef>` with Int values (§6.2)
8//!
9//! `DynamicKey` (dynamic_key.rs) bridges Praxis values into Rust's `Hash`/`Eq`
10//! by delegating to the descriptor's `hash`/`equals` callbacks — the mechanism
11//! that lets tuples and records be keys. A non-hashable key type (closure) is
12//! rejected at the capability layer (`supports_hash`) before reaching here.
13//!
14//! Counter's defining behavior (§6.2): absent keys read as zero, never fault.
15//! `min=`/`max=` map updates (§6.2) live in the ABI wrappers.
16
17use std::collections::{HashMap, HashSet};
18use std::fmt::{self, Write as _};
19
20use crate::DynamicHasher;
21use crate::GcRef;
22use crate::collections::nullable;
23use crate::descriptor::{BuiltinTypeId, FormatSink, Tracer, TypeDescriptor};
24use crate::dynamic_key::DynamicKey;
25
26// ---------------------------------------------------------------------------
27// Deterministic rendering
28// ---------------------------------------------------------------------------
29
30/// Render one value through `descriptor` into `out`.
31///
32/// # Safety
33/// `value`'s payload must match `descriptor`.
34pub(crate) unsafe fn render_into(
35    out: &mut FormatSink<'_>,
36    descriptor: &TypeDescriptor,
37    value: GcRef,
38) {
39    let payload = value.payload::<u8>() as *const u8;
40    // SAFETY: the caller guarantees the payload matches the descriptor.
41    unsafe { (descriptor.format)(payload, out) };
42}
43
44/// Write already-ordered `entries` between `open` and `close`, comma-separated.
45///
46/// It does **not** sort. The caller orders its keys through [`ordered_entries`]
47/// or [`ordered_members`] and renders in that order, so printing and iterating
48/// are one order by construction rather than by two sorts that happen to agree
49/// (ADR-138 decision 4). Sorting the rendered entry here would not even be that
50/// order: `':'` is below `'1'` in ASCII, so `"a1: 2"` would sort before
51/// `"a: 1"` while the key alone orders `a` first. This function's only job is
52/// the punctuation.
53pub(crate) fn write_ordered<I: Iterator<Item = String>>(
54    out: &mut dyn fmt::Write,
55    open: &str,
56    entries: I,
57    close: &str,
58) {
59    let _ = out.write_str(open);
60    for (i, entry) in entries.enumerate() {
61        if i > 0 {
62            let _ = out.write_str(", ");
63        }
64        let _ = out.write_str(&entry);
65    }
66    let _ = out.write_str(close);
67}
68
69/// The entries of a keyed collection in a **deterministic** order: sorted by the
70/// key's own order (ADR-138).
71///
72/// The order matters because `keys()` and `values()` promise to be index-aligned,
73/// and because a `HashMap`'s own iteration order is randomized per process — the
74/// same program would answer differently on two runs, in a place where the
75/// value, not just the printing, depends on it.
76///
77/// The sort key is [`crate::ordering::container_cmp`], which is the key's own
78/// `TypeDescriptor::compare` — the same callback `sorted()` and a heap's `Ord`
79/// go through. That is what makes `out(m.keys())` and `out(m.keys().sorted())`
80/// agree; sorting the *rendered* key instead would put `10` before `2` and make
81/// a program that walked a `Map[Int, V]` answer in an order no reader would
82/// predict.
83///
84/// `sort_by`, not `sort_unstable_by`: `container_cmp` leaves a tie only for two
85/// keys that render identically, and a stable sort at least keeps the answer
86/// independent of the sort's own internal choices.
87///
88/// # Safety
89/// Every key's payload must match the descriptor it carries.
90pub(crate) unsafe fn ordered_entries(entries: &HashMap<DynamicKey, GcRef>) -> Vec<(GcRef, GcRef)> {
91    let mut rows: Vec<(GcRef, GcRef)> = entries.iter().map(|(k, v)| (k.value(), *v)).collect();
92    // SAFETY: every key's payload matches the descriptor in its own header,
93    // which is what `DynamicKey::new` reads it from.
94    rows.sort_by(|a, b| unsafe { crate::ordering::container_cmp(a.0, b.0) });
95    rows
96}
97
98/// The members of a `Set` in the same **deterministic** order
99/// [`ordered_entries`] gives a keyed collection: sorted by the member's own
100/// order (ADR-138).
101///
102/// `for x in s` iterates a snapshot of this (ADR-066), so the order is the
103/// program's answer and not only its printing — the same reason
104/// [`ordered_entries`] exists. `set_format` renders in this order too, so
105/// `out(s)`, `for x in s` and `s.sorted()` are three readings of one sequence.
106///
107/// # Safety
108/// Every member's payload must match the descriptor it carries.
109pub(crate) unsafe fn ordered_members(entries: &HashSet<DynamicKey>) -> Vec<GcRef> {
110    let mut rows: Vec<GcRef> = entries.iter().map(DynamicKey::value).collect();
111    // SAFETY: as `ordered_entries`.
112    rows.sort_by(|a, b| unsafe { crate::ordering::container_cmp(*a, *b) });
113    rows
114}
115
116/// Render already-ordered `items` between `{` and `}`, each through `render`
117/// into its own scratch buffer.
118///
119/// The shared body of `map_format`, `set_format` and `counter_format`: the
120/// three differ only in what one entry renders as — `key: value` through the
121/// value's own descriptor, a bare member, `key: value` through `INT` — so that
122/// is the closure and everything around it is written once here.
123///
124/// The style is read off `out` **before** the first buffer is built, and held
125/// across them, because [`write_ordered`] borrows `out` for as long as the
126/// iterator it drains — so the entries cannot read the style off the sink they
127/// are ultimately written to. Every scratch buffer is a place the style could
128/// be dropped, and dropping it is silent: the value still renders, just in the
129/// other rendering.
130///
131/// It does not sort. `items` is already in the one order that printing and
132/// iterating share, from [`ordered_entries`] or [`ordered_members`] (ADR-138
133/// decision 4).
134fn write_braced<T>(
135    out: &mut FormatSink<'_>,
136    items: Vec<T>,
137    render: impl Fn(&mut FormatSink<'_>, T),
138) {
139    let style = out.style();
140    let entries = items.into_iter().map(|item| {
141        let mut buf = String::new();
142        {
143            let mut s = FormatSink::styled(&mut buf, style);
144            render(&mut s, item);
145        }
146        buf
147    });
148    write_ordered(out, "{", entries, "}");
149}
150
151// ---------------------------------------------------------------------------
152// Hashing
153// ---------------------------------------------------------------------------
154
155/// The odd 64-bit golden-ratio multiplier that `map_hash` and `counter_hash`
156/// mix a key's hash through before adding the value's, so that `{k1: v2}` and
157/// `{k2: v1}` do not cancel under the commutative (XOR) accumulator.
158///
159/// Named because the literal was written twice, **not** because the two have to
160/// agree. `MAP` and `COUNTER` are distinct descriptors that are never
161/// dispatched against each other, and they already hash the same logical
162/// contents differently on purpose: `map_hash` puts the value through its own
163/// descriptor's `hash` callback, `counter_hash` reads the raw `i64`. Nothing
164/// compares the two hashes, so changing this for one of them alone would be
165/// legal — it is one constant because it is one idea.
166const KEY_HASH_MIX: u64 = 0x9e3779b97f4a7c15;
167
168// ===========================================================================
169// Map[K, V]
170// ===========================================================================
171
172/// The `Map[K, V]` payload (§11.3). Both descriptors are **labels**: what the
173/// construction site knew about the type, or **null** when it knew nothing.
174/// Neither is the authority for an element-wise operation — every key carries
175/// its own descriptor on its [`DynamicKey`] and every value carries one in its
176/// object header, and that is what `format`/`equals`/`hash` dispatch through.
177/// ADR-066 decision 5 is the rule: a null descriptor slot is legal and means
178/// "the value's own descriptor answers". A non-nullable label would force
179/// `praxis_map_new` to spell an unknown type as `INT`, and anything that
180/// trusted it would read a `Map[Text, Text]`'s values as `i64`.
181#[repr(C)]
182pub struct MapPayload {
183    /// The descriptor for every key, or null when the construction site had no
184    /// static key type. Read it through [`MapPayload::key`].
185    pub key_descriptor: *const TypeDescriptor,
186    /// The descriptor for every value, or null when unknown. Read it through
187    /// [`MapPayload::value`].
188    pub value_descriptor: *const TypeDescriptor,
189    /// The entries. Keys are `DynamicKey`; values are `GcRef`.
190    pub entries: HashMap<DynamicKey, GcRef>,
191}
192
193impl MapPayload {
194    /// The key label, or `None` when this map was never told its key type.
195    #[must_use]
196    pub fn key(&self) -> Option<&'static TypeDescriptor> {
197        nullable(self.key_descriptor)
198    }
199
200    /// The value label, or `None` when this map was never told its value type.
201    #[must_use]
202    pub fn value(&self) -> Option<&'static TypeDescriptor> {
203        nullable(self.value_descriptor)
204    }
205}
206
207unsafe fn map_trace(payload: *mut u8, tracer: &mut dyn Tracer) {
208    // SAFETY: caller guarantees `payload` points at an initialized MapPayload.
209    let p = unsafe { &*(payload as *const MapPayload) };
210    for (k, v) in p.entries.iter() {
211        tracer.trace(k.value());
212        tracer.trace(*v);
213    }
214}
215
216unsafe fn map_drop(payload: *mut u8) {
217    // SAFETY: caller guarantees `payload` points at an initialized MapPayload.
218    unsafe { std::ptr::drop_in_place(payload as *mut MapPayload) };
219}
220
221unsafe fn map_format(payload: *const u8, out: &mut FormatSink<'_>) {
222    // SAFETY: caller guarantees `payload` points at an initialized MapPayload.
223    let p = unsafe { &*(payload as *const MapPayload) };
224    // Order first, render second: a printed `Map` is the same sequence a `for`
225    // over it walks, because both read `ordered_entries` (ADR-138 decision 4).
226    // SAFETY: every key's payload matches the descriptor its `DynamicKey` carries.
227    let rows = unsafe { ordered_entries(&p.entries) };
228    write_braced(out, rows, |s, (k, v)| {
229        // SAFETY: the key's payload matches its own header's descriptor, and
230        // so does the value's. Rendering through the *map's* value label
231        // instead would print a `Map[Text, Text]` as integers whenever that
232        // label is a guess.
233        unsafe {
234            render_into(s, k.descriptor(), k);
235            let _ = s.write_str(": ");
236            render_into(s, v.descriptor(), v);
237        }
238    });
239}
240
241unsafe fn map_equals(a: *const u8, b: *const u8) -> bool {
242    // SAFETY: caller guarantees both pointers point at initialized MapPayloads.
243    let pa = unsafe { &*(a as *const MapPayload) };
244    let pb = unsafe { &*(b as *const MapPayload) };
245    if pa.entries.len() != pb.entries.len() {
246        return false;
247    }
248    // Two maps are equal iff they have the same keys with equal values. Each
249    // pair compares through the *left* value's own descriptor, after checking
250    // the right value carries the same one: values of two different types are
251    // never equal, and dispatching one type's `equals` against the other's
252    // payload is precisely the wrong-type read a shared label would license.
253    for (k, va) in pa.entries.iter() {
254        // `get` uses DynamicKey's PartialEq (structural, via the descriptor).
255        let Some(vb) = pb.entries.get(k) else {
256            return false;
257        };
258        if !std::ptr::eq(va.descriptor(), vb.descriptor()) {
259            return false;
260        }
261        let Some(eq) = va.descriptor().equals else {
262            return false;
263        };
264        let va_p = va.payload::<u8>() as *const u8;
265        let vb_p = vb.payload::<u8>() as *const u8;
266        // SAFETY: both values carry the descriptor `eq` came from.
267        if !unsafe { eq(va_p, vb_p) } {
268            return false;
269        }
270    }
271    true
272}
273
274unsafe fn map_hash(payload: *const u8, hasher: &mut dyn DynamicHasher) {
275    // SAFETY: caller guarantees `payload` points at an initialized MapPayload.
276    let p = unsafe { &*(payload as *const MapPayload) };
277    // A map's hash is order-independent: hash the count, then each (key, value)
278    // pair and combine with a commutative accumulator (XOR). This matches the
279    // set/map hashing convention where insertion order must not affect the hash.
280    hasher.write_bytes(&(p.entries.len() as u64).to_le_bytes());
281    let mut acc: u64 = 0;
282    for (k, v) in p.entries.iter() {
283        // Each half hashes through its *own* descriptor rather than the map's
284        // label: equal values must hash equally, and only the value knows what
285        // it is.
286        let (Some(hash_key), Some(hash_val)) = (k.descriptor().hash, v.descriptor().hash) else {
287            return;
288        };
289        let mut kh = crate::descriptor::StructHasher::new();
290        let k_payload = k.value().payload::<u8>() as *const u8;
291        // SAFETY: key payload matches the descriptor its `DynamicKey` carries.
292        unsafe { hash_key(k_payload, &mut kh) };
293        let mut vh = crate::descriptor::StructHasher::new();
294        let v_payload = v.payload::<u8>() as *const u8;
295        // SAFETY: value payload matches the descriptor in its own header.
296        unsafe { hash_val(v_payload, &mut vh) };
297        // Pair the key and value hashes together before XOR, so (k1:v2) ≠ (k2:v1).
298        let pair = kh
299            .finish()
300            .wrapping_mul(KEY_HASH_MIX)
301            .wrapping_add(vh.finish());
302        acc ^= pair;
303    }
304    hasher.write_bytes(&acc.to_le_bytes());
305}
306
307/// Descriptor for `Map[K, V]` (§11.3). Per-instance key/value types live in the
308/// payload, so a single descriptor serves all `Map[K, V]`.
309pub static MAP: TypeDescriptor = TypeDescriptor::builtin::<MapPayload>(
310    BuiltinTypeId::Map,
311    "Map",
312    map_trace,
313    map_drop,
314    map_format,
315    Some(map_equals),
316    Some(map_hash),
317    // No container order: a mutable collection can never be a `Map` key or a
318    // `Set` member (ADR-057 D4), so nothing ever has to put one in a
319    // deterministic sequence (ADR-138).
320    None,
321)
322.with_owned_bytes(map_owned_bytes);
323
324impl MapPayload {
325    /// The hash table this payload owns beyond its GC block, for GC pacing —
326    /// `capacity` slots of key *and* value, not `len`.
327    ///
328    /// One statement of the size, with two readers (ADR-121):
329    /// [`VecPayload::owned_bytes`](crate::collections::VecPayload::owned_bytes)
330    /// is that statement.
331    #[must_use]
332    pub(crate) fn owned_bytes(&self) -> usize {
333        self.entries.capacity() * (std::mem::size_of::<DynamicKey>() + std::mem::size_of::<GcRef>())
334    }
335}
336
337unsafe fn map_owned_bytes(payload: *const u8) -> usize {
338    // SAFETY: caller guarantees `payload` points at an initialized MapPayload.
339    let p = unsafe { &*(payload as *const MapPayload) };
340    p.owned_bytes()
341}
342
343// ===========================================================================
344// Set[T]
345// ===========================================================================
346
347/// The `Set[T]` payload (§11.3). The element descriptor is a **label** — what
348/// the construction site knew, or null when it knew nothing. Each member's
349/// `DynamicKey` carries its own descriptor, which is what `hash` and `format`
350/// dispatch through.
351#[repr(C)]
352pub struct SetPayload {
353    /// The descriptor for every element, or null when the construction site had
354    /// no static element type. Read it through [`SetPayload::element`].
355    pub element_descriptor: *const TypeDescriptor,
356    /// The elements, as `DynamicKey`s.
357    pub entries: HashSet<DynamicKey>,
358}
359
360impl SetPayload {
361    /// The element label, or `None` when this set was never told its element
362    /// type.
363    #[must_use]
364    pub fn element(&self) -> Option<&'static TypeDescriptor> {
365        nullable(self.element_descriptor)
366    }
367}
368
369unsafe fn set_trace(payload: *mut u8, tracer: &mut dyn Tracer) {
370    // SAFETY: caller guarantees `payload` points at an initialized SetPayload.
371    let p = unsafe { &*(payload as *const SetPayload) };
372    for k in p.entries.iter() {
373        tracer.trace(k.value());
374    }
375}
376
377unsafe fn set_drop(payload: *mut u8) {
378    // SAFETY: caller guarantees `payload` points at an initialized SetPayload.
379    unsafe { std::ptr::drop_in_place(payload as *mut SetPayload) };
380}
381
382unsafe fn set_format(payload: *const u8, out: &mut FormatSink<'_>) {
383    // SAFETY: caller guarantees `payload` points at an initialized SetPayload.
384    let p = unsafe { &*(payload as *const SetPayload) };
385    // The order a `for` over this set walks (ADR-138 decision 4).
386    // SAFETY: every member's payload matches the descriptor it carries.
387    let members = unsafe { ordered_members(&p.entries) };
388    write_braced(out, members, |s, m| {
389        // SAFETY: the member's payload matches its own header's descriptor.
390        unsafe { render_into(s, m.descriptor(), m) };
391    });
392}
393
394unsafe fn set_equals(a: *const u8, b: *const u8) -> bool {
395    // SAFETY: caller guarantees both pointers point at initialized SetPayloads.
396    let pa = unsafe { &*(a as *const SetPayload) };
397    let pb = unsafe { &*(b as *const SetPayload) };
398    pa.entries.len() == pb.entries.len() && pa.entries.is_subset(&pb.entries)
399}
400
401unsafe fn set_hash(payload: *const u8, hasher: &mut dyn DynamicHasher) {
402    // SAFETY: caller guarantees `payload` points at an initialized SetPayload.
403    let p = unsafe { &*(payload as *const SetPayload) };
404    // Order-independent: XOR all element hashes.
405    hasher.write_bytes(&(p.entries.len() as u64).to_le_bytes());
406    let mut acc: u64 = 0;
407    for k in p.entries.iter() {
408        // Through the member's own descriptor, not the set's label — the same
409        // rule `format` reads `k.descriptor()` for.
410        let Some(hash_el) = k.descriptor().hash else {
411            return;
412        };
413        let mut h = crate::descriptor::StructHasher::new();
414        let k_payload = k.value().payload::<u8>() as *const u8;
415        // SAFETY: element payload matches the descriptor its key carries.
416        unsafe { hash_el(k_payload, &mut h) };
417        acc ^= h.finish();
418    }
419    hasher.write_bytes(&acc.to_le_bytes());
420}
421
422/// Descriptor for `Set[T]` (§11.3).
423pub static SET: TypeDescriptor = TypeDescriptor::builtin::<SetPayload>(
424    BuiltinTypeId::Set,
425    "Set",
426    set_trace,
427    set_drop,
428    set_format,
429    Some(set_equals),
430    Some(set_hash),
431    // No container order: a mutable collection can never be a `Map` key or a
432    // `Set` member (ADR-057 D4), so nothing ever has to put one in a
433    // deterministic sequence (ADR-138).
434    None,
435)
436.with_owned_bytes(set_owned_bytes);
437
438impl SetPayload {
439    /// The hash table this payload owns beyond its GC block, for GC pacing —
440    /// `capacity`, not `len`.
441    ///
442    /// One statement of the size, with two readers (ADR-121):
443    /// [`VecPayload::owned_bytes`](crate::collections::VecPayload::owned_bytes)
444    /// is that statement.
445    #[must_use]
446    pub(crate) fn owned_bytes(&self) -> usize {
447        self.entries.capacity() * std::mem::size_of::<DynamicKey>()
448    }
449}
450
451unsafe fn set_owned_bytes(payload: *const u8) -> usize {
452    // SAFETY: caller guarantees `payload` points at an initialized SetPayload.
453    let p = unsafe { &*(payload as *const SetPayload) };
454    p.owned_bytes()
455}
456
457// ===========================================================================
458// Counter[T]
459// ===========================================================================
460
461/// The `Counter[T]` payload (§6.2, §11.3). A map whose values are always `Int`
462/// and whose absent keys read as zero. Backed by `HashMap<DynamicKey, GcRef>`
463/// where each value is a boxed `Int`; the key descriptor selects hash/eq.
464#[repr(C)]
465pub struct CounterPayload {
466    /// The descriptor for every key, or null when the construction site had no
467    /// static key type. A label, not the authority: each key's `DynamicKey`
468    /// carries its own. Read it through [`CounterPayload::key`].
469    pub key_descriptor: *const TypeDescriptor,
470    /// The entries: key → boxed Int value.
471    pub entries: HashMap<DynamicKey, GcRef>,
472}
473
474impl CounterPayload {
475    /// The key label, or `None` when this counter was never told its key type.
476    #[must_use]
477    pub fn key(&self) -> Option<&'static TypeDescriptor> {
478        nullable(self.key_descriptor)
479    }
480}
481
482unsafe fn counter_trace(payload: *mut u8, tracer: &mut dyn Tracer) {
483    // SAFETY: caller guarantees `payload` points at an initialized CounterPayload.
484    let p = unsafe { &*(payload as *const CounterPayload) };
485    for (k, v) in p.entries.iter() {
486        tracer.trace(k.value());
487        tracer.trace(*v);
488    }
489}
490
491unsafe fn counter_drop(payload: *mut u8) {
492    // SAFETY: caller guarantees `payload` points at an initialized CounterPayload.
493    unsafe { std::ptr::drop_in_place(payload as *mut CounterPayload) };
494}
495
496unsafe fn counter_format(payload: *const u8, out: &mut FormatSink<'_>) {
497    // SAFETY: caller guarantees `payload` points at an initialized CounterPayload.
498    let p = unsafe { &*(payload as *const CounterPayload) };
499    // As `map_format`: the order is decided over the keys, then rendered.
500    // SAFETY: every key's payload matches the descriptor it carries.
501    let rows = unsafe { ordered_entries(&p.entries) };
502    write_braced(out, rows, |s, (k, v)| {
503        // SAFETY: the key's payload matches its descriptor; a Counter's
504        // values are always `Int` (§6.2).
505        unsafe {
506            render_into(s, k.descriptor(), k);
507            let _ = s.write_str(": ");
508            render_into(s, &crate::scalars::INT, v);
509        }
510    });
511}
512
513unsafe fn counter_equals(a: *const u8, b: *const u8) -> bool {
514    // SAFETY: caller guarantees both pointers point at initialized CounterPayloads.
515    let pa = unsafe { &*(a as *const CounterPayload) };
516    let pb = unsafe { &*(b as *const CounterPayload) };
517    if pa.entries.len() != pb.entries.len() {
518        return false;
519    }
520    for (k, va) in pa.entries.iter() {
521        let Some(vb) = pb.entries.get(k) else {
522            return false;
523        };
524        // Values are Ints; compare payloads directly.
525        let va_i = unsafe { *(va.payload::<i64>()) };
526        let vb_i = unsafe { *(vb.payload::<i64>()) };
527        if va_i != vb_i {
528            return false;
529        }
530    }
531    true
532}
533
534unsafe fn counter_hash(payload: *const u8, hasher: &mut dyn DynamicHasher) {
535    // SAFETY: caller guarantees `payload` points at an initialized CounterPayload.
536    let p = unsafe { &*(payload as *const CounterPayload) };
537    hasher.write_bytes(&(p.entries.len() as u64).to_le_bytes());
538    let mut acc: u64 = 0;
539    for (k, v) in p.entries.iter() {
540        // Through the key's own descriptor, not the counter's label.
541        let Some(hash_key) = k.descriptor().hash else {
542            return;
543        };
544        let mut kh = crate::descriptor::StructHasher::new();
545        let k_payload = k.value().payload::<u8>() as *const u8;
546        // SAFETY: key payload matches the key descriptor.
547        unsafe { hash_key(k_payload, &mut kh) };
548        let v_i = unsafe { *(v.payload::<i64>()) };
549        let pair = kh
550            .finish()
551            .wrapping_mul(KEY_HASH_MIX)
552            .wrapping_add(v_i as u64);
553        acc ^= pair;
554    }
555    hasher.write_bytes(&acc.to_le_bytes());
556}
557
558/// Descriptor for `Counter[T]` (§6.2).
559pub static COUNTER: TypeDescriptor = TypeDescriptor::builtin::<CounterPayload>(
560    BuiltinTypeId::Counter,
561    "Counter",
562    counter_trace,
563    counter_drop,
564    counter_format,
565    Some(counter_equals),
566    Some(counter_hash),
567    // No container order: a mutable collection can never be a `Map` key or a
568    // `Set` member (ADR-057 D4), so nothing ever has to put one in a
569    // deterministic sequence (ADR-138).
570    None,
571)
572.with_owned_bytes(counter_owned_bytes);
573
574impl CounterPayload {
575    /// The hash table this payload owns beyond its GC block, for GC pacing —
576    /// `capacity` slots of key *and* boxed-`Int` value, not `len`.
577    ///
578    /// One statement of the size, with two readers (ADR-121):
579    /// [`VecPayload::owned_bytes`](crate::collections::VecPayload::owned_bytes)
580    /// is that statement.
581    #[must_use]
582    pub(crate) fn owned_bytes(&self) -> usize {
583        self.entries.capacity() * (std::mem::size_of::<DynamicKey>() + std::mem::size_of::<GcRef>())
584    }
585}
586
587unsafe fn counter_owned_bytes(payload: *const u8) -> usize {
588    // SAFETY: caller guarantees `payload` points at an initialized CounterPayload.
589    let p = unsafe { &*(payload as *const CounterPayload) };
590    p.owned_bytes()
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596
597    #[test]
598    fn map_set_counter_descriptors_report_capabilities() {
599        // All three hash collections are eq-able and hashable themselves (so a
600        // Map/Set/Counter can be a value in another collection).
601        assert!(MAP.is_equatable() && MAP.is_hashable());
602        assert!(SET.is_equatable() && SET.is_hashable());
603        assert!(COUNTER.is_equatable() && COUNTER.is_hashable());
604        assert_eq!(MAP.name, "Map");
605        assert_eq!(SET.name, "Set");
606        assert_eq!(COUNTER.name, "Counter");
607    }
608
609    /// Render a payload through its own `format` callback, without allocating a
610    /// GC object to hold it.
611    fn rendered<P>(format: crate::FormatFn, payload: &P) -> String {
612        rendered_styled(format, payload, crate::FormatStyle::Display)
613    }
614
615    /// [`rendered`], in a style the caller picks — for the tests that are about
616    /// the two renderings differing.
617    fn rendered_styled<P>(
618        format: crate::FormatFn,
619        payload: &P,
620        style: crate::FormatStyle,
621    ) -> String {
622        let mut s = String::new();
623        let mut sink = FormatSink::styled(&mut s, style);
624        // SAFETY: `payload` is an initialized value of the type `format` reads.
625        unsafe { format((payload as *const P).cast::<u8>(), &mut sink) };
626        s
627    }
628
629    fn int_key(rt: &crate::Runtime, n: i64) -> DynamicKey {
630        DynamicKey::new(rt.alloc_int(n))
631    }
632
633    /// Rust randomizes hash-table iteration order **per process**, so a `Map`'s
634    /// printed form must not follow it: the same program would print a
635    /// different string on every run, and a program whose expected output
636    /// cannot be written down does not have one.
637    ///
638    /// Two maps built by inserting the same pairs in opposite orders is the
639    /// cheap in-process proxy: it does not reproduce the cross-run seed, but it
640    /// does catch "the output follows the table's internal layout".
641    #[test]
642    fn map_formatting_does_not_follow_hash_table_order() {
643        let rt = crate::Runtime::new();
644        let build = |order: [i64; 6]| MapPayload {
645            key_descriptor: &crate::scalars::INT,
646            value_descriptor: &crate::scalars::INT,
647            entries: order
648                .iter()
649                .map(|&n| (int_key(&rt, n), rt.alloc_int(n * 10)))
650                .collect(),
651        };
652
653        let forward = rendered(map_format, &build([1, 2, 3, 4, 5, 6]));
654        let backward = rendered(map_format, &build([6, 5, 4, 3, 2, 1]));
655        assert_eq!(forward, backward, "insertion order must not show through");
656        // Ordered by the key's own `compare` (ADR-138), which for `Int` is
657        // numeric. Every key here is one digit, so a lexicographic order would
658        // agree — `a_set_of_ints_orders_numerically_and_not_lexicographically`
659        // is the test that tells the two apart.
660        assert_eq!(forward, "{1: 10, 2: 20, 3: 30, 4: 40, 5: 50, 6: 60}");
661    }
662
663    /// A container's [`FormatStyle`] reaches its **elements**, including across
664    /// the scratch buffer `map_format` renders each entry into.
665    ///
666    /// This is the property that decided the design: a `format_debug` field
667    /// beside `format` would have quoted a `Text` local and left a `Text` inside
668    /// a `Map` bare, because the container's callback would have had no way to
669    /// know which of the two it was running as. The buffer is the place the
670    /// style is easiest to drop, since the entries are rendered before the sink
671    /// they end up in is written to at all.
672    #[test]
673    fn a_containers_style_reaches_the_values_inside_it() {
674        let rt = crate::Runtime::new();
675        let payload = MapPayload {
676            key_descriptor: &crate::text::TEXT,
677            value_descriptor: &crate::text::TEXT,
678            entries: [(
679                DynamicKey::new(rt.alloc_text("k")),
680                // Empty on purpose: in the program's rendering this entry's
681                // value is zero characters wide.
682                rt.alloc_text(""),
683            )]
684            .into_iter()
685            .collect(),
686        };
687        assert_eq!(
688            rendered_styled(map_format, &payload, crate::FormatStyle::Display),
689            "{k: }",
690            "the program's rendering is unchanged, empty value and all"
691        );
692        assert_eq!(
693            rendered_styled(map_format, &payload, crate::FormatStyle::Debug),
694            r#"{"k": ""}"#,
695            "the debugger's reaches both the key and the value"
696        );
697    }
698
699    #[test]
700    fn set_formatting_does_not_follow_hash_table_order() {
701        let rt = crate::Runtime::new();
702        let build = |order: [i64; 5]| SetPayload {
703            element_descriptor: &crate::scalars::INT,
704            entries: order.iter().map(|&n| int_key(&rt, n)).collect(),
705        };
706
707        let forward = rendered(set_format, &build([3, 1, 4, 5, 9]));
708        let backward = rendered(set_format, &build([9, 5, 4, 1, 3]));
709        assert_eq!(forward, backward);
710        assert_eq!(forward, "{1, 3, 4, 5, 9}");
711    }
712
713    /// A `Set`'s snapshot order is the order it prints in, and neither follows
714    /// the hash table's own.
715    ///
716    /// This matters more than the formatting rule it shares: `for x in s`
717    /// iterates the snapshot, so the order is the *answer* a program computes
718    /// and not only the string it prints. Rust randomizes the table's order per
719    /// process, so a program that concatenates its members would answer
720    /// differently on two runs.
721    #[test]
722    fn a_sets_members_come_out_in_the_order_it_prints_them() {
723        let rt = crate::Runtime::new();
724        let build = |order: [i64; 5]| SetPayload {
725            element_descriptor: &crate::scalars::INT,
726            entries: order.iter().map(|&n| int_key(&rt, n)).collect(),
727        };
728
729        let read_back = |p: &SetPayload| -> Vec<i64> {
730            // SAFETY: every member is an `Int` matching the element descriptor.
731            unsafe { ordered_members(&p.entries) }
732                .into_iter()
733                .map(|m| unsafe { *m.payload::<i64>() })
734                .collect()
735        };
736        let forward = read_back(&build([3, 1, 4, 5, 9]));
737        let backward = read_back(&build([9, 5, 4, 1, 3]));
738        assert_eq!(forward, backward, "insertion order must not show through");
739        assert_eq!(forward, vec![1, 3, 4, 5, 9]);
740        // …and it is the same order `set_format` writes, which is the property
741        // that keeps `out(s)` and `for x in s` from disagreeing.
742        assert_eq!(
743            rendered(set_format, &build([3, 1, 4, 5, 9])),
744            "{1, 3, 4, 5, 9}"
745        );
746    }
747
748    /// `keys()` and `values()` are index-aligned because they share one order,
749    /// and a `for` over the same map is the third caller of it.
750    #[test]
751    fn a_keyed_collections_entries_come_out_paired() {
752        let rt = crate::Runtime::new();
753        let p = MapPayload {
754            key_descriptor: &crate::scalars::INT,
755            value_descriptor: &crate::scalars::INT,
756            entries: [3, 1, 2]
757                .iter()
758                .map(|&n| (int_key(&rt, n), rt.alloc_int(n * 10)))
759                .collect(),
760        };
761        // SAFETY: every key and value is an `Int` matching its descriptor.
762        let rows = unsafe { ordered_entries(&p.entries) };
763        let pairs: Vec<(i64, i64)> = rows
764            .into_iter()
765            .map(|(k, v)| unsafe { (*k.payload::<i64>(), *v.payload::<i64>()) })
766            .collect();
767        assert_eq!(pairs, vec![(1, 10), (2, 20), (3, 30)]);
768    }
769
770    #[test]
771    fn counter_formatting_does_not_follow_hash_table_order() {
772        let rt = crate::Runtime::new();
773        let build = |order: [i64; 4]| CounterPayload {
774            key_descriptor: &crate::scalars::INT,
775            entries: order
776                .iter()
777                .map(|&n| (int_key(&rt, n), rt.alloc_int(n)))
778                .collect(),
779        };
780
781        let forward = rendered(counter_format, &build([2, 7, 1, 8]));
782        let backward = rendered(counter_format, &build([8, 1, 7, 2]));
783        assert_eq!(forward, backward);
784        assert_eq!(forward, "{1: 1, 2: 2, 7: 7, 8: 8}");
785    }
786
787    /// **The gate for ADR-138.** A `Set[Int]` orders by the number, not by how
788    /// the number prints: a sort key of the rendered member answers
789    /// `10, 100, 2, 9`, because `"10" < "2"`.
790    ///
791    /// A wrong order out of a `for` is an *answer*, not a formatting wart,
792    /// which is the shape of defect a test has to hold down rather than a
793    /// reader.
794    #[test]
795    fn a_set_of_ints_orders_numerically_and_not_lexicographically() {
796        let rt = crate::Runtime::new();
797        let build = |order: [i64; 4]| SetPayload {
798            element_descriptor: &crate::scalars::INT,
799            entries: order.iter().map(|&n| int_key(&rt, n)).collect(),
800        };
801        let read_back = |p: &SetPayload| -> Vec<i64> {
802            // SAFETY: every member is an `Int` matching the element descriptor.
803            unsafe { ordered_members(&p.entries) }
804                .into_iter()
805                .map(|m| unsafe { *m.payload::<i64>() })
806                .collect()
807        };
808        assert_eq!(read_back(&build([9, 10, 100, 2])), vec![2, 9, 10, 100]);
809        assert_eq!(read_back(&build([2, 100, 10, 9])), vec![2, 9, 10, 100]);
810        // …and the printing is that same sequence, which is what makes `out(s)`
811        // and `out(s.sorted())` agree.
812        assert_eq!(
813            rendered(set_format, &build([9, 10, 100, 2])),
814            "{2, 9, 10, 100}"
815        );
816    }
817
818    /// A keyed collection prints in the order it iterates (ADR-138 decision 4).
819    ///
820    /// `"a"` and `"a1"` are the pair that tells the two apart: sorting the
821    /// whole rendered *entry* puts `"a1: 2"` before `"a: 1"`, because `'1'`
822    /// (0x31) is below `':'` (0x3A), while sorting the *key* answers `a, a1`.
823    /// One `Map`, two orders, and a program that printed it and walked it
824    /// would disagree with itself.
825    #[test]
826    fn a_keyed_collection_prints_in_the_order_it_iterates() {
827        let rt = crate::Runtime::new();
828        let p = MapPayload {
829            key_descriptor: &crate::text::TEXT,
830            value_descriptor: &crate::scalars::INT,
831            entries: [("a1", 2), ("a", 1)]
832                .iter()
833                .map(|&(k, v)| (DynamicKey::new(rt.alloc_text(k)), rt.alloc_int(v)))
834                .collect(),
835        };
836        // SAFETY: every key is a `Text` and every value an `Int`.
837        let iterated: Vec<String> = unsafe { ordered_entries(&p.entries) }
838            .into_iter()
839            .map(|(k, _)| {
840                let mut s = String::new();
841                unsafe { render_into(&mut crate::FormatSink::display(&mut s), k.descriptor(), k) };
842                s
843            })
844            .collect();
845        assert_eq!(iterated, vec!["a".to_string(), "a1".to_string()]);
846        assert_eq!(rendered(map_format, &p), "{a: 1, a1: 2}");
847    }
848
849    /// A tuple key orders element-wise — a memo key of shape
850    /// `Map[(Text, Int), V]`, and the reason `TUPLE.compare` is populated
851    /// rather than left to the rendered-form fallback: `"(a, 10)"` sorts before
852    /// `"(a, 9)"` and `(a, 9)` does not.
853    #[test]
854    fn a_tuple_keyed_map_orders_element_wise() {
855        let mut rt = crate::Runtime::new();
856        let schema: &'static crate::tuples::TupleSchema =
857            Box::leak(Box::new(crate::tuples::TupleSchema {
858                descriptors: Box::leak(
859                    vec![
860                        &crate::text::TEXT as *const TypeDescriptor,
861                        &crate::scalars::INT as *const TypeDescriptor,
862                    ]
863                    .into_boxed_slice(),
864                ),
865            }));
866        let pairs = [("a", 10), ("a", 9), ("b", 1)];
867        let values: Vec<(GcRef, GcRef)> = pairs
868            .iter()
869            .map(|&(t, n)| (rt.alloc_text(t), rt.alloc_int(n)))
870            .collect();
871        let mut ctx = rt.context();
872        let keys: Vec<GcRef> = values
873            .into_iter()
874            .map(|(t, n)| {
875                // SAFETY: a live context, and the schema names exactly these
876                // two element types.
877                unsafe {
878                    let tup = crate::abi::praxis_alloc_tuple(&mut ctx, schema);
879                    crate::abi::praxis_tuple_set(&mut ctx, tup, 0, t);
880                    crate::abi::praxis_tuple_set(&mut ctx, tup, 1, n);
881                    tup
882                }
883            })
884            .collect();
885        let p = MapPayload {
886            key_descriptor: &crate::tuples::TUPLE,
887            value_descriptor: &crate::scalars::INT,
888            entries: keys
889                .into_iter()
890                .map(|k| (DynamicKey::new(k), rt.alloc_int(0)))
891                .collect(),
892        };
893        assert_eq!(
894            rendered(map_format, &p),
895            "{(a, 9): 0, (a, 10): 0, (b, 1): 0}"
896        );
897    }
898
899    /// A `Float` key orders numerically, with NaN last (ADR-045 decision 2).
900    ///
901    /// This ties that rule to the order a `Set` prints and iterates in: a
902    /// rendered-form order would put `10.25` between `1.5` and `2.0`.
903    #[test]
904    fn a_float_keyed_set_orders_numerically_and_puts_nan_last() {
905        let rt = crate::Runtime::new();
906        let p = SetPayload {
907            element_descriptor: &crate::scalars::FLOAT,
908            entries: [2.0, f64::NAN, 10.25, 1.5]
909                .iter()
910                .map(|&f| DynamicKey::new(rt.alloc_float(f)))
911                .collect(),
912        };
913        assert_eq!(rendered(set_format, &p), "{1.5, 2.0, 10.25, NaN}");
914    }
915}