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}