Files
AILang/crates/ailang-core/src/canonical.rs
T
Brummel c90926dbba Iter 13d: rustdoc polish for ailang-core + new docwriter agent
Adds ailang-docwriter to /agents/ — a recurring role for keeping
crate-, module-, and pub-item-level rustdoc accurate. First mission:
ailang-core. Crate root, every module root, every pub item documented;
intra-doc links throughout; Iter-13a additions (TypeDef.vars, Type::Con.args)
get an explicit backwards-compat note. Two stale broken-link warnings in
ailang-check fixed in passing. cargo doc --no-deps now warning-free across
the workspace; promoted to verification invariant 6 in DESIGN.md.
2026-05-07 15:02:35 +02:00

119 lines
3.9 KiB
Rust

//! 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, which is fine for the AILang AST (integers and short strings;
//! no floats).
//!
//! # 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<T: serde::Serialize>(value: &T) -> Vec<u8> {
let v = serde_json::to_value(value).expect("serializable");
let mut out = Vec::new();
write_value(&v, &mut out).expect("write to Vec");
out
}
fn write_value(v: &serde_json::Value, out: &mut Vec<u8>) -> 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'));
}
}