Fingerprint ReadPlan/OffsetMap: Hash + Eq + fingerprint() (ADR-012 §1/§4, plan phase 6)
- #[derive(Hash, Eq)] on ReadPlan, FieldPlan, CompositePlan, ReadKind, DiscriminatorPlan (schema: Arc<Value> hashes via serde_json Value Hash + Eq under preserve_order), and on OffsetMap (+ Clone; LeafMeta/OffsetEntry/ByteRange payload already Hash from phase 5 / this phase). - fingerprint() -> u64 on both via std DefaultHasher (deferred decision 3 resolved: no new dep, not hot, cross-version stability a non-goal per ADR-012). - Contract tests both sides: equal schemas -> equal PartialEq + fingerprint; field-kind / field-order / endianness changes each break equality and fingerprint; different root names over the same document fingerprint differently (ReadPlan). - ValidationPlan already carries its own Hash/Eq/fingerprint + contract test (phase 7 landed early). Verification: 474 tests pass (9 new fingerprint contract tests); clippy -D warnings clean; cargo doc zero warnings; wasm32 release build green.
This commit is contained in:
1 parent
255c8c493e
commit
537a2170fb
3 files changed
+193
-8
No files matched your search
@@ -641,7 +641,24 @@ wasm32-unknown-unknown --release`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Fingerprinting `ReadPlan`/`OffsetMap` (ADR-012 §1, §4)
|
||||
## Phase 6 — Fingerprinting `ReadPlan`/`OffsetMap` (ADR-012 §1, §4) — **DONE (2026-09-02)**
|
||||
|
||||
> **Status: implemented.** `#[derive(Hash, Eq)]` added to `ReadPlan`,
|
||||
> `FieldPlan`, `CompositePlan`, `ReadKind`, `DiscriminatorPlan`
|
||||
> (`ReadPlan`'s `schema: Arc<Value>` hashes fine — `serde_json::Value:
|
||||
> Hash + Eq` under the pinned `preserve_order` serde_json) and to
|
||||
> `OffsetMap` (`Clone` added alongside; its `LeafMeta`/`OffsetEntry`/
|
||||
> `ByteRange` payload gained `Hash` in phase 5 / this phase). The
|
||||
> POC's `VariantPlan`/`VariantKind` don't exist in the production
|
||||
> shape (phase 1 dropped them). `fingerprint() -> u64` on both via
|
||||
> `DefaultHasher` (deferred decision 3 resolved: std `DefaultHasher`,
|
||||
> no new dep; the fingerprint isn't hot; cross-version stability is a
|
||||
> non-goal per ADR-012). Fingerprint contract tests on both: same
|
||||
> schema twice → equal `PartialEq` + equal fingerprint; field-kind
|
||||
> change, field-order change, and endianness change each → different
|
||||
> fingerprints; (ReadPlan) different root names over the same document
|
||||
> → different fingerprints. `ValidationPlan` already carries its own
|
||||
> `Hash + Eq` + `fingerprint` + contract test (phase 7).
|
||||
|
||||
**Goal:** Add `Hash + Eq` derives + `fingerprint() -> u64` to `ReadPlan`
|
||||
and `OffsetMap`. Enables cross-run caching, `alkcall` schema handshake,
|
||||
|
||||
+78
-1
@@ -97,7 +97,11 @@ impl OffsetEntry {
|
||||
/// in the table as their fixed-position portion only (length prefix,
|
||||
/// `{offset, length}` pair, or `maxLength` reservation); the variable
|
||||
/// data lives outside the static layout.
|
||||
#[derive(Debug)]
|
||||
///
|
||||
/// `PartialEq`/`Eq`/`Hash`: two equal maps produce identical
|
||||
/// read/write behavior over identical buffers (the compute is a pure
|
||||
/// function of the document).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct OffsetMap {
|
||||
fields: Vec<(String, OffsetEntry)>,
|
||||
total_size: usize,
|
||||
@@ -166,6 +170,19 @@ impl OffsetMap {
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&str, &OffsetEntry)> {
|
||||
self.fields.iter().map(|(path, entry)| (path.as_str(), entry))
|
||||
}
|
||||
|
||||
/// A stable-within-version hash of the map (ADR-012 §1/§4).
|
||||
///
|
||||
/// Two maps with equal fingerprints (equal hashes) produce identical
|
||||
/// read/write behavior over identical buffers. Cross-version
|
||||
/// stability is a non-goal (ADR-012). Uses `DefaultHasher` (std, no
|
||||
/// new dep); the fingerprint is not hot.
|
||||
pub fn fingerprint(&self) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = std::hash::DefaultHasher::new();
|
||||
self.hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Mutable context threaded through the recursive offset computation.
|
||||
@@ -926,6 +943,66 @@ mod tests {
|
||||
assert_eq!(paths, vec!["a", "b"]);
|
||||
}
|
||||
|
||||
// ----- Fingerprint contract (ADR-012 §1/§4) ---------------------------
|
||||
|
||||
#[test]
|
||||
fn fingerprint_is_equal_for_equal_maps() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "a", "kind": "uint8" },
|
||||
{ "name": "b", "kind": "uint32" }
|
||||
]}}});
|
||||
let m1 = map(&root, "S");
|
||||
let m2 = map(&root, "S");
|
||||
assert_eq!(m1, m2);
|
||||
assert_eq!(m1.fingerprint(), m2.fingerprint());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_changes_when_a_field_changes() {
|
||||
let root_a = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "a", "kind": "uint8" },
|
||||
{ "name": "b", "kind": "uint32" }
|
||||
]}}});
|
||||
let root_b = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "a", "kind": "uint16" },
|
||||
{ "name": "b", "kind": "uint32" }
|
||||
]}}});
|
||||
let ma = map(&root_a, "S");
|
||||
let mb = map(&root_b, "S");
|
||||
assert_ne!(ma, mb);
|
||||
assert_ne!(ma.fingerprint(), mb.fingerprint());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_changes_when_field_order_changes() {
|
||||
let root_a = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "a", "kind": "uint8" },
|
||||
{ "name": "b", "kind": "uint8" }
|
||||
]}}});
|
||||
let root_b = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "b", "kind": "uint8" },
|
||||
{ "name": "a", "kind": "uint8" }
|
||||
]}}});
|
||||
let ma = map(&root_a, "S");
|
||||
let mb = map(&root_b, "S");
|
||||
assert_ne!(ma, mb);
|
||||
assert_ne!(ma.fingerprint(), mb.fingerprint());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_changes_when_endianness_changes() {
|
||||
let root_a = json!({ "$defs": { "S": { "kind": "struct", "endian": "little", "fields": [
|
||||
{ "name": "id", "kind": "uint32" }
|
||||
]}}});
|
||||
let root_b = json!({ "$defs": { "S": { "kind": "struct", "endian": "big", "fields": [
|
||||
{ "name": "id", "kind": "uint32" }
|
||||
]}}});
|
||||
let ma = map(&root_a, "S");
|
||||
let mb = map(&root_b, "S");
|
||||
assert_ne!(ma, mb);
|
||||
assert_ne!(ma.fingerprint(), mb.fingerprint());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_rejects_non_struct_top_level() {
|
||||
let root = json!({
|
||||
|
||||
+97
-6
@@ -61,7 +61,7 @@ const MAX_COMPILE_DEPTH: usize = 128;
|
||||
/// integration"). `PartialEq` holds: two equal plans produce identical
|
||||
/// reads over identical buffers (the compile is a pure function of the
|
||||
/// document + root name).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ReadPlan {
|
||||
endian: Endian,
|
||||
fields: Vec<FieldPlan>,
|
||||
@@ -72,7 +72,7 @@ pub struct ReadPlan {
|
||||
/// One compiled struct field: the read dispatch kind, the effective
|
||||
/// endianness, the field-level annotations, and (for composites) the
|
||||
/// compiled body.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct FieldPlan {
|
||||
name: String,
|
||||
kind: ReadKind,
|
||||
@@ -84,7 +84,7 @@ pub struct FieldPlan {
|
||||
|
||||
/// The read dispatch kind for a field. Composites carry their compiled
|
||||
/// body in [`FieldPlan::body`]; primitives and enums read directly.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ReadKind {
|
||||
Primitive(AlkTypeKind),
|
||||
Enum,
|
||||
@@ -96,7 +96,7 @@ pub enum ReadKind {
|
||||
|
||||
/// The compiled body of a composite node — the packed-read analog of
|
||||
/// `BastType`'s composite arms.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum CompositePlan {
|
||||
/// A nested struct — its own field list and container default.
|
||||
Struct(ReadPlan),
|
||||
@@ -129,7 +129,7 @@ pub enum CompositePlan {
|
||||
}
|
||||
|
||||
/// The compiled union discriminator (ADR-003, D-BAST-005).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum DiscriminatorPlan {
|
||||
/// A fixed-size integer at a known byte offset within the union's
|
||||
/// buffer. Mapping keys are stringified integers.
|
||||
@@ -196,10 +196,26 @@ impl ReadPlan {
|
||||
|
||||
/// The BAST document this plan was compiled from (retained as
|
||||
/// `Arc<Value>` — the reader's `schema()` accessor returns it, and
|
||||
/// phase 6's `Hash` derive covers it via `serde_json::Value: Hash`).
|
||||
/// the `Hash` derive covers it via `serde_json::Value: Hash`).
|
||||
pub fn schema(&self) -> &Value {
|
||||
&self.schema
|
||||
}
|
||||
|
||||
/// A stable-within-version hash of the plan (ADR-012 §1/§4).
|
||||
///
|
||||
/// Two plans with equal [`fingerprint`](Self::fingerprint)s (equal
|
||||
/// hashes) produce identical reads over identical bytes — the plan
|
||||
/// is a pure function of the document + root name, and the hash is
|
||||
/// derived from the full plan contents. Cross-version stability is
|
||||
/// a non-goal (ADR-012): a new `AlkTypeKind` may change the hash.
|
||||
/// Uses `DefaultHasher` (std, no new dep); the fingerprint is not
|
||||
/// hot.
|
||||
pub fn fingerprint(&self) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = std::hash::DefaultHasher::new();
|
||||
self.hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl FieldPlan {
|
||||
@@ -1255,6 +1271,81 @@ mod tests {
|
||||
assert!(debug.contains("ReadPlan"));
|
||||
}
|
||||
|
||||
// ----- Fingerprint contract (ADR-012 §1/§4) ---------------------------
|
||||
|
||||
#[test]
|
||||
fn fingerprint_is_equal_for_equal_plans() {
|
||||
let root = json!({ "$defs": {
|
||||
"S": { "kind": "struct", "endian": "big", "fields": [
|
||||
{ "name": "id", "kind": "uint32" },
|
||||
{ "name": "pt", "kind": { "$ref": "#/$defs/Point" } }
|
||||
]},
|
||||
"Point": { "kind": "struct", "fields": [
|
||||
{ "name": "x", "kind": "uint16" },
|
||||
{ "name": "y", "kind": "uint16" }
|
||||
]}
|
||||
}});
|
||||
let p1 = plan(&root, "S");
|
||||
let p2 = plan(&root, "S");
|
||||
assert_eq!(p1, p2);
|
||||
assert_eq!(p1.fingerprint(), p2.fingerprint());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_changes_when_a_field_changes() {
|
||||
let root_a = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "a", "kind": "uint8" }
|
||||
]}}});
|
||||
let root_b = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "a", "kind": "uint16" }
|
||||
]}}});
|
||||
let pa = plan(&root_a, "S");
|
||||
let pb = plan(&root_b, "S");
|
||||
assert_ne!(pa, pb);
|
||||
assert_ne!(pa.fingerprint(), pb.fingerprint());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_changes_when_field_order_changes() {
|
||||
let root_a = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "a", "kind": "uint8" },
|
||||
{ "name": "b", "kind": "uint8" }
|
||||
]}}});
|
||||
let root_b = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "b", "kind": "uint8" },
|
||||
{ "name": "a", "kind": "uint8" }
|
||||
]}}});
|
||||
let pa = plan(&root_a, "S");
|
||||
let pb = plan(&root_b, "S");
|
||||
assert_ne!(pa, pb);
|
||||
assert_ne!(pa.fingerprint(), pb.fingerprint());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_changes_when_endianness_changes() {
|
||||
let root_a = json!({ "$defs": { "S": { "kind": "struct", "endian": "little", "fields": [
|
||||
{ "name": "id", "kind": "uint32" }
|
||||
]}}});
|
||||
let root_b = json!({ "$defs": { "S": { "kind": "struct", "endian": "big", "fields": [
|
||||
{ "name": "id", "kind": "uint32" }
|
||||
]}}});
|
||||
let pa = plan(&root_a, "S");
|
||||
let pb = plan(&root_b, "S");
|
||||
assert_ne!(pa, pb);
|
||||
assert_ne!(pa.fingerprint(), pb.fingerprint());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_differs_for_different_root_names() {
|
||||
let root = json!({ "$defs": {
|
||||
"A": { "kind": "struct", "fields": [ { "name": "a", "kind": "uint8" } ] },
|
||||
"B": { "kind": "struct", "fields": [ { "name": "b", "kind": "uint16" } ] }
|
||||
}});
|
||||
let pa = plan(&root, "A");
|
||||
let pb = plan(&root, "B");
|
||||
assert_ne!(pa.fingerprint(), pb.fingerprint());
|
||||
}
|
||||
|
||||
// ----- Send + Sync (ADR-011 §"Engine integration") --------------------
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user