Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions core/src/schema/ir/frozen/cas/blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ use crate::package::build::cas::storage::Hash;
use crate::schema::ir::frozen::unit::FrozenUnit;
use eyre::{eyre, Result};

/// A canonical, cross-language-stable digest of one schema's frozen units:
/// BLAKE3 over their `bincode` encoding — the same serialization the CAS uses
/// for blobs (see [`frozen_unit_to_blob`]) — folded to 64 bits for the
/// connection handshake's `ir_hash`.
///
/// Deterministic for a given frozen IR: independent of the generator, the host,
/// and whether the units were read from the working tree or a committed
/// version. Two ends of a connection generated from the same schema — in any
/// target language — get the same value, so a generator embeds this verbatim
/// rather than hashing its own (language-specific) rendering.
pub fn schema_ir_hash(units: &[FrozenUnit]) -> u64 {
let bytes = bincode::serialize(units)
.expect("FrozenUnit implements Serialize; in-memory encoding cannot fail");
let digest = blake3::hash(&bytes);
u64::from_le_bytes(
digest.as_bytes()[..8]
.try_into()
.expect("a BLAKE3 digest is 32 bytes"),
)
}

/// Convert a FrozenUnit to a Blob
pub fn frozen_unit_to_blob(unit: &FrozenUnit) -> Result<Blob> {
// Serialize FrozenUnit using bincode
Expand Down
3 changes: 3 additions & 0 deletions core/src/schema/ir/frozen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
pub mod unit;
// Removed: basic_storage - replaced by CAS
pub mod cas; // CAS module (public for tests and build)

/// Canonical frozen-schema digest for the connection handshake's `ir_hash`.
pub use cas::blob::schema_ir_hash;
1 change: 1 addition & 0 deletions core/tests/cas/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ pub mod commit_tests;
pub mod config_in_commit_tests;
pub mod refs_tests;
pub mod span_hash_sensitivity_tests;
pub mod schema_ir_hash_tests;
45 changes: 45 additions & 0 deletions core/tests/cas/schema_ir_hash_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// `schema_ir_hash` — the canonical frozen-schema digest a generator embeds as
// the connection handshake's `IR_HASH`. It must be deterministic for a given
// frozen IR and move when the schema's meaning (or, like the CAS blob hash,
// its formatting) changes.

use comline_core::schema::ir::compiler::interpreter::incremental::IncrementalInterpreter;
use comline_core::schema::ir::compiler::Compile;
use comline_core::schema::ir::frozen::schema_ir_hash;

#[test]
fn identical_source_hashes_identically() {
let source = "struct User {\n id: u64\n name: str\n}\n";
let a = IncrementalInterpreter::from_source(source);
let b = IncrementalInterpreter::from_source(source);

assert_eq!(schema_ir_hash(&a), schema_ir_hash(&b));
}

#[test]
fn a_semantic_change_moves_the_hash() {
let before = IncrementalInterpreter::from_source("struct User {\n id: u64\n}\n");
let after = IncrementalInterpreter::from_source("struct User {\n id: u64\n name: str\n}\n");

assert_ne!(schema_ir_hash(&before), schema_ir_hash(&after));
}

#[test]
fn unit_order_is_significant() {
let ab = IncrementalInterpreter::from_source(
"struct A {\n x: u64\n}\nstruct B {\n y: u64\n}\n",
);
let ba = IncrementalInterpreter::from_source(
"struct B {\n y: u64\n}\nstruct A {\n x: u64\n}\n",
);

// Different declaration order ⇒ different frozen IR ⇒ different identity.
assert_ne!(schema_ir_hash(&ab), schema_ir_hash(&ba));
}

#[test]
fn an_empty_schema_still_hashes() {
let empty: Vec<comline_core::schema::ir::frozen::unit::FrozenUnit> = Vec::new();
// Just needs to not panic and be stable.
assert_eq!(schema_ir_hash(&empty), schema_ir_hash(&empty));
}
Loading