diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 716696670..43bf9f653 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -70,7 +70,7 @@ Decision (operator, 2026-10-13): the contract's only dependency becomes mask-ris | D-id | scope | status | gate / falsifier | |---|---|---|---| | **D-FOLD-CORE-a** | `lance-graph-r2il` becomes a workspace member; it, `lance-graph-nal` and `r2il-mask-abi-probe` take OGAR by git `main` (the `lance-graph-ogar` coordinate) instead of `../../../OGAR` paths; the root patches the contract's git URL to the member | Shipped #1534 `0a6708b` | `cargo metadata`: one `lance-graph-contract`, one `ogar-loco`, one `ogar-r2il`; tests: r2il 19, nal 8, probe 40, all green | -| **D-FOLD-CORE-b** | `crates/lance-graph` depends on mask-risc, quack and R2IL by default | Queued | core builds and tests green with the three in the default graph | +| **D-FOLD-CORE-b** | `crates/lance-graph` depends on mask-risc, quack and R2IL by default | In PR | core builds and tests green with the three in the default graph; `tests/fold_path.rs` runs a Quack query and an R2IL count through `lance_graph::fold` against row loops (disable run: dropping one row range turns it red, 303 vs 201) | | **D-FOLD-CORE-c** | NAL as a module of `crates/lance-graph`; `crates/lance-graph-nal` removed; W2 tests moved unchanged | Queued | the 8 W2 tests pass unchanged in core | ## D-PROD-V4 — production build tiers (2026-10-13) diff --git a/crates/lance-graph/Cargo.toml b/crates/lance-graph/Cargo.toml index 3d9a35104..2a909efb3 100644 --- a/crates/lance-graph/Cargo.toml +++ b/crates/lance-graph/Cargo.toml @@ -48,6 +48,14 @@ datafusion-functions-aggregate = { workspace = true } futures = "0.3" lance-graph-catalog = { path = "../lance-graph-catalog", version = "0.5.4" } lance-graph-contract = { path = "../lance-graph-contract" } +# The fold path, as a core capability (D-FOLD-CORE-b): mask-risc is the only +# bulk execution engine; Quack and R2IL are the two frontends that lower onto +# it (Quack for set/aggregate queries, R2IL for procedures such as NAL). +# Unconditional on purpose: the polyfill is part of core, not an add-on. +# Re-exported as `lance_graph::fold`. +lance-graph-mask-risc = { path = "../lance-graph-mask-risc" } +lance-graph-quack = { path = "../lance-graph-quack" } +lance-graph-r2il = { path = "../lance-graph-r2il" } lance = { workspace = true } lance-linalg = { workspace = true } lance-namespace = "13.*" diff --git a/crates/lance-graph/src/fold.rs b/crates/lance-graph/src/fold.rs new file mode 100644 index 000000000..733f59a59 --- /dev/null +++ b/crates/lance-graph/src/fold.rs @@ -0,0 +1,15 @@ +//! The fold path: mask-risc and the two frontends that lower onto it. +//! +//! - [`mask_risc`]: the bulk execution engine. Programs over resident mask +//! planes and value lanes; compiled once, run over a row extent. +//! - [`quack`]: the set/aggregate frontend. A `Query` (filter + aggregate) +//! lowers to one mask-risc program. +//! - [`r2il`]: the procedure frontend. R2IL calls emit mask-risc programs +//! through `FoldDialect`; NAL operations are procedures over it. +//! +//! These are re-exports, not wrappers: core adds no second algebra over them. +//! A capability that is missing here is missing in one of the three crates. + +pub use lance_graph_mask_risc as mask_risc; +pub use lance_graph_quack as quack; +pub use lance_graph_r2il as r2il; diff --git a/crates/lance-graph/src/lib.rs b/crates/lance-graph/src/lib.rs index 8f6dfd978..5d90b3379 100644 --- a/crates/lance-graph/src/lib.rs +++ b/crates/lance-graph/src/lib.rs @@ -43,6 +43,7 @@ pub mod csr_index; pub mod datafusion_planner; pub mod dev_s3_env; pub mod error; +pub mod fold; pub mod graph; pub mod lance_native_planner; pub mod lance_vector_search; diff --git a/crates/lance-graph/tests/fold_path.rs b/crates/lance-graph/tests/fold_path.rs new file mode 100644 index 000000000..40c0f67fb --- /dev/null +++ b/crates/lance-graph/tests/fold_path.rs @@ -0,0 +1,80 @@ +//! D-FOLD-CORE-b: the fold path is a core capability. +//! +//! `lance-graph` reaches mask-risc, Quack and R2IL through `lance_graph::fold` +//! with no extra dependency or feature. Each test runs a query through one +//! frontend and checks it against a plain row loop over the same lane. + +use lance_graph::fold::mask_risc::{self, Foreign, LaneRef, Planes, Pred, Scratch, Value}; +use lance_graph::fold::quack::{self, Agg, Cmp, Col, Filter, Query}; +use lance_graph::fold::r2il::{procs, FoldDialect, Val}; + +/// A deterministic lane whose values cover `0..1000` in scrambled row order, +/// so a value predicate selects scattered rows, not a row range. +fn lane() -> Vec { + (0..4096i32) + .map(|i| (i.wrapping_mul(7919)) % 1000) + .collect() +} + +/// A range of ROWS (`Cmp::Range` is a row range on the ordered axis, not a +/// value range). +fn rows(lo: u32, hi: u32) -> Filter { + Filter::Cmp(Col(0), Cmp::Range { lo, hi }) +} + +/// FAILS IF: core cannot lower a Quack query and run it on mask-risc, the two +/// row ranges are not intersected, or the value predicate is not applied +/// inside the intersection. +#[test] +fn quack_through_core_equals_the_row_loop() { + let v = lane(); + let lanes = [LaneRef::I32(&v)]; + let planes = Planes { + n_rows: v.len(), + masks: &[], + lanes: &lanes, + }; + let q = Query { + filter: Filter::and([ + rows(100, 700), + rows(300, 900), + Filter::Cmp(Col(0), Cmp::GeI32(500)), + ]), + agg: Agg::Count, + }; + let program = quack::lower_fused(&q).expect("lowers"); + let mut scratch = Scratch::for_program(&program, v.len()).expect("scratch"); + let got = mask_risc::execute(&program, &planes, &mut scratch, None).expect("runs"); + + let want = (300..700).filter(|&r| v[r] >= 500).count(); + // Anti-vacuity: the value predicate cuts inside the row intersection. + assert!(want > 0 && want < 400, "want = {want}"); + assert_eq!(got, Value::Count(want)); +} + +/// FAILS IF: core cannot run an R2IL procedure over mask-risc: a row range +/// counted through the fold dialect must equal its width. +#[test] +fn r2il_through_core_counts_a_row_range() { + let v = lane(); + let lanes = [LaneRef::I32(&v)]; + let planes = Planes { + n_rows: v.len(), + masks: &[], + lanes: &lanes, + }; + let foreign = Foreign { + planes: &[], + lanes: &[], + }; + let (lo, hi) = (130u32, 3001u32); + let mut d = FoldDialect::new(&planes, &foreign); + let mut stack: Vec = Vec::new(); + d.emit_pred(Pred::Range { lo, hi }, &mut stack); + procs::count(&mut d, &mut stack).expect("count"); + assert!(d.poison().is_none()); + match stack.as_slice() { + [Val::Scalar(n)] => assert_eq!(*n, i64::from(hi - lo)), + other => panic!("expected one scalar, got {other:?}"), + } +}