From d3ab0d90a040dbe2b169c3f9593e2ab3d7c6ea89 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 22:06:10 +0000 Subject: [PATCH 01/13] lance-graph-nal: NAL query operations as coordinate ranges over the fold path (D-NALP-1/2) Beliefs are sealed FacetCascade keys (S, copula, P, 255-c, 255-f); every query is a witnessed coordinate range. The typed nal::{Question, TruthAtLeast, Support, TruthAtLeastAcross, Choice} operations run their R2IL procedure once at bind and keep the mask-risc program; a call runs only the kept program over the range extent. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- Cargo.toml | 4 + crates/lance-graph-nal/Cargo.toml | 26 ++ crates/lance-graph-nal/examples/nal_cost.rs | 134 ++++++++ crates/lance-graph-nal/src/lib.rs | 336 ++++++++++++++++++++ crates/lance-graph-nal/src/nal.rs | 246 ++++++++++++++ crates/lance-graph-nal/tests/w2.rs | 212 ++++++++++++ 6 files changed, 958 insertions(+) create mode 100644 crates/lance-graph-nal/Cargo.toml create mode 100644 crates/lance-graph-nal/examples/nal_cost.rs create mode 100644 crates/lance-graph-nal/src/lib.rs create mode 100644 crates/lance-graph-nal/src/nal.rs create mode 100644 crates/lance-graph-nal/tests/w2.rs diff --git a/Cargo.toml b/Cargo.toml index d18234f7e..2b04104c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,6 +106,10 @@ exclude = [ # the OGAR sibling. Verify via # `cargo test --manifest-path crates/lance-graph-r2il/Cargo.toml`. "crates/lance-graph-r2il", + # NAL over the fold path (D-NALP-1/2): path-deps lance-graph-r2il, so it + # needs the OGAR checkout too. Own workspace root; verify via + # `cargo test --manifest-path crates/lance-graph-nal/Cargo.toml`. + "crates/lance-graph-nal", "crates/symbiont", # OGAR (Open Graph of Active Record) activation crate — re-exports OGAR's full # AR surface (ogar-vocab Class/codebook + ogar-class-view impl ClassView + diff --git a/crates/lance-graph-nal/Cargo.toml b/crates/lance-graph-nal/Cargo.toml new file mode 100644 index 000000000..040fb3501 --- /dev/null +++ b/crates/lance-graph-nal/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "lance-graph-nal" +version = "0.1.0" +edition = "2021" +publish = false +description = "NAL over the fold path: Narsese statements as sealed facet coordinates, NAL operations as R2IL procedures compiled once into mask-risc programs (D-NALP-1/2)." + +# Its OWN workspace root, like `lance-graph-r2il`: it path-deps the r2il crate, +# which path-deps the OGAR sibling. Verify via +# `cargo test --manifest-path crates/lance-graph-nal/Cargo.toml`. +[workspace] + +[dependencies] +# The coordinate machinery: FacetCascade keys, the sealed ordered lane and its witness. +lance-graph-contract = { path = "../lance-graph-contract" } +# The only bulk engine. +lance-graph-mask-risc = { path = "../lance-graph-mask-risc" } +# The procedure executor: NAL operations are R2IL call sequences over it. +lance-graph-r2il = { path = "../lance-graph-r2il" } +ogar-loco = { path = "../../../OGAR/crates/ogar-loco" } + +[patch."https://github.com/AdaWorldAPI/lance-graph"] +lance-graph-contract = { path = "../lance-graph-contract" } + +[patch."https://github.com/AdaWorldAPI/ndarray"] +ndarray = { path = "../../../ndarray" } diff --git a/crates/lance-graph-nal/examples/nal_cost.rs b/crates/lance-graph-nal/examples/nal_cost.rs new file mode 100644 index 000000000..f521aa385 --- /dev/null +++ b/crates/lance-graph-nal/examples/nal_cost.rs @@ -0,0 +1,134 @@ +//! Per-call cost of the bound NAL operations against a scalar row loop. +//! +//! `cargo run --release --manifest-path crates/lance-graph-nal/Cargo.toml --example nal_cost` +//! +//! Every answer is asserted equal to the row loop before anything is timed. + +use std::hint::black_box; +use std::time::Instant; + +use lance_graph_nal::{nal, Belief, Beliefs, Copula}; + +fn fixture(n: usize) -> Vec { + let mut x: u64 = 0x9E37_79B9_7F4A_7C15; + (0..n) + .map(|_| { + x = x.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = x; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + let r = z ^ (z >> 31); + Belief { + s: (r % 4096) as u16, + copula: Copula::ALL[((r >> 12) % 4) as usize], + p: ((r >> 16) % 4096) as u16, + f: (r >> 32) as u8, + c: (r >> 40) as u8, + } + }) + .collect() +} + +/// Median nanoseconds per call over 101 batches of `reps` calls. +fn median_ns(reps: u32, mut f: impl FnMut() -> usize) -> f64 { + let mut v: Vec = (0..101) + .map(|_| { + let t = Instant::now(); + for _ in 0..reps { + black_box(f()); + } + t.elapsed().as_nanos() as f64 / f64::from(reps) + }) + .collect(); + v.sort_by(f64::total_cmp); + v[50] +} + +fn main() { + let simd = if cfg!(target_feature = "avx512f") { + "avx512f" + } else if cfg!(target_feature = "avx2") { + "avx2" + } else { + "baseline" + }; + println!("target: {simd}"); + for n in [65_536usize, 1_048_576] { + let input = fixture(n); + let beliefs = Beliefs::seal(0x0E01_0001, &input, 1).unwrap(); + let b = beliefs.bind(&beliefs.witness()).unwrap(); + let st = input[n / 2].statement(); + let s = st.s; + + let q = nal::Question::bind(&b, st).unwrap(); + let mut qs = q.scratch(&b).unwrap(); + let t = nal::TruthAtLeast::bind(&b, st, 128).unwrap(); + let mut ts = t.scratch(&b).unwrap(); + let sup = nal::Support::bind(&b, s).unwrap(); + let mut ss = sup.scratch(&b).unwrap(); + let across = nal::TruthAtLeastAcross::bind(&b, s, 128).unwrap(); + let mut asx = across.scratch(&b).unwrap(); + + let same = |x: &Belief| x.statement() == st; + assert_eq!( + q.count(&b, &mut qs).unwrap(), + input.iter().filter(|x| same(x)).count() + ); + assert_eq!( + t.count(&b, &mut ts).unwrap(), + input.iter().filter(|x| same(x) && x.c >= 128).count() + ); + assert_eq!( + sup.count(&b, &mut ss).unwrap(), + input.iter().filter(|x| x.s == s).count() + ); + assert_eq!( + across.count(&b, &mut asx).unwrap(), + input.iter().filter(|x| x.s == s && x.c >= 128).count() + ); + + println!( + "n = {n} (subject support = {})", + sup.count(&b, &mut ss).unwrap() + ); + let r = |name: &str, ns: f64| println!(" {name:<34} {ns:>12.1} ns"); + r( + "bind Question (search + procedure)", + median_ns(100, || { + nal::Question::bind(&b, st).unwrap().program().ops.len() + }), + ); + r( + "Question::count (kept program)", + median_ns(10_000, || q.count(&b, &mut qs).unwrap()), + ); + r( + "TruthAtLeast::count", + median_ns(10_000, || t.count(&b, &mut ts).unwrap()), + ); + r( + "Support::count", + median_ns(10_000, || sup.count(&b, &mut ss).unwrap()), + ); + r( + "TruthAtLeastAcross::count", + median_ns(1_000, || across.count(&b, &mut asx).unwrap()), + ); + r( + "Choice::of", + median_ns(10_000, || { + nal::Choice::of(&b, st).map_or(0, |x| x.c as usize) + }), + ); + r( + "row loop: count statement", + median_ns(3, || input.iter().filter(|x| same(x)).count()), + ); + r( + "row loop: subject with c >= 128", + median_ns(3, || { + input.iter().filter(|x| x.s == s && x.c >= 128).count() + }), + ); + } +} diff --git a/crates/lance-graph-nal/src/lib.rs b/crates/lance-graph-nal/src/lib.rs new file mode 100644 index 000000000..721cc0720 --- /dev/null +++ b/crates/lance-graph-nal/src/lib.rs @@ -0,0 +1,336 @@ +//! `lance-graph-nal` — NAL over the fold path (D-NALP-1/2, +//! `.claude/plans/nal-polyfill-v1.md`). +//! +//! # Statements are coordinates +//! +//! A belief is not a row with foreign keys. It is an address: the reading +//! `nal-statement-v1` places it on one [`FacetCascade`] key, +//! +//! ```text +//! semantic tiles: [classid hi, classid lo, S, copula, P, 255 − c, 255 − f, 0] +//! ``` +//! +//! and the population is a [`SealedFacetLane`] of those keys, sorted at seal +//! and attested by an [`OrderedLaneWitness`]. Every query below is therefore +//! a coordinate range, found with two `partition_point`s: +//! +//! - `?` is the prefix of depth 5; +//! - "every belief about S" is the prefix of depth 3; +//! - `c ≥ t` within a statement is the start of that statement's range up to +//! the key with tile 5 = `255 − t`, because confidence is stored inverted +//! and the range is ordered by it; +//! - the highest-confidence belief of a statement (NAL choice) is the first +//! row of its range. +//! +//! The confidence lane `c` handed to mask-risc is decoded from the sealed keys +//! themselves, so it is in the sealed order by construction: the obligation +//! `ISS-WITNESSED-RANGE-DOES-NOT-ATTEST-PLANE-ORDER` names cannot be broken +//! here. +//! +//! # Operations are procedures, compiled once +//! +//! Each [`nal`] operation is an R2IL procedure ([`nal::NalOp::lower`]) over +//! [`lance_graph_r2il::FoldDialect`]. Binding a query runs that procedure once +//! and keeps the mask-risc [`Program`] it produced; every later call runs the +//! kept program through [`execute_compiled`]. A call does not parse, lower or +//! allocate. +//! +//! # Not here yet +//! +//! Revision (W3), the per-row truth laws (W4), syllogisms (W5) and the +//! Narsese text front end (W6). The term width is `u16` per tile: a +//! vocabulary larger than 65,536 terms scales through the classid prefix, not +//! by widening the tile. +//! +//! OPEN: R2IL `RANGE` takes its bounds from two `u8` immediates (`ogar-r2il` +//! arity 0), so it cannot name a range past row 255. The procedures emit the +//! bound through [`lance_graph_r2il::FoldDialect::emit_pred`] instead. + +#![forbid(unsafe_code)] + +pub mod nal; + +use lance_graph_contract::facet::FacetCascade; +use lance_graph_contract::ordered_lane::{OrderedLaneWitness, SealedFacetLane, WitnessError}; +use lance_graph_contract::temporal_pov::LanceVersion; +use lance_graph_mask_risc::{execute_compiled, ExecError, LaneRef, Program}; + +/// The NAL copulas this reading stores. NAL-1 and NAL-2 (inheritance, +/// similarity) and NAL-5 (implication, equivalence) statements over atomic +/// terms; compound terms are out of scope (ledger R10). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[repr(u16)] +pub enum Copula { + /// `-->` + Inheritance = 1, + /// `<->` + Similarity = 2, + /// `==>` + Implication = 3, + /// `<=>` + Equivalence = 4, +} + +impl Copula { + /// Every copula, in code order. + pub const ALL: [Copula; 4] = [ + Copula::Inheritance, + Copula::Similarity, + Copula::Implication, + Copula::Equivalence, + ]; + + fn from_code(c: u16) -> Option { + Self::ALL.into_iter().find(|k| *k as u16 == c) + } +} + +/// A statement ``: two term addresses and a copula. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Statement { + /// Subject term address. + pub s: u16, + /// Copula. + pub copula: Copula, + /// Predicate term address. + pub p: u16, +} + +/// One belief: a statement with a truth value in `u8` codes (`f`, `c` scaled +/// to `0..=255`, the codes `causal_edge::isa::truth` reads). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Belief { + /// Subject term address. + pub s: u16, + /// Copula. + pub copula: Copula, + /// Predicate term address. + pub p: u16, + /// Frequency code. + pub f: u8, + /// Confidence code. + pub c: u8, +} + +impl Belief { + /// The statement this belief is about. + pub fn statement(&self) -> Statement { + Statement { + s: self.s, + copula: self.copula, + p: self.p, + } + } +} + +/// Semantic tile positions of the `nal-statement-v1` reading. +const T_S: usize = 2; +const T_COPULA: usize = 3; +const T_P: usize = 4; +const T_INV_C: usize = 5; +const T_INV_F: usize = 6; + +fn tiles(class: u32, s: u16, copula: u16, p: u16, inv_c: u16, inv_f: u16, last: u16) -> [u16; 8] { + [ + (class >> 16) as u16, + class as u16, + s, + copula, + p, + inv_c, + inv_f, + last, + ] +} + +fn key_of(class: u32, b: &Belief) -> FacetCascade { + FacetCascade::from_semantic_tiles(tiles( + class, + b.s, + b.copula as u16, + b.p, + u16::from(255 - b.c), + u16::from(255 - b.f), + 0, + )) +} + +fn belief_of(k: &FacetCascade) -> Option { + let t = k.semantic_tiles(); + Some(Belief { + s: t[T_S], + copula: Copula::from_code(t[T_COPULA])?, + p: t[T_P], + c: 255 - u8::try_from(t[T_INV_C]).ok()?, + f: 255 - u8::try_from(t[T_INV_F]).ok()?, + }) +} + +/// A sealed belief population: the ordered key lane and the confidence lane +/// decoded from it. +#[derive(Debug, Clone)] +pub struct Beliefs { + class: u32, + lane: SealedFacetLane, + pub(crate) c: Vec, +} + +impl Beliefs { + /// Seal `beliefs` under the statement class `class` at `version`. Sorting + /// happens here, once; every read after it is a fold. + /// + /// # Errors + /// + /// [`WitnessError::TooManyRows`] past `u32::MAX` rows. + pub fn seal( + class: u32, + beliefs: &[Belief], + version: LanceVersion, + ) -> Result { + let lane = + SealedFacetLane::seal(beliefs.iter().map(|b| key_of(class, b)).collect(), version)?; + let c = lane + .keys() + .iter() + .map(|k| 255 - i32::from(k.semantic_tiles()[T_INV_C])) + .collect(); + Ok(Beliefs { class, lane, c }) + } + + /// The witness the lane was sealed with. A planner holds it and hands it + /// back to [`Beliefs::bind`]. + pub fn witness(&self) -> OrderedLaneWitness { + self.lane.witness() + } + + /// Validate `witness` against this lane (O(1)) and return the bound view + /// the operations run over. + /// + /// # Errors + /// + /// [`NalError::Witness`] when the witness is from another seal. + pub fn bind(&self, witness: &OrderedLaneWitness) -> Result, NalError> { + self.lane.validate(witness)?; + Ok(Bound { beliefs: self }) + } +} + +/// A belief population whose ordering witness has been validated. +#[derive(Debug, Clone, Copy)] +pub struct Bound<'a> { + pub(crate) beliefs: &'a Beliefs, +} + +/// Lane index of the confidence lane in the planes every NAL program runs over. +pub(crate) const LANE_C: u16 = 0; + +impl<'a> Bound<'a> { + /// Row count. + pub fn n_rows(&self) -> usize { + self.beliefs.c.len() + } + + fn keys(&self) -> &'a [FacetCascade] { + self.beliefs.lane.keys() + } + + /// The belief stored at sealed row `row`. + pub fn belief(&self, row: usize) -> Option { + self.keys().get(row).and_then(belief_of) + } + + /// `[lo, hi)` of every key in the closed interval `[lo_t, hi_t]` of + /// semantic tiles: two `partition_point`s on the witnessed order. + pub(crate) fn interval(&self, lo_t: [u16; 8], hi_t: [u16; 8]) -> (u32, u32) { + let (lo_k, hi_k) = ( + FacetCascade::from_semantic_tiles(lo_t), + FacetCascade::from_semantic_tiles(hi_t), + ); + let keys = self.keys(); + let lo = keys.partition_point(|k| k.cmp_numeric_projection(&lo_k).is_lt()); + let hi = keys.partition_point(|k| k.cmp_numeric_projection(&hi_k).is_le()); + (lo as u32, hi.max(lo) as u32) + } + + /// The rows of one statement. + pub(crate) fn statement(&self, st: Statement) -> (u32, u32) { + self.statement_at_least(st, 0) + } + + /// The rows of one statement with confidence `>= t`. + pub(crate) fn statement_at_least(&self, st: Statement, t: u8) -> (u32, u32) { + let class = self.beliefs.class; + let cop = st.copula as u16; + self.interval( + tiles(class, st.s, cop, st.p, 0, 0, 0), + tiles(class, st.s, cop, st.p, u16::from(255 - t), 0xFFFF, 0xFFFF), + ) + } + + /// The rows of every belief about subject `s`. + pub(crate) fn subject(&self, s: u16) -> (u32, u32) { + let class = self.beliefs.class; + self.interval( + tiles(class, s, 0, 0, 0, 0, 0), + tiles(class, s, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF), + ) + } + + /// Run a kept program over this population. + pub(crate) fn run( + &self, + program: &Program, + scratch: &mut lance_graph_mask_risc::Scratch<'_>, + extent: std::ops::Range, + ) -> Result { + let lanes = [LaneRef::I32(&self.beliefs.c)]; + let planes = lance_graph_mask_risc::Planes { + n_rows: self.n_rows(), + masks: &[], + lanes: &lanes, + }; + let foreign = lance_graph_mask_risc::Foreign { + planes: &[], + lanes: &[], + }; + execute_compiled( + &program.compile(), + &planes, + &foreign, + scratch, + lance_graph_mask_risc::Out::None, + extent, + ) + } +} + +/// Why a NAL operation did not answer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NalError { + /// The ordering witness does not belong to this lane. + Witness(WitnessError), + /// The procedure was refused while binding. + Fold(lance_graph_r2il::FoldError), + /// mask-risc refused the kept program. + Exec(ExecError), + /// The procedure ended without the one scalar it promised. + NoAnswer, +} + +impl From for NalError { + fn from(e: WitnessError) -> Self { + NalError::Witness(e) + } +} + +impl From for NalError { + fn from(e: lance_graph_r2il::FoldError) -> Self { + NalError::Fold(e) + } +} + +impl From for NalError { + fn from(e: ExecError) -> Self { + NalError::Exec(e) + } +} diff --git a/crates/lance-graph-nal/src/nal.rs b/crates/lance-graph-nal/src/nal.rs new file mode 100644 index 000000000..e89e4ac55 --- /dev/null +++ b/crates/lance-graph-nal/src/nal.rs @@ -0,0 +1,246 @@ +//! The typed NAL surface: `nal::{Operation}::bind(…)` (plan §4 G). +//! +//! Each operation is a zero-sized type. [`NalOp::lower`] is its procedure: the +//! R2IL calls it emits through [`FoldDialect`], reading its bound coordinates +//! as scalars off the loco stack. `bind` resolves the coordinates on the +//! witnessed order, runs the procedure ONCE and keeps the mask-risc program it +//! produced as a [`Prepared`]; every call after that runs the kept program. +//! +//! None of the operations here earns a byte (plan §4 A): `FN` is `None` for +//! all of them. + +use lance_graph_mask_risc::{Foreign, LaneRef, Lowering, Planes, Pred, Program, Scratch, Value}; +use lance_graph_r2il::{procs, FoldDialect, FoldError, Val, INT_S_LESS, LOAD}; +use ogar_loco::{Dialect, FnIndex}; + +use crate::{Belief, Bound, NalError, Statement, LANE_C}; + +/// One NAL operation: its byte, if it earned one, and its procedure. +pub trait NalOp { + /// The `ogar-nal` byte when the operation earned one, else `None`: the + /// operation is a procedure over existing R2IL calls. + const FN: Option; + /// Scalars popped, scalars pushed. + const ARITY: (u8, u8); + /// Emit the operation's calls. Never loops over rows. + fn lower(d: &mut FoldDialect<'_>, stack: &mut Vec) -> Result<(), FoldError>; +} + +fn pop_u32(stack: &mut Vec, f: FnIndex) -> Result { + match stack.pop() { + Some(Val::Scalar(v)) => u32::try_from(v).map_err(|_| FoldError::WrongOperandKind(f)), + Some(_) => Err(FoldError::WrongOperandKind(f)), + None => Err(FoldError::Underflow(f)), + } +} + +/// `lo hi -- n`: the number of rows in the coordinate range `[lo, hi)`. +/// +/// Calls: the range predicate, `PopCount`. +fn range_count(d: &mut FoldDialect<'_>, stack: &mut Vec) -> Result<(), FoldError> { + let hi = pop_u32(stack, procs_fn())?; + let lo = pop_u32(stack, procs_fn())?; + d.emit_pred(Pred::Range { lo, hi }, stack); + procs::count(d, stack) +} + +/// The function index named in a refusal raised by these procedures. +fn procs_fn() -> FnIndex { + lance_graph_r2il::POP_COUNT +} + +/// A query bound to its coordinates, with the program its procedure produced. +/// +/// `extent` is the coordinate range the program runs over. A query whose rows +/// are one range (a statement, a subject) is restricted to it before any lane +/// is read, so its cost follows the range, not the population. +#[derive(Debug, Clone)] +pub struct Prepared { + program: Program, + extent: std::ops::Range, +} + +impl Prepared { + fn of(b: &Bound<'_>, args: &[i64]) -> Result { + Self::over::(b, args, 0..b.n_rows()) + } + + fn over( + b: &Bound<'_>, + args: &[i64], + extent: std::ops::Range, + ) -> Result { + let lanes = [LaneRef::I32(&b.beliefs.c)]; + let planes = Planes { + n_rows: b.n_rows(), + masks: &[], + lanes: &lanes, + }; + let foreign = Foreign { + planes: &[], + lanes: &[], + }; + let mut d = FoldDialect::new(&planes, &foreign); + let mut stack: Vec = args.iter().map(|&v| Val::Scalar(v)).collect(); + O::lower(&mut d, &mut stack)?; + if let Some(e) = d.poison() { + return Err(e.into()); + } + match (stack.as_slice(), d.programs_run(), d.last_program()) { + ([Val::Scalar(_)], 1, Some(p)) => Ok(Prepared { + program: p.clone(), + extent, + }), + _ => Err(NalError::NoAnswer), + } + } + + /// The scratch this query needs: none for a range fold, one tile per slot + /// otherwise. Allocate it once and reuse it across calls. + /// + /// # Errors + /// + /// [`NalError::Exec`] if the program declares more slots than can exist. + pub fn scratch(&self, b: &Bound<'_>) -> Result, NalError> { + Ok(Scratch::for_program(&self.program, b.n_rows())?) + } + + /// How mask-risc folds this query. + pub fn lowering(&self) -> Lowering { + self.program.compile().lowering() + } + + /// The kept program. + pub fn program(&self) -> &Program { + &self.program + } + + /// Run the kept program and read its count. + /// + /// # Errors + /// + /// [`NalError::Exec`] if `b` is not the population the query was bound on + /// (a range past its rows), or [`NalError::NoAnswer`]. + pub fn count(&self, b: &Bound<'_>, scratch: &mut Scratch<'_>) -> Result { + match b.run(&self.program, scratch, self.extent.clone())? { + Value::Count(n) => Ok(n), + _ => Err(NalError::NoAnswer), + } + } +} + +/// `?` — how many beliefs the population holds about a statement. +pub struct Question; + +impl NalOp for Question { + const FN: Option = None; + const ARITY: (u8, u8) = (2, 1); + fn lower(d: &mut FoldDialect<'_>, stack: &mut Vec) -> Result<(), FoldError> { + range_count(d, stack) + } +} + +impl Question { + /// Bind the statement's coordinate range. + /// + /// # Errors + /// + /// [`NalError`] if the procedure is refused. + pub fn bind(b: &Bound<'_>, st: Statement) -> Result { + let (lo, hi) = b.statement(st); + Prepared::of::(b, &[i64::from(lo), i64::from(hi)]) + } +} + +/// The beliefs of a statement with confidence code `>= t`. +pub struct TruthAtLeast; + +impl NalOp for TruthAtLeast { + const FN: Option = None; + const ARITY: (u8, u8) = (2, 1); + fn lower(d: &mut FoldDialect<'_>, stack: &mut Vec) -> Result<(), FoldError> { + range_count(d, stack) + } +} + +impl TruthAtLeast { + /// Bind the statement's range cut at `t`: confidence is stored inverted, + /// so the threshold is the end of a prefix of the statement's range. + /// + /// # Errors + /// + /// [`NalError`] if the procedure is refused. + pub fn bind(b: &Bound<'_>, st: Statement, t: u8) -> Result { + let (lo, hi) = b.statement_at_least(st, t); + Prepared::of::(b, &[i64::from(lo), i64::from(hi)]) + } +} + +/// Every belief about a subject: the support of `S` in the population. +pub struct Support; + +impl NalOp for Support { + const FN: Option = None; + const ARITY: (u8, u8) = (2, 1); + fn lower(d: &mut FoldDialect<'_>, stack: &mut Vec) -> Result<(), FoldError> { + range_count(d, stack) + } +} + +impl Support { + /// Bind the subject's range. + /// + /// # Errors + /// + /// [`NalError`] if the procedure is refused. + pub fn bind(b: &Bound<'_>, s: u16) -> Result { + let (lo, hi) = b.subject(s); + Prepared::of::(b, &[i64::from(lo), i64::from(hi)]) + } +} + +/// Beliefs about a subject with confidence `>= t`, over every statement of +/// that subject. The subject is one range, the threshold is not (each +/// statement restarts the confidence order), so the program reads the `c` +/// lane, and only over the subject's range: the range is the execution +/// extent, never a mask the lane predicate is ANDed with. +pub struct TruthAtLeastAcross; + +impl NalOp for TruthAtLeastAcross { + const FN: Option = None; + const ARITY: (u8, u8) = (1, 1); + /// `t -- n`, counting `t − 1 < c`. Calls: `LOAD`, `IntSLess`, `PopCount`. + fn lower(d: &mut FoldDialect<'_>, stack: &mut Vec) -> Result<(), FoldError> { + let t = pop_u32(stack, procs_fn())?; + stack.push(Val::Scalar(i64::from(t) - 1)); + stack.push(Val::Scalar(i64::from(LANE_C))); + d.call(LOAD, [1, 0, 0], stack)?; + d.call(INT_S_LESS, [0; 3], stack)?; + procs::count(d, stack) + } +} + +impl TruthAtLeastAcross { + /// Bind the subject's range and the threshold. + /// + /// # Errors + /// + /// [`NalError`] if the procedure is refused. + pub fn bind(b: &Bound<'_>, s: u16, t: u8) -> Result { + let (lo, hi) = b.subject(s); + Prepared::over::(b, &[i64::from(t)], lo as usize..hi as usize) + } +} + +/// NAL choice: the highest-confidence belief about a statement. +pub struct Choice; + +impl Choice { + /// The first row of the statement's range. Confidence is stored inverted, + /// so that row has the highest confidence; among equal confidence, the + /// highest frequency. `None` when the statement has no belief. + pub fn of(b: &Bound<'_>, st: Statement) -> Option { + let (lo, hi) = b.statement(st); + (lo < hi).then(|| b.belief(lo as usize)).flatten() + } +} diff --git a/crates/lance-graph-nal/tests/w2.rs b/crates/lance-graph-nal/tests/w2.rs new file mode 100644 index 000000000..dbc0cca71 --- /dev/null +++ b/crates/lance-graph-nal/tests/w2.rs @@ -0,0 +1,212 @@ +//! D-NALP-2: the query operations that need no new byte, each checked against +//! a row-at-a-time oracle over the UNSORTED input beliefs. + +use lance_graph_contract::ordered_lane::OrderedLaneWitness; +use lance_graph_mask_risc::Lowering; +use lance_graph_nal::{nal, Belief, Beliefs, Copula, NalError, Statement}; + +const CLASS: u32 = 0x0E01_0001; + +/// Deterministic beliefs: SplitMix64 over 40 terms, 4 copulas, every truth code. +fn fixture(n: usize) -> Vec { + let mut x: u64 = 0x9E37_79B9_7F4A_7C15; + let mut next = || { + x = x.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = x; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + }; + (0..n) + .map(|_| { + let r = next(); + Belief { + s: (r % 40) as u16, + copula: Copula::ALL[((r >> 8) % 4) as usize], + p: ((r >> 16) % 40) as u16, + f: (r >> 24) as u8, + c: (r >> 32) as u8, + } + }) + .collect() +} + +fn sealed(input: &[Belief]) -> Beliefs { + Beliefs::seal(CLASS, input, 7).expect("seal") +} + +fn statements() -> Vec { + let mut v = Vec::new(); + for s in [0u16, 3, 17, 39, 40, 999] { + for copula in Copula::ALL { + for p in [0u16, 5, 39, 41] { + v.push(Statement { s, copula, p }); + } + } + } + v +} + +fn same(b: &Belief, st: Statement) -> bool { + b.s == st.s && b.copula == st.copula && b.p == st.p +} + +/// FAILS IF: the statement range is bound on the wrong tiles, or the compiled +/// fold counts anything but the bound range. +#[test] +fn question_count_equals_the_row_oracle() { + let input = fixture(5_000); + let beliefs = sealed(&input); + let bound = beliefs.bind(&beliefs.witness()).unwrap(); + let mut hits = 0; + for st in statements() { + let q = nal::Question::bind(&bound, st).unwrap(); + let mut scratch = q.scratch(&bound).unwrap(); + let want = input.iter().filter(|b| same(b, st)).count(); + assert_eq!(q.count(&bound, &mut scratch).unwrap(), want, "{st:?}"); + // The prepared program answers again, unchanged. + assert_eq!(q.count(&bound, &mut scratch).unwrap(), want, "{st:?}"); + hits += usize::from(want > 0); + } + // Anti-vacuity: the statement set hits and misses. + assert!(hits > 5 && hits < statements().len(), "hits = {hits}"); +} + +/// FAILS IF: the question folds through anything but the fused range, i.e. +/// the coordinate bound did not reach mask-risc as a range. +#[test] +fn question_lowers_to_the_fused_range() { + let beliefs = sealed(&fixture(2_000)); + let bound = beliefs.bind(&beliefs.witness()).unwrap(); + let q = nal::Question::bind( + &bound, + Statement { + s: 3, + copula: Copula::Inheritance, + p: 5, + }, + ) + .unwrap(); + assert!( + matches!(q.lowering(), Lowering::Range(_)), + "{:?}", + q.lowering() + ); +} + +/// FAILS IF: choice is not the highest-confidence belief of the statement, or +/// reports one for a statement with no belief. +#[test] +fn choice_is_the_highest_confidence_belief() { + let input = fixture(5_000); + let beliefs = sealed(&input); + let bound = beliefs.bind(&beliefs.witness()).unwrap(); + let (mut some, mut none) = (0, 0); + for st in statements() { + let want = input.iter().filter(|b| same(b, st)).map(|b| b.c).max(); + let got = nal::Choice::of(&bound, st); + assert_eq!(got.map(|b| b.c), want, "{st:?}"); + if let Some(b) = got { + assert!(same(&b, st)); + assert!(input.contains(&b), "choice must be a stored belief"); + some += 1; + } else { + none += 1; + } + } + assert!(some > 0 && none > 0); +} + +/// FAILS IF: the confidence threshold is off by one, inverted, or ignores the +/// statement. +#[test] +fn truth_at_least_equals_the_row_oracle() { + let input = fixture(5_000); + let beliefs = sealed(&input); + let bound = beliefs.bind(&beliefs.witness()).unwrap(); + let mut partial = 0; + for st in statements() { + for t in [0u8, 1, 100, 204, 255] { + let q = nal::TruthAtLeast::bind(&bound, st, t).unwrap(); + let mut scratch = q.scratch(&bound).unwrap(); + let all = input.iter().filter(|b| same(b, st)).count(); + let want = input.iter().filter(|b| same(b, st) && b.c >= t).count(); + assert_eq!(q.count(&bound, &mut scratch).unwrap(), want, "{st:?} t={t}"); + partial += usize::from(want > 0 && want < all); + } + } + // Anti-vacuity: some thresholds cut inside a statement. + assert!(partial > 0); +} + +/// FAILS IF: support over a subject prefix miscounts. +#[test] +fn support_counts_every_belief_about_a_subject() { + let input = fixture(5_000); + let beliefs = sealed(&input); + let bound = beliefs.bind(&beliefs.witness()).unwrap(); + for s in [0u16, 3, 39, 40] { + let q = nal::Support::bind(&bound, s).unwrap(); + let mut scratch = q.scratch(&bound).unwrap(); + let want = input.iter().filter(|b| b.s == s).count(); + assert_eq!(q.count(&bound, &mut scratch).unwrap(), want, "s={s}"); + } +} + +/// FAILS IF: the gated confidence predicate over a subject prefix (which is not +/// one interval) miscounts. +#[test] +fn truth_at_least_across_a_subject_equals_the_row_oracle() { + let input = fixture(5_000); + let beliefs = sealed(&input); + let bound = beliefs.bind(&beliefs.witness()).unwrap(); + for s in [0u16, 3, 39, 40] { + for t in [0u8, 128, 204, 255] { + let q = nal::TruthAtLeastAcross::bind(&bound, s, t).unwrap(); + let mut scratch = q.scratch(&bound).unwrap(); + let want = input.iter().filter(|b| b.s == s && b.c >= t).count(); + assert_eq!(q.count(&bound, &mut scratch).unwrap(), want, "s={s} t={t}"); + } + } +} + +/// FAILS IF: the empty and the full population answer wrongly. +#[test] +fn empty_and_full_populations() { + let empty = sealed(&[]); + let b = empty.bind(&empty.witness()).unwrap(); + let st = Statement { + s: 1, + copula: Copula::Similarity, + p: 2, + }; + let q = nal::Question::bind(&b, st).unwrap(); + assert_eq!(q.count(&b, &mut q.scratch(&b).unwrap()).unwrap(), 0); + assert_eq!(nal::Choice::of(&b, st), None); + + let one = vec![ + Belief { + s: 1, + copula: Copula::Similarity, + p: 2, + f: 9, + c: 200 + }; + 300 + ]; + let full = sealed(&one); + let b = full.bind(&full.witness()).unwrap(); + let q = nal::TruthAtLeast::bind(&b, st, 200).unwrap(); + assert_eq!(q.count(&b, &mut q.scratch(&b).unwrap()).unwrap(), 300); + let q = nal::TruthAtLeast::bind(&b, st, 201).unwrap(); + assert_eq!(q.count(&b, &mut q.scratch(&b).unwrap()).unwrap(), 0); +} + +/// FAILS IF: a witness from another seal binds. A stale witness must make the +/// coordinate path unavailable, never answer from the wrong order. +#[test] +fn a_stale_witness_refuses_to_bind() { + let beliefs = sealed(&fixture(1_000)); + let forged = OrderedLaneWitness::forged(7, beliefs.witness().n_rows(), 0xDEAD); + assert!(matches!(beliefs.bind(&forged), Err(NalError::Witness(_)))); +} From f5ad60fe67410b870ee9920ef99070a4072e2b1f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 22:07:28 +0000 Subject: [PATCH 02/13] board: D-NALP-1/2 status, measurements, RANGE u8-immediate gap Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .claude/board/STATUS_BOARD.md | 4 +-- .../2026-10-12-nal-coordinate-queries.md | 23 ++++++++++++++++ .claude/board/entries/README.md | 3 ++- .claude/plans/nal-polyfill-v1.md | 27 +++++++++++++++++++ 4 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 .claude/board/entries/2026-10-12-nal-coordinate-queries.md diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index cd01f268b..40c3558bd 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -35,8 +35,8 @@ Plan: `.claude/plans/nal-polyfill-v1.md`. Template: D-SQLP. No opcode, tenant or | D-id | scope | status | gate / falsifier | |---|---|---|---| | **D-NALP-0** | Plan, board rows | In PR | docs only | -| **D-NALP-1** | Statement layout + declared ClassView reading; additive `WideFieldMask` projection path on `ClassView`; binder's mask; term width decision | Queued | only the mask's lanes are read; undeclared reading and masked-out field refuse; Small = Wide answers | -| **D-NALP-2** | Query procedures: select, truth threshold, choice by confidence, support count | Queued | equal to a row-at-a-time oracle; one-pattern threshold disable red; no new opcode | +| **D-NALP-1** | Statement layout + declared ClassView reading; additive `WideFieldMask` projection path on `ClassView`; binder's mask; term width decision | In PR (layout `nal-statement-v1` + term width u16; ClassView wide projection and binder mask still queued) | only the mask's lanes are read; undeclared reading and masked-out field refuse; Small = Wide answers | +| **D-NALP-2** | Query procedures: select, truth threshold, choice by confidence, support count | In PR (`crates/lance-graph-nal`, 8 oracle tests, 7 disable runs red; 22–140 ns at 1,048,576 rows) | equal to a row-at-a-time oracle; one-pattern threshold disable red; no new opcode | | **D-NALP-3** | Revision as `GroupCrossPowerSumsI32` over stored `w`/`f` | Queued | equal to exact integer revision; OpenNARS revision points; duplicate evidence counts once | | **D-NALP-4** | Per-row truth law into a lane: `MapPairU8` vs fixed-point multiply-high | Queued | insufficiency proof; per-row parity with `isa::truth`, error in codes; one shape also serves SQL nullable arithmetic | | **D-NALP-5** | Deduction / induction / abduction / analogy over populations | Queued (after D-NALP-4) | equals pairwise scalar rule; abduction direction test red for the reversed statement | diff --git a/.claude/board/entries/2026-10-12-nal-coordinate-queries.md b/.claude/board/entries/2026-10-12-nal-coordinate-queries.md new file mode 100644 index 000000000..35226bb03 --- /dev/null +++ b/.claude/board/entries/2026-10-12-nal-coordinate-queries.md @@ -0,0 +1,23 @@ +# 2026-10-12 — NAL query operations as coordinate ranges (D-NALP-1/2) + +**Status:** In PR. Plan: `.claude/plans/nal-polyfill-v1.md` (status note before § 5). + +**Finding (MEASURED):** with beliefs sealed as `FacetCascade` keys +`[classid, S, copula, P, 255 − c, 255 − f]`, a belief question, a confidence +threshold within a statement, subject support and NAL choice are witnessed +coordinate ranges. The typed `nal::` operations bind once (R2IL procedure → +kept mask-risc program) and each call runs only that program over the range. +At 1,048,576 beliefs: range counts 24 ns, a confidence threshold across one +subject 139 ns, choice 265 ns, against 554–614 µs for the row loop +(`crates/lance-graph-nal/examples/nal_cost.rs`, AVX-512, release). Passing the +range as the execution extent, not as an ANDed mask, took the cross-statement +threshold from 195 µs to 139 ns. + +**Tests:** `crates/lance-graph-nal/tests/w2.rs`, 8 row-oracle tests over the +unsorted input; 7 disable runs red (threshold ignored / off by one, choice +reads the last row, cross-statement threshold off by one and without its +extent, witness not validated, subject bound truncated). + +**OPEN:** R2IL `RANGE` takes `u8` immediates and cannot name a range past row +255 (request to `ogar-r2il`). The rest of D-NALP-1 (ClassView `WideFieldMask` +projection, binder mask) and W3+ are queued. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index a7624e066..f6017836b 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. -319 entries, 2026-08-06 .. 2026-10-12. +320 entries, 2026-08-06 .. 2026-10-12. | date | entry id | finding | file | |---|---|---|---| @@ -33,6 +33,7 @@ exactly one of them, never both. | 2026-10-12 | `D-CMH-G1-6` | | [2026-10-12-ordered-conditions-and-extrema.md](2026-10-12-ordered-conditions-and-extrema.md) | | 2026-10-12 | `opencog-pln-entropy-research-map` | Source inventory and falsifiable PLN/ECAN/Shannon/Moore attention comparison: entropy target, evidence overlap, graph projection, resource fairness | [2026-10-12-opencog-pln-entropy-research-map.md](2026-10-12-opencog-pln-entropy-research-map.md) | | 2026-10-12 | `nars-self-evidence-literature` | NARS internal trace versus evidence, Jøsang dependent/independent fusion and fission, and the snapshot-stable fold assumption; source-grounded red tests for merged #1496 | [2026-10-12-nars-self-evidence-literature.md](2026-10-12-nars-self-evidence-literature.md) | +| 2026-10-12 | `D-NALP-1` | | [2026-10-12-nal-coordinate-queries.md](2026-10-12-nal-coordinate-queries.md) | | 2026-10-12 | `hyperon-metta-lg-translation` | Hyperon/MeTTa to LG sibling map: atom/hypergraph roundtrip, binding and rewrite parity, typed folds, scoped effects and 15 open questions | [2026-10-12-hyperon-metta-lg-translation.md](2026-10-12-hyperon-metta-lg-translation.md) | | 2026-10-12 | `D-CMH-G1-4a` | | [2026-10-12-functional-relation-carrier.md](2026-10-12-functional-relation-carrier.md) | | 2026-10-12 | `D-CMH-G1-4b` | | [2026-10-12-edge-table-anchor-distinct.md](2026-10-12-edge-table-anchor-distinct.md) | diff --git a/.claude/plans/nal-polyfill-v1.md b/.claude/plans/nal-polyfill-v1.md index a15bfbbc9..e3b6201a3 100644 --- a/.claude/plans/nal-polyfill-v1.md +++ b/.claude/plans/nal-polyfill-v1.md @@ -296,6 +296,33 @@ Rules that follow, each enforced in W6: - **`PROC_DEF` execution.** The same OPEN item as D-SQLP: until it exists, procedures are inlined call sequences. +### Status 2026-10-12 (D-NALP-1/2, `crates/lance-graph-nal`) + +- **Statements are coordinates, not rows with keys.** Reading + `nal-statement-v1` puts a belief on one `FacetCascade` key, semantic tiles + `[classid hi, classid lo, S, copula, P, 255 − c, 255 − f, 0]`, sealed into a + `SealedFacetLane` with its witness. This replaces the §4 A lowering column + for the first three rows: a belief query, a threshold within a statement and + choice are witnessed ranges (two `partition_point`s), not lane predicates. +- **Term width (OPEN above): decided `u16` per tile.** A vocabulary past + 65,536 terms scales through the classid prefix, not by widening a tile. +- **Bind once, run the kept program.** `nal::{Question, TruthAtLeast, + Support, TruthAtLeastAcross}` run their R2IL procedure once at bind and keep + the mask-risc `Program`; a call runs only `execute_compiled`. A query whose + rows are one range passes that range as the execution extent, so a lane + predicate reads only the range (`TruthAtLeastAcross`: 195 µs → 139 ns at + 1,048,576 rows once the range became the extent instead of an ANDed mask). +- **Measured** (`examples/nal_cost.rs`, release, AVX-512, median of 101, answers + asserted first), n = 1,048,576: range counts 24 ns, `TruthAtLeastAcross` 139 + ns over a 245-row subject, `Choice::of` 265 ns, bind 524 ns; the row loop + 554–614 µs. +- **OPEN, found here:** R2IL `RANGE` (arity 0) takes its bounds from two `u8` + immediates, so through the byte it cannot name a range past row 255. The + procedures emit the bound with `FoldDialect::emit_pred(Pred::Range)`. An + `ogar-r2il` request, not minted here. +- Not yet: the ClassView `WideFieldMask` projection and the binder's mask + (rest of D-NALP-1); W3 onwards. + ## 5. Waves | Wave | D-id | Repo | Work | Gate (all must pass) | From d406c6e689295859cfe1a689e66e4aace4004984 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 22:11:31 +0000 Subject: [PATCH 03/13] lance-graph-nal: NodeGuid as address and name, selection as a byte aperture The key carries only the belief's name (classid, S, copula, P, id); f and c are value lanes written in the sealed order. A selection is Aperture { key, bytes }: the leading bytes cared, the rest free, bound through the contract's SealedFacetLane::bound_aperture. Confidence thresholds are lane predicates over the aperture's range as the execution extent; Choice is a MAX fold. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- crates/lance-graph-nal/examples/nal_cost.rs | 45 ++-- crates/lance-graph-nal/src/lib.rs | 249 ++++++++++---------- crates/lance-graph-nal/src/nal.rs | 204 +++++++--------- crates/lance-graph-nal/tests/w2.rs | 175 ++++++++------ 4 files changed, 349 insertions(+), 324 deletions(-) diff --git a/crates/lance-graph-nal/examples/nal_cost.rs b/crates/lance-graph-nal/examples/nal_cost.rs index f521aa385..ced0bd81d 100644 --- a/crates/lance-graph-nal/examples/nal_cost.rs +++ b/crates/lance-graph-nal/examples/nal_cost.rs @@ -7,12 +7,14 @@ use std::hint::black_box; use std::time::Instant; -use lance_graph_nal::{nal, Belief, Beliefs, Copula}; +use lance_graph_nal::{nal, Aperture, Belief, Beliefs, Copula}; + +const CLASS: u32 = 0x0E01_0001; fn fixture(n: usize) -> Vec { let mut x: u64 = 0x9E37_79B9_7F4A_7C15; (0..n) - .map(|_| { + .map(|i| { x = x.wrapping_add(0x9E37_79B9_7F4A_7C15); let mut z = x; z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); @@ -22,6 +24,7 @@ fn fixture(n: usize) -> Vec { s: (r % 4096) as u16, copula: Copula::ALL[((r >> 12) % 4) as usize], p: ((r >> 16) % 4096) as u16, + id: i as u16, f: (r >> 32) as u8, c: (r >> 40) as u8, } @@ -55,19 +58,23 @@ fn main() { println!("target: {simd}"); for n in [65_536usize, 1_048_576] { let input = fixture(n); - let beliefs = Beliefs::seal(0x0E01_0001, &input, 1).unwrap(); + let beliefs = Beliefs::seal(CLASS, &input, 1).unwrap(); let b = beliefs.bind(&beliefs.witness()).unwrap(); let st = input[n / 2].statement(); let s = st.s; + let ast = Aperture::statement(CLASS, st); + let asub = Aperture::subject(CLASS, s); - let q = nal::Question::bind(&b, st).unwrap(); + let q = nal::Question::bind(&b, ast).unwrap(); let mut qs = q.scratch(&b).unwrap(); - let t = nal::TruthAtLeast::bind(&b, st, 128).unwrap(); + let t = nal::TruthAtLeast::bind(&b, ast, 128).unwrap(); let mut ts = t.scratch(&b).unwrap(); - let sup = nal::Support::bind(&b, s).unwrap(); + let sup = nal::Question::bind(&b, asub).unwrap(); let mut ss = sup.scratch(&b).unwrap(); - let across = nal::TruthAtLeastAcross::bind(&b, s, 128).unwrap(); + let across = nal::TruthAtLeast::bind(&b, asub, 128).unwrap(); let mut asx = across.scratch(&b).unwrap(); + let ch = nal::Choice::bind(&b, asub).unwrap(); + let mut cs = ch.scratch(&b).unwrap(); let same = |x: &Belief| x.statement() == st; assert_eq!( @@ -86,6 +93,14 @@ fn main() { across.count(&b, &mut asx).unwrap(), input.iter().filter(|x| x.s == s && x.c >= 128).count() ); + assert_eq!( + ch.max(&b, &mut cs).unwrap(), + input + .iter() + .filter(|x| x.s == s) + .map(|x| i32::from(x.c)) + .max() + ); println!( "n = {n} (subject support = {})", @@ -95,30 +110,28 @@ fn main() { r( "bind Question (search + procedure)", median_ns(100, || { - nal::Question::bind(&b, st).unwrap().program().ops.len() + nal::Question::bind(&b, ast).unwrap().program().ops.len() }), ); r( - "Question::count (kept program)", + "Question::count (statement)", median_ns(10_000, || q.count(&b, &mut qs).unwrap()), ); r( - "TruthAtLeast::count", + "TruthAtLeast::count (statement)", median_ns(10_000, || t.count(&b, &mut ts).unwrap()), ); r( - "Support::count", + "Question::count (subject)", median_ns(10_000, || sup.count(&b, &mut ss).unwrap()), ); r( - "TruthAtLeastAcross::count", + "TruthAtLeast::count (subject)", median_ns(1_000, || across.count(&b, &mut asx).unwrap()), ); r( - "Choice::of", - median_ns(10_000, || { - nal::Choice::of(&b, st).map_or(0, |x| x.c as usize) - }), + "Choice::max (subject)", + median_ns(1_000, || ch.max(&b, &mut cs).unwrap().unwrap_or(0) as usize), ); r( "row loop: count statement", diff --git a/crates/lance-graph-nal/src/lib.rs b/crates/lance-graph-nal/src/lib.rs index 721cc0720..9e42a18bb 100644 --- a/crates/lance-graph-nal/src/lib.rs +++ b/crates/lance-graph-nal/src/lib.rs @@ -1,31 +1,33 @@ //! `lance-graph-nal` — NAL over the fold path (D-NALP-1/2, //! `.claude/plans/nal-polyfill-v1.md`). //! -//! # Statements are coordinates +//! # The NodeGuid is the address and the distinguished name //! -//! A belief is not a row with foreign keys. It is an address: the reading -//! `nal-statement-v1` places it on one [`FacetCascade`] key, +//! A belief's key is its [`NodeGuid`]: 16 bytes, `classid(4) | 6 × (8:8)`, read +//! here as the reading `nal-statement-v1`, //! //! ```text -//! semantic tiles: [classid hi, classid lo, S, copula, P, 255 − c, 255 − f, 0] +//! semantic tiles: [classid hi, classid lo, S, copula, P, id, 0, 0] //! ``` //! -//! and the population is a [`SealedFacetLane`] of those keys, sorted at seal -//! and attested by an [`OrderedLaneWitness`]. Every query below is therefore -//! a coordinate range, found with two `partition_point`s: +//! The key names the belief and nothing else. Its truth value is not part of +//! its name: `f` and `c` are value lanes beside the key, in the key's sealed +//! order. //! -//! - `?` is the prefix of depth 5; -//! - "every belief about S" is the prefix of depth 3; -//! - `c ≥ t` within a statement is the start of that statement's range up to -//! the key with tile 5 = `255 − t`, because confidence is stored inverted -//! and the range is ordered by it; -//! - the highest-confidence belief of a statement (NAL choice) is the first -//! row of its range. +//! # A selection is the key minus an aperture //! -//! The confidence lane `c` handed to mask-risc is decoded from the sealed keys -//! themselves, so it is in the sealed order by construction: the obligation -//! `ISS-WITNESSED-RANGE-DOES-NOT-ATTEST-PLANE-ORDER` names cannot be broken -//! here. +//! A query names its rows with an [`Aperture`]: a key and a count `n` of +//! leading bytes that must match; the remaining `16 − n` bytes are free. On the +//! sealed, witnessed order (HHTL Cartesian: classid, then each tile coarse to +//! fine) the matches are one contiguous range, which +//! [`SealedFacetLane::bound_aperture`] finds with two `partition_point`s. +//! +//! - `n = 10`: one statement `` (classid 4 + S 2 + copula 2 + P 2); +//! - `n = 6`: every belief about subject `S`; +//! - `n = 4`: the whole statement class. +//! +//! A query runs over its range as the execution extent; a lane predicate (a +//! confidence threshold) reads only the rows of that range. //! //! # Operations are procedures, compiled once //! @@ -37,10 +39,10 @@ //! //! # Not here yet //! -//! Revision (W3), the per-row truth laws (W4), syllogisms (W5) and the -//! Narsese text front end (W6). The term width is `u16` per tile: a -//! vocabulary larger than 65,536 terms scales through the classid prefix, not -//! by widening the tile. +//! Revision (W3), the per-row truth laws (W4), syllogisms (W5), the Narsese +//! text front end (W6), and an argmax fold (which belief holds the maximum). +//! The term width is `u16` per tile: a vocabulary larger than 65,536 terms +//! scales through the classid prefix, not by widening the tile. //! //! OPEN: R2IL `RANGE` takes its bounds from two `u8` immediates (`ogar-r2il` //! arity 0), so it cannot name a range past row 255. The procedures emit the @@ -50,7 +52,8 @@ pub mod nal; -use lance_graph_contract::facet::FacetCascade; +use lance_graph_contract::canonical_node::NodeGuid; +use lance_graph_contract::facet::{FacetCascade, SemanticAperture}; use lance_graph_contract::ordered_lane::{OrderedLaneWitness, SealedFacetLane, WitnessError}; use lance_graph_contract::temporal_pov::LanceVersion; use lance_graph_mask_risc::{execute_compiled, ExecError, LaneRef, Program}; @@ -79,10 +82,6 @@ impl Copula { Copula::Implication, Copula::Equivalence, ]; - - fn from_code(c: u16) -> Option { - Self::ALL.into_iter().find(|k| *k as u16 == c) - } } /// A statement ``: two term addresses and a copula. @@ -96,8 +95,9 @@ pub struct Statement { pub p: u16, } -/// One belief: a statement with a truth value in `u8` codes (`f`, `c` scaled -/// to `0..=255`, the codes `causal_edge::isa::truth` reads). +/// One belief: its name (statement and identity) and its truth value in `u8` +/// codes (`f`, `c` scaled to `0..=255`, the codes `causal_edge::isa::truth` +/// reads). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Belief { /// Subject term address. @@ -106,6 +106,8 @@ pub struct Belief { pub copula: Copula, /// Predicate term address. pub p: u16, + /// Identity of this belief among the beliefs of its statement. + pub id: u16, /// Frequency code. pub f: u8, /// Confidence code. @@ -123,61 +125,76 @@ impl Belief { } } -/// Semantic tile positions of the `nal-statement-v1` reading. -const T_S: usize = 2; -const T_COPULA: usize = 3; -const T_P: usize = 4; -const T_INV_C: usize = 5; -const T_INV_F: usize = 6; - -fn tiles(class: u32, s: u16, copula: u16, p: u16, inv_c: u16, inv_f: u16, last: u16) -> [u16; 8] { - [ +/// The NodeGuid of a belief of class `class`. +pub fn guid(class: u32, s: u16, copula: Copula, p: u16, id: u16) -> NodeGuid { + FacetCascade::from_semantic_tiles([ (class >> 16) as u16, class as u16, s, - copula, + copula as u16, p, - inv_c, - inv_f, - last, - ] + id, + 0, + 0, + ]) + .into() } -fn key_of(class: u32, b: &Belief) -> FacetCascade { - FacetCascade::from_semantic_tiles(tiles( - class, - b.s, - b.copula as u16, - b.p, - u16::from(255 - b.c), - u16::from(255 - b.f), - 0, - )) +/// A selection: `key` with only its first `bytes` bytes (in semantic order: +/// classid, then tiles coarse to fine) cared, the rest free. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Aperture { + /// The key the cared bytes are read from. + pub key: NodeGuid, + /// Leading bytes that must match, `0..=16`. + pub bytes: u8, } -fn belief_of(k: &FacetCascade) -> Option { - let t = k.semantic_tiles(); - Some(Belief { - s: t[T_S], - copula: Copula::from_code(t[T_COPULA])?, - p: t[T_P], - c: 255 - u8::try_from(t[T_INV_C]).ok()?, - f: 255 - u8::try_from(t[T_INV_F]).ok()?, - }) +impl Aperture { + /// One statement of class `class` (10 bytes cared). + pub fn statement(class: u32, st: Statement) -> Self { + Aperture { + key: guid(class, st.s, st.copula, st.p, 0), + bytes: 10, + } + } + + /// Every belief about subject `s` in class `class` (6 bytes cared). + pub fn subject(class: u32, s: u16) -> Self { + Aperture { + key: guid(class, s, Copula::Inheritance, 0, 0), + bytes: 6, + } + } + + /// The contract's aperture: care = the leading `bytes` bytes. + pub fn semantic(self) -> SemanticAperture { + let bits = 8 * u32::from(self.bytes.min(16)); + let care: u128 = if bits == 0 { + 0 + } else { + u128::MAX << (128 - bits) + }; + let tiles = core::array::from_fn(|i| (care >> (112 - 16 * i)) as u16); + SemanticAperture::new(self.key.facet(), FacetCascade::from_semantic_tiles(tiles)) + } } -/// A sealed belief population: the ordered key lane and the confidence lane -/// decoded from it. +/// A sealed belief population: the ordered NodeGuid lane and the truth lanes +/// in the same order. #[derive(Debug, Clone)] pub struct Beliefs { class: u32, lane: SealedFacetLane, + pub(crate) f: Vec, pub(crate) c: Vec, } impl Beliefs { - /// Seal `beliefs` under the statement class `class` at `version`. Sorting - /// happens here, once; every read after it is a fold. + /// Seal `beliefs` under the statement class `class` at `version`. The + /// beliefs are sorted by key here, once, and the truth lanes are written in + /// that order, so key row `i` and truth row `i` are one belief by + /// construction. /// /// # Errors /// @@ -187,14 +204,27 @@ impl Beliefs { beliefs: &[Belief], version: LanceVersion, ) -> Result { - let lane = - SealedFacetLane::seal(beliefs.iter().map(|b| key_of(class, b)).collect(), version)?; - let c = lane - .keys() + let mut rows: Vec<(FacetCascade, i32, i32)> = beliefs .iter() - .map(|k| 255 - i32::from(k.semantic_tiles()[T_INV_C])) + .map(|b| { + ( + guid(class, b.s, b.copula, b.p, b.id).facet(), + i32::from(b.f), + i32::from(b.c), + ) + }) .collect(); - Ok(Beliefs { class, lane, c }) + rows.sort_by(|a, b| a.0.cmp_numeric_projection(&b.0)); + let f = rows.iter().map(|r| r.1).collect(); + let c = rows.iter().map(|r| r.2).collect(); + let lane = + SealedFacetLane::attest_sorted(rows.into_iter().map(|r| r.0).collect(), version)?; + Ok(Beliefs { class, lane, f, c }) + } + + /// The statement class. + pub fn class(&self) -> u32 { + self.class } /// The witness the lane was sealed with. A planner holds it and hands it @@ -211,7 +241,10 @@ impl Beliefs { /// [`NalError::Witness`] when the witness is from another seal. pub fn bind(&self, witness: &OrderedLaneWitness) -> Result, NalError> { self.lane.validate(witness)?; - Ok(Bound { beliefs: self }) + Ok(Bound { + beliefs: self, + witness: *witness, + }) } } @@ -219,71 +252,45 @@ impl Beliefs { #[derive(Debug, Clone, Copy)] pub struct Bound<'a> { pub(crate) beliefs: &'a Beliefs, + witness: OrderedLaneWitness, } -/// Lane index of the confidence lane in the planes every NAL program runs over. -pub(crate) const LANE_C: u16 = 0; +/// Lane index of `c` in the planes every NAL program runs over (`f` is lane 0). +pub(crate) const LANE_C: u16 = 1; -impl<'a> Bound<'a> { +impl Bound<'_> { /// Row count. pub fn n_rows(&self) -> usize { self.beliefs.c.len() } - fn keys(&self) -> &'a [FacetCascade] { - self.beliefs.lane.keys() - } - - /// The belief stored at sealed row `row`. - pub fn belief(&self, row: usize) -> Option { - self.keys().get(row).and_then(belief_of) - } - - /// `[lo, hi)` of every key in the closed interval `[lo_t, hi_t]` of - /// semantic tiles: two `partition_point`s on the witnessed order. - pub(crate) fn interval(&self, lo_t: [u16; 8], hi_t: [u16; 8]) -> (u32, u32) { - let (lo_k, hi_k) = ( - FacetCascade::from_semantic_tiles(lo_t), - FacetCascade::from_semantic_tiles(hi_t), - ); - let keys = self.keys(); - let lo = keys.partition_point(|k| k.cmp_numeric_projection(&lo_k).is_lt()); - let hi = keys.partition_point(|k| k.cmp_numeric_projection(&hi_k).is_le()); - (lo as u32, hi.max(lo) as u32) - } - - /// The rows of one statement. - pub(crate) fn statement(&self, st: Statement) -> (u32, u32) { - self.statement_at_least(st, 0) - } - - /// The rows of one statement with confidence `>= t`. - pub(crate) fn statement_at_least(&self, st: Statement, t: u8) -> (u32, u32) { - let class = self.beliefs.class; - let cop = st.copula as u16; - self.interval( - tiles(class, st.s, cop, st.p, 0, 0, 0), - tiles(class, st.s, cop, st.p, u16::from(255 - t), 0xFFFF, 0xFFFF), - ) + /// The statement class. + pub fn class(&self) -> u32 { + self.beliefs.class } - /// The rows of every belief about subject `s`. - pub(crate) fn subject(&self, s: u16) -> (u32, u32) { - let class = self.beliefs.class; - self.interval( - tiles(class, s, 0, 0, 0, 0, 0), - tiles(class, s, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF), - ) + /// The rows `[lo, hi)` an aperture selects. + /// + /// # Errors + /// + /// [`NalError::Witness`] if the witness no longer validates; + /// [`NalError::NotAnInterval`] if the aperture's care has a hole (it + /// cannot: [`Aperture`] cares a leading run of bytes). + pub fn range(&self, a: Aperture) -> Result<(u32, u32), NalError> { + self.beliefs + .lane + .bound_aperture(&self.witness, &a.semantic())? + .ok_or(NalError::NotAnInterval) } - /// Run a kept program over this population. + /// Run a kept program over `extent`. pub(crate) fn run( &self, program: &Program, scratch: &mut lance_graph_mask_risc::Scratch<'_>, extent: std::ops::Range, ) -> Result { - let lanes = [LaneRef::I32(&self.beliefs.c)]; + let lanes = [LaneRef::I32(&self.beliefs.f), LaneRef::I32(&self.beliefs.c)]; let planes = lance_graph_mask_risc::Planes { n_rows: self.n_rows(), masks: &[], @@ -309,6 +316,8 @@ impl<'a> Bound<'a> { pub enum NalError { /// The ordering witness does not belong to this lane. Witness(WitnessError), + /// The aperture is not one interval of the sealed order. + NotAnInterval, /// The procedure was refused while binding. Fold(lance_graph_r2il::FoldError), /// mask-risc refused the kept program. diff --git a/crates/lance-graph-nal/src/nal.rs b/crates/lance-graph-nal/src/nal.rs index e89e4ac55..dd5764152 100644 --- a/crates/lance-graph-nal/src/nal.rs +++ b/crates/lance-graph-nal/src/nal.rs @@ -1,19 +1,22 @@ //! The typed NAL surface: `nal::{Operation}::bind(…)` (plan §4 G). //! //! Each operation is a zero-sized type. [`NalOp::lower`] is its procedure: the -//! R2IL calls it emits through [`FoldDialect`], reading its bound coordinates -//! as scalars off the loco stack. `bind` resolves the coordinates on the -//! witnessed order, runs the procedure ONCE and keeps the mask-risc program it -//! produced as a [`Prepared`]; every call after that runs the kept program. +//! R2IL calls it emits through [`FoldDialect`], reading its scalars off the +//! loco stack. `bind` resolves the [`Aperture`] to its range on the witnessed +//! order, runs the procedure ONCE and keeps the mask-risc program it produced +//! as a [`Prepared`]; every call after that runs the kept program over the +//! range. //! //! None of the operations here earns a byte (plan §4 A): `FN` is `None` for //! all of them. +use std::ops::Range; + use lance_graph_mask_risc::{Foreign, LaneRef, Lowering, Planes, Pred, Program, Scratch, Value}; -use lance_graph_r2il::{procs, FoldDialect, FoldError, Val, INT_S_LESS, LOAD}; +use lance_graph_r2il::{procs, FoldDialect, FoldError, Val, INT_S_LESS, LOAD, POP_COUNT}; use ogar_loco::{Dialect, FnIndex}; -use crate::{Belief, Bound, NalError, Statement, LANE_C}; +use crate::{Aperture, Bound, NalError, LANE_C}; /// One NAL operation: its byte, if it earned one, and its procedure. pub trait NalOp { @@ -26,51 +29,38 @@ pub trait NalOp { fn lower(d: &mut FoldDialect<'_>, stack: &mut Vec) -> Result<(), FoldError>; } -fn pop_u32(stack: &mut Vec, f: FnIndex) -> Result { +fn pop_i64(stack: &mut Vec) -> Result { match stack.pop() { - Some(Val::Scalar(v)) => u32::try_from(v).map_err(|_| FoldError::WrongOperandKind(f)), - Some(_) => Err(FoldError::WrongOperandKind(f)), - None => Err(FoldError::Underflow(f)), + Some(Val::Scalar(v)) => Ok(v), + Some(_) => Err(FoldError::WrongOperandKind(POP_COUNT)), + None => Err(FoldError::Underflow(POP_COUNT)), } } -/// `lo hi -- n`: the number of rows in the coordinate range `[lo, hi)`. -/// -/// Calls: the range predicate, `PopCount`. -fn range_count(d: &mut FoldDialect<'_>, stack: &mut Vec) -> Result<(), FoldError> { - let hi = pop_u32(stack, procs_fn())?; - let lo = pop_u32(stack, procs_fn())?; - d.emit_pred(Pred::Range { lo, hi }, stack); - procs::count(d, stack) +fn pop_u32(stack: &mut Vec) -> Result { + u32::try_from(pop_i64(stack)?).map_err(|_| FoldError::WrongOperandKind(POP_COUNT)) } -/// The function index named in a refusal raised by these procedures. -fn procs_fn() -> FnIndex { - lance_graph_r2il::POP_COUNT +/// `lo hi --`, pushing the mask of rows `[lo, hi)`. +fn range(d: &mut FoldDialect<'_>, stack: &mut Vec) -> Result<(), FoldError> { + let hi = pop_u32(stack)?; + let lo = pop_u32(stack)?; + d.emit_pred(Pred::Range { lo, hi }, stack); + Ok(()) } -/// A query bound to its coordinates, with the program its procedure produced. -/// -/// `extent` is the coordinate range the program runs over. A query whose rows -/// are one range (a statement, a subject) is restricted to it before any lane -/// is read, so its cost follows the range, not the population. +/// A query bound to its range, with the program its procedure produced. #[derive(Debug, Clone)] pub struct Prepared { program: Program, - extent: std::ops::Range, + extent: Range, } impl Prepared { - fn of(b: &Bound<'_>, args: &[i64]) -> Result { - Self::over::(b, args, 0..b.n_rows()) - } - - fn over( - b: &Bound<'_>, - args: &[i64], - extent: std::ops::Range, - ) -> Result { - let lanes = [LaneRef::I32(&b.beliefs.c)]; + /// Run `O`'s procedure once over `extent` with `args` on the stack and keep + /// the program. + fn of(b: &Bound<'_>, args: &[i64], extent: Range) -> Result { + let lanes = [LaneRef::I32(&b.beliefs.f), LaneRef::I32(&b.beliefs.c)]; let planes = Planes { n_rows: b.n_rows(), masks: &[], @@ -115,132 +105,120 @@ impl Prepared { &self.program } - /// Run the kept program and read its count. + /// The rows the program runs over. + pub fn extent(&self) -> Range { + self.extent.clone() + } + + fn value(&self, b: &Bound<'_>, scratch: &mut Scratch<'_>) -> Result { + Ok(b.run(&self.program, scratch, self.extent.clone())?) + } + + /// Run a counting query. /// /// # Errors /// - /// [`NalError::Exec`] if `b` is not the population the query was bound on - /// (a range past its rows), or [`NalError::NoAnswer`]. + /// [`NalError::Exec`] if `b` is not the population the query was bound on, + /// or [`NalError::NoAnswer`] if the query does not count. pub fn count(&self, b: &Bound<'_>, scratch: &mut Scratch<'_>) -> Result { - match b.run(&self.program, scratch, self.extent.clone())? { + match self.value(b, scratch)? { Value::Count(n) => Ok(n), _ => Err(NalError::NoAnswer), } } + + /// Run a maximum query; `None` for an empty selection. + /// + /// # Errors + /// + /// As [`Prepared::count`]. + pub fn max(&self, b: &Bound<'_>, scratch: &mut Scratch<'_>) -> Result, NalError> { + match self.value(b, scratch)? { + Value::OptI32(v) => Ok(v), + _ => Err(NalError::NoAnswer), + } + } } -/// `?` — how many beliefs the population holds about a statement. +/// How many beliefs an aperture selects: `?` with a statement +/// aperture, the support of `S` with a subject aperture. pub struct Question; impl NalOp for Question { const FN: Option = None; const ARITY: (u8, u8) = (2, 1); + /// `lo hi -- n`. Calls: the range predicate, `PopCount`. fn lower(d: &mut FoldDialect<'_>, stack: &mut Vec) -> Result<(), FoldError> { - range_count(d, stack) + range(d, stack)?; + procs::count(d, stack) } } impl Question { - /// Bind the statement's coordinate range. + /// Bind the aperture's range. /// /// # Errors /// - /// [`NalError`] if the procedure is refused. - pub fn bind(b: &Bound<'_>, st: Statement) -> Result { - let (lo, hi) = b.statement(st); - Prepared::of::(b, &[i64::from(lo), i64::from(hi)]) + /// [`NalError`] if the witness or the procedure is refused. + pub fn bind(b: &Bound<'_>, a: Aperture) -> Result { + let (lo, hi) = b.range(a)?; + Prepared::of::(b, &[i64::from(lo), i64::from(hi)], 0..b.n_rows()) } } -/// The beliefs of a statement with confidence code `>= t`. +/// The beliefs an aperture selects with confidence code `>= t`. Confidence is +/// a value lane, not part of the name, so this is a lane predicate; it reads +/// only the aperture's range, which is the execution extent. pub struct TruthAtLeast; impl NalOp for TruthAtLeast { const FN: Option = None; - const ARITY: (u8, u8) = (2, 1); + const ARITY: (u8, u8) = (1, 1); + /// `t -- n`, counting `t − 1 < c`. Calls: `LOAD`, `IntSLess`, `PopCount`. fn lower(d: &mut FoldDialect<'_>, stack: &mut Vec) -> Result<(), FoldError> { - range_count(d, stack) + let t = pop_i64(stack)?; + stack.push(Val::Scalar(t - 1)); + stack.push(Val::Scalar(i64::from(LANE_C))); + d.call(LOAD, [1, 0, 0], stack)?; + d.call(INT_S_LESS, [0; 3], stack)?; + procs::count(d, stack) } } impl TruthAtLeast { - /// Bind the statement's range cut at `t`: confidence is stored inverted, - /// so the threshold is the end of a prefix of the statement's range. + /// Bind the aperture's range and the threshold. /// /// # Errors /// - /// [`NalError`] if the procedure is refused. - pub fn bind(b: &Bound<'_>, st: Statement, t: u8) -> Result { - let (lo, hi) = b.statement_at_least(st, t); - Prepared::of::(b, &[i64::from(lo), i64::from(hi)]) + /// [`NalError`] if the witness or the procedure is refused. + pub fn bind(b: &Bound<'_>, a: Aperture, t: u8) -> Result { + let (lo, hi) = b.range(a)?; + Prepared::of::(b, &[i64::from(t)], lo as usize..hi as usize) } } -/// Every belief about a subject: the support of `S` in the population. -pub struct Support; +/// NAL choice: the highest confidence among the beliefs an aperture selects. +/// Which belief holds it (an argmax) is not a fold mask-risc has yet. +pub struct Choice; -impl NalOp for Support { +impl NalOp for Choice { const FN: Option = None; const ARITY: (u8, u8) = (2, 1); + /// `lo hi -- max c`. Calls: the range predicate, `LOAD`, `MAX`. fn lower(d: &mut FoldDialect<'_>, stack: &mut Vec) -> Result<(), FoldError> { - range_count(d, stack) + range(d, stack)?; + procs::max(d, stack, LANE_C) } } -impl Support { - /// Bind the subject's range. - /// - /// # Errors - /// - /// [`NalError`] if the procedure is refused. - pub fn bind(b: &Bound<'_>, s: u16) -> Result { - let (lo, hi) = b.subject(s); - Prepared::of::(b, &[i64::from(lo), i64::from(hi)]) - } -} - -/// Beliefs about a subject with confidence `>= t`, over every statement of -/// that subject. The subject is one range, the threshold is not (each -/// statement restarts the confidence order), so the program reads the `c` -/// lane, and only over the subject's range: the range is the execution -/// extent, never a mask the lane predicate is ANDed with. -pub struct TruthAtLeastAcross; - -impl NalOp for TruthAtLeastAcross { - const FN: Option = None; - const ARITY: (u8, u8) = (1, 1); - /// `t -- n`, counting `t − 1 < c`. Calls: `LOAD`, `IntSLess`, `PopCount`. - fn lower(d: &mut FoldDialect<'_>, stack: &mut Vec) -> Result<(), FoldError> { - let t = pop_u32(stack, procs_fn())?; - stack.push(Val::Scalar(i64::from(t) - 1)); - stack.push(Val::Scalar(i64::from(LANE_C))); - d.call(LOAD, [1, 0, 0], stack)?; - d.call(INT_S_LESS, [0; 3], stack)?; - procs::count(d, stack) - } -} - -impl TruthAtLeastAcross { - /// Bind the subject's range and the threshold. +impl Choice { + /// Bind the aperture's range. /// /// # Errors /// - /// [`NalError`] if the procedure is refused. - pub fn bind(b: &Bound<'_>, s: u16, t: u8) -> Result { - let (lo, hi) = b.subject(s); - Prepared::over::(b, &[i64::from(t)], lo as usize..hi as usize) - } -} - -/// NAL choice: the highest-confidence belief about a statement. -pub struct Choice; - -impl Choice { - /// The first row of the statement's range. Confidence is stored inverted, - /// so that row has the highest confidence; among equal confidence, the - /// highest frequency. `None` when the statement has no belief. - pub fn of(b: &Bound<'_>, st: Statement) -> Option { - let (lo, hi) = b.statement(st); - (lo < hi).then(|| b.belief(lo as usize)).flatten() + /// [`NalError`] if the witness or the procedure is refused. + pub fn bind(b: &Bound<'_>, a: Aperture) -> Result { + let (lo, hi) = b.range(a)?; + Prepared::of::(b, &[i64::from(lo), i64::from(hi)], lo as usize..hi as usize) } } diff --git a/crates/lance-graph-nal/tests/w2.rs b/crates/lance-graph-nal/tests/w2.rs index dbc0cca71..614c802ed 100644 --- a/crates/lance-graph-nal/tests/w2.rs +++ b/crates/lance-graph-nal/tests/w2.rs @@ -3,7 +3,7 @@ use lance_graph_contract::ordered_lane::OrderedLaneWitness; use lance_graph_mask_risc::Lowering; -use lance_graph_nal::{nal, Belief, Beliefs, Copula, NalError, Statement}; +use lance_graph_nal::{nal, Aperture, Belief, Beliefs, Copula, NalError, Statement}; const CLASS: u32 = 0x0E01_0001; @@ -18,12 +18,13 @@ fn fixture(n: usize) -> Vec { z ^ (z >> 31) }; (0..n) - .map(|_| { + .map(|i| { let r = next(); Belief { s: (r % 40) as u16, copula: Copula::ALL[((r >> 8) % 4) as usize], p: ((r >> 16) % 40) as u16, + id: i as u16, f: (r >> 24) as u8, c: (r >> 32) as u8, } @@ -48,10 +49,10 @@ fn statements() -> Vec { } fn same(b: &Belief, st: Statement) -> bool { - b.s == st.s && b.copula == st.copula && b.p == st.p + b.statement() == st } -/// FAILS IF: the statement range is bound on the wrong tiles, or the compiled +/// FAILS IF: the statement aperture cares the wrong bytes, or the compiled /// fold counts anything but the bound range. #[test] fn question_count_equals_the_row_oracle() { @@ -60,11 +61,11 @@ fn question_count_equals_the_row_oracle() { let bound = beliefs.bind(&beliefs.witness()).unwrap(); let mut hits = 0; for st in statements() { - let q = nal::Question::bind(&bound, st).unwrap(); + let q = nal::Question::bind(&bound, Aperture::statement(CLASS, st)).unwrap(); let mut scratch = q.scratch(&bound).unwrap(); let want = input.iter().filter(|b| same(b, st)).count(); assert_eq!(q.count(&bound, &mut scratch).unwrap(), want, "{st:?}"); - // The prepared program answers again, unchanged. + // The kept program answers again, unchanged. assert_eq!(q.count(&bound, &mut scratch).unwrap(), want, "{st:?}"); hits += usize::from(want > 0); } @@ -72,21 +73,57 @@ fn question_count_equals_the_row_oracle() { assert!(hits > 5 && hits < statements().len(), "hits = {hits}"); } +/// FAILS IF: the aperture byte count does not decide the selection: the class +/// (4 bytes), the subject (6) and the statement (10) of one key must nest, and +/// another class must select nothing. +#[test] +fn the_aperture_width_is_the_selection() { + let input = fixture(5_000); + let beliefs = sealed(&input); + let b = beliefs.bind(&beliefs.witness()).unwrap(); + let st = input[17].statement(); + let key = lance_graph_nal::guid(CLASS, st.s, st.copula, st.p, 0); + let count = |a: Aperture| { + let q = nal::Question::bind(&b, a).unwrap(); + q.count(&b, &mut q.scratch(&b).unwrap()).unwrap() + }; + let class = count(Aperture { key, bytes: 4 }); + let subject = count(Aperture { key, bytes: 6 }); + let statement = count(Aperture { key, bytes: 10 }); + assert_eq!(class, input.len()); + assert_eq!(subject, input.iter().filter(|x| x.s == st.s).count()); + assert_eq!(statement, input.iter().filter(|x| same(x, st)).count()); + assert!(class > subject && subject > statement && statement > 0); + let other = lance_graph_nal::guid(CLASS + 1, st.s, st.copula, st.p, 0); + assert_eq!( + count(Aperture { + key: other, + bytes: 4 + }), + 0 + ); + // Zero bytes cared selects every row. + assert_eq!( + count(Aperture { + key: other, + bytes: 0 + }), + input.len() + ); +} + /// FAILS IF: the question folds through anything but the fused range, i.e. /// the coordinate bound did not reach mask-risc as a range. #[test] fn question_lowers_to_the_fused_range() { let beliefs = sealed(&fixture(2_000)); let bound = beliefs.bind(&beliefs.witness()).unwrap(); - let q = nal::Question::bind( - &bound, - Statement { - s: 3, - copula: Copula::Inheritance, - p: 5, - }, - ) - .unwrap(); + let st = Statement { + s: 3, + copula: Copula::Inheritance, + p: 5, + }; + let q = nal::Question::bind(&bound, Aperture::statement(CLASS, st)).unwrap(); assert!( matches!(q.lowering(), Lowering::Range(_)), "{:?}", @@ -94,21 +131,24 @@ fn question_lowers_to_the_fused_range() { ); } -/// FAILS IF: choice is not the highest-confidence belief of the statement, or -/// reports one for a statement with no belief. +/// FAILS IF: choice is not the highest confidence of the selection, or reports +/// one for an empty selection. #[test] -fn choice_is_the_highest_confidence_belief() { +fn choice_is_the_highest_confidence() { let input = fixture(5_000); let beliefs = sealed(&input); let bound = beliefs.bind(&beliefs.witness()).unwrap(); let (mut some, mut none) = (0, 0); for st in statements() { - let want = input.iter().filter(|b| same(b, st)).map(|b| b.c).max(); - let got = nal::Choice::of(&bound, st); - assert_eq!(got.map(|b| b.c), want, "{st:?}"); - if let Some(b) = got { - assert!(same(&b, st)); - assert!(input.contains(&b), "choice must be a stored belief"); + let want = input + .iter() + .filter(|b| same(b, st)) + .map(|b| i32::from(b.c)) + .max(); + let q = nal::Choice::bind(&bound, Aperture::statement(CLASS, st)).unwrap(); + let got = q.max(&bound, &mut q.scratch(&bound).unwrap()).unwrap(); + assert_eq!(got, want, "{st:?}"); + if want.is_some() { some += 1; } else { none += 1; @@ -117,88 +157,73 @@ fn choice_is_the_highest_confidence_belief() { assert!(some > 0 && none > 0); } -/// FAILS IF: the confidence threshold is off by one, inverted, or ignores the -/// statement. +/// FAILS IF: the confidence threshold is off by one, inverted, ignores the +/// selection, or reads outside the selection's range. #[test] fn truth_at_least_equals_the_row_oracle() { let input = fixture(5_000); let beliefs = sealed(&input); let bound = beliefs.bind(&beliefs.witness()).unwrap(); let mut partial = 0; - for st in statements() { + let selections = statements() + .into_iter() + .map(|st| { + ( + Aperture::statement(CLASS, st), + Box::new(move |b: &Belief| same(b, st)) as Box bool>, + ) + }) + .chain([0u16, 3, 39, 40].into_iter().map(|s| { + ( + Aperture::subject(CLASS, s), + Box::new(move |b: &Belief| b.s == s) as Box bool>, + ) + })); + for (a, sel) in selections { for t in [0u8, 1, 100, 204, 255] { - let q = nal::TruthAtLeast::bind(&bound, st, t).unwrap(); + let q = nal::TruthAtLeast::bind(&bound, a, t).unwrap(); let mut scratch = q.scratch(&bound).unwrap(); - let all = input.iter().filter(|b| same(b, st)).count(); - let want = input.iter().filter(|b| same(b, st) && b.c >= t).count(); - assert_eq!(q.count(&bound, &mut scratch).unwrap(), want, "{st:?} t={t}"); + let all = input.iter().filter(|b| sel(b)).count(); + let want = input.iter().filter(|b| sel(b) && b.c >= t).count(); + assert_eq!(q.count(&bound, &mut scratch).unwrap(), want, "{a:?} t={t}"); partial += usize::from(want > 0 && want < all); } } - // Anti-vacuity: some thresholds cut inside a statement. + // Anti-vacuity: some thresholds cut inside a selection. assert!(partial > 0); } -/// FAILS IF: support over a subject prefix miscounts. -#[test] -fn support_counts_every_belief_about_a_subject() { - let input = fixture(5_000); - let beliefs = sealed(&input); - let bound = beliefs.bind(&beliefs.witness()).unwrap(); - for s in [0u16, 3, 39, 40] { - let q = nal::Support::bind(&bound, s).unwrap(); - let mut scratch = q.scratch(&bound).unwrap(); - let want = input.iter().filter(|b| b.s == s).count(); - assert_eq!(q.count(&bound, &mut scratch).unwrap(), want, "s={s}"); - } -} - -/// FAILS IF: the gated confidence predicate over a subject prefix (which is not -/// one interval) miscounts. -#[test] -fn truth_at_least_across_a_subject_equals_the_row_oracle() { - let input = fixture(5_000); - let beliefs = sealed(&input); - let bound = beliefs.bind(&beliefs.witness()).unwrap(); - for s in [0u16, 3, 39, 40] { - for t in [0u8, 128, 204, 255] { - let q = nal::TruthAtLeastAcross::bind(&bound, s, t).unwrap(); - let mut scratch = q.scratch(&bound).unwrap(); - let want = input.iter().filter(|b| b.s == s && b.c >= t).count(); - assert_eq!(q.count(&bound, &mut scratch).unwrap(), want, "s={s} t={t}"); - } - } -} - /// FAILS IF: the empty and the full population answer wrongly. #[test] fn empty_and_full_populations() { - let empty = sealed(&[]); - let b = empty.bind(&empty.witness()).unwrap(); let st = Statement { s: 1, copula: Copula::Similarity, p: 2, }; - let q = nal::Question::bind(&b, st).unwrap(); + let a = Aperture::statement(CLASS, st); + let empty = sealed(&[]); + let b = empty.bind(&empty.witness()).unwrap(); + let q = nal::Question::bind(&b, a).unwrap(); assert_eq!(q.count(&b, &mut q.scratch(&b).unwrap()).unwrap(), 0); - assert_eq!(nal::Choice::of(&b, st), None); + let q = nal::Choice::bind(&b, a).unwrap(); + assert_eq!(q.max(&b, &mut q.scratch(&b).unwrap()).unwrap(), None); - let one = vec![ - Belief { + let one: Vec = (0..300) + .map(|i| Belief { s: 1, copula: Copula::Similarity, p: 2, + id: i, f: 9, - c: 200 - }; - 300 - ]; + c: 200, + }) + .collect(); let full = sealed(&one); let b = full.bind(&full.witness()).unwrap(); - let q = nal::TruthAtLeast::bind(&b, st, 200).unwrap(); + let q = nal::TruthAtLeast::bind(&b, a, 200).unwrap(); assert_eq!(q.count(&b, &mut q.scratch(&b).unwrap()).unwrap(), 300); - let q = nal::TruthAtLeast::bind(&b, st, 201).unwrap(); + let q = nal::TruthAtLeast::bind(&b, a, 201).unwrap(); assert_eq!(q.count(&b, &mut q.scratch(&b).unwrap()).unwrap(), 0); } From 9e37c1847def2241b8606c87e4b1d6ad04026a11 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 22:12:21 +0000 Subject: [PATCH 04/13] board: D-NALP-1/2 restated on NodeGuid keys and byte apertures Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .claude/board/STATUS_BOARD.md | 4 +- .../2026-10-12-nal-coordinate-queries.md | 38 +++++++++-------- .claude/plans/nal-polyfill-v1.md | 42 ++++++++++++------- 3 files changed, 49 insertions(+), 35 deletions(-) diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 40c3558bd..a74af722a 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -35,8 +35,8 @@ Plan: `.claude/plans/nal-polyfill-v1.md`. Template: D-SQLP. No opcode, tenant or | D-id | scope | status | gate / falsifier | |---|---|---|---| | **D-NALP-0** | Plan, board rows | In PR | docs only | -| **D-NALP-1** | Statement layout + declared ClassView reading; additive `WideFieldMask` projection path on `ClassView`; binder's mask; term width decision | In PR (layout `nal-statement-v1` + term width u16; ClassView wide projection and binder mask still queued) | only the mask's lanes are read; undeclared reading and masked-out field refuse; Small = Wide answers | -| **D-NALP-2** | Query procedures: select, truth threshold, choice by confidence, support count | In PR (`crates/lance-graph-nal`, 8 oracle tests, 7 disable runs red; 22–140 ns at 1,048,576 rows) | equal to a row-at-a-time oracle; one-pattern threshold disable red; no new opcode | +| **D-NALP-1** | Statement layout + declared ClassView reading; additive `WideFieldMask` projection path on `ClassView`; binder's mask; term width decision | In PR #1525 (NodeGuid reading `nal-statement-v1`, term width u16, aperture selection; ClassView wide projection and binder mask still queued) | only the mask's lanes are read; undeclared reading and masked-out field refuse; Small = Wide answers | +| **D-NALP-2** | Query procedures: select, truth threshold, choice by confidence, support count | In PR #1525 (`crates/lance-graph-nal`: NodeGuid key, byte apertures; 7 oracle tests, 7 disable runs red; 19–248 ns at 1,048,576 rows) | equal to a row-at-a-time oracle; one-pattern threshold disable red; no new opcode | | **D-NALP-3** | Revision as `GroupCrossPowerSumsI32` over stored `w`/`f` | Queued | equal to exact integer revision; OpenNARS revision points; duplicate evidence counts once | | **D-NALP-4** | Per-row truth law into a lane: `MapPairU8` vs fixed-point multiply-high | Queued | insufficiency proof; per-row parity with `isa::truth`, error in codes; one shape also serves SQL nullable arithmetic | | **D-NALP-5** | Deduction / induction / abduction / analogy over populations | Queued (after D-NALP-4) | equals pairwise scalar rule; abduction direction test red for the reversed statement | diff --git a/.claude/board/entries/2026-10-12-nal-coordinate-queries.md b/.claude/board/entries/2026-10-12-nal-coordinate-queries.md index 35226bb03..44812221f 100644 --- a/.claude/board/entries/2026-10-12-nal-coordinate-queries.md +++ b/.claude/board/entries/2026-10-12-nal-coordinate-queries.md @@ -1,23 +1,27 @@ # 2026-10-12 — NAL query operations as coordinate ranges (D-NALP-1/2) -**Status:** In PR. Plan: `.claude/plans/nal-polyfill-v1.md` (status note before § 5). +**Status:** In PR (#1525). Plan: `.claude/plans/nal-polyfill-v1.md` (status note before § 5). -**Finding (MEASURED):** with beliefs sealed as `FacetCascade` keys -`[classid, S, copula, P, 255 − c, 255 − f]`, a belief question, a confidence -threshold within a statement, subject support and NAL choice are witnessed -coordinate ranges. The typed `nal::` operations bind once (R2IL procedure → -kept mask-risc program) and each call runs only that program over the range. -At 1,048,576 beliefs: range counts 24 ns, a confidence threshold across one -subject 139 ns, choice 265 ns, against 554–614 µs for the row loop -(`crates/lance-graph-nal/examples/nal_cost.rs`, AVX-512, release). Passing the -range as the execution extent, not as an ANDed mask, took the cross-statement -threshold from 195 µs to 139 ns. +**Decision (operator, 2026-10-12):** the NodeGuid is always the address and the +distinguished name; a selection is the 16-byte key minus an `n`-byte aperture. +A belief's key carries only its name (classid, S, copula, P, id); its truth +value lives in value lanes in the sealed order. -**Tests:** `crates/lance-graph-nal/tests/w2.rs`, 8 row-oracle tests over the -unsorted input; 7 disable runs red (threshold ignored / off by one, choice -reads the last row, cross-statement threshold off by one and without its -extent, witness not validated, subject bound truncated). +**Finding (MEASURED):** a statement, a subject or a whole class is an aperture +of 10, 6 or 4 cared bytes and one witnessed range +(`SealedFacetLane::bound_aperture`). The typed `nal::` operations bind once +(R2IL procedure → kept mask-risc program) and each call runs only that program +over the range as its execution extent. At 1,048,576 beliefs: range counts +19–20 ns, a confidence threshold over a 245-row subject 127 ns, the maximum +confidence 248 ns, against 495–559 µs for the row loop +(`crates/lance-graph-nal/examples/nal_cost.rs`, AVX-512, release). + +**Tests:** `crates/lance-graph-nal/tests/w2.rs`, 7 row-oracle tests over the +unsorted input; 7 disable runs red (statement aperture too narrow, aperture +one byte short, threshold off by one, threshold without its extent, choice +reading `f`, truth lanes misaligned with keys, witness not validated). **OPEN:** R2IL `RANGE` takes `u8` immediates and cannot name a range past row -255 (request to `ogar-r2il`). The rest of D-NALP-1 (ClassView `WideFieldMask` -projection, binder mask) and W3+ are queued. +255 (request to `ogar-r2il`); no argmax fold (which belief holds the maximum); +the rest of D-NALP-1 (ClassView `WideFieldMask` projection, binder mask) and +W3+ are queued. diff --git a/.claude/plans/nal-polyfill-v1.md b/.claude/plans/nal-polyfill-v1.md index e3b6201a3..71e91a66d 100644 --- a/.claude/plans/nal-polyfill-v1.md +++ b/.claude/plans/nal-polyfill-v1.md @@ -298,28 +298,38 @@ Rules that follow, each enforced in W6: ### Status 2026-10-12 (D-NALP-1/2, `crates/lance-graph-nal`) -- **Statements are coordinates, not rows with keys.** Reading - `nal-statement-v1` puts a belief on one `FacetCascade` key, semantic tiles - `[classid hi, classid lo, S, copula, P, 255 − c, 255 − f, 0]`, sealed into a - `SealedFacetLane` with its witness. This replaces the §4 A lowering column - for the first three rows: a belief query, a threshold within a statement and - choice are witnessed ranges (two `partition_point`s), not lane predicates. +- **The NodeGuid is the address and the distinguished name** (operator, + 2026-10-12). Reading `nal-statement-v1`: semantic tiles `[classid hi, + classid lo, S, copula, P, id, 0, 0]`. The key names the belief and nothing + else; `f` and `c` are value lanes written in the key's sealed order, so key + row `i` and truth row `i` are one belief by construction. Truth is never + part of a name. +- **A selection is the 16-byte key minus an aperture** (operator, + 2026-10-12): `Aperture { key, bytes }` cares the leading `bytes` bytes and + frees the rest. On the witnessed HHTL order (classid, then each tile coarse + to fine) the matches are one range, found by the contract's + `SealedFacetLane::bound_aperture`: 4 bytes = the class, 6 = a subject, 10 = + a statement. This replaces the §4 A lowering column for the belief query: + it is a range, not a lane predicate. - **Term width (OPEN above): decided `u16` per tile.** A vocabulary past 65,536 terms scales through the classid prefix, not by widening a tile. -- **Bind once, run the kept program.** `nal::{Question, TruthAtLeast, - Support, TruthAtLeastAcross}` run their R2IL procedure once at bind and keep - the mask-risc `Program`; a call runs only `execute_compiled`. A query whose - rows are one range passes that range as the execution extent, so a lane - predicate reads only the range (`TruthAtLeastAcross`: 195 µs → 139 ns at - 1,048,576 rows once the range became the extent instead of an ANDed mask). -- **Measured** (`examples/nal_cost.rs`, release, AVX-512, median of 101, answers - asserted first), n = 1,048,576: range counts 24 ns, `TruthAtLeastAcross` 139 - ns over a 245-row subject, `Choice::of` 265 ns, bind 524 ns; the row loop - 554–614 µs. +- **Bind once, run the kept program.** `nal::{Question, TruthAtLeast, Choice}` + run their R2IL procedure once at bind and keep the mask-risc `Program`; a + call runs only `execute_compiled`. The aperture's range is the execution + extent, so the confidence predicate and the `MAX` fold read only its rows + (a range passed as an ANDed mask instead cost 195 µs at 1,048,576 rows; + as the extent, 127 ns). +- **Measured** (`examples/nal_cost.rs`, release, AVX-512, median of 101, + answers asserted first), n = 1,048,576: range counts 19–20 ns; confidence + threshold over one statement 81 ns, over a 245-row subject 127 ns; choice + (max confidence) over the subject 248 ns; bind 274 ns; the row loop + 495–559 µs. - **OPEN, found here:** R2IL `RANGE` (arity 0) takes its bounds from two `u8` immediates, so through the byte it cannot name a range past row 255. The procedures emit the bound with `FoldDialect::emit_pred(Pred::Range)`. An `ogar-r2il` request, not minted here. +- **OPEN:** which belief holds the maximum (argmax) is not a mask-risc fold; + `Choice` answers the maximum confidence only. - Not yet: the ClassView `WideFieldMask` projection and the binder's mask (rest of D-NALP-1); W3 onwards. From 04684f7eaa5ae2bded255075de41cb643fcfd6c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 22:21:20 +0000 Subject: [PATCH 05/13] board: regenerate indexes after merging main Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .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 a228782e6..1bfc4fa0c 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. -320 entries, 2026-08-06 .. 2026-10-12. +321 entries, 2026-08-06 .. 2026-10-12. | date | entry id | finding | file | |---|---|---|---| From 253eee950db4fbffbfe20aa52df3647b65defe96 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 22:26:13 +0000 Subject: [PATCH 06/13] nal: refuse another population; keep the recognised lowering (CompiledProgram) A Prepared keeps the witness it was bound with and refuses a Bound for any other population (its kept range is in the first population's coordinates). mask-risc gains CompiledProgram, a program owned with its recognised lowering, so a prepared query no longer re-runs recognition per call. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- crates/lance-graph-mask-risc/src/ir.rs | 39 ++++++++++++++ crates/lance-graph-mask-risc/src/lib.rs | 8 +-- .../tests/compiled_program.rs | 52 +++++++++++++++++++ crates/lance-graph-nal/src/lib.rs | 14 +++-- crates/lance-graph-nal/src/nal.rs | 26 +++++++--- crates/lance-graph-nal/tests/w2.rs | 36 +++++++++++++ 6 files changed, 160 insertions(+), 15 deletions(-) create mode 100644 crates/lance-graph-mask-risc/tests/compiled_program.rs diff --git a/crates/lance-graph-mask-risc/src/ir.rs b/crates/lance-graph-mask-risc/src/ir.rs index 069c45750..aa977c39b 100644 --- a/crates/lance-graph-mask-risc/src/ir.rs +++ b/crates/lance-graph-mask-risc/src/ir.rs @@ -1416,6 +1416,45 @@ impl<'p> Compiled<'p> { } } +/// A [`Program`] owned together with its recognised lowering. +/// +/// [`Compiled`] borrows its program, so a caller that keeps a prepared query +/// (bound once, run many times) cannot keep a `Compiled` beside the program +/// it borrows from. This type holds both; [`CompiledProgram::compiled`] hands +/// out a `Compiled` without recognising the lowering again. The program is +/// private, so it cannot change after the lowering was recognised. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompiledProgram { + program: Program, + lowering: Lowering, +} + +impl CompiledProgram { + /// Recognise `program`'s lowering once and keep both. + pub fn new(program: Program) -> Self { + let lowering = program.lowering(); + CompiledProgram { program, lowering } + } + + /// The program. + pub fn program(&self) -> &Program { + &self.program + } + + /// The lowering recognised at construction. + pub fn lowering(&self) -> Lowering { + self.lowering + } + + /// The compiled view for [`crate::exec::execute_compiled`]; no recognition. + pub fn compiled(&self) -> Compiled<'_> { + Compiled { + program: &self.program, + lowering: self.lowering, + } + } +} + /// A program the executor folds without writing membership bits: see /// [`Program::fused_terminal`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/lance-graph-mask-risc/src/lib.rs b/crates/lance-graph-mask-risc/src/lib.rs index bf90bb0bf..7fd7d3e96 100644 --- a/crates/lance-graph-mask-risc/src/lib.rs +++ b/crates/lance-graph-mask-risc/src/lib.rs @@ -127,10 +127,10 @@ pub use exec::{ }; pub use fuse::{fuse, fuse_program, ternlog_imm, BoolExpr, FuseError, Fused}; pub use ir::{ - touched_words, Compiled, Foreign, ForeignPlane, FusedFold, FusedKeep, FusedTerminal, - FusedTern2, FusedTern3, FusedTernlog, GroupFold, GroupKey, LaneRef, Lowering, MaskOp, Operand, - Planes, Pred, Program, StridedRef, Terminal, Tern2Fold, Tern3In, Tern3Step, FUSED_SLOT_CAP, - GROUP_SUM_SYM_MAX_ROWS, MASKED_SUM_I32_MAX_ROWS, MAX_SCRATCH_SLOTS, + touched_words, Compiled, CompiledProgram, Foreign, ForeignPlane, FusedFold, FusedKeep, + FusedTerminal, FusedTern2, FusedTern3, FusedTernlog, GroupFold, GroupKey, LaneRef, Lowering, + MaskOp, Operand, Planes, Pred, Program, StridedRef, Terminal, Tern2Fold, Tern3In, Tern3Step, + FUSED_SLOT_CAP, GROUP_SUM_SYM_MAX_ROWS, MASKED_SUM_I32_MAX_ROWS, MAX_SCRATCH_SLOTS, }; pub use reference::{ reference_execute, reference_execute_into, reference_scratch, reference_scratch_with_foreign, diff --git a/crates/lance-graph-mask-risc/tests/compiled_program.rs b/crates/lance-graph-mask-risc/tests/compiled_program.rs new file mode 100644 index 000000000..52c28327a --- /dev/null +++ b/crates/lance-graph-mask-risc/tests/compiled_program.rs @@ -0,0 +1,52 @@ +//! `CompiledProgram`: a program owned with its recognised lowering. + +use lance_graph_mask_risc::{ + execute_compiled, CompiledProgram, Foreign, Lowering, MaskOp, Operand, Out, Planes, Pred, + Program, Scratch, Terminal, Value, +}; + +fn range_count(lo: u32, hi: u32) -> Program { + Program::new( + vec![MaskOp::Pred { + pred: Pred::Range { lo, hi }, + under: None, + dst: 0, + }], + Terminal::Count { + mask: Operand::Scratch(0), + }, + ) +} + +/// FAILS IF: the owned lowering differs from the one the program recognises, +/// or the compiled view does not answer like `Program::compile`. +#[test] +fn the_owned_lowering_is_the_recognised_one_and_answers_alike() { + let p = range_count(3, 70); + let owned = CompiledProgram::new(p.clone()); + assert_eq!(owned.lowering(), p.lowering()); + assert!(matches!(owned.lowering(), Lowering::Range(_))); + assert_eq!(owned.program(), &p); + let planes = Planes { + n_rows: 100, + masks: &[], + lanes: &[], + }; + let foreign = Foreign { + planes: &[], + lanes: &[], + }; + let mut s = Scratch::for_program(&p, 100).unwrap(); + let a = execute_compiled( + &owned.compiled(), + &planes, + &foreign, + &mut s, + Out::None, + 0..100, + ) + .unwrap(); + let b = execute_compiled(&p.compile(), &planes, &foreign, &mut s, Out::None, 0..100).unwrap(); + assert_eq!(a, b); + assert_eq!(a, Value::Count(67)); +} diff --git a/crates/lance-graph-nal/src/lib.rs b/crates/lance-graph-nal/src/lib.rs index 9e42a18bb..3a52a45a6 100644 --- a/crates/lance-graph-nal/src/lib.rs +++ b/crates/lance-graph-nal/src/lib.rs @@ -56,7 +56,7 @@ use lance_graph_contract::canonical_node::NodeGuid; use lance_graph_contract::facet::{FacetCascade, SemanticAperture}; use lance_graph_contract::ordered_lane::{OrderedLaneWitness, SealedFacetLane, WitnessError}; use lance_graph_contract::temporal_pov::LanceVersion; -use lance_graph_mask_risc::{execute_compiled, ExecError, LaneRef, Program}; +use lance_graph_mask_risc::{execute_compiled, CompiledProgram, ExecError, LaneRef}; /// The NAL copulas this reading stores. NAL-1 and NAL-2 (inheritance, /// similarity) and NAL-5 (implication, equivalence) statements over atomic @@ -283,10 +283,15 @@ impl Bound<'_> { .ok_or(NalError::NotAnInterval) } + /// The witness this view was bound with. + pub(crate) fn witness(&self) -> OrderedLaneWitness { + self.witness + } + /// Run a kept program over `extent`. pub(crate) fn run( &self, - program: &Program, + program: &CompiledProgram, scratch: &mut lance_graph_mask_risc::Scratch<'_>, extent: std::ops::Range, ) -> Result { @@ -301,7 +306,7 @@ impl Bound<'_> { lanes: &[], }; execute_compiled( - &program.compile(), + &program.compiled(), &planes, &foreign, scratch, @@ -324,6 +329,9 @@ pub enum NalError { Exec(ExecError), /// The procedure ended without the one scalar it promised. NoAnswer, + /// A prepared query was run on a population other than the one it was + /// bound on: its kept range is in the other population's coordinates. + WrongPopulation, } impl From for NalError { diff --git a/crates/lance-graph-nal/src/nal.rs b/crates/lance-graph-nal/src/nal.rs index dd5764152..6fdadcd14 100644 --- a/crates/lance-graph-nal/src/nal.rs +++ b/crates/lance-graph-nal/src/nal.rs @@ -12,7 +12,10 @@ use std::ops::Range; -use lance_graph_mask_risc::{Foreign, LaneRef, Lowering, Planes, Pred, Program, Scratch, Value}; +use lance_graph_contract::ordered_lane::OrderedLaneWitness; +use lance_graph_mask_risc::{ + CompiledProgram, Foreign, LaneRef, Lowering, Planes, Pred, Program, Scratch, Value, +}; use lance_graph_r2il::{procs, FoldDialect, FoldError, Val, INT_S_LESS, LOAD, POP_COUNT}; use ogar_loco::{Dialect, FnIndex}; @@ -52,8 +55,10 @@ fn range(d: &mut FoldDialect<'_>, stack: &mut Vec) -> Result<(), FoldError> /// A query bound to its range, with the program its procedure produced. #[derive(Debug, Clone)] pub struct Prepared { - program: Program, + program: CompiledProgram, extent: Range, + /// The population the range belongs to; a run on any other is refused. + witness: OrderedLaneWitness, } impl Prepared { @@ -78,8 +83,9 @@ impl Prepared { } match (stack.as_slice(), d.programs_run(), d.last_program()) { ([Val::Scalar(_)], 1, Some(p)) => Ok(Prepared { - program: p.clone(), + program: CompiledProgram::new(p.clone()), extent, + witness: b.witness(), }), _ => Err(NalError::NoAnswer), } @@ -92,17 +98,17 @@ impl Prepared { /// /// [`NalError::Exec`] if the program declares more slots than can exist. pub fn scratch(&self, b: &Bound<'_>) -> Result, NalError> { - Ok(Scratch::for_program(&self.program, b.n_rows())?) + Ok(Scratch::for_program(self.program.program(), b.n_rows())?) } /// How mask-risc folds this query. pub fn lowering(&self) -> Lowering { - self.program.compile().lowering() + self.program.lowering() } /// The kept program. pub fn program(&self) -> &Program { - &self.program + self.program.program() } /// The rows the program runs over. @@ -111,6 +117,9 @@ impl Prepared { } fn value(&self, b: &Bound<'_>, scratch: &mut Scratch<'_>) -> Result { + if b.witness() != self.witness { + return Err(NalError::WrongPopulation); + } Ok(b.run(&self.program, scratch, self.extent.clone())?) } @@ -118,8 +127,9 @@ impl Prepared { /// /// # Errors /// - /// [`NalError::Exec`] if `b` is not the population the query was bound on, - /// or [`NalError::NoAnswer`] if the query does not count. + /// [`NalError::WrongPopulation`] if `b` is not the population the query was + /// bound on, [`NalError::Exec`] if mask-risc refuses the program, or + /// [`NalError::NoAnswer`] if the query does not count. pub fn count(&self, b: &Bound<'_>, scratch: &mut Scratch<'_>) -> Result { match self.value(b, scratch)? { Value::Count(n) => Ok(n), diff --git a/crates/lance-graph-nal/tests/w2.rs b/crates/lance-graph-nal/tests/w2.rs index 614c802ed..6f41b10cb 100644 --- a/crates/lance-graph-nal/tests/w2.rs +++ b/crates/lance-graph-nal/tests/w2.rs @@ -235,3 +235,39 @@ fn a_stale_witness_refuses_to_bind() { let forged = OrderedLaneWitness::forged(7, beliefs.witness().n_rows(), 0xDEAD); assert!(matches!(beliefs.bind(&forged), Err(NalError::Witness(_)))); } + +/// FAILS IF: a query bound on one population runs on another of the same +/// size. Its kept range is in the first population's coordinates, so the +/// second would answer silently for the wrong beliefs. +#[test] +fn a_prepared_query_refuses_another_population() { + let a = sealed(&fixture(2_000)); + let mut other = fixture(2_000); + other.iter_mut().for_each(|b| b.s = (b.s + 7) % 40); + let b = sealed(&other); + assert_eq!(a.witness().n_rows(), b.witness().n_rows()); + let (ba, bb) = (a.bind(&a.witness()).unwrap(), b.bind(&b.witness()).unwrap()); + let st = Statement { + s: 3, + copula: Copula::Inheritance, + p: 5, + }; + let ap = Aperture::statement(CLASS, st); + let q = nal::Question::bind(&ba, ap).unwrap(); + assert!(matches!( + q.count(&bb, &mut q.scratch(&bb).unwrap()), + Err(NalError::WrongPopulation) + )); + let t = nal::TruthAtLeast::bind(&ba, ap, 100).unwrap(); + assert!(matches!( + t.count(&bb, &mut t.scratch(&bb).unwrap()), + Err(NalError::WrongPopulation) + )); + let c = nal::Choice::bind(&ba, ap).unwrap(); + assert!(matches!( + c.max(&bb, &mut c.scratch(&bb).unwrap()), + Err(NalError::WrongPopulation) + )); + // The population it was bound on still answers. + assert!(q.count(&ba, &mut q.scratch(&ba).unwrap()).is_ok()); +} From 70f13f3cf82af1ba9b35168dde76509c06dfb11e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 22:36:14 +0000 Subject: [PATCH 07/13] nal: record NAL operations as global core methods NARS is global: the NAL operations run over any belief population, check no classid, and need no hot-plug or minted concept. Classid-dependent methods are declared at compile time through a consumer's HotPlug; that path is separate. Drops W8's concept-id mint request. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .claude/plans/nal-polyfill-v1.md | 7 +++++++ crates/lance-graph-nal/src/lib.rs | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/.claude/plans/nal-polyfill-v1.md b/.claude/plans/nal-polyfill-v1.md index 71e91a66d..a27030e08 100644 --- a/.claude/plans/nal-polyfill-v1.md +++ b/.claude/plans/nal-polyfill-v1.md @@ -298,6 +298,13 @@ Rules that follow, each enforced in W6: ### Status 2026-10-12 (D-NALP-1/2, `crates/lance-graph-nal`) +- **NAL operations are global core methods** (operator, 2026-10-12: "NARS + ist global"). They run over any belief population, check no classid, and + need no hot-plug and no minted concept; they read the one canonical reading + `nal-statement-v1`. Classid-dependent methods are the other kind: declared at + compile time through a consumer's `HotPlug` and checked against the row's + classid (the D-CMH path, not this crate). This removes W8's concept-id mint + request; W8 keeps only the bytes W4/W5 earn. - **The NodeGuid is the address and the distinguished name** (operator, 2026-10-12). Reading `nal-statement-v1`: semantic tiles `[classid hi, classid lo, S, copula, P, id, 0, 0]`. The key names the belief and nothing diff --git a/crates/lance-graph-nal/src/lib.rs b/crates/lance-graph-nal/src/lib.rs index 3a52a45a6..d42ddf697 100644 --- a/crates/lance-graph-nal/src/lib.rs +++ b/crates/lance-graph-nal/src/lib.rs @@ -37,6 +37,18 @@ //! kept program through [`execute_compiled`]. A call does not parse, lower or //! allocate. //! +//! # Global core methods, not class methods +//! +//! NARS is global (operator, 2026-10-12). The NAL operations are **global core +//! methods**: they run over any belief population and check no classid, need +//! no hot-plug and no minted concept. The classid in a key is only the prefix +//! that addresses the population. They read one canonical reading, +//! `nal-statement-v1`, the same for every class. +//! +//! Methods that depend on a classid (a method of one business class) are the +//! other kind: declared at compile time through the consumer's `HotPlug` and +//! checked against the row's classid. That path is not this crate. +//! //! # Not here yet //! //! Revision (W3), the per-row truth laws (W4), syllogisms (W5), the Narsese From 443e2d5fcd2685ddd29add4307534637cef14bf4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 22:36:27 +0000 Subject: [PATCH 08/13] nal: fix the Program doc link Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- crates/lance-graph-nal/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lance-graph-nal/src/lib.rs b/crates/lance-graph-nal/src/lib.rs index d42ddf697..fa9552b54 100644 --- a/crates/lance-graph-nal/src/lib.rs +++ b/crates/lance-graph-nal/src/lib.rs @@ -33,7 +33,7 @@ //! //! Each [`nal`] operation is an R2IL procedure ([`nal::NalOp::lower`]) over //! [`lance_graph_r2il::FoldDialect`]. Binding a query runs that procedure once -//! and keeps the mask-risc [`Program`] it produced; every later call runs the +//! and keeps the mask-risc [`Program`](lance_graph_mask_risc::Program) it produced; every later call runs the //! kept program through [`execute_compiled`]. A call does not parse, lower or //! allocate. //! From 96c4ea1e5fdf33ade1ef87c5b50d904669651fb0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 04:59:17 +0000 Subject: [PATCH 09/13] ci: key the native-target caches to the runner's CPU .cargo/config.toml builds with target-cpu=native, and cargo fingerprints the flag text, not the CPU. dir-sim-persist.yml and identity.yml set no RUSTFLAGS and restore target/ from Swatinem/rust-cache, so an artifact built on a runner with AVX-512 was reused on one without: the lance-graph-contract build script died with SIGILL (dir-sim log in Lance, red on main since 10325b35). Both workflows now hash /proc/cpuinfo's flags into the cache key. Also corrects the config comment that claimed every workflow overrides native with RUSTFLAGS; only build, rust-test and style do. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .cargo/config.toml | 10 +++++++--- .github/workflows/dir-sim-persist.yml | 8 ++++++++ .github/workflows/identity.yml | 16 ++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 8fdd89a22..7f8a0b32f 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -35,9 +35,13 @@ # portable baseline (AVX2): cargo build --config .cargo/config-v3.toml ... # AVX-512 (v4): cargo build --config .cargo/config-avx512.toml ... # -# CI is unaffected: every workflow sets `RUSTFLAGS` with an explicit -# `-C target-cpu=x86-64-v3`, and a RUSTFLAGS env REPLACES every cargo-config -# rustflags entry. Everything else that ships pins x86_64 to v3 without +# CI: build.yml, rust-test.yml and style.yml set `RUSTFLAGS` with an explicit +# `-C target-cpu=x86-64-v3` (a RUSTFLAGS env REPLACES every cargo-config +# rustflags entry). Every other workflow builds with `native`. Cargo +# fingerprints the flag TEXT, not the CPU, so a workflow that caches `target/` +# must key the cache to the runner's CPU flags (dir-sim-persist.yml and +# identity.yml do); without that, an artifact built on a runner with AVX-512 +# is reused on one without and dies with SIGILL. Everything else that ships pins x86_64 to v3 without # touching other architectures: the Dockerfiles that copy this tree replace # this file with config-v3.toml (x86_64-only), the thinking-engine images set # CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS, and `make publish` in diff --git a/.github/workflows/dir-sim-persist.yml b/.github/workflows/dir-sim-persist.yml index 77300aed4..fae13e688 100644 --- a/.github/workflows/dir-sim-persist.yml +++ b/.github/workflows/dir-sim-persist.yml @@ -62,8 +62,16 @@ jobs: run: sudo apt-get update && sudo apt-get install -y protobuf-compiler libprotobuf-dev - name: Setup rust toolchain (pinned by rust-toolchain.toml) run: rustup show + # `.cargo/config.toml` builds with target-cpu=native, and cargo's + # fingerprint sees the flag text, not the CPU: a cached artifact built + # on a runner with AVX-512 would be reused on one without and die with + # SIGILL. Key the cache to this runner's CPU feature flags. + - name: Key the cache to this runner's CPU + id: cpu + run: echo "flags=$(grep -m1 '^flags' /proc/cpuinfo | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" - uses: Swatinem/rust-cache@v2 with: + key: cpu-${{ steps.cpu.outputs.flags }} workspaces: lance-graph/crates/lance-graph-dir-sim-lance - name: clippy run: cargo clippy --manifest-path crates/lance-graph-dir-sim-lance/Cargo.toml --all-targets -- -D warnings diff --git a/.github/workflows/identity.yml b/.github/workflows/identity.yml index 93fd412f1..90dada256 100644 --- a/.github/workflows/identity.yml +++ b/.github/workflows/identity.yml @@ -73,8 +73,16 @@ jobs: persist-credentials: false - name: Setup rust toolchain (pinned by rust-toolchain.toml) run: rustup show + # `.cargo/config.toml` builds with target-cpu=native, and cargo's + # fingerprint sees the flag text, not the CPU: a cached artifact built + # on a runner with AVX-512 would be reused on one without and die with + # SIGILL. Key the cache to this runner's CPU feature flags. + - name: Key the cache to this runner's CPU + id: cpu + run: echo "flags=$(grep -m1 '^flags' /proc/cpuinfo | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" - uses: Swatinem/rust-cache@v2 with: + key: cpu-${{ steps.cpu.outputs.flags }} workspaces: lance-graph/crates/lance-graph-dir-sim - name: Run the simulator's tests run: cargo test --manifest-path crates/lance-graph-dir-sim/Cargo.toml @@ -213,8 +221,16 @@ jobs: persist-credentials: false - name: Setup rust toolchain (pinned by rust-toolchain.toml) run: rustup show + # `.cargo/config.toml` builds with target-cpu=native, and cargo's + # fingerprint sees the flag text, not the CPU: a cached artifact built + # on a runner with AVX-512 would be reused on one without and die with + # SIGILL. Key the cache to this runner's CPU feature flags. + - name: Key the cache to this runner's CPU + id: cpu + run: echo "flags=$(grep -m1 '^flags' /proc/cpuinfo | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" - uses: Swatinem/rust-cache@v2 with: + key: cpu-${{ steps.cpu.outputs.flags }} workspaces: lance-graph/crates/lance-graph-rbac - name: Run the IAM tests run: cargo test --manifest-path crates/lance-graph-rbac/Cargo.toml From ab6851908bab1f82af8b3115cdd054ec4d84bfe9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 05:05:44 +0000 Subject: [PATCH 10/13] ci: resolve target-cpu=native to this runner's CPU before building A step resolves native to the CPU name and exact target-feature list (.github/scripts/resolve-native-rustflags.sh) and sets RUSTFLAGS. Cargo then sees different flag text on a different CPU and recompiles by itself instead of reusing an artifact that dies with SIGILL. The cache key is derived from the same flags, replacing the /proc/cpuinfo hash. Checked locally: the resolved flags give the same cfg as native; the same CPU rebuilds nothing; another CPU's flags rebuild 16 crates of lance-graph-contract; clippy -D warnings passes with them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .cargo/config.toml | 8 +++--- .github/scripts/resolve-native-rustflags.sh | 22 +++++++++++++++ .github/workflows/dir-sim-persist.yml | 15 +++++++---- .github/workflows/identity.yml | 30 ++++++++++++++------- 4 files changed, 57 insertions(+), 18 deletions(-) create mode 100755 .github/scripts/resolve-native-rustflags.sh diff --git a/.cargo/config.toml b/.cargo/config.toml index 7f8a0b32f..00a0c4af5 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -39,9 +39,11 @@ # `-C target-cpu=x86-64-v3` (a RUSTFLAGS env REPLACES every cargo-config # rustflags entry). Every other workflow builds with `native`. Cargo # fingerprints the flag TEXT, not the CPU, so a workflow that caches `target/` -# must key the cache to the runner's CPU flags (dir-sim-persist.yml and -# identity.yml do); without that, an artifact built on a runner with AVX-512 -# is reused on one without and dies with SIGILL. Everything else that ships pins x86_64 to v3 without +# resolves `native` to the runner's CPU name and feature list first +# (.github/scripts/resolve-native-rustflags.sh, used by dir-sim-persist.yml and +# identity.yml) and keys the cache to those flags; without that, an artifact +# built on a runner with AVX-512 is reused on one without and dies with SIGILL. +# The same script works locally: RUSTFLAGS="$(.github/scripts/resolve-native-rustflags.sh)". Everything else that ships pins x86_64 to v3 without # touching other architectures: the Dockerfiles that copy this tree replace # this file with config-v3.toml (x86_64-only), the thinking-engine images set # CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS, and `make publish` in diff --git a/.github/scripts/resolve-native-rustflags.sh b/.github/scripts/resolve-native-rustflags.sh new file mode 100755 index 000000000..97bcb5104 --- /dev/null +++ b/.github/scripts/resolve-native-rustflags.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Print RUSTFLAGS equivalent to `.cargo/config.toml`'s `-Ctarget-cpu=native`, +# with `native` resolved to this machine's CPU name and exact feature list. +# +# Why: cargo fingerprints the flag TEXT. `-Ctarget-cpu=native` reads the same +# on every machine, so an artifact built on a CPU with AVX-512 is reused on one +# without and dies with SIGILL. Resolved, the text names the actual features: +# a different CPU gives different flags, and cargo recompiles by itself. +# +# The feature list, not just the CPU name, is what matters: a hypervisor can +# hide features from a CPU that still reports the same model name. +# +# RUSTFLAGS replaces every rustflags entry in .cargo/config.toml. That file +# sets only target-cpu, so nothing else is lost; extra flags given as +# arguments are appended. +set -euo pipefail +cpu=$(rustc --print target-cpus | sed -n 's/.*native.*(currently \([^)]*\)).*/\1/p') +[ -n "$cpu" ] || { echo "resolve-native-rustflags: rustc did not report the native CPU" >&2; exit 1; } +features=$(rustc --print cfg -C target-cpu=native \ + | sed -n 's/^target_feature="\(.*\)"$/+\1/p' | sort | paste -sd, -) +[ -n "$features" ] || { echo "resolve-native-rustflags: no target features reported" >&2; exit 1; } +echo "-Ctarget-cpu=$cpu -Ctarget-feature=$features${*:+ $*}" diff --git a/.github/workflows/dir-sim-persist.yml b/.github/workflows/dir-sim-persist.yml index fae13e688..b9d17da5f 100644 --- a/.github/workflows/dir-sim-persist.yml +++ b/.github/workflows/dir-sim-persist.yml @@ -63,12 +63,17 @@ jobs: - name: Setup rust toolchain (pinned by rust-toolchain.toml) run: rustup show # `.cargo/config.toml` builds with target-cpu=native, and cargo's - # fingerprint sees the flag text, not the CPU: a cached artifact built - # on a runner with AVX-512 would be reused on one without and die with - # SIGILL. Key the cache to this runner's CPU feature flags. - - name: Key the cache to this runner's CPU + # fingerprint sees the flag text, not the CPU: an artifact built on a + # runner with AVX-512 would be reused on one without and die with + # SIGILL. Resolve `native` to this runner's CPU name and feature list: + # a different CPU then gives different flags, cargo recompiles by itself, + # and the cache is keyed to the same flags. + - name: Resolve target-cpu=native for this runner id: cpu - run: echo "flags=$(grep -m1 '^flags' /proc/cpuinfo | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" + run: | + flags=$(.github/scripts/resolve-native-rustflags.sh) + echo "RUSTFLAGS=$flags" >> "$GITHUB_ENV" + echo "flags=$(printf %s "$flags" | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" - uses: Swatinem/rust-cache@v2 with: key: cpu-${{ steps.cpu.outputs.flags }} diff --git a/.github/workflows/identity.yml b/.github/workflows/identity.yml index 90dada256..6d0cee0a2 100644 --- a/.github/workflows/identity.yml +++ b/.github/workflows/identity.yml @@ -74,12 +74,17 @@ jobs: - name: Setup rust toolchain (pinned by rust-toolchain.toml) run: rustup show # `.cargo/config.toml` builds with target-cpu=native, and cargo's - # fingerprint sees the flag text, not the CPU: a cached artifact built - # on a runner with AVX-512 would be reused on one without and die with - # SIGILL. Key the cache to this runner's CPU feature flags. - - name: Key the cache to this runner's CPU + # fingerprint sees the flag text, not the CPU: an artifact built on a + # runner with AVX-512 would be reused on one without and die with + # SIGILL. Resolve `native` to this runner's CPU name and feature list: + # a different CPU then gives different flags, cargo recompiles by itself, + # and the cache is keyed to the same flags. + - name: Resolve target-cpu=native for this runner id: cpu - run: echo "flags=$(grep -m1 '^flags' /proc/cpuinfo | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" + run: | + flags=$(.github/scripts/resolve-native-rustflags.sh) + echo "RUSTFLAGS=$flags" >> "$GITHUB_ENV" + echo "flags=$(printf %s "$flags" | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" - uses: Swatinem/rust-cache@v2 with: key: cpu-${{ steps.cpu.outputs.flags }} @@ -222,12 +227,17 @@ jobs: - name: Setup rust toolchain (pinned by rust-toolchain.toml) run: rustup show # `.cargo/config.toml` builds with target-cpu=native, and cargo's - # fingerprint sees the flag text, not the CPU: a cached artifact built - # on a runner with AVX-512 would be reused on one without and die with - # SIGILL. Key the cache to this runner's CPU feature flags. - - name: Key the cache to this runner's CPU + # fingerprint sees the flag text, not the CPU: an artifact built on a + # runner with AVX-512 would be reused on one without and die with + # SIGILL. Resolve `native` to this runner's CPU name and feature list: + # a different CPU then gives different flags, cargo recompiles by itself, + # and the cache is keyed to the same flags. + - name: Resolve target-cpu=native for this runner id: cpu - run: echo "flags=$(grep -m1 '^flags' /proc/cpuinfo | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" + run: | + flags=$(.github/scripts/resolve-native-rustflags.sh) + echo "RUSTFLAGS=$flags" >> "$GITHUB_ENV" + echo "flags=$(printf %s "$flags" | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" - uses: Swatinem/rust-cache@v2 with: key: cpu-${{ steps.cpu.outputs.flags }} From d70d8a768b8e3a9d621bf11ad38b3d03ee7d0b3d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 05:10:58 +0000 Subject: [PATCH 11/13] ci: report the resolved CPU in the job summary Each resolve step writes the CPU name and feature hash it resolved native to into $GITHUB_STEP_SUMMARY, so every run shows which runner CPU the job landed on without a tracked list. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .github/workflows/dir-sim-persist.yml | 4 +++- .github/workflows/identity.yml | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dir-sim-persist.yml b/.github/workflows/dir-sim-persist.yml index b9d17da5f..ea4e7e19f 100644 --- a/.github/workflows/dir-sim-persist.yml +++ b/.github/workflows/dir-sim-persist.yml @@ -73,7 +73,9 @@ jobs: run: | flags=$(.github/scripts/resolve-native-rustflags.sh) echo "RUSTFLAGS=$flags" >> "$GITHUB_ENV" - echo "flags=$(printf %s "$flags" | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" + hash=$(printf %s "$flags" | sha256sum | cut -c1-16) + echo "flags=$hash" >> "$GITHUB_OUTPUT" + echo "native resolved to \`${flags%% *}\` (features $hash) for job \`$GITHUB_JOB\`" >> "$GITHUB_STEP_SUMMARY" - uses: Swatinem/rust-cache@v2 with: key: cpu-${{ steps.cpu.outputs.flags }} diff --git a/.github/workflows/identity.yml b/.github/workflows/identity.yml index 6d0cee0a2..c6220f716 100644 --- a/.github/workflows/identity.yml +++ b/.github/workflows/identity.yml @@ -84,7 +84,9 @@ jobs: run: | flags=$(.github/scripts/resolve-native-rustflags.sh) echo "RUSTFLAGS=$flags" >> "$GITHUB_ENV" - echo "flags=$(printf %s "$flags" | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" + hash=$(printf %s "$flags" | sha256sum | cut -c1-16) + echo "flags=$hash" >> "$GITHUB_OUTPUT" + echo "native resolved to \`${flags%% *}\` (features $hash) for job \`$GITHUB_JOB\`" >> "$GITHUB_STEP_SUMMARY" - uses: Swatinem/rust-cache@v2 with: key: cpu-${{ steps.cpu.outputs.flags }} @@ -237,7 +239,9 @@ jobs: run: | flags=$(.github/scripts/resolve-native-rustflags.sh) echo "RUSTFLAGS=$flags" >> "$GITHUB_ENV" - echo "flags=$(printf %s "$flags" | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" + hash=$(printf %s "$flags" | sha256sum | cut -c1-16) + echo "flags=$hash" >> "$GITHUB_OUTPUT" + echo "native resolved to \`${flags%% *}\` (features $hash) for job \`$GITHUB_JOB\`" >> "$GITHUB_STEP_SUMMARY" - uses: Swatinem/rust-cache@v2 with: key: cpu-${{ steps.cpu.outputs.flags }} From 268bce5e0733fc3d1ae4cf070adf8711caf68cfd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 05:23:08 +0000 Subject: [PATCH 12/13] ci: spell native out as a feature list, without a CPU name A named target-cpu enables its model's default features, and a +feature list cannot take them back: rustc --print cfg -Ctarget-cpu=skylake-avx512 -Ctarget-feature=+sse2 still reports avx512f. On a VM that hides AVX-512 from a CPU that keeps its model name, the resolved flags compiled AVX-512 back in, the SIGILL this script exists to prevent. The flags are now the target's baseline CPU plus the exact native feature list, and the script checks that rustc --print cfg under those flags equals the native set, failing otherwise. Disable run: with the CPU name put back (sapphirerapids on this cascadelake host) the check fails and lists the 16 features the name adds. --cpu prints the native CPU name for the job summary. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .cargo/config.toml | 2 +- .github/scripts/resolve-native-rustflags.sh | 46 +++++++++++++++------ .github/workflows/dir-sim-persist.yml | 4 +- .github/workflows/identity.yml | 8 ++-- 4 files changed, 41 insertions(+), 19 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 00a0c4af5..ba4523219 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -39,7 +39,7 @@ # `-C target-cpu=x86-64-v3` (a RUSTFLAGS env REPLACES every cargo-config # rustflags entry). Every other workflow builds with `native`. Cargo # fingerprints the flag TEXT, not the CPU, so a workflow that caches `target/` -# resolves `native` to the runner's CPU name and feature list first +# spells `native` out as the runner's exact feature list first # (.github/scripts/resolve-native-rustflags.sh, used by dir-sim-persist.yml and # identity.yml) and keys the cache to those flags; without that, an artifact # built on a runner with AVX-512 is reused on one without and dies with SIGILL. diff --git a/.github/scripts/resolve-native-rustflags.sh b/.github/scripts/resolve-native-rustflags.sh index 97bcb5104..a665ae3c8 100755 --- a/.github/scripts/resolve-native-rustflags.sh +++ b/.github/scripts/resolve-native-rustflags.sh @@ -1,22 +1,44 @@ #!/usr/bin/env bash -# Print RUSTFLAGS equivalent to `.cargo/config.toml`'s `-Ctarget-cpu=native`, -# with `native` resolved to this machine's CPU name and exact feature list. +# Print RUSTFLAGS whose target features equal `.cargo/config.toml`'s +# `-Ctarget-cpu=native` on this machine, spelled out as an explicit list. +# With --cpu, print the name rustc resolves `native` to instead (for logs). # # Why: cargo fingerprints the flag TEXT. `-Ctarget-cpu=native` reads the same # on every machine, so an artifact built on a CPU with AVX-512 is reused on one -# without and dies with SIGILL. Resolved, the text names the actual features: -# a different CPU gives different flags, and cargo recompiles by itself. +# without and dies with SIGILL. Spelled out, the text names the actual +# features: a different CPU gives different flags, and cargo recompiles. # -# The feature list, not just the CPU name, is what matters: a hypervisor can -# hide features from a CPU that still reports the same model name. +# No `-Ctarget-cpu=`: a named CPU enables its model's default features, +# and a `+feature` list cannot take them back. On a VM that hides AVX-512 from +# a CPU still reporting a Skylake-server model, `-Ctarget-cpu=skylake-avx512` +# would compile AVX-512 back in. The target's baseline CPU plus the exact +# native list has no hidden defaults; the cost is tuning, not correctness. +# Features rustc hides on stable (amx, avx10) are not in the list, so they are +# never enabled; native could have enabled them, this cannot. # # RUSTFLAGS replaces every rustflags entry in .cargo/config.toml. That file # sets only target-cpu, so nothing else is lost; extra flags given as # arguments are appended. set -euo pipefail -cpu=$(rustc --print target-cpus | sed -n 's/.*native.*(currently \([^)]*\)).*/\1/p') -[ -n "$cpu" ] || { echo "resolve-native-rustflags: rustc did not report the native CPU" >&2; exit 1; } -features=$(rustc --print cfg -C target-cpu=native \ - | sed -n 's/^target_feature="\(.*\)"$/+\1/p' | sort | paste -sd, -) -[ -n "$features" ] || { echo "resolve-native-rustflags: no target features reported" >&2; exit 1; } -echo "-Ctarget-cpu=$cpu -Ctarget-feature=$features${*:+ $*}" +die() { echo "resolve-native-rustflags: $*" >&2; exit 1; } + +if [ "${1:-}" = "--cpu" ]; then + cpu=$(rustc --print target-cpus | sed -n 's/.*native.*(currently \([^)]*\)).*/\1/p') + [ -n "$cpu" ] || die "rustc did not report the native CPU" + echo "$cpu" + exit 0 +fi + +target_features() { rustc --print cfg "$@" | sed -n 's/^target_feature="\(.*\)"$/\1/p' | sort; } + +native=$(target_features -C target-cpu=native) +[ -n "$native" ] || die "no target features reported" +flags="-Ctarget-feature=$(printf '%s\n' "$native" | sed 's/^/+/' | paste -sd, -)" + +# The point of the script: these flags must enable exactly what native does. +# shellcheck disable=SC2086 +resolved=$(target_features $flags) +[ "$resolved" = "$native" ] || die "flags do not reproduce native: +$(diff <(printf '%s\n' "$native") <(printf '%s\n' "$resolved") || true)" + +echo "$flags${*:+ $*}" diff --git a/.github/workflows/dir-sim-persist.yml b/.github/workflows/dir-sim-persist.yml index ea4e7e19f..89c682181 100644 --- a/.github/workflows/dir-sim-persist.yml +++ b/.github/workflows/dir-sim-persist.yml @@ -65,7 +65,7 @@ jobs: # `.cargo/config.toml` builds with target-cpu=native, and cargo's # fingerprint sees the flag text, not the CPU: an artifact built on a # runner with AVX-512 would be reused on one without and die with - # SIGILL. Resolve `native` to this runner's CPU name and feature list: + # SIGILL. Spell `native` out as this runner's exact feature list: # a different CPU then gives different flags, cargo recompiles by itself, # and the cache is keyed to the same flags. - name: Resolve target-cpu=native for this runner @@ -75,7 +75,7 @@ jobs: echo "RUSTFLAGS=$flags" >> "$GITHUB_ENV" hash=$(printf %s "$flags" | sha256sum | cut -c1-16) echo "flags=$hash" >> "$GITHUB_OUTPUT" - echo "native resolved to \`${flags%% *}\` (features $hash) for job \`$GITHUB_JOB\`" >> "$GITHUB_STEP_SUMMARY" + echo "native is \`$(.github/scripts/resolve-native-rustflags.sh --cpu)\`, features $hash, job \`$GITHUB_JOB\`" >> "$GITHUB_STEP_SUMMARY" - uses: Swatinem/rust-cache@v2 with: key: cpu-${{ steps.cpu.outputs.flags }} diff --git a/.github/workflows/identity.yml b/.github/workflows/identity.yml index c6220f716..04a9465e7 100644 --- a/.github/workflows/identity.yml +++ b/.github/workflows/identity.yml @@ -76,7 +76,7 @@ jobs: # `.cargo/config.toml` builds with target-cpu=native, and cargo's # fingerprint sees the flag text, not the CPU: an artifact built on a # runner with AVX-512 would be reused on one without and die with - # SIGILL. Resolve `native` to this runner's CPU name and feature list: + # SIGILL. Spell `native` out as this runner's exact feature list: # a different CPU then gives different flags, cargo recompiles by itself, # and the cache is keyed to the same flags. - name: Resolve target-cpu=native for this runner @@ -86,7 +86,7 @@ jobs: echo "RUSTFLAGS=$flags" >> "$GITHUB_ENV" hash=$(printf %s "$flags" | sha256sum | cut -c1-16) echo "flags=$hash" >> "$GITHUB_OUTPUT" - echo "native resolved to \`${flags%% *}\` (features $hash) for job \`$GITHUB_JOB\`" >> "$GITHUB_STEP_SUMMARY" + echo "native is \`$(.github/scripts/resolve-native-rustflags.sh --cpu)\`, features $hash, job \`$GITHUB_JOB\`" >> "$GITHUB_STEP_SUMMARY" - uses: Swatinem/rust-cache@v2 with: key: cpu-${{ steps.cpu.outputs.flags }} @@ -231,7 +231,7 @@ jobs: # `.cargo/config.toml` builds with target-cpu=native, and cargo's # fingerprint sees the flag text, not the CPU: an artifact built on a # runner with AVX-512 would be reused on one without and die with - # SIGILL. Resolve `native` to this runner's CPU name and feature list: + # SIGILL. Spell `native` out as this runner's exact feature list: # a different CPU then gives different flags, cargo recompiles by itself, # and the cache is keyed to the same flags. - name: Resolve target-cpu=native for this runner @@ -241,7 +241,7 @@ jobs: echo "RUSTFLAGS=$flags" >> "$GITHUB_ENV" hash=$(printf %s "$flags" | sha256sum | cut -c1-16) echo "flags=$hash" >> "$GITHUB_OUTPUT" - echo "native resolved to \`${flags%% *}\` (features $hash) for job \`$GITHUB_JOB\`" >> "$GITHUB_STEP_SUMMARY" + echo "native is \`$(.github/scripts/resolve-native-rustflags.sh --cpu)\`, features $hash, job \`$GITHUB_JOB\`" >> "$GITHUB_STEP_SUMMARY" - uses: Swatinem/rust-cache@v2 with: key: cpu-${{ steps.cpu.outputs.flags }} From 5b373796282b3ce2435816444352b529ceb0ccf4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 05:24:17 +0000 Subject: [PATCH 13/13] ci: take the native-CPU cache fix back out of this PR The four ci commits ported here from #1526 are reverted so this PR carries only the NAL work and the CI change stays visible in its own PR. #1526 merges first; this branch then picks it up from main. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .cargo/config.toml | 12 ++---- .github/scripts/resolve-native-rustflags.sh | 44 --------------------- .github/workflows/dir-sim-persist.yml | 15 ------- .github/workflows/identity.yml | 30 -------------- 4 files changed, 3 insertions(+), 98 deletions(-) delete mode 100755 .github/scripts/resolve-native-rustflags.sh diff --git a/.cargo/config.toml b/.cargo/config.toml index ba4523219..8fdd89a22 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -35,15 +35,9 @@ # portable baseline (AVX2): cargo build --config .cargo/config-v3.toml ... # AVX-512 (v4): cargo build --config .cargo/config-avx512.toml ... # -# CI: build.yml, rust-test.yml and style.yml set `RUSTFLAGS` with an explicit -# `-C target-cpu=x86-64-v3` (a RUSTFLAGS env REPLACES every cargo-config -# rustflags entry). Every other workflow builds with `native`. Cargo -# fingerprints the flag TEXT, not the CPU, so a workflow that caches `target/` -# spells `native` out as the runner's exact feature list first -# (.github/scripts/resolve-native-rustflags.sh, used by dir-sim-persist.yml and -# identity.yml) and keys the cache to those flags; without that, an artifact -# built on a runner with AVX-512 is reused on one without and dies with SIGILL. -# The same script works locally: RUSTFLAGS="$(.github/scripts/resolve-native-rustflags.sh)". Everything else that ships pins x86_64 to v3 without +# CI is unaffected: every workflow sets `RUSTFLAGS` with an explicit +# `-C target-cpu=x86-64-v3`, and a RUSTFLAGS env REPLACES every cargo-config +# rustflags entry. Everything else that ships pins x86_64 to v3 without # touching other architectures: the Dockerfiles that copy this tree replace # this file with config-v3.toml (x86_64-only), the thinking-engine images set # CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS, and `make publish` in diff --git a/.github/scripts/resolve-native-rustflags.sh b/.github/scripts/resolve-native-rustflags.sh deleted file mode 100755 index a665ae3c8..000000000 --- a/.github/scripts/resolve-native-rustflags.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -# Print RUSTFLAGS whose target features equal `.cargo/config.toml`'s -# `-Ctarget-cpu=native` on this machine, spelled out as an explicit list. -# With --cpu, print the name rustc resolves `native` to instead (for logs). -# -# Why: cargo fingerprints the flag TEXT. `-Ctarget-cpu=native` reads the same -# on every machine, so an artifact built on a CPU with AVX-512 is reused on one -# without and dies with SIGILL. Spelled out, the text names the actual -# features: a different CPU gives different flags, and cargo recompiles. -# -# No `-Ctarget-cpu=`: a named CPU enables its model's default features, -# and a `+feature` list cannot take them back. On a VM that hides AVX-512 from -# a CPU still reporting a Skylake-server model, `-Ctarget-cpu=skylake-avx512` -# would compile AVX-512 back in. The target's baseline CPU plus the exact -# native list has no hidden defaults; the cost is tuning, not correctness. -# Features rustc hides on stable (amx, avx10) are not in the list, so they are -# never enabled; native could have enabled them, this cannot. -# -# RUSTFLAGS replaces every rustflags entry in .cargo/config.toml. That file -# sets only target-cpu, so nothing else is lost; extra flags given as -# arguments are appended. -set -euo pipefail -die() { echo "resolve-native-rustflags: $*" >&2; exit 1; } - -if [ "${1:-}" = "--cpu" ]; then - cpu=$(rustc --print target-cpus | sed -n 's/.*native.*(currently \([^)]*\)).*/\1/p') - [ -n "$cpu" ] || die "rustc did not report the native CPU" - echo "$cpu" - exit 0 -fi - -target_features() { rustc --print cfg "$@" | sed -n 's/^target_feature="\(.*\)"$/\1/p' | sort; } - -native=$(target_features -C target-cpu=native) -[ -n "$native" ] || die "no target features reported" -flags="-Ctarget-feature=$(printf '%s\n' "$native" | sed 's/^/+/' | paste -sd, -)" - -# The point of the script: these flags must enable exactly what native does. -# shellcheck disable=SC2086 -resolved=$(target_features $flags) -[ "$resolved" = "$native" ] || die "flags do not reproduce native: -$(diff <(printf '%s\n' "$native") <(printf '%s\n' "$resolved") || true)" - -echo "$flags${*:+ $*}" diff --git a/.github/workflows/dir-sim-persist.yml b/.github/workflows/dir-sim-persist.yml index 89c682181..77300aed4 100644 --- a/.github/workflows/dir-sim-persist.yml +++ b/.github/workflows/dir-sim-persist.yml @@ -62,23 +62,8 @@ jobs: run: sudo apt-get update && sudo apt-get install -y protobuf-compiler libprotobuf-dev - name: Setup rust toolchain (pinned by rust-toolchain.toml) run: rustup show - # `.cargo/config.toml` builds with target-cpu=native, and cargo's - # fingerprint sees the flag text, not the CPU: an artifact built on a - # runner with AVX-512 would be reused on one without and die with - # SIGILL. Spell `native` out as this runner's exact feature list: - # a different CPU then gives different flags, cargo recompiles by itself, - # and the cache is keyed to the same flags. - - name: Resolve target-cpu=native for this runner - id: cpu - run: | - flags=$(.github/scripts/resolve-native-rustflags.sh) - echo "RUSTFLAGS=$flags" >> "$GITHUB_ENV" - hash=$(printf %s "$flags" | sha256sum | cut -c1-16) - echo "flags=$hash" >> "$GITHUB_OUTPUT" - echo "native is \`$(.github/scripts/resolve-native-rustflags.sh --cpu)\`, features $hash, job \`$GITHUB_JOB\`" >> "$GITHUB_STEP_SUMMARY" - uses: Swatinem/rust-cache@v2 with: - key: cpu-${{ steps.cpu.outputs.flags }} workspaces: lance-graph/crates/lance-graph-dir-sim-lance - name: clippy run: cargo clippy --manifest-path crates/lance-graph-dir-sim-lance/Cargo.toml --all-targets -- -D warnings diff --git a/.github/workflows/identity.yml b/.github/workflows/identity.yml index 04a9465e7..93fd412f1 100644 --- a/.github/workflows/identity.yml +++ b/.github/workflows/identity.yml @@ -73,23 +73,8 @@ jobs: persist-credentials: false - name: Setup rust toolchain (pinned by rust-toolchain.toml) run: rustup show - # `.cargo/config.toml` builds with target-cpu=native, and cargo's - # fingerprint sees the flag text, not the CPU: an artifact built on a - # runner with AVX-512 would be reused on one without and die with - # SIGILL. Spell `native` out as this runner's exact feature list: - # a different CPU then gives different flags, cargo recompiles by itself, - # and the cache is keyed to the same flags. - - name: Resolve target-cpu=native for this runner - id: cpu - run: | - flags=$(.github/scripts/resolve-native-rustflags.sh) - echo "RUSTFLAGS=$flags" >> "$GITHUB_ENV" - hash=$(printf %s "$flags" | sha256sum | cut -c1-16) - echo "flags=$hash" >> "$GITHUB_OUTPUT" - echo "native is \`$(.github/scripts/resolve-native-rustflags.sh --cpu)\`, features $hash, job \`$GITHUB_JOB\`" >> "$GITHUB_STEP_SUMMARY" - uses: Swatinem/rust-cache@v2 with: - key: cpu-${{ steps.cpu.outputs.flags }} workspaces: lance-graph/crates/lance-graph-dir-sim - name: Run the simulator's tests run: cargo test --manifest-path crates/lance-graph-dir-sim/Cargo.toml @@ -228,23 +213,8 @@ jobs: persist-credentials: false - name: Setup rust toolchain (pinned by rust-toolchain.toml) run: rustup show - # `.cargo/config.toml` builds with target-cpu=native, and cargo's - # fingerprint sees the flag text, not the CPU: an artifact built on a - # runner with AVX-512 would be reused on one without and die with - # SIGILL. Spell `native` out as this runner's exact feature list: - # a different CPU then gives different flags, cargo recompiles by itself, - # and the cache is keyed to the same flags. - - name: Resolve target-cpu=native for this runner - id: cpu - run: | - flags=$(.github/scripts/resolve-native-rustflags.sh) - echo "RUSTFLAGS=$flags" >> "$GITHUB_ENV" - hash=$(printf %s "$flags" | sha256sum | cut -c1-16) - echo "flags=$hash" >> "$GITHUB_OUTPUT" - echo "native is \`$(.github/scripts/resolve-native-rustflags.sh --cpu)\`, features $hash, job \`$GITHUB_JOB\`" >> "$GITHUB_STEP_SUMMARY" - uses: Swatinem/rust-cache@v2 with: - key: cpu-${{ steps.cpu.outputs.flags }} workspaces: lance-graph/crates/lance-graph-rbac - name: Run the IAM tests run: cargo test --manifest-path crates/lance-graph-rbac/Cargo.toml