diff --git a/.claude/board/entries/2026-10-20-u1-jc-batch.md b/.claude/board/entries/2026-10-20-u1-jc-batch.md index bc4770185..cb4760d32 100644 --- a/.claude/board/entries/2026-10-20-u1-jc-batch.md +++ b/.claude/board/entries/2026-10-20-u1-jc-batch.md @@ -11,6 +11,7 @@ - A reversed pair swaps the sides. - Statistics: mean, variance, one-way ANOVA, covariance, Pearson, R², and the simple regression line in both directions. - Both paths refuse a request built for another version (`BatchError::VersionMismatch`). +- Both paths refuse a selection that names a resident mask plane (`BatchError::ResidentPlane`). Resident planes are U2. - `Cost` is counted from the programs that actually run: programs, selection passes, sum passes, rows traversed, values read, mask bytes. - `examples/bench.rs` (`BENCH_N`, `BENCH_REPS`). @@ -38,13 +39,16 @@ The time ratio is below the values-read ratio because a cross pass folds five su - At `i32::MIN`/`i32::MAX` values, `Σx²` passes `u64` and `Σxy` passes `i64`; the test asserts both. Mean and variance still equal the exact `i128` values. - At an offset of 10⁹, the naive one-pass f64 Pearson is off by more than 10⁻³. The exact path is within 10⁻¹² of the two-pass value. -**Tests and disables.** 8 tests pass. Six disable runs each turned red, with every disable patch's anchor asserted: +**Tests and disables.** 10 tests pass. Nine disable runs each turned red, with every disable patch's anchor asserted: - swap the reversed pair; - read the `y` side as `x`; - drop the pair dedup; - skip `normalize`; - merge only the first group; - drop the version check. +- drop the resident-plane refusal (`Rel::Plane` selections are refused, Codex on #1643); +- stop that check at `Not`. +- check planes before versions in one pass (a version mismatch must win wherever a plane appears, Codex on #1645). ## OPEN diff --git a/crates/jc-batch-probe/src/lib.rs b/crates/jc-batch-probe/src/lib.rs index e08f5541d..be12a3cfb 100644 --- a/crates/jc-batch-probe/src/lib.rs +++ b/crates/jc-batch-probe/src/lib.rs @@ -143,6 +143,10 @@ pub enum BatchError { /// The selection was decided from its description alone (all rows or /// none). The probe runs only selections that need a pass. SymbolicSelection { request: usize }, + /// The selection names a resident mask plane (`Rel::Plane`). A batch + /// reads only the table's lanes: resident planes are U2's let-planes, and + /// [`Cost`] does not count plane reads. + ResidentPlane { request: usize }, /// relalg refused the selection. Relalg(RelError), /// mask-risc refused a pass. @@ -211,7 +215,21 @@ fn selection_lanes(rel: &Rel, out: &mut Vec) { } } -fn check_versions(t: &Table<'_>, reqs: &[Request]) -> Result<(), BatchError> { +/// Whether a selection names a resident mask plane anywhere. +fn names_plane(rel: &Rel) -> bool { + match rel { + Rel::Plane(_) => true, + Rel::All | Rel::Empty | Rel::Atom(_) => false, + Rel::And(xs) | Rel::Or(xs) => xs.iter().any(names_plane), + Rel::Not(x) => names_plane(x), + } +} + +/// Refuses a request for another version, or one whose selection names a +/// resident plane, before anything runs. +fn check_requests(t: &Table<'_>, reqs: &[Request]) -> Result<(), BatchError> { + // Versions first, over the whole batch: a batch with a stale request is + // refused as `VersionMismatch` wherever a resident plane appears. for (i, r) in reqs.iter().enumerate() { if r.version != t.version { return Err(BatchError::VersionMismatch { @@ -221,7 +239,10 @@ fn check_versions(t: &Table<'_>, reqs: &[Request]) -> Result<(), BatchError> { }); } } - Ok(()) + match reqs.iter().position(|r| names_plane(&r.select)) { + Some(i) => Err(BatchError::ResidentPlane { request: i }), + None => Ok(()), + } } /// Evaluates `select` into a mask (one relalg program). @@ -412,7 +433,7 @@ pub fn answer_separately( t: &Table<'_>, reqs: &[Request], ) -> Result<(Vec, Cost), BatchError> { - check_versions(t, reqs)?; + check_requests(t, reqs)?; let mut cost = Cost::default(); let mut out = Vec::with_capacity(reqs.len()); for (i, r) in reqs.iter().enumerate() { @@ -455,7 +476,7 @@ pub fn answer_batched( t: &Table<'_>, reqs: &[Request], ) -> Result<(Vec, Cost, Reduction), BatchError> { - check_versions(t, reqs)?; + check_requests(t, reqs)?; let mut cost = Cost::default(); let mut red = Reduction::default(); diff --git a/crates/jc-batch-probe/tests/batch.rs b/crates/jc-batch-probe/tests/batch.rs index fc4129a0e..7fc8571b3 100644 --- a/crates/jc-batch-probe/tests/batch.rs +++ b/crates/jc-batch-probe/tests/batch.rs @@ -430,3 +430,46 @@ fn a_lane_only_univariate_requests_need_gets_one_pass() { assert_eq!((red.cross_passes, red.power_passes), (1, 1)); assert_eq!(cost.sum_passes, 2); } + +/// FAILS IF: a selection that names a resident mask plane, alone or inside a +/// conjunction, is run instead of refused. Neither path has resident planes, +/// so running it could only fail later inside the fold. +#[test] +fn a_selection_naming_a_resident_plane_is_refused() { + let fx = Fixture::new(500, 0x91A7); + let lanes = fx.lanes(); + let t = fx.table(&lanes); + for (k, select) in [ + Rel::Plane(0), + Rel::And(vec![low(), Rel::Not(Box::new(Rel::Plane(1)))]), + ] + .into_iter() + .enumerate() + { + let mut reqs = requests(); + reqs[5 + k].select = select; + let want = Err(BatchError::ResidentPlane { request: 5 + k }); + assert_eq!(answer_batched(&t, &reqs).map(|_| ()), want); + assert_eq!(answer_separately(&t, &reqs).map(|_| ()), want); + } +} + +/// FAILS IF: a resident-plane refusal takes precedence over a version +/// mismatch. The plane comes first (request 2) and the stale request later +/// (request 7), so a single in-order pass would report the plane. +#[test] +fn a_version_mismatch_wins_over_a_resident_plane_anywhere_in_the_batch() { + let fx = Fixture::new(500, 0x0DE7); + let lanes = fx.lanes(); + let t = fx.table(&lanes); + let mut reqs = requests(); + reqs[2].select = Rel::Plane(0); + reqs[7].version = VERSION + 1; + let want = Err(BatchError::VersionMismatch { + request: 7, + request_version: VERSION + 1, + table_version: VERSION, + }); + assert_eq!(answer_batched(&t, &reqs).map(|_| ()), want); + assert_eq!(answer_separately(&t, &reqs).map(|_| ()), want); +}