diff --git a/.claude/board/entries/2026-10-31-polyfill-positional-lens.md b/.claude/board/entries/2026-10-31-polyfill-positional-lens.md new file mode 100644 index 000000000..d4f21bda3 --- /dev/null +++ b/.claude/board/entries/2026-10-31-polyfill-positional-lens.md @@ -0,0 +1,21 @@ +# The polyfill lens `0xFFFF :: 0xF000` binds by position; R2IL, SQL and Quack names are aliases — 2026-10-31 + +**Status:** MEASURED (tests in `crates/lance-graph-r2il/tests/integration/polyfill.rs`). Implements D-DT64-7 §1a/§1b (plan on PR #1898) for the relational polyfill only. + +## What changed +- `lance_graph_r2il::polyfill`: `POLYFILL_LENS = compose_classid(0xFFFF, 0xF000)` and `POLYFILL`, 17 positions in `binding::EXECUTABLE` order. Each position carries its opcode, arity and aliases: the R2IL name first, then a SQL name where one fits, then the Quack variant. +- `binding::prepare` accepts this lens beside the R2IL fold reading. Under it, word `p` is `POLYFILL[p]`; the view's label at `p` must be an alias of `p`, and a name belonging to another position is refused (`ExecRefusal::NotAnAlias`), never followed. The R2IL fold reading keeps its label binding. + +## Measured +- R2IL, SQL and Quack views bind the same steps and the same plan id, and answer the scalar oracle. +- Methods registered by Quack names (`Cmp::LtI32`, `Cmp::EqI32`, `Filter::and`, `Filter::negate`, `Agg::{Count, Any, All, SumI32, MinI32, MaxI32, CountDistinctOrderedU32}`) answer exactly as Quack's own `lower` + mask-risc `execute` over the same lanes: 21 cases over a partial, a full and an empty selection, plus one compound filter. +- The table matches `R2ILVocabulary` (name, stack arity, one result) and the `EXECUTABLE` order; every alias names one position; the table's arity overrules a wrong runtime `declare_algebra`. +- Disable runs, each red: alias check removed (1 fail), positional path removed (4), lens not accepted (5), SUM/MIN opcodes swapped (3), a duplicate alias (1), table arity check removed (1, after adding the wrong-declaration test; it was green before because registration already ties arity). + +## What "register SQL via Quack" turned out to mean +Quack has no SQL text parser. Its SQL surface is the typed builder (`bind::table(..).where_eq(..).count()`) lowering to `Query { Filter, Agg }`. So SQL registers through Quack's variant names, and those are now aliases with a parity test against Quack's own execution. The SQL keyword aliases (`<`, `AND`, `COUNT`, `EXISTS`, ...) are recorded, but nothing in the tree parses SQL text into them. + +## Not covered (OPEN) +- Quack aggregates with no single executable word: `Rows`, `BlendI32`, `ScatterOrU32`, the `Group*` family (`GROUP_SUM` and `KEEP` write sinks and are refused by the binding). `Cmp::{Ne, Le, Gt, Ge}*`, the ternary matches, strided compares and `Range` are composable or absent, not aliased. +- SQL `NULL` over an empty selection: `SUM`/`MIN`/`MAX` answer the fold identity (`0`, `i64::MAX`, `i64::MIN`), as Quack's `Agg` does through this mapping; three-valued bindings are refused. +- The lens is still declared at runtime through `BindStore::declare_algebra`, and DT64 does not yet act as the authority for `0xFFFF` (D-DT64-7-4, O8). The opcodes are still `ogar-loco` `FnIndex` values. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 7fcb1bf4a..747048ea3 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. -533 entries, 2026-08-06 .. 2026-10-31. +534 entries, 2026-08-06 .. 2026-10-31. | date | entry id | finding | file | |---|---|---|---| | 2026-10-31 | `x2-dt64-admission-policy` | | [2026-10-31-x2-dt64-admission-policy.md](2026-10-31-x2-dt64-admission-policy.md) | | 2026-10-31 | `r1-r2il-binding-reading` | | [2026-10-31-r1-r2il-binding-reading.md](2026-10-31-r1-r2il-binding-reading.md) | | 2026-10-31 | `r1-native-prepared-successor` | | [2026-10-31-r1-native-prepared-successor.md](2026-10-31-r1-native-prepared-successor.md) | +| 2026-10-31 | `polyfill-positional-lens` | | [2026-10-31-polyfill-positional-lens.md](2026-10-31-polyfill-positional-lens.md) | | 2026-10-31 | `ogar-behavior-probe-p2-p3-p9` | | [2026-10-31-ogar-behavior-probe-p2-p3-p9.md](2026-10-31-ogar-behavior-probe-p2-p3-p9.md) | | 2026-10-31 | `D-SAG-6` | | [2026-10-31-kythe-bridge-ra-facts.md](2026-10-31-kythe-bridge-ra-facts.md) | | 2026-10-31 | `D-XLOG-NATIVE-6` | | [2026-10-31-d-xlog-native-6-ndarray-simd-execution.md](2026-10-31-d-xlog-native-6-ndarray-simd-execution.md) | diff --git a/crates/lance-graph-r2il/Cargo.toml b/crates/lance-graph-r2il/Cargo.toml index 617ad9dcd..f098ec063 100644 --- a/crates/lance-graph-r2il/Cargo.toml +++ b/crates/lance-graph-r2il/Cargo.toml @@ -28,3 +28,9 @@ ogar-r2il = { git = "https://github.com/AdaWorldAPI/OGAR", branch = "main" } # this crate executes it. Both are zero-dep beyond the contract. lance-graph-dt64 = { path = "../lance-graph-dt64" } lance-graph-contract = { path = "../lance-graph-contract" } + +[dev-dependencies] +# The polyfill alias test (`tests/integration/polyfill.rs`) registers methods +# by Quack's names and checks each answer against Quack's own lowering, run +# through mask-risc. Quack is zero-OGAR and does not depend on this crate. +lance-graph-quack = { path = "../lance-graph-quack" } diff --git a/crates/lance-graph-r2il/src/binding.rs b/crates/lance-graph-r2il/src/binding.rs index ae37cf49f..5b095de92 100644 --- a/crates/lance-graph-r2il/src/binding.rs +++ b/crates/lance-graph-r2il/src/binding.rs @@ -26,19 +26,28 @@ //! **never** read as an [`FnIndex`] because the two happen to be `u8`. //! [`prepare`] checks each effect before binding it: //! -//! 1. the lens is the one this seam executes, and its concept half is -//! OGAR's R2IL fold reading ([`ogar_r2il::CONCEPT_R2IL_FOLD`], provisional -//! in OGAR: a lab lens, never a persisted identity); +//! 1. the lens is the one this seam executes, and it is one of two readings: +//! - the hardcoded polyfill lens [`POLYFILL_LENS`] (`0xFFFF :: 0xF000`), +//! where the POSITION binds (see 3); +//! - a lens whose concept half is OGAR's R2IL fold reading +//! ([`ogar_r2il::CONCEPT_R2IL_FOLD`], provisional in OGAR: a lab lens, +//! never a persisted identity), where the LABEL binds; //! 2. the lens is bound in the store and its basis matches its `ClassView` //! at the stated schema revision ([`BindStore::resolve_algebra`]); -//! 3. the word's LABEL resolves to exactly one opcode by OGAR's own -//! vocabulary ([`R2ILVocabulary`]), so the position a lens gives a word is -//! free and only the name binds; +//! 3. the word becomes an opcode: +//! - under [`POLYFILL_LENS`], word `p` is [`POLYFILL`]`[p]`; the view's +//! label at `p` must be one of that position's aliases (an R2IL, SQL or +//! Quack name), and a label belonging to another position is refused, +//! never followed; +//! - under the R2IL fold reading, the label resolves to exactly one opcode +//! by OGAR's own vocabulary ([`R2ILVocabulary`]), so the position is +//! free and only the name binds; //! 4. the opcode is in the subset this seam translates faithfully //! ([`EXECUTABLE`]); -//! 5. OGAR's stack arity equals the effect's argument count (which DT64's -//! registration already ties to the lens's declared arity), and OGAR says -//! the opcode pushes one result. +//! 5. the opcode's stack arity equals the effect's argument count (which +//! DT64's registration already ties to the lens's declared arity) and it +//! pushes one result: by the polyfill table under [`POLYFILL_LENS`], by +//! OGAR's vocabulary under the R2IL fold reading. //! //! # What is checked when //! @@ -74,6 +83,7 @@ use lance_graph_mask_risc::{LaneKind, LaneRef, Planes}; use ogar_loco::{FnIndex, Vocabulary}; use ogar_r2il::{R2ILVocabulary, ALL, ANY, CONCEPT_R2IL_FOLD, KEY_RUNS, MAX, MIN, SUM}; +use crate::polyfill::{POLYFILL, POLYFILL_LENS}; use crate::{ Addr, FoldDialect, FoldError, Val, INT_ADD, INT_AND, INT_EQUAL, INT_MULT, INT_NOT, INT_OR, INT_SUB, INT_S_DIV, INT_S_LESS, INT_XOR, POP_COUNT, @@ -199,8 +209,16 @@ pub enum ExecRefusal { ThreeValuedNotExecutable, /// An effect names a lens other than the R2IL lens. WrongLens { effect: usize, lens: u32 }, - /// The R2IL lens's concept half is not OGAR's R2IL fold reading. + /// The lens is neither [`POLYFILL_LENS`] nor a lens whose concept half + /// is OGAR's R2IL fold reading. NotR2ilReading { lens: u32 }, + /// Under [`POLYFILL_LENS`], the view names the word with a label that is + /// not an alias of its position. + NotAnAlias { + effect: usize, + word: AlgebraRef, + label: String, + }, /// The word's position is outside the lens's basis. WordOutOfBasis { effect: usize, word: AlgebraRef }, /// The word's label is not an R2IL opcode name, or names more than one. @@ -275,7 +293,7 @@ pub fn prepare( if binding.truth == Truth::ThreeValued { return Err(ExecRefusal::ThreeValuedNotExecutable); } - if classid_concept(lens.lens) != CONCEPT_R2IL_FOLD { + if lens.lens != POLYFILL_LENS && classid_concept(lens.lens) != CONCEPT_R2IL_FOLD { return Err(ExecRefusal::NotR2ilReading { lens: lens.lens }); } let steps = binding @@ -493,6 +511,9 @@ fn opcode( })? .label .as_str(); + if lens.lens == POLYFILL_LENS { + return frozen_opcode(effect, e, label); + } let mut named = (0..=u8::MAX) .map(FnIndex) .filter(|&f| R2ILVocabulary.name(f) == Some(label)); @@ -527,6 +548,33 @@ fn opcode( Ok(f) } +/// The opcode at a polyfill position: the position binds, the view's label +/// must be one of its aliases, and the table's arity must match. +fn frozen_opcode(effect: usize, e: &Effect, label: &str) -> Result { + let word = POLYFILL + .get(usize::from(e.algebra.word)) + .ok_or(ExecRefusal::WordOutOfBasis { + effect, + word: e.algebra, + })?; + if !word.aliases.contains(&label) { + return Err(ExecRefusal::NotAnAlias { + effect, + word: e.algebra, + label: label.to_string(), + }); + } + if usize::from(word.arity) != e.args.len() { + return Err(ExecRefusal::Arity { + effect, + ogar: None, + declared: Some(word.arity), + args: e.args.len(), + }); + } + Ok(word.opcode) +} + /// The address of the resident lane field `p` is bound to. pub(crate) fn lane( planes: &Planes<'_>, diff --git a/crates/lance-graph-r2il/src/lib.rs b/crates/lance-graph-r2il/src/lib.rs index 6173078c2..c1e316680 100644 --- a/crates/lance-graph-r2il/src/lib.rs +++ b/crates/lance-graph-r2il/src/lib.rs @@ -105,6 +105,7 @@ pub mod binding; pub mod binding_native; +pub mod polyfill; pub mod procs; use std::cell::Cell; diff --git a/crates/lance-graph-r2il/src/polyfill.rs b/crates/lance-graph-r2il/src/polyfill.rs new file mode 100644 index 000000000..21751e2a7 --- /dev/null +++ b/crates/lance-graph-r2il/src/polyfill.rs @@ -0,0 +1,102 @@ +//! The hardcoded relational polyfill as a positional lens (D-DT64-7 §1a, §1b). +//! +//! Hardcoded behaviour is addressed `0xFFFF :: 0xF###`: left side `0xFFFF`, +//! right side a behaviour lens in a concern range. [`POLYFILL_LENS`] is +//! `0xFFFF :: 0xF000`, the relational / mask-risc polyfill. +//! +//! **Position binds; labels are aliases.** Word `p` of this lens is +//! [`POLYFILL`]`[p]`, whatever any view calls it. A view over the lens +//! supplies its language's names for the positions (an R2IL opcode name, a +//! SQL keyword, a Quack variant), and each name is checked against the +//! aliases fixed at that position. A name that belongs to another position, +//! or to none, is refused; it never selects a different word. +//! +//! An alias states that the word computes the same thing as the named +//! operation over a non-empty selection. Over an empty selection SQL answers +//! `NULL` for `SUM`, `MIN` and `MAX`, while this reading answers the fold's +//! identity (`0`, `i64::MAX`, `i64::MIN`); a three-valued binding is refused +//! by [`crate::binding::prepare`], so `NULL` never reaches it. +//! +//! The positions are the [`crate::binding::EXECUTABLE`] opcodes in that +//! order. The opcode, arity and R2IL name of each entry are checked against +//! OGAR's `R2ILVocabulary` by test, not at run time. + +use lance_graph_contract::ogar_codebook::compose_classid; +use ogar_loco::FnIndex; +use ogar_r2il::{ALL, ANY, KEY_RUNS, MAX, MIN, SUM}; + +use crate::{ + INT_ADD, INT_AND, INT_EQUAL, INT_MULT, INT_NOT, INT_OR, INT_SUB, INT_S_DIV, INT_S_LESS, + INT_XOR, POP_COUNT, +}; + +/// The left side of every hardcoded behaviour classid. +pub const HARDCODED: u16 = 0xFFFF; + +/// The relational / mask-risc polyfill lens, `0xFFFF :: 0xF000`. +pub const POLYFILL_LENS: u32 = compose_classid(HARDCODED, 0xF000); + +/// One word of the polyfill lens. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FrozenWord { + /// The fold-dialect opcode the word runs as. + pub opcode: FnIndex, + /// The number of stack operands it takes; it always pushes one value. + pub arity: u8, + /// Every name a view may give this position. The first is the R2IL + /// opcode name. + pub aliases: &'static [&'static str], +} + +const fn w(opcode: FnIndex, arity: u8, aliases: &'static [&'static str]) -> FrozenWord { + FrozenWord { + opcode, + arity, + aliases, + } +} + +/// The polyfill lens, by position. +pub const POLYFILL: [FrozenWord; 17] = [ + w(INT_ADD, 2, &["IntAdd", "+"]), + w(INT_SUB, 2, &["IntSub", "-"]), + w(INT_MULT, 2, &["IntMult", "*"]), + w(INT_S_DIV, 2, &["IntSDiv", "/"]), + w(INT_EQUAL, 2, &["IntEqual", "=", "Cmp::EqI32", "Cmp::EqU32"]), + w(INT_S_LESS, 2, &["IntSLess", "<", "Cmp::LtI32"]), + w(INT_AND, 2, &["IntAnd", "AND", "Filter::and"]), + w(INT_OR, 2, &["IntOr", "OR", "Filter::or"]), + w(INT_XOR, 2, &["IntXor", "XOR"]), + w(INT_NOT, 1, &["IntNot", "NOT", "Filter::negate"]), + w(POP_COUNT, 1, &["PopCount", "COUNT", "Agg::Count"]), + w(SUM, 2, &["SUM", "Agg::SumI32"]), + w(MIN, 2, &["MIN", "Agg::MinI32"]), + w(MAX, 2, &["MAX", "Agg::MaxI32"]), + w(ANY, 1, &["ANY", "EXISTS", "Agg::Any"]), + w(ALL, 1, &["ALL", "Agg::All"]), + w( + KEY_RUNS, + 2, + &["KEY_RUNS", "COUNT DISTINCT", "Agg::CountDistinctOrderedU32"], + ), +]; + +/// The lens's declared arities, in position order, for +/// `BindStore::declare_algebra`. +#[must_use] +pub fn polyfill_arities() -> Vec { + POLYFILL.iter().map(|w| w.arity).collect() +} + +/// The position whose aliases include `label`, if exactly one does. +#[must_use] +pub fn position_of(label: &str) -> Option { + let mut hits = POLYFILL + .iter() + .enumerate() + .filter(|(_, w)| w.aliases.contains(&label)); + match (hits.next(), hits.next()) { + (Some((p, _)), None) => u8::try_from(p).ok(), + _ => None, + } +} diff --git a/crates/lance-graph-r2il/tests/integration/main.rs b/crates/lance-graph-r2il/tests/integration/main.rs index cd3fe6c8b..ae0287139 100644 --- a/crates/lance-graph-r2il/tests/integration/main.rs +++ b/crates/lance-graph-r2il/tests/integration/main.rs @@ -3,3 +3,4 @@ pub mod binding; pub mod binding_native; +pub mod polyfill; diff --git a/crates/lance-graph-r2il/tests/integration/polyfill.rs b/crates/lance-graph-r2il/tests/integration/polyfill.rs new file mode 100644 index 000000000..ab9766e1c --- /dev/null +++ b/crates/lance-graph-r2il/tests/integration/polyfill.rs @@ -0,0 +1,531 @@ +//! The hardcoded polyfill lens `0xFFFF :: 0xF000` (`polyfill`): words bind +//! by position, and R2IL, SQL and Quack names are aliases of a position. +//! +//! Methods here are registered by their names in one language and run +//! through the R2IL binding; each answer is checked against a scalar oracle +//! and, for the Quack names, against Quack's own lowering run through +//! mask-risc over the same lanes. + +use lance_graph_contract::class_view::{ClassId, ClassView, WideFieldMask}; +use lance_graph_contract::ogar_codebook::{classid_canon_compat, classid_concept}; +use lance_graph_contract::ontology::{DisplayTemplate, FieldRef}; +use lance_graph_contract::set_rules::{Population, Truth}; +use lance_graph_dt64::dr::{Focus, FrameRevisions, FrameStore, Guards, Scope}; +use lance_graph_dt64::{ + AlgebraRef, Arg, BindStore, ClassSchema, Effect, Extent, MethodBinding, MethodId, MethodRef, + RouteVersion, SchemaRev, SigRev, ValueDemand, +}; +use lance_graph_mask_risc::{execute, Foreign, LaneRef, Planes, Scratch, Value}; +use lance_graph_quack::{lower, Agg, Cmp, Col, Filter, Query}; +use lance_graph_r2il::binding::{ + prepare, run, ExecPermit, ExecRefusal, FieldLanes, Prepared, R2ilLens, Resolution, EXECUTABLE, +}; +use lance_graph_r2il::polyfill::{ + polyfill_arities, position_of, HARDCODED, POLYFILL, POLYFILL_LENS, +}; +use lance_graph_r2il::FoldDialect; +use ogar_loco::Vocabulary; +use ogar_r2il::R2ILVocabulary; + +const CLASS: u32 = 0x7F0B_0001; +const CLASS_VIEW: ClassId = 0x7F0B; +const REV: SchemaRev = SchemaRev(1); +const LENS: R2ilLens = R2ilLens { + lens: POLYFILL_LENS, + schema_rev: REV, +}; +const FIELDS: [&str; 8] = ["a", "b", "k", "m", "s", "t", "u", "v"]; +const LANES: [Option; 8] = [Some(0), Some(1), Some(2), None, None, None, None, None]; +const FOCUS: Focus = Focus { revision: 1 }; +const SCOPE: Scope = Scope { id: 1 }; +const NOW: (Focus, Scope) = (FOCUS, SCOPE); +const POSITION: u16 = 3; +const REVS: FrameRevisions = FrameRevisions { + source: 1, + relation_map: 1, + coordinates: 1, +}; + +// ---------------------------------------------------------------- the table + +#[test] +fn the_lens_is_hardcoded_and_every_reader_sees_ffff() { + assert_eq!(POLYFILL_LENS, 0xFFFF_F000); + assert_eq!(classid_concept(POLYFILL_LENS), HARDCODED); + // The compat reader misreads a left-empty classid (TD-CLASSID-CANON- + // COMPAT-MISREADS-LEFT-EMPTY); a 0xFFFF left side is not affected. + assert_eq!(classid_canon_compat(POLYFILL_LENS), HARDCODED); +} + +#[test] +fn the_table_matches_ogar_and_the_executable_subset() { + let opcodes: Vec<_> = POLYFILL.iter().map(|w| w.opcode).collect(); + assert_eq!(opcodes, EXECUTABLE.to_vec(), "positions follow EXECUTABLE"); + for (p, w) in POLYFILL.iter().enumerate() { + assert_eq!( + R2ILVocabulary.name(w.opcode), + Some(w.aliases[0]), + "position {p}: the first alias is the R2IL name" + ); + assert_eq!(R2ILVocabulary.stack_arity(w.opcode), Some(w.arity), "{p}"); + assert_eq!(R2ILVocabulary.pushes_result(w.opcode), Some(true), "{p}"); + } +} + +#[test] +fn every_alias_names_exactly_one_position() { + let mut n = 0; + for (p, w) in POLYFILL.iter().enumerate() { + for a in w.aliases { + assert_eq!(position_of(a), u8::try_from(p).ok(), "alias {a:?}"); + n += 1; + } + } + assert!(n > POLYFILL.len() * 2, "aliases beyond the R2IL names: {n}"); + assert_eq!(position_of("NotAName"), None); +} + +// ---------------------------------------------------------------- fixture + +/// One view of the polyfill lens: a label for every position. +struct Views { + class: Vec, + lens: Vec, +} + +impl Views { + /// Label every position with its alias in `lang` (an index into each + /// position's aliases, falling back to the R2IL name). + fn language(lang: &str) -> Self { + let pick = |w: &lance_graph_r2il::polyfill::FrozenWord| -> String { + let found = match lang { + "sql" => w + .aliases + .iter() + .skip(1) + .find(|a| !a.contains("::")) + .copied(), + "quack" => w.aliases.iter().find(|a| a.contains("::")).copied(), + _ => None, + }; + found.unwrap_or(w.aliases[0]).to_string() + }; + Self::with_labels(POLYFILL.iter().map(pick).collect()) + } + fn with_labels(labels: Vec) -> Self { + Self { + class: FIELDS + .iter() + .map(|l| FieldRef::new(format!("lab:class/{l}"), (*l).to_string())) + .collect(), + lens: labels + .into_iter() + .map(|l| FieldRef::new(format!("lab:polyfill/{l}"), l)) + .collect(), + } + } +} + +impl ClassView for Views { + fn fields(&self, class: ClassId) -> &[FieldRef] { + match class { + CLASS_VIEW => &self.class, + HARDCODED => &self.lens, + _ => &[], + } + } + fn template(&self, _class: ClassId) -> DisplayTemplate { + DisplayTemplate::Card + } + fn dolce_category_id(&self, _class: ClassId) -> u8 { + 0 + } +} + +fn pos(name: &str) -> u8 { + position_of(name).unwrap_or_else(|| panic!("{name:?} is no alias")) +} + +fn step(target: u8, name: &str, args: Vec) -> Effect { + Effect { + target, + algebra: AlgebraRef { + lens: POLYFILL_LENS, + word: pos(name), + }, + args, + } +} + +fn method(id: u32, reads: &[u8], effects: Vec) -> MethodBinding { + MethodBinding { + id: MethodId(id), + sig_rev: SigRev(1), + name: format!("m{id}"), + on: WideFieldMask::from_positions(reads), + when: None, + reads: WideFieldMask::from_positions(reads), + effects, + population: Population::Distinct, + truth: Truth::TwoValued, + demand: ValueDemand::Values, + } +} + +fn mref(id: u32) -> MethodRef { + MethodRef { + class: CLASS, + method: MethodId(id), + sig_rev: SigRev(1), + guard: None, + } +} + +fn store(methods: Vec) -> (BindStore, RouteVersion) { + let mut s = BindStore::new(); + s.declare_algebra(POLYFILL_LENS, REV, Extent::DR64_CUBE, polyfill_arities()) + .unwrap(); + s.register( + CLASS, + ClassSchema { + schema_rev: REV, + field_count: FIELDS.len(), + extent: Extent::DR64_CUBE, + }, + methods, + ) + .unwrap(); + let v = s.route(POSITION, CLASS).unwrap(); + (s, v) +} + +/// `a = i % 37 - 10`, `b = (7i) % 23`, `k = i / 7` (key-ordered). +struct Data { + a: Vec, + b: Vec, + k: Vec, +} + +impl Data { + fn new(n: usize) -> Self { + Self { + a: (0..n).map(|i| (i % 37) as i32 - 10).collect(), + b: (0..n).map(|i| ((7 * i) % 23) as i32).collect(), + k: (0..n).map(|i| (i / 7) as u32).collect(), + } + } + fn lanes(&self) -> [LaneRef<'_>; 3] { + [ + LaneRef::I32(&self.a), + LaneRef::I32(&self.b), + LaneRef::U32(&self.k), + ] + } +} + +struct Run { + store: BindStore, + version: RouteVersion, + frames: FrameStore, +} + +impl Run { + fn new(methods: Vec) -> Self { + let (store, version) = store(methods); + Self { + store, + version, + frames: FrameStore::new(REVS), + } + } + fn prepare(&self, views: &Views, id: u32) -> Result { + let r = mref(id); + let g = Guards { + frame: self.frames.seal(), + focus: FOCUS, + scope: SCOPE, + }; + prepare( + &self.store, + views, + LENS, + &Resolution { + method: &r, + schema_rev: REV, + route: (self.version, POSITION), + guards: &g, + }, + &self.frames, + NOW, + ) + } + /// The value the plan leaves in field 4 (`s`). + fn answer(&self, plan: &Prepared, data: &Data) -> i64 { + let lanes = data.lanes(); + let planes = Planes { + n_rows: data.a.len(), + masks: &[], + lanes: &lanes, + }; + let foreign = Foreign::NONE; + let mut d = FoldDialect::new(&planes, &foreign); + let receipt = run( + plan, + &self.store, + &self.frames, + NOW, + &ExecPermit { + methods: vec![plan.method.clone()], + }, + FieldLanes { lanes: &LANES }, + &mut d, + ) + .expect("runs"); + receipt + .output + .values + .iter() + .find(|(f, _)| *f == 4) + .map(|(_, v)| *v) + .expect("field 4 written") + } +} + +// ------------------------------------------- position binds, labels alias + +const SUM_BELOW: u32 = 1; // s = SUM(b) where a < 5 + +fn sum_below() -> MethodBinding { + method( + SUM_BELOW, + &[0, 1], + vec![ + step(3, "IntSLess", vec![Arg::Field(0), Arg::Const(5)]), + step(4, "SUM", vec![Arg::Field(3), Arg::Field(1)]), + ], + ) +} + +#[test] +fn three_languages_bind_the_same_plan_and_answer() { + let run = Run::new(vec![sum_below()]); + let data = Data::new(1000); + let oracle: i64 = data + .a + .iter() + .zip(&data.b) + .filter(|(a, _)| **a < 5) + .map(|(_, b)| i64::from(*b)) + .sum(); + let mut plans = Vec::new(); + for lang in ["r2il", "sql", "quack"] { + let views = Views::language(lang); + let plan = run.prepare(&views, SUM_BELOW).expect(lang); + assert_eq!(run.answer(&plan, &data), oracle, "{lang}"); + plans.push(plan); + } + // The languages differ in their labels (`IntSLess`, `<`, `Cmp::LtI32`) + // and nowhere else: the bound plan, and its identity, are the same. + assert_eq!( + Views::language("sql").lens[pos("IntSLess") as usize].label, + "<" + ); + for p in &plans[1..] { + assert_eq!(p.steps, plans[0].steps); + assert_eq!(p.plan_id, plans[0].plan_id); + } +} + +#[test] +fn a_label_from_another_position_is_refused_not_followed() { + let run = Run::new(vec![sum_below()]); + // The view calls the SUM position "MIN": a real alias, of another word. + let mut labels: Vec = POLYFILL.iter().map(|w| w.aliases[0].to_string()).collect(); + labels[pos("SUM") as usize] = "MIN".to_string(); + let err = run + .prepare(&Views::with_labels(labels), SUM_BELOW) + .unwrap_err(); + assert_eq!( + err, + ExecRefusal::NotAnAlias { + effect: 1, + word: AlgebraRef { + lens: POLYFILL_LENS, + word: pos("SUM"), + }, + label: "MIN".to_string(), + } + ); +} + +#[test] +fn the_position_binds_where_the_label_binding_would_take_another_opcode() { + // Under the R2IL fold reading the label binds, so a lens that put "MIN" + // at the SUM position would run MIN. Under the polyfill lens the same + // label at that position is refused (above), and the R2IL name binds + // the position's own opcode even when another position comes first in + // the view's order. + let run = Run::new(vec![sum_below()]); + let views = Views::language("r2il"); + let plan = run.prepare(&views, SUM_BELOW).unwrap(); + assert_eq!(plan.steps[1].opcode, POLYFILL[pos("SUM") as usize].opcode); + assert_ne!(plan.steps[1].opcode, POLYFILL[pos("MIN") as usize].opcode); +} + +// ---------------------------------------------- Quack names, Quack answers + +/// Quack's answer for `q` over `data`, as the fold dialect reports it. +fn quack(q: &Query, data: &Data, agg: Agg) -> i64 { + let p = lower(q).expect("lowers"); + let lanes = data.lanes(); + let planes = Planes { + n_rows: data.a.len(), + masks: &[], + lanes: &lanes, + }; + let mut scratch = Scratch::for_program(&p, data.a.len()).expect("scratch"); + match (execute(&p, &planes, &mut scratch, None).expect("runs"), agg) { + (Value::Count(n), _) => i64::try_from(n).unwrap(), + (Value::Bool(b), _) => i64::from(b), + (Value::SumI64(s), _) => s, + (Value::OptI32(v), Agg::MinI32(_)) => v.map_or(i64::MAX, i64::from), + (Value::OptI32(v), _) => v.map_or(i64::MIN, i64::from), + (other, _) => panic!("unexpected {other:?}"), + } +} + +/// A method registered by Quack names: `m = a < t`, then the aggregate. +fn by_quack_names(id: u32, t: i64, agg: Agg) -> MethodBinding { + let filter = step(3, "Cmp::LtI32", vec![Arg::Field(0), Arg::Const(t)]); + let fold = match agg { + Agg::Count => step(4, "Agg::Count", vec![Arg::Field(3)]), + Agg::Any => step(4, "Agg::Any", vec![Arg::Field(3)]), + Agg::All => step(4, "Agg::All", vec![Arg::Field(3)]), + Agg::SumI32(_) => step(4, "Agg::SumI32", vec![Arg::Field(3), Arg::Field(1)]), + Agg::MinI32(_) => step(4, "Agg::MinI32", vec![Arg::Field(3), Arg::Field(1)]), + Agg::MaxI32(_) => step(4, "Agg::MaxI32", vec![Arg::Field(3), Arg::Field(1)]), + Agg::CountDistinctOrderedU32 { .. } => step( + 4, + "Agg::CountDistinctOrderedU32", + vec![Arg::Field(3), Arg::Field(2)], + ), + other => panic!("not covered: {other:?}"), + }; + method(id, &[0, 1, 2], vec![filter, fold]) +} + +fn aggs() -> [Agg; 7] { + [ + Agg::Count, + Agg::Any, + Agg::All, + Agg::SumI32(Col(1)), + Agg::MinI32(Col(1)), + Agg::MaxI32(Col(1)), + Agg::CountDistinctOrderedU32 { key: Col(2) }, + ] +} + +#[test] +fn quack_names_register_methods_that_answer_as_quack_does() { + let data = Data::new(1000); + // Three thresholds: a partial selection, everything (a <= 26 < 100), + // and nothing (a >= -10 > -50). + let thresholds = [5_i64, 100, -50]; + let mut methods = Vec::new(); + let mut cases = Vec::new(); + for (ti, &t) in thresholds.iter().enumerate() { + for (ai, agg) in aggs().into_iter().enumerate() { + let id = u32::try_from(100 + ti * 10 + ai).unwrap(); + methods.push(by_quack_names(id, t, agg)); + cases.push((id, t, agg)); + } + } + let run = Run::new(methods); + let views = Views::language("quack"); + let mut distinct = std::collections::BTreeSet::new(); + for (id, t, agg) in cases { + let plan = run.prepare(&views, id).expect("prepares"); + let got = run.answer(&plan, &data); + let q = Query { + filter: Filter::cmp(Col(0), Cmp::LtI32(i32::try_from(t).unwrap())), + agg, + }; + assert_eq!(got, quack(&q, &data, agg), "{agg:?} where a < {t}"); + distinct.insert(got); + } + // Anti-vacuity: the 21 cases do not all collapse to a few values. + assert!(distinct.len() >= 10, "answers: {distinct:?}"); +} + +#[test] +fn a_compound_quack_filter_registers_by_name_and_matches() { + // COUNT where a < 5 AND NOT a = -3. + let m = method( + 200, + &[0], + vec![ + step(3, "Cmp::LtI32", vec![Arg::Field(0), Arg::Const(5)]), + step(5, "Cmp::EqI32", vec![Arg::Field(0), Arg::Const(-3)]), + step(6, "Filter::negate", vec![Arg::Field(5)]), + step(7, "Filter::and", vec![Arg::Field(3), Arg::Field(6)]), + step(4, "Agg::Count", vec![Arg::Field(7)]), + ], + ); + let run = Run::new(vec![m]); + let data = Data::new(1000); + let plan = run.prepare(&Views::language("quack"), 200).unwrap(); + let got = run.answer(&plan, &data); + let q = Query { + filter: Filter::and([ + Filter::cmp(Col(0), Cmp::LtI32(5)), + Filter::negate(Filter::cmp(Col(0), Cmp::EqI32(-3))), + ]), + agg: Agg::Count, + }; + assert_eq!(got, quack(&q, &data, Agg::Count)); + let oracle = data.a.iter().filter(|a| **a < 5 && **a != -3).count() as i64; + assert_eq!(got, oracle); + assert!(oracle > 0 && oracle < 1000); +} + +#[test] +fn a_wrong_runtime_arity_for_the_lens_is_refused_by_the_table() { + // The store's declared arity is a runtime input; the table is not. A + // store that declares every polyfill word unary accepts a one-argument + // SUM at registration, and the table refuses it at prepare. + let mut s = BindStore::new(); + s.declare_algebra( + POLYFILL_LENS, + REV, + Extent::DR64_CUBE, + vec![1; POLYFILL.len()], + ) + .unwrap(); + s.register( + CLASS, + ClassSchema { + schema_rev: REV, + field_count: FIELDS.len(), + extent: Extent::DR64_CUBE, + }, + vec![method(300, &[0], vec![step(4, "SUM", vec![Arg::Field(0)])])], + ) + .unwrap(); + let version = s.route(POSITION, CLASS).unwrap(); + let run = Run { + store: s, + version, + frames: FrameStore::new(REVS), + }; + let err = run.prepare(&Views::language("sql"), 300).unwrap_err(); + assert_eq!( + err, + ExecRefusal::Arity { + effect: 0, + ogar: None, + declared: Some(2), + args: 1, + } + ); +}