//! Canonical JSON serialization. //! //! Writes JSON without whitespace and with lexicographically sorted //! object keys. This makes the representation deterministic across //! runs, platforms, and `serde_json` versions, which is what makes it //! suitable as the pre-image for [`crate::def_hash`] / //! [`crate::module_hash`]. //! //! The single entry point is [`to_bytes`]. This module deliberately //! does **not** parse, validate, or hash — it only emits bytes. It also //! does not attempt to be a general-purpose canonical-JSON library; //! Number formatting follows whatever `serde_json::Number::to_string` //! does — used only for the AST integer literals. Float literals do //! not go through the number path; [`crate::ast::Literal::Float`] //! carries its IEEE-754 bit pattern as a `u64` and serialises via a //! 16-character lowercase hex *string*, which the canonical writer //! treats like any other string. The string path is what makes float //! canonical bytes bit-stable across `serde_json` versions and what //! lets NaN / ±Inf round-trip at all (they cannot, as JSON numbers). //! //! # Examples //! //! ```ignore //! use ailang_core::canonical; //! use serde_json::json; //! //! // Object keys are sorted; no whitespace appears in the output. //! let bytes = canonical::to_bytes(&json!({ "b": 1, "a": 2 })); //! assert_eq!(std::str::from_utf8(&bytes).unwrap(), r#"{"a":2,"b":1}"#); //! ``` use std::io::Write; /// Serialize `value` to canonical JSON bytes. /// /// Object keys are sorted lexicographically, no whitespace is emitted, /// and arrays preserve their input order. The output is the byte /// pre-image used by [`crate::def_hash`] and /// [`crate::workspace::module_hash`]; if you change what bytes this /// function emits for a given value, all on-disk hashes change with it. /// /// # Panics /// /// Panics if `value` cannot be converted to a `serde_json::Value` /// (i.e. its `Serialize` impl errors). For the AST types in /// [`crate::ast`] this never happens. pub fn to_bytes(value: &T) -> Vec { let v = serde_json::to_value(value).expect("serializable"); let mut out = Vec::new(); write_value(&v, &mut out).expect("write to Vec"); out } /// Iter 22b.1: 16-hex-char hash of a [`crate::ast::Type`] in isolation. /// /// Used by [`crate::workspace::Registry`] to key `InstanceDef`s by /// their target type. Parallel in shape to [`crate::def_hash`] and /// [`crate::workspace::module_hash`]: BLAKE3 over canonical-JSON /// bytes, truncated to 16 hex chars. /// /// The key property is determinism — two `Type` values that are /// canonically equal MUST hash identically — so the workspace /// registry's "is `instance C T` already declared?" check is /// representation-independent. pub fn type_hash(t: &crate::ast::Type) -> String { let bytes = to_bytes(t); let h = blake3::hash(&bytes); h.to_hex().as_str()[..16].to_string() } fn write_value(v: &serde_json::Value, out: &mut Vec) -> std::io::Result<()> { use serde_json::Value; match v { Value::Null => out.write_all(b"null"), Value::Bool(true) => out.write_all(b"true"), Value::Bool(false) => out.write_all(b"false"), Value::Number(n) => out.write_all(n.to_string().as_bytes()), Value::String(s) => { let escaped = serde_json::to_string(s).expect("string serializable"); out.write_all(escaped.as_bytes()) } Value::Array(arr) => { out.write_all(b"[")?; for (i, item) in arr.iter().enumerate() { if i > 0 { out.write_all(b",")?; } write_value(item, out)?; } out.write_all(b"]") } Value::Object(map) => { let mut keys: Vec<&String> = map.keys().collect(); keys.sort(); out.write_all(b"{")?; for (i, k) in keys.iter().enumerate() { if i > 0 { out.write_all(b",")?; } let kj = serde_json::to_string(k).expect("key serializable"); out.write_all(kj.as_bytes())?; out.write_all(b":")?; write_value(&map[*k], out)?; } out.write_all(b"}") } } } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn sorts_keys() { let v = json!({ "b": 1, "a": 2 }); let bytes = to_bytes(&v); assert_eq!(std::str::from_utf8(&bytes).unwrap(), r#"{"a":2,"b":1}"#); } #[test] fn nested_keys_sorted() { let v = json!({ "z": { "y": 1, "x": [3, { "b": 2, "a": 1 }] } }); let bytes = to_bytes(&v); assert_eq!( std::str::from_utf8(&bytes).unwrap(), r#"{"z":{"x":[3,{"a":1,"b":2}],"y":1}}"# ); } #[test] fn no_whitespace() { let v = json!({ "a": 1, "b": [1, 2, 3] }); let bytes = to_bytes(&v); let s = std::str::from_utf8(&bytes).unwrap(); assert!(!s.contains(' ')); assert!(!s.contains('\n')); } /// Iter 22-floats.1 RED: a `Literal::Float` carries the IEEE-754 /// binary64 bit pattern as a `u64`; canonical JSON encodes it as /// `{"bits":"<16-lowercase-hex>","kind":"float"}` — string path, /// NOT through `serde_json::Number` (which is not bit-stable for /// floats and cannot represent NaN / ±Inf at all). /// /// Pinned literal: `1.5_f64` → bit pattern `0x3FF8000000000000` → /// hex string `"3ff8000000000000"`. Sorted-key order is /// `bits` < `kind` lexicographically. #[test] fn float_literal_canonical_bytes() { use crate::ast::Literal; let lit = Literal::Float { bits: 0x3ff8_0000_0000_0000u64, }; let bytes = to_bytes(&lit); 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), } } } }