diff --git a/.claude/board/entries/2026-10-26-dr256-global-union-lab.md b/.claude/board/entries/2026-10-26-dr256-global-union-lab.md new file mode 100644 index 000000000..8085348af --- /dev/null +++ b/.claude/board/entries/2026-10-26-dr256-global-union-lab.md @@ -0,0 +1,87 @@ +# 2026-10-26 — D-DR256 lab: 64 chaotically mapped DR64 registers -> one global 8 KiB mask + +**Status:** MEASURED (lab probe). Decision: the global mask **EXISTS** as a demanded result; using it as an input to +further Boolean algebra is a **CONTRACT BLOCKER**, except for one legal form (`G AND h`, below). Nothing in +production changed. +Probe: `crates/drf-focus-probe/examples/global_union.rs` (workspace-excluded lab crate). Run from that crate: +`cargo run --release --example global_union`. + +## Source facts + +- **Mapping representation.** One register = `drf_focus_probe::SealedMap { id, seal, ord: Box<[u16; 4096]> }` + (`crates/drf-focus-probe/src/lib.rs:48-51`). `ord[slot]` is the slot's canonical address in the 65,536 universe, + so the global universe is exactly the u16 domain. `membership::BoundMap::bind` checks seal == source + `index_seal` and every ordinal < `n_rows` (`membership.rs:38`). `RowSet` is already a 1,024-word global bitset + (`membership.rs:22`). The existing direction is global → local (`membership`, `membership.rs:103`); the + union asked for here is the reverse, local → global. +- **Scatter.** `ndarray::simd::mask_scatter_or_u32` ORs `src` bits into `out` at `index[i]`, cost ∝ selected bits, + repeats union, and does not zero its output, so 64 calls union (`simd_masking_ops.rs:1322`, test `:6700`; + ndarray `195844bd`). `Terminal::ScatterOrU32` wraps it (`ir.rs:322-341`), but the executor zeroes `Out::Mask` + at the start of each program (`exec.rs:1604-1607`). So the 64 registers must run as ONE program over a + 262,144-row population: register r at rows `r*4096..`, masks laid out contiguously, one u32 index lane. +- **The restriction.** `ScatterOrU32`'s survival condition (`ir.rs:329-337`): the scattered mask "is legal only + when it IS the externally demanded result … It is never an intermediate: a follow-on program or fold that + consumes it is the forbidden projection → population → projection shape." TERNLOG combines aligned planes + (`MaskOp::Ternlog`, `ir.rs:250`); it never permutes addresses. +- **What stays legal.** A Boolean function h of global resident planes can be built with `Ternlog` + `Keep` and + then pushed down: `Gather` h through each map under `L` (`MaskOp::Gather`, `ir.rs:277`, gated form), then + scatter. This gives `scatter(L AND h∘ord) = G AND h` without reading G. `G OR h`, `NOT G`, `G XOR h` and + `G1 AND G2` of two scattered masks are not expressible through the current mask-risc terminal; they would + consume G. That is not "not expressible at all": `mask_scatter_or_u32` preserves its destination, so seeding + the demanded output with h before the scatter could yield `G OR h` as one terminal result without an + intermediate G. mask-risc zeroes `Out::Mask` first (`exec.rs:1604-1607`), so this is a hypothesis for a + later contract review, not a capability. The demonstrated consumer needs neither: G is the demanded output, + and its zero bits already exclude the other 61,440 addresses without a complement. + +## Results (two runs, avx512f, 4 cores, load 0.9–2.9; ns per run, median of 31 batches; reported, not gated) + +| fixture | selected slots | global bits | nonzero words | bind | widen | ndarray x64 (B1) | mask-risc 1 prog (B2) | oracle (A) | +|---|---|---|---|---|---|---|---|---| +| chaotic, 50 % | 130,990 | 56,927 | 1,024 | 86–108 µs | 35–37 µs | 157–168 µs | 198–227 µs | 16,753–16,813 µs | +| clustered 64×64 | 4,096 | 4,096 | 64 | 59–76 µs | 37–43 µs | 9.0–12.0 µs | 14.7–19.0 µs | 624–712 µs | +| scattered 4,096 | 4,096 | 4,096 | 1,008 | 61–73 µs | 38–45 µs | 11.1–11.2 µs | 18.1–19.4 µs | 666–700 µs | +| empty | 0 | 0 | 0 | 58–59 µs | 36–37 µs | 1.6–2.1 µs | 6.2–7.4 µs | 243–251 µs | +| full | 262,144 | 65,536 | 1,024 | 58–82 µs | 37–40 µs | 267–411 µs | 376–488 µs | 27,856–28,430 µs | + +- Parity: B1 and B2 equal the oracle exactly on every fixture. +- Scatter cost is about 1.0–1.6 ns per selected slot. The oracle is a `BTreeSet` reference arm, reported in µs and + not gated. +- Bind (64 seal and range checks over 262,144 ordinals) and widen (u16 → u32, a 1 MiB derived lane) are charged + separately, and at small selections they cost more than the scatter. +- `G AND h`: h = `x & (y | !z)` over three global planes, 20,078 bits. Building h: 0.38–0.81 µs. Gated + gather + scatter: 11.5 µs (empty), 26.8–41.0 µs (4,096 selected), 422–819 µs (full / 50 %). Parity is exact on + every fixture. +- The clustered block occupies 64 of 1,024 mask words, and the scattered set 1,008. That is selection + geometry, not a statement about Lance bytes (P1, #1789, failed). + +## Controls (all asserted) + +- Identity map: G equals L zero-extended. +- Chaotic maps: collisions occur (130,990 selected slots → 56,927 members). +- Clustered: exactly 4,096 members, 61,440 excluded. +- Empty and full fixtures. +- Two local coordinates of one entity give one member, with no false separation. Equal slot numbers in two + registers naming different entities give two members, where a slot-number union would give 1: no false merge. +- Stale seal → `StaleMap`; ordinal outside a declared 61,440 universe → `OrdinalOutOfRange`; one register at + another source version → `DomainMismatch`; another scope → `ScopeMismatch`. All refuse before any union. +- Negative controls, asserted in the probe on every run (added after codex review on #1790; the first version + reported these from uncommitted disable runs, which the command could not reproduce): + - index XOR: flipping bit 15 of every index (a bijection on the universe) must make B1 disagree with the + oracle on chaotic, clustered and scattered, and must leave `empty` and `full` equal. Bit 0 would not do: + the clustered block (x 64..128) is closed under XOR 1, and a run with bit 0 fails on that fixture. + - gather gate: the `G AND h` program without its `under` gate must disagree with the oracle on every + fixture whose L is not all ones, and agree on `full`. + - Each control goes red when its perturbation is removed (`^ 0`; gate kept), so it cannot pass vacuously. +- The Boolean union collapses multiplicity (2 selected slots → 1 member), so G is never a count, a Bag, a + provenance record or a K3 result. Inputs must already be TRUE-only. + +## Gaps (named, not built) + +- No u16-index scatter or gather: the u16 maps are widened to a 1 MiB u32 lane per 64 registers. +- No multi-register union witness: per-map `bind` exists, but "all 64 bound to one source and one scope" is + checked only in the probe. +- Running through mask-risc needs the 64 local masks contiguous (one population); a scattered layout would need a + copy or 64 ndarray calls. +- Keeping G resident for reuse would be a maintained inverse; D-HOLO H5 found that is a cost, not a default. + +OPEN: further Boolean operators over G wait for a concrete consumer (the current one needs only G); whether G should ever feed a sealed-read `RowAddrMask` (separate decision; MemWAL has no such input). diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 6559580dc..f9a3efc45 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,10 +25,11 @@ 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. -478 entries, 2026-08-06 .. 2026-10-26. +479 entries, 2026-08-06 .. 2026-10-26. | date | entry id | finding | file | |---|---|---|---| +| 2026-10-26 | `D-DR256` | | [2026-10-26-dr256-global-union-lab.md](2026-10-26-dr256-global-union-lab.md) | | 2026-10-26 | `D-DQM` | FAIL | [2026-10-26-dqm-p1-chunk-touched.md](2026-10-26-dqm-p1-chunk-touched.md) | | 2026-10-26 | `coresearch-lance13-masked-read-dr64` | | [2026-10-26-coresearch-lance13-masked-read-dr64.md](2026-10-26-coresearch-lance13-masked-read-dr64.md) | | 2026-10-25 | `D-DRF-W8` | | [2026-10-25-drf-w8-budget.md](2026-10-25-drf-w8-budget.md) | diff --git a/crates/drf-focus-probe/Cargo.toml b/crates/drf-focus-probe/Cargo.toml index 7d2489abb..62978ac3f 100644 --- a/crates/drf-focus-probe/Cargo.toml +++ b/crates/drf-focus-probe/Cargo.toml @@ -26,6 +26,8 @@ lance-graph-supervisor = { path = "../lance-graph-supervisor", features = ["cycl lance-graph-planner = { path = "../lance-graph-planner" } lance-graph-contract = { path = "../lance-graph-contract" } tokio = { version = "1", features = ["rt", "macros", "sync"] } +# examples/global_union.rs (D-DR256 lab): the one mask-RISC evaluator, unchanged. +lance-graph-mask-risc = { path = "../lance-graph-mask-risc" } [patch."https://github.com/AdaWorldAPI/ndarray"] ndarray = { path = "../../../ndarray" } diff --git a/crates/drf-focus-probe/examples/global_union.rs b/crates/drf-focus-probe/examples/global_union.rs new file mode 100644 index 000000000..511b60a97 --- /dev/null +++ b/crates/drf-focus-probe/examples/global_union.rs @@ -0,0 +1,474 @@ +//! D-DR256 lab probe: 64 chaotically mapped 64x64 registers -> ONE global +//! 256x256 Boolean mask (65,536 bits, 1,024 u64 words, 8 KiB). Lab only. +//! +//! A register is one `SealedMap` (`ord[slot]` = the slot's canonical global +//! address, u16) plus one 4,096-bit local selection `L[r]`. The global set is +//! `G[g] = OR_{r,s} L[r][s] AND (ord_r[s] == g)`, a set union: membership only, +//! never a count, never provenance, never SQL K3 (inputs are TRUE-only masks). +//! +//! Arms, all producing the same 8 KiB mask from the same inputs: +//! A oracle: BTreeSet of resolved addresses, then bits from the set. +//! B1 ndarray `mask_scatter_or_u32`, 64 calls unioning into one buffer. +//! B2 mask-risc, ONE program over the 64 registers laid out as one 262,144-row +//! population, `Terminal::ScatterOrU32 { out_rows: 65_536 }` (the +//! executor zeroes its `Out::Mask`, so 64 programs could not union). +//! C `G AND h`, h = TERNLOG(X, Y, Z) of three global planes: h is a `Keep` +//! over the 65,536-row universe; then per slot `Gather` of h through the +//! map under `L` and `ScatterOrU32`. The scattered mask is never an input: +//! `Terminal::ScatterOrU32`'s survival condition (ir.rs) forbids it. +//! +//! Run: `cargo run --release --example global_union` (from this crate). +//! Timings are ns per run, median of 31 batches, reported, not gated. +use drf_focus_probe::membership::BoundMap; +use drf_focus_probe::{MapId, Refusal, Scope, SealedMap, SLOTS, SourceId, TILE_WORDS}; +use lance_graph_mask_risc::exec::{execute_into, Scratch}; +use lance_graph_mask_risc::fuse::ternlog_imm; +use lance_graph_mask_risc::{ + scratch_words_for, words_for, Foreign, ForeignPlane, LaneRef, MaskOp, Operand, Out, Planes, + Program, Terminal, Value, +}; +use ndarray::simd::mask_scatter_or_u32; +use std::collections::BTreeSet; +use std::hint::black_box; +use std::sync::Arc; +use std::time::Instant; + +const U: usize = 65_536; +const GW: usize = U / 64; +const R: usize = 64; +const POP: usize = R * SLOTS; +const S0: Operand = Operand::Scratch(0); + +fn mix(mut z: u64) -> u64 { + z = z.wrapping_add(0x9E37_79B9_7F4A_7C15); + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) +} + +/// Fisher-Yates over 0..65536, seeded. +fn permutation(seed: u64) -> Vec { + let mut p: Vec = (0..U as u32).map(|x| x as u16).collect(); + for i in (1..U).rev() { + let j = (mix(seed ^ i as u64) % (i as u64 + 1)) as usize; + p.swap(i, j); + } + p +} + +struct Fx { + name: &'static str, + maps: Vec>, + /// The 64 local selections, register r at words [r*64, r*64+64). + local: Vec, +} + +fn source() -> SourceId { + SourceId { dataset: 7, branch: 0, version: 3, index_seal: 0xD256 } +} + +fn map_of(r: usize, ord: Vec) -> Arc { + let ord: Box<[u16; SLOTS]> = ord.into_boxed_slice().try_into().expect("4096 slots"); + Arc::new(SealedMap::new(MapId(r as u64), source().index_seal, ord)) +} + +/// Every register an independent shuffled injection into the 65,536 universe. +fn chaotic_maps(seed: u64) -> Vec> { + (0..R).map(|r| map_of(r, permutation(seed ^ (r as u64) << 20)[..SLOTS].to_vec())).collect() +} + +fn set(local: &mut [u64], r: usize, s: usize) { + local[r * TILE_WORDS + s / 64] |= 1 << (s % 64); +} + +/// Select exactly the given global addresses: each address is assigned to a +/// register and written into a random free slot of that register's map. +fn place(name: &'static str, targets: &[u16], seed: u64) -> Fx { + let mut ords: Vec> = + (0..R).map(|r| permutation(seed ^ 0xA5A5 ^ (r as u64) << 20)[..SLOTS].to_vec()).collect(); + let mut local = vec![0u64; R * TILE_WORDS]; + let per = targets.len().div_ceil(R); + for (r, chunk) in targets.chunks(per.max(1)).enumerate() { + let slots = permutation(seed ^ 0x51 ^ (r as u64) << 24); + for (&g, &s) in chunk.iter().zip(slots.iter().filter(|&&s| (s as usize) < SLOTS)) { + ords[r][s as usize] = g; + set(&mut local, r, s as usize); + } + } + Fx { name, maps: ords.into_iter().enumerate().map(|(r, o)| map_of(r, o)).collect(), local } +} + +// ------------------------------------------------------------------ witness + +/// Bind every register through the existing `BoundMap::bind` (seal and range), +/// then require ONE source and ONE scope across all 64 before any union: a +/// union of addresses from two sources or scopes is not one global set. +fn bind_all( + maps: &[Arc], + sources: &[SourceId], + scopes: &[Scope], + universe: usize, +) -> Result, Refusal> { + let mut out = Vec::with_capacity(maps.len()); + for ((m, &src), &sc) in maps.iter().zip(sources).zip(scopes) { + if src != sources[0] { + return Err(Refusal::DomainMismatch { map: src, set: sources[0] }); + } + if sc != scopes[0] { + return Err(Refusal::ScopeMismatch { map: sc, set: scopes[0] }); + } + out.push(BoundMap::bind(m.clone(), src, sc, universe)?); + } + Ok(out) +} + +/// The u32 index lane the scatter/gather primitives take: the u16 ordinals, +/// widened once (a disposable derived lane, 1 MiB for 64 registers). +fn widen(maps: &[Arc], lane: &mut [u32]) { + for (r, m) in maps.iter().enumerate() { + for (d, &o) in lane[r * SLOTS..(r + 1) * SLOTS].iter_mut().zip(m.ord.iter()) { + *d = u32::from(o); + } + } +} + +// ------------------------------------------------------------------ arms + +fn arm_a(fx: &Fx) -> Vec { + let mut s = BTreeSet::new(); + for (r, m) in fx.maps.iter().enumerate() { + for slot in 0..SLOTS { + if fx.local[r * TILE_WORDS + slot / 64] >> (slot % 64) & 1 == 1 { + s.insert(m.ord[slot]); + } + } + } + let mut g = vec![0u64; GW]; + for a in s { + g[a as usize / 64] |= 1 << (a % 64); + } + g +} + +fn arm_b1(fx: &Fx, lane: &[u32], g: &mut [u64]) { + g.fill(0); + for r in 0..R { + mask_scatter_or_u32( + &fx.local[r * TILE_WORDS..(r + 1) * TILE_WORDS], + &lane[r * SLOTS..(r + 1) * SLOTS], + g, + U, + ); + } +} + +fn scatter_program() -> Program { + Program::new(vec![], Terminal::ScatterOrU32 { mask: Operand::Plane(0), lane: 0, out_rows: U as u32 }) +} + +fn arm_b2(p: &Program, fx: &Fx, lane: &[u32], buf: &mut [u64], g: &mut [u64]) -> Value { + let masks: [&[u64]; 1] = [&fx.local]; + let lanes = [LaneRef::U32(lane)]; + let planes = Planes { n_rows: POP, masks: &masks, lanes: &lanes }; + let mut sc = Scratch::over(buf, words_for(POP), p.scratch_slots as usize).expect("scratch"); + execute_into(p, &planes, &Foreign::NONE, &mut sc, Out::Mask(g)).expect("scatter runs") +} + +/// h = TERNLOG(X, Y, Z) over three global planes, elected with `Keep`. +fn h_program(imm: u8) -> Program { + Program::new( + vec![MaskOp::Ternlog { + imm, + a: Operand::Plane(0), + b: Operand::Plane(1), + c: Operand::Plane(2), + dst: 0, + }], + Terminal::Keep { mask: S0 }, + ) +} + +fn arm_h(p: &Program, xyz: &[Vec; 3], buf: &mut [u64]) -> Vec { + let masks: [&[u64]; 3] = [&xyz[0], &xyz[1], &xyz[2]]; + let planes = Planes { n_rows: U, masks: &masks, lanes: &[] }; + let mut sc = Scratch::over(buf, words_for(U), p.scratch_slots as usize).expect("scratch"); + execute_into(p, &planes, &Foreign::NONE, &mut sc, Out::None).expect("h runs"); + sc.slot(0).expect("kept").to_vec() +} + +/// `G AND h`: gather h through each map under L, then scatter. h is a foreign +/// plane over the global universe; G itself is never read. +fn and_program() -> Program { + Program::new( + vec![MaskOp::Gather { lane: 0, foreign: 0, under: Some(Operand::Plane(0)), dst: 0 }], + Terminal::ScatterOrU32 { mask: S0, lane: 0, out_rows: U as u32 }, + ) +} + +fn arm_c(p: &Program, fx: &Fx, lane: &[u32], h: &[u64], buf: &mut [u64], g: &mut [u64]) { + let masks: [&[u64]; 1] = [&fx.local]; + let lanes = [LaneRef::U32(lane)]; + let planes = Planes { n_rows: POP, masks: &masks, lanes: &lanes }; + let fp = [ForeignPlane { words: h, rows: U }]; + let foreign = Foreign { planes: &fp, lanes: &[] }; + let mut sc = Scratch::over(buf, words_for(POP), p.scratch_slots as usize).expect("scratch"); + execute_into(p, &planes, &foreign, &mut sc, Out::Mask(g)).expect("and runs"); +} + +/// Negative control for C: the same program without the local-mask gate, so +/// h is gathered through every slot, selected or not. +fn and_program_ungated() -> Program { + Program::new( + vec![MaskOp::Gather { lane: 0, foreign: 0, under: None, dst: 0 }], + Terminal::ScatterOrU32 { mask: S0, lane: 0, out_rows: U as u32 }, + ) +} + +/// Fixtures whose answer cannot move under a bijection of the address space +/// (nothing selected, or everything): a perturbation control must stay equal +/// there, so it is checked two-sided rather than skipped. +fn permutation_invariant(name: &str) -> bool { + matches!(name, "empty" | "full") +} + +// ------------------------------------------------------------------ helpers + +fn pop(w: &[u64]) -> u32 { + w.iter().map(|x| x.count_ones()).sum() +} + +fn time(mut f: impl FnMut()) -> f64 { + let mut med = Vec::new(); + for _ in 0..31 { + let reps = 20; + let t = Instant::now(); + for _ in 0..reps { + f(); + } + med.push(t.elapsed().as_nanos() as f64 / reps as f64); + } + med.sort_by(|a, b| a.partial_cmp(b).unwrap()); + med[med.len() / 2] +} + +fn plane(seed: u64, density_per_256: u64) -> Vec { + let mut w = vec![0u64; GW]; + for g in 0..U { + if mix(seed ^ g as u64) % 256 < density_per_256 { + w[g / 64] |= 1 << (g % 64); + } + } + w +} + +fn main() { + println!( + "tier: {}", + if cfg!(target_feature = "avx512f") { "avx512f" } else { "below v4: timings not comparable to v4 runs" } + ); + let universe = U; + let scope = Scope(1); + let src = source(); + + // ---------------------------------------------------------- fixtures + let mut fixtures = Vec::new(); + // chaotic: 64 shuffled injections, 50 % local selection each + { + let maps = chaotic_maps(0xC4A0); + let mut local = vec![0u64; R * TILE_WORDS]; + for (i, w) in local.iter_mut().enumerate() { + *w = mix(0x50 ^ i as u64); + } + fixtures.push(Fx { name: "chaotic-50%", maps, local }); + } + // clustered 64x64 block of the 256x256 grid: x 64..128, y 128..192 + let block: Vec = (128..192u32) + .flat_map(|y| (64..128u32).map(move |x| (y * 256 + x) as u16)) + .collect(); + fixtures.push(place("clustered-64x64", &block, 0xB10C)); + // scattered: 4,096 distinct random addresses + let scattered: Vec = permutation(0x5CA7)[..4096].to_vec(); + fixtures.push(place("scattered-4096", &scattered, 0x5CA7)); + // empty + fixtures.push(Fx { name: "empty", maps: chaotic_maps(0xE), local: vec![0; R * TILE_WORDS] }); + // full: registers 0..16 partition the universe; the rest are shuffled + { + let mut maps = chaotic_maps(0xF); + for (r, m) in maps.iter_mut().enumerate().take(16) { + *m = map_of(r, (r * SLOTS..(r + 1) * SLOTS).map(|g| g as u16).collect()); + } + fixtures.push(Fx { name: "full", maps, local: vec![u64::MAX; R * TILE_WORDS] }); + } + + let mut lane = vec![0u32; POP]; + let mut g_b1 = vec![0u64; GW]; + let mut g_b2 = vec![0u64; GW]; + let p_scatter = scatter_program(); + let mut buf_pop = + vec![0u64; scratch_words_for(words_for(POP), (p_scatter.scratch_slots as usize).max(1)).unwrap()]; + + println!("csv,fixture,selected_slots,global_bits,nonzero_words,parity_b1,parity_b2,bind_ns,widen_ns,oracle_ns,b1_ns,b2_ns"); + for fx in &fixtures { + let sources = vec![src; R]; + let scopes = vec![scope; R]; + bind_all(&fx.maps, &sources, &scopes, universe).expect("binds"); + widen(&fx.maps, &mut lane); + let want = arm_a(fx); + arm_b1(fx, &lane, &mut g_b1); + let v = arm_b2(&p_scatter, fx, &lane, &mut buf_pop, &mut g_b2); + assert!(matches!(v, Value::Scattered)); + let (ok1, ok2) = (g_b1 == want, g_b2 == want); + assert!(ok1 && ok2, "{}: parity", fx.name); + + // Negative control (asserted, every run): flip bit 15 of every index, + // a bijection on the 65,536-address universe. The parity check must + // reject it wherever the answer can move, and only there. Bit 0 would + // be undetectable on the clustered block (x 64..128 is closed under + // XOR 1); bit 15 moves y 128..192 to 0..64. + let bad: Vec = lane.iter().map(|&i| i ^ 0x8000).collect(); + let mut g_bad = vec![0u64; GW]; + arm_b1(fx, &bad, &mut g_bad); + assert_eq!( + g_bad != want, + !permutation_invariant(fx.name), + "{}: index-XOR control", + fx.name + ); + + let t_bind = time(|| { + black_box(bind_all(&fx.maps, &sources, &scopes, universe).unwrap()); + }); + let t_widen = time(|| widen(&fx.maps, black_box(&mut lane))); + let t_a = time(|| { + black_box(arm_a(fx)); + }); + let t_b1 = time(|| arm_b1(fx, &lane, black_box(&mut g_b1))); + let t_b2 = time(|| { + black_box(arm_b2(&p_scatter, fx, &lane, &mut buf_pop, &mut g_b2)); + }); + println!( + "csv,{},{},{},{},{},{},{:.0},{:.0},{:.0},{:.0},{:.0}", + fx.name, + pop(&fx.local), + pop(&want), + want.iter().filter(|&&w| w != 0).count(), + ok1, + ok2, + t_bind, + t_widen, + t_a, + t_b1, + t_b2 + ); + } + + // ---------------------------------------------------------- composition C + let imm = ternlog_imm(|x, y, z| x && (y || !z)); + let xyz = [plane(0x11, 128), plane(0x22, 96), plane(0x33, 160)]; + let p_h = h_program(imm); + let mut buf_u = vec![0u64; scratch_words_for(words_for(U), p_h.scratch_slots as usize).unwrap()]; + let p_and = and_program(); + let p_and_ungated = and_program_ungated(); + assert!(p_and_ungated.scratch_slots <= p_and.scratch_slots); + let mut buf_and = + vec![0u64; scratch_words_for(words_for(POP), p_and.scratch_slots as usize).unwrap()]; + println!("csv_c,fixture,h_bits,g_and_h_bits,parity,h_ns,gather_scatter_ns"); + for fx in &fixtures { + widen(&fx.maps, &mut lane); + let h = arm_h(&p_h, &xyz, &mut buf_u); + let want_h: Vec = + (0..GW).map(|i| xyz[0][i] & (xyz[1][i] | !xyz[2][i])).collect(); + assert_eq!(h, want_h, "h = TERNLOG(X, Y, Z)"); + let g = arm_a(fx); + let want: Vec = g.iter().zip(&h).map(|(a, b)| a & b).collect(); + let mut got = vec![0u64; GW]; + arm_c(&p_and, fx, &lane, &h, &mut buf_and, &mut got); + let ok = got == want; + assert!(ok, "{}: G AND h parity", fx.name); + + // Negative control (asserted, every run): drop the gather gate. The + // result then ignores L, so parity must fail unless L selects every slot. + let mut ungated = vec![0u64; GW]; + arm_c(&p_and_ungated, fx, &lane, &h, &mut buf_and, &mut ungated); + let l_full = fx.local.iter().all(|&w| w == u64::MAX); + assert_eq!(ungated != want, !l_full, "{}: gate-drop control", fx.name); + let t_h = time(|| { + black_box(arm_h(&p_h, &xyz, &mut buf_u)); + }); + let t_c = time(|| arm_c(&p_and, fx, &lane, &h, &mut buf_and, black_box(&mut got))); + println!("csv_c,{},{},{},{},{:.0},{:.0}", fx.name, pop(&h), pop(&want), ok, t_h, t_c); + } + + println!("negative controls: index-XOR and gate-drop fire on every non-invariant fixture, silent on the rest"); + controls(&src, scope); +} + +/// The membership and refusal cases (§3 of the brief), each asserted. +fn controls(src: &SourceId, scope: Scope) { + let s = *src; + let id = |r: usize| map_of(r, (0..SLOTS).map(|g| g as u16).collect()); + + // 1. identity: one register, identity map -> G is L zero-extended. + let mut local = vec![0u64; R * TILE_WORDS]; + for w in 0..TILE_WORDS { + local[w] = mix(0x1D ^ w as u64); + } + let fx = Fx { name: "identity", maps: (0..R).map(id).collect(), local: local.clone() }; + let g = arm_a(&fx); + assert_eq!(&g[..TILE_WORDS], &local[..TILE_WORDS]); + assert!(g[TILE_WORDS..].iter().all(|&w| w == 0)); + + // 3 + 8. collisions / same entity: two registers select slots that both + // resolve to 777 -> exactly one bit, and the union is not a count. + let mut o3: Vec = (0..SLOTS).map(|g| g as u16).collect(); + let mut o9: Vec = (0..SLOTS).map(|g| (g + 8192) as u16).collect(); + o3[10] = 777; + o9[2000] = 777; + let mut maps: Vec> = (0..R).map(id).collect(); + maps[3] = map_of(3, o3); + maps[9] = map_of(9, o9); + let mut local = vec![0u64; R * TILE_WORDS]; + set(&mut local, 3, 10); + set(&mut local, 9, 2000); + let fx = Fx { name: "same-entity", maps, local }; + let g = arm_a(&fx); + assert_eq!(pop(&g), 1, "two local coordinates of one entity are one member"); + assert!(pop(&fx.local) > pop(&g), "Boolean union collapsed multiplicity 2 -> 1 (not a count)"); + + // 9. equal slot numbers, different entities: slot 5 of registers 0 and 1. + let mut o0: Vec = (0..SLOTS).map(|g| g as u16).collect(); + let mut o1: Vec = (0..SLOTS).map(|g| (g + 4096) as u16).collect(); + o0[5] = 100; + o1[5] = 200; + let mut maps: Vec> = (0..R).map(id).collect(); + maps[0] = map_of(0, o0); + maps[1] = map_of(1, o1); + let mut local = vec![0u64; R * TILE_WORDS]; + set(&mut local, 0, 5); + set(&mut local, 1, 5); + let fx = Fx { name: "same-slot", maps, local }; + let g = arm_a(&fx); + assert_eq!(pop(&g), 2, "same slot number, two entities: no false merge"); + let slot_keyed = (fx.local[0] | fx.local[TILE_WORDS]).count_ones(); + assert_eq!(slot_keyed, 1, "a slot-number union would have merged them"); + + // 7. refusals, before any union. + let maps: Vec> = (0..R).map(id).collect(); + let scopes = vec![scope; R]; + let mut stale = maps.clone(); + stale[17] = Arc::new(SealedMap::new(MapId(17), s.index_seal ^ 1, maps[17].ord.clone())); + assert!(matches!(bind_all(&stale, &vec![s; R], &scopes, U), Err(Refusal::StaleMap { .. }))); + let mut wide = maps.clone(); + wide[40] = map_of(40, (0..SLOTS).map(|g| (g + 61_440) as u16).collect()); + assert!(matches!( + bind_all(&wide, &vec![s; R], &scopes, 61_440), + Err(Refusal::OrdinalOutOfRange { .. }) + )); + let mut versions = vec![s; R]; + versions[5].version += 1; + assert!(matches!(bind_all(&maps, &versions, &scopes, U), Err(Refusal::DomainMismatch { .. }))); + let mut sc = scopes.clone(); + sc[63] = Scope(2); + assert!(matches!(bind_all(&maps, &vec![s; R], &sc, U), Err(Refusal::ScopeMismatch { .. }))); + println!("controls: identity, same-entity, same-slot, stale seal, out-of-universe, version, scope -> all hold"); +}