diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 5fafc5cdd..b0bbdd076 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -10,6 +10,11 @@ - #1874 landed the co-research hydration map and the DR64³ contraction test. - No contract type changed. +## 2026-10-31 — main after #1869 and #1871 (`lance-graph-dt64`) + +- New production crate `crates/lance-graph-dt64` (contract-only): the bind store (D-DT64-6 A.5). It holds classes, methods, K3 guards, ordered effects and one resolve, plus the polyfill as one lens classid. +- The thin-provisioned extent and the `dr` addressing module are not on main yet; they are in the follow-up PR. + ## 2026-10-30 — main after #1862 (merged `abe0b7afc`) - #1862 landed docs and two tests: D-DT64-6 Amendment A, the Morton aligned-square test, and the mask-risc dose test; D-LITHO folded into D-DT64-6. diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index 1886ed1a7..596116d7b 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -4,6 +4,27 @@ - **Reverted inside the PR:** the #1879 commits. That PR, which put the address in the row key, answered a question selected by mistake and was closed unmerged. - **Confidence:** LAB, measured: 25 tests + 2 doctests; disable runs M1, M3 and M4 red; M2 (seif dedup) recorded as representation, not addressing. Merged before Codex and CI finished. +## 2026-10-30 — lance-graph PR #1871 (merged `e1df34da1`, branch `ccr-45a41fb4-0j3i0l`, head `6a0124a06`) — lance-graph-dt64: the polyfill as one lens classid; DR extent; fmt fix + +- **Added:** + - The polyfill vocabulary is one lens classid. Its words are fields: `AlgebraRef { lens, word }`, labels come from the lens's ClassView, `declare_algebra`, `resolve_algebra`, `words_used`. + - `AlgebraId`, the string catalogue and the per-entry `SigRev` are removed. + - `Extent` per class, with fixed per-axis widths (W64/W256). + - `morton8x8.rs` formatted. Main's Style Check format step had been red since #1862. + - STATUS_BOARD catch-up after #1869/#1870. Plan D-DT64-6 A.5 addenda 7–10. +- **Not in it:** the thin-provisioned extent (`9014fa07b`) and addendum 11. They were pushed after the merge and land in the follow-up PR. +- **Confidence:** 20 tests; 7 disable runs red. + +## 2026-10-30 — lance-graph PR #1869 (merged `9d119bb5f`, branch `ccr-45a41fb4-0j3i0l`, head `1761d4ce8`) — lance-graph-dt64: the bind store for meaning and behaviour + +- **Added:** + - `crates/lance-graph-dt64`, a workspace member that depends only on `lance-graph-contract`. + - `MethodId` + `SigRev`, K3 `Guard` (`SetExpr` + `Logic`) with `witness`, ordered `Effect`s with operands. + - One `resolve`, D5 immutable classid with derived classid + versioned route log, `admit(MethodRef)`. + - `field_view` / `field_meaning` / `mask_where`, the dataflow check, `footprint`. + - Plan D-DT64-6 A.5 + addenda 1–6. +- **Confidence:** 19 tests; every guard disable-verified red. + ## 2026-10-31 — lance-graph PR #1875 (merged `b7adb0c`, branch `claude/mistaber-ruff-harvest-9gtxd4`, head `ed71916`) — D-MIST-NATIVE-1a: HHTL16 Register128 rows replace the name interner - **Added:** diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index d4a0ceff8..f4412eef9 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -69,7 +69,7 @@ | D-DT64-6-3 | register OGIT verbs as DT64 predicates in 64-wide blocks | Queued | D-DT64-6-2 green | | D-DT64-6-4 | consumer read-only access via HotPlug activation | Queued | D-DT64-6-3 green | | D-DT64-6-5 | OGIT becomes cross-check oracle once a domain has attested predicates | Queued | one attested domain | -| D-DT64-6-BS | `crates/lance-graph-dt64`: the bind store (workspace member, contract-only dep). Closes D-DRDT §4 gaps 1–4 (MethodId + SigRev, K3 guard as `SetExpr` + `Logic`, ordered effects, one `resolve`); D5 immutable classid, derived classid + versioned route log; D-DRDT §5 falsifier | **Shipped** #1869 | 19 tests green, clippy/fmt clean; disable runs (Amendment A.5) | +| D-DT64-6-BS | `crates/lance-graph-dt64`: the bind store (workspace member, contract-only dep). Closes D-DRDT §4 gaps 1–4 (MethodId + SigRev, K3 guard as `SetExpr` + `Logic`, ordered effects, one `resolve`); D5 immutable classid, derived classid + versioned route log; D-DRDT §5 falsifier | **Shipped** #1869, #1871; `dr` module + thin-provisioned extent in PR | 27 tests green (21 binding, 6 `dr`), clippy/fmt clean; disable runs (Amendment A.5 addenda 9–12) | ## D-DT64 — DTO Tensor 64: versioned semantic predicate adapter and DR projections (2026-10-26) diff --git a/.claude/board/entries/2026-10-31-d-dt64-6-dr-addressing-module.md b/.claude/board/entries/2026-10-31-d-dt64-6-dr-addressing-module.md new file mode 100644 index 000000000..a1b63e064 --- /dev/null +++ b/.claude/board/entries/2026-10-31-d-dt64-6-dr-addressing-module.md @@ -0,0 +1,14 @@ +# D-DT64-6-DR: DR addressing as `lance_graph_dt64::dr` + +**Date 2026-10-31 · Status VERIFIED-IN-CODE · Plan [`d-dt64-6-routing-bindstore-behavior-v1.md`](../../plans/d-dt64-6-routing-bindstore-behavior-v1.md) A.5 addendum 12** + +DR's addressing moves from `dr-math-binding-probe` into the bind store as the module `dr`. It holds reservations, the route log, the frame witness, the guards, the typed axis and the global → local map. It names nothing from the binding side ("DR routes, DT interprets"), and a test pins that. DR's computations stay with Quack. + +The thin-provisioned extent (`9014fa07b`), which #1871 merged without, is restored in the same PR. + +27 tests; 6 disable runs red. + +**OPEN:** +- the probe owner switches to `dt64::dr`; +- `bind_dr64` waits on the `MethodSignature` vs `MethodRef` identity (EXO-50); +- the reservation as a global range (start + length). diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 03d83fa38..0e17f9a3a 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,13 +25,14 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately opposite directions; the stranding this convention prevents shows up in exactly one of them, never both. -519 entries, 2026-08-06 .. 2026-10-31. +520 entries, 2026-08-06 .. 2026-10-31. | date | entry id | finding | file | |---|---|---|---| | 2026-10-31 | `D-SAG-6-2-LAB` | | [2026-10-31-d-sag-6-2-lab-projection.md](2026-10-31-d-sag-6-2-lab-projection.md) | | 2026-10-31 | `D-SAG-6-1-CHECKLIST` | | [2026-10-31-d-sag-6-1-checklist.md](2026-10-31-d-sag-6-1-checklist.md) | | 2026-10-31 | `D-MIST-NATIVE-1b` | | [2026-10-31-d-mist-native-1b-makor-hydrate.md](2026-10-31-d-mist-native-1b-makor-hydrate.md) | +| 2026-10-31 | `D-DT64-6-DR` | | [2026-10-31-d-dt64-6-dr-addressing-module.md](2026-10-31-d-dt64-6-dr-addressing-module.md) | | 2026-10-30 | `D-XLOG-NATIVE-0` | | [2026-10-30-d-xlog-native-crosswalk.md](2026-10-30-d-xlog-native-crosswalk.md) | | 2026-10-30 | `D-XLOG-DR64-0` | XLOG's CPU frontend lowers onto DR64 relcomp (answer {2}, 2 oracles); its optimizer emits ChainJoin and swapped orders; arrow 60 vs 58 blocks in-tree use; masks lose to scalar on selective data | [2026-10-30-d-xlog-dr64-0-xlog-rir-over-dr64-relcomp.md](2026-10-30-d-xlog-dr64-0-xlog-rir-over-dr64-relcomp.md) | | 2026-10-30 | `D-TRACT-SHAPE-1` | | [2026-10-30-d-tract-shape-1-cold-shape-folding.md](2026-10-30-d-tract-shape-1-cold-shape-folding.md) | diff --git a/.claude/plans/d-dt64-6-routing-bindstore-behavior-v1.md b/.claude/plans/d-dt64-6-routing-bindstore-behavior-v1.md index a9c64f655..4c2be5fbd 100644 --- a/.claude/plans/d-dt64-6-routing-bindstore-behavior-v1.md +++ b/.claude/plans/d-dt64-6-routing-bindstore-behavior-v1.md @@ -337,3 +337,57 @@ Disable runs on the extent commit: | W256 read as 64 | 2 | Still capped at 64: `tract-harvest-probe`'s `dr3::TILE = 64`, a single DR64 tile built from `relcomp::Rows = [u64; 64]`. That is the tract session's lab crate. Its full model (4,146 of 7,620 nodes WHEN-true) already reports ">64 positions untiled". Lifting it is that session's change; the binding side no longer forces 64. + +**A.5 addendum 11 (2026-10-30, operator: "reserve but don't claim, thin provisioning; Jina as a result is not able to use its 7000 nodes").** Addendum 10 gave each axis one width, W64 or W256, so the most any class could address was 256 identities, and Jina's 7,620 nodes could not be bound. Each axis is now a reservation, `Axis { tile: W64 | W256, tiles }`: +- `Extent::DR256_GLOBAL_X64` reserves the whole DR256 identity space: 65,536 positions in 256 tiles of 256. +- Reserving claims nothing. `BindStore::claimed(class, version)` counts the identity tiles that hold a position routed to the class at that version. +- `tile_bytes` states the cost of one claimed tile: 256 × 256 × 64 / 8 = 512 KiB. Routing Jina's 7,620 nodes claims 30 tiles (15 MiB stated, nothing allocated), out of 256 reserved. +- `route` refuses a position past the class's identity reservation (`OutsideReservation`) and moves no version. +- An axis wider than it can address is refused (`ReservationTooWide`), never truncated. + +Disable runs, each failing exactly one test: + +| guard removed | failing tests | +|---|---| +| the reservation check in `route` | 1 | +| claiming by tile (counting positions instead) | 1 | +| the too-wide check | 1 | +| costing the reservation instead of the claim | 1 | + +Still capped, and stated: one class's property axis is at most 256 fields, because a `WideFieldMask` position is a `u8` (contract). Wider would mean tiled field addressing in the contract, which this crate does not own. In the bind store, Jina's nodes are identity positions, and an ONNX lens's fields are its op signatures, so this does not block Jina here. The tract lab's own DR#3 layout puts Jina's 7,620 values on its property axis, and `dr3::TILE = 64` is that session's to lift. + +**A.5 addendum 12 (2026-10-31, operator: "wouldn't it make sense to move DR into DT64 — at least from the lab probe crate").** DR's addressing is now the module `lance_graph_dt64::dr`, lifted from `dr-math-binding-probe`. Your diagram's "DR routes, DT interprets" becomes a module boundary inside the bind store, not a separate crate. D4 already makes DT64 the bind store for all holographic addresses, and §3 puts the route versioning there. + +`dr` holds: +- reservations: `Width`, `Axis`, `Extent`, `Claim`, `MAX_RESERVED`; +- the route log: `RouteLog` with `route`, `retract`, `routed_at`, `claimed`; +- the frame witness: `FrameStore`, `Frame`, `FrameRevisions`, `FrameRefusal`; +- the guards: `Focus`, `Scope`, `Guards::check`; +- the typed axis: `AxisKind`, `ClassAxis`; +- the global → local pointer table: `LocalMap`, `address`, `coordinates`, `rotate_yz`. + +It names nothing from the binding side, and a test pins that. `BindStore` keeps its API and delegates to `RouteLog`. + +Two changes against the probe: +- The probe's `Axis { class, kind }` is `ClassAxis` here, because `Axis` is the reservation axis. +- `LocalMap` and `address` take a tile `Width` instead of a fixed 64, so a DR256 tile is addressed the same way. + +Not lifted: +- `relcomp`, `where_dr3`, `population`, `recipe` and the folds in `hhtl16` are computations, proofs and oracles, so they belong to Quack. `relcomp` also carries egglog and sha2, which a contract-only crate must not. +- `dr4::route` produces values, so it is execution. +- `bind_dr64` / `BoundDr64` wait on the `MethodSignature` vs `MethodRef` identity question (EXO-50). + +The probe itself is unchanged; its owner switches to `dt64::dr`. + +Also restored here: #1871 merged before its last two commits were pushed. `9014fa07b` (thin-provisioned extent) and addendum 11 were on the branch only and are cherry-picked into this PR. + +Disable runs on the `dr` commit, each turning a test red: + +| guard removed | failing tests | +|---|---| +| the foreign-store check | 2 | +| the source-revision refusal | 1 | +| the focus guard | 1 | +| `LocalMap` width read as 64 | 1 | +| address width read as 64 | 1 | +| a `use crate::` in `dr.rs` (the boundary test) | 1 | diff --git a/crates/lance-graph-dt64/src/dr.rs b/crates/lance-graph-dt64/src/dr.rs new file mode 100644 index 000000000..062a0d652 --- /dev/null +++ b/crates/lance-graph-dt64/src/dr.rs @@ -0,0 +1,535 @@ +//! DR: addressing. Positions, tiles, reservations, the route log, the frame +//! witness and the global → local pointer table. +//! +//! "DR routes, DT interprets." This module knows no method, guard or effect; +//! the binding side of the crate reads it, and it reads nothing back. DR's +//! computations (relation composition, `DR256::WHERE`, membership folds) are +//! not here: they are executions, and execution is Quack's (relalg → +//! mask-risc → `ndarray::simd`), referenced from a binding as a polyfill word. +//! +//! Lifted from `dr-math-binding-probe` (D-DRMC-1: [`FrameStore`], [`Frame`], +//! [`Focus`], [`Scope`], [`Guards`], [`AxisKind`]; D-DRCA-1: [`LocalMap`], +//! [`address`], [`coordinates`], [`rotate_yz`]). Two changes against the probe: +//! - The probe's `Axis { class, kind }` is [`ClassAxis`] here, because [`Axis`] +//! is the reservation axis. +//! - [`LocalMap`] and [`address`] take a tile [`Width`] instead of a fixed 64, +//! so a DR256 tile is addressed the same way. +//! +//! Nothing here holds a value. A route maps a position to a classid; a tile +//! is a reservation, claimed when a position in it is routed; a local map +//! holds identity references only. + +use std::cell::Cell; +use std::collections::{BTreeMap, BTreeSet}; +use std::sync::{Arc, Weak}; + +use lance_graph_contract::class_view::ClassId; +use lance_graph_contract::morton8x8::Morton8x8; + +/// Positions per axis of a DR tile (D-DRF-1 §1): DR64 or DR256. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Width { + W64, + W256, +} + +impl Width { + pub const fn positions(self) -> usize { + match self { + Width::W64 => 64, + Width::W256 => 256, + } + } +} + +/// One axis of a reservation: `tiles` tiles of `tile` positions each. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Axis { + pub tile: Width, + pub tiles: u32, +} + +impl Axis { + pub const fn new(tile: Width, tiles: u32) -> Axis { + Axis { tile, tiles } + } + + /// Positions reserved on this axis. + pub const fn reserved(self) -> usize { + self.tile.positions() * self.tiles as usize + } + + /// The tile a position falls in. + pub const fn tile_of(self, position: usize) -> usize { + position / self.tile.positions() + } +} + +/// What a class reserves, per axis: Identity × Property × Predicate, each a +/// number of DR64 or DR256 tiles (D-DT64-6 A.1: the binding names DR64 or +/// DR256 and states its cost). Thin provisioning: a reservation costs +/// nothing; only the identity tiles a route actually touches are claimed +/// ([`RouteLog::claimed`]), and nothing is ever allocated here (§7). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Extent { + pub identity: Axis, + pub property: Axis, + /// The class's predicates: its methods (ON × WHEN × DO registrations). + pub predicate: Axis, +} + +impl Extent { + /// DR64³: one 64-wide tile per axis. + pub const DR64_CUBE: Extent = Extent { + identity: Axis::new(Width::W64, 1), + property: Axis::new(Width::W64, 1), + predicate: Axis::new(Width::W64, 1), + }; + /// DR256² × 64: one 256 × 256 Identity × Property tile under 64 + /// predicates. + pub const DR256_SQ_X64: Extent = Extent { + identity: Axis::new(Width::W256, 1), + property: Axis::new(Width::W256, 1), + predicate: Axis::new(Width::W64, 1), + }; + /// The whole DR256 identity space (65,536 positions, 256 tiles of 256) + /// reserved over a 256-wide property axis and 64 predicates. Reserving + /// it claims nothing. + pub const DR256_GLOBAL_X64: Extent = Extent { + identity: Axis::new(Width::W256, 256), + property: Axis::new(Width::W256, 1), + predicate: Axis::new(Width::W64, 1), + }; + + /// Logical cells reserved. + pub const fn reserved_cells(self) -> usize { + self.identity.reserved() * self.property.reserved() * self.predicate.reserved() + } + + /// What one claimed identity tile would cost as a dense Boolean volume, + /// in bytes: a statement of cost, not an allocation. DR64³ is 32 KiB + /// (D-DRF-1 §1). + pub const fn tile_bytes(self) -> usize { + self.identity.tile.positions() * self.property.reserved() * self.predicate.reserved() / 8 + } +} + +/// What a class has claimed of its reservation at one route version. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Claim { + /// Distinct identity tiles holding at least one position routed to the + /// class. + pub identity_tiles: usize, + /// `identity_tiles × Extent::tile_bytes`. + pub bytes: usize, +} + +/// The widest identity or predicate reservation: route positions are `u16`. +pub const MAX_RESERVED: usize = 1 << 16; + +/// A position in the append-only route log. `RouteVersion(0)` is the empty +/// log; each route or retract advances it by one. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RouteVersion(pub u64); + +/// Why the route log refused. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RouteRefusal { + /// A route to a position outside the class's identity reservation. + OutsideReservation { + class: u32, + position: u16, + reserved: usize, + }, + /// The version is past the end of the route log. + FutureVersion { + asked: RouteVersion, + current: RouteVersion, + }, +} + +/// DR64 direct routing: the append-only log from a DR position to the +/// classid bound there. Only routing is versioned (D-DT64-6 §3); what a +/// classid means is the binding side's, and does not change. +#[derive(Debug, Clone, Default)] +pub struct RouteLog { + /// Per position, the route events in version order: `(version, class)`, + /// `None` for a retract. + routes: BTreeMap)>>, + version: RouteVersion, +} + +impl RouteLog { + pub fn new() -> RouteLog { + RouteLog::default() + } + + /// The current end of the log. + pub fn current_version(&self) -> RouteVersion { + self.version + } + + /// Route `position` to `class`, whose identity reservation is `identity`; + /// returns the new version. A position past the reservation is refused + /// and moves no version. + pub fn route( + &mut self, + position: u16, + class: u32, + identity: Axis, + ) -> Result { + let reserved = identity.reserved(); + if usize::from(position) >= reserved { + return Err(RouteRefusal::OutsideReservation { + class, + position, + reserved, + }); + } + Ok(self.push(position, Some(class))) + } + + /// Remove `position`'s route; returns the new version. + pub fn retract(&mut self, position: u16) -> RouteVersion { + self.push(position, None) + } + + fn push(&mut self, position: u16, class: Option) -> RouteVersion { + self.version = RouteVersion(self.version.0 + 1); + self.routes + .entry(position) + .or_default() + .push((self.version, class)); + self.version + } + + /// The classid `position` routed to at `version`, `None` if it was + /// unrouted then. + pub fn routed_at( + &self, + version: RouteVersion, + position: u16, + ) -> Result, RouteRefusal> { + if version > self.version { + return Err(RouteRefusal::FutureVersion { + asked: version, + current: self.version, + }); + } + let Some(events) = self.routes.get(&position) else { + return Ok(None); + }; + // Events are in version order; take the last one at or before `version`. + let seen = events.partition_point(|(v, _)| *v <= version); + Ok(seen.checked_sub(1).and_then(|i| events[i].1)) + } + + /// What `class` has claimed of its reservation `extent` at `version`: the + /// identity tiles that hold a position routed to it then. A reservation + /// alone claims nothing. + pub fn claimed( + &self, + class: u32, + extent: Extent, + version: RouteVersion, + ) -> Result { + let mut tiles = BTreeSet::new(); + for &position in self.routes.keys() { + if self.routed_at(version, position)? == Some(class) { + tiles.insert(extent.identity.tile_of(usize::from(position))); + } + } + Ok(Claim { + identity_tiles: tiles.len(), + bytes: tiles.len() * extent.tile_bytes(), + }) + } +} + +/// What one numeric coordinate means according to the resolved ClassView. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum AxisKind { + Identity, + Property, + Community, + Capability, +} + +/// A typed coordinate axis: which class's ClassView, and which kind. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct ClassAxis { + pub class: ClassId, + pub kind: AxisKind, +} + +/// The revisions that identify a reference: which source, which relation +/// map, which coordinate assignment. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameRevisions { + pub source: u64, + pub relation_map: u64, + pub coordinates: u64, +} + +/// Why a frame was refused. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameRefusal { + /// Sealed by another store, even at equal revisions. + ForeignFrame, + SourceChanged, + MapChanged, + CoordinatesChanged, + /// Re-sealed at equal revisions: still a new frame. + FrameResealed, +} + +/// The owner of a frame. Only it issues [`Frame`]s, and a frame compares by +/// identity of that issue, never by value: two stores at the same numbers do +/// not alias, and re-sealing equal numbers is a new frame. +#[derive(Debug)] +pub struct FrameStore { + identity: Arc<()>, + current: Arc, +} + +impl FrameStore { + pub fn new(revisions: FrameRevisions) -> Self { + Self { + identity: Arc::new(()), + current: Arc::new(revisions), + } + } + + /// Seal the current frame. + pub fn seal(&self) -> Frame { + Frame { + store: Arc::downgrade(&self.identity), + seal: Arc::downgrade(&self.current), + at: *self.current, + } + } + + /// Move to new revisions; every frame sealed before is stale from now on. + pub fn advance(&mut self, revisions: FrameRevisions) { + self.current = Arc::new(revisions); + } + + pub fn revisions(&self) -> FrameRevisions { + *self.current + } + + /// `Ok` iff `frame` is this store's current seal. A frame of another store + /// is `ForeignFrame` even at equal numbers; a stale frame of this store + /// names the first revision that moved, or `FrameResealed` if none did. + pub fn check(&self, frame: &Frame) -> Result<(), FrameRefusal> { + if !Weak::ptr_eq(&frame.store, &Arc::downgrade(&self.identity)) { + return Err(FrameRefusal::ForeignFrame); + } + if Weak::ptr_eq(&frame.seal, &Arc::downgrade(&self.current)) { + return Ok(()); + } + let (then, now) = (frame.at, *self.current); + Err(if then.source != now.source { + FrameRefusal::SourceChanged + } else if then.relation_map != now.relation_map { + FrameRefusal::MapChanged + } else if then.coordinates != now.coordinates { + FrameRefusal::CoordinatesChanged + } else { + FrameRefusal::FrameResealed + }) + } +} + +/// A store-issued seal over the reference revisions. No public constructor: a +/// caller cannot build a frame from numbers, only receive one from +/// [`FrameStore::seal`]. +#[derive(Clone, Debug)] +pub struct Frame { + store: Weak<()>, + seal: Weak, + at: FrameRevisions, +} + +impl Frame { + /// The revisions this frame was sealed at (for reporting, never identity). + pub fn revisions(&self) -> FrameRevisions { + self.at + } + + /// Same issue of the same store. + pub fn same_issue(&self, other: &Frame) -> bool { + Weak::ptr_eq(&self.store, &other.store) && Weak::ptr_eq(&self.seal, &other.seal) + } +} + +/// Which tile of the frame is read. Motion, not reference: a new focus over +/// the same frame is not a new frame. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Focus { + pub revision: u64, +} + +/// What the reader is authorized to see. Policy, checked as its own guard; +/// never part of frame identity. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Scope { + pub id: u64, +} + +/// What a read was made under. +#[derive(Clone, Debug)] +pub struct Guards { + pub frame: Frame, + pub focus: Focus, + pub scope: Scope, +} + +/// Why a guard set was refused. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum GuardRefusal { + Frame(FrameRefusal), + FocusChanged, + ScopeChanged, +} + +impl Guards { + /// Each guard on its own: the frame against the store that issued it, then + /// the focus, then the scope. + pub fn check(&self, store: &FrameStore, now: (Focus, Scope)) -> Result<(), GuardRefusal> { + store.check(&self.frame).map_err(GuardRefusal::Frame)?; + if self.focus != now.0 { + return Err(GuardRefusal::FocusChanged); + } + if self.scope != now.1 { + return Err(GuardRefusal::ScopeChanged); + } + Ok(()) + } +} + +/// Why a local mapping or an address was refused. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MapRefusal { + /// More identities than the tile holds. + TooMany, + /// The same global id offered twice. + Duplicate, + /// A global id this map does not hold. + Unmapped, + /// A Morton code outside the tile. + OutOfTile, +} + +/// The prepared global → local pointer table for one tile under one frame +/// (EXO-46). It holds identity references and local positions only, never +/// values. Lookups are counted, so a test can show a projection never calls +/// it. +#[derive(Debug)] +pub struct LocalMap { + width: Width, + /// local → global (`globals[local]`), in the order the source supplied. + globals: Vec, + /// Globals sorted, with the local position each maps to. + sorted: Vec, + sorted_local: Vec, + lookups: Cell, +} + +impl LocalMap { + /// Prepare the map for one tile of `width`. Refuses more ids than the + /// tile holds, and duplicates. + pub fn prepare(width: Width, globals: &[u64]) -> Result { + if globals.len() > width.positions() { + return Err(MapRefusal::TooMany); + } + // `globals.len() <= 256`, so every local position fits a `u8`. + let mut order: Vec = (0..globals.len()).map(|l| l as u8).collect(); + order.sort_unstable_by_key(|&l| globals[l as usize]); + let sorted: Vec = order.iter().map(|&l| globals[l as usize]).collect(); + if sorted.windows(2).any(|w| w[0] == w[1]) { + return Err(MapRefusal::Duplicate); + } + Ok(Self { + width, + globals: globals.to_vec(), + sorted, + sorted_local: order, + lookups: Cell::new(0), + }) + } + + pub fn width(&self) -> Width { + self.width + } + + /// global → local. Counted. + pub fn local(&self, global: u64) -> Result { + self.lookups.set(self.lookups.get() + 1); + self.sorted + .binary_search(&global) + .map(|at| self.sorted_local[at]) + .map_err(|_| MapRefusal::Unmapped) + } + + /// local → global (the inverse). Not counted: it is the pointer table read. + pub fn global(&self, local: u8) -> Option { + self.globals.get(usize::from(local)).copied() + } + + pub fn len(&self) -> usize { + self.globals.len() + } + + pub fn is_empty(&self) -> bool { + self.globals.is_empty() + } + + /// How many global → local lookups this map has served. + pub fn lookups(&self) -> u64 { + self.lookups.get() + } + + /// FNV-1a over the local → global table, in local order. A frame states + /// which coordinate assignment it was sealed under by carrying this value + /// as its `coordinates` revision. A digest, not an identity: two maps + /// over the same ids in the same order are the same assignment. + pub fn digest(&self) -> u64 { + let mut h: u64 = 0xcbf2_9ce4_8422_2325; + for &g in &self.globals { + for b in g.to_le_bytes() { + h ^= u64::from(b); + h = h.wrapping_mul(0x0000_0100_0000_01b3); + } + } + h + } +} + +/// The HHTL16 Cartesian address of cell `(x, y)` of one tile of `width`: the +/// contract's [`Morton8x8`] code, `x` the row coordinate, `y` the column. +#[inline] +pub fn address(width: Width, x: u8, y: u8) -> Result { + if usize::from(x) >= width.positions() || usize::from(y) >= width.positions() { + return Err(MapRefusal::OutOfTile); + } + Ok(Morton8x8::from_xy(x, y)) +} + +/// The inverse of [`address`]: the `(x, y)` a code addresses in a tile of +/// `width`. +#[inline] +pub fn coordinates(width: Width, code: Morton8x8) -> Result<(u8, u8), MapRefusal> { + let (x, y) = (code.x(), code.y()); + if usize::from(x) >= width.positions() || usize::from(y) >= width.positions() { + return Err(MapRefusal::OutOfTile); + } + Ok((x, y)) +} + +/// YZ rotation: swap the two Cartesian axes of a code. An address map; it +/// moves no stored bits, and it is its own inverse. +#[inline] +pub fn rotate_yz(code: Morton8x8) -> Morton8x8 { + Morton8x8::from_xy(code.y(), code.x()) +} diff --git a/crates/lance-graph-dt64/src/lib.rs b/crates/lance-graph-dt64/src/lib.rs index e37916c87..d6e4aecdd 100644 --- a/crates/lance-graph-dt64/src/lib.rs +++ b/crates/lance-graph-dt64/src/lib.rs @@ -85,6 +85,11 @@ use lance_graph_contract::property::{PropertySchema, PropertySpec}; use lance_graph_contract::set_algebra::{normalize, normalize_kleene, Logic, SetExpr}; use lance_graph_contract::set_rules::{Population, Truth}; +pub mod dr; + +pub use dr::{Axis, Claim, Extent, RouteVersion, Width, MAX_RESERVED}; +use dr::{RouteLog, RouteRefusal}; + /// A method's identity within its class, independent of its source name. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct MethodId(pub u32); @@ -98,11 +103,6 @@ pub struct SigRev(pub u32); #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SchemaRev(pub u32); -/// A position in the append-only route log. `RouteVersion(0)` is the empty -/// log; each route or retract advances it by one. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct RouteVersion(pub u64); - /// A polyfill word: a field position under the polyfill's lens classid. The /// word's name (a mask-risc op, an `ndarray::simd` facade word) is that /// field's label in the lens's [`ClassView`], resolved late like any other @@ -228,61 +228,6 @@ impl MethodBinding { } } -/// Positions per axis of a DR focus (D-DRF-1 §1): DR64 or DR256. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum Width { - W64, - W256, -} - -impl Width { - pub const fn positions(self) -> usize { - match self { - Width::W64 => 64, - Width::W256 => 256, - } - } -} - -/// The logical extent a class binds over, chosen by the consumer per axis: -/// Identity × Property × Predicate. D-DT64-6 A.1: the DT binding names DR64 -/// or DR256 and states its cost in those units. Logical only: nothing is -/// allocated here, and no cube is assumed to exist (§7). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Extent { - pub identity: Width, - pub property: Width, - /// The class's predicates: its methods (ON × WHEN × DO registrations). - pub predicate: Width, -} - -impl Extent { - /// DR64³: 64 × 64 × 64. - pub const DR64_CUBE: Extent = Extent { - identity: Width::W64, - property: Width::W64, - predicate: Width::W64, - }; - /// DR256² × 64: a 256 × 256 Identity × Property tile under 64 predicates. - pub const DR256_SQ_X64: Extent = Extent { - identity: Width::W256, - property: Width::W256, - predicate: Width::W64, - }; - - /// Logical cells. - pub const fn cells(self) -> usize { - self.identity.positions() * self.property.positions() * self.predicate.positions() - } - - /// What a dense Boolean volume of this extent would cost, in bytes, if - /// anyone materialized it (DR64³ is 32 KiB, D-DRF-1 §1). A statement of - /// cost, not an allocation. - pub const fn dense_bytes(self) -> usize { - self.cells() / 8 - } -} - /// A class's schema as bound: the schema revision, the field count the /// methods were bound against, and the extent the consumer chose. The field /// basis itself is the [`ClassView`] class [`view_class`] of the classid, @@ -353,17 +298,31 @@ pub enum Refusal { }, /// The declared field count exceeds [`MAX_FIELDS`]. BasisTooWide { class: u32, field_count: usize }, - /// More fields than the extent's Property axis holds. + /// More fields than the class reserved on its Property axis. FieldsExceedExtent { class: u32, field_count: usize, - property: Width, + reserved: usize, }, - /// More methods than the extent's Predicate axis holds. + /// More methods than the class reserved on its Predicate axis. MethodsExceedExtent { class: u32, methods: usize, - predicate: Width, + reserved: usize, + }, + /// A reservation wider than the axis can address: identity and predicate + /// up to [`MAX_RESERVED`], property up to [`MAX_FIELDS`] (a + /// `WideFieldMask` position is a `u8`). + ReservationTooWide { + class: u32, + axis: &'static str, + reserved: usize, + }, + /// A route to a position outside the class's identity reservation. + OutsideReservation { + class: u32, + position: u16, + reserved: usize, }, /// Asked at another schema revision than the one bound. SchemaRevisionMismatch { @@ -436,6 +395,25 @@ impl std::fmt::Display for Refusal { impl std::error::Error for Refusal {} +impl From for Refusal { + fn from(r: RouteRefusal) -> Refusal { + match r { + RouteRefusal::OutsideReservation { + class, + position, + reserved, + } => Refusal::OutsideReservation { + class, + position, + reserved, + }, + RouteRefusal::FutureVersion { asked, current } => { + Refusal::FutureVersion { asked, current } + } + } + } +} + /// The result of one lookup: the class schema as bound, its fields as the /// [`ClassView`] holds them now (borrowed), and its methods. #[derive(Debug, Clone, Copy)] @@ -564,10 +542,8 @@ pub struct BindStore { classes: BTreeMap, /// The bound polyfill lenses, by lens classid (D6). algebra: BTreeMap, - /// Per position, the route events in version order: `(version, class)`, - /// `None` for a retract. - routes: BTreeMap)>>, - version: RouteVersion, + /// DR's route log. The store binds; DR routes. + routes: RouteLog, } impl BindStore { @@ -740,47 +716,42 @@ impl BindStore { /// The current end of the route log. pub fn current_version(&self) -> RouteVersion { - self.version + self.routes.current_version() } - /// Route `position` to the bound `class`; returns the new version. + /// Route `position` to the bound `class`; returns the new version. A + /// position past the class's identity reservation is refused. pub fn route(&mut self, position: u16, class: u32) -> Result { - if !self.classes.contains_key(&class) { - return Err(Refusal::UnknownClass(class)); - } - Ok(self.push_route(position, Some(class))) + let entry = self + .classes + .get(&class) + .ok_or(Refusal::UnknownClass(class))?; + Ok(self + .routes + .route(position, class, entry.schema.extent.identity)?) + } + + /// What `class` has claimed of its reservation at `version`: the identity + /// tiles that hold a position routed to it then. A reservation alone + /// claims nothing. + pub fn claimed(&self, class: u32, version: RouteVersion) -> Result { + let entry = self + .classes + .get(&class) + .ok_or(Refusal::UnknownClass(class))?; + Ok(self.routes.claimed(class, entry.schema.extent, version)?) } /// Remove `position`'s route; returns the new version. The classid it /// routed to keeps its behaviour. pub fn retract(&mut self, position: u16) -> RouteVersion { - self.push_route(position, None) - } - - fn push_route(&mut self, position: u16, class: Option) -> RouteVersion { - self.version = RouteVersion(self.version.0 + 1); - self.routes - .entry(position) - .or_default() - .push((self.version, class)); - self.version + self.routes.retract(position) } /// The classid `position` routed to at `version`, `None` if it was /// unrouted then. pub fn routed_at(&self, version: RouteVersion, position: u16) -> Result, Refusal> { - if version > self.version { - return Err(Refusal::FutureVersion { - asked: version, - current: self.version, - }); - } - let Some(events) = self.routes.get(&position) else { - return Ok(None); - }; - // Events are in version order; take the last one at or before `version`. - let seen = events.partition_point(|(v, _)| *v <= version); - Ok(seen.checked_sub(1).and_then(|i| events[i].1)) + Ok(self.routes.routed_at(version, position)?) } /// The one lookup: `(classid, schema revision) → { fields, methods }`. @@ -889,18 +860,31 @@ fn validate(class: u32, schema: &ClassSchema, methods: &[MethodBinding]) -> Resu }); } let extent = schema.extent; - if schema.field_count > extent.property.positions() { + for (axis, reserved, max) in [ + ("identity", extent.identity.reserved(), MAX_RESERVED), + ("property", extent.property.reserved(), MAX_FIELDS), + ("predicate", extent.predicate.reserved(), MAX_RESERVED), + ] { + if reserved > max { + return Err(Refusal::ReservationTooWide { + class, + axis, + reserved, + }); + } + } + if schema.field_count > extent.property.reserved() { return Err(Refusal::FieldsExceedExtent { class, field_count: schema.field_count, - property: extent.property, + reserved: extent.property.reserved(), }); } - if methods.len() > extent.predicate.positions() { + if methods.len() > extent.predicate.reserved() { return Err(Refusal::MethodsExceedExtent { class, methods: methods.len(), - predicate: extent.predicate, + reserved: extent.predicate.reserved(), }); } for (i, m) in methods.iter().enumerate() { diff --git a/crates/lance-graph-dt64/tests/bind_store.rs b/crates/lance-graph-dt64/tests/bind_store.rs index 9b231c8c0..ba5acfe6d 100644 --- a/crates/lance-graph-dt64/tests/bind_store.rs +++ b/crates/lance-graph-dt64/tests/bind_store.rs @@ -6,8 +6,9 @@ use lance_graph_contract::ontology::{DisplayTemplate, FieldRef}; use lance_graph_contract::set_algebra::{Cmp, Logic, SetExpr}; use lance_graph_contract::set_rules::{Population, Truth}; use lance_graph_dt64::{ - view_class, AlgebraRef, Arg, BindStore, ClassSchema, Effect, Extent, Guard, MethodBinding, - MethodId, MethodRef, Refusal, RouteVersion, SchemaRev, SigRev, ValueDemand, Width, + view_class, AlgebraRef, Arg, Axis, BindStore, ClassSchema, Effect, Extent, Guard, + MethodBinding, MethodId, MethodRef, Refusal, RouteVersion, SchemaRev, SigRev, ValueDemand, + Width, }; /// A ClassView with one class of `n` fields; `n` can be changed to model a @@ -833,35 +834,37 @@ fn a_risk_mask_reaches_the_methods_that_touch_it() { ); } -/// A consumer chooses its extent; nothing caps it at 64. An ONNX graph with -/// 200 values and 70 nodes does not fit DR64³ and is refused there with the -/// axis named; DR256² × 64 holds the 200 values, and a 256-wide predicate -/// axis holds the 70 methods. The cost is stated, not allocated. -#[test] -fn a_consumer_chooses_its_extent_and_is_not_capped_at_64() { - assert_eq!(Extent::DR64_CUBE.dense_bytes(), 32 * 1024); - assert_eq!(Extent::DR256_SQ_X64.dense_bytes(), 512 * 1024); - - let node = |id: u32| MethodBinding { +fn node(id: u32) -> MethodBinding { + MethodBinding { id: MethodId(id), when: None, on: WideFieldMask::from_positions(&[]), reads: WideFieldMask::from_positions(&[]), ..method(id, vec![]) - }; + } +} + +const ONNX_BIG: u32 = 0x7F03_0002; +const JINA: u32 = 0x7F03_0003; + +/// A consumer reserves what it needs per axis, in DR64 or DR256 tiles, so +/// nothing caps it at 64: 200 fields need a 256-wide property axis, and 70 +/// methods need a predicate axis wider than one 64 tile. The reservation is +/// part of the immutable binding. +#[test] +fn a_consumer_reserves_its_extent_and_is_not_capped_at_64() { let mut s = catalogue(); assert_eq!( s.register(ONNX, schema(1, 200), vec![node(1)]), Err(Refusal::FieldsExceedExtent { class: ONNX, field_count: 200, - property: Width::W64 + reserved: 64 }) ); assert_eq!( s.register(ONNX, schema_in(1, 200, Extent::DR256_SQ_X64), vec![node(1)]), - Ok(()), - "200 values fit the 256-wide property axis" + Ok(()) ); let nodes: Vec = (1..=70).map(node).collect(); @@ -874,19 +877,90 @@ fn a_consumer_chooses_its_extent_and_is_not_capped_at_64() { Err(Refusal::MethodsExceedExtent { class: ONNX_BIG, methods: 70, - predicate: Width::W64 + reserved: 64 }) ); - let wide = Extent { - predicate: Width::W256, + let two_predicate_tiles = Extent { + predicate: Axis::new(Width::W64, 2), ..Extent::DR256_SQ_X64 }; - assert_eq!(s.register(ONNX_BIG, schema_in(1, 200, wide), nodes), Ok(())); - // The extent is part of the binding, so it cannot be changed later. assert_eq!( - s.register(ONNX, schema_in(1, 200, wide), vec![node(1)]), - Err(Refusal::BehaviourImmutable(ONNX)) + s.register(ONNX_BIG, schema_in(1, 200, two_predicate_tiles), nodes), + Ok(()) + ); + assert_eq!( + s.register(ONNX, schema_in(1, 200, two_predicate_tiles), vec![node(1)]), + Err(Refusal::BehaviourImmutable(ONNX)), + "the reservation cannot be changed after binding" + ); + // An axis wider than it can address is refused, not truncated. + let too_wide = Extent { + property: Axis::new(Width::W256, 2), + ..Extent::DR256_SQ_X64 + }; + assert_eq!( + s.register(JINA, schema_in(1, 4, too_wide), vec![]), + Err(Refusal::ReservationTooWide { + class: JINA, + axis: "property", + reserved: 512 + }) ); } -const ONNX_BIG: u32 = 0x7F03_0002; +/// Thin provisioning: Jina's 7,620 nodes are identity positions. Reserving +/// the whole DR256 identity space claims nothing; routing the 7,620 nodes +/// claims the 30 tiles of 256 they fall in, and the cost is stated for those +/// 30 tiles, not for the 256 reserved. A one-tile reservation refuses node +/// 7,619 instead of silently dropping it. +#[test] +fn jina_reserves_the_dr256_identity_space_and_claims_only_routed_tiles() { + const NODES: u16 = 7_620; + let mut s = catalogue(); + s.register( + JINA, + schema_in(1, 4, Extent::DR256_GLOBAL_X64), + vec![node(1)], + ) + .unwrap(); + assert_eq!(Extent::DR256_GLOBAL_X64.identity.reserved(), 65_536); + let none = s.claimed(JINA, s.current_version()).unwrap(); + assert_eq!( + (none.identity_tiles, none.bytes), + (0, 0), + "reserving claims nothing" + ); + + for position in 0..NODES { + s.route(position, JINA).unwrap(); + } + let claim = s.claimed(JINA, s.current_version()).unwrap(); + assert_eq!(claim.identity_tiles, 30, "ceil(7,620 / 256)"); + // One claimed tile: 256 identities x 256 properties x 64 predicates. + assert_eq!(Extent::DR256_GLOBAL_X64.tile_bytes(), 512 * 1024); + assert_eq!(claim.bytes, 30 * 512 * 1024); + assert!(claim.bytes < Extent::DR256_GLOBAL_X64.reserved_cells() / 8); + // Replay: before the last tile's first route, 29 tiles were claimed. + assert_eq!( + s.claimed(JINA, RouteVersion(7_424)).unwrap().identity_tiles, + 29 + ); + + // A one-tile reservation refuses a node past it, naming the reservation. + s.register(ONNX, schema_in(1, 4, Extent::DR256_SQ_X64), vec![node(1)]) + .unwrap(); + let before = s.current_version(); + assert_eq!( + s.route(NODES - 1, ONNX), + Err(Refusal::OutsideReservation { + class: ONNX, + position: NODES - 1, + reserved: 256 + }) + ); + assert_eq!( + s.current_version(), + before, + "a refused route moves no version" + ); +} diff --git a/crates/lance-graph-dt64/tests/dr.rs b/crates/lance-graph-dt64/tests/dr.rs new file mode 100644 index 000000000..c3242af74 --- /dev/null +++ b/crates/lance-graph-dt64/tests/dr.rs @@ -0,0 +1,161 @@ +//! DR addressing in `lance_graph_dt64::dr`: the frame witness and guards +//! (lifted from `dr-math-binding-probe`'s binding tests), the global → local +//! map and addressing at both tile widths, and the module boundary. + +use lance_graph_contract::morton8x8::Morton8x8; +use lance_graph_dt64::dr::{ + address, coordinates, rotate_yz, Focus, FrameRefusal, FrameRevisions, FrameStore, GuardRefusal, + Guards, LocalMap, MapRefusal, Scope, Width, +}; + +fn revisions() -> FrameRevisions { + FrameRevisions { + source: 7, + relation_map: 11, + coordinates: 3, + } +} +const FOCUS: Focus = Focus { revision: 5 }; +const SCOPE: Scope = Scope { id: 100 }; + +fn guards(store: &FrameStore) -> Guards { + Guards { + frame: store.seal(), + focus: FOCUS, + scope: SCOPE, + } +} + +/// Each moved revision is its own refusal, and a re-seal at equal numbers is +/// still a new frame. +#[test] +fn every_frame_revision_is_a_separate_refusal() { + let r = revisions(); + let cases = [ + ( + FrameRevisions { source: 8, ..r }, + FrameRefusal::SourceChanged, + ), + ( + FrameRevisions { + relation_map: 12, + ..r + }, + FrameRefusal::MapChanged, + ), + ( + FrameRevisions { + coordinates: 4, + ..r + }, + FrameRefusal::CoordinatesChanged, + ), + (r, FrameRefusal::FrameResealed), + ]; + for (moved, expected) in cases { + let mut store = FrameStore::new(r); + let frame = store.seal(); + assert_eq!(store.check(&frame), Ok(())); + store.advance(moved); + assert_eq!(store.check(&frame), Err(expected)); + } +} + +/// The seal is store-issued identity, not equality of numbers. +#[test] +fn two_stores_at_equal_revisions_do_not_alias() { + let first = FrameStore::new(revisions()); + let second = FrameStore::new(revisions()); + assert_eq!(first.revisions(), second.revisions()); + assert!(!first.seal().same_issue(&second.seal())); + assert!(first.seal().same_issue(&first.seal())); + assert_eq!(first.check(&second.seal()), Err(FrameRefusal::ForeignFrame)); +} + +/// Focus is motion and scope is policy: each is its own guard, and moving +/// either leaves the frame valid. +#[test] +fn focus_and_scope_are_guards_apart_from_the_frame() { + let store = FrameStore::new(revisions()); + let g = guards(&store); + assert_eq!( + g.check(&store, (Focus { revision: 6 }, SCOPE)), + Err(GuardRefusal::FocusChanged) + ); + assert_eq!( + g.check(&store, (FOCUS, Scope { id: 101 })), + Err(GuardRefusal::ScopeChanged) + ); + assert_eq!(store.check(&g.frame), Ok(())); + assert_eq!(g.check(&store, (FOCUS, SCOPE)), Ok(())); + let other = FrameStore::new(revisions()); + assert_eq!( + g.check(&other, (FOCUS, SCOPE)), + Err(GuardRefusal::Frame(FrameRefusal::ForeignFrame)) + ); +} + +/// The local map is per tile and takes the tile's width: a DR64 tile refuses +/// 65 identities, a DR256 tile holds 200. Lookups are counted; the inverse +/// read is not. +#[test] +fn a_local_map_holds_one_tile_of_either_width() { + let ids: Vec = (0..200).map(|i| 1_000 + 7 * i).collect(); + assert_eq!( + LocalMap::prepare(Width::W64, &ids[..65]).unwrap_err(), + MapRefusal::TooMany + ); + assert!(LocalMap::prepare(Width::W64, &ids[..64]).is_ok()); + + let map = LocalMap::prepare(Width::W256, &ids).unwrap(); + assert_eq!(map.len(), 200); + assert_eq!(map.local(1_000 + 7 * 199), Ok(199)); + assert_eq!(map.local(3), Err(MapRefusal::Unmapped)); + assert_eq!(map.lookups(), 2); + assert_eq!(map.global(199), Some(1_000 + 7 * 199)); + assert_eq!(map.lookups(), 2, "the inverse is the table read, uncounted"); + + assert_eq!( + LocalMap::prepare(Width::W256, &[5, 9, 5]).unwrap_err(), + MapRefusal::Duplicate + ); + // The digest names the assignment: same ids in the same order agree, + // another order does not. + let a = LocalMap::prepare(Width::W64, &[1, 2, 3]).unwrap(); + let b = LocalMap::prepare(Width::W64, &[1, 2, 3]).unwrap(); + let c = LocalMap::prepare(Width::W64, &[3, 2, 1]).unwrap(); + assert_eq!(a.digest(), b.digest()); + assert_ne!(a.digest(), c.digest()); +} + +/// Addressing reads through the contract's Morton code at the tile's width: +/// (200, 17) is a DR256 cell and outside a DR64 tile. Rotation swaps the +/// axes and is its own inverse. +#[test] +fn addresses_follow_the_tile_width_and_rotation_is_an_involution() { + let code = address(Width::W256, 200, 17).unwrap(); + assert_eq!(code, Morton8x8::from_xy(200, 17)); + assert_eq!(coordinates(Width::W256, code), Ok((200, 17))); + assert_eq!(address(Width::W64, 200, 17), Err(MapRefusal::OutOfTile)); + assert_eq!(coordinates(Width::W64, code), Err(MapRefusal::OutOfTile)); + assert_eq!( + address(Width::W64, 63, 63).map(|c| (c.x(), c.y())), + Ok((63, 63)) + ); + + let rotated = rotate_yz(code); + assert_eq!((rotated.x(), rotated.y()), (17, 200)); + assert_eq!(rotate_yz(rotated), code); +} + +/// "DR routes, DT interprets": the DR module reads nothing from the binding +/// side of the crate. +#[test] +fn dr_does_not_depend_on_the_binding_side() { + let src = include_str!("../src/dr.rs"); + assert!( + !src.contains("crate::"), + "dr.rs must not name the crate root" + ); + assert!(!src.contains("super::"), "dr.rs must not name its parent"); +}