praxis_runtime/heaps.rs
1//! `MinHeap[T]` and `MaxHeap[T]` (§6.1, §11.2).
2//!
3//! Both reuse Rust's `BinaryHeap` behind an opaque GC object. Rust's
4//! `BinaryHeap` is a max-heap, so `MaxHeap[T]` maps directly
5//! (`BinaryHeap<HeapEntry>`) and `MinHeap[T]` wraps entries in `Reverse`
6//! (`BinaryHeap<Reverse<HeapEntry>>`) so the smallest element surfaces first.
7//!
8//! The element type must be orderable (§5.4 `SupportsOrd`); the capability
9//! check rejects non-orderable types at compile time. Ordering goes through the
10//! element descriptor's `compare` callback (ADR-045), so a `MinHeap[Float]`
11//! orders numerically and a `MinHeap[Text]` lexicographically.
12
13use std::cmp::Reverse;
14use std::collections::BinaryHeap;
15use std::fmt::Write as _;
16
17use crate::GcRef;
18use crate::collections::nullable;
19use crate::descriptor::{BuiltinTypeId, FormatSink, Tracer, TypeDescriptor};
20
21/// A heap's elements in **pop order** — the order the program would see them if
22/// it drained the heap, without draining it.
23///
24/// `BinaryHeap::iter` yields the backing array, which is heap-ordered only at
25/// the root: `[3, 1, 2]` and `[3, 2, 1]` are both valid layouts for the same
26/// contents, and which one exists depends on insertion history. Iterating it
27/// would let the same heap print two ways — and, since `for x in h` iterates a
28/// snapshot of this (ADR-066), *answer* two ways. Sorting descending by the
29/// heap's own `Ord` is pop order for a `BinaryHeap<T>` whatever `T` is — for
30/// `MinHeap`, whose `T` is `Reverse<HeapEntry>`, that comes out ascending by
31/// element, which is what `pop` gives.
32///
33/// This is the one collection whose deterministic order is also *meaningful*:
34/// heaps carry an ordering by construction.
35pub(crate) fn in_pop_order<T: Ord, F: Fn(&T) -> GcRef>(
36 items: &BinaryHeap<T>,
37 value_of: F,
38) -> Vec<GcRef> {
39 let mut ordered: Vec<&T> = items.iter().collect();
40 ordered.sort_unstable_by(|a, b| b.cmp(a));
41 ordered.into_iter().map(value_of).collect()
42}
43
44/// Write a heap's elements in [`in_pop_order`].
45///
46/// Each element formats through the descriptor in its **own** header, not
47/// through the heap's element label. The label is only what the construction
48/// site knew, and it is null when it knew nothing, so it is not something
49/// formatting can dispatch on; `HeapEntry::cmp` orders through the element's own
50/// descriptor for the same reason.
51unsafe fn write_in_pop_order<T: Ord, F: Fn(&T) -> GcRef>(
52 out: &mut FormatSink<'_>,
53 items: &BinaryHeap<T>,
54 value_of: F,
55) {
56 let _ = out.write_str("[");
57 for (i, value) in in_pop_order(items, value_of).into_iter().enumerate() {
58 if i > 0 {
59 let _ = out.write_str(", ");
60 }
61 let ep = value.payload::<u8>() as *const u8;
62 // SAFETY: an object's payload always matches the descriptor in its own
63 // header.
64 unsafe { (value.descriptor().format)(ep, out) };
65 }
66 let _ = out.write_str("]");
67}
68
69/// A max-heap entry: the element `GcRef` plus its descriptor. `Ord` dispatches
70/// to the descriptor's `compare` callback (ADR-045). `MaxHeap` uses this
71/// directly; `MinHeap` wraps it in `Reverse`.
72#[derive(Clone, Copy)]
73pub struct HeapEntry {
74 /// The element value.
75 pub value: GcRef,
76 /// The element's descriptor (for trace/equals/hash/format/compare).
77 pub descriptor: &'static TypeDescriptor,
78}
79
80impl PartialEq for HeapEntry {
81 fn eq(&self, other: &Self) -> bool {
82 if self.value == other.value {
83 return true;
84 }
85 match self.descriptor.equals {
86 Some(equals) => {
87 // SAFETY: both values match the descriptor (homogeneous heap).
88 let a = self.value.payload::<u8>() as *const u8;
89 let b = other.value.payload::<u8>() as *const u8;
90 unsafe { equals(a, b) }
91 }
92 None => false,
93 }
94 }
95}
96
97impl Eq for HeapEntry {}
98
99impl PartialOrd for HeapEntry {
100 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
101 Some(self.cmp(other))
102 }
103}
104
105impl Ord for HeapEntry {
106 /// The element type's own order, through its descriptor (ADR-045).
107 ///
108 /// Two answers are `Equal` for a reason rather than by accident: entries
109 /// whose descriptors differ (which a homogeneous heap never has, so this is
110 /// the miscompile case) and an element type with no ordering at all. There
111 /// is no fault channel inside `Ord`, and a heap whose comparisons are all
112 /// `Equal` is a consistent total order — the heap degrades to a bag instead
113 /// of corrupting its sift invariants. What it must never do is read every
114 /// payload as an `i64`: that puts `-2.0` after `-1.0` and reads four bytes
115 /// past a `Char`.
116 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
117 if !std::ptr::eq(self.descriptor, other.descriptor) {
118 return std::cmp::Ordering::Equal;
119 }
120 match self.descriptor.compare {
121 // SAFETY: both values carry this descriptor (checked above), so
122 // both payloads are values of its type.
123 Some(compare) => unsafe {
124 compare(
125 self.value.payload::<u8>() as *const u8,
126 other.value.payload::<u8>() as *const u8,
127 )
128 },
129 None => std::cmp::Ordering::Equal,
130 }
131 }
132}
133
134impl std::fmt::Debug for HeapEntry {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 let mut rendered = String::new();
137 // SAFETY: the entry's value is a live object of its descriptor's type.
138 // The debug style, for `DynamicKey`'s reason: this is a Rust `Debug`
139 // impl, and a quoted string is what one of those shows.
140 unsafe {
141 (self.descriptor.format)(
142 self.value.payload::<u8>() as *const u8,
143 &mut FormatSink::debug(&mut rendered),
144 );
145 };
146 write!(f, "HeapEntry({rendered})")
147 }
148}
149
150// --- MaxHeap payload -------------------------------------------------------
151
152/// The `MaxHeap[T]` payload (§11.2): a max-heap of `HeapEntry`.
153#[repr(C)]
154pub struct MaxHeapPayload {
155 /// The descriptor for every element, or null when the construction site had
156 /// no static element type. A label: each element carries its own descriptor,
157 /// both on its `HeapEntry` and in its object header. Read it through
158 /// [`MaxHeapPayload::element`].
159 pub element_descriptor: *const TypeDescriptor,
160 /// The elements, in max-heap order (largest surfaces first).
161 pub items: BinaryHeap<HeapEntry>,
162}
163
164impl MaxHeapPayload {
165 /// The element label, or `None` when this heap was never told its element
166 /// type.
167 #[must_use]
168 pub fn element(&self) -> Option<&'static TypeDescriptor> {
169 nullable(self.element_descriptor)
170 }
171}
172
173unsafe fn max_heap_trace(payload: *mut u8, tracer: &mut dyn Tracer) {
174 let p = unsafe { &*(payload as *const MaxHeapPayload) };
175 for entry in p.items.iter() {
176 tracer.trace(entry.value);
177 }
178}
179
180unsafe fn max_heap_drop(payload: *mut u8) {
181 unsafe { std::ptr::drop_in_place(payload as *mut MaxHeapPayload) };
182}
183
184unsafe fn max_heap_format(payload: *const u8, out: &mut FormatSink<'_>) {
185 let p = unsafe { &*(payload as *const MaxHeapPayload) };
186 // SAFETY: every element is a live object, and `write_in_pop_order` formats
187 // each through the descriptor in its own header.
188 unsafe { write_in_pop_order(out, &p.items, |e| e.value) };
189}
190
191/// Descriptor for `MaxHeap[T]` (§11.2, TypeId 14).
192// Heaps are not equatable/hashable (contents + order define identity).
193pub static MAX_HEAP: TypeDescriptor = TypeDescriptor::builtin::<MaxHeapPayload>(
194 BuiltinTypeId::MaxHeap,
195 "MaxHeap",
196 max_heap_trace,
197 max_heap_drop,
198 max_heap_format,
199 None,
200 None,
201 // No container order: a mutable collection can never be a `Map` key or a
202 // `Set` member (ADR-057 D4), so nothing ever has to put one in a
203 // deterministic sequence (ADR-138).
204 None,
205)
206.with_owned_bytes(max_heap_owned_bytes);
207
208impl MaxHeapPayload {
209 /// The sift array this payload owns beyond its GC block, for GC pacing —
210 /// `capacity`, not `len`.
211 ///
212 /// One statement of the size, with two readers (ADR-121):
213 /// [`VecPayload::owned_bytes`](crate::collections::VecPayload::owned_bytes)
214 /// is that statement.
215 #[must_use]
216 pub(crate) fn owned_bytes(&self) -> usize {
217 self.items.capacity() * std::mem::size_of::<HeapEntry>()
218 }
219}
220
221unsafe fn max_heap_owned_bytes(payload: *const u8) -> usize {
222 // SAFETY: caller guarantees `payload` points at an initialized MaxHeapPayload.
223 let p = unsafe { &*(payload as *const MaxHeapPayload) };
224 p.owned_bytes()
225}
226
227// --- MinHeap payload -------------------------------------------------------
228
229/// The `MinHeap[T]` payload (§11.2): a min-heap via `Reverse<HeapEntry>`.
230#[repr(C)]
231pub struct MinHeapPayload {
232 /// The descriptor for every element, or null when the construction site had
233 /// no static element type. See [`MaxHeapPayload::element_descriptor`].
234 pub element_descriptor: *const TypeDescriptor,
235 /// The elements, wrapped in `Reverse` so the smallest surfaces first.
236 pub items: BinaryHeap<Reverse<HeapEntry>>,
237}
238
239impl MinHeapPayload {
240 /// The element label, or `None` when this heap was never told its element
241 /// type.
242 #[must_use]
243 pub fn element(&self) -> Option<&'static TypeDescriptor> {
244 nullable(self.element_descriptor)
245 }
246}
247
248unsafe fn min_heap_trace(payload: *mut u8, tracer: &mut dyn Tracer) {
249 let p = unsafe { &*(payload as *const MinHeapPayload) };
250 for entry in p.items.iter() {
251 tracer.trace(entry.0.value);
252 }
253}
254
255unsafe fn min_heap_drop(payload: *mut u8) {
256 unsafe { std::ptr::drop_in_place(payload as *mut MinHeapPayload) };
257}
258
259unsafe fn min_heap_format(payload: *const u8, out: &mut FormatSink<'_>) {
260 let p = unsafe { &*(payload as *const MinHeapPayload) };
261 // SAFETY: every element is a live object, and `write_in_pop_order` formats
262 // each through the descriptor in its own header. The stored entry is
263 // `Reverse<HeapEntry>`, so pop order is ascending by element.
264 unsafe { write_in_pop_order(out, &p.items, |e| e.0.value) };
265}
266
267/// Descriptor for `MinHeap[T]` (§11.2, TypeId 13).
268pub static MIN_HEAP: TypeDescriptor = TypeDescriptor::builtin::<MinHeapPayload>(
269 BuiltinTypeId::MinHeap,
270 "MinHeap",
271 min_heap_trace,
272 min_heap_drop,
273 min_heap_format,
274 None,
275 None,
276 // No container order: a mutable collection can never be a `Map` key or a
277 // `Set` member (ADR-057 D4), so nothing ever has to put one in a
278 // deterministic sequence (ADR-138).
279 None,
280)
281.with_owned_bytes(min_heap_owned_bytes);
282
283impl MinHeapPayload {
284 /// The sift array this payload owns beyond its GC block, for GC pacing —
285 /// `capacity`, not `len`, and of `Reverse<HeapEntry>` because that is what
286 /// a min-heap stores.
287 ///
288 /// One statement of the size, with two readers (ADR-121):
289 /// [`VecPayload::owned_bytes`](crate::collections::VecPayload::owned_bytes)
290 /// is that statement.
291 #[must_use]
292 pub(crate) fn owned_bytes(&self) -> usize {
293 self.items.capacity() * std::mem::size_of::<Reverse<HeapEntry>>()
294 }
295}
296
297unsafe fn min_heap_owned_bytes(payload: *const u8) -> usize {
298 // SAFETY: caller guarantees `payload` points at an initialized MinHeapPayload.
299 let p = unsafe { &*(payload as *const MinHeapPayload) };
300 p.owned_bytes()
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn heap_descriptors_are_non_equatable() {
309 assert!(!MAX_HEAP.is_equatable());
310 assert!(!MAX_HEAP.is_hashable());
311 assert_eq!(MAX_HEAP.name, "MaxHeap");
312 assert!(!MIN_HEAP.is_equatable());
313 assert!(!MIN_HEAP.is_hashable());
314 assert_eq!(MIN_HEAP.name, "MinHeap");
315 }
316
317 #[test]
318 fn float_heap_entries_use_numeric_order() {
319 let rt = crate::Runtime::new();
320 let minus_two = HeapEntry {
321 value: rt.alloc_float(-2.0),
322 descriptor: &crate::scalars::FLOAT,
323 };
324 let minus_one = HeapEntry {
325 value: rt.alloc_float(-1.0),
326 descriptor: &crate::scalars::FLOAT,
327 };
328
329 assert_eq!(
330 minus_two.cmp(&minus_one),
331 std::cmp::Ordering::Less,
332 "orderable Float values must use IEEE numeric ordering, not signed bit-pattern order"
333 );
334 }
335
336 /// A `Char` payload is four bytes, so ordering must go through the `Char`
337 /// descriptor: reading eight bytes from a four-byte-aligned address would
338 /// make the order depend on whatever follows the object.
339 #[test]
340 fn char_heap_entries_order_by_unicode_scalar_value() {
341 let rt = crate::Runtime::new();
342 let a = HeapEntry {
343 value: rt.alloc_char('a' as u32),
344 descriptor: &crate::scalars::CHAR,
345 };
346 let beta = HeapEntry {
347 value: rt.alloc_char('β' as u32),
348 descriptor: &crate::scalars::CHAR,
349 };
350 assert_eq!(a.cmp(&beta), std::cmp::Ordering::Less);
351 assert_eq!(beta.cmp(&a), std::cmp::Ordering::Greater);
352 }
353
354 /// `in_pop_order` answers what draining the heap would, without draining it
355 /// — and for a `MinHeap`, whose entries are `Reverse`d, that is ascending.
356 ///
357 /// The backing array is the thing this is not: `[3, 1, 2]` and `[3, 2, 1]`
358 /// are both valid layouts for the same heap, so a `for` that indexed the
359 /// array would answer by insertion history (ADR-066).
360 #[test]
361 fn a_heaps_snapshot_is_the_order_draining_it_would_give() {
362 let rt = crate::Runtime::new();
363 let entry = |n: i64| HeapEntry {
364 value: rt.alloc_int(n),
365 descriptor: &crate::scalars::INT,
366 };
367 let read_back = |items: Vec<GcRef>| -> Vec<i64> {
368 // SAFETY: every element is an `Int`.
369 items
370 .into_iter()
371 .map(|v| unsafe { *v.payload::<i64>() })
372 .collect()
373 };
374
375 // Built in an order whose array layout is not sorted, so "the array" and
376 // "pop order" are different sequences.
377 let mut max: BinaryHeap<HeapEntry> = BinaryHeap::new();
378 for n in [3, 1, 2] {
379 max.push(entry(n));
380 }
381 assert_eq!(read_back(in_pop_order(&max, |e| e.value)), vec![3, 2, 1]);
382
383 let mut min: BinaryHeap<Reverse<HeapEntry>> = BinaryHeap::new();
384 for n in [3, 1, 2] {
385 min.push(Reverse(entry(n)));
386 }
387 assert_eq!(read_back(in_pop_order(&min, |e| e.0.value)), vec![1, 2, 3]);
388
389 // Draining the same heap agrees, which is the property that makes this
390 // order the meaningful one rather than merely a fixed one.
391 let mut drained = Vec::new();
392 while let Some(Reverse(e)) = min.pop() {
393 drained.push(unsafe { *e.value.payload::<i64>() });
394 }
395 assert_eq!(drained, vec![1, 2, 3]);
396
397 // And the snapshot did not consume the heap it was taken from.
398 let mut max_again: BinaryHeap<HeapEntry> = BinaryHeap::new();
399 for n in [3, 1, 2] {
400 max_again.push(entry(n));
401 }
402 let _ = in_pop_order(&max_again, |e| e.value);
403 assert_eq!(max_again.len(), 3, "iterating is not popping");
404 }
405
406 /// ADR-045 decision 3: an entry never dispatches a `compare` callback at a
407 /// value of another type. A homogeneous heap cannot reach this; a
408 /// miscompiled one gets `Equal` rather than a `Text` payload read as an
409 /// `Int`.
410 #[test]
411 fn entries_of_different_types_do_not_dispatch_a_callback() {
412 let rt = crate::Runtime::new();
413 let int = HeapEntry {
414 value: rt.alloc_int(1),
415 descriptor: &crate::scalars::INT,
416 };
417 let text = HeapEntry {
418 value: rt.alloc_text("zzz"),
419 descriptor: &crate::text::TEXT,
420 };
421 assert_eq!(int.cmp(&text), std::cmp::Ordering::Equal);
422 assert_eq!(text.cmp(&int), std::cmp::Ordering::Equal);
423 }
424
425 /// An element type with no ordering leaves the heap a bag: every comparison
426 /// is `Equal`, which is still a consistent total order, so `BinaryHeap`
427 /// keeps its invariants.
428 ///
429 /// The fixture is a closure, which is the strongest remaining case: every
430 /// type a `Map` key can be has a `compare` (ADR-138), so what is left
431 /// without one is exactly what can never be a key, and a closure is the type
432 /// that can never even be compared.
433 #[test]
434 fn an_element_type_with_no_container_order_compares_equal_rather_than_reading_bytes() {
435 let mut rt = crate::Runtime::new();
436 assert!(
437 !crate::closures::CLOSURE.is_orderable(),
438 "a closure has no ordering of any kind (ADR-138)"
439 );
440 let mut ctx = rt.context();
441 // SAFETY: a live context, and a closure that captures nothing.
442 let make = |ctx: &mut crate::RuntimeContext| HeapEntry {
443 value: unsafe { crate::abi::praxis_alloc_closure(ctx, std::ptr::null(), 0) },
444 descriptor: &crate::closures::CLOSURE,
445 };
446 let a = make(&mut ctx);
447 let b = make(&mut ctx);
448 assert_eq!(a.cmp(&b), std::cmp::Ordering::Equal);
449 }
450
451 /// A `MinHeap[Text]` pops lexicographically. The end-to-end proof that the
452 /// heap's order is the descriptor's order and not the payload's address:
453 /// these three texts are allocated in an order that does not match their
454 /// lexicographic one.
455 #[test]
456 fn a_text_heap_pops_in_lexicographic_order() {
457 let rt = crate::Runtime::new();
458 let mut items = BinaryHeap::new();
459 for s in ["pear", "apple", "quince", "banana"] {
460 items.push(Reverse(HeapEntry {
461 value: rt.alloc_text(s),
462 descriptor: &crate::text::TEXT,
463 }));
464 }
465 let payload = MinHeapPayload {
466 element_descriptor: &crate::text::TEXT,
467 items,
468 };
469 assert_eq!(
470 rendered(min_heap_format, &payload),
471 "[apple, banana, pear, quince]"
472 );
473 }
474
475 /// Render a heap payload through its own `format` callback, without
476 /// allocating a GC object to hold it — the callback takes a payload
477 /// pointer, which is all a formatting test needs.
478 fn rendered<P>(format: crate::FormatFn, payload: &P) -> String {
479 let mut s = String::new();
480 // The program's own rendering, which is what these tests are about.
481 let mut sink = FormatSink::display(&mut s);
482 // SAFETY: `payload` is an initialized value of the type `format` reads.
483 unsafe { format((payload as *const P).cast::<u8>(), &mut sink) };
484 s
485 }
486
487 /// `BinaryHeap::iter` walks the backing array, which is heap-ordered only at
488 /// the root — `[3, 1, 2]` and `[3, 2, 1]` are both valid layouts for the
489 /// same contents, and which one exists depends on insertion history, so
490 /// printing it would make two heaps that are equal as values print
491 /// differently. Pop order is the order the program would observe, and it is
492 /// the same for both.
493 #[test]
494 fn heap_formatting_does_not_depend_on_insertion_order() {
495 let rt = crate::Runtime::new();
496 let build = |order: [i64; 5]| {
497 let mut items = BinaryHeap::new();
498 for n in order {
499 items.push(HeapEntry {
500 value: rt.alloc_int(n),
501 descriptor: &crate::scalars::INT,
502 });
503 }
504 MaxHeapPayload {
505 element_descriptor: &crate::scalars::INT,
506 items,
507 }
508 };
509
510 let ascending = build([1, 5, 3, 9, 2]);
511 let descending = build([2, 9, 3, 5, 1]);
512 let backing =
513 |p: &MaxHeapPayload| p.items.iter().map(|e| format!("{e:?}")).collect::<Vec<_>>();
514 assert_ne!(
515 backing(&ascending),
516 backing(&descending),
517 "the two backing arrays must actually differ, or this proves nothing"
518 );
519
520 let a = rendered(max_heap_format, &ascending);
521 let d = rendered(max_heap_format, &descending);
522 assert_eq!(a, d, "the same contents must render the same way");
523 assert_eq!(a, "[9, 5, 3, 2, 1]", "a max-heap renders in pop order");
524 }
525
526 /// A `MinHeap` stores `Reverse<HeapEntry>`, so "descending by the stored
527 /// `Ord`" comes out ascending by element — which is what `pop` gives.
528 #[test]
529 fn a_min_heap_renders_smallest_first() {
530 let rt = crate::Runtime::new();
531 let mut items = BinaryHeap::new();
532 for n in [4_i64, 1, 7, 2] {
533 items.push(Reverse(HeapEntry {
534 value: rt.alloc_int(n),
535 descriptor: &crate::scalars::INT,
536 }));
537 }
538 let payload = MinHeapPayload {
539 element_descriptor: &crate::scalars::INT,
540 items,
541 };
542 assert_eq!(rendered(min_heap_format, &payload), "[1, 2, 4, 7]");
543 }
544}