diff --git a/.claude/probes/2026-11-03-d-global-where/probe/src/bin/falsify.rs b/.claude/probes/2026-11-03-d-global-where/probe/src/bin/falsify.rs index aa1131f27..ae004e57c 100644 --- a/.claude/probes/2026-11-03-d-global-where/probe/src/bin/falsify.rs +++ b/.claude/probes/2026-11-03-d-global-where/probe/src/bin/falsify.rs @@ -690,7 +690,7 @@ fn main() { let f_other = t2.seal(); let foreign = table.map(&f_other, 0); let foreign_ok = matches!(foreign, Err(MapRead::Stale(FrameRefusal::ForeignFrame))); - let future = table.map_at(RouteVersion(table.current_version().0 + 7), 0); + let future = table.diagnose_at(RouteVersion(table.current_version().0 + 7), 0); let future_ok = matches!(future, Err(MapRead::FutureVersion { .. })); let now = table.seal(); let ambiguous = y_sh.map(|g| table.locate(&now, &[10, 11], g)); diff --git a/crates/lance-graph-dt64/src/dr.rs b/crates/lance-graph-dt64/src/dr.rs index c085ba89f..64d3b35c4 100644 --- a/crates/lance-graph-dt64/src/dr.rs +++ b/crates/lance-graph-dt64/src/dr.rs @@ -158,7 +158,7 @@ pub struct RouteVersion(pub u64); /// The one counter behind [`RouteVersion`]. A [`RouteLog`], a [`FrameStore`] /// and a [`MapTable`] built from clones of the same clock advance ONE /// sequence: a version then names a single point of the DR log, and a frame's -/// version answers `RouteLog::routed_at` as well as `MapTable::map_at`. +/// version answers `RouteLog::routed_at` as well as `MapTable::diagnose_at`. /// /// `clone` SHARES the counter (that is how two structures are joined); /// [`VersionClock::fork`] copies its present value into an independent one. @@ -797,6 +797,40 @@ impl<'t> TileMap<'t> { } } +/// A read-only view of a tile's map at a bare version number. It exposes the +/// same reads as [`TileMap`] but is a distinct type with no conversion into +/// one, so it cannot be passed to an executor (`MidMap`). Only +/// [`MapTable::map`], which checks the frame's issuer, returns an executable +/// map. +#[derive(Clone, Copy, Debug)] +pub struct MapDiagnostic<'t>(TileMap<'t>); + +impl MapDiagnostic<'_> { + pub fn width(&self) -> Width { + self.0.width() + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn local(&self, global: u64) -> Result { + self.0.local(global) + } + + pub fn global(&self, local: u8) -> Option { + self.0.global(local) + } + + pub fn digest(&self) -> u64 { + self.0.digest() + } +} + /// The versioned global → local table (D-DT64-7 O5): every tile's history of /// [`LocalMap`]s under one [`FrameStore`]. Maps change only through /// [`MapTable::advance`], which appends: an extension is a length at a @@ -922,10 +956,24 @@ impl MapTable { self.view(version, tile) } - /// The map of `tile` as of a numeric `version` (for replay where only the - /// number survives). A version past the end of the sequence is - /// `FutureVersion`. - pub fn map_at(&self, version: RouteVersion, tile: TileId) -> Result, MapRead> { + /// A read-only diagnostic of `tile` as of a numeric `version`. A bare + /// number carries no issuer, so a frame of another table at an equal + /// version would be answered with THIS table's layout; the result is + /// therefore a [`MapDiagnostic`], not an executable [`TileMap`]. Execute + /// only through [`MapTable::map`]. A version past the end of the sequence + /// is `FutureVersion`. + /// + /// ```compile_fail + /// use lance_graph_dt64::dr::{MapTable, RouteVersion, TileMap}; + /// fn executes(_: TileMap<'_>) {} + /// let t = MapTable::new(1, 1); + /// executes(t.diagnose_at(RouteVersion(0), 1).unwrap()); + /// ``` + pub fn diagnose_at( + &self, + version: RouteVersion, + tile: TileId, + ) -> Result, MapRead> { let current = self.frames.clock.current(); if version > current { return Err(MapRead::FutureVersion { @@ -933,7 +981,7 @@ impl MapTable { current, }); } - self.view(version, tile) + self.view(version, tile).map(MapDiagnostic) } fn view(&self, version: RouteVersion, tile: TileId) -> Result, MapRead> { diff --git a/crates/lance-graph-dt64/tests/dr.rs b/crates/lance-graph-dt64/tests/dr.rs index 8df6081c8..662da83b0 100644 --- a/crates/lance-graph-dt64/tests/dr.rs +++ b/crates/lance-graph-dt64/tests/dr.rs @@ -402,8 +402,8 @@ mod vmap { assert_ne!(v1[0].1, v2[0].1); assert_ne!(v1[2].1, v2[2].1); // The numeric read agrees with the frame read. - assert_eq!(t.map_at(f1.version(), 1).unwrap().local(10), Ok(0)); - assert_eq!(t.map_at(f2.version(), 1).unwrap().local(10), Ok(2)); + assert_eq!(t.diagnose_at(f1.version(), 1).unwrap().local(10), Ok(0)); + assert_eq!(t.diagnose_at(f2.version(), 1).unwrap().local(10), Ok(2)); } /// F2 extension is a length at a version. Fails (disable: drop the @@ -555,23 +555,48 @@ mod vmap { /// Q2: a version past the end is a typed refusal, not a clamp. #[test] - fn map_at_refuses_a_version_past_the_end() { + fn diagnose_at_refuses_a_version_past_the_end() { let mut t = MapTable::new(1, 1); t.advance(1, 1, vec![(1, Some(map(&[10])))]); let cur = t.current_version(); - assert!(t.map_at(cur, 1).is_ok()); + assert!(t.diagnose_at(cur, 1).is_ok()); let future = RouteVersion(cur.0 + 1); assert_eq!( - t.map_at(future, 1).unwrap_err(), + t.diagnose_at(future, 1).unwrap_err(), MapRead::FutureVersion { asked: future, current: cur } ); assert_eq!( - t.map_at(RouteVersion(0), 1).unwrap_err(), + t.diagnose_at(RouteVersion(0), 1).unwrap_err(), MapRead::NoSuchTile(1), "before the tile existed" ); } + + /// P9 (E1): a foreign frame's version, read numerically, must not yield an + /// executable map. `map(frame)` refuses the frame; `diagnose_at` can still + /// show the number's layout in THIS table, but only as a `MapDiagnostic`, + /// which is not a `TileMap` and so cannot reach `MidMap` executors. + #[test] + fn p9_equal_version_foreign_frame_is_not_executable() { + let mut a = MapTable::new(1, 1); + let mut b = MapTable::new(1, 1); + a.advance(1, 1, vec![(1, Some(map(&[10, 20])))]); + b.advance(1, 1, vec![(1, Some(map(&[20, 10])))]); + let fb = b.seal(); + assert_eq!(fb.version(), a.seal().version(), "equal versions"); + // The executable read refuses the foreign frame. + assert_eq!( + a.map(&fb, 1).unwrap_err(), + MapRead::Stale(FrameRefusal::ForeignFrame) + ); + // The issuer answers differently from A's layout at that number: + // the numeric read in A is a different layout than B's frame means. + assert_eq!(b.map(&fb, 1).unwrap().local(10), Ok(1)); + let d = a.diagnose_at(fb.version(), 1).unwrap(); + assert_eq!(d.local(10), Ok(0), "A's layout, not the frame's"); + assert_ne!(d.digest(), b.map(&fb, 1).unwrap().digest()); + } }