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}