Skip to main content

praxis_runtime/
scalars.rs

1//! Built-in scalar type descriptors (§4.3, §11.4).
2//!
3//! Each scalar type has one `static` [`TypeDescriptor`] (`UNIT`, `BOOL`, `INT`,
4//! `BYTE`, `CHAR`, `FLOAT`), so its address is its identity. Every payload-aware
5//! operation routes through these descriptors; there are no type switches
6//! elsewhere (§11.4).
7//!
8//! Scalar payloads contain no `GcRef`s, so every scalar `trace` is a no-op —
9//! scalars are the leaf case that proves the descriptor machinery without
10//! exercising nested references. Composite tracing is covered by `Vec[T]`
11//! (ADR-013).
12//!
13//! Each descriptor is followed by its [`Payload`] handle — `INT_PAYLOAD` beside
14//! `INT` — which is what the allocators take. The descriptor derives its width
15//! from the payload type and then erases it, so an allocator handed a bare
16//! `&TypeDescriptor` could only compare widths at runtime; the handle carries
17//! the type, and the pairing is checked when the `static` is evaluated.
18
19use std::cmp::Ordering;
20use std::fmt::{self, Write as _};
21use std::hash::Hash;
22
23use crate::descriptor::{
24    BuiltinTypeId, DynamicHasher, FormatSink, Payload, Tracer, TypeDescriptor, hash_value,
25};
26use crate::heap::InlineClaimSite;
27
28// ---- payload types ---------------------------------------------------------
29//
30// These are the concrete in-payload Rust representations of each scalar. They
31// are `Copy` (no `Drop`), so every scalar `drop_value` is a no-op.
32
33/// `Unit` payload: no data.
34pub type UnitPayload = ();
35
36/// `Bool` payload: `0` is false, `1` is true (§4.3).
37pub type BoolPayload = u8;
38
39/// `Int` payload: signed 64-bit (§4.3).
40pub type IntPayload = i64;
41
42/// `Byte` payload: unsigned 8-bit (§4.3).
43pub type BytePayload = u8;
44
45/// `Char` payload: a validated Unicode scalar value (§4.3). Stored as `u32`.
46pub type CharPayload = u32;
47
48/// `Float` payload: IEEE 754 binary64 (§4.3).
49pub type FloatPayload = f64;
50
51// ---- shared callbacks ------------------------------------------------------
52//
53// One implementation per operation, instantiated at each descriptor with the
54// payload type that descriptor was built from — `TypeDescriptor::builtin::<P>`
55// and `scalar_compare::<P>` on lines you can read together, so a callback that
56// reads the wrong width — eight bytes off `Char`'s four-byte payload, say — is
57// visible where the descriptor is written rather than four functions away.
58//
59// `BoolPayload` and `BytePayload` are both `u8`, so `Bool` and `Byte` share
60// these instantiations outright — which is exactly why a `Payload` handle names
61// its descriptor rather than deriving it from the payload type (see
62// `BYTE_PAYLOAD`). The descriptor is the identity (ADR-038); the callback is
63// not.
64//
65// `Unit` and `Float` keep their own: `Unit`'s callbacks never read the pointer
66// they are handed, and `Float`'s equality, hash and order are each IEEE-754
67// decisions rather than the derived Rust ones (§4.12, ADR-045).
68//
69// `int_equals` and `char_equals` — the names `small_int.rs`, `small_char.rs`,
70// `dynamic_key.rs` and ADR-100/ADR-107 use — are `scalar_equals` instantiated
71// at `IntPayload` / `CharPayload`. The claims those places make about them (a
72// reflexive `u32 ==`, and so on) hold of these instantiations.
73
74/// A scalar payload holds no `GcRef`s, so there is nothing to report (ADR-013).
75unsafe fn scalar_trace(_: *mut u8, _: &mut dyn Tracer) {}
76
77/// A scalar payload is `Copy`, so there is nothing to release at sweep.
78unsafe fn scalar_drop(_: *mut u8) {}
79
80/// Structural equality for a scalar: Rust `==` on the payload type.
81///
82/// # Safety
83/// Both pointers must point at `P`s.
84unsafe fn scalar_equals<P: Copy + PartialEq>(a: *const u8, b: *const u8) -> bool {
85    // SAFETY: caller guarantees both pointers point at `P`s.
86    unsafe { *(a as *const P) == *(b as *const P) }
87}
88
89/// Structural hash for a scalar: the payload type's own `Hash`.
90///
91/// # Safety
92/// `payload` must point at a `P`.
93unsafe fn scalar_hash<P: Copy + Hash>(payload: *const u8, hasher: &mut dyn DynamicHasher) {
94    // SAFETY: caller guarantees `payload` points at a `P`.
95    let v = unsafe { *(payload as *const P) };
96    hash_value(hasher, &v);
97}
98
99/// Container order for a scalar: the payload type's own `Ord` (ADR-045). *Which*
100/// order that is, is a per-type decision — recorded at each descriptor below.
101///
102/// # Safety
103/// Both pointers must point at `P`s.
104unsafe fn scalar_compare<P: Copy + Ord>(a: *const u8, b: *const u8) -> Ordering {
105    // SAFETY: caller guarantees both pointers point at `P`s.
106    unsafe { (*(a as *const P)).cmp(&*(b as *const P)) }
107}
108
109// ---- Unit ------------------------------------------------------------------
110
111unsafe fn unit_format(_: *const u8, out: &mut FormatSink<'_>) {
112    let _ = out.write_str("Unit");
113}
114unsafe fn unit_equals(_: *const u8, _: *const u8) -> bool {
115    true
116}
117unsafe fn unit_hash(_: *const u8, hasher: &mut dyn DynamicHasher) {
118    // Unit is a singleton; all instances hash equally.
119    hash_value(hasher, &());
120}
121unsafe fn unit_compare(_: *const u8, _: *const u8) -> Ordering {
122    // A singleton has one value, so `Equal` is the only answer that agrees with
123    // `unit_equals` — and agreeing with equality is what makes it a total order
124    // rather than a shrug (ADR-138).
125    Ordering::Equal
126}
127
128/// Descriptor for the `Unit` scalar (§4.3).
129pub static UNIT: TypeDescriptor = TypeDescriptor::builtin::<UnitPayload>(
130    BuiltinTypeId::Unit,
131    "Unit",
132    scalar_trace,
133    scalar_drop,
134    unit_format,
135    Some(unit_equals),
136    Some(unit_hash),
137    // A `Unit` can be a `Map` key, so a container has to be able to order one
138    // (ADR-138). `<` on a `Unit` is still Y006 — that is `supports_ord`'s
139    // question, and it is deliberately a different one.
140    Some(unit_compare),
141);
142
143/// `Unit`'s payload handle. Its one value is an immortal, minted at startup —
144/// nothing gc-allocates a `Unit`.
145pub static UNIT_PAYLOAD: Payload<UnitPayload> = Payload::new(&UNIT);
146
147// ---- Bool ------------------------------------------------------------------
148
149unsafe fn bool_format(payload: *const u8, out: &mut FormatSink<'_>) {
150    // SAFETY: caller guarantees `payload` points at a `BoolPayload`.
151    let v = unsafe { *(payload as *const BoolPayload) };
152    let _ = out.write_str(if v != 0 { "true" } else { "false" });
153}
154
155/// Descriptor for the `Bool` scalar (§4.3).
156pub static BOOL: TypeDescriptor = TypeDescriptor::builtin::<BoolPayload>(
157    BuiltinTypeId::Bool,
158    "Bool",
159    scalar_trace,
160    scalar_drop,
161    bool_format,
162    Some(scalar_equals::<BoolPayload>),
163    Some(scalar_hash::<BoolPayload>),
164    // A `Bool` can be a `Map` key, so a container has to be able to order one
165    // (ADR-138). `true < false` is still Y006 — see `unit_compare`. The order is
166    // `false` before `true`, which is both the conventional one and the order of
167    // the rendered forms.
168    Some(scalar_compare::<BoolPayload>),
169);
170
171/// `Bool`'s payload handle. Both values are immortals too (RT-03), so this
172/// mints the pair at startup rather than one per comparison.
173pub static BOOL_PAYLOAD: Payload<BoolPayload> = Payload::new(&BOOL);
174
175// ---- Int -------------------------------------------------------------------
176
177/// Render an `Int`: the decimal digits, with a leading `-` when negative.
178///
179/// Factored out of [`int_format`] so `Int.to_text()` calls *this* rather than
180/// writing a second `write!` of its own (ADR-143). `out(n)` and `n.to_text()`
181/// disagreeing would be a defect in itself, and one writer with two callers is
182/// what makes it unrepresentable instead of merely tested — the shape
183/// [`write_float`] already has.
184pub(crate) fn write_int(out: &mut dyn fmt::Write, v: IntPayload) {
185    let _ = write!(out, "{v}");
186}
187
188unsafe fn int_format(payload: *const u8, out: &mut FormatSink<'_>) {
189    // SAFETY: caller guarantees `payload` points at an `IntPayload`.
190    let v = unsafe { *(payload as *const IntPayload) };
191    write_int(out, v);
192}
193
194/// Descriptor for the `Int` scalar (§4.3).
195pub static INT: TypeDescriptor = TypeDescriptor::builtin::<IntPayload>(
196    BuiltinTypeId::Int,
197    "Int",
198    scalar_trace,
199    scalar_drop,
200    int_format,
201    Some(scalar_equals::<IntPayload>),
202    Some(scalar_hash::<IntPayload>),
203    // Signed numeric order (ADR-045).
204    Some(scalar_compare::<IntPayload>),
205);
206
207/// `Int`'s payload handle. `IntPayload` is `i64` while a Rust integer literal
208/// defaults to `i32`; the handle is what makes the compiler resolve that
209/// mismatch rather than a runtime width check.
210pub static INT_PAYLOAD: Payload<IntPayload> = Payload::new(&INT);
211
212/// The inline bitmap claim for an `Int` that [`crate::small_int`]'s table does
213/// not hold (ADR-119).
214///
215/// **Minted here, beside the descriptor**, for [`crate::small_int`]'s reason one
216/// level up: the site's whole content is a function of `INT`, and a site minted
217/// anywhere else would be a second place that has to agree about which
218/// descriptor generated code is about to write into a header. `unwrap` in a
219/// `const` initializer means "fails the build": `INT` carries no `owned_bytes`
220/// callback and its 24-byte block is on the ladder, and if either stops being
221/// true this stops compiling rather than starting to under-charge the pacer.
222pub const INT_CLAIM_SITE: InlineClaimSite = match InlineClaimSite::of(&INT) {
223    Some(site) => site,
224    None => panic!("Int has no owned_bytes charge and its block is on the ladder"),
225};
226
227// ---- Byte ------------------------------------------------------------------
228
229unsafe fn byte_format(payload: *const u8, out: &mut FormatSink<'_>) {
230    // SAFETY: caller guarantees `payload` points at a `BytePayload`.
231    let v = unsafe { *(payload as *const BytePayload) };
232    let _ = write!(out, "{v}");
233}
234
235/// Descriptor for the `Byte` scalar (§4.3).
236pub static BYTE: TypeDescriptor = TypeDescriptor::builtin::<BytePayload>(
237    BuiltinTypeId::Byte,
238    "Byte",
239    scalar_trace,
240    scalar_drop,
241    byte_format,
242    Some(scalar_equals::<BytePayload>),
243    Some(scalar_hash::<BytePayload>),
244    // Unsigned numeric order (ADR-045).
245    Some(scalar_compare::<BytePayload>),
246);
247
248/// `Byte`'s payload handle. Its payload is the same Rust type as `Bool`'s,
249/// which is why a handle names its descriptor explicitly instead of the pairing
250/// being derived from the payload type.
251pub static BYTE_PAYLOAD: Payload<BytePayload> = Payload::new(&BYTE);
252
253// ---- Char ------------------------------------------------------------------
254
255/// Render a `Char`: the character itself, with no quotes and no escaping.
256///
257/// Factored out of [`char_format`] for [`write_int`]'s reason (ADR-143): the
258/// `U+FFFD` fallback below is a decision about what an impossible payload looks
259/// like, and `Char.to_text()` answering something else would make one of the two
260/// wrong without saying which.
261pub(crate) fn write_char(out: &mut dyn fmt::Write, v: CharPayload) {
262    match char::from_u32(v) {
263        Some(c) => {
264            let _ = write!(out, "{c}");
265        }
266        // Should not happen for a constructed Char, but never panic across a
267        // descriptor callback (§10.4 spirit): render a replacement.
268        None => {
269            let _ = out.write_str("\u{FFFD}");
270        }
271    }
272}
273
274unsafe fn char_format(payload: *const u8, out: &mut FormatSink<'_>) {
275    // SAFETY: caller guarantees `payload` points at a validated `CharPayload`.
276    let raw = unsafe { *(payload as *const CharPayload) };
277    write_char(out, raw);
278}
279
280/// Descriptor for the `Char` scalar (§4.3).
281pub static CHAR: TypeDescriptor = TypeDescriptor::builtin::<CharPayload>(
282    BuiltinTypeId::Char,
283    "Char",
284    scalar_trace,
285    scalar_drop,
286    char_format,
287    Some(scalar_equals::<CharPayload>),
288    Some(scalar_hash::<CharPayload>),
289    // Unicode scalar value order (ADR-045). The payload is the Unicode scalar
290    // value, so `u32` order *is* code-point order — and it is four bytes, so
291    // reading it as an `i64` would be both wrong and out of bounds.
292    // `CharPayload` here is what says so.
293    Some(scalar_compare::<CharPayload>),
294);
295
296/// `Char`'s payload handle. `CharPayload` is `u32`, not `char`: the two share a
297/// layout, so a runtime width check cannot tell them apart. The type argument
298/// is what says which.
299pub static CHAR_PAYLOAD: Payload<CharPayload> = Payload::new(&CHAR);
300
301// ---- Float ------------------------------------------------------------------
302
303/// Render a `Float` the way §4.12 asks: in the shortest form that reads back as
304/// **the same Praxis `Float`** (ADR-083).
305///
306/// Rust's `{}` is shortest-round-trippable for Rust, where a bare `1` re-reads
307/// as an `f64`. Praxis is not Rust here: §4.12's typing rule is that `42` is
308/// strictly an `Int` literal and that `Float` and `Int` never mix, so a `Float`
309/// rendered `1` does not read back as a `Float` at all — and, printed inside a
310/// collection, a `Vec[Float]` of `[3.0, 5.0]` would be indistinguishable from a
311/// `Vec[Int]`. So a finite value with no `.` and no exponent in its digits gets
312/// a `.0`, and everything else — including `inf`/`-inf`/`NaN`, which §4.12 names
313/// as those literals — is Rust's rendering unchanged.
314pub(crate) fn write_float(out: &mut dyn fmt::Write, v: FloatPayload) {
315    let rendered = format!("{v}");
316    let is_a_float_literal = rendered
317        .bytes()
318        .any(|b| b == b'.' || b == b'e' || b == b'E');
319    if v.is_finite() && !is_a_float_literal {
320        let _ = write!(out, "{rendered}.0");
321    } else {
322        let _ = out.write_str(&rendered);
323    }
324}
325
326unsafe fn float_format(payload: *const u8, out: &mut FormatSink<'_>) {
327    // SAFETY: caller guarantees `payload` points at a `FloatPayload`.
328    let v = unsafe { *(payload as *const FloatPayload) };
329    write_float(out, v);
330}
331unsafe fn float_equals(a: *const u8, b: *const u8) -> bool {
332    // SAFETY: caller guarantees both pointers point at `FloatPayload`s.
333    //
334    // IEEE-754 comparison: NaN compares unequal to everything, including
335    // itself. This matches the language's float comparison semantics (§4.12)
336    // and the Cranelift `fcmp` lowering used in generated code.
337    unsafe { *(a as *const FloatPayload) == *(b as *const FloatPayload) }
338}
339unsafe fn float_hash(payload: *const u8, hasher: &mut dyn DynamicHasher) {
340    // SAFETY: caller guarantees `payload` points at a `FloatPayload`.
341    //
342    // Hash the bit pattern so that equal floats hash equally (and unequal NaN
343    // bit patterns can differ, which is acceptable — NaN equality is already
344    // false for every NaN pair). Canonicalize -0.0 and +0.0 to the same bits
345    // so they hash identically, matching `==` (which treats them as equal).
346    let v = unsafe { *(payload as *const FloatPayload) };
347    let bits = if v == 0.0 {
348        v.to_bits() & 0x7fff_ffff_ffff_ffff
349    } else {
350        v.to_bits()
351    };
352    hash_value(hasher, &bits);
353}
354
355/// The **container** ordering of two `Float`s (ADR-045 decision 2): numeric for
356/// everything `partial_cmp` can answer — which makes `-0.0` equal to `+0.0`,
357/// agreeing with [`float_equals`] — and NaN last, equal to itself.
358///
359/// Not the source-level `<`: that is `Inst::FloatCmp`, stays IEEE-754, and
360/// answers `false` whenever either operand is NaN (§4.12). This callback exists
361/// because a `BinaryHeap` needs a *total* `Ord` or it corrupts its own sift
362/// invariants, and `f64::total_cmp` is rejected for splitting the two zeros.
363///
364/// # Safety
365/// Both pointers must point at `FloatPayload`s.
366unsafe fn float_compare(a: *const u8, b: *const u8) -> Ordering {
367    // SAFETY: caller guarantees both pointers point at `FloatPayload`s.
368    let (x, y) = unsafe { (*(a as *const FloatPayload), *(b as *const FloatPayload)) };
369    match x.partial_cmp(&y) {
370        Some(o) => o,
371        // Unordered: at least one is NaN. NaN sorts after every number and
372        // ties with another NaN.
373        None => match (x.is_nan(), y.is_nan()) {
374            (true, true) => Ordering::Equal,
375            (true, false) => Ordering::Greater,
376            (false, true) => Ordering::Less,
377            // Unreachable: `partial_cmp` on two non-NaN f64s always answers.
378            (false, false) => Ordering::Equal,
379        },
380    }
381}
382
383/// Descriptor for the `Float` scalar (§4.3).
384pub static FLOAT: TypeDescriptor = TypeDescriptor::builtin::<FloatPayload>(
385    BuiltinTypeId::Float,
386    "Float",
387    scalar_trace,
388    scalar_drop,
389    float_format,
390    Some(float_equals),
391    Some(float_hash),
392    // Numeric order with NaN last (ADR-045).
393    Some(float_compare),
394);
395
396/// `Float`'s payload handle.
397pub static FLOAT_PAYLOAD: Payload<FloatPayload> = Payload::new(&FLOAT);
398
399/// The inline bitmap claim for a `Float` (ADR-119). See [`INT_CLAIM_SITE`].
400///
401/// A `Float` has no intern table, so unlike `Int` this is the *whole* inline
402/// form of the box: there is no probe in front of it and the wrapper behind it
403/// is reached only when the claim itself bails.
404pub const FLOAT_CLAIM_SITE: InlineClaimSite = match InlineClaimSite::of(&FLOAT) {
405    Some(site) => site,
406    None => panic!("Float has no owned_bytes charge and its block is on the ladder"),
407};
408
409// ---- validation helper -----------------------------------------------------
410
411/// True iff `v` is a valid Unicode scalar value (a `Char` payload invariant).
412/// Used by the allocation helpers to uphold §4.3's "validated scalar value".
413pub(crate) fn is_valid_char(v: u32) -> bool {
414    char::from_u32(v).is_some()
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420    use crate::descriptor::StructHasher;
421
422    /// Exercise every scalar descriptor's format/equals/hash against a stack
423    /// payload, proving the vtable is wired correctly without going through the
424    /// allocator. This is the unit-level check; allocation/reclamation are
425    /// tested through the `Heap` in `heap.rs`.
426    #[test]
427    fn scalar_descriptors_format_equality_hash() {
428        use std::ptr;
429        let mut buf = String::new();
430
431        // Unit
432        buf.clear();
433        // SAFETY: Unit's format ignores its payload pointer.
434        unsafe { (UNIT.format)(ptr::null(), &mut crate::FormatSink::display(&mut buf)) };
435        assert_eq!(buf, "Unit");
436
437        // Bool
438        let t: BoolPayload = 1;
439        let f: BoolPayload = 0;
440        buf.clear();
441        unsafe { (BOOL.format)(ptr::addr_of!(t), &mut crate::FormatSink::display(&mut buf)) };
442        assert_eq!(buf, "true");
443        buf.clear();
444        unsafe { (BOOL.format)(ptr::addr_of!(f), &mut crate::FormatSink::display(&mut buf)) };
445        assert_eq!(buf, "false");
446        assert!(unsafe { (BOOL.equals.unwrap())(ptr::addr_of!(t), ptr::addr_of!(t)) });
447        assert!(!unsafe { (BOOL.equals.unwrap())(ptr::addr_of!(t), ptr::addr_of!(f)) });
448
449        // Int
450        let a: IntPayload = 42;
451        let b: IntPayload = -7;
452        buf.clear();
453        unsafe {
454            (INT.format)(
455                ptr::addr_of!(a) as *const u8,
456                &mut crate::FormatSink::display(&mut buf),
457            )
458        };
459        assert_eq!(buf, "42");
460        assert!(unsafe {
461            (INT.equals.unwrap())(ptr::addr_of!(a) as *const u8, ptr::addr_of!(a) as *const u8)
462        });
463        assert!(!unsafe {
464            (INT.equals.unwrap())(ptr::addr_of!(a) as *const u8, ptr::addr_of!(b) as *const u8)
465        });
466
467        // Byte
468        let by: BytePayload = 255;
469        buf.clear();
470        unsafe { (BYTE.format)(ptr::addr_of!(by), &mut crate::FormatSink::display(&mut buf)) };
471        assert_eq!(buf, "255");
472
473        // Char
474        let ch: CharPayload = 'A' as u32;
475        buf.clear();
476        unsafe {
477            (CHAR.format)(
478                ptr::addr_of!(ch) as *const u8,
479                &mut crate::FormatSink::display(&mut buf),
480            )
481        };
482        assert_eq!(buf, "A");
483
484        // Float — finite value formats via Rust's shortest round-trip form.
485        let f: FloatPayload = 2.5;
486        buf.clear();
487        unsafe {
488            (FLOAT.format)(
489                ptr::addr_of!(f) as *const u8,
490                &mut crate::FormatSink::display(&mut buf),
491            )
492        };
493        assert_eq!(buf, "2.5");
494        assert!(unsafe {
495            (FLOAT.equals.unwrap())(ptr::addr_of!(f) as *const u8, ptr::addr_of!(f) as *const u8)
496        });
497        // NaN compares unequal to everything, including itself.
498        let nan: FloatPayload = f64::NAN;
499        assert!(!unsafe {
500            (FLOAT.equals.unwrap())(
501                ptr::addr_of!(nan) as *const u8,
502                ptr::addr_of!(nan) as *const u8,
503            )
504        });
505        // ±0.0 are equal.
506        let pos_zero: FloatPayload = 0.0;
507        let neg_zero: FloatPayload = -0.0;
508        assert!(unsafe {
509            (FLOAT.equals.unwrap())(
510                ptr::addr_of!(pos_zero) as *const u8,
511                ptr::addr_of!(neg_zero) as *const u8,
512            )
513        });
514        // Special values format as literals.
515        let inf: FloatPayload = f64::INFINITY;
516        let neg_inf: FloatPayload = f64::NEG_INFINITY;
517        buf.clear();
518        unsafe {
519            (FLOAT.format)(
520                ptr::addr_of!(inf) as *const u8,
521                &mut crate::FormatSink::display(&mut buf),
522            )
523        };
524        assert_eq!(buf, "inf");
525        buf.clear();
526        unsafe {
527            (FLOAT.format)(
528                ptr::addr_of!(neg_inf) as *const u8,
529                &mut crate::FormatSink::display(&mut buf),
530            )
531        };
532        assert_eq!(buf, "-inf");
533        buf.clear();
534        unsafe {
535            (FLOAT.format)(
536                ptr::addr_of!(nan) as *const u8,
537                &mut crate::FormatSink::display(&mut buf),
538            )
539        };
540        assert_eq!(buf, "NaN");
541    }
542
543    #[test]
544    fn scalar_hash_is_stable() {
545        use std::ptr;
546        let a: IntPayload = 1234;
547        let mut h1 = StructHasher::new();
548        unsafe { (INT.hash.unwrap())(ptr::addr_of!(a) as *const u8, &mut h1) };
549        let mut h2 = StructHasher::new();
550        unsafe { (INT.hash.unwrap())(ptr::addr_of!(a) as *const u8, &mut h2) };
551        assert_eq!(h1.finish(), h2.finish());
552
553        // Different value → (almost certainly) different hash.
554        let b: IntPayload = 1235;
555        let mut h3 = StructHasher::new();
556        unsafe { (INT.hash.unwrap())(ptr::addr_of!(b) as *const u8, &mut h3) };
557        assert_ne!(h1.finish(), h3.finish());
558
559        // Float hashing is stable, and +0.0 / -0.0 collide (they compare equal).
560        let fp: FloatPayload = 2.5;
561        let mut fh1 = StructHasher::new();
562        unsafe { (FLOAT.hash.unwrap())(ptr::addr_of!(fp) as *const u8, &mut fh1) };
563        let mut fh2 = StructHasher::new();
564        unsafe { (FLOAT.hash.unwrap())(ptr::addr_of!(fp) as *const u8, &mut fh2) };
565        assert_eq!(fh1.finish(), fh2.finish());
566        let pos_zero: FloatPayload = 0.0;
567        let neg_zero: FloatPayload = -0.0;
568        let mut hp = StructHasher::new();
569        let mut hn = StructHasher::new();
570        unsafe { (FLOAT.hash.unwrap())(ptr::addr_of!(pos_zero) as *const u8, &mut hp) };
571        unsafe { (FLOAT.hash.unwrap())(ptr::addr_of!(neg_zero) as *const u8, &mut hn) };
572        assert_eq!(hp.finish(), hn.finish());
573    }
574
575    /// ADR-045 decision 2. The container order is total, so it has to answer
576    /// for NaN — and it has to agree with `equals` everywhere else, which is
577    /// why `f64::total_cmp` (which splits the two zeros) is not used.
578    #[test]
579    fn float_compare_is_numeric_with_nan_last() {
580        use std::ptr;
581        let cmp = FLOAT.compare.expect("Float is orderable");
582        let at = |v: &FloatPayload| ptr::addr_of!(*v) as *const u8;
583
584        let minus_two: FloatPayload = -2.0;
585        let minus_one: FloatPayload = -1.0;
586        let one: FloatPayload = 1.0;
587        // Numeric, not the signed bit pattern: -2.0 has the *larger* magnitude
588        // and so the larger unsigned payload.
589        assert_eq!(
590            unsafe { cmp(at(&minus_two), at(&minus_one)) },
591            Ordering::Less
592        );
593        assert_eq!(unsafe { cmp(at(&one), at(&minus_one)) }, Ordering::Greater);
594
595        // The two zeros are one value, as they are for `equals`.
596        let pos_zero: FloatPayload = 0.0;
597        let neg_zero: FloatPayload = -0.0;
598        assert_eq!(
599            unsafe { cmp(at(&pos_zero), at(&neg_zero)) },
600            Ordering::Equal
601        );
602
603        // NaN sorts after every number, including infinity, and ties with NaN.
604        let nan: FloatPayload = f64::NAN;
605        let inf: FloatPayload = f64::INFINITY;
606        assert_eq!(unsafe { cmp(at(&nan), at(&inf)) }, Ordering::Greater);
607        assert_eq!(unsafe { cmp(at(&inf), at(&nan)) }, Ordering::Less);
608        assert_eq!(unsafe { cmp(at(&nan), at(&nan)) }, Ordering::Equal);
609    }
610
611    /// The `compare` callback reads the payload it was declared for. A `Char`
612    /// payload is four bytes, so an ordering that read eight would be both
613    /// wrong and out of bounds.
614    #[test]
615    fn scalar_compare_reads_its_own_payload_width() {
616        use std::ptr;
617        let int_cmp = INT.compare.expect("Int is orderable");
618        let a: IntPayload = -5;
619        let b: IntPayload = 3;
620        assert_eq!(
621            unsafe { int_cmp(ptr::addr_of!(a) as *const u8, ptr::addr_of!(b) as *const u8,) },
622            Ordering::Less
623        );
624
625        let char_cmp = CHAR.compare.expect("Char is orderable");
626        let lower_a: CharPayload = 'a' as u32;
627        let beta: CharPayload = 'β' as u32;
628        assert_eq!(
629            unsafe {
630                char_cmp(
631                    ptr::addr_of!(lower_a) as *const u8,
632                    ptr::addr_of!(beta) as *const u8,
633                )
634            },
635            Ordering::Less,
636            "'a' (U+0061) precedes 'β' (U+03B2) by scalar value"
637        );
638
639        let byte_cmp = BYTE.compare.expect("Byte is orderable");
640        let low: BytePayload = 1;
641        let high: BytePayload = 200;
642        assert_eq!(
643            unsafe { byte_cmp(ptr::addr_of!(low), ptr::addr_of!(high)) },
644            Ordering::Less,
645            "Byte is unsigned: 200 is not negative"
646        );
647    }
648
649    /// `Bool` and `Unit` have a **container** order and no **source** order
650    /// (ADR-138). Both can be a `Map` key, so `out(m)` and `for k in m` have to
651    /// put them in some sequence and it has to be the same sequence twice;
652    /// `true < false` is still refused at check time, which is
653    /// `praxis_hir::capability::supports_ord`'s question and not this one.
654    #[test]
655    fn bool_and_unit_have_a_container_order_and_no_source_order() {
656        use std::ptr;
657        let (f, t): (BoolPayload, BoolPayload) = (0, 1);
658        assert_eq!(
659            unsafe {
660                scalar_compare::<BoolPayload>(ptr::addr_of!(f).cast(), ptr::addr_of!(t).cast())
661            },
662            Ordering::Less,
663            "false sorts before true"
664        );
665        let unit: UnitPayload = ();
666        assert_eq!(
667            unsafe { unit_compare(ptr::addr_of!(unit).cast(), ptr::addr_of!(unit).cast()) },
668            Ordering::Equal,
669            "a singleton equals itself and nothing else exists to order it against"
670        );
671        assert!(BOOL.is_orderable());
672        assert!(UNIT.is_orderable());
673        assert!(INT.is_orderable());
674        assert!(CHAR.is_orderable());
675        assert!(FLOAT.is_orderable());
676        assert!(BYTE.is_orderable());
677    }
678
679    #[test]
680    fn char_validation_matches_std() {
681        assert!(is_valid_char('A' as u32));
682        assert!(is_valid_char(0x10FFFF));
683        assert!(!is_valid_char(0x110000));
684        assert!(!is_valid_char(0xD800)); // surrogate
685    }
686
687    /// Every scalar's payload handle names the type its descriptor describes,
688    /// and the allocator writes exactly that.
689    ///
690    /// The *pairing* is already a compile-time property — each `Payload::new`
691    /// above runs during const evaluation of a `static`, so a handle whose type
692    /// argument disagreed with its descriptor would not build, and the value's
693    /// type is checked at every `gc_alloc` call site. What a test can still add
694    /// is that the list is **complete** (a scalar with no handle is one whose
695    /// callers must fall back to something unchecked) and that the round trip
696    /// through a real allocation reads back the value that went in — the
697    /// "declared payload type matches what its allocator writes" half.
698    #[test]
699    fn every_scalar_has_a_payload_handle_and_it_round_trips() {
700        use crate::descriptor::{BuiltinTypeId, Payload};
701
702        // One entry per scalar `BuiltinTypeId`, so a new scalar without a handle
703        // fails the exhaustive match below rather than being forgotten.
704        fn declared(id: BuiltinTypeId) -> Option<(&'static TypeDescriptor, usize, usize)> {
705            /// The layout `Payload<T>` promises, read back off the handle.
706            fn of<T: Copy>(p: Payload<T>) -> (&'static TypeDescriptor, usize, usize) {
707                (
708                    p.descriptor(),
709                    std::mem::size_of::<T>(),
710                    std::mem::align_of::<T>(),
711                )
712            }
713            Some(match id {
714                BuiltinTypeId::Unit => of(UNIT_PAYLOAD),
715                BuiltinTypeId::Bool => of(BOOL_PAYLOAD),
716                BuiltinTypeId::Int => of(INT_PAYLOAD),
717                BuiltinTypeId::Byte => of(BYTE_PAYLOAD),
718                BuiltinTypeId::Char => of(CHAR_PAYLOAD),
719                BuiltinTypeId::Float => of(FLOAT_PAYLOAD),
720                // Not a scalar: its payload owns Rust resources or is composite,
721                // so it is allocated through `alloc_with` and has no `Copy`
722                // handle. `Range` is the one non-scalar that does — see
723                // `range.rs`.
724                _ => return None,
725            })
726        }
727
728        for (id, expected) in [
729            (BuiltinTypeId::Unit, &UNIT),
730            (BuiltinTypeId::Bool, &BOOL),
731            (BuiltinTypeId::Int, &INT),
732            (BuiltinTypeId::Byte, &BYTE),
733            (BuiltinTypeId::Char, &CHAR),
734            (BuiltinTypeId::Float, &FLOAT),
735        ] {
736            let (descriptor, size, align) =
737                declared(id).unwrap_or_else(|| panic!("{id:?} has no payload handle"));
738            // The handle carries the one `static`, so descriptor identity — which
739            // is pointer identity (ADR-038) — survives being wrapped.
740            assert!(
741                std::ptr::eq(descriptor, expected),
742                "{id:?}'s handle names another descriptor"
743            );
744            assert_eq!(size, descriptor.size(), "{id:?} payload width");
745            assert_eq!(align, descriptor.align(), "{id:?} payload alignment");
746        }
747
748        // The round trip: what each allocator writes is readable as the declared
749        // payload type. The width is the handle's, and this is the value
750        // arriving intact through it.
751        let rt = crate::Runtime::new();
752        assert_eq!(rt.alloc_int(-42).as_int(), -42);
753        assert_eq!(rt.alloc_float(2.5).as_float(), 2.5);
754        // SAFETY: each ref was just allocated with the matching descriptor, so
755        // its payload is a value of that type.
756        unsafe {
757            assert_eq!(*rt.alloc_byte(255).payload::<BytePayload>(), 255);
758            assert_eq!(
759                *rt.alloc_char('A' as u32).payload::<CharPayload>(),
760                'A' as u32
761            );
762        }
763    }
764
765    /// **ADR-083.** A `Float` renders as a Praxis `Float` literal.
766    ///
767    /// The descriptor test above cannot catch this: its one value is `2.5`,
768    /// which already carries a `.`, so it passes whichever rule is in force.
769    /// Every case here is a whole-numbered value, an exponent, or a non-finite
770    /// one — the three places the two rules differ.
771    #[test]
772    fn a_whole_numbered_float_renders_as_a_float() {
773        let rendered = |v: FloatPayload| {
774            let mut buf = String::new();
775            // SAFETY: `v` is a `FloatPayload` and `FLOAT` is its descriptor.
776            unsafe {
777                (FLOAT.format)(
778                    std::ptr::addr_of!(v) as *const u8,
779                    &mut crate::FormatSink::display(&mut buf),
780                )
781            };
782            buf
783        };
784        // Without the `.0` these render identically to an `Int`, so `[3.0, 5.0]`
785        // and `[3, 5]` would print the same and neither would read back as the
786        // other's type.
787        assert_eq!(rendered(1.0), "1.0");
788        assert_eq!(rendered(0.0), "0.0");
789        assert_eq!(rendered(-7.0), "-7.0");
790        assert_eq!(rendered(1e10), "10000000000.0");
791        // Already a literal: untouched, and no second `.0`.
792        assert_eq!(rendered(2.5), "2.5");
793        assert_eq!(rendered(0.1 + 0.2), "0.30000000000000004");
794        // §4.12 names these three, and none of them takes a `.0`.
795        assert_eq!(rendered(f64::INFINITY), "inf");
796        assert_eq!(rendered(f64::NEG_INFINITY), "-inf");
797        assert_eq!(rendered(f64::NAN), "NaN");
798    }
799
800    /// **ADR-083's rule stated as a round trip.** The rendered form of a
801    /// `Float` is the text that reads back as *the same* `Float`, so the check
802    /// is a re-read and not a string comparison. `to_bits` is what tells the two
803    /// zeros apart; `==` cannot, because IEEE-754 says they are equal.
804    ///
805    /// This asks the *formatter*. `run_pass_float_negative_zero` in
806    /// `praxis-cli`'s `run.rs` asks the evaluator the same question. Both halves
807    /// are worth having, because an edit to `FLOAT.format` would otherwise stop
808    /// satisfying the round trip without any evaluator test noticing.
809    #[test]
810    fn a_rendered_float_reads_back_as_the_same_float() {
811        let rendered = |v: FloatPayload| {
812            let mut buf = String::new();
813            // SAFETY: `v` is a `FloatPayload` and `FLOAT` is its descriptor.
814            unsafe {
815                (FLOAT.format)(
816                    std::ptr::addr_of!(v) as *const u8,
817                    &mut crate::FormatSink::display(&mut buf),
818                )
819            };
820            buf
821        };
822        for v in [
823            0.0_f64,
824            -0.0,
825            1.0,
826            -7.0,
827            2.5,
828            1e10,
829            0.1 + 0.2,
830            f64::MAX,
831            f64::MIN_POSITIVE,
832        ] {
833            let text = rendered(v);
834            let reread: f64 = text
835                .parse()
836                .unwrap_or_else(|e| panic!("`{text}` does not read back as a Float: {e}"));
837            assert_eq!(
838                reread.to_bits(),
839                v.to_bits(),
840                "`{text}` read back as a different Float"
841            );
842        }
843        // The signed zeros are distinct *values* — different bit patterns — and
844        // so must render distinctly for the round trip above to mean anything.
845        // They are NOT distinct to a container: ADR-045's `compare` treats them
846        // as equal, and rejected `f64::total_cmp` precisely for splitting them,
847        // so a `Map` keyed on them holds one entry. Rendering and ordering
848        // disagree here on purpose; see §4.12.
849        assert_eq!(rendered(-0.0), "-0.0");
850        assert_ne!(rendered(-0.0), rendered(0.0));
851    }
852}