From 7c95a697809c22e22a428980d8082f09cf132401 Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 10 May 2026 14:45:34 +0200 Subject: [PATCH] floats iter 1.3: bit-stability tests for Literal::Float --- crates/ailang-core/src/canonical.rs | 84 +++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/crates/ailang-core/src/canonical.rs b/crates/ailang-core/src/canonical.rs index 3948dfa..8ab7a0e 100644 --- a/crates/ailang-core/src/canonical.rs +++ b/crates/ailang-core/src/canonical.rs @@ -152,4 +152,88 @@ mod tests { let s = std::str::from_utf8(&bytes).unwrap(); assert_eq!(s, r#"{"bits":"3ff8000000000000","kind":"float"}"#); } + + /// A5 guarantee: -0.0 and +0.0 are distinct bit patterns and + /// therefore distinct Form-A literals (they hash distinctly even + /// though IEEE-`==` will report them equal at the value level + /// when arithmetic comparison ships). + #[test] + fn negative_zero_and_positive_zero_serialise_distinctly() { + use crate::ast::Literal; + let pos = Literal::Float { bits: 0x0u64 }; + let neg = Literal::Float { bits: 0x8000_0000_0000_0000u64 }; + let pos_bytes = to_bytes(&pos); + let neg_bytes = to_bytes(&neg); + assert_ne!(pos_bytes, neg_bytes, "+0 and -0 must hash distinctly"); + assert_eq!( + std::str::from_utf8(&pos_bytes).unwrap(), + r#"{"bits":"0000000000000000","kind":"float"}"# + ); + assert_eq!( + std::str::from_utf8(&neg_bytes).unwrap(), + r#"{"bits":"8000000000000000","kind":"float"}"# + ); + } + + /// A1 guarantee: a NaN bit pattern survives canonicalisation + /// without collapse to JSON `null` (which is what the `serde_json` + /// number path does for non-finite floats — and the reason + /// `Literal::Float` routes through the *string* path). + #[test] + fn nan_bits_preserved() { + use crate::ast::Literal; + let qnan = Literal::Float { bits: 0x7ff8_0000_0000_0000u64 }; + let bytes = to_bytes(&qnan); + assert_eq!( + std::str::from_utf8(&bytes).unwrap(), + r#"{"bits":"7ff8000000000000","kind":"float"}"# + ); + } + + /// A1 guarantee: ±Inf bit patterns survive canonicalisation — + /// JSON numbers cannot represent infinity at all, the string path + /// must. + #[test] + fn inf_bits_preserved() { + use crate::ast::Literal; + let pos_inf = Literal::Float { bits: 0x7ff0_0000_0000_0000u64 }; + let neg_inf = Literal::Float { bits: 0xfff0_0000_0000_0000u64 }; + assert_eq!( + std::str::from_utf8(&to_bytes(&pos_inf)).unwrap(), + r#"{"bits":"7ff0000000000000","kind":"float"}"# + ); + assert_eq!( + std::str::from_utf8(&to_bytes(&neg_inf)).unwrap(), + r#"{"bits":"fff0000000000000","kind":"float"}"# + ); + } + + /// Round-trip: serialise then deserialise via `serde_json` — bits + /// preserved bit-for-bit. + #[test] + fn float_literal_serde_roundtrip() { + use crate::ast::Literal; + let cases = [ + 0x0u64, + 0x8000_0000_0000_0000u64, + 0x3ff8_0000_0000_0000u64, // 1.5 + 0x7ff8_0000_0000_0000u64, // qNaN + 0x7ff0_0000_0000_0000u64, // +Inf + 0xfff0_0000_0000_0000u64, // -Inf + 0xffff_ffff_ffff_ffffu64, // saturated + ]; + for &bits in &cases { + let lit = Literal::Float { bits }; + let json = serde_json::to_string(&lit).unwrap(); + let back: Literal = serde_json::from_str(&json).unwrap(); + match back { + Literal::Float { bits: got } => assert_eq!( + got, bits, + "round-trip lost bits for {:#018x}: json={}", + bits, json + ), + other => panic!("expected Literal::Float, got {:?}", other), + } + } + } }