use crate::ast::{DefaultValue, EnumBacking, PrimitiveType, SemanticType, SubByteType};
use crate::ir::{
CompiledSchema, DeprecatedInfo, Encoding, FieldEncoding, ResolvedAnnotations, ResolvedType,
TombstoneDef, TypeDef, TypeId, TypeRegistry,
};
pub fn canonical_form(compiled: &CompiledSchema) -> String {
let mut out = String::new();
out.push_str("namespace ");
out.push_str(&compiled.namespace.join("."));
if compiled.annotations != ResolvedAnnotations::default() {
out.push(' ');
let mut ann_buf = String::new();
emit_annotations(&mut ann_buf, &compiled.annotations);
out.push_str(ann_buf.trim_end());
}
for &type_id in &compiled.declarations {
out.push(' ');
emit_type_def(&mut out, type_id, &compiled.registry);
}
out
}
pub fn schema_hash(compiled: &CompiledSchema) -> [u8; 32] {
let form = canonical_form(compiled);
*blake3::hash(form.as_bytes()).as_bytes()
}
fn type_str(ty: &ResolvedType, registry: &TypeRegistry) -> String {
#[allow(unreachable_patterns)]
match ty {
ResolvedType::Primitive(p) => primitive_str(p).to_owned(),
ResolvedType::SubByte(s) => sub_byte_str(s),
ResolvedType::Semantic(s) => semantic_str(s).to_owned(),
ResolvedType::Named(id) => {
if let Some(def) = registry.get(*id) {
type_def_name(def).to_owned()
} else {
"<unresolved>".to_owned()
}
}
ResolvedType::Optional(inner) => format!("optional<{}>", type_str(inner, registry)),
ResolvedType::Array(inner) => format!("array<{}>", type_str(inner, registry)),
ResolvedType::FixedArray(inner, size) => {
format!("array<{}, {}>", type_str(inner, registry), size)
}
ResolvedType::Set(inner) => {
format!("set<{}>", type_str(inner, registry))
}
ResolvedType::Map(k, v) => {
format!("map<{}, {}>", type_str(k, registry), type_str(v, registry))
}
ResolvedType::Result(ok, err) => {
format!(
"result<{}, {}>",
type_str(ok, registry),
type_str(err, registry)
)
}
ResolvedType::Vec2(inner) => format!("vec2<{}>", type_str(inner, registry)),
ResolvedType::Vec3(inner) => format!("vec3<{}>", type_str(inner, registry)),
ResolvedType::Vec4(inner) => format!("vec4<{}>", type_str(inner, registry)),
ResolvedType::Quat(inner) => format!("quat<{}>", type_str(inner, registry)),
ResolvedType::Mat3(inner) => format!("mat3<{}>", type_str(inner, registry)),
ResolvedType::Mat4(inner) => format!("mat4<{}>", type_str(inner, registry)),
ResolvedType::BitsInline(names) => {
format!("bits {{ {} }}", names.join(", "))
}
_ => {
debug_assert!(false, "unknown ResolvedType variant in canonical form");
"<unknown>".to_owned()
}
}
}
fn primitive_str(p: &PrimitiveType) -> &'static str {
match p {
PrimitiveType::Bool => "bool",
PrimitiveType::U8 => "u8",
PrimitiveType::U16 => "u16",
PrimitiveType::U32 => "u32",
PrimitiveType::U64 => "u64",
PrimitiveType::I8 => "i8",
PrimitiveType::I16 => "i16",
PrimitiveType::I32 => "i32",
PrimitiveType::I64 => "i64",
PrimitiveType::F32 => "f32",
PrimitiveType::F64 => "f64",
PrimitiveType::Fixed32 => "fixed32",
PrimitiveType::Fixed64 => "fixed64",
PrimitiveType::Void => "void",
}
}
fn sub_byte_str(s: &SubByteType) -> String {
if s.signed {
format!("i{}", s.bits)
} else {
format!("u{}", s.bits)
}
}
fn semantic_str(s: &SemanticType) -> &'static str {
match s {
SemanticType::String => "string",
SemanticType::Bytes => "bytes",
SemanticType::Rgb => "rgb",
SemanticType::Uuid => "uuid",
SemanticType::Timestamp => "timestamp",
SemanticType::Hash => "hash",
}
}
fn type_def_name(def: &TypeDef) -> &str {
#[allow(unreachable_patterns)]
match def {
TypeDef::Message(d) => d.name.as_str(),
TypeDef::Enum(d) => d.name.as_str(),
TypeDef::Flags(d) => d.name.as_str(),
TypeDef::Union(d) => d.name.as_str(),
TypeDef::Newtype(d) => d.name.as_str(),
TypeDef::Config(d) => d.name.as_str(),
_ => {
"<unknown>"
}
}
}
fn emit_annotations(out: &mut String, ann: &ResolvedAnnotations) {
if let Some(dep) = &ann.deprecated {
emit_deprecated(out, dep);
}
for d in &ann.doc {
out.push_str("@doc(\"");
out.push_str(d.as_str());
out.push_str("\") ");
}
if ann.non_exhaustive {
out.push_str("@non_exhaustive ");
}
if let Some(rev) = ann.revision {
out.push_str(&format!("@revision({rev}) "));
}
if let Some(since) = &ann.since {
out.push_str(&format!("@since(\"{}\") ", since.as_str()));
}
if let Some(ver) = &ann.version {
out.push_str(&format!("@version(\"{}\") ", ver.as_str()));
}
}
fn emit_deprecated(out: &mut String, dep: &DeprecatedInfo) {
if let Some(since) = &dep.since {
out.push_str(&format!(
"@deprecated(reason: \"{}\", since: \"{}\") ",
dep.reason.as_str(),
since.as_str()
));
} else {
out.push_str(&format!(
"@deprecated(reason: \"{}\") ",
dep.reason.as_str()
));
}
}
fn emit_encoding(out: &mut String, enc: &FieldEncoding) {
emit_encoding_inner(out, &enc.encoding);
if let Some(limit) = enc.limit {
out.push_str(&format!("@limit({limit}) "));
}
}
fn emit_encoding_inner(out: &mut String, enc: &Encoding) {
#[allow(unreachable_patterns)]
match enc {
Encoding::Default => {}
Encoding::Varint => out.push_str("@varint "),
Encoding::ZigZag => out.push_str("@zigzag "),
Encoding::Delta(inner) => {
out.push_str("@delta ");
emit_encoding_inner(out, inner);
}
_ => {
debug_assert!(false, "unknown Encoding variant in canonical form");
}
}
}
fn emit_tombstones(out: &mut String, tombstones: &[TombstoneDef]) {
let mut sorted: Vec<&TombstoneDef> = tombstones.iter().collect();
sorted.sort_by_key(|t| t.ordinal);
for t in sorted {
if let Some(since) = &t.since {
out.push_str(&format!(
"@removed({}, \"{}\", since: \"{}\") ",
t.ordinal,
t.reason.as_str(),
since.as_str()
));
} else {
out.push_str(&format!(
"@removed({}, \"{}\") ",
t.ordinal,
t.reason.as_str()
));
}
}
}
fn emit_type_def(out: &mut String, type_id: TypeId, registry: &TypeRegistry) {
let Some(def) = registry.get(type_id) else {
return;
};
#[allow(unreachable_patterns)]
match def {
TypeDef::Message(msg) => {
let mut ann_buf = String::new();
emit_annotations(&mut ann_buf, &msg.annotations);
if !ann_buf.is_empty() {
out.push_str(ann_buf.trim_end());
out.push(' ');
}
out.push_str("message ");
out.push_str(msg.name.as_str());
out.push_str(" {");
let mut body = String::new();
let mut fields = msg.fields.clone();
fields.sort_by_key(|f| f.ordinal);
for field in &fields {
let mut f_ann = String::new();
emit_annotations(&mut f_ann, &field.annotations);
let type_s = type_str(&field.resolved_type, registry);
let mut enc_buf = String::new();
emit_encoding(&mut enc_buf, &field.encoding);
let mut field_str =
format!("{} @{} : {}", field.name.as_str(), field.ordinal, type_s);
if !enc_buf.is_empty() {
field_str.push(' ');
field_str.push_str(enc_buf.trim_end());
}
if !f_ann.is_empty() {
field_str = format!("{} {}", f_ann.trim_end(), field_str);
}
body.push_str(&field_str);
body.push(' ');
}
emit_tombstones(&mut body, &msg.tombstones);
let body_trimmed = body.trim_end();
if body_trimmed.is_empty() {
out.push('}');
} else {
out.push(' ');
out.push_str(body_trimmed);
out.push_str(" }");
}
}
TypeDef::Enum(enm) => {
let mut ann_buf = String::new();
emit_annotations(&mut ann_buf, &enm.annotations);
if !ann_buf.is_empty() {
out.push_str(ann_buf.trim_end());
out.push(' ');
}
out.push_str("enum ");
out.push_str(enm.name.as_str());
if let Some(backing) = &enm.backing {
let backing_str = match backing {
EnumBacking::U8 => "u8",
EnumBacking::U16 => "u16",
EnumBacking::U32 => "u32",
EnumBacking::U64 => "u64",
};
out.push_str(&format!(" : {}", backing_str));
}
out.push_str(" {");
let mut body = String::new();
let mut variants = enm.variants.clone();
variants.sort_by_key(|v| v.ordinal);
for v in &variants {
let mut v_ann = String::new();
emit_annotations(&mut v_ann, &v.annotations);
if !v_ann.is_empty() {
body.push_str(v_ann.trim_end());
body.push(' ');
}
body.push_str(&format!("{} = {} ", v.name.as_str(), v.ordinal));
}
emit_tombstones(&mut body, &enm.tombstones);
let body_trimmed = body.trim_end();
if body_trimmed.is_empty() {
out.push('}');
} else {
out.push(' ');
out.push_str(body_trimmed);
out.push_str(" }");
}
}
TypeDef::Flags(flags) => {
let mut ann_buf = String::new();
emit_annotations(&mut ann_buf, &flags.annotations);
if !ann_buf.is_empty() {
out.push_str(ann_buf.trim_end());
out.push(' ');
}
out.push_str("flags ");
out.push_str(flags.name.as_str());
out.push_str(" {");
let mut body = String::new();
let mut bits = flags.bits.clone();
bits.sort_by_key(|b| b.bit);
for b in &bits {
let mut b_ann = String::new();
emit_annotations(&mut b_ann, &b.annotations);
if !b_ann.is_empty() {
body.push_str(b_ann.trim_end());
body.push(' ');
}
body.push_str(&format!("{} = {} ", b.name.as_str(), b.bit));
}
emit_tombstones(&mut body, &flags.tombstones);
let body_trimmed = body.trim_end();
if body_trimmed.is_empty() {
out.push('}');
} else {
out.push(' ');
out.push_str(body_trimmed);
out.push_str(" }");
}
}
TypeDef::Union(u) => {
let mut ann_buf = String::new();
emit_annotations(&mut ann_buf, &u.annotations);
if !ann_buf.is_empty() {
out.push_str(ann_buf.trim_end());
out.push(' ');
}
out.push_str("union ");
out.push_str(u.name.as_str());
out.push_str(" {");
let mut body = String::new();
let mut variants = u.variants.clone();
variants.sort_by_key(|v| v.ordinal);
for var in &variants {
let mut v_ann = String::new();
emit_annotations(&mut v_ann, &var.annotations);
if !v_ann.is_empty() {
body.push_str(v_ann.trim_end());
body.push(' ');
}
body.push_str(&format!("{} @{} {{", var.name.as_str(), var.ordinal));
let mut var_body = String::new();
let mut fields = var.fields.clone();
fields.sort_by_key(|f| f.ordinal);
for field in &fields {
let mut f_ann = String::new();
emit_annotations(&mut f_ann, &field.annotations);
let type_s = type_str(&field.resolved_type, registry);
let mut enc_buf = String::new();
emit_encoding(&mut enc_buf, &field.encoding);
let mut field_str =
format!("{} @{} : {}", field.name.as_str(), field.ordinal, type_s);
if !enc_buf.is_empty() {
field_str.push(' ');
field_str.push_str(enc_buf.trim_end());
}
if !f_ann.is_empty() {
field_str = format!("{} {}", f_ann.trim_end(), field_str);
}
var_body.push_str(&field_str);
var_body.push(' ');
}
emit_tombstones(&mut var_body, &var.tombstones);
let var_body_trimmed = var_body.trim_end();
if var_body_trimmed.is_empty() {
body.push('}');
} else {
body.push(' ');
body.push_str(var_body_trimmed);
body.push_str(" }");
}
body.push(' ');
}
emit_tombstones(&mut body, &u.tombstones);
let body_trimmed = body.trim_end();
if body_trimmed.is_empty() {
out.push('}');
} else {
out.push(' ');
out.push_str(body_trimmed);
out.push_str(" }");
}
}
TypeDef::Newtype(nt) => {
let mut ann_buf = String::new();
emit_annotations(&mut ann_buf, &nt.annotations);
if !ann_buf.is_empty() {
out.push_str(ann_buf.trim_end());
out.push(' ');
}
let type_s = type_str(&nt.inner_type, registry);
out.push_str(&format!("newtype {} = {}", nt.name.as_str(), type_s));
}
TypeDef::Config(cfg) => {
let mut ann_buf = String::new();
emit_annotations(&mut ann_buf, &cfg.annotations);
if !ann_buf.is_empty() {
out.push_str(ann_buf.trim_end());
out.push(' ');
}
out.push_str("config ");
out.push_str(cfg.name.as_str());
out.push_str(" {");
let mut body = String::new();
let mut fields = cfg.fields.clone();
fields.sort_by(|a, b| a.name.cmp(&b.name));
for field in &fields {
let mut f_ann = String::new();
emit_annotations(&mut f_ann, &field.annotations);
if !f_ann.is_empty() {
body.push_str(f_ann.trim_end());
body.push(' ');
}
let type_s = type_str(&field.resolved_type, registry);
body.push_str(&format!("{} : {}", field.name.as_str(), type_s));
body.push_str(" = ");
emit_default_value(&mut body, &field.default_value);
body.push(' ');
}
let body_trimmed = body.trim_end();
if body_trimmed.is_empty() {
out.push('}');
} else {
out.push(' ');
out.push_str(body_trimmed);
out.push_str(" }");
}
}
_ => {
}
}
}
fn emit_default_value(out: &mut String, val: &DefaultValue) {
match val {
DefaultValue::None => out.push_str("none"),
DefaultValue::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
DefaultValue::Int(n) => out.push_str(&format!("{n}")),
DefaultValue::UInt(n) => out.push_str(&format!("{n}")),
DefaultValue::Float(f) => out.push_str(&format!("{f:?}")),
DefaultValue::Str(s) => out.push_str(&format!("\"{s}\"")),
DefaultValue::Ident(s) => out.push_str(s.as_str()),
DefaultValue::UpperIdent(s) => out.push_str(s.as_str()),
DefaultValue::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
emit_default_value(out, &item.node);
}
out.push(']');
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{PrimitiveType, SemanticType, SubByteType};
fn dummy_registry() -> crate::ir::TypeRegistry {
crate::ir::TypeRegistry::new()
}
#[test]
fn minimal_namespace_only() {
let result = crate::compile("namespace test.minimal\nmessage Empty {}");
let compiled = result.compiled.unwrap();
let form = canonical_form(&compiled);
assert!(form.starts_with("namespace test.minimal"));
let hash = schema_hash(&compiled);
assert_eq!(hash.len(), 32);
}
#[test]
fn type_string_primitives() {
assert_eq!(
type_str(
&ResolvedType::Primitive(PrimitiveType::Bool),
&dummy_registry()
),
"bool"
);
assert_eq!(
type_str(
&ResolvedType::Primitive(PrimitiveType::U32),
&dummy_registry()
),
"u32"
);
assert_eq!(
type_str(
&ResolvedType::Primitive(PrimitiveType::I64),
&dummy_registry()
),
"i64"
);
assert_eq!(
type_str(
&ResolvedType::Primitive(PrimitiveType::F64),
&dummy_registry()
),
"f64"
);
assert_eq!(
type_str(
&ResolvedType::Primitive(PrimitiveType::Void),
&dummy_registry()
),
"void"
);
}
#[test]
fn type_string_sub_byte() {
assert_eq!(
type_str(
&ResolvedType::SubByte(SubByteType {
bits: 3,
signed: false
}),
&dummy_registry()
),
"u3"
);
assert_eq!(
type_str(
&ResolvedType::SubByte(SubByteType {
bits: 5,
signed: true
}),
&dummy_registry()
),
"i5"
);
}
#[test]
fn type_string_semantic() {
assert_eq!(
type_str(
&ResolvedType::Semantic(SemanticType::String),
&dummy_registry()
),
"string"
);
assert_eq!(
type_str(
&ResolvedType::Semantic(SemanticType::Uuid),
&dummy_registry()
),
"uuid"
);
assert_eq!(
type_str(
&ResolvedType::Semantic(SemanticType::Timestamp),
&dummy_registry()
),
"timestamp"
);
}
#[test]
fn type_string_parameterized() {
let inner = ResolvedType::Primitive(PrimitiveType::U32);
assert_eq!(
type_str(
&ResolvedType::Optional(Box::new(inner.clone())),
&dummy_registry()
),
"optional<u32>"
);
assert_eq!(
type_str(
&ResolvedType::Array(Box::new(inner.clone())),
&dummy_registry()
),
"array<u32>"
);
let key = ResolvedType::Semantic(SemanticType::String);
assert_eq!(
type_str(
&ResolvedType::Map(Box::new(key), Box::new(inner.clone())),
&dummy_registry()
),
"map<string, u32>"
);
let ok = ResolvedType::Primitive(PrimitiveType::U32);
let err = ResolvedType::Semantic(SemanticType::String);
assert_eq!(
type_str(
&ResolvedType::Result(Box::new(ok), Box::new(err)),
&dummy_registry()
),
"result<u32, string>"
);
}
#[test]
fn annotation_sorting() {
let result = crate::compile(
r#"
@version("2.0.0")
namespace test.anno
@since("1.0") @doc("hello") @deprecated(reason: "old") @revision(3)
@non_exhaustive
enum Foo { A @0 }
"#,
);
let compiled = result.compiled.unwrap();
let form = canonical_form(&compiled);
assert!(
form.contains("namespace test.anno @version(\"2.0.0\")"),
"form was: {form}"
);
assert!(
form.contains(
"@deprecated(reason: \"old\") @doc(\"hello\") @non_exhaustive @revision(3) @since(\"1.0\") enum Foo"
),
"form was: {form}"
);
}
#[test]
fn encoding_annotations() {
let result = crate::compile(
r#"
namespace test.enc
message Enc {
a @0 : u32 @varint
b @1 : i32 @zigzag
c @2 : u64 @delta
d @3 : u32 @delta @varint
e @4 : string @limit(100)
}
"#,
);
let compiled = result.compiled.unwrap();
let form = canonical_form(&compiled);
assert!(form.contains("a @0 : u32 @varint"), "form was: {form}");
assert!(form.contains("b @1 : i32 @zigzag"), "form was: {form}");
assert!(form.contains("c @2 : u64 @delta"), "form was: {form}");
assert!(
form.contains("d @3 : u32 @delta @varint"),
"form was: {form}"
);
assert!(
form.contains("e @4 : string @limit(100)"),
"form was: {form}"
);
}
#[test]
fn canonical_message() {
let result = crate::compile("namespace t.m\nmessage Foo { x @0 : u32 y @1 : string }");
let form = canonical_form(&result.compiled.unwrap());
assert!(
form.contains("message Foo { x @0 : u32 y @1 : string }"),
"form was: {form}"
);
}
#[test]
fn canonical_enum() {
let result = crate::compile("namespace t.e\nenum Color { Red @0 Green @1 Blue @2 }");
let form = canonical_form(&result.compiled.unwrap());
assert!(
form.contains("enum Color { Red = 0 Green = 1 Blue = 2 }"),
"form was: {form}"
);
}
#[test]
fn canonical_enum_with_backing() {
let result = crate::compile("namespace t.e\nenum Small : u8 { A @0 B @1 }");
let form = canonical_form(&result.compiled.unwrap());
assert!(
form.contains("enum Small : u8 { A = 0 B = 1 }"),
"form was: {form}"
);
}
#[test]
fn canonical_flags() {
let result = crate::compile("namespace t.f\nflags Perms { Read @0 Write @1 Exec @2 }");
let form = canonical_form(&result.compiled.unwrap());
assert!(
form.contains("flags Perms { Read = 0 Write = 1 Exec = 2 }"),
"form was: {form}"
);
}
#[test]
fn canonical_union() {
let result = crate::compile("namespace t.u\nunion Shape { Circle @0 { radius @0 : f32 } Rect @1 { w @0 : f32 h @1 : f32 } }");
let form = canonical_form(&result.compiled.unwrap());
assert!(
form.contains(
"union Shape { Circle @0 { radius @0 : f32 } Rect @1 { w @0 : f32 h @1 : f32 } }"
),
"form was: {form}"
);
}
#[test]
fn canonical_newtype() {
let result = crate::compile("namespace t.n\nnewtype UserId : u64");
let form = canonical_form(&result.compiled.unwrap());
assert!(form.contains("newtype UserId = u64"), "form was: {form}");
}
#[test]
fn canonical_config() {
let result = crate::compile(
"namespace t.c\nconfig Defaults { timeout : u32 = 30 name : string = \"hello\" }",
);
let form = canonical_form(&result.compiled.unwrap());
assert!(
form.contains("config Defaults { name : string = \"hello\" timeout : u32 = 30 }"),
"form was: {form}"
);
}
#[test]
fn canonical_tombstones() {
let result = crate::compile(
r#"
namespace t.t
message Evolving {
name @0 : string
@removed(1, reason: "replaced by full_name")
@removed(2, reason: "no longer needed", since: "2.0")
}
"#,
);
let form = canonical_form(&result.compiled.unwrap());
assert!(form.contains("name @0 : string @removed(1, \"replaced by full_name\") @removed(2, \"no longer needed\", since: \"2.0\")"), "form was: {form}");
}
#[test]
fn tombstone_original_type_is_canonical_hash_inert() {
let untyped = r#"
namespace t.tombstone_hash
message Evolving {
live @0 : u32
@removed(1, reason: "historical")
next @2 : u64
}
"#;
let typed_u32 = r#"
namespace t.tombstone_hash
message Evolving {
live @0 : u32
@removed(1, reason: "historical") : u32
next @2 : u64
}
"#;
let typed_string = r#"
namespace t.tombstone_hash
message Evolving {
live @0 : u32
@removed(1, reason: "historical") : string
next @2 : u64
}
"#;
let untyped = crate::compile(untyped).compiled.expect("untyped schema");
let typed_u32 = crate::compile(typed_u32)
.compiled
.expect("typed u32 schema");
let typed_string = crate::compile(typed_string)
.compiled
.expect("typed string schema");
assert_eq!(canonical_form(&untyped), canonical_form(&typed_u32));
assert_eq!(canonical_form(&untyped), canonical_form(&typed_string));
assert_eq!(schema_hash(&untyped), schema_hash(&typed_u32));
assert_eq!(schema_hash(&untyped), schema_hash(&typed_string));
}
#[test]
fn whitespace_invariance() {
let compact = "namespace t.w\nmessage Foo { x @0 : u32 }";
let spacey =
" namespace t.w \n\n# comment\n message Foo { \n x @0 : u32 \n } \n";
let h1 = schema_hash(&crate::compile(compact).compiled.unwrap());
let h2 = schema_hash(&crate::compile(spacey).compiled.unwrap());
assert_eq!(h1, h2, "whitespace/comments should not affect hash");
}
#[test]
fn field_order_invariance() {
let ordered = "namespace t.o\nmessage M { a @0 : u32 b @1 : string }";
let reversed = "namespace t.o\nmessage M { b @1 : string a @0 : u32 }";
let h1 = schema_hash(&crate::compile(ordered).compiled.unwrap());
let h2 = schema_hash(&crate::compile(reversed).compiled.unwrap());
assert_eq!(h1, h2, "field ordering in source should not affect hash");
}
#[test]
fn trait_function_body_is_wire_hash_inert() {
let old = r#"
namespace test.trait_hash
trait Adjustable { fn adjust(delta: i32) -> i32 }
message Counter { value @0 : i32 }
impl Adjustable for Counter {
fn adjust(delta: i32) -> i32 { return delta }
}
"#;
let new = r#"
namespace test.trait_hash
trait Adjustable { fn adjust(delta: i32) -> i32 }
message Counter { value @0 : i32 }
impl Adjustable for Counter {
fn adjust(delta: i32) -> i32 { return delta + 1 }
}
"#;
let old_hash = schema_hash(&crate::compile(old).compiled.unwrap());
let new_hash = schema_hash(&crate::compile(new).compiled.unwrap());
assert_eq!(old_hash, new_hash);
}
#[test]
fn trait_field_tags_are_wire_hash_inert() {
let first = r#"
namespace test.trait_tag_hash
trait Tagged {
value @0 : i32
label @1 : string
}
message Item { value @0 : i32 label @1 : string }
impl Tagged for Item { }
"#;
let retagged = r#"
namespace test.trait_tag_hash
trait Tagged {
value @99 : i32
label @99 : string
}
message Item { value @0 : i32 label @1 : string }
impl Tagged for Item { }
"#;
let first_hash = schema_hash(&crate::compile(first).compiled.unwrap());
let retagged_hash = schema_hash(&crate::compile(retagged).compiled.unwrap());
assert_eq!(first_hash, retagged_hash);
}
fn corpus_path(name: &str) -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("corpus/valid")
.join(format!("{name}.vexil"))
}
#[test]
fn corpus_hash_006_message() {
let src = std::fs::read_to_string(corpus_path("006_message")).unwrap();
let result = crate::compile(&src);
let compiled = result.compiled.unwrap();
let hash = schema_hash(&compiled);
assert_eq!(
hash,
[
188, 96, 181, 233, 102, 36, 0, 18, 211, 8, 236, 203, 192, 3, 12, 59, 196, 242, 126,
178, 201, 65, 246, 11, 56, 144, 145, 254, 211, 197, 41, 97
]
);
}
#[test]
fn corpus_hash_007_enum() {
let src = std::fs::read_to_string(corpus_path("007_enum")).unwrap();
let result = crate::compile(&src);
let compiled = result.compiled.unwrap();
let hash = schema_hash(&compiled);
assert_eq!(
hash,
[
138, 197, 37, 206, 10, 236, 186, 122, 241, 40, 175, 48, 188, 33, 67, 224, 250, 89,
27, 74, 98, 246, 215, 25, 144, 116, 233, 202, 132, 158, 52, 104
]
);
}
#[test]
fn corpus_hash_008_flags() {
let src = std::fs::read_to_string(corpus_path("008_flags")).unwrap();
let result = crate::compile(&src);
let compiled = result.compiled.unwrap();
let hash = schema_hash(&compiled);
assert_eq!(
hash,
[
144, 166, 172, 6, 197, 254, 133, 114, 181, 46, 38, 139, 229, 239, 207, 217, 64,
152, 185, 41, 110, 171, 102, 229, 46, 76, 68, 150, 49, 130, 90, 163
]
);
}
#[test]
fn corpus_hash_009_union() {
let src = std::fs::read_to_string(corpus_path("009_union")).unwrap();
let result = crate::compile(&src);
let compiled = result.compiled.unwrap();
let hash = schema_hash(&compiled);
assert_eq!(
hash,
[
47, 99, 143, 62, 23, 143, 80, 156, 143, 123, 251, 109, 56, 168, 86, 143, 110, 140,
201, 173, 227, 240, 165, 6, 154, 92, 157, 151, 151, 113, 167, 185
]
);
}
#[test]
fn corpus_hash_010_newtype() {
let src = std::fs::read_to_string(corpus_path("010_newtype")).unwrap();
let result = crate::compile(&src);
let compiled = result.compiled.unwrap();
let hash = schema_hash(&compiled);
assert_eq!(
hash,
[
193, 84, 236, 76, 195, 197, 171, 37, 118, 187, 158, 139, 23, 234, 116, 209, 31,
129, 94, 68, 24, 8, 62, 62, 167, 233, 122, 197, 220, 13, 188, 23
]
);
}
#[test]
fn corpus_hash_011_config() {
let src = std::fs::read_to_string(corpus_path("011_config")).unwrap();
let result = crate::compile(&src);
let compiled = result.compiled.unwrap();
let hash = schema_hash(&compiled);
assert_eq!(
hash,
[
184, 20, 231, 186, 60, 172, 191, 182, 164, 184, 226, 177, 56, 55, 239, 169, 103,
131, 129, 177, 111, 104, 58, 248, 192, 117, 48, 121, 43, 139, 124, 110
]
);
}
#[test]
fn corpus_hash_013_annotations() {
let src = std::fs::read_to_string(corpus_path("013_annotations")).unwrap();
let result = crate::compile(&src);
let compiled = result.compiled.unwrap();
let hash = schema_hash(&compiled);
assert_eq!(
hash,
[
121, 17, 205, 68, 4, 109, 16, 122, 241, 241, 130, 155, 123, 114, 123, 54, 28, 116,
191, 8, 35, 183, 125, 236, 73, 24, 233, 212, 224, 179, 5, 103
]
);
}
#[test]
fn corpus_hash_016_recursive() {
let src = std::fs::read_to_string(corpus_path("016_recursive")).unwrap();
let result = crate::compile(&src);
let compiled = result.compiled.unwrap();
let hash = schema_hash(&compiled);
assert_eq!(
hash,
[
161, 37, 203, 80, 41, 62, 163, 37, 96, 115, 8, 66, 82, 197, 202, 240, 131, 245,
221, 247, 48, 24, 248, 81, 142, 84, 169, 253, 190, 235, 145, 174
]
);
}
#[test]
fn corpus_hash_049_trait_function_portable_body() {
let src = std::fs::read_to_string(corpus_path("049_trait_function_portable_body")).unwrap();
let compiled = crate::compile(&src).compiled.unwrap();
assert_eq!(
schema_hash(&compiled),
[
0xf0, 0x55, 0x8d, 0xce, 0xf6, 0x4f, 0x18, 0xb1, 0x08, 0xa4, 0x01, 0xc7, 0xba, 0xd5,
0x29, 0x8d, 0x06, 0x0b, 0xe3, 0x08, 0x48, 0xa3, 0xac, 0xab, 0x0c, 0xcc, 0x34, 0xfe,
0xc7, 0x42, 0xef, 0xb2,
]
);
}
}