Section 03: Hash-Forwarded Signatures
can identify types by hash without needing access to the originating Pool. This enables O(1) type lookup at import boundaries (Section 04) and pool-independent type comparison (Section 06).
Why this matters: Currently, FunctionSig.param_types and FunctionSig.return_type are
Vec<Idx> and Idx — pool-local handles. When Module B receives A’s FunctionSig via the
Salsa typed() query, these Idx values are meaningless in B’s pool. The type checker must
re-derive types from the AST. With embedded Merkle hashes, B can do a direct O(1) lookup:
intern_map.get(&hash) → local Idx, skipping the AST walk entirely.
This section depends on Section 01 (Merkle hash computation) and Section 02 (stability tests).
03.1 FunctionSig Hash Fields — COMPLETE
during signature inference.
File: compiler/ori_types/src/output/mod.rs (lines 244-442)
Current FunctionSig:
pub struct FunctionSig {
pub name: Name,
pub type_params: Vec<Name>,
pub const_params: Vec<ConstParamInfo>,
pub param_names: Vec<Name>,
pub param_types: Vec<Idx>, // ← pool-local
pub return_type: Idx, // ← pool-local
pub capabilities: Vec<Name>,
pub is_public: bool,
pub is_test: bool,
pub is_main: bool,
pub is_fbip: bool,
pub type_param_bounds: Vec<Vec<Name>>,
pub where_clauses: Vec<FnWhereClause>,
pub generic_param_mapping: Vec<Option<usize>>,
pub scheme_var_ids: Vec<u32>,
pub required_params: usize,
pub param_defaults: Vec<Option<ExprId>>,
}
New fields:
pub struct FunctionSig {
// ... existing fields unchanged ...
/// Merkle hashes for parameter types — stable across Pool instances.
///
/// `param_hashes[i]` is the content-addressed hash of `param_types[i]`.
/// Used for cross-module type identity: receiving modules can look up
/// types by hash in their own `intern_map` without AST re-walking.
/// Always `param_hashes.len() == param_types.len()`.
pub param_hashes: Vec<u64>,
/// Merkle hash for the return type — stable across Pool instances.
pub return_hash: u64,
}
Invariants:
param_hashes.len() == param_types.len()— always in syncparam_hashes[i] == pool.hash(param_types[i])— hash matches Idxreturn_hash == pool.hash(return_type)— hash matches Idx
Impact on derived traits: FunctionSig derives Clone, Eq, PartialEq, Hash, Debug.
Adding Vec<u64> and u64 fields preserves all derives — u64 and Vec<u64> implement
all required traits. No Salsa compatibility issues.
Impact on existing code: Every place that constructs a FunctionSig must now populate
the hash fields. Search for FunctionSig { in the codebase to find all construction sites.
Known sites:
check/signatures/mod.rs—infer_function_signature_with_arena()(primary)- Any test code that constructs FunctionSig directly
Default for backward compatibility during migration:
impl FunctionSig {
/// Create hash fields from a Pool. Call after param_types/return_type are set.
pub fn populate_hashes(&mut self, pool: &Pool) {
self.param_hashes = self.param_types.iter()
.map(|&idx| pool.hash(idx))
.collect();
self.return_hash = pool.hash(self.return_type);
}
}
Exit Criteria:
-
param_hashesandreturn_hashfields added toFunctionSig(2026-02-26) - All
FunctionSigconstruction sites updated to populate hashes (2026-02-26) -
populate_hashes()helper available for cases where pool is available (2026-02-26) -
FunctionSigstill derivesClone, Eq, PartialEq, Hash, Debug(2026-02-26) -
cargo csucceeds for all crates (2026-02-26)
03.2 TypeEntry Hash Fields — COMPLETE
cross-module type identity of structs, enums, aliases, and newtypes.
File: compiler/ori_types/src/registry/types/mod.rs
Current TypeEntry structure: (investigate exact fields — may contain Idx values for
field types, variant types, etc.)
New fields:
pub struct TypeEntry {
// ... existing fields ...
/// Merkle hash of the type's Pool representation.
/// Stable across Pool instances for cross-module identity.
pub merkle_hash: u64,
}
Why: When Module B imports a struct type from Module A, B needs to verify it’s the same
struct (same name, same field types). With merkle_hash, this is a single u64 comparison
instead of field-by-field structural comparison.
Exit Criteria:
-
merkle_hashfield added toTypeEntry(2026-02-26) - All
TypeEntryconstruction sites populate the hash (2026-02-26) - Hash derived from the Pool’s Merkle hash for the corresponding Idx (2026-02-26)
-
cargo csucceeds (2026-02-26)
03.3 Hash Population During Type Checking — COMPLETE
type checking, ensuring hashes are always consistent with Idx values.
File: compiler/ori_types/src/check/signatures/mod.rs (lines 132-274)
Current flow in infer_function_signature_with_arena():
- Resolve parameter types →
Vec<Idx>in local pool - Resolve return type →
Idxin local pool - Construct
FunctionSig { param_types, return_type, ... }
New flow:
- Resolve parameter types →
Vec<Idx>in local pool - Resolve return type →
Idxin local pool - Compute param hashes:
param_types.iter().map(|&idx| pool.hash(idx)).collect() - Compute return hash:
pool.hash(return_type) - Construct
FunctionSig { param_types, return_type, param_hashes, return_hash, ... }
Key constraint: The pool must be accessible at construction time. In
infer_function_signature_with_arena(), the checker owns the pool via self.pool.
After resolving all types, calling self.pool.hash(idx) for each type is safe
because all types are already interned.
Also update register_imported_function() (check/mod.rs:420-438):
After constructing the FunctionSig, call sig.populate_hashes(&self.pool) if
hashes weren’t already set during signature inference.
Test:
#[test]
fn function_sig_hashes_match_pool() {
let mut pool = Pool::new();
let param_types = vec![Idx::INT, pool.list(Idx::STR)];
let return_type = pool.option(Idx::BOOL);
let sig = FunctionSig {
param_types: param_types.clone(),
return_type,
param_hashes: param_types.iter().map(|&idx| pool.hash(idx)).collect(),
return_hash: pool.hash(return_type),
// ... other fields ...
};
assert_eq!(sig.param_hashes.len(), sig.param_types.len());
for (i, (&idx, &hash)) in sig.param_types.iter().zip(&sig.param_hashes).enumerate() {
assert_eq!(pool.hash(idx), hash,
"param[{i}] hash mismatch");
}
assert_eq!(pool.hash(sig.return_type), sig.return_hash);
}
Exit Criteria:
- Hash population integrated into signature inference pipeline (2026-02-26)
- Hashes always computed from the same pool that holds the Idx values (2026-02-26)
-
populate_hashes()available as fallback for code paths that construct sigs differently (2026-02-26) - Test verifying hash/Idx consistency (2026-02-26)
03.4 Salsa Compatibility Verification — COMPLETE
behavior.
Concerns:
-
TypeCheckResultequality: Salsa usesEqto detect when a query’s output changed. Adding hash fields changesEq— but this is correct: if hashes change, the types changed. If hashes don’t change, the types didn’t change. Hash equality is a stricter check than what we had before (Idx equality, which could give false positives across builds if Idx assignment order changed). -
TypeCheckResultHash trait: Salsa usesHashfor memoization keys. Addingu64fields to theHashderivation is fine — it doesn’t change the semantics, just adds more data to the hash. -
Early cutoff: If Module A’s types don’t change,
typed(A)returns the sameTypeCheckResult(same Idx values, same hashes). Downstream queries see no change → early cutoff works. Adding hash fields doesn’t break this because: same types → same hashes. -
Pool side-cache: The Pool is stored in
PoolCacheoutside Salsa. Merkle hashes are stored INSIDE the Pool (inself.hashes[]) AND inFunctionSig(inparam_hashes/return_hash). The FunctionSig hashes survive Salsa memoization (they’re part ofTypeCheckResult). The Pool hashes survive viaPoolCache. Both are consistent because they’re computed from the samemerkle_hash()function.
Test:
#[test]
fn typed_result_salsa_compat() {
// Construct a TypeCheckResult with hash fields
let result = TypeCheckResult { /* ... */ };
// Verify Clone, Eq, Hash all work
let cloned = result.clone();
assert_eq!(result, cloned);
let mut hasher = std::collections::hash_map::DefaultHasher::new();
result.hash(&mut hasher);
let h1 = hasher.finish();
let mut hasher = std::collections::hash_map::DefaultHasher::new();
cloned.hash(&mut hasher);
let h2 = hasher.finish();
assert_eq!(h1, h2, "Hash must be deterministic");
}
Exit Criteria:
-
TypeCheckResultwith hash fields passes Salsa trait requirements (2026-02-26) - Early cutoff behavior verified (same types → same result → no re-execution) (2026-02-26)
- No performance regression in
typed()query (hash computation is O(1) per type) (2026-02-26) -
./test-all.shpasses with new fields (2026-02-26, 10,617 tests pass)
Section 03 Completion Checklist
-
param_hashesandreturn_hashadded toFunctionSig(03.1) (2026-02-26) -
merkle_hashadded toTypeEntry(03.2) (2026-02-26) - Hash population in signature inference pipeline (03.3) (2026-02-26)
-
populate_hashes()helper method (03.3) (2026-02-26) - Salsa compatibility verified (03.4) (2026-02-26)
- All construction sites updated (no uninitialized hash fields) (2026-02-26)
-
cargo cand./test-all.shpass (2026-02-26)