use std::collections::HashMap;
use panproto_expr::{Expr, Literal, Pattern};
use panproto_gat::Name;
use smallvec::SmallVec;
use crate::schema::{
CoercionSpec, Constraint, Edge, HyperEdge, Schema, UsageMode, Variant, Vertex,
};
#[must_use]
pub fn canonical_bytes(schema: &Schema) -> Vec<u8> {
let mut out = Vec::new();
push_graph(&mut out, schema);
push_annotations(&mut out, schema);
push_enrichment(&mut out, schema);
push_indices(&mut out, schema);
out
}
#[must_use]
pub fn canonical_digest(schema: &Schema) -> [u8; 32] {
*blake3::hash(&canonical_bytes(schema)).as_bytes()
}
fn push_graph(out: &mut Vec<u8>, schema: &Schema) {
push_str(out, &schema.protocol);
let vertices = sorted(&schema.vertices);
push_len(out, vertices.len());
for (id, vertex) in vertices {
push_name(out, id);
push_vertex(out, vertex);
}
let edges = sorted(&schema.edges);
push_len(out, edges.len());
for (edge, kind) in edges {
push_edge(out, edge);
push_name(out, kind);
}
let hyper_edges = sorted(&schema.hyper_edges);
push_len(out, hyper_edges.len());
for (id, hyper_edge) in hyper_edges {
push_name(out, id);
push_hyper_edge(out, hyper_edge);
}
let constraints = sorted(&schema.constraints);
push_len(out, constraints.len());
for (id, list) in constraints {
push_name(out, id);
push_len(out, list.len());
for constraint in list {
push_constraint(out, constraint);
}
}
let required = sorted(&schema.required);
push_len(out, required.len());
for (id, list) in required {
push_name(out, id);
push_len(out, list.len());
for edge in list {
push_edge(out, edge);
}
}
let nsids = sorted(&schema.nsids);
push_len(out, nsids.len());
for (id, nsid) in nsids {
push_name(out, id);
push_name(out, nsid);
}
push_len(out, schema.entries.len());
for entry in &schema.entries {
push_name(out, entry);
}
}
fn push_annotations(out: &mut Vec<u8>, schema: &Schema) {
let variants = sorted(&schema.variants);
push_len(out, variants.len());
for (parent, arms) in variants {
push_name(out, parent);
push_len(out, arms.len());
for arm in arms {
push_variant(out, arm);
}
}
let orderings = sorted(&schema.orderings);
push_len(out, orderings.len());
for (edge, position) in orderings {
push_edge(out, edge);
out.extend_from_slice(&position.to_le_bytes());
}
let recursion_points = sorted(&schema.recursion_points);
push_len(out, recursion_points.len());
for (mu, point) in recursion_points {
push_name(out, mu);
push_name(out, &point.target_vertex);
}
let spans = sorted(&schema.spans);
push_len(out, spans.len());
for (id, span) in spans {
push_name(out, id);
push_name(out, &span.id);
push_name(out, &span.left);
push_name(out, &span.right);
}
let usage_modes = sorted(&schema.usage_modes);
push_len(out, usage_modes.len());
for (edge, mode) in usage_modes {
push_edge(out, edge);
push_usage_mode(out, mode);
}
let nominal = sorted(&schema.nominal);
push_len(out, nominal.len());
for (id, flag) in nominal {
push_name(out, id);
out.push(u8::from(*flag));
}
}
fn push_enrichment(out: &mut Vec<u8>, schema: &Schema) {
let coercions = sorted(&schema.coercions);
push_len(out, coercions.len());
for ((source_kind, target_kind), spec) in coercions {
push_name(out, source_kind);
push_name(out, target_kind);
push_coercion_spec(out, spec);
}
push_expr_map(out, &schema.mergers);
push_expr_map(out, &schema.defaults);
push_expr_map(out, &schema.policies);
}
fn push_indices(out: &mut Vec<u8>, schema: &Schema) {
push_vertex_index(out, &schema.outgoing);
push_vertex_index(out, &schema.incoming);
let between = sorted(&schema.between);
push_len(out, between.len());
for ((src, tgt), bucket) in between {
push_name(out, src);
push_name(out, tgt);
push_edges(out, bucket.as_slice());
}
}
fn sorted<K: Ord, V, S>(map: &HashMap<K, V, S>) -> Vec<(&K, &V)> {
let mut items: Vec<(&K, &V)> = map.iter().collect();
items.sort_by(|left, right| left.0.cmp(right.0));
items
}
fn push_len(out: &mut Vec<u8>, len: usize) {
let wide = u64::try_from(len).unwrap_or(u64::MAX);
out.extend_from_slice(&wide.to_le_bytes());
}
fn push_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
push_len(out, bytes.len());
out.extend_from_slice(bytes);
}
fn push_str(out: &mut Vec<u8>, value: &str) {
push_bytes(out, value.as_bytes());
}
fn push_name(out: &mut Vec<u8>, name: &Name) {
push_str(out, name.as_ref());
}
fn push_opt_name(out: &mut Vec<u8>, name: Option<&Name>) {
match name {
None => out.push(0),
Some(value) => {
out.push(1);
push_name(out, value);
}
}
}
fn push_vertex(out: &mut Vec<u8>, vertex: &Vertex) {
push_name(out, &vertex.id);
push_name(out, &vertex.kind);
push_opt_name(out, vertex.nsid.as_ref());
}
fn push_edge(out: &mut Vec<u8>, edge: &Edge) {
push_name(out, &edge.src);
push_name(out, &edge.tgt);
push_name(out, &edge.kind);
push_opt_name(out, edge.name.as_ref());
}
fn push_edges(out: &mut Vec<u8>, edges: &[Edge]) {
push_len(out, edges.len());
for edge in edges {
push_edge(out, edge);
}
}
fn push_hyper_edge(out: &mut Vec<u8>, hyper_edge: &HyperEdge) {
push_name(out, &hyper_edge.id);
push_name(out, &hyper_edge.kind);
let signature = sorted(&hyper_edge.signature);
push_len(out, signature.len());
for (label, vertex_id) in signature {
push_name(out, label);
push_name(out, vertex_id);
}
push_name(out, &hyper_edge.parent_label);
}
fn push_constraint(out: &mut Vec<u8>, constraint: &Constraint) {
push_name(out, &constraint.sort);
push_str(out, &constraint.value);
}
fn push_variant(out: &mut Vec<u8>, variant: &Variant) {
push_name(out, &variant.id);
push_name(out, &variant.parent_vertex);
push_opt_name(out, variant.tag.as_ref());
}
fn push_usage_mode(out: &mut Vec<u8>, mode: &UsageMode) {
out.push(match mode {
UsageMode::Structural => 0,
UsageMode::Linear => 1,
UsageMode::Affine => 2,
});
}
fn push_coercion_spec(out: &mut Vec<u8>, spec: &CoercionSpec) {
push_expr(out, &spec.forward);
match &spec.inverse {
None => out.push(0),
Some(inverse) => {
out.push(1);
push_expr(out, inverse);
}
}
push_str(out, &format!("{:?}", spec.class));
}
fn push_expr_map<S>(out: &mut Vec<u8>, map: &HashMap<Name, Expr, S>) {
let items = sorted(map);
push_len(out, items.len());
for (key, expr) in items {
push_name(out, key);
push_expr(out, expr);
}
}
fn push_vertex_index<S>(out: &mut Vec<u8>, index: &HashMap<Name, SmallVec<Edge, 4>, S>) {
let items = sorted(index);
push_len(out, items.len());
for (vertex_id, bucket) in items {
push_name(out, vertex_id);
push_edges(out, bucket.as_slice());
}
}
fn push_arc_str(out: &mut Vec<u8>, value: &std::sync::Arc<str>) {
push_str(out, value);
}
fn push_expr(out: &mut Vec<u8>, expr: &Expr) {
match expr {
Expr::Var(name) => {
out.push(0);
push_arc_str(out, name);
}
Expr::Lam(param, body) => {
out.push(1);
push_arc_str(out, param);
push_expr(out, body);
}
Expr::App(func, arg) => {
out.push(2);
push_expr(out, func);
push_expr(out, arg);
}
Expr::Lit(literal) => {
out.push(3);
push_literal(out, literal);
}
Expr::Record(fields) => {
out.push(4);
push_len(out, fields.len());
for (name, value) in fields {
push_arc_str(out, name);
push_expr(out, value);
}
}
Expr::List(items) => {
out.push(5);
push_len(out, items.len());
for item in items {
push_expr(out, item);
}
}
Expr::Field(base, field) => {
out.push(6);
push_expr(out, base);
push_arc_str(out, field);
}
Expr::Index(base, index) => {
out.push(7);
push_expr(out, base);
push_expr(out, index);
}
Expr::Match { scrutinee, arms } => {
out.push(8);
push_expr(out, scrutinee);
push_len(out, arms.len());
for (pattern, body) in arms {
push_pattern(out, pattern);
push_expr(out, body);
}
}
Expr::Let { name, value, body } => {
out.push(9);
push_arc_str(out, name);
push_expr(out, value);
push_expr(out, body);
}
Expr::Builtin(op, args) => {
out.push(10);
push_str(out, &format!("{op:?}"));
push_len(out, args.len());
for arg in args {
push_expr(out, arg);
}
}
}
}
fn push_literal(out: &mut Vec<u8>, literal: &Literal) {
match literal {
Literal::Bool(value) => {
out.push(0);
out.push(u8::from(*value));
}
Literal::Int(value) => {
out.push(1);
out.extend_from_slice(&value.to_le_bytes());
}
Literal::Float(value) => {
out.push(2);
out.extend_from_slice(&value.to_bits().to_le_bytes());
}
Literal::Str(value) => {
out.push(3);
push_str(out, value);
}
Literal::Bytes(value) => {
out.push(4);
push_bytes(out, value);
}
Literal::Null => out.push(5),
Literal::Record(fields) => {
out.push(6);
push_len(out, fields.len());
for (name, value) in fields {
push_arc_str(out, name);
push_literal(out, value);
}
}
Literal::List(items) => {
out.push(7);
push_len(out, items.len());
for item in items {
push_literal(out, item);
}
}
Literal::Closure { param, body, env } => {
out.push(8);
push_arc_str(out, param);
push_expr(out, body);
push_len(out, env.len());
for (name, value) in env.iter() {
push_arc_str(out, name);
push_literal(out, value);
}
}
}
}
fn push_pattern(out: &mut Vec<u8>, pattern: &Pattern) {
match pattern {
Pattern::Wildcard => out.push(0),
Pattern::Var(name) => {
out.push(1);
push_arc_str(out, name);
}
Pattern::Lit(literal) => {
out.push(2);
push_literal(out, literal);
}
Pattern::Record(fields) => {
out.push(3);
push_len(out, fields.len());
for (name, sub) in fields {
push_arc_str(out, name);
push_pattern(out, sub);
}
}
Pattern::List(items) => {
out.push(4);
push_len(out, items.len());
for item in items {
push_pattern(out, item);
}
}
Pattern::Constructor(tag, args) => {
out.push(5);
push_arc_str(out, tag);
push_len(out, args.len());
for arg in args {
push_pattern(out, arg);
}
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::builder::SchemaBuilder;
use crate::protocol::Protocol;
use crate::schema::{RecursionPoint, Span};
use panproto_gat::CoercionClass;
fn protocol() -> Protocol {
Protocol {
name: "fixture".to_owned(),
..Protocol::default()
}
}
fn fixture() -> Schema {
let protocol = protocol();
let mut signature: HashMap<String, String> = HashMap::new();
signature.insert("parent".to_owned(), "root".to_owned());
signature.insert("child".to_owned(), "leaf".to_owned());
let mut schema = SchemaBuilder::new(&protocol)
.vertex("root", "object", Some("com.example.root"))
.expect("root")
.vertex("leaf", "string", None)
.expect("leaf")
.edge("root", "leaf", "prop", Some("leaf"))
.expect("root -> leaf")
.hyper_edge("he", "record", signature, "parent")
.expect("he")
.constraint("leaf", "maxLength", "10")
.constraint("leaf", "format", "uuid")
.required(
"root",
vec![Edge {
src: Name::from("root"),
tgt: Name::from("leaf"),
kind: Name::from("prop"),
name: Some(Name::from("leaf")),
}],
)
.coercion(
"object",
"string",
CoercionSpec {
forward: Expr::Lit(Literal::Float(1.5)),
inverse: Some(Expr::Var("x".into())),
class: CoercionClass::Retraction,
},
)
.merger("root", Expr::Lit(Literal::Int(7)))
.default_expr("leaf", Expr::Lit(Literal::Null))
.policy("maxLength", Expr::Lit(Literal::Bool(true)))
.entry("root")
.build()
.expect("build");
schema.variants.insert(
Name::from("root"),
vec![Variant {
id: Name::from("leaf"),
parent_vertex: Name::from("root"),
tag: Some(Name::from("t")),
}],
);
let edge = Edge {
src: Name::from("root"),
tgt: Name::from("leaf"),
kind: Name::from("prop"),
name: Some(Name::from("leaf")),
};
schema.orderings.insert(edge.clone(), 3);
schema.recursion_points.insert(
Name::from("root"),
RecursionPoint {
target_vertex: Name::from("leaf"),
},
);
schema.spans.insert(
Name::from("s"),
Span {
id: Name::from("s"),
left: Name::from("root"),
right: Name::from("leaf"),
},
);
schema.usage_modes.insert(edge, UsageMode::Linear);
schema.nominal.insert(Name::from("root"), true);
schema
}
#[test]
fn encoding_is_stable_across_a_hundred_independent_builds() {
let baseline = canonical_bytes(&fixture());
for round in 0..100 {
assert_eq!(
canonical_bytes(&fixture()),
baseline,
"encoding drifted on round {round}"
);
}
}
#[test]
fn the_pointing_is_part_of_the_identity() {
let unpointed = fixture();
let mut repointed = unpointed.clone();
repointed.entries = vec![Name::from("leaf"), Name::from("root")];
assert_ne!(canonical_bytes(&unpointed), canonical_bytes(&repointed));
let mut reordered = unpointed.clone();
reordered.entries = vec![Name::from("root"), Name::from("leaf")];
let mut other_order = unpointed;
other_order.entries = vec![Name::from("leaf"), Name::from("root")];
assert_ne!(canonical_bytes(&reordered), canonical_bytes(&other_order));
}
#[test]
fn every_field_group_contributes() {
let base = fixture();
let mut changed_graph = base.clone();
changed_graph.protocol = "other".to_owned();
assert_ne!(canonical_bytes(&base), canonical_bytes(&changed_graph));
let mut changed_annotation = base.clone();
changed_annotation.nominal.insert(Name::from("leaf"), false);
assert_ne!(canonical_bytes(&base), canonical_bytes(&changed_annotation));
let mut changed_enrichment = base.clone();
changed_enrichment
.policies
.insert(Name::from("format"), Expr::Lit(Literal::Null));
assert_ne!(canonical_bytes(&base), canonical_bytes(&changed_enrichment));
let mut changed_index = base.clone();
changed_index.outgoing.remove("root");
assert_ne!(canonical_bytes(&base), canonical_bytes(&changed_index));
}
#[test]
fn concatenation_is_unambiguous() {
let protocol = protocol();
let split = SchemaBuilder::new(&protocol)
.vertex("a", "object", None)
.expect("a")
.vertex("b", "object", None)
.expect("b")
.build()
.expect("build");
let joined = SchemaBuilder::new(&protocol)
.vertex("ab", "object", None)
.expect("ab")
.build()
.expect("build");
assert_ne!(canonical_bytes(&split), canonical_bytes(&joined));
let one = SchemaBuilder::new(&protocol)
.vertex("a", "aab", None)
.expect("a")
.build()
.expect("build");
let other = SchemaBuilder::new(&protocol)
.vertex("aa", "b", None)
.expect("aa")
.build()
.expect("build");
assert_ne!(
canonical_bytes(&one),
canonical_bytes(&other),
"id ++ id ++ kind concatenates alike, so only the string prefix separates these"
);
}
#[test]
fn float_bits_separate_signed_zeroes() {
let base = fixture();
let mut negative = base.clone();
negative.coercions.insert(
(Name::from("object"), Name::from("string")),
CoercionSpec {
forward: Expr::Lit(Literal::Float(-0.0)),
inverse: None,
class: CoercionClass::Retraction,
},
);
let mut positive = base;
positive.coercions.insert(
(Name::from("object"), Name::from("string")),
CoercionSpec {
forward: Expr::Lit(Literal::Float(0.0)),
inverse: None,
class: CoercionClass::Retraction,
},
);
assert_ne!(canonical_bytes(&negative), canonical_bytes(&positive));
}
#[test]
fn an_empty_list_does_not_encode_as_an_absent_key() {
let base = fixture();
let mut empty_required = base.clone();
empty_required
.required
.insert(Name::from("leaf"), Vec::new());
assert_ne!(
canonical_bytes(&base),
canonical_bytes(&empty_required),
"an empty `required` list must not encode as an absent key"
);
let mut empty_constraints = base.clone();
empty_constraints
.constraints
.insert(Name::from("root"), Vec::new());
assert_ne!(
canonical_bytes(&base),
canonical_bytes(&empty_constraints),
"an empty `constraints` list must not encode as an absent key"
);
let mut empty_variants = base.clone();
empty_variants
.variants
.insert(Name::from("leaf"), Vec::new());
assert_ne!(canonical_bytes(&base), canonical_bytes(&empty_variants));
}
#[test]
fn constraint_order_is_part_of_the_identity() {
let field = |value: &str| Constraint {
sort: Name::from("field:op"),
value: value.to_owned(),
};
let mut plus_first = fixture();
plus_first
.constraints
.insert(Name::from("root"), vec![field("+"), field("-")]);
let mut minus_first = fixture();
minus_first
.constraints
.insert(Name::from("root"), vec![field("-"), field("+")]);
assert_eq!(plus_first.field_text("root", "op"), Some("+"));
assert_eq!(minus_first.field_text("root", "op"), Some("-"));
assert_ne!(
canonical_bytes(&plus_first),
canonical_bytes(&minus_first),
"two schemas that read back different field text must not share an encoding"
);
}
#[test]
fn required_edge_order_is_part_of_the_identity() {
let labelled = Edge {
src: Name::from("root"),
tgt: Name::from("leaf"),
kind: Name::from("prop"),
name: Some(Name::from("leaf")),
};
let bare = Edge {
name: None,
..labelled.clone()
};
let mut forward = fixture();
forward
.required
.insert(Name::from("root"), vec![labelled.clone(), bare.clone()]);
let mut backward = fixture();
backward
.required
.insert(Name::from("root"), vec![bare, labelled]);
assert_ne!(canonical_bytes(&forward), canonical_bytes(&backward));
}
#[test]
fn a_permuted_adjacency_bucket_is_visible() {
let protocol = protocol();
let build = |first: &str, second: &str| {
SchemaBuilder::new(&protocol)
.vertex("root", "object", None)
.expect("root")
.vertex("leaf", "string", None)
.expect("leaf")
.edge("root", "leaf", "prop", Some(first))
.expect("first")
.edge("root", "leaf", "prop", Some(second))
.expect("second")
.build()
.expect("build")
};
let ab = build("a", "b");
let ba = build("b", "a");
assert_eq!(ab.edges, ba.edges, "the two differ only in bucket order");
assert_ne!(canonical_bytes(&ab), canonical_bytes(&ba));
}
#[test]
fn the_digest_tracks_the_bytes() {
let base = fixture();
assert_eq!(canonical_digest(&base), canonical_digest(&fixture()));
let mut repointed = base.clone();
repointed.entries = vec![Name::from("leaf")];
assert_ne!(canonical_digest(&base), canonical_digest(&repointed));
assert_eq!(
canonical_digest(&base),
*blake3::hash(&canonical_bytes(&base)).as_bytes()
);
}
#[test]
fn induced_apices_that_agree_encode_alike() {
let protocol = protocol();
let schema = fixture();
let keep: rustc_hash::FxHashSet<Name> =
["root", "leaf"].into_iter().map(Name::from).collect();
let once = crate::induce::induce_on_vertices(&schema, &protocol, &keep).expect("once");
let twice = crate::induce::induce_on_vertices(&once, &protocol, &keep).expect("twice");
assert_eq!(canonical_bytes(&once), canonical_bytes(&twice));
}
}