Skip to main content

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}