From e6a956e85ea6251ebd80d13cc3384c8348e6a84e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 13:33:40 +0000 Subject: [PATCH 1/3] lance-graph-report: CoordSpec::MaskSet, a set coordinate over resident masks A many-to-many axis (paperless-ngx tags) had no coordinate: a report coordinate held one ordinal per row, so "count per tag" was one scalar plan per tag. MaskSet { base, count } makes member m the resident mask base + m. A row lands in every member holding it, so the dimension's cells do not sum to the population. - Planned as Provider::MaskPlanes: each member is a mask plane read in place, always a partition, never the fold key. - UnknownMask for a missing member; new EmptyMaskSet for count == 0. - CoordSpec::field() returns Option; explain and render name the set. - The test oracle places a row in the cartesian product of its per-dimension memberships. tests/mask_set.rs (6) checks against the oracle, dense and sparse, alone and crossed with an ordinal key. Three disable runs go red. Board: entries/2026-09-25-report-mask-set-coordinate.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DCEP2fZdHYdMCtcEVcTpS2 --- .../2026-09-25-report-mask-set-coordinate.md | 16 ++ .claude/board/entries/README.md | 1 + crates/lance-graph-report/src/exec.rs | 54 ++++-- crates/lance-graph-report/src/explain.rs | 17 +- crates/lance-graph-report/src/lib.rs | 2 + crates/lance-graph-report/src/plan.rs | 34 +++- crates/lance-graph-report/src/render.rs | 3 + crates/lance-graph-report/tests/common/mod.rs | 49 ++++- crates/lance-graph-report/tests/mask_set.rs | 177 ++++++++++++++++++ 9 files changed, 328 insertions(+), 25 deletions(-) create mode 100644 .claude/board/entries/2026-09-25-report-mask-set-coordinate.md create mode 100644 crates/lance-graph-report/tests/mask_set.rs diff --git a/.claude/board/entries/2026-09-25-report-mask-set-coordinate.md b/.claude/board/entries/2026-09-25-report-mask-set-coordinate.md new file mode 100644 index 000000000..13385b6ed --- /dev/null +++ b/.claude/board/entries/2026-09-25-report-mask-set-coordinate.md @@ -0,0 +1,16 @@ +# 2026-09-25 — A SET coordinate for the report substrate: `CoordSpec::MaskSet` + +**Status:** TEST-PINNED · OPEN (one pass per member, not one keyed pass) + +## What landed +- `CoordSpec::MaskSet { base, count }`: member `m` is the resident mask `base + m`. It is the coordinate of a many-to-many axis — paperless-ngx's tags, the gap `tesseract-paperless::axes` named ("count per tag is one scalar fold per tag, not one pivot"). +- A row lands in every member whose mask holds it (zero, one or several), so the dimension's cells do not sum to the selected population. The per-row test oracle (`tests/common`) now places a row in the cartesian product of its per-dimension memberships. +- Planned as `Provider::MaskPlanes`: each member is one mask plane read in place, always a partition, never the fold key. A missing member mask is `UnknownMask`; `count == 0` is the new `EmptyMaskSet`. +- `CoordSpec::field()` now returns `Option` (a mask set reads no lane). + +## Evidence +- `tests/mask_set.rs` (6): alone, crossed with an ordinal fold key (dense and sparse), under a selection — all against the oracle; per-tag counts equal the (row, tag) membership count and exceed the tagged rows; missing mask refused; empty set refused; explain names the set. The fixture asserts it contains untagged rows and rows with ≥ 2 tags. +- Disable runs, each red then restored: member filter reading the validity plane (3 tests fail); empty-set guard removed; missing mask read as the validity plane. + +## Open +- Cost is one population pass per member (× the other partitions), exactly as before — the win is one plan, one result space and one render, not fewer passes. A keyed multi-membership fold (one pass for all members) would need a mask-RISC aggregation that does not exist. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 59e082182..0dbc1a09b 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -34,6 +34,7 @@ exactly one of them, never both. | 2026-09-25 | `tern3-six-view-window` | | [2026-09-25-tern3-six-view-window.md](2026-09-25-tern3-six-view-window.md) | | 2026-09-25 | `tern2-two-level-lowering` | | [2026-09-25-tern2-two-level-lowering.md](2026-09-25-tern2-two-level-lowering.md) | | 2026-09-25 | `strided-field-views-close-the-mask-risc-ir-gap` | | [2026-09-25-strided-field-views-close-the-mask-risc-ir-gap.md](2026-09-25-strided-field-views-close-the-mask-risc-ir-gap.md) | +| 2026-09-25 | `report-mask-set-coordinate` | | [2026-09-25-report-mask-set-coordinate.md](2026-09-25-report-mask-set-coordinate.md) | | 2026-09-25 | `llvm-whole-stack-fold-ceiling` | | [2026-09-25-llvm-whole-stack-fold-ceiling.md](2026-09-25-llvm-whole-stack-fold-ceiling.md) | | 2026-09-25 | `lance12-lancedb039-sweep` | | [2026-09-25-lance12-lancedb039-sweep.md](2026-09-25-lance12-lancedb039-sweep.md) | | 2026-09-25 | `keep-fold` | | [2026-09-25-keep-fold.md](2026-09-25-keep-fold.md) | diff --git a/crates/lance-graph-report/src/exec.rs b/crates/lance-graph-report/src/exec.rs index 7a385f28c..50880af88 100644 --- a/crates/lance-graph-report/src/exec.rs +++ b/crates/lance-graph-report/src/exec.rs @@ -101,6 +101,12 @@ pub enum Provider { /// Lane index of the bucketed field. lane: u16, }, + /// A set coordinate: member `m` is the resident mask plane `planes[m]`, + /// read in place. Never the fold key — a row may sit in several members. + MaskPlanes { + /// Plane index of each member's mask. + planes: Vec, + }, } /// One canonical dimension as planned. @@ -232,8 +238,19 @@ fn member_filter(d: &DimPlan, m: u32) -> Filter { _ => Filter::And(parts), } } - (Provider::DerivedBucket { .. }, CoordSpec::Field(_)) => { - unreachable!("bucket provider on a field") + (Provider::MaskPlanes { planes }, _) => Filter::Plane(Mask(planes[m as usize])), + (Provider::DerivedBucket { .. }, _) => { + unreachable!("bucket provider on a non-bucket coordinate") + } + } +} + +/// The lane of the fold key. Only an ordinal lane is ever chosen as the key. +fn fold_key_lane(d: &DimPlan) -> u16 { + match d.provider { + Provider::OrdinalLane { lane } => lane, + Provider::DerivedBucket { .. } | Provider::MaskPlanes { .. } => { + unreachable!("the fold key is always an ordinal lane") } } } @@ -245,7 +262,24 @@ fn resolve( ) -> Result { let mut dims = Vec::with_capacity(key.coords.len()); for c in &key.coords { - let f = c.field(); + if let CoordSpec::MaskSet { base, count } = c { + if *count == 0 { + return Err(ReportError::EmptyMaskSet(*base)); + } + let planes = (0..*count) + .map(|m| { + let id = c.member_mask(m).ok_or(ReportError::TooManyPlanes)?; + batch.plane_of(id).ok_or(ReportError::UnknownMask(id)) + }) + .collect::, _>>()?; + dims.push(DimPlan { + coord: c.clone(), + domain: *count, + provider: Provider::MaskPlanes { planes }, + }); + continue; + } + let f = c.field().expect("a lane coordinate names a field"); let (lane, col) = batch.column(f).ok_or(ReportError::UnknownField(f))?; let dim = match c { CoordSpec::Field(_) => DimPlan { @@ -263,6 +297,7 @@ fn resolve( provider: Provider::DerivedBucket { lane }, } } + CoordSpec::MaskSet { .. } => unreachable!("handled above"), }; dims.push(dim); } @@ -462,10 +497,7 @@ impl ReportPlan { } else { base }; - let key_lane = r.fold_key.map(|i| match r.dims[i].provider { - Provider::OrdinalLane { lane } => lane, - Provider::DerivedBucket { lane } => lane, - }); + let key_lane = r.fold_key.map(|i| fold_key_lane(&r.dims[i])); let first_pass = r .states .iter() @@ -555,9 +587,7 @@ impl ReportPlan { lanes: &lanes, }; - let key_lane = r.fold_key.map(|i| match r.dims[i].provider { - Provider::OrdinalLane { lane } | Provider::DerivedBucket { lane } => lane, - }); + let key_lane = r.fold_key.map(|i| fold_key_lane(&r.dims[i])); let key_domain = r.fold_key.map_or(1, |i| r.dims[i].domain as usize); // One program run: lowers, sizes branch-private scratch, executes. @@ -717,7 +747,9 @@ impl ReportPlan { stats.tile_mask_ops += prog.ops.len() as u64; radices.push((0..d.domain).filter(|&m| buf[m as usize] > 0).collect()); } - Provider::DerivedBucket { .. } => radices.push((0..d.domain).collect()), + Provider::DerivedBucket { .. } | Provider::MaskPlanes { .. } => { + radices.push((0..d.domain).collect()); + } } } let passes: u128 = radices.iter().map(|r| r.len() as u128).product(); diff --git a/crates/lance-graph-report/src/explain.rs b/crates/lance-graph-report/src/explain.rs index ac0cbbf9e..dcec09c39 100644 --- a/crates/lance-graph-report/src/explain.rs +++ b/crates/lance-graph-report/src/explain.rs @@ -48,6 +48,12 @@ impl fmt::Display for PhysicalPlan { format!("derived bucket (origin {origin}, width {width}; not materialized)") } (Provider::DerivedBucket { .. }, _) => "derived".to_string(), + (Provider::MaskPlanes { planes }, _) => { + format!( + "mask set ({} resident masks; a row may sit in several)", + planes.len() + ) + } }; let role = if self.fold_key == Some(i) { "fold key" @@ -57,7 +63,7 @@ impl fmt::Display for PhysicalPlan { writeln!( f, " {} {prov} · domain {} · {role}", - d.coord.field(), + coord_name(&d.coord), d.domain )?; } @@ -85,3 +91,12 @@ impl fmt::Display for PhysicalPlan { write!(f, "Materialization: terminal only") } } + +fn coord_name(c: &CoordSpec) -> String { + match c { + CoordSpec::MaskSet { base, count } => { + format!("{base}..M{}", u64::from(base.0) + u64::from(*count)) + } + _ => c.field().map_or_else(String::new, |f| f.to_string()), + } +} diff --git a/crates/lance-graph-report/src/lib.rs b/crates/lance-graph-report/src/lib.rs index 5566aa6bd..06e07c6df 100644 --- a/crates/lance-graph-report/src/lib.rs +++ b/crates/lance-graph-report/src/lib.rs @@ -102,6 +102,8 @@ pub enum ReportError { NotACoordinate(FieldId), /// A bucket coordinate over a non-`I32` field or with width <= 0. BadBucket(FieldId), + /// A mask-set coordinate with no members. + EmptyMaskSet(MaskId), /// A measure over a non-`I32` field. NotAMeasure(FieldId), /// The plan was minted against another source or generation. diff --git a/crates/lance-graph-report/src/plan.rs b/crates/lance-graph-report/src/plan.rs index 3d0679a2a..ac2bc8efa 100644 --- a/crates/lance-graph-report/src/plan.rs +++ b/crates/lance-graph-report/src/plan.rs @@ -13,7 +13,7 @@ //! computed for one is reinterpreted for the other without a rescan //! (falsifiers F13 / F14). -use crate::ids::{FieldId, SourceId}; +use crate::ids::{FieldId, MaskId, SourceId}; use crate::selection::Selection; /// The role an axis plays in the presented coordinate system. @@ -29,7 +29,7 @@ pub enum AxisRole { /// A coordinate provider — WHERE in the aggregate space a row lands. /// -/// Two providers, and neither knows what it measures: +/// Three providers, and none knows what it measures: /// /// * [`CoordSpec::Field`] — a resident `U32` code lane with a declared domain. /// It can serve as the fold KEY (the one dimension a single substrate pass @@ -39,6 +39,12 @@ pub enum AxisRole { /// a population lane: each member is a pair of tile-evaluated range /// predicates (`origin + b·width <= v < origin + (b+1)·width`) applied /// during the fold. A value outside every bucket lies outside the domain. +/// * [`CoordSpec::MaskSet`] — a SET coordinate: member `m` is the resident +/// mask `base + m`. It is the coordinate of a many-to-many axis (a document +/// carries several tags). A row lands in EVERY member whose mask holds it, +/// so the cells of this dimension do not sum to the selected population — +/// a row in two tags counts once in each, and a row in none lands nowhere. +/// Each member is one mask plane read in place; it is never the fold key. #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum CoordSpec { /// A resident ordinal field. @@ -54,13 +60,31 @@ pub enum CoordSpec { /// Number of buckets. count: u32, }, + /// A set coordinate over `count` resident masks `base .. base + count`. + MaskSet { + /// The mask of member 0. + base: MaskId, + /// Number of members. + count: u32, + }, } impl CoordSpec { - /// The field this coordinate reads. - pub fn field(&self) -> FieldId { + /// The field this coordinate reads, or `None` for a [`CoordSpec::MaskSet`], + /// which reads masks rather than a lane. + pub fn field(&self) -> Option { + match self { + CoordSpec::Field(f) | CoordSpec::Bucket { field: f, .. } => Some(*f), + CoordSpec::MaskSet { .. } => None, + } + } + + /// The mask of member `m` of a [`CoordSpec::MaskSet`], or `None` for a + /// lane coordinate or a member past the set. + pub fn member_mask(&self, m: u32) -> Option { match self { - CoordSpec::Field(f) | CoordSpec::Bucket { field: f, .. } => *f, + CoordSpec::MaskSet { base, count } if m < *count => base.0.checked_add(m).map(MaskId), + _ => None, } } } diff --git a/crates/lance-graph-report/src/render.rs b/crates/lance-graph-report/src/render.rs index 2f1885fed..d3f7c4ebd 100644 --- a/crates/lance-graph-report/src/render.rs +++ b/crates/lance-graph-report/src/render.rs @@ -138,6 +138,9 @@ impl Terminal<'_> { let lo = origin + width * i64::from(m); format!("[{lo},{})", lo + width) } + CoordSpec::MaskSet { .. } => c + .member_mask(m) + .map_or_else(|| m.to_string(), |id| id.to_string()), } } diff --git a/crates/lance-graph-report/tests/common/mod.rs b/crates/lance-graph-report/tests/common/mod.rs index fdf5f3df0..6ce12da85 100644 --- a/crates/lance-graph-report/tests/common/mod.rs +++ b/crates/lance-graph-report/tests/common/mod.rs @@ -122,10 +122,44 @@ impl Fixture { let b = (v - origin).div_euclid(*width); (0..i64::from(*count)).contains(&b).then_some(b as u32) } + CoordSpec::MaskSet { .. } => { + panic!("a mask set places a row in several cells; use `cells`") + } }) .collect() } + fn in_mask(&self, m: MaskId, i: usize) -> bool { + let w = &self.masks.iter().find(|(x, _)| *x == m).unwrap().1; + w[i / 64] >> (i % 64) & 1 == 1 + } + + /// Every canonical coordinate row `i` lands on: one per lane dimension, + /// and one per member of each mask set holding the row (so zero or + /// several). The cartesian product of the per-dimension choices. + pub fn cells(&self, coords: &[CoordSpec], i: usize) -> Vec> { + let mut out: Vec> = vec![Vec::new()]; + for c in coords { + let members: Vec = match c { + CoordSpec::MaskSet { count, .. } => (0..*count) + .filter(|&m| self.in_mask(c.member_mask(m).unwrap(), i)) + .collect(), + _ => self.coord(std::slice::from_ref(c), i).unwrap_or_default(), + }; + out = out + .into_iter() + .flat_map(|prefix| { + members.iter().map(move |&m| { + let mut p = prefix.clone(); + p.push(m); + p + }) + }) + .collect(); + } + out + } + /// Oracle cells: canonical coordinate → (count, sum, min, max) of `m`. pub fn oracle( &self, @@ -138,15 +172,14 @@ impl Fixture { if !self.selected(&plan.selection, i) { continue; } - let Some(c) = self.coord(&coords, i) else { - continue; - }; let v = m.map_or(0, |f| i64::from(self.val(f).unwrap()[i])); - let e = out.entry(c).or_insert((0, 0, i64::MAX, i64::MIN)); - e.0 += 1; - e.1 += v; - e.2 = e.2.min(v); - e.3 = e.3.max(v); + for c in self.cells(&coords, i) { + let e = out.entry(c).or_insert((0, 0, i64::MAX, i64::MIN)); + e.0 += 1; + e.1 += v; + e.2 = e.2.min(v); + e.3 = e.3.max(v); + } } out } diff --git a/crates/lance-graph-report/tests/mask_set.rs b/crates/lance-graph-report/tests/mask_set.rs new file mode 100644 index 000000000..b7ad8688e --- /dev/null +++ b/crates/lance-graph-report/tests/mask_set.rs @@ -0,0 +1,177 @@ +//! The SET coordinate: a many-to-many axis whose members are resident masks. +//! +//! A row lands in every member whose mask holds it — zero, one or several — +//! so this dimension's cells do not sum to the selected population. Every +//! plan here is checked against the per-row oracle, which places a row in the +//! cartesian product of its per-dimension memberships. + +mod common; + +use common::*; +use lance_graph_report::*; + +const A: FieldId = FieldId(0); +const V: FieldId = FieldId(100); +const BASE: MaskId = MaskId(10); +const TAGS: u32 = 5; + +fn tags() -> CoordSpec { + CoordSpec::MaskSet { + base: BASE, + count: TAGS, + } +} + +/// `n` rows, an ordinal field A (domain 3), a value field, and `TAGS` masks +/// where each row carries each tag with probability 1/3 — so rows with no +/// tag and rows with several both occur. +fn tagged(n: usize, seed: u64) -> Fixture { + let mut fx = synthetic(n, &[3], seed); + let mut r = Rng(seed.wrapping_mul(7) | 1); + let words = n.div_ceil(64); + for t in 0..TAGS { + let mut w = vec![0u64; words]; + for i in 0..n { + if r.below(3) == 0 { + w[i / 64] |= 1 << (i % 64); + } + } + fx.masks.push((MaskId(BASE.0 + t), w)); + } + fx +} + +fn membership(fx: &Fixture, i: usize) -> usize { + (0..TAGS) + .filter(|&t| { + let w = &fx.masks.iter().find(|(m, _)| m.0 == BASE.0 + t).unwrap().1; + w[i / 64] >> (i % 64) & 1 == 1 + }) + .count() +} + +/// The fixture really exercises multi-membership: some rows carry no tag and +/// some carry several. Without both, the tests below prove nothing about sets. +fn assert_fixture_is_a_set(fx: &Fixture) { + let counts: Vec = (0..fx.n).map(|i| membership(fx, i)).collect(); + assert!(counts.contains(&0), "some row must carry no tag"); + assert!( + counts.iter().any(|&c| c >= 2), + "some row must carry several tags" + ); +} + +#[test] +fn a_tag_set_alone_counts_each_row_in_every_tag_it_carries() { + let fx = tagged(3_000, 21); + assert_fixture_is_a_set(&fx); + let batch = fx.batch(); + let plan = with_four(ReportPlan::over(src()).axis(tags(), AxisRole::Row), V); + let (res, _) = plan.execute(&batch, &PlannerPolicy::default()).unwrap(); + assert_matches_oracle(&fx, &plan, &res, V); + + // The set semantics, stated as numbers: the per-tag counts add up to the + // number of (row, tag) memberships, which exceeds the tagged rows. + let m = &res.measures()[0]; + let per_tag: i64 = (0..TAGS) + .map(|t| match res.value(m, &[], &[t], &[]) { + CellValue::Int(c) => c, + other => panic!("tag {t}: {other:?}"), + }) + .sum(); + let memberships: usize = (0..fx.n).map(|i| membership(&fx, i)).sum(); + let tagged_rows = (0..fx.n).filter(|&i| membership(&fx, i) > 0).count(); + assert_eq!(per_tag, memberships as i64); + assert!( + per_tag > tagged_rows as i64, + "a row in two tags counts twice" + ); +} + +#[test] +fn a_tag_set_crossed_with_an_ordinal_field_matches_the_oracle_dense_and_sparse() { + let fx = tagged(4_000, 33); + assert_fixture_is_a_set(&fx); + let batch = fx.batch(); + let plan = with_four( + ReportPlan::over(src()).pivot(&[tags()], &[CoordSpec::Field(A)]), + V, + ); + let dense = PlannerPolicy::default(); + let sparse = PlannerPolicy { + dense_cell_budget: 1, + ..PlannerPolicy::default() + }; + for policy in [dense, sparse] { + let pp = plan.explain(&batch, &policy).unwrap(); + // The ordinal field is the fold key; the set is never chosen for it. + let key = pp.fold_key.expect("the ordinal field keys the fold"); + assert_eq!(pp.dims[key].coord, CoordSpec::Field(A)); + let (res, st) = plan.execute(&batch, &policy).unwrap(); + assert_eq!(res.space().is_sparse(), policy == sparse); + assert_matches_oracle(&fx, &plan, &res, V); + assert!(st.population_scans >= u64::from(TAGS)); + } +} + +#[test] +fn a_selection_narrows_every_tag_cell() { + let fx = tagged(3_000, 45); + let batch = fx.batch(); + let plan = with_four( + ReportPlan::over(src()) + .filter(Selection::cmp(A, CmpOp::Eq, Scalar::Ordinal(1))) + .axis(tags(), AxisRole::Row), + V, + ); + let (res, _) = plan.execute(&batch, &PlannerPolicy::default()).unwrap(); + assert_matches_oracle(&fx, &plan, &res, V); +} + +#[test] +fn a_missing_tag_mask_is_refused_not_read_as_empty() { + let mut fx = tagged(500, 57); + fx.masks.retain(|(m, _)| m.0 != BASE.0 + 3); + let batch = fx.batch(); + let plan = ReportPlan::over(src()) + .axis(tags(), AxisRole::Row) + .measure(Measure::count()); + assert_eq!( + plan.execute(&batch, &PlannerPolicy::default()).unwrap_err(), + ReportError::UnknownMask(MaskId(BASE.0 + 3)) + ); +} + +#[test] +fn an_empty_tag_set_is_refused() { + let fx = tagged(500, 69); + let batch = fx.batch(); + let plan = ReportPlan::over(src()) + .axis( + CoordSpec::MaskSet { + base: BASE, + count: 0, + }, + AxisRole::Row, + ) + .measure(Measure::count()); + assert_eq!( + plan.explain(&batch, &PlannerPolicy::default()).unwrap_err(), + ReportError::EmptyMaskSet(BASE) + ); +} + +#[test] +fn explain_names_the_set_and_its_members() { + let fx = tagged(500, 81); + let batch = fx.batch(); + let plan = ReportPlan::over(src()) + .axis(tags(), AxisRole::Row) + .measure(Measure::count()); + let text = plan + .explain(&batch, &PlannerPolicy::default()) + .unwrap() + .to_string(); + assert!(text.contains("M10..M15"), "{text}"); + assert!(text.contains("mask set (5 resident masks"), "{text}"); +} From da9b9754b78a082afb593855195429dc122690f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 13:42:32 +0000 Subject: [PATCH 2/3] lance-graph-report: refuse mask planes past u16, resolve a mask set in one pass AbiBatch::plane_of cast a mask's position with `as u16`, so the 65,537th plane wrapped onto plane 0 (validity) and a MaskSet member there counted every valid row. Plane resolution is now checked (`resolve_plane`, used by selection masks) and returns TooManyPlanes instead of wrapping. A MaskSet resolves every member through one id->position table instead of a scan per member, which made planning quadratic in the set size. Tests build validity + 65,536 masks; each check disable-verified red. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DCEP2fZdHYdMCtcEVcTpS2 --- crates/lance-graph-report/src/batch.rs | 101 ++++++++++++++++++++- crates/lance-graph-report/src/exec.rs | 6 +- crates/lance-graph-report/src/selection.rs | 4 +- 3 files changed, 104 insertions(+), 7 deletions(-) diff --git a/crates/lance-graph-report/src/batch.rs b/crates/lance-graph-report/src/batch.rs index 1670c2741..f083d3218 100644 --- a/crates/lance-graph-report/src/batch.rs +++ b/crates/lance-graph-report/src/batch.rs @@ -13,11 +13,13 @@ //! Names and labels are not stored here at all — they live in the catalog and //! the CAM label store at the boundary. +use std::collections::HashMap; use std::sync::Arc; use lance_graph_mask_risc::{words_for, LaneRef}; use crate::ids::{FieldId, MaskId, SourceId}; +use crate::ReportError; /// One resident fixed-width lane. #[derive(Debug, Clone)] @@ -209,12 +211,34 @@ impl AbiBatch { .map(|(i, c)| (i as u16, c)) } - /// Plane index of a resident mask. + /// Plane index of a resident mask, or `None` when no resident mask has + /// `id`. A mask past plane `u16::MAX` has no plane index and also reads + /// as `None`; [`Self::resolve_plane`] tells the two apart. pub fn plane_of(&self, id: MaskId) -> Option { - self.masks + self.resolve_plane(id).ok() + } + + /// Plane index of a resident mask. Refuses an unknown id, and refuses a + /// mask whose position does not fit a plane index rather than wrapping + /// onto another plane. + pub(crate) fn resolve_plane(&self, id: MaskId) -> Result { + let i = self + .masks .iter() .position(|(m, _)| *m == id) - .map(|i| i as u16) + .ok_or(ReportError::UnknownMask(id))?; + u16::try_from(i).map_err(|_| ReportError::TooManyPlanes) + } + + /// Every resident mask's position, built in one pass, for resolving many + /// ids at once without a scan per id. Positions are unchecked; convert + /// with `u16::try_from` at the point of use. + pub(crate) fn mask_positions(&self) -> HashMap { + self.masks + .iter() + .enumerate() + .map(|(i, (m, _))| (*m, i)) + .collect() } /// Borrowed plane views for the evaluator: O(columns + masks) pointers. @@ -239,3 +263,74 @@ impl AbiBatch { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{AxisRole, CoordSpec, Measure, PlannerPolicy, ReportPlan, SourceRef}; + + /// A one-row batch holding validity plus `n` masks `M1..=Mn`. The masks + /// are pushed directly: `with_mask`'s duplicate check is a scan per call, + /// which would make building 65,536 of them quadratic. + fn wide(n: u32) -> AbiBatch { + let mut b = AbiBatch::new(SourceId(1), 1, 1); + let words: Arc<[u64]> = vec![0u64].into(); + b.masks + .extend((1..=n).map(|i| (MaskId(i), Arc::clone(&words)))); + b + } + + #[test] + fn a_mask_past_plane_u16_max_is_refused_not_wrapped() { + let b = wide(65_536); + // Position 65_535 is the last addressable plane. + assert_eq!(b.resolve_plane(MaskId(65_535)), Ok(65_535)); + // Position 65_536 would wrap to plane 0, the validity plane. + assert_eq!( + b.resolve_plane(MaskId(65_536)), + Err(ReportError::TooManyPlanes) + ); + assert_eq!(b.plane_of(MaskId(65_536)), None); + assert_eq!( + b.resolve_plane(MaskId(70_000)), + Err(ReportError::UnknownMask(MaskId(70_000))) + ); + } + + /// The set coordinate resolves every member, so its last member is the + /// one that would have landed on the validity plane. + #[test] + fn a_mask_set_reaching_past_plane_u16_max_is_refused() { + let b = wide(65_536); + let plan = ReportPlan::over(SourceRef { + id: SourceId(1), + generation: 1, + }) + .axis( + CoordSpec::MaskSet { + base: MaskId(1), + count: 65_536, + }, + AxisRole::Row, + ) + .measure(Measure::count()); + assert_eq!( + plan.explain(&b, &PlannerPolicy::default()).unwrap_err(), + ReportError::TooManyPlanes + ); + // One member fewer stays inside the addressable planes. + let ok = ReportPlan::over(SourceRef { + id: SourceId(1), + generation: 1, + }) + .axis( + CoordSpec::MaskSet { + base: MaskId(1), + count: 65_535, + }, + AxisRole::Row, + ) + .measure(Measure::count()); + assert!(ok.explain(&b, &PlannerPolicy::default()).is_ok()); + } +} diff --git a/crates/lance-graph-report/src/exec.rs b/crates/lance-graph-report/src/exec.rs index 50880af88..bf6fa9575 100644 --- a/crates/lance-graph-report/src/exec.rs +++ b/crates/lance-graph-report/src/exec.rs @@ -266,10 +266,14 @@ fn resolve( if *count == 0 { return Err(ReportError::EmptyMaskSet(*base)); } + // One lookup table for the whole set: a scan per member would make + // resolution quadratic in the set size. + let positions = batch.mask_positions(); let planes = (0..*count) .map(|m| { let id = c.member_mask(m).ok_or(ReportError::TooManyPlanes)?; - batch.plane_of(id).ok_or(ReportError::UnknownMask(id)) + let i = *positions.get(&id).ok_or(ReportError::UnknownMask(id))?; + u16::try_from(i).map_err(|_| ReportError::TooManyPlanes) }) .collect::, _>>()?; dims.push(DimPlan { diff --git a/crates/lance-graph-report/src/selection.rs b/crates/lance-graph-report/src/selection.rs index 9e9b82abc..eca908286 100644 --- a/crates/lance-graph-report/src/selection.rs +++ b/crates/lance-graph-report/src/selection.rs @@ -178,9 +178,7 @@ impl Selection { } Filter::Cmp(Col(0), Cmp::Range { lo: r.lo, hi: r.hi }) } - Selection::Mask(id) => Filter::Plane(Mask( - batch.plane_of(*id).ok_or(ReportError::UnknownMask(*id))?, - )), + Selection::Mask(id) => Filter::Plane(Mask(batch.resolve_plane(*id)?)), Selection::Predicate(p) => lower_predicate(p, batch)?, Selection::And(a, b) => Filter::and([a.lower(batch)?, b.lower(batch)?]), Selection::Or(a, b) => Filter::or([a.lower(batch)?, b.lower(batch)?]), From 53c820fbad021b03f73b5f99221b37331ddf07a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 13:43:01 +0000 Subject: [PATCH 3/3] board: regenerate entries index after rebase onto main Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DCEP2fZdHYdMCtcEVcTpS2 --- .claude/board/entries/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 0dbc1a09b..461e54459 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,7 +25,7 @@ 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. -171 entries, 2026-08-06 .. 2026-09-25. +173 entries, 2026-08-06 .. 2026-09-25. | date | entry id | finding | file | |---|---|---|---|