Pack uses Merkle-tree hashing to enable O(1) interface compatibility checking. Every type, function, and interface has a content-addressed hash computed from its structure. If two hashes match, the interfaces are compatible.
Actor A Actor B
│ │
│ import-hashes: │ export-hashes:
│ "math/ops" → a1b2c3d4... │ "math/ops" → a1b2c3d4...
│ │
└──────────── hashes match ──────────┘
✓ compatible!
Type names are NOT part of the hash. Two types with the same structure have the same hash, regardless of what they're called:
record Point { x: s32, y: s32 } // hash: abc123...
record Vec2 { x: s32, y: s32 } // hash: abc123... (same!)This enables structural compatibility: if you expect a Point and receive a Vec2 with the same fields, they're compatible.
Field and case names are part of the hash, because they affect how data is accessed:
record Point { x: s32, y: s32 } // hash: abc123...
record Point { a: s32, b: s32 } // hash: def456... (different!)You access .x vs .a - that's semantically meaningful.
While types are structural, interfaces include their binding names:
interface A {
type point = { x: s32, y: s32 }
translate: func(p: point) -> point
}
interface B {
type vec2 = { x: s32, y: s32 }
translate: func(p: vec2) -> vec2
}These have different interface hashes because the bindings differ (point vs vec2), even though the underlying type structure is the same.
Parameter names are documentation, not semantics:
func add(a: s32, b: s32) -> s32 // hash: xyz789...
func add(x: s32, y: s32) -> s32 // hash: xyz789... (same!)What matters is the types, not what you call the parameters.
Primitives have fixed, well-known hashes:
| Type | Hash (prefix) |
|---|---|
| bool | 0x0001... |
| u8 | 0x0002... |
| u16 | 0x0003... |
| ... | ... |
| string | 0x000d... |
Compound types hash their components:
hash(list<T>) = sha256(TAG_LIST || hash(T))
hash(option<T>) = sha256(TAG_OPTION || hash(T))
hash(result<T, E>) = sha256(TAG_RESULT || hash(T) || hash(E))
hash(tuple<T1, T2>) = sha256(TAG_TUPLE || count || hash(T1) || hash(T2))
Records and variants hash their fields/cases in sorted order (by name) for canonical ordering:
hash(record { y: s32, x: s32 })
= sha256(TAG_RECORD || 2 || "x" || hash(s32) || "y" || hash(s32))
// Same as hash(record { x: s32, y: s32 }) - order in source doesn't matter
Type names are NOT included:
hash(record Point { x: s32 }) == hash(record Vec2 { x: s32 })
Functions hash parameter types and result types (names excluded):
hash(func add(a: s32, b: s32) -> s32)
= sha256(TAG_FUNCTION || 2 || hash(s32) || hash(s32) || 1 || hash(s32))
^params ^results
Interfaces hash their name plus sorted bindings:
hash(interface Math { add: func..., mul: func... })
= sha256(TAG_INTERFACE || "Math"
|| type_bindings... // sorted by name
|| func_bindings...) // sorted by name
Bindings are (name, hash) pairs, so the binding names ARE part of the interface hash.
Recursive types use a self-reference placeholder:
variant sexpr {
sym(string),
lst(list<sexpr>), // recursive!
}The hash uses HASH_SELF_REF for the recursive reference:
hash(sexpr) = sha256(TAG_VARIANT || 2
|| "lst" || hash(list<SELF_REF>)
|| "sym" || hash(string))
This produces a stable hash even for recursive structures.
Package metadata includes interface hashes:
record package-metadata {
imports: list<function-sig>,
exports: list<function-sig>,
import-hashes: list<interface-hash>, // NEW
export-hashes: list<interface-hash>, // NEW
}
record interface-hash {
name: string, // e.g., "theater:simple/runtime"
hash: tuple<u64, u64, u64, u64>, // SHA-256 as 4 u64s
}
- Parse wit+ interface definitions
- Compute hash for each type, function, interface
- Embed hashes in package metadata
- Load package, call
__pack_typesto get metadata - Decode
import-hashesandexport-hashes - For each import, check if provider's export hash matches
- Mismatch → error with details
let metadata = decode_metadata_with_hashes(&bytes)?;
for import in &metadata.import_hashes {
let provider_hash = get_provider_export_hash(&import.name)?;
if import.hash != provider_hash {
return Err(InterfaceMismatch {
interface: import.name.clone(),
expected: import.hash,
got: provider_hash,
});
}
}use pack_abi::{
TypeHash,
hash_list, hash_option, hash_result, hash_tuple,
hash_record, hash_variant, hash_function, hash_interface,
Binding,
};
// Compute a record hash
let point_hash = hash_record(&[
("x", HASH_S32),
("y", HASH_S32),
]);
// Compute an interface hash
let math_hash = hash_interface(
"math/ops",
&[], // type bindings
&[
Binding { name: "add", hash: add_func_hash },
Binding { name: "mul", hash: mul_func_hash },
],
);use pack::{decode_metadata_with_hashes, MetadataWithHashes, InterfaceHash};
let metadata: MetadataWithHashes = decode_metadata_with_hashes(&bytes)?;
// Check import hashes
for import in &metadata.import_hashes {
println!("Import: {} -> {}", import.name, import.hash);
}
// Check export hashes
for export in &metadata.export_hashes {
println!("Export: {} -> {}", export.name, export.hash);
}- O(1) Compatibility Check: Hash comparison instead of structural traversal
- Precise Diffs: Merkle tree structure shows exactly where incompatibility lies
- Structural Sharing: Same type structure = same hash, regardless of name
- Content Addressing: Types become cacheable, distributable by hash
- Versioning: Same interface name + different hash = different version
- Hash-based type registry: Global cache of type definitions by hash
- Distributed type checking: Verify compatibility across network boundaries
- Lazy type resolution: Send hash first, fetch definition only if needed
- Interface evolution rules: Define when hash changes are backwards-compatible