eb4db9dafc
Adds the workspace-global typeclass instance registry per Decision 11
"Resolution and monomorphisation". Built at the end of load_workspace
after the DFS over imports completes; keyed by (class-name, type-hash)
where type-hash uses the new canonical::type_hash (16-hex prefix,
parallel to def_hash and module_hash).
Three coherence checks fire from build_registry, each surfacing as a
distinct WorkspaceLoadError variant:
- OrphanInstance: `instance C T` not in C's or T's defining module.
- DuplicateInstance: two entries share the same (class, type-hash) key.
- MissingMethod: instance omits a required (non-default) method.
The CLI's workspace_error_to_diagnostic is extended with the three new
codes (orphan-instance / duplicate-instance / missing-method).
All existing Workspace { ... } construction sites get the new
`registry` field, defaulting to Registry::default() at synthetic /
test-only paths and threading through ws.registry.clone() at codepath
sites that already hold a real workspace.
Includes hash-stability regression tests (iter22b1_schema_extension_*
and iter22b1_classdef_empty_optionals_hash_stable) and the empty-
registry positive test against examples/sum.ail.json. Test count:
288 → 291 (3 new tests, all pass).
136 lines
4.6 KiB
Rust
136 lines
4.6 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
|
|
}
|
|
|
|
/// 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<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'));
|
|
}
|
|
}
|