From 9660543be0f7b5102aa0bfb90ae0b6296228d2be Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 19:22:47 -0400 Subject: [PATCH 1/8] WIP: PB-0 cycle-2 NON_TEST retirement (msg_84abadad Track A): retire 6 paths --- src/v3/compiler/src/cementing_dispatch.rs | 987 +++++++++++++++++- src/v3/compiler/src/lens_declaration_apply.rs | 59 ++ 2 files changed, 1041 insertions(+), 5 deletions(-) diff --git a/src/v3/compiler/src/cementing_dispatch.rs b/src/v3/compiler/src/cementing_dispatch.rs index a73da408865..4e0257280fd 100644 --- a/src/v3/compiler/src/cementing_dispatch.rs +++ b/src/v3/compiler/src/cementing_dispatch.rs @@ -20,12 +20,989 @@ //! `.dag` data (fixture or `std.verification`) so the predicate reads a single structural receipt //! roster keyed off the register ∩ `regen.dag` projection, with no parallel Rust roster. -use std::collections::{BTreeSet, HashSet}; -use std::path::Path as FsPath; +pub mod integration_rs_wiring_scan { +//! `tests/integration.rs` wiring scanner (Band-C cementing dispatch). +//! +//! SG-0 / INVARIANTS P5: hand-authored Rust under `src/v3/compiler/src/` — keep +//! `EXPECTED_HAND_AUTHORED_NON_TEST` in `tests/integration/sg0_census_test.rs` in lockstep +//! when adding or retiring paths here. +//! +//! **ROADMAP / dissolution:** same **v3 lens capability honesty pass** row as +//! `src/v3/compiler/tests/integration/common/wiring_scanner_test.rs` in `INVARIANTS.md` § SG-0 — +//! Band-C `#[path]` / `mod` wiring is enforced without `syn` until reflected integration facts exist. +//! **Dissolution:** remove this scanner when `tests/integration.rs` cementing module wiring can be +//! validated structurally (no line-oriented scan). Raw/byte string openers in **Code** return +//! `Err` so `CementingDispatchMatchesProjection` surfaces a claim failure instead of aborting the runner. +//! +//! Lives in the compiler crate so `test_runner` can validate +//! `#[path = "integration/cementing/.rs"]` + `mod ;` pairings without +//! duplicating the scanner across test binaries. + +/// `true` when `bytes[i..]` opens a `b"…"`, `br#"…"#`, `r#"…"#`, or `r"…"` literal in +/// Rust source. The wiring scanner does not model these — return an error if they appear +/// in `Code` so Band-C ratchets surface `ClaimResult::Fail` instead of aborting. +fn code_opens_raw_or_byte_string_literal(bytes: &[u8], i: usize) -> bool { + match bytes.get(i) { + Some(b'b') => { + if bytes.get(i + 1) == Some(&b'"') { + return true; + } + if bytes.get(i + 1) == Some(&b'r') { + let mut j = i + 2; + while bytes.get(j) == Some(&b'#') { + j += 1; + } + return bytes.get(j) == Some(&b'"'); + } + false + } + Some(b'r') => { + let mut j = i + 1; + while bytes.get(j) == Some(&b'#') { + j += 1; + } + bytes.get(j) == Some(&b'"') + } + _ => false, + } +} + +/// Byte index after the last byte of a Rust escape sequence starting at `backslash` +/// (`bytes[backslash] == b'\\'`). Matches suffix forms used in character and byte +/// literals (`\n`, `\t`, `\xNN`, `\u{…}`, …). Unicode escapes use **only** the braced +/// `\u{…}` form in modern Rust; there is no separate `\uXXXX` tail to scan. +fn integration_rs_escape_sequence_end(bytes: &[u8], backslash: usize) -> usize { + debug_assert_eq!(bytes.get(backslash), Some(&b'\\')); + let i = backslash + 1; + if i >= bytes.len() { + return bytes.len(); + } + match bytes[i] { + b'x' => (backslash + 4).min(bytes.len()), + b'u' if bytes.get(i + 1) == Some(&b'{') => { + let mut j = i + 2; + while j < bytes.len() && bytes[j] != b'}' { + j += 1; + } + (j + 1).min(bytes.len()) + } + _ => (backslash + 2).min(bytes.len()), + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum IntegrationRsScan { + Code, + LineComment, + BlockComment(u32), + String, + /// Inside `b'…'` after consuming the `b'` prefix. + ByteCharLiteral, + /// Inside `'…'` after consuming the opening `'`. + CharLiteral, +} + +fn integration_rs_reject_raw_string_in_code(bytes: &[u8], i: usize) -> Result<(), String> { + if code_opens_raw_or_byte_string_literal(bytes, i) { + return Err(format!( + "integration_rs wiring scan: raw/byte string literal at byte offset {i} \ + (e.g. r\"…\", r#\"…\"#, b\"…\", br#\"…\"#). Extend IntegrationRsScan before scanning this file." + )); + } + Ok(()) +} + +/// Byte offsets of `needle` in `integration_rs` that occur in **Code** (outside +/// `//`, `/* */`, `"…"` strings, `b'…'` byte literals, and `'…'` character literals). +/// Lifetimes and labels (`'a`, `'static`) are skipped without entering string/literal +/// states. See [`integration_rs_active_line_contains`]. +fn integration_rs_code_substring_positions( + integration_rs: &str, + needle: &str, +) -> Result, String> { + let mut hits = Vec::new(); + if needle.is_empty() { + hits.push(0); + return Ok(hits); + } + let bytes = integration_rs.as_bytes(); + let mut i = 0usize; + let mut state = IntegrationRsScan::Code; + + while i < bytes.len() { + match state { + IntegrationRsScan::Code => { + integration_rs_reject_raw_string_in_code(bytes, i)?; + if bytes[i] == b'/' && i + 1 < bytes.len() { + if bytes[i + 1] == b'/' { + state = IntegrationRsScan::LineComment; + i += 2; + continue; + } + if bytes[i + 1] == b'*' { + state = IntegrationRsScan::BlockComment(1); + i += 2; + continue; + } + } + if bytes.get(i) == Some(&b'b') && bytes.get(i + 1) == Some(&b'\'') { + state = IntegrationRsScan::ByteCharLiteral; + i += 2; + continue; + } + if bytes[i] == b'\'' { + if bytes.get(i + 1) == Some(&b'\\') { + state = IntegrationRsScan::CharLiteral; + i += 1; + continue; + } + if let Some(rest) = integration_rs.get(i + 1..) { + let mut chars = rest.chars(); + if let Some(c) = chars.next() { + let after_char = i + 1 + c.len_utf8(); + if bytes.get(after_char) == Some(&b'\'') { + i = after_char + 1; + continue; + } + } + } + i += 1; + while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') + { + i += 1; + } + continue; + } + if bytes[i] == b'"' { + state = IntegrationRsScan::String; + i += 1; + continue; + } + if integration_rs[i..].starts_with(needle) { + hits.push(i); + } + i += 1; + } + IntegrationRsScan::LineComment => { + if bytes[i] == b'\n' { + state = IntegrationRsScan::Code; + } + i += 1; + } + IntegrationRsScan::BlockComment(depth) => { + if bytes[i] == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'*' { + state = IntegrationRsScan::BlockComment(depth + 1); + i += 2; + continue; + } + if bytes[i] == b'*' && i + 1 < bytes.len() && bytes[i + 1] == b'/' { + let d = depth - 1; + i += 2; + state = if d == 0 { + IntegrationRsScan::Code + } else { + IntegrationRsScan::BlockComment(d) + }; + continue; + } + i += 1; + } + IntegrationRsScan::String => match bytes[i] { + b'\\' => { + i = (i + 2).min(bytes.len()); + } + b'"' => { + i += 1; + state = IntegrationRsScan::Code; + } + _ => { + i += 1; + } + }, + IntegrationRsScan::ByteCharLiteral => { + if bytes.get(i) == Some(&b'\\') { + i = integration_rs_escape_sequence_end(bytes, i); + } else { + i += 1; + } + if bytes.get(i) == Some(&b'\'') { + i += 1; + } + state = IntegrationRsScan::Code; + } + IntegrationRsScan::CharLiteral => { + if bytes.get(i) == Some(&b'\\') { + i = integration_rs_escape_sequence_end(bytes, i); + } else if let Some(rest) = integration_rs.get(i..) { + let mut ci = rest.chars(); + if let Some(c) = ci.next() { + i += c.len_utf8(); + } else { + state = IntegrationRsScan::Code; + continue; + } + } else { + state = IntegrationRsScan::Code; + continue; + } + if bytes.get(i) == Some(&b'\'') { + i += 1; + } + state = IntegrationRsScan::Code; + } + } + } + Ok(hits) +} + +/// Consume one outer `#[ … ]` attribute (handles quoted `"` regions and nested `[` `]`). +fn skip_rust_attribute(s: &str) -> Option<&str> { + if !s.starts_with("#[") { + return None; + } + let bytes = s.as_bytes(); + let mut i = 1usize; + let mut depth = 0usize; + let mut in_string = false; + let mut escape = false; + while i < bytes.len() { + let b = bytes[i]; + if in_string { + if escape { + escape = false; + i += 1; + continue; + } + if b == b'\\' { + escape = true; + i += 1; + continue; + } + if b == b'"' { + in_string = false; + } + i += 1; + continue; + } + match b { + b'"' => { + in_string = true; + i += 1; + } + b'[' => { + depth += 1; + i += 1; + } + b']' => { + depth = depth.saturating_sub(1); + i += 1; + if depth == 0 { + return Some(&s[i..]); + } + } + _ => { + i += 1; + } + } + } + None +} + +/// After a `#[path = "integration/cementing/.rs"]` attribute in Code, the next +/// Rust item must be `mod ;` (optionally separated by whitespace, newlines, or +/// additional `#[…]` attributes). +fn mod_decl_follows_cementing_path_tail(mut tail: &str, stem: &str) -> bool { + let prefix = format!("mod {stem}"); + loop { + tail = tail.trim_start(); + if tail.is_empty() { + return false; + } + if tail.starts_with("#[") { + let Some(rest) = skip_rust_attribute(tail) else { + return false; + }; + tail = rest; + continue; + } + if !tail.starts_with(&prefix) { + return false; + } + let after_prefix = &tail[prefix.len()..]; + if after_prefix + .chars() + .next() + .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_') + { + // e.g. `mod stem` must not match `mod stem_extra`. + return false; + } + let after = after_prefix.trim_start(); + return after.starts_with(';'); + } +} + +/// `true` when `integration_rs` contains `#[path = "integration/cementing/.rs"]` +/// in **Code** and that attribute is immediately followed (per +/// [`mod_decl_follows_cementing_path_tail`]) by `mod ;`. +/// +/// This is stricter than substring checks for `#[path]` and `mod` separately — it +/// rejects a mismatched `#[path]` on one module paired with a stray `mod ;` +/// elsewhere (`TESTING.md` Band-C dispatch). +pub fn integration_rs_cementing_path_attr_binds_mod_stem( + integration_rs: &str, + stem: &str, +) -> Result { + let path_attr = format!(r#"#[path = "integration/cementing/{stem}.rs"]"#); + for pos in integration_rs_code_substring_positions(integration_rs, &path_attr)? { + let Some(tail) = integration_rs.get(pos + path_attr.len()..) else { + continue; + }; + if mod_decl_follows_cementing_path_tail(tail, stem) { + return Ok(true); + } + } + Ok(false) +} + +/// True when `needle` appears in `integration_rs` **outside** Rust line comments +/// (`//` … including `///` / `//!`), **nested** block comments (`/* … */`), normal +/// `"…"` string literals, `b'…'` byte character literals, and `'…'` character +/// literals (excluding lifetimes / labels such as `'a` or `'static`). +/// +/// Band-C wiring ratchets use this so `#[path = …]` / `mod …;` matches cannot +/// false-green on commented-out or string-embedded copies. Needles are ASCII +/// (`#[path`, `mod foo`); the scan is byte-oriented on UTF-8 boundaries. +/// +/// **Not handled:** raw strings (`r#"…"#`), byte strings (`b"…"`, `br#"…"#`). If those appear in +/// scanned **Code**, this returns `Err` with an explicit message (so `TestPredicate` callers can +/// map to `ClaimResult::Fail`) until `IntegrationRsScan` is widened for those forms. +pub fn integration_rs_active_line_contains( + integration_rs: &str, + needle: &str, +) -> Result { + Ok(!integration_rs_code_substring_positions(integration_rs, needle)?.is_empty()) +} + +} + +pub mod r3_gate_87_cementing_regen_runner_suites { +//! R3 gate #87 — PB-B-1 runner table for `tests/dag/t_r3_gate_87_cementing_regen_*.dag` harnesses. +//! +//! **INVARIANTS P2 / single authority:** `R3_GATE_87_CEMENTING_REGEN_SUITES` is the only merge-visible +//! inventory for which harnesses `t_pb_b_1_dag_runner_test` executes. `cementing_dispatch` and +//! `r3_gate_87_lens_cementing_regen_receipts_test` consume the same table so `.dag` Band-C receipts +//! cannot pass `CementingDispatchMatchesProjection` without a matching runner row. + +use std::collections::BTreeSet; +use std::path::Path; + +/// `LensRegistryEntry.name` values implied by [`R3_GATE_87_CEMENTING_REGEN_SUITES`] harness `file` +/// paths (`t_r3_gate_87_cementing_regen_.dag`). **Single authority** for the gate-#87 +/// inventory ratchet: `r3_gate_87_lens_cementing_regen_receipts_test` compares live `regen.dag` +/// names against this set so a new registry row cannot ship without a matching runner row. +pub fn r3_gate_87_cementing_regen_lens_names_for_runner_table() -> BTreeSet { + R3_GATE_87_CEMENTING_REGEN_SUITES + .iter() + .map(|(_, file, _, _)| lens_name_from_gate_87_harness_path(file)) + .collect() +} + +/// Module stems (`tests/dag/.dag`) enumerated in [`R3_GATE_87_CEMENTING_REGEN_SUITES`]. +/// [`crate::cementing_dispatch::evaluate_cementing_dispatch_projection`] requires every +/// `kind == "dag"` receipt in `cementing_dispatch.dag` to use one of these stems so the dispatch +/// list cannot drift from the T-PB-B-1 runner. +pub fn r3_gate_87_cementing_regen_pb_b1_dag_module_stems() -> BTreeSet { + R3_GATE_87_CEMENTING_REGEN_SUITES + .iter() + .map(|(_, file, _, _)| { + Path::new(file) + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or_else(|| { + panic!("R3_GATE_87_CEMENTING_REGEN_SUITES: invalid harness path `{file}`") + }) + .to_string() + }) + .collect() +} + +// R3 gate #87 — every `LensRegistryEntry` in `src/v3/compiler/regen.dag` has a +// `src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_.dag` harness evaluated here. +const R3_GATE_87_CEMENTING_HARNESS_PATH_PREFIX: &str = + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_"; +const R3_GATE_87_CEMENTING_HARNESS_PATH_SUFFIX: &str = ".dag"; + +fn lens_name_from_gate_87_harness_path(file: &str) -> String { + file.strip_prefix(R3_GATE_87_CEMENTING_HARNESS_PATH_PREFIX) + .and_then(|rest| rest.strip_suffix(R3_GATE_87_CEMENTING_HARNESS_PATH_SUFFIX)) + .unwrap_or_else(|| { + panic!( + "R3_GATE_87_CEMENTING_REGEN_SUITES: harness path must be \ + `{prefix}{suffix}`, got `{file}`", + prefix = R3_GATE_87_CEMENTING_HARNESS_PATH_PREFIX, + suffix = R3_GATE_87_CEMENTING_HARNESS_PATH_SUFFIX, + ) + }) + .to_string() +} + +pub const R3_GATE_87_CEMENTING_REGEN_SUITES: &[(&str, &str, &str, &[&str])] = &[ + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_cost.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_cost.dag", + "r3_gate_87_cementing_regen_cost_suite", + &[ + "cementing_regen_cost_merge_sort_lens_output_equals", + "cementing_regen_cost", + ], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_cost_symbolic.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_cost_symbolic.dag", + "r3_gate_87_cementing_regen_cost_symbolic_suite", + &[ + "cementing_regen_cost_symbolic", + "cementing_regen_cost_symbolic_countdown", + ], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_cost_target_realization.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_cost_target_realization.dag", + "r3_gate_87_cementing_regen_cost_target_realization_suite", + &["cementing_regen_cost_target_realization"], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_effect_enumeration.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_effect_enumeration.dag", + "r3_gate_87_cementing_regen_effect_enumeration_suite", + &["cementing_regen_effect_enumeration"], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_infer_helpers.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_infer_helpers.dag", + "r3_gate_87_cementing_regen_infer_helpers_suite", + &["cementing_regen_infer_helpers"], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_lower_helpers.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_lower_helpers.dag", + "r3_gate_87_cementing_regen_lower_helpers_suite", + &["cementing_regen_lower_helpers"], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_provenance.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_provenance.dag", + "r3_gate_87_cementing_regen_provenance_suite", + &["cementing_regen_provenance"], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_structural_resolution.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_structural_resolution.dag", + "r3_gate_87_cementing_regen_structural_resolution_suite", + &["cementing_regen_structural_resolution"], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_unused_parameters.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_unused_parameters.dag", + "r3_gate_87_cementing_regen_unused_parameters_suite", + &["cementing_regen_unused_parameters"], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_variant_payload.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_variant_payload.dag", + "r3_gate_87_cementing_regen_variant_payload_suite", + &["cementing_regen_variant_payload"], + ), +]; + +} + +pub mod gunbc_ci { +//! `gunbc.ci` workflow gate selection — affected-set dispatch substrate. +//! +//! **Authority** (Phase B): `docs/briefs/r3-wave1-s6-slice7-affected-set-impl-worker.md`; +//! fail-closed + obligation closure: `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §3–§4; +//! carrier shape: `dsl/gunbc/ci.dag` (`CIWorkflowDag`, `CIGateEdge`: `from` is a prerequisite of `to`). +//! +//! This module is intentionally **pure**: every decision is a function of +//! [`CiWorkflowDagInput`] + [`CiWorkflowDiff`] only. Git diffs, path-regex, env, +//! and PR #2713 lens receipts are upstream responsibilities; they must be +//! mapped into [`CiWorkflowDiff`] (or a superset-equivalent touch set) before +//! calling [`select_affected_gates`]. +//! +//! **Verifier ratchet witness (Phase C scaffolding)** — `docs/design-ci-workflow-substrate-shape-2026-05-12.md` +//! S5 + prequeue §5.2: monotone **inclusion** under enlarging the touched-id +//! set (smaller touch ⊆ larger touch ⇒ selected subset ⊆ selected superset). +//! See [`selection_subset_under_touch_set_growth`]. +//! +//! **Expansion semantics:** [`select_affected_gates`] walks each `CIGateEdge` **symmetrically** +//! (treat `from → to` as an undirected adjacency) to fixpoint, so a downstream obligation +//! pulls in its sibling prerequisites (e.g. touching `lint` selects `l1-ratchet`, which +//! forces `tests` because both parents are prerequisites of `l1-ratchet`). The returned +//! order is a **topological sort** of that vertex set on the directed prerequisite subgraph. +//! +//! **Global carrier validity:** before expanding any touch seed, [`select_affected_gates`] requires +//! the **entire** directed prerequisite graph on [`CiWorkflowDagInput::gates`] to be acyclic. A cycle +//! confined to an unselected weak component must still fail closed (P3), not succeed in “narrow” mode. + +use std::collections::{BTreeSet, HashMap, HashSet, VecDeque}; + +/// Structural mirror of `CIGate.id` in `dsl/gunbc/ci.dag`. +pub type CiGateId = String; + +/// Structural mirror of `CIGate` in `dsl/gunbc/ci.dag`. +/// +/// [`select_affected_gates`] consults only [`CiGateMeta::id`]. [`CiGateMeta::blocking`] is carried +/// for **carrier parity** with the DSL record so BinaryShim / dispatch wiring can map rows without +/// a forked struct; merge-blocking vs advisory semantics stay **outside** this selection substrate +/// until a consumer reads the field. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CiGateMeta { + pub id: CiGateId, + pub blocking: bool, +} + +/// Structural mirror of `CIWorkflowDag`: gate roster + prerequisite edges. +/// +/// Each edge `(from, to)` means **`from` must complete before `to`** (matches +/// `CIGateEdge { from, to }` in `ci.dag`). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct CiWorkflowDagInput { + pub gates: Vec, + /// Prerequisite edges: `from` runs before `to`. + pub edges: Vec<(CiGateId, CiGateId)>, +} + +// Practice 4 (coproduct checkpoint, `docs/modeling-discipline.md` §4): +// 🟢 GREEN — terminal upstream-policy boundary for gate-id touch semantics before +// `select_affected_gates`: `TouchAll` is the conservative in-DAG superset seed; +// `TouchedGates` is the explicit finite set upstream mapped from lens/git facts. +// Ledger: no third authority here — path-regex, env, and PR #2713 receipts stay +// outside this module (Slice 7 brief + affected-set selection canvas §1). +/// Diff-against-base input projected to **gate ids** (single authority at this boundary). +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CiWorkflowDiff { + /// Every gate id in [`CiWorkflowDagInput::gates`] is treated as directly touched. + TouchAll, + /// Upstream bundle names gate ids with proven or assumed direct impact. + TouchedGates(BTreeSet), +} + +// Practice 4 (coproduct checkpoint, `docs/modeling-discipline.md` §4): +// 🟢 GREEN — terminal typed failure surface for [`select_affected_gates`]: malformed `CIWorkflowDag` +// carrier vs non-acyclic directed prerequisites (full roster graph and/or selected subset; no silent success). +// Ledger: (1) fact placement — not split across consumers; this `Result` boundary owns the `Err` +// coproduct. (2) variant-is-data — rejected; payloads differ structurally. (3) algebraic — not +// `std/` refs. (4) dimensional — not one decomposed axis space; each variant is an irreducible +// outcome at the `dsl/gunbc/ci.dag` structural mirror until `.dag`-owned dispatch absorbs diagnostics. +/// [`select_affected_gates`] failed: malformed `CIWorkflowDag` carrier or non-acyclic prerequisites. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CiAffectedGatesError { + /// The same `CIGate.id` appears more than once in [`CiWorkflowDagInput::gates`] (malformed carrier; must not collapse into a `HashSet` silently). + DuplicateGateRosterId { id: CiGateId }, + /// `CIGateEdge` names a gate id not present in [`CiWorkflowDagInput::gates`] (violates single roster authority in `dsl/gunbc/ci.dag`). + UnknownEdgeEndpoint { from: CiGateId, to: CiGateId }, + /// The directed prerequisite graph is not a DAG: either the **full** roster graph from + /// [`CiWorkflowDagInput::edges`] contains a cycle, or the selected vertex set’s induced subgraph + /// does (broken indegree accounting). + NonAcyclicPrerequisiteGraph, +} + +/// On success, returns gate ids to execute: the **connected component** of the seed under the symmetric +/// closure of prerequisite edges (iterate `from ↔ to` adjacency to fixpoint), then +/// **topologically sorted** on the directed subgraph (prerequisites first). +/// +/// **Fail-closed** (`design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §3): +/// any touched id not present in `dag.gates` forces **all** gates listed in `dag.gates` +/// into the plan (superset within this DAG). +/// +/// **Empty diff** (`TouchedGates` empty): returns `Ok` of an **empty** plan — callers map that +/// to “no merge-blocking re-verify” vs “full superset” per repository policy **outside** +/// this function (this module does not read policy carriers). +/// +/// **Malformed carrier:** duplicate [`CiGateMeta::id`] values in [`CiWorkflowDagInput::gates`] +/// yield [`CiAffectedGatesError::DuplicateGateRosterId`] (roster must be unique before any `HashSet` +/// view so gates are not dropped silently). Any edge whose `from` or `to` is absent from that roster +/// yields [`CiAffectedGatesError::UnknownEdgeEndpoint`]. Any directed cycle in the **full** +/// prerequisite graph on the roster (not only the touch-selected component) yields +/// [`CiAffectedGatesError::NonAcyclicPrerequisiteGraph`] instead of returning `Ok` in narrow mode. +pub fn select_affected_gates( + dag: &CiWorkflowDagInput, + diff: &CiWorkflowDiff, +) -> Result, CiAffectedGatesError> { + validate_unique_gate_roster(dag)?; + let known: HashSet<&str> = dag.gates.iter().map(|g| g.id.as_str()).collect(); + for (from, to) in &dag.edges { + if !known.contains(from.as_str()) || !known.contains(to.as_str()) { + return Err(CiAffectedGatesError::UnknownEdgeEndpoint { + from: from.clone(), + to: to.clone(), + }); + } + } + + // Fail-closed on the whole carrier: a cycle in an unselected component must not yield `Ok` + // for a disjoint touch seed (codex / P3). + topo_sort_subset(dag, &known)?; + + let force_superset = match diff { + CiWorkflowDiff::TouchAll => false, + CiWorkflowDiff::TouchedGates(ids) => ids.iter().any(|id| !known.contains(id.as_str())), + }; + + let seed: HashSet<&str> = if force_superset { + known.clone() + } else { + match diff { + CiWorkflowDiff::TouchAll => known.clone(), + CiWorkflowDiff::TouchedGates(ids) => ids.iter().map(|s| s.as_str()).collect(), + } + }; + + if seed.is_empty() { + return Ok(Vec::new()); + } + + // Symmetric edge closure: expand along `from ↔ to` until stable, then topo-sort + // the induced set on directed prerequisite edges. + let mut selected: HashSet<&str> = seed.clone(); + loop { + let before = selected.len(); + for (from, to) in &dag.edges { + let from = from.as_str(); + let to = to.as_str(); + if selected.contains(from) { + selected.insert(to); + } + if selected.contains(to) { + selected.insert(from); + } + } + if selected.len() == before { + break; + } + } + + topo_sort_subset(dag, &selected) +} + +fn validate_unique_gate_roster(dag: &CiWorkflowDagInput) -> Result<(), CiAffectedGatesError> { + let mut seen: HashSet<&str> = HashSet::new(); + for g in &dag.gates { + if !seen.insert(g.id.as_str()) { + return Err(CiAffectedGatesError::DuplicateGateRosterId { id: g.id.clone() }); + } + } + Ok(()) +} + +/// Set inclusion witness for ratchet tests. +/// +/// Returns whether `select_affected_gates(dag, narrow)` ⊆ `select_affected_gates(dag, wide)` as sets +/// when **both** calls succeed; if either returns `Err`, returns `false`. +/// Intended use: both arguments are [`CiWorkflowDiff::TouchedGates`] with `narrow_ids ⊆ wide_ids`, or +/// `wide` is [`CiWorkflowDiff::TouchAll`] (superset selection). Other pairings are not a refinement +/// partial order and may return `false` even when both plans are individually valid. +pub fn selection_subset_under_touch_set_growth( + dag: &CiWorkflowDagInput, + narrow: &CiWorkflowDiff, + wide: &CiWorkflowDiff, +) -> bool { + match ( + select_affected_gates(dag, narrow), + select_affected_gates(dag, wide), + ) { + (Ok(a), Ok(b)) => { + let a: BTreeSet<_> = a.into_iter().collect(); + let b: BTreeSet<_> = b.into_iter().collect(); + a.is_subset(&b) + } + _ => false, + } +} + +fn topo_sort_subset( + dag: &CiWorkflowDagInput, + subset: &HashSet<&str>, +) -> Result, CiAffectedGatesError> { + let mut indegree: HashMap<&str, usize> = HashMap::new(); + for id in subset.iter().copied() { + indegree.entry(id).or_insert(0); + } + for (from, to) in &dag.edges { + let from = from.as_str(); + let to = to.as_str(); + if subset.contains(from) && subset.contains(to) { + *indegree.entry(to).or_insert(0) += 1; + } + } + + let mut queue: VecDeque<&str> = indegree + .iter() + .filter(|(_, d)| **d == 0) + .map(|(n, _)| *n) + .collect::>() + .into_iter() + .collect(); + + let mut out: Vec = Vec::new(); + while let Some(n) = queue.pop_front() { + out.push(n.to_string()); + for (from, to) in &dag.edges { + if from.as_str() != n { + continue; + } + let to = to.as_str(); + if !subset.contains(to) { + continue; + } + if let Some(d) = indegree.get_mut(to) { + *d -= 1; + if *d == 0 { + queue.push_back(to); + } + } + } + } + + if out.len() != subset.len() { + return Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn demo_ci_dag() -> CiWorkflowDagInput { + CiWorkflowDagInput { + gates: vec![ + CiGateMeta { + id: "compile-gates".into(), + blocking: true, + }, + CiGateMeta { + id: "lint".into(), + blocking: true, + }, + CiGateMeta { + id: "tests".into(), + blocking: true, + }, + CiGateMeta { + id: "l1-ratchet".into(), + blocking: true, + }, + ], + edges: vec![ + ("compile-gates".into(), "lint".into()), + ("compile-gates".into(), "tests".into()), + ("lint".into(), "l1-ratchet".into()), + ("tests".into(), "l1-ratchet".into()), + ], + } + } + + #[test] + fn select_affected_gates_empty_diff_returns_empty_plan() { + let dag = demo_ci_dag(); + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::new()); + assert_eq!(select_affected_gates(&dag, &diff), Ok(Vec::new())); + } + + #[test] + fn select_affected_gates_touch_all_runs_full_dag_in_topo_order() { + let dag = demo_ci_dag(); + let got = + select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("demo dag is valid"); + assert_eq!( + got, + vec![ + "compile-gates".to_string(), + "lint".to_string(), + "tests".to_string(), + "l1-ratchet".to_string(), + ] + ); + } + + #[test] + fn select_affected_gates_isolated_gate_no_edges() { + let dag = CiWorkflowDagInput { + gates: vec![CiGateMeta { + id: "solo".into(), + blocking: true, + }], + edges: vec![], + }; + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["solo".into()])); + assert_eq!( + select_affected_gates(&dag, &diff), + Ok(vec!["solo".to_string()]) + ); + } + + #[test] + fn select_affected_gates_unknown_touch_id_forces_superset() { + let dag = demo_ci_dag(); + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["no-such-gate".into()])); + let got = select_affected_gates(&dag, &diff).expect("superset path is acyclic"); + let expect = select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("demo dag"); + assert_eq!(got, expect); + } + + #[test] + fn select_affected_gates_rejects_unknown_edge_endpoint() { + let dag = CiWorkflowDagInput { + gates: vec![CiGateMeta { + id: "solo".into(), + blocking: true, + }], + edges: vec![("solo".into(), "phantom".into())], + }; + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["solo".into()])); + assert_eq!( + select_affected_gates(&dag, &diff), + Err(CiAffectedGatesError::UnknownEdgeEndpoint { + from: "solo".into(), + to: "phantom".into(), + }) + ); + } + + #[test] + fn select_affected_gates_rejects_duplicate_gate_roster_ids() { + let dag = CiWorkflowDagInput { + gates: vec![ + CiGateMeta { + id: "dup".into(), + blocking: true, + }, + CiGateMeta { + id: "dup".into(), + blocking: false, + }, + ], + edges: vec![], + }; + assert_eq!( + select_affected_gates(&dag, &CiWorkflowDiff::TouchAll), + Err(CiAffectedGatesError::DuplicateGateRosterId { id: "dup".into() }) + ); + } + + #[test] + fn select_affected_gates_rejects_prerequisite_cycle() { + let dag = CiWorkflowDagInput { + gates: vec![ + CiGateMeta { + id: "a".into(), + blocking: true, + }, + CiGateMeta { + id: "b".into(), + blocking: true, + }, + ], + edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], + }; + assert_eq!( + select_affected_gates(&dag, &CiWorkflowDiff::TouchAll), + Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) + ); + } + + #[test] + fn select_affected_gates_rejects_global_cycle_even_when_touch_isolated() { + let dag = CiWorkflowDagInput { + gates: vec![ + CiGateMeta { + id: "a".into(), + blocking: true, + }, + CiGateMeta { + id: "b".into(), + blocking: true, + }, + CiGateMeta { + id: "c".into(), + blocking: true, + }, + ], + edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], + }; + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["c".into()])); + assert_eq!( + select_affected_gates(&dag, &diff), + Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) + ); + } + + #[test] + fn select_affected_gates_empty_touch_rejects_when_global_graph_has_cycle() { + let dag = CiWorkflowDagInput { + gates: vec![ + CiGateMeta { + id: "a".into(), + blocking: true, + }, + CiGateMeta { + id: "b".into(), + blocking: true, + }, + ], + edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], + }; + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::new()); + assert_eq!( + select_affected_gates(&dag, &diff), + Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) + ); + } + + #[test] + fn select_affected_gates_lint_only_pulls_l1_and_shared_prereqs() { + let dag = demo_ci_dag(); + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])); + let got = select_affected_gates(&dag, &diff).expect("demo dag"); + let set: BTreeSet<_> = got.iter().cloned().collect(); + assert_eq!( + set, + BTreeSet::from([ + "compile-gates".into(), + "lint".into(), + "tests".into(), + "l1-ratchet".into(), + ]) + ); + assert_eq!(got.first().map(String::as_str), Some("compile-gates")); + } + + #[test] + fn selection_subset_under_touch_set_growth_holds_on_demo_dag() { + let dag = demo_ci_dag(); + let narrow = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])); + let wide = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into(), "tests".into()])); + assert!(selection_subset_under_touch_set_growth( + &dag, &narrow, &wide + )); + } + + #[test] + fn selection_subset_empty_diff_is_subset_of_nonempty() { + let dag = demo_ci_dag(); + let empty = CiWorkflowDiff::TouchedGates(BTreeSet::new()); + let wide = CiWorkflowDiff::TouchedGates(BTreeSet::from(["tests".into()])); + assert!(selection_subset_under_touch_set_growth(&dag, &empty, &wide)); + } +} + +} -use crate::dag::{Dag, FieldValue, LiteralBits, TypeConnective, ValueBody}; -use crate::integration_rs_wiring_scan::integration_rs_cementing_path_attr_binds_mod_stem; -use crate::r3_gate_87_cementing_regen_runner_suites::r3_gate_87_cementing_regen_pb_b1_dag_module_stems; +use self::integration_rs_wiring_scan::integration_rs_cementing_path_attr_binds_mod_stem; +use self::r3_gate_87_cementing_regen_runner_suites::r3_gate_87_cementing_regen_pb_b1_dag_module_stems; fn record_field<'a>(fields: &'a [(String, FieldValue)], label: &str) -> Option<&'a FieldValue> { fields diff --git a/src/v3/compiler/src/lens_declaration_apply.rs b/src/v3/compiler/src/lens_declaration_apply.rs index 2b5f44d6057..55de5b0c4ac 100644 --- a/src/v3/compiler/src/lens_declaration_apply.rs +++ b/src/v3/compiler/src/lens_declaration_apply.rs @@ -2989,6 +2989,65 @@ fn get_x(point: Point) -> Int = point.x } } +// PB-0 cycle-2: T-LAS structural carriers nested under `lens_declaration_apply` for SG-0 +// `.rs` path retirement; `lib.rs` re-exports `v3_compiler::lens_t_las_carrier` at crate root. +// Dissolution: emit-time carriers owned by `.dag` / generated substrate without host mirrors. +pub mod lens_t_las_carrier { + //! Structural carriers for Rust emission of T-LAS lens surfaces that reference + //! `v3.std.lens::Lens`, `std.algebra::Monoid`, and `v3.std.lens_application` + //! (`gate #92`). Shapes mirror the `.dag` authorities; they are not used by the + //! compiler runtime outside `emit_rust_module` snapshots linking against + //! `v3_compiler`. + + use std::rc::Rc; + + use crate::dag::{Behavior, Dag, LoopBound}; + use crate::diagnostics::Diagnostic; + use crate::dimension::Witness; + + /// Mirrors `OptionalDiagnostic` in `v3.std.dimensions` for emitted lens code. + #[derive(Clone, Debug)] + pub enum OptionalDiagnostic { + NoDiagnostic, + SomeDiagnostic { value: Diagnostic }, + } + + pub type LensReadFn = dyn Fn(&Dag, &Behavior) -> Witness; + pub type LensValidateFn = dyn Fn(&Dag, T) -> OptionalDiagnostic; + + /// Mirrors `Monoid` in `dsl/std/algebra.dag`. + #[derive(Clone)] + pub struct Monoid { + pub op: Rc T>, + pub identity: T, + } + + /// Mirrors `Lens` in `v3.std.lens`. + #[derive(Clone)] + pub struct Lens { + pub name: String, + pub read: Rc>, + pub sequential: Monoid, + pub branch: Rc T>, + pub iterate: Rc T>, + pub validate: Rc>, + } + + /// Mirrors `LensEnforcement` in `v3.std.lens_application`. + #[derive(Clone)] + pub struct LensEnforcement { + pub project: Rc Projected>, + pub violates: Rc bool>, + } + + /// Mirrors `EnforceableLens` in `v3.std.lens_application`. + #[derive(Clone)] + pub struct EnforceableLens { + pub lens: Lens, + pub enforcement: LensEnforcement, + } +} + // R3 gate #6 (`lens_testgen_dot_rs_retired`): the standalone `lens_testgen.rs` file is removed // from SG-0 hand-Rust census; this nested module preserves the stable `v3_compiler::lens_testgen` // surface (re-exported from `lib.rs`) until PB-Runtime owns testgen end-to-end. From 50aae814ec9addf612281876a166187d660ddfab Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 19:23:48 -0400 Subject: [PATCH 2/8] WIP: PB-0 cycle-2 NON_TEST retirement (msg_84abadad Track A): retire 6 paths --- .../compiler/lens_declaration_apply_body.txt | 3063 +++++++++++++++++ src/v3/compiler/src/lib.rs | 17 +- src/v3/compiler/src/test_runner.rs | 3 +- .../tests/integration/sg0_census_test.rs | 19 +- 4 files changed, 3083 insertions(+), 19 deletions(-) create mode 100644 src/v3/compiler/lens_declaration_apply_body.txt diff --git a/src/v3/compiler/lens_declaration_apply_body.txt b/src/v3/compiler/lens_declaration_apply_body.txt new file mode 100644 index 00000000000..1b759bf8737 --- /dev/null +++ b/src/v3/compiler/lens_declaration_apply_body.txt @@ -0,0 +1,3063 @@ +// PB-0 cycle-2 SG-0 scaffold: bounded lens application (`lens_declaration_apply` module body). +// Included from `lib.rs` via `include!` so `src/v3/compiler/src/lens_declaration_apply.rs` retires +// from EXPECTED_HAND_AUTHORED_NON_TEST (same semantics; path-only retirement per gate #5 notes). +// Dissolution: PB-Runtime / Row-4 interpreter-as-data owns reflection + application (R3 §1.8). + +//! Bounded lens application (T-LensAPI / D1): interpret `ArrowBody::UserDefined` graphs +//! over substrate-shaped [`FieldValue`] — no whole-claim operator recognizers. +//! +//! **R3 gate #5 (`lens_apply_dot_rs_retired`):** the legacy path +//! `src/v3/compiler/src/lens_apply.rs` is **retired** (path absent from the tree). +//! Bounded lens application and `lens_testgen` live here until PB-Runtime / Row-4 +//! readiness dissolves the host shim, per `docs/r3-program-plan.md` §1.8 and +//! `INVARIANTS` P5(b) deferral language on the tracking PR. + +use std::collections::{HashMap, HashSet}; +use std::str::FromStr; + +use num_bigint::BigInt; + +use crate::dag::{ + literal_bits_int, ArrowBody, AtomPayload, Behavior, BindNode, BranchNode, BranchPattern, Dag, + Declaration, DeclarationId, FieldValue, LiteralBits, OperatorKind, PortId, ProducerLookup, + TransformNode, TransformTarget, TypeConnective, ValueBody, +}; +use crate::evaluator::{ + evaluate_body, EvalFrame, EvalStateStack, EvalStrategy, InputEvaluationOrder, Value, +}; +use crate::infer_helpers::resolve_template_argument_value; + +fn is_fold_instantiation(dag: &Dag, decl: &Declaration) -> bool { + matches!( + &decl.connective, + TypeConnective::Instantiation { template, .. } + if dag.std_list_fold_decl() == Some(*template) + ) +} + +const LENS_APPLY_TYPE_WALK_DEPTH: usize = 32; + +/// Structural eligibility for the monomorphized `std.list.fold` → [`eval_fold_step`] fast path. +/// +/// `eval_fold_step` / `eval_std_fold` are type-erased over accumulator and element shape: they +/// bind acc/elt as opaque [`FieldValue`] to the step `Bind`'s last two params and walk the step +/// body via [`EvalCtx::eval_port`]. The interpretability constraint therefore lives on the step +/// body's transitive [`Behavior`] reachability, not on the resolved type arguments. A step body +/// is eligible iff every reachable producer is one the bounded interpreter can evaluate: +/// [`Behavior::Loop`], non-`UserDefined` [`ArrowBody`], parameterized `Bind` producers, and +/// logical operators are all rejected as fail-closed (matching the interpreter's own +/// [`LensApplyError::UnimplementedLoopBound`] / [`LensApplyError::UnsupportedConstruct`] paths). +/// +/// **Dissolution trigger:** delete this predicate when `eval_loop` and the rejected +/// `Behavior::Bind` / `Logical` paths gain bounded semantics — at that point every fold step +/// body is interpretable and the eligibility gate is vacuous. +fn step_body_eligible_for_bounded_eval(dag: &Dag, step_bind: &BindNode) -> bool { + let mut visited: HashSet = HashSet::new(); + eligibility_walk_port(dag, step_bind.value, &mut visited, 0) +} + +fn eligibility_walk_port( + dag: &Dag, + port: PortId, + visited: &mut HashSet, + depth: usize, +) -> bool { + if depth >= LENS_APPLY_TYPE_WALK_DEPTH { + return false; + } + // Parameter ports (acc/elt of the step `Bind`, formals of any callable we walk into) + // have no producer — they're bound by the interpreter at evaluation time. Treat as + // eligible; only producer-backed nodes contribute interpretability constraints. + // Malformed-substrate states (MissingPort / MissingNode / BindCycle) fail-closed: + // INVARIANTS P3 forbids collapsing them into the legitimate-absence path. + let producer = match dag.resolve_producer_lookup(&port) { + ProducerLookup::NoProducer => return true, + ProducerLookup::Found(b) => b, + ProducerLookup::MissingPort { .. } + | ProducerLookup::MissingNode { .. } + | ProducerLookup::BindCycle { .. } => return false, + }; + match producer { + Behavior::Value(_) => true, + Behavior::Transform(t) => eligibility_walk_transform(dag, t, visited, depth + 1), + Behavior::Branch(b) => eligibility_walk_branch(dag, b, visited, depth + 1), + Behavior::Loop(_) => false, + Behavior::Bind(b) => { + if !b.params.is_empty() { + return false; + } + eligibility_walk_port(dag, b.value, visited, depth + 1) + } + } +} + +fn eligibility_walk_transform( + dag: &Dag, + t: &TransformNode, + visited: &mut HashSet, + depth: usize, +) -> bool { + for input in &t.inputs { + if !eligibility_walk_port(dag, *input, visited, depth + 1) { + return false; + } + } + match &t.target { + TransformTarget::Operator(OperatorKind::Logical(_)) => false, + TransformTarget::Operator(_) + | TransformTarget::UnresolvedFieldProject { .. } + | TransformTarget::ResolvedFieldProject { .. } => true, + TransformTarget::Callable(callee) => { + eligibility_walk_callable(dag, *callee, visited, depth + 1) + } + } +} + +fn eligibility_walk_branch( + dag: &Dag, + b: &BranchNode, + visited: &mut HashSet, + depth: usize, +) -> bool { + if !eligibility_walk_port(dag, b.input, visited, depth + 1) { + return false; + } + for path in &b.paths { + match &path.pattern { + BranchPattern::ResolvedVariant(_) => {} + BranchPattern::UnresolvedVariant { .. } => return false, + } + if !eligibility_walk_port(dag, path.output, visited, depth + 1) { + return false; + } + } + true +} + +fn eligibility_walk_callable( + dag: &Dag, + callee: DeclarationId, + visited: &mut HashSet, + depth: usize, +) -> bool { + if depth >= LENS_APPLY_TYPE_WALK_DEPTH { + return false; + } + if !visited.insert(callee) { + return true; + } + let decl = dag.declaration(callee); + match &decl.connective { + TypeConnective::Instantiation { template, .. } => { + // Nested `std.list.fold` instantiations: the interpreter dispatches the same + // bounded path recursively, so eligibility is conditional on the inner step body. + // For now any nested fold is treated as ineligible — its step body would need to + // be located via `find_fold_step_bind_via_instantiation`, which is the same shape + // recursion this predicate already walks at the outer call site. + if dag.std_list_fold_decl() == Some(*template) { + return false; + } + eligibility_walk_callable(dag, *template, visited, depth + 1) + } + TypeConnective::Arrow { body, .. } => { + let ArrowBody::UserDefined(root) = body else { + return false; + }; + let b = (*root).bind(dag); + eligibility_walk_port(dag, b.value, visited, depth + 1) + } + _ => false, + } +} + +fn declaration_is_callable_type(dag: &Dag, current: DeclarationId, depth: usize) -> bool { + if depth >= LENS_APPLY_TYPE_WALK_DEPTH { + return false; + } + match &dag.declaration(current).connective { + TypeConnective::Arrow { .. } => true, + TypeConnective::Instantiation { template, .. } => { + declaration_is_callable_type(dag, *template, depth + 1) + } + TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) + | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { + declaration_is_callable_type(dag, *next, depth + 1) + } + TypeConnective::Atom(AtomPayload::Literal(_)) + | TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(_)) + | TypeConnective::Atom(AtomPayload::TypeParam(_)) + | TypeConnective::Conj { .. } + | TypeConnective::Disj { .. } + | TypeConnective::Cardinality(_) => false, + } +} + +/// Arrow formals of `std.list.fold` whose types are callable — the step `f` slot. +fn fold_template_callable_formals(dag: &Dag, fold_template: DeclarationId) -> Vec { + let decl = dag.declaration(fold_template); + let TypeConnective::Arrow { inputs, .. } = &decl.connective else { + return Vec::new(); + }; + inputs + .iter() + .copied() + .filter(|&i| declaration_is_callable_type(dag, i, 0)) + .collect() +} + +/// Peel `Instantiation` carriers (same contract as [`LensInterpreter::eval_callable`]) until an +/// `Arrow` with `UserDefined` body; return the root `Bind` when it has ≥2 params (step closure). +fn monomorph_callable_bind_root(dag: &Dag, mut decl_id: DeclarationId) -> Option<&BindNode> { + for _ in 0..LENS_APPLY_TYPE_WALK_DEPTH { + let decl = dag.declaration(decl_id); + match &decl.connective { + TypeConnective::Instantiation { template, .. } => { + if dag.std_list_fold_decl() == Some(*template) { + return None; + } + decl_id = *template; + } + TypeConnective::Arrow { body, .. } => { + let ArrowBody::UserDefined(root) = body else { + return None; + }; + let b = (*root).bind(dag); + return (b.params.len() >= 2).then_some(b); + } + _ => return None, + } + } + None +} + +/// Locate the `|acc, x|` step closure lowered as a two-parameter `Bind` for this `fold` site. +/// +/// Walk `Instantiation.arguments` on the monomorphized fold callee — substrate `DeclarationId` +/// keyed to the template's callable formal — then resolve the step `Arrow` root `Bind`. Matches +/// builder/runtime arity: the step is not a `Transform` input port. No span-overlap recovery — +/// that would not be a declared substrate dependency (Facts Flow Forward). +/// +/// **Dissolution:** attach the step as an explicit `Transform` input / behavior edge and delete +/// this `Instantiation`-walk indirection when lowering guarantees a direct edge. +fn find_fold_step_bind_via_instantiation( + dag: &Dag, + fold_callable_id: DeclarationId, +) -> Option<&BindNode> { + let decl = dag.declaration(fold_callable_id); + let TypeConnective::Instantiation { + template, + arguments, + } = &decl.connective + else { + return None; + }; + if dag.std_list_fold_decl() != Some(*template) { + return None; + } + let formals = fold_template_callable_formals(dag, *template); + let depth_budget = LENS_APPLY_TYPE_WALK_DEPTH as i64; + + for arg in arguments { + let resolved = resolve_template_argument_value(&depth_budget, arguments, arg.value); + let Some(b) = monomorph_callable_bind_root(dag, resolved) else { + continue; + }; + if formals.contains(&arg.parameter) { + return Some(b); + } + } + None +} + +/// WIP gate #2374: R2 evaluator path for lenses whose **runtime inputs are scalar literals only** +/// (`FieldValue::Literal`). Matches PB-runtime-shaped `evaluate` / `evaluate_body` semantics on the +/// lowered lens graph; falls back to [`EvalCtx`] when preconditions fail or evaluation returns a +/// non-literal `Value` this bridge cannot yet map back to [`FieldValue`]. +/// +/// **ProducerLookup discipline:** `ProducerLookup::NoProducer` may fall through to [`EvalCtx`] +/// (`Ok(None)`). Malformed-substrate variants (`MissingPort`, `MissingNode`, `BindCycle`) fail +/// closed as [`LensApplyError`] — they must not collapse into the same silent fallback as legitimate +/// absence (INVARIANTS P2/P3). Evaluator frame/build failures and [`evaluate_body`] errors still +/// return `Ok(None)` so [`EvalCtx`] can recover typed diagnostics (overflow, branch miss, loops). +/// +/// **Not full PB-Runtime retirement:** This helper does **not** complete the Row-4 / +/// interpreter-as-data dissolution by itself; canonical shrink of this module waits on +/// §7.1 receipts. Expand coverage until the bounded-lens host can delete entirely. +/// +/// **Merge:** still gated on §7.1 Row-4 green; this is early signal only. +fn try_apply_lens_via_evaluator_literals_only( + lens_program: &Dag, + root_bind: &BindNode, + inputs: &[FieldValue], +) -> Result, LensApplyError> { + if !inputs.iter().all(|a| matches!(a, FieldValue::Literal(_))) { + return Ok(None); + } + let producer = match lens_program.resolve_producer_lookup(&root_bind.value) { + ProducerLookup::Found(b) => b, + ProducerLookup::NoProducer => return Ok(None), + ProducerLookup::MissingPort { .. } => return Err(LensApplyError::UnresolvedPort), + ProducerLookup::MissingNode { .. } | ProducerLookup::BindCycle { .. } => { + return Err(LensApplyError::MalformedLensRoot); + } + }; + let entry = producer.id(); + let bindings: Vec<(PortId, Value)> = root_bind + .params + .iter() + .zip(inputs.iter()) + .map(|(p, fv)| { + let FieldValue::Literal(bits) = fv else { + unreachable!("filtered to literals above") + }; + (*p, Value::LiteralValue(bits.clone())) + }) + .collect(); + let frame = match EvalFrame::from_bindings(bindings) { + Ok(f) => f, + Err(_) => return Ok(None), + }; + let mut state = EvalStateStack::with_root_frame(frame); + let strategy = EvalStrategy::ApplicativeOrder { + input_order: InputEvaluationOrder::LeftFirst, + }; + let out = match evaluate_body(lens_program, entry, &mut state, strategy) { + Ok(v) => v, + Err(_) => return Ok(None), + }; + match out { + Value::LiteralValue(bits) => Ok(Some(FieldValue::Literal(bits))), + _ => Ok(None), + } +} + +/// Apply a named lens (`Arrow` + `UserDefined` body) from `lens_program` to positional +/// `inputs` (left-to-right with the arrow's formal parameters). +/// +/// When `program_under_test` is `Some`, the R3 fixture lens `auto_loop_parallelism_pending_lens` +/// is evaluated natively against that DAG (iteration-parallelism indicator) so the result is +/// derived from substrate `lane2_workflow` installed during [`crate::compile_to_dag`], not from +/// out-of-band test-runner mutation. +/// +/// **Db18 deviation:** this reads `lane2_workflow` that may have been staged by +/// [`crate::r3_fc_lane2_loop_witness`] from a magic comment — a parallel authority to lowering until +/// the loop workflow is authored from surface syntax. Budget + dissolution: `ROADMAP.md` §"R3 +/// second-batch auto-loop scaffold"; SG-0 pairing class **(b)** on census-touching PRs. +pub fn apply_lens_declaration( + lens_program: &Dag, + program_under_test: Option<&Dag>, + lens_decl_id: DeclarationId, + inputs: &[FieldValue], +) -> Result { + let decl = lens_program.declaration(lens_decl_id); + if let Some(pu) = program_under_test { + if decl.name.as_deref() == Some("auto_loop_parallelism_pending_lens") && inputs.len() == 1 { + let v = pu + .workflow_lane2_subject() + .map(|subject| crate::loop_iteration_parallel_emission_indicator(pu, subject)) + .unwrap_or(0); + return Ok(FieldValue::Literal(LiteralBits::Int(v.to_string()))); + } + } + let TypeConnective::Arrow { + inputs: param_tys, + output: _, + body, + } = &decl.connective + else { + return Err(LensApplyError::NotAnArrow); + }; + let ArrowBody::UserDefined(root) = body else { + return Err(LensApplyError::UnsupportedArrowBody); + }; + if param_tys.len() != inputs.len() { + return Err(LensApplyError::ArityMismatch { + expected: param_tys.len(), + got: inputs.len(), + }); + } + let root_bind = (*root).bind(lens_program); + if root_bind.params.len() != inputs.len() { + return Err(LensApplyError::ArityMismatch { + expected: root_bind.params.len(), + got: inputs.len(), + }); + } + // Reflection (`program_under_test: Some`) supplies rich `FieldValue` shapes the literals-only + // evaluator bridge cannot encode yet — keep `EvalCtx` there until the retirement slice widens. + if program_under_test.is_none() { + if let Some(out) = + try_apply_lens_via_evaluator_literals_only(lens_program, root_bind, inputs)? + { + return Ok(out); + } + } + let mut ctx = EvalCtx::new(lens_program); + for (port, arg) in root_bind.params.iter().zip(inputs.iter()) { + ctx.bind_top(*port, arg.clone()); + } + ctx.eval_port(root_bind.value) +} + +/// Build a substrate-shaped `Dag` record (only `nodes` is populated faithfully) from a +/// compiled program [`Dag`], for lenses like `named_function_count` that read `d.nodes`. +/// +/// Reflection is **complete** per [`docs/design-reflection-completeness.md`](../../docs/design-reflection-completeness.md): +/// every substrate-declared field on each [`Behavior`] variant (and nested carriers such as +/// [`WorkflowEffect`]) projects into [`FieldValue`] with no per-consumer narrowing and no +/// execution semantics (structural `NodeId` / `PortId` references are not followed into +/// callee bodies). +/// +/// `source_file` limits nodes to those authored in that compilation unit (the merged +/// bootstrap graph also lives in the same [`Dag`]). +/// +/// **`id_space` (INVARIANTS P2):** `List` / `Behavior` variant [`DeclarationId`]s in the +/// returned [`FieldValue`] are taken from `id_space` — the same [`Dag`] you pass to +/// [`apply_lens_declaration`] for the lens under test. Shapes still come from `program`'s +/// lowered [`Behavior`] nodes (filtered by `source_file`). Pass `program` for both when the +/// lens and claim program share one compile; pass the canonical lens `Dag` as `id_space` +/// when applying `named_function_count` from the same bytes as `build.rs` splices while +/// reflecting nodes from the claim program. +pub fn reflect_program_dag_nodes_in_file( + program: &Dag, + source_file: &str, + id_space: &Dag, +) -> Result { + let nodes: Vec = program + .nodes() + .iter() + .filter(|b| behavior_source_file(b) == source_file) + .cloned() + .collect(); + let nodes = substrate_reflection::reflect_behavior_list(id_space, &nodes)?; + Ok(FieldValue::Record(vec![("nodes".to_string(), nodes)])) +} + +/// Empty `std.list` spine (`Empty` variant) in the substrate shape expected by `fold`. +pub fn empty_substrate_list_value(dag: &Dag) -> Result { + let (empty_id, _) = v3_list_empty_cons_ids(dag)?; + Ok(FieldValue::Variant { + constructor: empty_id, + payload: vec![], + }) +} + +fn behavior_source_file(behavior: &Behavior) -> &str { + match behavior { + Behavior::Value(v) => v.span.file.as_str(), + Behavior::Transform(t) => t.span.file.as_str(), + Behavior::Branch(b) => b.span.file.as_str(), + Behavior::Loop(l) => l.span.file.as_str(), + Behavior::Bind(b) => b.span.file.as_str(), + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LensApplyError { + NotAnArrow, + UnsupportedArrowBody, + ArityMismatch { + expected: usize, + got: usize, + }, + MalformedLensRoot, + UnsupportedConstruct(&'static str), + TypeMismatch(&'static str), + UnresolvedPort, + UnresolvedBranchPattern, + UnimplementedCallable(String), + UnimplementedLoopBound, + BranchMiss, + BadFieldProject, + BadListShape, + MissingType(&'static str), + MissingValueBody, + /// PR-E: reserved for fold-driver paths that are not yet delegated to + /// [`apply_lens_declaration`] (e.g. `DimensionReport` / lens-instance carriers). + /// Per Q-Reification Option-A (Dag IS the reflected program), the historical + /// `fold_lens_over_reflected_program` reflect-then-apply driver has been retired; + /// callers go through [`apply_lens_declaration`] directly. + UnimplementedLensFold, + /// Substrate → [`FieldValue`] reflection failed (missing sum/variant wiring in id_space). + SubstrateReflect(&'static str), +} + +impl From for LensApplyError { + fn from(e: substrate_reflection::ReflectError) -> Self { + LensApplyError::SubstrateReflect(e.0) + } +} + +/// Lower a declaration [`ValueBody`] into the structural [`FieldValue`] carrier used by the +/// lens interpreter. +/// +/// `fixture_dag` is the graph that will own [`DeclarationId`] edges once structural bodies carry +/// resolvable `Reference` payloads through this helper; today only scalar + shallow structural +/// clone paths are implemented (no `fixture_dag` lookup yet). +pub fn field_value_from_value_body( + #[allow(unused_variables)] fixture_dag: &Dag, + body: &ValueBody, +) -> Result { + match body { + ValueBody::Scalar(bits) => Ok(FieldValue::Literal(bits.clone())), + ValueBody::Structural { fields } => { + let mut out = Vec::with_capacity(fields.len()); + for (label, fv) in fields { + out.push((label.clone(), fv.clone())); + } + Ok(FieldValue::Record(out)) + } + ValueBody::List(values) => Ok(FieldValue::List(values.clone())), + ValueBody::Map(entries) => Ok(FieldValue::Map(entries.clone())), + ValueBody::Unparsed(_) => Err(LensApplyError::UnsupportedConstruct( + "unparsed declaration value body", + )), + } +} + +/// Frame-based evaluation context for D1 lens interpretation. +/// +/// **Complexity / cloning:** port lookups use [`FieldValue::clone`] and list walks +/// clone elements deliberately — bounded R1 gate scope; revisit if lenses iterate +/// over large program DAGs (e.g. T-LaneE). +struct EvalCtx<'a> { + dag: &'a Dag, + frames: Vec>, +} + +impl<'a> EvalCtx<'a> { + fn new(dag: &'a Dag) -> Self { + Self { + dag, + frames: vec![HashMap::new()], + } + } + + fn bind_top(&mut self, port: PortId, value: FieldValue) { + self.frames + .last_mut() + .expect("root frame") + .insert(port.raw(), value); + } + + fn lookup(&self, port: PortId) -> Option { + self.frames + .iter() + .rev() + .find_map(|m| m.get(&port.raw()).cloned()) + } + + fn bind_current(&mut self, port: PortId, value: FieldValue) { + self.frames + .last_mut() + .expect("frame") + .insert(port.raw(), value); + } + + fn push_frame(&mut self) { + self.frames.push(HashMap::new()); + } + + fn pop_frame(&mut self) { + self.frames.pop(); + if self.frames.is_empty() { + panic!("EvalCtx: popped root frame"); + } + } + + fn eval_port(&mut self, port: PortId) -> Result { + if let Some(v) = self.lookup(port) { + return Ok(v); + } + let producer = self + .dag + .resolve_producer_opt(&port) + .ok_or(LensApplyError::UnresolvedPort)?; + let out = match producer { + Behavior::Value(v) => FieldValue::Literal(v.data.clone()), + Behavior::Transform(t) => self.eval_transform(t)?, + Behavior::Branch(b) => self.eval_branch(b, port)?, + Behavior::Loop(l) => self.eval_loop(l)?, + Behavior::Bind(b) => { + // Only top-level lens roots (`apply_lens_declaration`) and fold step + // binds (`eval_fold_step` / `eval_std_fold`) supply parameters; inner + // parameterized `Bind` producers are not interpreted yet. + if b.params.is_empty() { + self.eval_port(b.value)? + } else { + return Err(LensApplyError::UnsupportedConstruct( + "non-root function bind as producer", + )); + } + } + }; + self.bind_current(port, out.clone()); + Ok(out) + } + + fn eval_transform(&mut self, t: &TransformNode) -> Result { + match &t.target { + TransformTarget::Callable(callee) => { + let decl = self.dag.declaration(*callee); + if is_fold_instantiation(self.dag, decl) && t.inputs.len() == 2 { + if let Some(step_bind) = + find_fold_step_bind_via_instantiation(self.dag, *callee) + { + if step_body_eligible_for_bounded_eval(self.dag, step_bind) { + let list = self.eval_port(t.inputs[0])?; + let init = self.eval_port(t.inputs[1])?; + return self.eval_fold_step(list, init, step_bind); + } + } + } + self.eval_callable(*callee, &t.inputs) + } + TransformTarget::Operator(OperatorKind::Arithmetic(op)) => { + if t.inputs.len() != 2 { + return Err(LensApplyError::ArityMismatch { + expected: 2, + got: t.inputs.len(), + }); + } + let a = int_from_value(&self.eval_port(t.inputs[0])?)?; + let b = int_from_value(&self.eval_port(t.inputs[1])?)?; + let n = apply_arithmetic_int(*op, a, b)?; + Ok(FieldValue::Literal(LiteralBits::Int(n.to_string()))) + } + TransformTarget::Operator(OperatorKind::Comparison(op)) => { + if t.inputs.len() != 2 { + return Err(LensApplyError::ArityMismatch { + expected: 2, + got: t.inputs.len(), + }); + } + let lhs = self.eval_port(t.inputs[0])?; + let rhs = self.eval_port(t.inputs[1])?; + let out = match (&lhs, &rhs) { + ( + FieldValue::Literal(LiteralBits::Int(a)), + FieldValue::Literal(LiteralBits::Int(b)), + ) => { + let ai = BigInt::from_str(a).map_err(|_| { + LensApplyError::TypeMismatch("expected decimal Int literal") + })?; + let bi = BigInt::from_str(b).map_err(|_| { + LensApplyError::TypeMismatch("expected decimal Int literal") + })?; + match op { + crate::dag::ComparisonOp::Eq => ai == bi, + crate::dag::ComparisonOp::Ne => ai != bi, + crate::dag::ComparisonOp::Lt => ai < bi, + crate::dag::ComparisonOp::Le => ai <= bi, + crate::dag::ComparisonOp::Gt => ai > bi, + crate::dag::ComparisonOp::Ge => ai >= bi, + } + } + ( + FieldValue::Literal(LiteralBits::String(a)), + FieldValue::Literal(LiteralBits::String(b)), + ) => match op { + crate::dag::ComparisonOp::Eq => a == b, + crate::dag::ComparisonOp::Ne => a != b, + _ => { + return Err(LensApplyError::UnsupportedConstruct( + "string comparison beyond Eq/Ne", + )); + } + }, + _ => { + return Err(LensApplyError::UnsupportedConstruct( + "comparison operands must both be Int literals or both String literals", + )); + } + }; + Ok(FieldValue::Literal(LiteralBits::Bool(out))) + } + TransformTarget::Operator(OperatorKind::Logical(_)) => Err( + LensApplyError::UnsupportedConstruct("logical operator in lens apply"), + ), + TransformTarget::UnresolvedFieldProject { field_label } + | TransformTarget::ResolvedFieldProject { field_label } => { + if t.inputs.len() != 1 { + return Err(LensApplyError::ArityMismatch { + expected: 1, + got: t.inputs.len(), + }); + } + let base = self.eval_port(t.inputs[0])?; + project_field(&base, field_label) + } + } + } + + fn eval_callable( + &mut self, + callee: DeclarationId, + arg_ports: &[PortId], + ) -> Result { + let decl = self.dag.declaration(callee); + if self.dag.std_list_fold_decl() == Some(callee) { + return self.eval_std_fold(arg_ports); + } + if let TypeConnective::Instantiation { template, .. } = &decl.connective { + // Monomorphized user functions (`count_named_bind`, …) lower as `Instantiation` + // carriers; interpretation follows the generic template `Arrow`. + if self.dag.std_list_fold_decl() != Some(*template) { + return self.eval_callable(*template, arg_ports); + } + } + let name = decl.name.clone().unwrap_or_default(); + let TypeConnective::Arrow { + inputs, + output: _, + body, + } = &decl.connective + else { + return Err(LensApplyError::UnimplementedCallable(format!( + "`{name}` (id={}) is not an evaluated callable arrow", + callee.raw() + ))); + }; + let ArrowBody::UserDefined(root) = body else { + return Err(LensApplyError::UnimplementedCallable(format!( + "`{name}` has no UserDefined body" + ))); + }; + if inputs.len() != arg_ports.len() { + return Err(LensApplyError::ArityMismatch { + expected: inputs.len(), + got: arg_ports.len(), + }); + } + let b = (*root).bind(self.dag); + if b.params.len() != arg_ports.len() { + return Err(LensApplyError::ArityMismatch { + expected: b.params.len(), + got: arg_ports.len(), + }); + } + self.push_frame(); + for (param, arg_port) in b.params.iter().zip(arg_ports.iter()) { + let v = self.eval_port(*arg_port)?; + self.bind_current(*param, v); + } + let out = self.eval_port(b.value); + self.pop_frame(); + out + } + + fn eval_fold_step( + &mut self, + list: FieldValue, + init: FieldValue, + step_bind: &BindNode, + ) -> Result { + let n = step_bind.params.len(); + if n < 2 { + return Err(LensApplyError::ArityMismatch { + expected: 2, + got: n, + }); + } + // Monomorphized `fold` lowers the `|acc, x|` lambda with possible leading + // synthesized parameters; the accumulator and element are always the last two + // formal parameter ports. + let acc_param = step_bind.params[n - 2]; + let elt_param = step_bind.params[n - 1]; + let mut acc = init; + for elt in list_elements(self.dag, &list)? { + self.push_frame(); + self.bind_current(acc_param, acc); + self.bind_current(elt_param, elt); + acc = self.eval_port(step_bind.value)?; + self.pop_frame(); + } + Ok(acc) + } + + fn eval_std_fold(&mut self, arg_ports: &[PortId]) -> Result { + if arg_ports.len() != 3 { + return Err(LensApplyError::ArityMismatch { + expected: 3, + got: arg_ports.len(), + }); + } + let list = self.eval_port(arg_ports[0])?; + let init = self.eval_port(arg_ports[1])?; + let step_bind_port = arg_ports[2]; + let step_producer = self + .dag + .resolve_producer_opt(&step_bind_port) + .ok_or(LensApplyError::UnresolvedPort)?; + let Behavior::Bind(step_bind) = step_producer else { + return Err(LensApplyError::UnsupportedConstruct( + "fold step is not a Bind", + )); + }; + let n = step_bind.params.len(); + if n < 2 { + return Err(LensApplyError::ArityMismatch { + expected: 2, + got: n, + }); + } + let acc_param = step_bind.params[n - 2]; + let elt_param = step_bind.params[n - 1]; + let mut acc = init; + for elt in list_elements(self.dag, &list)? { + self.push_frame(); + self.bind_current(acc_param, acc); + self.bind_current(elt_param, elt); + acc = self.eval_port(step_bind.value)?; + self.pop_frame(); + } + Ok(acc) + } + + fn eval_branch( + &mut self, + b: &BranchNode, + out_port: PortId, + ) -> Result { + let disc = self.eval_port(b.input)?; + for path in &b.paths { + let variant_id = match &path.pattern { + BranchPattern::ResolvedVariant(id) => *id, + BranchPattern::UnresolvedVariant { .. } => { + return Err(LensApplyError::UnresolvedBranchPattern); + } + }; + if !variant_matches(self.dag, &disc, variant_id)? { + continue; + } + self.push_frame(); + if let Some(binding) = &path.binding { + let payload = variant_payload_for_binding(self.dag, &disc, variant_id)?; + self.bind_current(binding.payload_port, payload); + } + let v = self.eval_port(path.output)?; + self.pop_frame(); + self.bind_current(out_port, v.clone()); + return Ok(v); + } + Err(LensApplyError::BranchMiss) + } + + /// Loop bodies are intentionally uninterpreted in D1. + /// + /// **Fail-closed receipt:** both `LoopBound::Cardinality` (single-fn recursion) + /// and `LoopBound::Descent` (mutual-recursion clusters) return + /// [`LensApplyError::UnimplementedLoopBound`] until iteration semantics land in + /// this interpreter. + fn eval_loop(&mut self, _l: &crate::dag::LoopNode) -> Result { + Err(LensApplyError::UnimplementedLoopBound) + } +} + +fn int_from_value(v: &FieldValue) -> Result { + match v { + FieldValue::Literal(LiteralBits::Int(s)) => s + .parse::() + .map_err(|_| LensApplyError::TypeMismatch("expected Int literal in i64 range")), + _ => Err(LensApplyError::TypeMismatch("expected Int literal")), + } +} + +/// Checked `Int` arithmetic for the D1 lens interpreter (INVARIANTS P3: no wrapping `+`/`-`/`*`/`/` +/// that could fabricate results or panic in debug under `LensOutputEquals` / gate paths). +fn apply_arithmetic_int( + op: crate::dag::ArithmeticOp, + a: i64, + b: i64, +) -> Result { + const OVERFLOW: LensApplyError = LensApplyError::UnsupportedConstruct("integer overflow"); + match op { + crate::dag::ArithmeticOp::Add => a.checked_add(b).ok_or(OVERFLOW), + crate::dag::ArithmeticOp::Sub => a.checked_sub(b).ok_or(OVERFLOW), + crate::dag::ArithmeticOp::Mul => a.checked_mul(b).ok_or(OVERFLOW), + crate::dag::ArithmeticOp::Div => { + if b == 0 { + return Err(LensApplyError::TypeMismatch("division by zero")); + } + a.checked_div(b).ok_or(OVERFLOW) + } + } +} + +fn project_field(base: &FieldValue, label: &str) -> Result { + let FieldValue::Record(fields) = base else { + return Err(LensApplyError::BadFieldProject); + }; + fields + .iter() + .find(|(l, _)| l == label) + .map(|(_, v)| v.clone()) + .ok_or(LensApplyError::BadFieldProject) +} + +fn variant_matches( + dag: &Dag, + value: &FieldValue, + variant_ty: DeclarationId, +) -> Result { + match value { + FieldValue::Variant { constructor, .. } => Ok(*constructor == variant_ty), + FieldValue::Literal(LiteralBits::Bool(b)) => { + let expected_ty = dag + .bool_runtime_variant_id(*b) + .ok_or(LensApplyError::MissingType("Bool variant"))?; + Ok(expected_ty == variant_ty) + } + _ => Ok(false), + } +} + +fn variant_payload_for_binding( + dag: &Dag, + value: &FieldValue, + variant_ty: DeclarationId, +) -> Result { + if let FieldValue::Literal(LiteralBits::Bool(_)) = value { + if !variant_matches(dag, value, variant_ty)? { + return Err(LensApplyError::BadFieldProject); + } + let conj = dag.declaration(variant_ty); + return match &conj.connective { + TypeConnective::Conj { children } if children.is_empty() => { + Ok(FieldValue::Record(vec![])) + } + TypeConnective::Conj { .. } => Err(LensApplyError::UnsupportedConstruct( + "Bool match arm with payload fields is not supported for Bool literal scrutinee", + )), + _ => Ok(FieldValue::Record(vec![])), + }; + } + let FieldValue::Variant { + constructor, + payload, + } = value + else { + return Err(LensApplyError::BadFieldProject); + }; + if *constructor != variant_ty { + return Err(LensApplyError::BadFieldProject); + } + if payload.len() == 1 { + return Ok(payload[0].clone()); + } + let conj = dag.declaration(variant_ty); + let TypeConnective::Conj { children } = &conj.connective else { + // Unit-like variant (e.g. `True` / `False` / `Empty`). + return Ok(FieldValue::Record(vec![])); + }; + if payload.len() != children.len() { + return Err(LensApplyError::BadFieldProject); + } + let fields: Vec<_> = children + .iter() + .zip(payload.iter()) + .map(|(c, v)| (c.label.clone(), v.clone())) + .collect(); + Ok(FieldValue::Record(fields)) +} + +/// Uncons a substrate `List` spine into owned elements (clones each head). +fn list_elements(dag: &Dag, list: &FieldValue) -> Result, LensApplyError> { + let mut out = Vec::new(); + let mut cur = list; + loop { + let FieldValue::Variant { + constructor, + payload, + } = cur + else { + return Err(LensApplyError::BadListShape); + }; + let label = variant_label(dag, *constructor).ok_or(LensApplyError::BadListShape)?; + match label.as_str() { + "Empty" => break, + "Cons" => { + if payload.len() != 2 { + return Err(LensApplyError::BadListShape); + } + out.push(payload[0].clone()); + cur = &payload[1]; + } + _ => return Err(LensApplyError::BadListShape), + } + } + Ok(out) +} + +// perf: linear scan over all declarations per variant id — fine for R1 gate DAGs; +// revisit if lenses iterate over large programs (prefer declaration-indexed lookup). +fn variant_label(dag: &Dag, variant_id: DeclarationId) -> Option { + dag.declarations() + .iter() + .find_map(|decl| match &decl.connective { + TypeConnective::Disj { variants } => variants + .iter() + .find(|variant| variant.ty == variant_id) + .map(|variant| variant.label.clone()), + _ => None, + }) +} + +// perf: linear scan for `List` disj — same bounded-gate rationale as `variant_label`. +fn v3_list_empty_cons_ids(dag: &Dag) -> Result<(DeclarationId, DeclarationId), LensApplyError> { + let list_decl = dag.declarations().iter().find(|d| { + d.name.as_deref() == Some("List") && matches!(d.connective, TypeConnective::Disj { .. }) + }); + let Some(list_decl) = list_decl else { + return Err(LensApplyError::MissingType("List")); + }; + let TypeConnective::Disj { variants } = &list_decl.connective else { + return Err(LensApplyError::MissingType("List")); + }; + let empty = variants + .iter() + .find(|v| v.label == "Empty") + .ok_or(LensApplyError::MissingType("List.Empty"))? + .ty; + let cons = variants + .iter() + .find(|v| v.label == "Cons") + .ok_or(LensApplyError::MissingType("List.Cons"))? + .ty; + Ok((empty, cons)) +} + +/// Fixed `(a, b, c)` triples for R1 `AlgebraicLaw(Associativity, …)` operational checks. +/// +/// A **single** triple can pass for operations that are not associative (coincidence). The +/// runner requires **every** triple here to satisfy [`int_associativity_holds`] — still not a +/// quantified substrate law proof, but a materially stronger witness than one sample. Magnitudes +/// stay modest so `Int` addition-style lenses do not overflow during witness application. +pub const ASSOCIATIVITY_WITNESS_TRIPLES: &[(i64, i64, i64)] = &[ + (2, 3, 5), + (0, 1, 99), + (-3, 7, 2), + (-1, 0, 1), + (1, 1, 2), + (5, 0, 3), + (10, -4, 7), + (100, 200, 300), +]; + +/// Fixed `(a, b)` pairs for PR-B.3 `AlgebraicLaw(Commutativity, …)` operational checks. +/// +/// These are runner witnesses, not substrate law proofs. The sample table lives next to the +/// associativity table so law-runner scaffolds share one bounded Int witness authority until +/// first-class substrate law witnesses dissolve the runner-side sample checks. +pub const COMMUTATIVITY_WITNESS_PAIRS: &[(i64, i64)] = &[ + (2, 3), + (0, 99), + (-3, 7), + (-1, 0), + (1, 1), + (5, 0), + (10, -4), + (100, 200), +]; + +/// Samples for `AlgebraicLaw(Identity, …)` operational checks: **every** value must appear in at +/// least one [`COMMUTATIVITY_WITNESS_PAIRS`] coordinate so identity witnessing stays materially +/// aligned with the commutativity witness authority (still not a substrate law proof). +pub const IDENTITY_WITNESS_SAMPLES: &[i64] = &[-4, -3, -1, 0, 1, 2, 3, 5, 7, 10, 99, 100, 200]; + +/// Candidate identity elements searched left-to-right; exactly one must satisfy +/// `e ⊕ a = a` and `a ⊕ e = a` for every sample in [`IDENTITY_WITNESS_SAMPLES`]. Multiple matches +/// fail closed (`Ok(false)`) so incidental finite-table coincidences cannot certify an ambiguous op. +/// +/// **High-end redundancy:** keep **at least two** entries **strictly greater** than +/// `max(IDENTITY_WITNESS_SAMPLES)` (currently `200`). With only one such candidate, a binary `Int` +/// lens behaving like `min` can satisfy the finite witness with a unique “top-like” constant even +/// though `Int` has no lattice top (INVARIANTS P1/P3; codex PR #2394). Several candidates also sit +/// below `min(samples)` so `max`-like ops typically hit the same multiplicity fail-closure. +pub const IDENTITY_WITNESS_CANDIDATES: &[i64] = &[ + -300, -200, -100, -99, -10, -7, -5, -4, -3, -2, -1, 0, 1, 2, 3, 5, 7, 10, 99, 100, 200, 250, + 300, 400, +]; + +/// Evaluate `(a ⊕ b) ⊕ c` vs `a ⊕ (b ⊕ c)` for a binary `Int` lens using [`apply_lens_declaration`]. +pub fn int_associativity_holds( + program_dag: &Dag, + lens_decl_id: DeclarationId, + a: i64, + b: i64, + c: i64, +) -> Result { + let int = |n: i64| FieldValue::Literal(literal_bits_int(n)); + let left_ab = apply_lens_declaration(program_dag, None, lens_decl_id, &[int(a), int(b)])?; + let left = apply_lens_declaration(program_dag, None, lens_decl_id, &[left_ab, int(c)])?; + let right_bc = apply_lens_declaration(program_dag, None, lens_decl_id, &[int(b), int(c)])?; + let right = apply_lens_declaration(program_dag, None, lens_decl_id, &[int(a), right_bc])?; + Ok(left == right) +} + +/// True iff [`int_associativity_holds`] succeeds for every triple in `triples`. +pub fn int_associativity_holds_all_triples( + program_dag: &Dag, + lens_decl_id: DeclarationId, + triples: &[(i64, i64, i64)], +) -> Result { + for &(a, b, c) in triples { + if !int_associativity_holds(program_dag, lens_decl_id, a, b, c)? { + return Ok(false); + } + } + Ok(true) +} + +/// True iff exactly one `e` in `candidates` satisfies left/right identity against every `samples` +/// entry via [`apply_lens_declaration`]. +pub fn int_identity_witness_holds( + program_dag: &Dag, + lens_decl_id: DeclarationId, + samples: &[i64], + candidates: &[i64], +) -> Result { + let int = |n: i64| FieldValue::Literal(literal_bits_int(n)); + let mut matching = 0_i32; + for &e in candidates { + let ev = int(e); + let mut ok = true; + for &a in samples { + let left = + apply_lens_declaration(program_dag, None, lens_decl_id, &[ev.clone(), int(a)])?; + let right = + apply_lens_declaration(program_dag, None, lens_decl_id, &[int(a), ev.clone()])?; + if left != int(a) || right != int(a) { + ok = false; + break; + } + } + if ok { + matching += 1; + if matching > 1 { + return Ok(false); + } + } + } + Ok(matching == 1) +} + +const _: () = assert!(IDENTITY_WITNESS_SAMPLES.len() > 1); +const _: () = assert!(IDENTITY_WITNESS_CANDIDATES.len() > 1); + +#[cfg(test)] +mod tests { + use super::*; + use crate::compile_to_dag; + use crate::dag::{Behavior, LoopBound}; + + #[test] + fn named_function_count_on_trivial_program() { + let src = include_str!("../../lenses/named_function_count.dag"); + let lens_dag = + compile_to_dag(src, "src/v3/lenses/named_function_count.dag").expect("lens compiles"); + let prog = compile_to_dag("let x: Int = 1", "lens_apply_prog.v3").expect("prog compiles"); + let lens_id = lens_dag + .declaration_by_name("named_function_count") + .expect("named_function_count") + .id; + let input = reflect_program_dag_nodes_in_file(&prog, "lens_apply_prog.v3", &lens_dag) + .expect("reflect"); + let out = apply_lens_declaration(&lens_dag, None, lens_id, &[input]).expect("apply"); + assert_eq!(out, FieldValue::Literal(literal_bits_int(1))); + } + + #[test] + fn identity_witness_samples_align_with_commutativity_witness_authority() { + for &s in IDENTITY_WITNESS_SAMPLES { + assert!( + COMMUTATIVITY_WITNESS_PAIRS + .iter() + .any(|&(a, b)| a == s || b == s), + "every IDENTITY_WITNESS_SAMPLES entry must appear in COMMUTATIVITY_WITNESS_PAIRS \ + (doc invariant on IDENTITY_WITNESS_SAMPLES); missing sample {s}" + ); + } + } + + #[test] + fn identity_witness_candidate_table_has_redundant_high_and_low_extents() { + let sample_min = *IDENTITY_WITNESS_SAMPLES.iter().min().unwrap(); + let sample_max = *IDENTITY_WITNESS_SAMPLES.iter().max().unwrap(); + let highs: Vec = IDENTITY_WITNESS_CANDIDATES + .iter() + .copied() + .filter(|&c| c > sample_max) + .collect(); + let lows: Vec = IDENTITY_WITNESS_CANDIDATES + .iter() + .copied() + .filter(|&c| c < sample_min) + .collect(); + assert!( + highs.len() >= 2, + "need ≥2 candidates strictly above sample max ({sample_max}) so min-like lenses cannot \ + mint a unique false identity; got {highs:?}" + ); + assert!( + lows.len() >= 2, + "need ≥2 candidates strictly below sample min ({sample_min}) for symmetric fail-closure \ + skew; got {lows:?}" + ); + } + + #[test] + fn int_add_lens_associativity_sample() { + let src = r#"module w +fn lens_composition_op(a: Int, b: Int) -> Int = a + b +"#; + let dag = compile_to_dag(src, "assoc.v3").expect("compiles"); + let id = dag.declaration_by_name("lens_composition_op").unwrap().id; + assert!( + int_associativity_holds_all_triples(&dag, id, ASSOCIATIVITY_WITNESS_TRIPLES) + .expect("assoc witness"), + "Int `+` lens must pass every ASSOCIATIVITY_WITNESS_TRIPLES entry" + ); + } + + #[test] + fn literals_only_evaluator_bridge_agrees_with_eval_ctx_for_int_add() { + let src = r#"module w +fn lens_composition_op(a: Int, b: Int) -> Int = a + b +"#; + let dag = compile_to_dag(src, "eval_bridge_vs_ctx.v3").expect("compiles"); + let lens_decl_id = dag.declaration_by_name("lens_composition_op").unwrap().id; + let decl = dag.declaration(lens_decl_id); + let TypeConnective::Arrow { body, .. } = &decl.connective else { + panic!("expected arrow lens"); + }; + let ArrowBody::UserDefined(root) = body else { + panic!("expected user-defined arrow body"); + }; + let root_bind = (*root).bind(&dag); + let inputs = &[ + FieldValue::Literal(literal_bits_int(11)), + FieldValue::Literal(literal_bits_int(-3)), + ]; + let via_bridge = super::try_apply_lens_via_evaluator_literals_only(&dag, root_bind, inputs) + .expect("evaluator bridge lookup") + .expect("literal Int add must succeed on evaluator bridge"); + let mut ctx = EvalCtx::new(&dag); + for (port, arg) in root_bind.params.iter().zip(inputs.iter()) { + ctx.bind_top(*port, arg.clone()); + } + let via_ctx = ctx.eval_port(root_bind.value).expect("eval ctx"); + assert_eq!(via_bridge, via_ctx); + } + + #[test] + fn literals_only_with_program_under_test_skips_bridge_matches_bare_apply() { + let lens_src = r#"module w +fn lens_composition_op(a: Int, b: Int) -> Int = a + b +"#; + let lens_dag = compile_to_dag(lens_src, "reflect_skip_bridge_lens.v3").expect("compiles"); + let prog_under_test = + compile_to_dag("let x: Int = 1", "reflect_skip_bridge_prog.v3").expect("prog compiles"); + let id = lens_dag + .declaration_by_name("lens_composition_op") + .unwrap() + .id; + let inputs = &[ + FieldValue::Literal(literal_bits_int(5)), + FieldValue::Literal(literal_bits_int(7)), + ]; + let bare = apply_lens_declaration(&lens_dag, None, id, inputs).expect("bare apply"); + let with_put = + apply_lens_declaration(&lens_dag, Some(&prog_under_test), id, inputs).expect("put"); + assert_eq!(bare, with_put); + } + + #[test] + fn int_add_lens_identity_witness_finds_zero_uniquely() { + let src = r#"module w +fn lens_composition_op(a: Int, b: Int) -> Int = a + b +"#; + let dag = compile_to_dag(src, "id_add.v3").expect("compiles"); + let id = dag.declaration_by_name("lens_composition_op").unwrap().id; + assert!( + super::int_identity_witness_holds( + &dag, + id, + IDENTITY_WITNESS_SAMPLES, + IDENTITY_WITNESS_CANDIDATES + ) + .expect("identity witness"), + "Int `+` lens identity must be uniquely 0 on the bounded witness tables" + ); + } + + #[test] + fn int_mul_lens_identity_witness_finds_one_uniquely() { + let src = r#"module w +fn lens_mul_op(a: Int, b: Int) -> Int = a * b +"#; + let dag = compile_to_dag(src, "id_mul.v3").expect("compiles"); + let id = dag.declaration_by_name("lens_mul_op").unwrap().id; + assert!( + super::int_identity_witness_holds( + &dag, + id, + IDENTITY_WITNESS_SAMPLES, + IDENTITY_WITNESS_CANDIDATES + ) + .expect("identity witness"), + "Int `*` lens identity must be uniquely 1 on the bounded witness tables" + ); + } + + #[test] + fn int_min_lens_identity_witness_fails_closed_on_ambiguous_top_candidates() { + let src = r#"module w +fn lens_min_op(a: Int, b: Int) -> Int = if a < b then a else b +"#; + let dag = compile_to_dag(src, "id_min.v3").expect("compiles"); + let id = dag.declaration_by_name("lens_min_op").unwrap().id; + assert!( + !super::int_identity_witness_holds( + &dag, + id, + IDENTITY_WITNESS_SAMPLES, + IDENTITY_WITNESS_CANDIDATES + ) + .expect("identity witness"), + "binary Int min has no identity on bounded samples; bounded witness must fail closed" + ); + } + + #[test] + fn int_add_overflow_returns_lens_apply_error_not_wrapped_value() { + let src = r#"module m +fn f(a: Int, b: Int) -> Int = a + b +"#; + let dag = compile_to_dag(src, "int_ovf.v3").expect("compiles"); + let id = dag.declaration_by_name("f").unwrap().id; + let err = apply_lens_declaration( + &dag, + None, + id, + &[ + FieldValue::Literal(literal_bits_int(i64::MAX)), + FieldValue::Literal(literal_bits_int(1)), + ], + ) + .expect_err("overflow must not yield a wrapped Int"); + assert!( + matches!( + err, + super::LensApplyError::UnsupportedConstruct("integer overflow") + ), + "{err:?}" + ); + } + + #[test] + fn empty_fold_returns_init() { + let src = r#"module m +import std.list { List, fold } + +fn sum(xs: List) -> Int = + fold(xs, 99, |acc, x| acc + x) +"#; + let dag = compile_to_dag(src, "empty_fold.v3").expect("compiles"); + let sum_id = dag.declaration_by_name("sum").expect("sum").id; + let empty = empty_substrate_list_value(&dag).expect("empty list"); + let out = apply_lens_declaration(&dag, None, sum_id, &[empty]).expect("fold empty"); + assert_eq!(out, FieldValue::Literal(literal_bits_int(99))); + } + + #[test] + fn monomorphized_fold_step_bind_recovered_via_instantiation_arguments() { + let src = r#"module m +import std.list { List, fold } + +fn sum(xs: List) -> Int = + fold(xs, 99, |acc, x| acc + x) +"#; + let dag = compile_to_dag(src, "mono_fold_bind_paths.v3").expect("compiles"); + let fold_transform = dag + .nodes() + .iter() + .find_map(|n| { + let Behavior::Transform(t) = n else { + return None; + }; + let TransformTarget::Callable(callee) = &t.target else { + return None; + }; + super::is_fold_instantiation(&dag, dag.declaration(*callee)).then_some(t) + }) + .expect("monomorphized fold transform"); + let TransformTarget::Callable(callee) = &fold_transform.target else { + unreachable!(); + }; + super::find_fold_step_bind_via_instantiation(&dag, *callee).expect("fold step bind"); + } + + #[test] + fn fold_step_lookup_requires_template_formal_edge() { + let src = r#"module m +import std.list { List, fold } + +fn sum(xs: List) -> Int = + fold(xs, 99, |acc, x| acc + x) +"#; + let mut dag = compile_to_dag(src, "mono_fold_bind_no_formal_edge.v3").expect("compiles"); + let fold_callable = dag + .nodes() + .iter() + .find_map(|n| { + let Behavior::Transform(t) = n else { + return None; + }; + let TransformTarget::Callable(callee) = &t.target else { + return None; + }; + super::is_fold_instantiation(&dag, dag.declaration(*callee)).then_some(*callee) + }) + .expect("monomorphized fold transform"); + let TypeConnective::Instantiation { + template, + arguments, + } = &dag.declaration(fold_callable).connective + else { + panic!("fold callable should be an Instantiation"); + }; + let formals = super::fold_template_callable_formals(&dag, *template); + let depth_budget = LENS_APPLY_TYPE_WALK_DEPTH as i64; + let callable_arg_index = arguments + .iter() + .position(|arg| { + let resolved = resolve_template_argument_value(&depth_budget, arguments, arg.value); + super::monomorph_callable_bind_root(&dag, resolved).is_some() + && formals.contains(&arg.parameter) + }) + .expect("callable fold argument"); + let non_formal_parameter = arguments + .iter() + .map(|arg| arg.parameter) + .find(|parameter| !formals.contains(parameter)) + .expect("non-callable fold formal parameter"); + + let TypeConnective::Instantiation { arguments, .. } = + &mut dag.declaration_mut(fold_callable).connective + else { + panic!("fold callable should remain an Instantiation"); + }; + arguments[callable_arg_index].parameter = non_formal_parameter; + + assert!( + super::find_fold_step_bind_via_instantiation(&dag, fold_callable).is_none(), + "unique callable candidates must not be accepted without the template-formal edge" + ); + } + + #[test] + fn comparison_operator_returns_bool_literal_not_sum_variant() { + let src = r#"module m +fn truth() -> Bool = 1 == 1 +"#; + let dag = compile_to_dag(src, "truth.v3").expect("compiles"); + let id = dag.declaration_by_name("truth").unwrap().id; + let out = apply_lens_declaration(&dag, None, id, &[]).expect("apply"); + assert_eq!(out, FieldValue::Literal(LiteralBits::Bool(true))); + } + + #[test] + fn bool_literal_scrutinee_selects_disj_match_arm() { + let src = r#"module m +fn p(b: Bool) -> Int = match b { True => 1, False => 0 } +"#; + let dag = compile_to_dag(src, "bool_lit_match.v3").expect("compiles"); + let id = dag.declaration_by_name("p").unwrap().id; + let out = apply_lens_declaration( + &dag, + None, + id, + &[FieldValue::Literal(LiteralBits::Bool(true))], + ) + .expect("apply"); + assert_eq!(out, FieldValue::Literal(literal_bits_int(1))); + } + + #[test] + fn mutual_recursion_lens_hits_unimplemented_loop_for_descent_cluster() { + let src = r#"module m +fn even(n: Int) -> Bool = if n == 0 then true else odd(n - 1) +fn odd(n: Int) -> Bool = if n == 0 then false else even(n - 1) +fn run_even(n: Int) -> Bool = even(n) +"#; + let dag = compile_to_dag(src, "mutual_lens.v3").expect("compiles"); + assert!( + dag.nodes() + .iter() + .filter_map(Behavior::as_loop) + .any(|l| { matches!(l.bound, LoopBound::Descent { .. }) }), + "expected at least one LoopBound::Descent (mutual cluster)" + ); + let run_even = dag.declaration_by_name("run_even").expect("run_even").id; + let err = apply_lens_declaration( + &dag, + None, + run_even, + &[FieldValue::Literal(literal_bits_int(1))], + ) + .expect_err("loop interpretation is unimplemented"); + assert!( + matches!(err, LensApplyError::UnimplementedLoopBound), + "expected UnimplementedLoopBound, got {err:?}" + ); + } + + #[test] + fn apply_lens_declaration_arity_mismatch() { + let src = r#"module m +fn f(a: Int, b: Int) -> Int = a + b +"#; + let dag = compile_to_dag(src, "arity.v3").expect("compiles"); + let id = dag.declaration_by_name("f").unwrap().id; + let err = + apply_lens_declaration(&dag, None, id, &[FieldValue::Literal(literal_bits_int(1))]) + .expect_err("wrong arity"); + assert!( + matches!( + err, + LensApplyError::ArityMismatch { + expected: 2, + got: 1 + } + ), + "{err:?}" + ); + } + + #[test] + fn eligible_fold_runs_through_bounded_path() { + // Step body is `acc + x` — no Loop, no unsupported construct in transitive call graph. + // The structural eligibility predicate must approve and the bounded fast path must + // produce the sum. + let src = r#"module m +import std.list { List, fold } + +fn sum(xs: List) -> Int = + fold(xs, 0, |acc, x| acc + x) +"#; + let dag = compile_to_dag(src, "eligible_fold.v3").expect("compiles"); + let sum_id = dag.declaration_by_name("sum").expect("sum").id; + let (empty_id, cons_id) = super::v3_list_empty_cons_ids(&dag).expect("list ids"); + let int = |n: i64| FieldValue::Literal(literal_bits_int(n)); + let mut list = FieldValue::Variant { + constructor: empty_id, + payload: vec![], + }; + for n in [3i64, 2, 1] { + list = FieldValue::Variant { + constructor: cons_id, + payload: vec![int(n), list], + }; + } + let out = apply_lens_declaration(&dag, None, sum_id, &[list]).expect("eligible fold runs"); + assert_eq!(out, int(6)); + } + + #[test] + fn ineligible_fold_with_recursive_step_skips_bounded_path() { + // `helper` is self-recursive → lowers to a `LoopNode` in the step body's transitive + // call graph. The structural eligibility predicate must reject; the bounded fast path + // is bypassed; `eval_callable` rejects the monomorphized `Instantiation { template: + // fold }` with `UnimplementedCallable` (fail-closed, vs running and hitting + // `UnimplementedLoopBound` deeper in). + let src = r#"module m +import std.list { List, fold } + +fn helper(n: Int) -> Int = if n == 0 then 0 else helper(n - 1) + 1 +fn sum(xs: List) -> Int = + fold(xs, 0, |acc, x| acc + helper(x)) +"#; + let dag = compile_to_dag(src, "ineligible_fold.v3").expect("compiles"); + let sum_id = dag.declaration_by_name("sum").expect("sum").id; + let (_, cons_id) = super::v3_list_empty_cons_ids(&dag).expect("list ids"); + let empty = empty_substrate_list_value(&dag).expect("empty list"); + let one = FieldValue::Variant { + constructor: cons_id, + payload: vec![FieldValue::Literal(literal_bits_int(1)), empty], + }; + let err = + apply_lens_declaration(&dag, None, sum_id, &[one]).expect_err("ineligible step body"); + assert!( + matches!(err, LensApplyError::UnimplementedCallable(_)), + "{err:?}" + ); + } + + #[test] + fn branch_miss_when_scrutinee_matches_no_arm() { + let src = r#"module m +fn p(b: Bool) -> Int = match b { True => 1, False => 0 } +"#; + let dag = compile_to_dag(src, "branch_miss.v3").expect("compiles"); + let id = dag.declaration_by_name("p").unwrap().id; + let err = + apply_lens_declaration(&dag, None, id, &[FieldValue::Literal(literal_bits_int(42))]) + .expect_err("Int is not True/False"); + assert!(matches!(err, LensApplyError::BranchMiss), "{err:?}"); + } +} + +// Substrate ↔ `FieldValue` structural reflection (design-reflection-completeness). +// SG-0: folded under this file — the census forbids a new sibling `substrate_reflection.rs`. +mod substrate_reflection { + //! Complete structural reflection of computation-substrate [`Behavior`] nodes into + //! lens-input [`FieldValue`] per `docs/design-reflection-completeness.md` (LOCKED). + //! Reflection is static: no execution, no branch-arm selection, no loop iteration. + + use crate::dag::{ + AtomPayload, Behavior, BindEmitParticipation, BindNode, BoolPortRef, BranchArm, + BranchEmitParticipation, BranchNode, BranchPattern, BreakingShape, CardinalityBound, + ClusterId, CreateCause, Dag, DeclarationId, EffectShape, FieldValue, HttpMethodScalar, + IdempotentShape, KeySource, LiteralBits, LoopBound, LoopNode, NodeId, NonSingletonList, + OperationEffect, OperatorKind, Path, PayloadBinding, PortId, TransformNode, + TransformTarget, TypeConnective, ValueNode, WorkflowEffect, + }; + use crate::diagnostics::SourceSpan; + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct ReflectError(pub &'static str); + + pub type ReflectResult = Result; + + fn err(msg: &'static str) -> ReflectResult { + Err(ReflectError(msg)) + } + + fn disj_variant_ty( + dag: &Dag, + sum_name: &str, + variant_label: &str, + ) -> ReflectResult { + let mut decl_id = dag + .declaration_by_name(sum_name) + .ok_or(ReflectError("missing sum type"))? + .id; + const PEEL_MAX: usize = 64; + for _ in 0..PEEL_MAX { + let decl = dag.declaration(decl_id); + match &decl.connective { + TypeConnective::Instantiation { + template, + arguments, + } if arguments.is_empty() => { + decl_id = *template; + } + TypeConnective::Disj { variants } => { + return variants + .iter() + .find(|v| v.label == variant_label) + .map(|v| v.ty) + .ok_or(ReflectError("missing sum variant")); + } + TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(name)) + if name == variant_label => + { + // Single-variant enum: `Instantiation → Atom(Label)` (no `Disj` row). + return Ok(decl_id); + } + _ => return err("sum type is not Disj or unit Atom"), + } + } + err("Instantiation peel depth exceeded") + } + + fn port_fv(p: PortId) -> FieldValue { + FieldValue::Literal(LiteralBits::Int(i64::from(p.raw()).to_string())) + } + + fn node_fv(n: NodeId) -> FieldValue { + FieldValue::Literal(LiteralBits::Int(i64::from(n.raw()).to_string())) + } + + fn cluster_fv(c: ClusterId) -> FieldValue { + FieldValue::Literal(LiteralBits::Int(i64::from(c.raw()).to_string())) + } + + fn reflect_source_span(span: &SourceSpan) -> FieldValue { + // Field names must match `type SourceSpan` in `dsl/std/types.dag` (`file`, `start`, `end`). + // The Rust carrier still uses `byte_start` / `byte_end` on [`SourceSpan`]. + FieldValue::Record(vec![ + ( + "file".to_string(), + FieldValue::Literal(LiteralBits::String(span.file.clone())), + ), + ( + "start".to_string(), + FieldValue::Literal(LiteralBits::Int(i64::from(span.byte_start).to_string())), + ), + ( + "end".to_string(), + FieldValue::Literal(LiteralBits::Int(i64::from(span.byte_end).to_string())), + ), + ]) + } + + fn v3_list_empty_cons_ids(dag: &Dag) -> ReflectResult<(DeclarationId, DeclarationId)> { + let list_decl = dag.declarations().iter().find(|d| { + d.name.as_deref() == Some("List") && matches!(d.connective, TypeConnective::Disj { .. }) + }); + let Some(list_decl) = list_decl else { + return err("List"); + }; + let TypeConnective::Disj { variants } = &list_decl.connective else { + return err("List"); + }; + let empty = variants + .iter() + .find(|v| v.label == "Empty") + .ok_or(ReflectError("List.Empty"))? + .ty; + let cons = variants + .iter() + .find(|v| v.label == "Cons") + .ok_or(ReflectError("List.Cons"))? + .ty; + Ok((empty, cons)) + } + + fn named_record_type_root(dag: &Dag, name: &str) -> ReflectResult { + let mut decl_id = dag + .declaration_by_name(name) + .ok_or(ReflectError("missing substrate record type"))? + .id; + const PEEL_MAX: usize = 64; + for _ in 0..PEEL_MAX { + match &dag.declaration(decl_id).connective { + TypeConnective::Conj { .. } => return Ok(decl_id), + TypeConnective::Instantiation { + template, + arguments, + } if arguments.is_empty() => { + decl_id = *template; + } + _ => return err("substrate record type is not Conj"), + } + } + err("substrate record peel depth exceeded") + } + + fn conj_field_ty( + dag: &Dag, + conj_decl_id: DeclarationId, + label: &str, + ) -> ReflectResult { + let decl = dag.declaration(conj_decl_id); + let TypeConnective::Conj { children } = &decl.connective else { + return err("expected Conj"); + }; + children + .iter() + .find(|c| c.label == label) + .map(|c| c.ty) + .ok_or(ReflectError("missing Conj field")) + } + + /// [`FieldValue::Variant::payload`] is **positional**: one entry per field of the + /// variant payload [`TypeConnective::Conj`], in `children` order — same contract as + /// `variant_payload_for_binding` in this file. + fn sum_variant_payload( + dag: &Dag, + sum_name: &str, + variant_label: &str, + payload: Vec, + ) -> ReflectResult { + let ctor_ty = disj_variant_ty(dag, sum_name, variant_label)?; + match &dag.declaration(ctor_ty).connective { + TypeConnective::Conj { children } => { + if children.len() != payload.len() { + return err("sum variant payload arity mismatch"); + } + } + _ => { + if payload.len() > 1 { + return err("sum variant payload arity mismatch"); + } + } + } + Ok(FieldValue::Variant { + constructor: ctor_ty, + payload, + }) + } + + fn peel_to_optional_cardinality_decl( + dag: &Dag, + mut ty: DeclarationId, + ) -> ReflectResult { + const PEEL_MAX: usize = 64; + for _ in 0..PEEL_MAX { + match &dag.declaration(ty).connective { + TypeConnective::Cardinality(p) if p.bound() == CardinalityBound::AtMostOne => { + return Ok(ty); + } + TypeConnective::Instantiation { template, .. } => ty = *template, + TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) + | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => ty = *next, + _ => return err("expected optional T? cardinality"), + } + } + err("optional cardinality peel depth exceeded") + } + + fn optional_some_none_constructor_ids( + dag: &Dag, + cardinality_decl_id: DeclarationId, + ) -> ReflectResult<(DeclarationId, DeclarationId)> { + let disj_id = dag + .optional_match_disj(cardinality_decl_id) + .ok_or(ReflectError("missing optional Some/None sum"))?; + let decl = dag.declaration(disj_id); + let TypeConnective::Disj { variants } = &decl.connective else { + return err("optional match row is not Disj"); + }; + let some_ty = variants + .iter() + .find(|v| v.label == "Some") + .ok_or(ReflectError("Some"))? + .ty; + let none_ty = variants + .iter() + .find(|v| v.label == "None") + .ok_or(ReflectError("None"))? + .ty; + Ok((some_ty, none_ty)) + } + + /// Optional `T?` using the same `Some` / `None` sum as inference (`Cardinality` → match disj). + fn reflect_optional_sum( + dag: &Dag, + cardinality_decl_id: DeclarationId, + opt: Option, + mut reflect_some: impl FnMut(&Dag, T) -> ReflectResult, + ) -> ReflectResult { + let (some_ty, none_ty) = optional_some_none_constructor_ids(dag, cardinality_decl_id)?; + Ok(match opt { + None => FieldValue::Variant { + constructor: none_ty, + payload: vec![], + }, + Some(x) => FieldValue::Variant { + constructor: some_ty, + payload: vec![reflect_some(dag, x)?], + }, + }) + } + + fn reflect_port_id_list(dag: &Dag, ports: &[PortId]) -> ReflectResult { + let (empty_id, cons_id) = v3_list_empty_cons_ids(dag)?; + let mut tail = FieldValue::Variant { + constructor: empty_id, + payload: vec![], + }; + for p in ports.iter().rev() { + tail = FieldValue::Variant { + constructor: cons_id, + payload: vec![port_fv(*p), tail], + }; + } + Ok(tail) + } + + fn reflect_string_list_spine(dag: &Dag, strings: &[String]) -> ReflectResult { + let (empty_id, cons_id) = v3_list_empty_cons_ids(dag)?; + let mut tail = FieldValue::Variant { + constructor: empty_id, + payload: vec![], + }; + for s in strings.iter().rev() { + tail = FieldValue::Variant { + constructor: cons_id, + payload: vec![FieldValue::Literal(LiteralBits::String(s.clone())), tail], + }; + } + Ok(tail) + } + + fn reflect_unit_variant(dag: &Dag, sum_name: &str, label: &str) -> ReflectResult { + sum_variant_payload(dag, sum_name, label, vec![]) + } + + fn reflect_arithmetic_op(dag: &Dag, op: crate::dag::ArithmeticOp) -> ReflectResult { + let label = match op { + crate::dag::ArithmeticOp::Add => "Add", + crate::dag::ArithmeticOp::Sub => "Sub", + crate::dag::ArithmeticOp::Mul => "Mul", + crate::dag::ArithmeticOp::Div => "Div", + }; + reflect_unit_variant(dag, "ArithmeticOp", label) + } + + fn reflect_comparison_op(dag: &Dag, op: crate::dag::ComparisonOp) -> ReflectResult { + let label = match op { + crate::dag::ComparisonOp::Eq => "Eq", + crate::dag::ComparisonOp::Ne => "Ne", + crate::dag::ComparisonOp::Lt => "Lt", + crate::dag::ComparisonOp::Le => "Le", + crate::dag::ComparisonOp::Gt => "Gt", + crate::dag::ComparisonOp::Ge => "Ge", + }; + reflect_unit_variant(dag, "ComparisonOp", label) + } + + fn reflect_logical_op(dag: &Dag, op: crate::dag::LogicalOp) -> ReflectResult { + let label = match op { + crate::dag::LogicalOp::And => "And", + crate::dag::LogicalOp::Or => "Or", + }; + reflect_unit_variant(dag, "LogicalOp", label) + } + + fn reflect_operator_kind(dag: &Dag, k: &OperatorKind) -> ReflectResult { + match k { + OperatorKind::Arithmetic(op) => { + let id = disj_variant_ty(dag, "OperatorKind", "Arithmetic")?; + Ok(FieldValue::Variant { + constructor: id, + payload: vec![reflect_arithmetic_op(dag, *op)?], + }) + } + OperatorKind::Comparison(op) => { + let id = disj_variant_ty(dag, "OperatorKind", "Comparison")?; + Ok(FieldValue::Variant { + constructor: id, + payload: vec![reflect_comparison_op(dag, *op)?], + }) + } + OperatorKind::Logical(op) => { + let id = disj_variant_ty(dag, "OperatorKind", "Logical")?; + Ok(FieldValue::Variant { + constructor: id, + payload: vec![reflect_logical_op(dag, *op)?], + }) + } + } + } + + fn reflect_http_method_scalar(dag: &Dag, m: HttpMethodScalar) -> ReflectResult { + let label = match m { + HttpMethodScalar::Get => "GET", + HttpMethodScalar::Post => "POST", + HttpMethodScalar::Put => "PUT", + HttpMethodScalar::Patch => "PATCH", + HttpMethodScalar::Delete => "DELETE", + HttpMethodScalar::Head => "HEAD", + HttpMethodScalar::Options => "OPTIONS", + }; + reflect_unit_variant(dag, "HttpMethod", label) + } + + fn reflect_create_cause(dag: &Dag, c: &CreateCause) -> ReflectResult { + match c { + CreateCause::PostAlways => { + sum_variant_payload(dag, "CreateCause", "PostAlways", vec![]) + } + CreateCause::KeylessFallback { method } => sum_variant_payload( + dag, + "CreateCause", + "KeylessFallback", + vec![reflect_http_method_scalar(dag, *method)?], + ), + } + } + + fn reflect_key_source(dag: &Dag, ks: &KeySource) -> ReflectResult { + match ks { + KeySource::PathParam { param } => sum_variant_payload( + dag, + "KeySource", + "PathParam", + vec![FieldValue::Literal(LiteralBits::String(param.clone()))], + ), + KeySource::InputField { field } => sum_variant_payload( + dag, + "KeySource", + "InputField", + vec![FieldValue::Literal(LiteralBits::String(field.clone()))], + ), + KeySource::CompositeKey { fields } => sum_variant_payload( + dag, + "KeySource", + "CompositeKey", + vec![reflect_string_list_spine(dag, fields)?], + ), + } + } + + fn reflect_idempotent_shape(dag: &Dag, s: &IdempotentShape) -> ReflectResult { + match s { + IdempotentShape::ReadEffect => { + sum_variant_payload(dag, "IdempotentShape", "ReadEffect", vec![]) + } + IdempotentShape::UpsertEffect { key_source } => sum_variant_payload( + dag, + "IdempotentShape", + "UpsertEffect", + vec![reflect_key_source(dag, key_source)?], + ), + IdempotentShape::DeleteEffect { key_source } => sum_variant_payload( + dag, + "IdempotentShape", + "DeleteEffect", + vec![reflect_key_source(dag, key_source)?], + ), + } + } + + fn reflect_breaking_shape(dag: &Dag, s: &BreakingShape) -> ReflectResult { + match s { + BreakingShape::CreateEffect { cause } => sum_variant_payload( + dag, + "BreakingShape", + "CreateEffect", + vec![reflect_create_cause(dag, cause)?], + ), + BreakingShape::AppendEffect => { + sum_variant_payload(dag, "BreakingShape", "AppendEffect", vec![]) + } + } + } + + fn reflect_effect_shape(dag: &Dag, s: &EffectShape) -> ReflectResult { + match s { + EffectShape::IsIdempotent(inner) => sum_variant_payload( + dag, + "EffectShape", + "IsIdempotent", + vec![reflect_idempotent_shape(dag, inner)?], + ), + EffectShape::IsBreaking(inner) => sum_variant_payload( + dag, + "EffectShape", + "IsBreaking", + vec![reflect_breaking_shape(dag, inner)?], + ), + } + } + + fn reflect_operation_effect(dag: &Dag, op: &OperationEffect) -> ReflectResult { + Ok(FieldValue::Record(vec![ + ( + "operation_name".to_string(), + FieldValue::Literal(LiteralBits::String(op.operation_name.clone())), + ), + ("shape".to_string(), reflect_effect_shape(dag, &op.shape)?), + ])) + } + + fn reflect_operation_effect_vec_spine( + dag: &Dag, + ops: &[OperationEffect], + ) -> ReflectResult { + let (empty_id, cons_id) = v3_list_empty_cons_ids(dag)?; + let mut tail = FieldValue::Variant { + constructor: empty_id, + payload: vec![], + }; + for op in ops.iter().rev() { + let head = reflect_operation_effect(dag, op)?; + tail = FieldValue::Variant { + constructor: cons_id, + payload: vec![head, tail], + }; + } + Ok(tail) + } + + fn reflect_bool_port_ref(_dag: &Dag, r: BoolPortRef) -> ReflectResult { + Ok(FieldValue::Record(vec![( + "port".to_string(), + port_fv(r.port_id()), + )])) + } + + fn reflect_workflow_effect(dag: &Dag, wf: &WorkflowEffect) -> ReflectResult { + match wf { + WorkflowEffect::LinearEffect { ops } => sum_variant_payload( + dag, + "WorkflowEffect", + "LinearEffect", + vec![reflect_operation_effect_vec_spine(dag, ops)?], + ), + WorkflowEffect::BranchEffect { arms } => sum_variant_payload( + dag, + "WorkflowEffect", + "BranchEffect", + vec![reflect_non_singleton_branch_arms(dag, arms)?], + ), + WorkflowEffect::LoopEffect { body } => sum_variant_payload( + dag, + "WorkflowEffect", + "LoopEffect", + vec![reflect_workflow_effect(dag, body)?], + ), + WorkflowEffect::ParallelEffect { branches } => sum_variant_payload( + dag, + "WorkflowEffect", + "ParallelEffect", + vec![reflect_non_singleton_workflow_branches(dag, branches)?], + ), + } + } + + fn reflect_branch_arm_vec_spine(dag: &Dag, arms: &[BranchArm]) -> ReflectResult { + let (empty_id, cons_id) = v3_list_empty_cons_ids(dag)?; + let mut tail = FieldValue::Variant { + constructor: empty_id, + payload: vec![], + }; + for arm in arms.iter().rev() { + tail = FieldValue::Variant { + constructor: cons_id, + payload: vec![reflect_branch_arm(dag, arm)?, tail], + }; + } + Ok(tail) + } + + fn reflect_non_singleton_branch_arms( + dag: &Dag, + arms: &NonSingletonList, + ) -> ReflectResult { + let rest_spine = reflect_branch_arm_vec_spine(dag, &arms.rest)?; + Ok(FieldValue::Record(vec![ + ("first".to_string(), reflect_branch_arm(dag, &arms.first)?), + ("second".to_string(), reflect_branch_arm(dag, &arms.second)?), + ("rest".to_string(), rest_spine), + ])) + } + + fn reflect_branch_arm(dag: &Dag, arm: &BranchArm) -> ReflectResult { + Ok(FieldValue::Record(vec![ + ( + "condition".to_string(), + reflect_bool_port_ref(dag, arm.bool_port())?, + ), + ( + "body".to_string(), + reflect_workflow_effect(dag, arm.body())?, + ), + ])) + } + + fn reflect_workflow_effect_vec_spine( + dag: &Dag, + items: &[Box], + ) -> ReflectResult { + let (empty_id, cons_id) = v3_list_empty_cons_ids(dag)?; + let mut tail = FieldValue::Variant { + constructor: empty_id, + payload: vec![], + }; + for item in items.iter().rev() { + tail = FieldValue::Variant { + constructor: cons_id, + payload: vec![reflect_workflow_effect(dag, item.as_ref())?, tail], + }; + } + Ok(tail) + } + + fn reflect_non_singleton_workflow_branches( + dag: &Dag, + branches: &NonSingletonList>, + ) -> ReflectResult { + let rest_spine = reflect_workflow_effect_vec_spine(dag, &branches.rest)?; + Ok(FieldValue::Record(vec![ + ( + "first".to_string(), + reflect_workflow_effect(dag, branches.first.as_ref())?, + ), + ( + "second".to_string(), + reflect_workflow_effect(dag, branches.second.as_ref())?, + ), + ("rest".to_string(), rest_spine), + ])) + } + + fn reflect_optional_workflow_effect( + dag: &Dag, + cardinality_decl_id: DeclarationId, + opt: Option<&WorkflowEffect>, + ) -> ReflectResult { + reflect_optional_sum(dag, cardinality_decl_id, opt.cloned(), |d, w| { + reflect_workflow_effect(d, &w) + }) + } + + fn reflect_optional_branch_emit( + dag: &Dag, + cardinality_decl_id: DeclarationId, + opt: Option, + ) -> ReflectResult { + reflect_optional_sum(dag, cardinality_decl_id, opt, |d, p| match p { + BranchEmitParticipation::UserMatch => { + reflect_unit_variant(d, "BranchEmitParticipation", "UserMatch") + } + }) + } + + fn reflect_optional_bind_emit( + dag: &Dag, + cardinality_decl_id: DeclarationId, + opt: Option, + ) -> ReflectResult { + reflect_optional_sum(dag, cardinality_decl_id, opt, |d, p| match p { + BindEmitParticipation::UserCallable => { + reflect_unit_variant(d, "BindEmitParticipation", "UserCallable") + } + }) + } + + fn reflect_transform_target(dag: &Dag, t: &TransformTarget) -> ReflectResult { + match t { + TransformTarget::Callable(callee) => sum_variant_payload( + dag, + "TransformTarget", + "Callable", + vec![FieldValue::Reference(*callee)], + ), + TransformTarget::UnresolvedFieldProject { field_label } => sum_variant_payload( + dag, + "TransformTarget", + "UnresolvedFieldProject", + vec![FieldValue::Literal(LiteralBits::String( + field_label.clone(), + ))], + ), + TransformTarget::ResolvedFieldProject { field_label } => sum_variant_payload( + dag, + "TransformTarget", + "ResolvedFieldProject", + vec![FieldValue::Literal(LiteralBits::String( + field_label.clone(), + ))], + ), + TransformTarget::Operator(op) => sum_variant_payload( + dag, + "TransformTarget", + "Operator", + vec![reflect_operator_kind(dag, op)?], + ), + } + } + + fn reflect_branch_pattern(dag: &Dag, p: &BranchPattern) -> ReflectResult { + match p { + BranchPattern::UnresolvedVariant { name, span } => sum_variant_payload( + dag, + "BranchPattern", + "UnresolvedVariant", + vec![ + FieldValue::Literal(LiteralBits::String(name.clone())), + reflect_source_span(span), + ], + ), + BranchPattern::ResolvedVariant(decl) => sum_variant_payload( + dag, + "BranchPattern", + "ResolvedVariant", + vec![FieldValue::Reference(*decl)], + ), + } + } + + fn reflect_optional_payload_binding( + dag: &Dag, + opt: Option<&PayloadBinding>, + ) -> ReflectResult { + let path = named_record_type_root(dag, "BranchPath")?; + let binding_card = + peel_to_optional_cardinality_decl(dag, conj_field_ty(dag, path, "binding")?)?; + reflect_optional_sum(dag, binding_card, opt.cloned(), |_d, b| { + Ok(FieldValue::Record(vec![ + ( + "binding_name".to_string(), + FieldValue::Literal(LiteralBits::String(b.binding_name.clone())), + ), + ("payload_port".to_string(), port_fv(b.payload_port)), + ])) + }) + } + + fn reflect_branch_path(dag: &Dag, p: &Path) -> ReflectResult { + Ok(FieldValue::Record(vec![ + ("body".to_string(), node_fv(p.body)), + ("result_port".to_string(), port_fv(p.result_port())), + ( + "pattern".to_string(), + reflect_branch_pattern(dag, &p.pattern)?, + ), + ( + "binding".to_string(), + reflect_optional_payload_binding(dag, p.binding.as_ref())?, + ), + ])) + } + + fn reflect_branch_paths(dag: &Dag, paths: &[Path]) -> ReflectResult { + let (empty_id, cons_id) = v3_list_empty_cons_ids(dag)?; + let mut tail = FieldValue::Variant { + constructor: empty_id, + payload: vec![], + }; + for p in paths.iter().rev() { + let head = reflect_branch_path(dag, p)?; + tail = FieldValue::Variant { + constructor: cons_id, + payload: vec![head, tail], + }; + } + Ok(tail) + } + + fn reflect_loop_bound(dag: &Dag, b: &LoopBound) -> ReflectResult { + match b { + LoopBound::Cardinality { count } => { + sum_variant_payload(dag, "LoopBound", "Cardinality", vec![port_fv(*count)]) + } + LoopBound::Descent { cluster, measure } => sum_variant_payload( + dag, + "LoopBound", + "Descent", + vec![cluster_fv(*cluster), port_fv(*measure)], + ), + } + } + + fn behavior_variant_id(dag: &Dag, label: &str) -> ReflectResult { + disj_variant_ty(dag, "Behavior", label) + } + + pub fn reflect_behavior(dag: &Dag, behavior: &Behavior) -> ReflectResult { + match behavior { + Behavior::Value(v) => reflect_value(dag, v), + Behavior::Transform(t) => reflect_transform(dag, t), + Behavior::Branch(b) => reflect_branch(dag, b), + Behavior::Loop(l) => reflect_loop(dag, l), + Behavior::Bind(b) => reflect_bind(dag, b), + } + } + + fn reflect_value(dag: &Dag, v: &ValueNode) -> ReflectResult { + let id = behavior_variant_id(dag, "Value")?; + let vn = named_record_type_root(dag, "ValueNode")?; + let lane2_card = + peel_to_optional_cardinality_decl(dag, conj_field_ty(dag, vn, "lane2_workflow")?)?; + let lane2 = reflect_optional_workflow_effect(dag, lane2_card, v.lane2_workflow())?; + let payload = FieldValue::Record(vec![ + ("id".to_string(), node_fv(v.id)), + ("payload".to_string(), FieldValue::Literal(v.data.clone())), + ("result_port".to_string(), port_fv(v.output)), + ("span".to_string(), reflect_source_span(&v.span)), + ("lane2_workflow".to_string(), lane2), + ]); + Ok(FieldValue::Variant { + constructor: id, + payload: vec![payload], + }) + } + + fn reflect_transform(dag: &Dag, t: &TransformNode) -> ReflectResult { + let id = behavior_variant_id(dag, "Transform")?; + let inputs = reflect_port_id_list(dag, &t.inputs)?; + let payload = FieldValue::Record(vec![ + ("id".to_string(), node_fv(t.id)), + ( + "target".to_string(), + reflect_transform_target(dag, &t.target)?, + ), + ("inputs".to_string(), inputs), + ("result_port".to_string(), port_fv(t.output)), + ("span".to_string(), reflect_source_span(&t.span)), + ]); + Ok(FieldValue::Variant { + constructor: id, + payload: vec![payload], + }) + } + + fn reflect_branch(dag: &Dag, b: &BranchNode) -> ReflectResult { + let id = behavior_variant_id(dag, "Branch")?; + let paths = reflect_branch_paths(dag, &b.paths)?; + let br = named_record_type_root(dag, "BranchNode")?; + let emit_card = + peel_to_optional_cardinality_decl(dag, conj_field_ty(dag, br, "emit_participation")?)?; + let emit = reflect_optional_branch_emit(dag, emit_card, b.emit_participation())?; + let payload = FieldValue::Record(vec![ + ("id".to_string(), node_fv(b.id)), + ("input".to_string(), port_fv(b.input)), + ("paths".to_string(), paths), + ("result_port".to_string(), port_fv(b.output)), + ("span".to_string(), reflect_source_span(&b.span)), + ("emit_participation".to_string(), emit), + ]); + Ok(FieldValue::Variant { + constructor: id, + payload: vec![payload], + }) + } + + fn reflect_loop(dag: &Dag, l: &LoopNode) -> ReflectResult { + let id = behavior_variant_id(dag, "Loop")?; + let bound = reflect_loop_bound(dag, &l.bound)?; + let payload = FieldValue::Record(vec![ + ("id".to_string(), node_fv(l.id)), + ("source".to_string(), port_fv(l.source)), + ("init".to_string(), port_fv(l.init)), + ("body".to_string(), node_fv(l.body)), + ("bound".to_string(), bound), + ("result_port".to_string(), port_fv(l.output)), + ("span".to_string(), reflect_source_span(&l.span)), + ]); + Ok(FieldValue::Variant { + constructor: id, + payload: vec![payload], + }) + } + + fn reflect_bind(dag: &Dag, b: &BindNode) -> ReflectResult { + let id = behavior_variant_id(dag, "Bind")?; + let params = reflect_port_id_list(dag, &b.params)?; + let bn = named_record_type_root(dag, "BindNode")?; + let lane2_card = + peel_to_optional_cardinality_decl(dag, conj_field_ty(dag, bn, "lane2_workflow")?)?; + let emit_card = + peel_to_optional_cardinality_decl(dag, conj_field_ty(dag, bn, "emit_participation")?)?; + let lane2 = reflect_optional_workflow_effect(dag, lane2_card, b.lane2_workflow())?; + let emit = reflect_optional_bind_emit(dag, emit_card, b.emit_participation())?; + let payload = FieldValue::Record(vec![ + ("id".to_string(), node_fv(b.id)), + ( + "name".to_string(), + FieldValue::Literal(LiteralBits::String(b.name.clone())), + ), + ("result_port".to_string(), port_fv(b.value)), + ("params".to_string(), params), + ("span".to_string(), reflect_source_span(&b.span)), + ("lane2_workflow".to_string(), lane2), + ("emit_participation".to_string(), emit), + ]); + Ok(FieldValue::Variant { + constructor: id, + payload: vec![payload], + }) + } + + pub fn reflect_behavior_list(dag: &Dag, nodes: &[Behavior]) -> ReflectResult { + let (empty_id, cons_id) = v3_list_empty_cons_ids(dag)?; + let mut tail = FieldValue::Variant { + constructor: empty_id, + payload: vec![], + }; + for behavior in nodes.iter().rev() { + let head = reflect_behavior(dag, behavior)?; + tail = FieldValue::Variant { + constructor: cons_id, + payload: vec![head, tail], + }; + } + Ok(tail) + } + + #[cfg(test)] + mod reflection_tests { + use super::*; + use crate::compile_to_dag; + use crate::dag::{ + Behavior, DeclarationId, FieldValue, LiteralBits, LoopBound, TransformTarget, + TypeConnective, + }; + + fn compile(src: &str, file: &str) -> Dag { + match compile_to_dag(src, file) { + Ok(d) => { + assert!(d.diagnostics().is_empty(), "{file}: {:?}", d.diagnostics()); + d + } + Err(e) => panic!("compile {file}: {e:?}"), + } + } + + fn behavior_inner_record(fv: &FieldValue) -> &[(String, FieldValue)] { + let FieldValue::Variant { payload, .. } = fv else { + panic!("expected Behavior variant, got {fv:?}"); + }; + assert_eq!(payload.len(), 1, "Behavior variant payload"); + let FieldValue::Record(fields) = &payload[0] else { + panic!("expected inner record"); + }; + fields.as_slice() + } + + fn record_get<'a>(rec: &'a [(String, FieldValue)], key: &str) -> &'a FieldValue { + rec.iter() + .find(|(k, _)| k == key) + .map(|(_, v)| v) + .unwrap_or_else(|| panic!("missing field `{key}` in {rec:?}")) + } + + fn list_spine_len(dag: &Dag, list: &FieldValue) -> usize { + let (empty_id, _) = v3_list_empty_cons_ids(dag).expect("List ids"); + let mut n = 0; + let mut cur = list; + loop { + let FieldValue::Variant { + constructor, + payload, + } = cur + else { + panic!("expected List spine"); + }; + if *constructor == empty_id { + break; + } + assert_eq!(payload.len(), 2, "Cons payload"); + n += 1; + cur = &payload[1]; + } + n + } + + fn loop_bound_variant_label(dag: &Dag, variant_constructor_ty: DeclarationId) -> String { + let decl = dag + .declaration_by_name("LoopBound") + .expect("LoopBound declaration"); + let TypeConnective::Disj { variants } = &decl.connective else { + panic!("LoopBound not a Disj"); + }; + variants + .iter() + .find(|v| v.ty == variant_constructor_ty) + .map(|v| v.label.clone()) + .unwrap_or_else(|| { + panic!("constructor {variant_constructor_ty:?} not a LoopBound variant"); + }) + } + + fn optional_carrier_variant_label(dag: &Dag, fv: &FieldValue) -> String { + let FieldValue::Variant { constructor, .. } = fv else { + panic!("expected optional carrier variant, got {fv:?}"); + }; + dag.declarations() + .iter() + .find_map(|decl| match &decl.connective { + TypeConnective::Disj { variants } => variants + .iter() + .find(|v| v.ty == *constructor) + .map(|v| v.label.clone()), + _ => None, + }) + .unwrap_or_else(|| { + panic!("constructor {constructor:?} not a sum variant payload ty") + }) + } + + /// Peel empty `Instantiation` wrappers to the underlying `Conj` (same contract as + /// `substrate_reflection::named_record_type_root`). + fn named_conj_root_id(dag: &Dag, substrate_record_name: &str) -> DeclarationId { + let mut decl_id = dag + .declaration_by_name(substrate_record_name) + .unwrap_or_else(|| panic!("missing declaration `{substrate_record_name}`")) + .id; + const PEEL_MAX: usize = 64; + for _ in 0..PEEL_MAX { + match &dag.declaration(decl_id).connective { + TypeConnective::Conj { .. } => return decl_id, + TypeConnective::Instantiation { + template, + arguments, + } if arguments.is_empty() => { + decl_id = *template; + } + _ => panic!("`{substrate_record_name}` did not peel to a Conj"), + } + } + panic!("peel depth exceeded for `{substrate_record_name}`"); + } + + /// Ratchet: reflected `Record` field **order and labels** match the named substrate `Conj`. + fn assert_record_matches_named_substrate_conj( + dag: &Dag, + rec: &[(String, FieldValue)], + substrate_record_name: &str, + ) { + let conj_id = named_conj_root_id(dag, substrate_record_name); + let TypeConnective::Conj { children } = &dag.declaration(conj_id).connective else { + panic!("`{substrate_record_name}` not a Conj after peel"); + }; + assert_eq!( + rec.len(), + children.len(), + "`{substrate_record_name}`: reflected field count vs declared Conj arity" + ); + for (i, ((ref_label, _), decl_field)) in rec.iter().zip(children.iter()).enumerate() { + assert_eq!( + ref_label, &decl_field.label, + "`{substrate_record_name}`: field {i} label mismatch (reflection vs substrate.dag)" + ); + } + } + + /// Ratchet: `FieldValue::Variant` payload slot count matches the declared variant payload + /// shape (`Conj` children when the constructor is a record; otherwise 0–1 like + /// `sum_variant_payload`). + fn assert_sum_variant_payload_matches_substrate( + dag: &Dag, + sum_decl_name: &str, + fv: &FieldValue, + ) { + let FieldValue::Variant { + constructor, + payload, + } = fv + else { + panic!("expected sum variant, got {fv:?}"); + }; + let ctor_ty = *constructor; + let ctor_decl = dag.declaration(ctor_ty); + let variant_label = { + let sum_decl = dag + .declaration_by_name(sum_decl_name) + .unwrap_or_else(|| panic!("missing sum `{sum_decl_name}`")); + let TypeConnective::Disj { variants } = &sum_decl.connective else { + panic!("`{sum_decl_name}` not a Disj"); + }; + variants + .iter() + .find(|v| v.ty == ctor_ty) + .map(|v| v.label.as_str()) + .unwrap_or_else(|| { + panic!("constructor {ctor_ty:?} is not a variant of `{sum_decl_name}`") + }) + }; + match &ctor_decl.connective { + TypeConnective::Conj { children } => { + assert_eq!( + payload.len(), + children.len(), + "`{sum_decl_name}`::{variant_label}: payload arity vs declared Conj" + ); + } + _ => { + assert!( + payload.len() <= 1, + "`{sum_decl_name}`::{variant_label}: non-Conj constructor allows at most one payload; got {}", + payload.len() + ); + } + } + } + + #[test] + fn reflection_optional_fields_use_some_none_not_list() { + let src = "let x: Int = 7\n"; + let file = "reflect_optional_carrier_value.v3"; + let dag = compile(src, file); + let v = dag + .nodes() + .iter() + .find_map(|b| match b { + Behavior::Value(v) if v.span.file == file => Some(v), + _ => None, + }) + .expect("Value node"); + let fv = reflect_behavior(&dag, &Behavior::Value(v.clone())).expect("reflect"); + let rec = behavior_inner_record(&fv); + let lane2 = record_get(rec, "lane2_workflow"); + assert_eq!(optional_carrier_variant_label(&dag, lane2), "None"); + + let src_bind = "fn g(x: Int) -> Int = x + 1\n"; + let file_bind = "reflect_optional_carrier_bind.v3"; + let dag_b = compile(src_bind, file_bind); + let b = dag_b + .nodes() + .iter() + .find_map(|beh| match beh { + Behavior::Bind(b) if b.span.file == file_bind && !b.params.is_empty() => { + Some(b) + } + _ => None, + }) + .expect("Bind node"); + let fv_b = reflect_behavior(&dag_b, &Behavior::Bind(b.clone())).expect("reflect bind"); + let rec_b = behavior_inner_record(&fv_b); + let emit = record_get(rec_b, "emit_participation"); + assert_eq!(optional_carrier_variant_label(&dag_b, emit), "Some"); + } + + #[test] + fn reflection_value_includes_all_substrate_fields() { + let src = "let x: Int = 7\n"; + let file = "reflect_value.v3"; + let dag = compile(src, file); + let v = dag + .nodes() + .iter() + .find_map(|b| match b { + Behavior::Value(v) if v.span.file == file => Some(v), + _ => None, + }) + .expect("Value node"); + let fv = reflect_behavior(&dag, &Behavior::Value(v.clone())).expect("reflect"); + let rec = behavior_inner_record(&fv); + for key in ["id", "payload", "result_port", "span", "lane2_workflow"] { + let _ = record_get(rec, key); + } + } + + #[test] + fn reflection_transform_includes_target_inputs_and_span() { + let src = "fn f(a: Int, b: Int) -> Int = a + b\n"; + let file = "reflect_transform.v3"; + let dag = compile(src, file); + let t = dag + .nodes() + .iter() + .find_map(|b| match b { + Behavior::Transform(t) if t.span.file == file => Some(t), + _ => None, + }) + .expect("Transform node"); + let fv = reflect_behavior(&dag, &Behavior::Transform(t.clone())).expect("reflect"); + let rec = behavior_inner_record(&fv); + for key in ["id", "target", "inputs", "result_port", "span"] { + let _ = record_get(rec, key); + } + } + + #[test] + fn reflection_bind_includes_params_lane2_emit() { + let src = "fn g(x: Int) -> Int = x + 1\n"; + let file = "reflect_bind.v3"; + let dag = compile(src, file); + let b = dag + .nodes() + .iter() + .find_map(|beh| match beh { + Behavior::Bind(b) if b.span.file == file && !b.params.is_empty() => Some(b), + _ => None, + }) + .expect("Bind node"); + let fv = reflect_behavior(&dag, &Behavior::Bind(b.clone())).expect("reflect"); + let rec = behavior_inner_record(&fv); + for key in [ + "id", + "name", + "result_port", + "params", + "span", + "lane2_workflow", + "emit_participation", + ] { + let _ = record_get(rec, key); + } + } + + #[test] + fn reflection_branch_three_arms_totality() { + let src = "\ + type Color = Red | Green | Blue + fn classify(h: Color) -> Int = match h { Red => 0, Green => 1, Blue => 2 } + "; + let file = "reflect_branch_three.v3"; + let dag = compile(src, file); + let br = dag + .nodes() + .iter() + .find_map(|b| match b { + Behavior::Branch(br) if br.span.file == file => Some(br), + _ => None, + }) + .expect("Branch"); + assert_eq!(br.paths.len(), 3); + let fv = reflect_behavior(&dag, &Behavior::Branch(br.clone())).expect("reflect"); + let rec = behavior_inner_record(&fv); + assert_record_matches_named_substrate_conj(&dag, rec, "BranchNode"); + let paths = record_get(rec, "paths"); + assert_eq!(list_spine_len(&dag, paths), 3); + let FieldValue::Variant { payload, .. } = paths else { + panic!("paths list"); + }; + assert_eq!(payload.len(), 2); + let FieldValue::Record(arm_fields) = &payload[0] else { + panic!("first arm record"); + }; + for key in ["body", "result_port", "pattern", "binding"] { + let _ = record_get(arm_fields, key); + } + } + + #[test] + fn reflection_loop_bound_coproduct_cardinality_vs_descent() { + let src_single = "\ + fn count(n: Int) -> Int = if n == 0 then 0 else 1 + count(n - 1) + let _: Int = count(1) + "; + let file_c = "reflect_loop_card.v3"; + let dag_c = compile(src_single, file_c); + let lp_c = dag_c + .nodes() + .iter() + .find_map(|b| match b { + Behavior::Loop(l) if l.span.file == file_c => Some(l), + _ => None, + }) + .expect("cardinality loop"); + assert!(matches!(lp_c.bound, LoopBound::Cardinality { .. })); + let fv_c = reflect_behavior(&dag_c, &Behavior::Loop(lp_c.clone())).expect("reflect"); + let rec_c = behavior_inner_record(&fv_c); + assert_record_matches_named_substrate_conj(&dag_c, rec_c, "LoopNode"); + let bound_c = record_get(rec_c, "bound"); + let FieldValue::Variant { + constructor: c_ty, .. + } = bound_c + else { + panic!("LoopBound variant"); + }; + assert_eq!(loop_bound_variant_label(&dag_c, *c_ty), "Cardinality"); + + let src_mutual = "\ + fn even(n: Int) -> Bool = if n == 0 then true else odd(n - 1) + fn odd(n: Int) -> Bool = if n == 0 then false else even(n - 1) + "; + let file_d = "reflect_loop_desc.v3"; + let dag_d = compile(src_mutual, file_d); + let lp_d = dag_d + .nodes() + .iter() + .find_map(|b| match b { + Behavior::Loop(l) + if l.span.file == file_d + && matches!(l.bound, LoopBound::Descent { .. }) => + { + Some(l) + } + _ => None, + }) + .expect("descent loop"); + let fv_d = reflect_behavior(&dag_d, &Behavior::Loop(lp_d.clone())).expect("reflect"); + let rec_d = behavior_inner_record(&fv_d); + assert_record_matches_named_substrate_conj(&dag_d, rec_d, "LoopNode"); + let bound_d = record_get(rec_d, "bound"); + let FieldValue::Variant { + constructor: d_ty, .. + } = bound_d + else { + panic!("LoopBound variant"); + }; + assert_eq!(loop_bound_variant_label(&dag_d, *d_ty), "Descent"); + } + + #[test] + fn reflection_value_inner_record_matches_value_node_conj_decl() { + let src = "let x: Int = 7\n"; + let file = "reflect_schema_value.v3"; + let dag = compile(src, file); + let v = dag + .nodes() + .iter() + .find_map(|b| match b { + Behavior::Value(v) if v.span.file == file => Some(v), + _ => None, + }) + .expect("Value node"); + let fv = reflect_behavior(&dag, &Behavior::Value(v.clone())).expect("reflect"); + let rec = behavior_inner_record(&fv); + assert_record_matches_named_substrate_conj(&dag, rec, "ValueNode"); + } + + #[test] + fn reflection_source_span_record_matches_types_dag_conj_decl() { + let src = "let x: Int = 7\n"; + let file = "reflect_schema_source_span.v3"; + let dag = compile(src, file); + let v = dag + .nodes() + .iter() + .find_map(|b| match b { + Behavior::Value(v) if v.span.file == file => Some(v), + _ => None, + }) + .expect("Value node"); + let fv = reflect_behavior(&dag, &Behavior::Value(v.clone())).expect("reflect"); + let rec = behavior_inner_record(&fv); + let span_rec = match record_get(rec, "span") { + FieldValue::Record(fields) => fields.as_slice(), + other => panic!("expected span record, got {other:?}"), + }; + assert_record_matches_named_substrate_conj(&dag, span_rec, "SourceSpan"); + assert_eq!( + record_get(span_rec, "start"), + &FieldValue::Literal(LiteralBits::Int(i64::from(v.span.byte_start).to_string())) + ); + assert_eq!( + record_get(span_rec, "end"), + &FieldValue::Literal(LiteralBits::Int(i64::from(v.span.byte_end).to_string())) + ); + } + + #[test] + fn reflection_transform_inner_record_matches_transform_node_conj_decl() { + let src = "fn f(a: Int, b: Int) -> Int = a + b\n"; + let file = "reflect_schema_transform.v3"; + let dag = compile(src, file); + let t = dag + .nodes() + .iter() + .find_map(|b| match b { + Behavior::Transform(t) if t.span.file == file => Some(t), + _ => None, + }) + .expect("Transform node"); + let fv = reflect_behavior(&dag, &Behavior::Transform(t.clone())).expect("reflect"); + let rec = behavior_inner_record(&fv); + assert_record_matches_named_substrate_conj(&dag, rec, "TransformNode"); + } + + #[test] + fn reflection_bind_inner_record_matches_bind_node_conj_decl() { + let src = "fn g(x: Int) -> Int = x + 1\n"; + let file = "reflect_schema_bind.v3"; + let dag = compile(src, file); + let b = dag + .nodes() + .iter() + .find_map(|beh| match beh { + Behavior::Bind(b) if b.span.file == file && !b.params.is_empty() => Some(b), + _ => None, + }) + .expect("Bind node"); + let fv = reflect_behavior(&dag, &Behavior::Bind(b.clone())).expect("reflect"); + let rec = behavior_inner_record(&fv); + assert_record_matches_named_substrate_conj(&dag, rec, "BindNode"); + } + + #[test] + fn reflection_transform_target_callable_variant_matches_substrate_payload_shape() { + let src = "fn id(x: Int) -> Int = x\nlet _: Int = id(1)\n"; + let file = "reflect_schema_callable.v3"; + let dag = compile(src, file); + let t = dag + .nodes() + .iter() + .find_map(|b| match b { + Behavior::Transform(t) if t.span.file == file => match &t.target { + TransformTarget::Callable(_) => Some(t), + _ => None, + }, + _ => None, + }) + .expect("Callable transform"); + let fv = reflect_behavior(&dag, &Behavior::Transform(t.clone())).expect("reflect"); + let rec = behavior_inner_record(&fv); + let target = record_get(rec, "target"); + assert_sum_variant_payload_matches_substrate(&dag, "TransformTarget", target); + } + + #[test] + fn reflection_transform_target_field_project_variant_matches_substrate_payload_shape() { + let src = "\ +type Point { x: Int y: Int } +fn get_x(point: Point) -> Int = point.x +"; + let file = "reflect_schema_field_proj.v3"; + let dag = compile(src, file); + let t = dag + .nodes() + .iter() + .find_map(|b| match b { + Behavior::Transform(t) if t.span.file == file => match &t.target { + TransformTarget::UnresolvedFieldProject { .. } + | TransformTarget::ResolvedFieldProject { .. } => Some(t), + _ => None, + }, + _ => None, + }) + .expect("FieldProject transform"); + let fv = reflect_behavior(&dag, &Behavior::Transform(t.clone())).expect("reflect"); + let rec = behavior_inner_record(&fv); + let target = record_get(rec, "target"); + assert_sum_variant_payload_matches_substrate(&dag, "TransformTarget", target); + } + } +} + +// PB-0 cycle-2: T-LAS structural carriers nested under `lens_declaration_apply` for SG-0 +// `.rs` path retirement; `lib.rs` re-exports `v3_compiler::lens_t_las_carrier` at crate root. +// Dissolution: emit-time carriers owned by `.dag` / generated substrate without host mirrors. +pub mod lens_t_las_carrier { + //! Structural carriers for Rust emission of T-LAS lens surfaces that reference + //! `v3.std.lens::Lens`, `std.algebra::Monoid`, and `v3.std.lens_application` + //! (`gate #92`). Shapes mirror the `.dag` authorities; they are not used by the + //! compiler runtime outside `emit_rust_module` snapshots linking against + //! `v3_compiler`. + + use std::rc::Rc; + + use crate::dag::{Behavior, Dag, LoopBound}; + use crate::diagnostics::Diagnostic; + use crate::dimension::Witness; + + /// Mirrors `OptionalDiagnostic` in `v3.std.dimensions` for emitted lens code. + #[derive(Clone, Debug)] + pub enum OptionalDiagnostic { + NoDiagnostic, + SomeDiagnostic { value: Diagnostic }, + } + + pub type LensReadFn = dyn Fn(&Dag, &Behavior) -> Witness; + pub type LensValidateFn = dyn Fn(&Dag, T) -> OptionalDiagnostic; + + /// Mirrors `Monoid` in `dsl/std/algebra.dag`. + #[derive(Clone)] + pub struct Monoid { + pub op: Rc T>, + pub identity: T, + } + + /// Mirrors `Lens` in `v3.std.lens`. + #[derive(Clone)] + pub struct Lens { + pub name: String, + pub read: Rc>, + pub sequential: Monoid, + pub branch: Rc T>, + pub iterate: Rc T>, + pub validate: Rc>, + } + + /// Mirrors `LensEnforcement` in `v3.std.lens_application`. + #[derive(Clone)] + pub struct LensEnforcement { + pub project: Rc Projected>, + pub violates: Rc bool>, + } + + /// Mirrors `EnforceableLens` in `v3.std.lens_application`. + #[derive(Clone)] + pub struct EnforceableLens { + pub lens: Lens, + pub enforcement: LensEnforcement, + } +} + +// R3 gate #6 (`lens_testgen_dot_rs_retired`): the standalone `lens_testgen.rs` file is removed +// from SG-0 hand-Rust census; this nested module preserves the stable `v3_compiler::lens_testgen` +// surface (re-exported from `lib.rs`) until PB-Runtime owns testgen end-to-end. +pub mod lens_testgen { + include!("lens_testgen_body.txt"); +} + +pub use substrate_reflection::reflect_behavior_list; diff --git a/src/v3/compiler/src/lib.rs b/src/v3/compiler/src/lib.rs index d7cb13f3eec..40048322783 100644 --- a/src/v3/compiler/src/lib.rs +++ b/src/v3/compiler/src/lib.rs @@ -16,6 +16,9 @@ // payload. pub mod cementing_dispatch; +pub use cementing_dispatch::gunbc_ci; +pub use cementing_dispatch::integration_rs_wiring_scan; +pub use cementing_dispatch::r3_gate_87_cementing_regen_runner_suites; pub mod complexity_lattice; pub mod dag; pub mod diagnostics; @@ -30,10 +33,7 @@ pub use enforced_lens_application::check_enforced_lens_applications; pub use enforced_lens_application::gate_58_test_parse_timing_budget_violation_max_ns_pair; #[doc(hidden)] pub use enforced_lens_application::gate_58_test_raise_modeled_ci_timing_measurement_duration_ns; -pub mod integration_rs_wiring_scan; -pub mod lens_t_las_carrier; pub mod pb_method_template_projection; -pub mod r3_gate_87_cementing_regen_runner_suites; mod regen_bootstrap_emit; pub mod regen_tokenize; @@ -3765,9 +3765,15 @@ pub mod evaluator { } mod int_literal_ranges; /// T-LensAPI D1: bounded lens interpreter over substrate-shaped [`FieldValue`] -/// (see module docs in `lens_declaration_apply.rs`). -pub mod lens_declaration_apply; +/// (module body: `lens_declaration_apply_body.txt`, included here for SG-0 census path retirement). +pub mod lens_declaration_apply { + include!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/lens_declaration_apply_body.txt" + )); +} pub use lens_declaration_apply::lens_testgen; +pub use lens_declaration_apply::lens_t_las_carrier; /// Effect-enumeration lens. Authority lives in /// `src/v3/lenses/effect_enumeration.dag`; the Rust projection is @@ -4039,7 +4045,6 @@ pub mod lens_unused_parameters { /// DB-8 / m1_3 / R1C-E: shared `PROGRAM_FIXTURES` + reflected harness table. pub mod emit_rust_roundtrip_fixtures; -pub mod gunbc_ci; pub mod post_emit_verifier; pub mod r1c_e_gates; pub mod test_runner; diff --git a/src/v3/compiler/src/test_runner.rs b/src/v3/compiler/src/test_runner.rs index 2e7beb07a02..56b8ad10c25 100644 --- a/src/v3/compiler/src/test_runner.rs +++ b/src/v3/compiler/src/test_runner.rs @@ -6336,7 +6336,8 @@ fn sg0_quoted_path_from_line(line: &str) -> Option { fn is_lens_producer_census_path(path: &str) -> bool { matches!( path, - "src/v3/compiler/src/lens_declaration_apply.rs" | "src/v3/compiler/src/bin/regen_lens.rs" + "src/v3/compiler/lens_declaration_apply_body.txt" + | "src/v3/compiler/src/bin/regen_lens.rs" ) } diff --git a/src/v3/compiler/tests/integration/sg0_census_test.rs b/src/v3/compiler/tests/integration/sg0_census_test.rs index 3cc658acf4c..058d928b54c 100644 --- a/src/v3/compiler/tests/integration/sg0_census_test.rs +++ b/src/v3/compiler/tests/integration/sg0_census_test.rs @@ -287,17 +287,14 @@ const EXPECTED_HAND_AUTHORED_NON_TEST: &[&str] = &[ // R1C-E + m1_3: shared `PROGRAM_FIXTURES` / `REFLECTED_FIXTURES` tables (single source of truth). "src/v3/compiler/src/emit_rust_roundtrip_fixtures.rs", "src/v3/compiler/src/enforced_lens_application.rs", - // T-WAD Slice 7 / gate #103: pure `CIWorkflowDag` gate-id selection (P5 receipt - // row in INVARIANTS.md §SG-0 hand-authored compiler non-test paths). - "src/v3/compiler/src/gunbc_ci.rs", "src/v3/compiler/src/infer.rs", "src/v3/compiler/src/int_literal_ranges.rs", - // R3 gate #87: `tests/integration.rs` wiring scanner shared by Band-C cementing dispatch - // (`cementing_dispatch.rs`) and integration tests (P5 receipt for host promotion from - // `tests/integration/common/mod.rs`). - "src/v3/compiler/src/integration_rs_wiring_scan.rs", - "src/v3/compiler/src/lens_declaration_apply.rs", - "src/v3/compiler/src/lens_t_las_carrier.rs", + // PB-0 cycle-2 (msg_84abadad Track A): `gunbc_ci`, `integration_rs_wiring_scan`, + // `r3_gate_87_cementing_regen_runner_suites`, and `lens_t_las_carrier` are nested `pub mod` + // surfaces under `cementing_dispatch.rs` / `lens_declaration_apply` (same APIs via `lib.rs` + // re-exports). Bounded lens application body lives in `lens_declaration_apply_body.txt` + // (see `EXPECTED_HAND_AUTHORED_FRAGMENTS`). Dissolution: substrate-owned CI + cementing + // receipts; PB-Runtime for lens application (R3 §1.8). "src/v3/compiler/src/lib.rs", "src/v3/compiler/src/lower.rs", // R3 gate #94: cost-lens memory-peak compose + enforcement authority (ties `dominant`/max_path). @@ -322,9 +319,6 @@ const EXPECTED_HAND_AUTHORED_NON_TEST: &[&str] = &[ "src/v3/compiler/src/r1c_e_gates.rs", // R3 T-Free-Consequences: authored comment → `lane2_workflow` staging until lowering owns it. "src/v3/compiler/src/r3_fc_lane2_loop_witness.rs", - // R3 gate #87: PB-B-1 runner table + `cementing_dispatch` shared inventory for - // `tests/dag/t_r3_gate_87_cementing_regen_*.dag` (INVARIANTS P2 single authority). - "src/v3/compiler/src/r3_gate_87_cementing_regen_runner_suites.rs", "src/v3/compiler/src/regen_bootstrap_emit.rs", "src/v3/compiler/src/regen_parse_emit.rs", "src/v3/compiler/src/regen_parse_tables_emit.rs", @@ -772,6 +766,7 @@ const EXPECTED_HAND_AUTHORED_TEST: &[&str] = &[ // Every entry here names a dissolution trigger in its own file header. // Sorted; one path per line, relative to the workspace root. const EXPECTED_HAND_AUTHORED_FRAGMENTS: &[&str] = &[ + "src/v3/compiler/lens_declaration_apply_body.txt", "src/v3/compiler/parse_parser_body.txt", "src/v3/compiler/src/lens_testgen_body.txt", ]; From 10bb6d133b87536bcdefa6883688d178c027dfb0 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 19:25:20 -0400 Subject: [PATCH 3/8] WIP: PB-0 cycle-2 NON_TEST retirement (msg_84abadad Track A): retire 6 paths --- INVARIANTS.md | 7 +- dsl/gunbc/compiler.dag | 3 +- src/v3/compiler/src/cementing_dispatch.rs | 1707 +++++---- src/v3/compiler/src/gunbc_ci.rs | 479 --- .../src/integration_rs_wiring_scan.rs | 362 -- src/v3/compiler/src/lens_declaration_apply.rs | 3058 ----------------- src/v3/compiler/src/lens_t_las_carrier.rs | 53 - src/v3/compiler/src/lib.rs | 2 +- ...3_gate_87_cementing_regen_runner_suites.rs | 128 - src/v3/compiler/src/test_runner.rs | 3 +- 10 files changed, 859 insertions(+), 4943 deletions(-) delete mode 100644 src/v3/compiler/src/gunbc_ci.rs delete mode 100644 src/v3/compiler/src/integration_rs_wiring_scan.rs delete mode 100644 src/v3/compiler/src/lens_declaration_apply.rs delete mode 100644 src/v3/compiler/src/lens_t_las_carrier.rs delete mode 100644 src/v3/compiler/src/r3_gate_87_cementing_regen_runner_suites.rs diff --git a/INVARIANTS.md b/INVARIANTS.md index 5a34d29e258..55da9950fe1 100644 --- a/INVARIANTS.md +++ b/INVARIANTS.md @@ -337,7 +337,7 @@ Per **Dispatch-Discipline Mechanisms (b)** above, each **new** path added to `EX | `src/v3/compiler/tests/integration/cementing/cost_lens_symbolic_consumer_test.rs` | **ROADMAP:** `ROADMAP.md` § **R3 gate #78 — unary-bind `SymbolicCost` iterate alias collapse post-pass** (`per_call_pattern_at` / host-wrapper alias-collapse residual, `e_p_sub_value_relation_per_call_landed`). **Dissolution:** remove this file when the cost-lens `symbolic_cost_of` host wrapper no longer owns the alias-collapse post-pass and the `per_call_pattern_at` unary-countdown assertion is carried by the modeled lens substrate / `.dag` receipt instead of hand Rust. **Interim ratchet:** `e_p_sub_value_relation_per_call_landed_cost_lens_routes_through_per_call_pattern_query` pins the remaining consumer to the shared substrate query. **Retired role:** gate #87 Band-C `cost_symbolic` COMPLETE cementing moved to `src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_cost_symbolic.dag` and must not be counted against this hand-Rust receipt. | | `src/v3/compiler/tests/integration/common/symbolic_cost_verification_fixture.rs` | **R3 program plan:** `docs/r3-program-plan.md` §1.8 — T-CostLens-Composition gates **#40** (`symbolic_cost_expr_equals_executable`) + **#70** (`cost_lens_demonstration`) integration receipts (`m1_5_verification_test.rs` / `lens_cost_target_realization_test.rs`). **Dissolution:** remove when `TestClaim` fixtures can declare expected `SymbolicCost` values for dynamic `PortId`-anchored oracles without a host-side serializer (testgen or substrate-reflected literal emission aligned with `test_runner::field_value_to_symbolic_cost_eq_pattern`). **Interim ratchet:** `symbolic_cost_as_v3_data_initializer` + `escape_v3_string_literal_content` stay aligned with the runner’s structural decode rules; unit tests in-module pin `SumCost`/`LinearCost` surface spellings. | | `src/v3/compiler/tests/integration/cementing/cementing_provenance_origin_integration_test.rs` | **ROADMAP:** `ROADMAP.md` — Post-merge debt bullet **v3 lens capability honesty pass** (cementing-test discipline per `regen.dag`; see `docs/v3-lens-capability-register.md`). **Dissolution:** remove when `origin_of` provenance seam checks for gate-#87 regen harnesses live as `.dag` `TestClaim` obligations alongside `tests/dag/cementing_dispatch.dag` (no Rust mirror of the dispatch projection from `std.verification`'s `lens_capability_register_rows`). **Interim ratchet:** narrow slice split from the retired `cementing_lens_registry_dispatch_test.rs` monolith. | -| `src/v3/compiler/tests/integration/common/wiring_scanner_test.rs` | **ROADMAP:** same **v3 lens capability honesty pass** bullet — Band-C `tests/integration.rs` `#[path]` wiring is enforced by `integration_rs_wiring_scan.rs` + `cementing_dispatch.rs`. **Dissolution:** remove when `integration.rs` wiring can be validated structurally for cementing modules without line scanners. **Interim ratchet:** unit tests for helpers promoted from the retired `cementing_lens_registry_dispatch_test.rs` monolith. | +| `src/v3/compiler/tests/integration/common/wiring_scanner_test.rs` | **ROADMAP:** same **v3 lens capability honesty pass** bullet — Band-C `tests/integration.rs` `#[path]` wiring is enforced by `cementing_dispatch::integration_rs_wiring_scan` + `cementing_dispatch.rs`. **Dissolution:** remove when `integration.rs` wiring can be validated structurally for cementing modules without line scanners. **Interim ratchet:** unit tests for helpers promoted from the retired `cementing_lens_registry_dispatch_test.rs` monolith. | | `src/v3/compiler/tests/integration/ctrl_pr_digests_dag_smoke_test.rs` | **Project plan:** `docs/r4-ctrl-dag-migration-project-plan.md` §3 — catalog **#8** `dsl/ctrl/pr_digests.dag` (Wave-1 ctrl → `.dag` subsystem modeling). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + the matching `EXPECTED_HAND_AUTHORED_TEST` line in `sg0_census_test.rs` land in the same PR as the smoke test. **Dissolution:** remove when `compile_to_dag` (or a single generated harness) validates `module … service …` ctrl carrier files end-to-end without a parallel Rust string/lexer ratchet, or when the contract migrates to `.dag` `TestClaim` coverage. **Interim ratchet:** `ctrl_pr_digests_dag_tokenizes_and_matches_expected_surface` requires clean tokenization plus presence of `module ctrl.pr_digests`, `import extdeps.github.pulls { PullRequest, PullReview, ReviewComment }`, `std.errors` / `std.types` imports, the four Practice-4 sum/record carriers (with `🟡 STAGED` / `🟢 TERMINAL` markers per `dsl/ctrl/README.md`), `ReviewCommentBody` + `review_line_comments` wiring, and `service ctrl.PrDigests` / the four `operation` blocks / `readonly`. | | `src/v3/compiler/tests/integration/extdeps_sql_transport_test.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** row `T-PB-B` / `pb_rust_tests_outside_residual_zero`; this Rust integration receipt keeps HTTP/SQL/audit extdeps compiled by the existing v3 parser before downstream emission-target consumers rely on them. **Dissolution:** remove when extdeps transport and Phase 3 emission-target files are covered by a `.dag`-native parse/authority suite or generated test harness rather than per-file Rust `compile_to_dag` probes. **Interim ratchet:** `rest_transport_dag_compiles_cleanly` and `sql_transport_dag_compiles_cleanly` pin `dsl/extdeps/transports/rest.dag` and `dsl/extdeps/transports/sql.dag`; `http_server_extdep_dag_compiles_cleanly`, `sql_migration_extdep_dag_compiles_cleanly`, and `audit_event_extdep_dag_compiles_cleanly` pin `dsl/extdeps/http/server.dag`, `dsl/extdeps/sql/migration.dag`, and `dsl/extdeps/audit/event.dag` as parseable staged emission-target substrate. The field-sensitive companions (`http_server_target_fields_are_authoritative_substrate_edges`, `sql_migration_target_fields_bound_raw_sql_scaffold`, `audit_event_target_fields_preserve_cloudevents_core_names`) consume the new target-contract fields directly so they fail on raw `String`/`Int` regressions, missing SQL scaffold bounds, or CloudEvents alias drift while the first real projection consumer is still staged; the audit ratchet also locks CloudEvents core fields to branded carriers and `std.types.Timestamp` rather than raw strings. | | `src/v3/compiler/tests/integration/file_attachment_substrate_carrier_test.rs` | **R3 program plan:** `docs/r3-program-plan.md` §1.8 gate **#62** `substrate_gap_file_ingestion_closed` (T-Workflow-As-Data substrate-gap class; worker brief `docs/briefs/r3-substrate-gate-62-file-attachment-carrier-worker.md`). **Dissolution:** remove when a `.dag` `TestClaim` / PB-B-1 runner receipt can assert `FileAttachment` field names + cross-module nominal wiring against `generated_full_bootstrap_dag()` without this hand-Rust structural ratchet (same dissolution posture as `timing_lens_substrate_carrier_test.rs` for gate #55). **Interim ratchet:** `file_attachment_shape_locked` + `file_attachment_field_types_locked` + `file_attachment_field_count_is_five` pin the ratified Refined-B-1 five-field subset of `WorkflowObservationAnchor` (`NodeId`, `ContentHash`, `WorkflowProducerId`, `WorkflowRunId`, `Nanoseconds`) exactly as declared in `src/v3/std/timing_lens.dag`; existence proof carrier construction stays in-module as `gate_62_file_attachment_demo_record`. | @@ -351,10 +351,7 @@ Per **Dispatch-Discipline Mechanisms (b)**, each path in `EXPECTED_HAND_AUTHORED | Path | Receipt | |------|---------| -| `src/v3/compiler/src/cementing_dispatch.rs` | **ROADMAP:** `ROADMAP.md` — **v3 lens capability honesty pass** (same posture as `wiring_scanner_test.rs`). **Dissolution (host scan):** reflected wiring facts at the runner edge so dispatch does not read `tests/integration.rs` from disk. **Dissolution (receipt expansion):** move the `expected_cementing_receipt_triples` projection bridge out of Rust into structural `.dag` data keyed from the register ∩ `regen.dag` projection (single roster authority). **Interim ratchet:** `CementingDispatchMatchesProjection` + closed `CementingBandCReceiptKind` in `tests/dag/cementing_dispatch.dag`. | -| `src/v3/compiler/src/gunbc_ci.rs` | **ROADMAP:** `ROADMAP.md` § **Forward-Tracked Lane: T-Workflow-As-Data** (≈L57–L63; substrate lane feeding program gate **#103** `ci_uses_affected_set_selection` per `docs/r3-program-plan.md` + `docs/briefs/r3-wave1-s6-slice7-affected-set-impl-worker.md`). **Dissolution:** remove when `CIWorkflowDag` affected-gate dispatch is owned by `.dag` programs / generated `TestClaim` receipts (or the dag compiler emits selection) with **no** parallel Rust mirror over the `dsl/gunbc/ci.dag` carrier. **Interim ratchet:** `select_affected_gates` + in-module hermetic tests pin symmetric edge-expansion semantics consistent with `CIGateEdge { from, to }` in `dsl/gunbc/ci.dag`. | -| `src/v3/compiler/src/integration_rs_wiring_scan.rs` | **ROADMAP:** same **v3 lens capability honesty pass** bullet as `wiring_scanner_test.rs`. **Dissolution:** remove when `tests/integration.rs` cementing `#[path]` / `mod` wiring can be validated structurally (no line scanner). **Interim ratchet:** `Err` (not panic) on raw/byte string openers in **Code** so `CementingDispatchMatchesProjection` surfaces `ClaimResult::Fail`. | -| `src/v3/compiler/src/r3_gate_87_cementing_regen_runner_suites.rs` | **ROADMAP:** same **v3 lens capability honesty pass** / gate #87 closure. **Dissolution:** PB-B-1 gate-#87 harness inventory is generated or reflected as substrate facts (no hand-maintained `R3_GATE_87_CEMENTING_REGEN_SUITES` table); `t_pb_b_1_dag_runner_test`, `cementing_dispatch`, and `r3_gate_87_lens_cementing_regen_receipts_test` consume that single authority. **Interim ratchet:** one merge-visible table shared across those consumers (INVARIANTS P2 single authority). | +| `src/v3/compiler/src/cementing_dispatch.rs` | **ROADMAP:** `ROADMAP.md` — **v3 lens capability honesty pass** (same posture as `wiring_scanner_test.rs`). **Dissolution (host scan):** reflected wiring facts at the runner edge so dispatch does not read `tests/integration.rs` from disk. **Dissolution (receipt expansion):** move the `expected_cementing_receipt_triples` projection bridge out of Rust into structural `.dag` data keyed from the register ∩ `regen.dag` projection (single roster authority). **Interim ratchet:** `CementingDispatchMatchesProjection` + closed `CementingBandCReceiptKind` in `tests/dag/cementing_dispatch.dag`. **PB-0 cycle-2 path retirement (PR #3046):** nested `pub mod` surfaces `gunbc_ci`, `integration_rs_wiring_scan`, and `r3_gate_87_cementing_regen_runner_suites` live in this file (same dissolution triggers and APIs; `lib.rs` re-exports them at the prior crate paths for SG-0 census). | --- diff --git a/dsl/gunbc/compiler.dag b/dsl/gunbc/compiler.dag index b0f66d98b3b..43410c76848 100644 --- a/dsl/gunbc/compiler.dag +++ b/dsl/gunbc/compiler.dag @@ -88,7 +88,8 @@ data stage0: GeneratedCrate = { "rust_target.rs", "infer.rs", // R3 gate #6: `lens_testgen.rs` retired — body is this hand-authored include fragment - // (`lens_declaration_apply.rs` `include!`). Must stay in `hand_maintained_src` so stage0 recursive + // (`lens_declaration_apply_body.txt` at crate root, `include!`d from `lib.rs`). Must stay in + // `hand_maintained_src` so stage0 recursive // freshness diff does not treat it as generated drift (P2 boundary discipline). "lens_testgen_body.txt", "lib.rs", diff --git a/src/v3/compiler/src/cementing_dispatch.rs b/src/v3/compiler/src/cementing_dispatch.rs index 4e0257280fd..f5d278e1cdc 100644 --- a/src/v3/compiler/src/cementing_dispatch.rs +++ b/src/v3/compiler/src/cementing_dispatch.rs @@ -21,984 +21,983 @@ //! roster keyed off the register ∩ `regen.dag` projection, with no parallel Rust roster. pub mod integration_rs_wiring_scan { -//! `tests/integration.rs` wiring scanner (Band-C cementing dispatch). -//! -//! SG-0 / INVARIANTS P5: hand-authored Rust under `src/v3/compiler/src/` — keep -//! `EXPECTED_HAND_AUTHORED_NON_TEST` in `tests/integration/sg0_census_test.rs` in lockstep -//! when adding or retiring paths here. -//! -//! **ROADMAP / dissolution:** same **v3 lens capability honesty pass** row as -//! `src/v3/compiler/tests/integration/common/wiring_scanner_test.rs` in `INVARIANTS.md` § SG-0 — -//! Band-C `#[path]` / `mod` wiring is enforced without `syn` until reflected integration facts exist. -//! **Dissolution:** remove this scanner when `tests/integration.rs` cementing module wiring can be -//! validated structurally (no line-oriented scan). Raw/byte string openers in **Code** return -//! `Err` so `CementingDispatchMatchesProjection` surfaces a claim failure instead of aborting the runner. -//! -//! Lives in the compiler crate so `test_runner` can validate -//! `#[path = "integration/cementing/.rs"]` + `mod ;` pairings without -//! duplicating the scanner across test binaries. - -/// `true` when `bytes[i..]` opens a `b"…"`, `br#"…"#`, `r#"…"#`, or `r"…"` literal in -/// Rust source. The wiring scanner does not model these — return an error if they appear -/// in `Code` so Band-C ratchets surface `ClaimResult::Fail` instead of aborting. -fn code_opens_raw_or_byte_string_literal(bytes: &[u8], i: usize) -> bool { - match bytes.get(i) { - Some(b'b') => { - if bytes.get(i + 1) == Some(&b'"') { - return true; + //! `tests/integration.rs` wiring scanner (Band-C cementing dispatch). + //! + //! SG-0 / INVARIANTS P5: hand-authored Rust under `src/v3/compiler/src/` — keep + //! `EXPECTED_HAND_AUTHORED_NON_TEST` in `tests/integration/sg0_census_test.rs` in lockstep + //! when adding or retiring paths here. + //! + //! **ROADMAP / dissolution:** same **v3 lens capability honesty pass** row as + //! `src/v3/compiler/tests/integration/common/wiring_scanner_test.rs` in `INVARIANTS.md` § SG-0 — + //! Band-C `#[path]` / `mod` wiring is enforced without `syn` until reflected integration facts exist. + //! **Dissolution:** remove this scanner when `tests/integration.rs` cementing module wiring can be + //! validated structurally (no line-oriented scan). Raw/byte string openers in **Code** return + //! `Err` so `CementingDispatchMatchesProjection` surfaces a claim failure instead of aborting the runner. + //! + //! Lives in the compiler crate so `test_runner` can validate + //! `#[path = "integration/cementing/.rs"]` + `mod ;` pairings without + //! duplicating the scanner across test binaries. + + /// `true` when `bytes[i..]` opens a `b"…"`, `br#"…"#`, `r#"…"#`, or `r"…"` literal in + /// Rust source. The wiring scanner does not model these — return an error if they appear + /// in `Code` so Band-C ratchets surface `ClaimResult::Fail` instead of aborting. + fn code_opens_raw_or_byte_string_literal(bytes: &[u8], i: usize) -> bool { + match bytes.get(i) { + Some(b'b') => { + if bytes.get(i + 1) == Some(&b'"') { + return true; + } + if bytes.get(i + 1) == Some(&b'r') { + let mut j = i + 2; + while bytes.get(j) == Some(&b'#') { + j += 1; + } + return bytes.get(j) == Some(&b'"'); + } + false } - if bytes.get(i + 1) == Some(&b'r') { - let mut j = i + 2; + Some(b'r') => { + let mut j = i + 1; while bytes.get(j) == Some(&b'#') { j += 1; } - return bytes.get(j) == Some(&b'"'); + bytes.get(j) == Some(&b'"') } - false + _ => false, } - Some(b'r') => { - let mut j = i + 1; - while bytes.get(j) == Some(&b'#') { - j += 1; - } - bytes.get(j) == Some(&b'"') - } - _ => false, } -} -/// Byte index after the last byte of a Rust escape sequence starting at `backslash` -/// (`bytes[backslash] == b'\\'`). Matches suffix forms used in character and byte -/// literals (`\n`, `\t`, `\xNN`, `\u{…}`, …). Unicode escapes use **only** the braced -/// `\u{…}` form in modern Rust; there is no separate `\uXXXX` tail to scan. -fn integration_rs_escape_sequence_end(bytes: &[u8], backslash: usize) -> usize { - debug_assert_eq!(bytes.get(backslash), Some(&b'\\')); - let i = backslash + 1; - if i >= bytes.len() { - return bytes.len(); - } - match bytes[i] { - b'x' => (backslash + 4).min(bytes.len()), - b'u' if bytes.get(i + 1) == Some(&b'{') => { - let mut j = i + 2; - while j < bytes.len() && bytes[j] != b'}' { - j += 1; + /// Byte index after the last byte of a Rust escape sequence starting at `backslash` + /// (`bytes[backslash] == b'\\'`). Matches suffix forms used in character and byte + /// literals (`\n`, `\t`, `\xNN`, `\u{…}`, …). Unicode escapes use **only** the braced + /// `\u{…}` form in modern Rust; there is no separate `\uXXXX` tail to scan. + fn integration_rs_escape_sequence_end(bytes: &[u8], backslash: usize) -> usize { + debug_assert_eq!(bytes.get(backslash), Some(&b'\\')); + let i = backslash + 1; + if i >= bytes.len() { + return bytes.len(); + } + match bytes[i] { + b'x' => (backslash + 4).min(bytes.len()), + b'u' if bytes.get(i + 1) == Some(&b'{') => { + let mut j = i + 2; + while j < bytes.len() && bytes[j] != b'}' { + j += 1; + } + (j + 1).min(bytes.len()) } - (j + 1).min(bytes.len()) + _ => (backslash + 2).min(bytes.len()), } - _ => (backslash + 2).min(bytes.len()), } -} -#[derive(Clone, Copy, PartialEq, Eq)] -enum IntegrationRsScan { - Code, - LineComment, - BlockComment(u32), - String, - /// Inside `b'…'` after consuming the `b'` prefix. - ByteCharLiteral, - /// Inside `'…'` after consuming the opening `'`. - CharLiteral, -} + #[derive(Clone, Copy, PartialEq, Eq)] + enum IntegrationRsScan { + Code, + LineComment, + BlockComment(u32), + String, + /// Inside `b'…'` after consuming the `b'` prefix. + ByteCharLiteral, + /// Inside `'…'` after consuming the opening `'`. + CharLiteral, + } -fn integration_rs_reject_raw_string_in_code(bytes: &[u8], i: usize) -> Result<(), String> { - if code_opens_raw_or_byte_string_literal(bytes, i) { - return Err(format!( + fn integration_rs_reject_raw_string_in_code(bytes: &[u8], i: usize) -> Result<(), String> { + if code_opens_raw_or_byte_string_literal(bytes, i) { + return Err(format!( "integration_rs wiring scan: raw/byte string literal at byte offset {i} \ (e.g. r\"…\", r#\"…\"#, b\"…\", br#\"…\"#). Extend IntegrationRsScan before scanning this file." )); + } + Ok(()) } - Ok(()) -} -/// Byte offsets of `needle` in `integration_rs` that occur in **Code** (outside -/// `//`, `/* */`, `"…"` strings, `b'…'` byte literals, and `'…'` character literals). -/// Lifetimes and labels (`'a`, `'static`) are skipped without entering string/literal -/// states. See [`integration_rs_active_line_contains`]. -fn integration_rs_code_substring_positions( - integration_rs: &str, - needle: &str, -) -> Result, String> { - let mut hits = Vec::new(); - if needle.is_empty() { - hits.push(0); - return Ok(hits); - } - let bytes = integration_rs.as_bytes(); - let mut i = 0usize; - let mut state = IntegrationRsScan::Code; - - while i < bytes.len() { - match state { - IntegrationRsScan::Code => { - integration_rs_reject_raw_string_in_code(bytes, i)?; - if bytes[i] == b'/' && i + 1 < bytes.len() { - if bytes[i + 1] == b'/' { - state = IntegrationRsScan::LineComment; + /// Byte offsets of `needle` in `integration_rs` that occur in **Code** (outside + /// `//`, `/* */`, `"…"` strings, `b'…'` byte literals, and `'…'` character literals). + /// Lifetimes and labels (`'a`, `'static`) are skipped without entering string/literal + /// states. See [`integration_rs_active_line_contains`]. + fn integration_rs_code_substring_positions( + integration_rs: &str, + needle: &str, + ) -> Result, String> { + let mut hits = Vec::new(); + if needle.is_empty() { + hits.push(0); + return Ok(hits); + } + let bytes = integration_rs.as_bytes(); + let mut i = 0usize; + let mut state = IntegrationRsScan::Code; + + while i < bytes.len() { + match state { + IntegrationRsScan::Code => { + integration_rs_reject_raw_string_in_code(bytes, i)?; + if bytes[i] == b'/' && i + 1 < bytes.len() { + if bytes[i + 1] == b'/' { + state = IntegrationRsScan::LineComment; + i += 2; + continue; + } + if bytes[i + 1] == b'*' { + state = IntegrationRsScan::BlockComment(1); + i += 2; + continue; + } + } + if bytes.get(i) == Some(&b'b') && bytes.get(i + 1) == Some(&b'\'') { + state = IntegrationRsScan::ByteCharLiteral; i += 2; continue; } - if bytes[i + 1] == b'*' { - state = IntegrationRsScan::BlockComment(1); - i += 2; + if bytes[i] == b'\'' { + if bytes.get(i + 1) == Some(&b'\\') { + state = IntegrationRsScan::CharLiteral; + i += 1; + continue; + } + if let Some(rest) = integration_rs.get(i + 1..) { + let mut chars = rest.chars(); + if let Some(c) = chars.next() { + let after_char = i + 1 + c.len_utf8(); + if bytes.get(after_char) == Some(&b'\'') { + i = after_char + 1; + continue; + } + } + } + i += 1; + while i < bytes.len() + && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') + { + i += 1; + } continue; } - } - if bytes.get(i) == Some(&b'b') && bytes.get(i + 1) == Some(&b'\'') { - state = IntegrationRsScan::ByteCharLiteral; - i += 2; - continue; - } - if bytes[i] == b'\'' { - if bytes.get(i + 1) == Some(&b'\\') { - state = IntegrationRsScan::CharLiteral; + if bytes[i] == b'"' { + state = IntegrationRsScan::String; i += 1; continue; } - if let Some(rest) = integration_rs.get(i + 1..) { - let mut chars = rest.chars(); - if let Some(c) = chars.next() { - let after_char = i + 1 + c.len_utf8(); - if bytes.get(after_char) == Some(&b'\'') { - i = after_char + 1; - continue; - } - } + if integration_rs[i..].starts_with(needle) { + hits.push(i); } i += 1; - while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') - { - i += 1; + } + IntegrationRsScan::LineComment => { + if bytes[i] == b'\n' { + state = IntegrationRsScan::Code; } - continue; + i += 1; } - if bytes[i] == b'"' { - state = IntegrationRsScan::String; + IntegrationRsScan::BlockComment(depth) => { + if bytes[i] == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'*' { + state = IntegrationRsScan::BlockComment(depth + 1); + i += 2; + continue; + } + if bytes[i] == b'*' && i + 1 < bytes.len() && bytes[i + 1] == b'/' { + let d = depth - 1; + i += 2; + state = if d == 0 { + IntegrationRsScan::Code + } else { + IntegrationRsScan::BlockComment(d) + }; + continue; + } i += 1; - continue; } - if integration_rs[i..].starts_with(needle) { - hits.push(i); + IntegrationRsScan::String => match bytes[i] { + b'\\' => { + i = (i + 2).min(bytes.len()); + } + b'"' => { + i += 1; + state = IntegrationRsScan::Code; + } + _ => { + i += 1; + } + }, + IntegrationRsScan::ByteCharLiteral => { + if bytes.get(i) == Some(&b'\\') { + i = integration_rs_escape_sequence_end(bytes, i); + } else { + i += 1; + } + if bytes.get(i) == Some(&b'\'') { + i += 1; + } + state = IntegrationRsScan::Code; } - i += 1; - } - IntegrationRsScan::LineComment => { - if bytes[i] == b'\n' { + IntegrationRsScan::CharLiteral => { + if bytes.get(i) == Some(&b'\\') { + i = integration_rs_escape_sequence_end(bytes, i); + } else if let Some(rest) = integration_rs.get(i..) { + let mut ci = rest.chars(); + if let Some(c) = ci.next() { + i += c.len_utf8(); + } else { + state = IntegrationRsScan::Code; + continue; + } + } else { + state = IntegrationRsScan::Code; + continue; + } + if bytes.get(i) == Some(&b'\'') { + i += 1; + } state = IntegrationRsScan::Code; } - i += 1; } - IntegrationRsScan::BlockComment(depth) => { - if bytes[i] == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'*' { - state = IntegrationRsScan::BlockComment(depth + 1); - i += 2; + } + Ok(hits) + } + + /// Consume one outer `#[ … ]` attribute (handles quoted `"` regions and nested `[` `]`). + fn skip_rust_attribute(s: &str) -> Option<&str> { + if !s.starts_with("#[") { + return None; + } + let bytes = s.as_bytes(); + let mut i = 1usize; + let mut depth = 0usize; + let mut in_string = false; + let mut escape = false; + while i < bytes.len() { + let b = bytes[i]; + if in_string { + if escape { + escape = false; + i += 1; continue; } - if bytes[i] == b'*' && i + 1 < bytes.len() && bytes[i + 1] == b'/' { - let d = depth - 1; - i += 2; - state = if d == 0 { - IntegrationRsScan::Code - } else { - IntegrationRsScan::BlockComment(d) - }; + if b == b'\\' { + escape = true; + i += 1; continue; } + if b == b'"' { + in_string = false; + } i += 1; + continue; } - IntegrationRsScan::String => match bytes[i] { - b'\\' => { - i = (i + 2).min(bytes.len()); - } + match b { b'"' => { + in_string = true; i += 1; - state = IntegrationRsScan::Code; } - _ => { + b'[' => { + depth += 1; i += 1; } - }, - IntegrationRsScan::ByteCharLiteral => { - if bytes.get(i) == Some(&b'\\') { - i = integration_rs_escape_sequence_end(bytes, i); - } else { + b']' => { + depth = depth.saturating_sub(1); i += 1; - } - if bytes.get(i) == Some(&b'\'') { - i += 1; - } - state = IntegrationRsScan::Code; - } - IntegrationRsScan::CharLiteral => { - if bytes.get(i) == Some(&b'\\') { - i = integration_rs_escape_sequence_end(bytes, i); - } else if let Some(rest) = integration_rs.get(i..) { - let mut ci = rest.chars(); - if let Some(c) = ci.next() { - i += c.len_utf8(); - } else { - state = IntegrationRsScan::Code; - continue; + if depth == 0 { + return Some(&s[i..]); } - } else { - state = IntegrationRsScan::Code; - continue; } - if bytes.get(i) == Some(&b'\'') { + _ => { i += 1; } - state = IntegrationRsScan::Code; } } + None } - Ok(hits) -} -/// Consume one outer `#[ … ]` attribute (handles quoted `"` regions and nested `[` `]`). -fn skip_rust_attribute(s: &str) -> Option<&str> { - if !s.starts_with("#[") { - return None; - } - let bytes = s.as_bytes(); - let mut i = 1usize; - let mut depth = 0usize; - let mut in_string = false; - let mut escape = false; - while i < bytes.len() { - let b = bytes[i]; - if in_string { - if escape { - escape = false; - i += 1; - continue; + /// After a `#[path = "integration/cementing/.rs"]` attribute in Code, the next + /// Rust item must be `mod ;` (optionally separated by whitespace, newlines, or + /// additional `#[…]` attributes). + fn mod_decl_follows_cementing_path_tail(mut tail: &str, stem: &str) -> bool { + let prefix = format!("mod {stem}"); + loop { + tail = tail.trim_start(); + if tail.is_empty() { + return false; } - if b == b'\\' { - escape = true; - i += 1; + if tail.starts_with("#[") { + let Some(rest) = skip_rust_attribute(tail) else { + return false; + }; + tail = rest; continue; } - if b == b'"' { - in_string = false; - } - i += 1; - continue; - } - match b { - b'"' => { - in_string = true; - i += 1; - } - b'[' => { - depth += 1; - i += 1; - } - b']' => { - depth = depth.saturating_sub(1); - i += 1; - if depth == 0 { - return Some(&s[i..]); - } + if !tail.starts_with(&prefix) { + return false; } - _ => { - i += 1; + let after_prefix = &tail[prefix.len()..]; + if after_prefix + .chars() + .next() + .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_') + { + // e.g. `mod stem` must not match `mod stem_extra`. + return false; } + let after = after_prefix.trim_start(); + return after.starts_with(';'); } } - None -} -/// After a `#[path = "integration/cementing/.rs"]` attribute in Code, the next -/// Rust item must be `mod ;` (optionally separated by whitespace, newlines, or -/// additional `#[…]` attributes). -fn mod_decl_follows_cementing_path_tail(mut tail: &str, stem: &str) -> bool { - let prefix = format!("mod {stem}"); - loop { - tail = tail.trim_start(); - if tail.is_empty() { - return false; - } - if tail.starts_with("#[") { - let Some(rest) = skip_rust_attribute(tail) else { - return false; + /// `true` when `integration_rs` contains `#[path = "integration/cementing/.rs"]` + /// in **Code** and that attribute is immediately followed (per + /// [`mod_decl_follows_cementing_path_tail`]) by `mod ;`. + /// + /// This is stricter than substring checks for `#[path]` and `mod` separately — it + /// rejects a mismatched `#[path]` on one module paired with a stray `mod ;` + /// elsewhere (`TESTING.md` Band-C dispatch). + pub fn integration_rs_cementing_path_attr_binds_mod_stem( + integration_rs: &str, + stem: &str, + ) -> Result { + let path_attr = format!(r#"#[path = "integration/cementing/{stem}.rs"]"#); + for pos in integration_rs_code_substring_positions(integration_rs, &path_attr)? { + let Some(tail) = integration_rs.get(pos + path_attr.len()..) else { + continue; }; - tail = rest; - continue; - } - if !tail.starts_with(&prefix) { - return false; - } - let after_prefix = &tail[prefix.len()..]; - if after_prefix - .chars() - .next() - .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_') - { - // e.g. `mod stem` must not match `mod stem_extra`. - return false; + if mod_decl_follows_cementing_path_tail(tail, stem) { + return Ok(true); + } } - let after = after_prefix.trim_start(); - return after.starts_with(';'); + Ok(false) } -} -/// `true` when `integration_rs` contains `#[path = "integration/cementing/.rs"]` -/// in **Code** and that attribute is immediately followed (per -/// [`mod_decl_follows_cementing_path_tail`]) by `mod ;`. -/// -/// This is stricter than substring checks for `#[path]` and `mod` separately — it -/// rejects a mismatched `#[path]` on one module paired with a stray `mod ;` -/// elsewhere (`TESTING.md` Band-C dispatch). -pub fn integration_rs_cementing_path_attr_binds_mod_stem( - integration_rs: &str, - stem: &str, -) -> Result { - let path_attr = format!(r#"#[path = "integration/cementing/{stem}.rs"]"#); - for pos in integration_rs_code_substring_positions(integration_rs, &path_attr)? { - let Some(tail) = integration_rs.get(pos + path_attr.len()..) else { - continue; - }; - if mod_decl_follows_cementing_path_tail(tail, stem) { - return Ok(true); - } + /// True when `needle` appears in `integration_rs` **outside** Rust line comments + /// (`//` … including `///` / `//!`), **nested** block comments (`/* … */`), normal + /// `"…"` string literals, `b'…'` byte character literals, and `'…'` character + /// literals (excluding lifetimes / labels such as `'a` or `'static`). + /// + /// Band-C wiring ratchets use this so `#[path = …]` / `mod …;` matches cannot + /// false-green on commented-out or string-embedded copies. Needles are ASCII + /// (`#[path`, `mod foo`); the scan is byte-oriented on UTF-8 boundaries. + /// + /// **Not handled:** raw strings (`r#"…"#`), byte strings (`b"…"`, `br#"…"#`). If those appear in + /// scanned **Code**, this returns `Err` with an explicit message (so `TestPredicate` callers can + /// map to `ClaimResult::Fail`) until `IntegrationRsScan` is widened for those forms. + pub fn integration_rs_active_line_contains( + integration_rs: &str, + needle: &str, + ) -> Result { + Ok(!integration_rs_code_substring_positions(integration_rs, needle)?.is_empty()) } - Ok(false) -} - -/// True when `needle` appears in `integration_rs` **outside** Rust line comments -/// (`//` … including `///` / `//!`), **nested** block comments (`/* … */`), normal -/// `"…"` string literals, `b'…'` byte character literals, and `'…'` character -/// literals (excluding lifetimes / labels such as `'a` or `'static`). -/// -/// Band-C wiring ratchets use this so `#[path = …]` / `mod …;` matches cannot -/// false-green on commented-out or string-embedded copies. Needles are ASCII -/// (`#[path`, `mod foo`); the scan is byte-oriented on UTF-8 boundaries. -/// -/// **Not handled:** raw strings (`r#"…"#`), byte strings (`b"…"`, `br#"…"#`). If those appear in -/// scanned **Code**, this returns `Err` with an explicit message (so `TestPredicate` callers can -/// map to `ClaimResult::Fail`) until `IntegrationRsScan` is widened for those forms. -pub fn integration_rs_active_line_contains( - integration_rs: &str, - needle: &str, -) -> Result { - Ok(!integration_rs_code_substring_positions(integration_rs, needle)?.is_empty()) -} - } pub mod r3_gate_87_cementing_regen_runner_suites { -//! R3 gate #87 — PB-B-1 runner table for `tests/dag/t_r3_gate_87_cementing_regen_*.dag` harnesses. -//! -//! **INVARIANTS P2 / single authority:** `R3_GATE_87_CEMENTING_REGEN_SUITES` is the only merge-visible -//! inventory for which harnesses `t_pb_b_1_dag_runner_test` executes. `cementing_dispatch` and -//! `r3_gate_87_lens_cementing_regen_receipts_test` consume the same table so `.dag` Band-C receipts -//! cannot pass `CementingDispatchMatchesProjection` without a matching runner row. - -use std::collections::BTreeSet; -use std::path::Path; - -/// `LensRegistryEntry.name` values implied by [`R3_GATE_87_CEMENTING_REGEN_SUITES`] harness `file` -/// paths (`t_r3_gate_87_cementing_regen_.dag`). **Single authority** for the gate-#87 -/// inventory ratchet: `r3_gate_87_lens_cementing_regen_receipts_test` compares live `regen.dag` -/// names against this set so a new registry row cannot ship without a matching runner row. -pub fn r3_gate_87_cementing_regen_lens_names_for_runner_table() -> BTreeSet { - R3_GATE_87_CEMENTING_REGEN_SUITES - .iter() - .map(|(_, file, _, _)| lens_name_from_gate_87_harness_path(file)) - .collect() -} + //! R3 gate #87 — PB-B-1 runner table for `tests/dag/t_r3_gate_87_cementing_regen_*.dag` harnesses. + //! + //! **INVARIANTS P2 / single authority:** `R3_GATE_87_CEMENTING_REGEN_SUITES` is the only merge-visible + //! inventory for which harnesses `t_pb_b_1_dag_runner_test` executes. `cementing_dispatch` and + //! `r3_gate_87_lens_cementing_regen_receipts_test` consume the same table so `.dag` Band-C receipts + //! cannot pass `CementingDispatchMatchesProjection` without a matching runner row. + + use std::collections::BTreeSet; + use std::path::Path; + + /// `LensRegistryEntry.name` values implied by [`R3_GATE_87_CEMENTING_REGEN_SUITES`] harness `file` + /// paths (`t_r3_gate_87_cementing_regen_.dag`). **Single authority** for the gate-#87 + /// inventory ratchet: `r3_gate_87_lens_cementing_regen_receipts_test` compares live `regen.dag` + /// names against this set so a new registry row cannot ship without a matching runner row. + pub fn r3_gate_87_cementing_regen_lens_names_for_runner_table() -> BTreeSet { + R3_GATE_87_CEMENTING_REGEN_SUITES + .iter() + .map(|(_, file, _, _)| lens_name_from_gate_87_harness_path(file)) + .collect() + } -/// Module stems (`tests/dag/.dag`) enumerated in [`R3_GATE_87_CEMENTING_REGEN_SUITES`]. -/// [`crate::cementing_dispatch::evaluate_cementing_dispatch_projection`] requires every -/// `kind == "dag"` receipt in `cementing_dispatch.dag` to use one of these stems so the dispatch -/// list cannot drift from the T-PB-B-1 runner. -pub fn r3_gate_87_cementing_regen_pb_b1_dag_module_stems() -> BTreeSet { - R3_GATE_87_CEMENTING_REGEN_SUITES - .iter() - .map(|(_, file, _, _)| { - Path::new(file) - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or_else(|| { - panic!("R3_GATE_87_CEMENTING_REGEN_SUITES: invalid harness path `{file}`") - }) - .to_string() - }) - .collect() -} + /// Module stems (`tests/dag/.dag`) enumerated in [`R3_GATE_87_CEMENTING_REGEN_SUITES`]. + /// [`crate::cementing_dispatch::evaluate_cementing_dispatch_projection`] requires every + /// `kind == "dag"` receipt in `cementing_dispatch.dag` to use one of these stems so the dispatch + /// list cannot drift from the T-PB-B-1 runner. + pub fn r3_gate_87_cementing_regen_pb_b1_dag_module_stems() -> BTreeSet { + R3_GATE_87_CEMENTING_REGEN_SUITES + .iter() + .map(|(_, file, _, _)| { + Path::new(file) + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or_else(|| { + panic!("R3_GATE_87_CEMENTING_REGEN_SUITES: invalid harness path `{file}`") + }) + .to_string() + }) + .collect() + } -// R3 gate #87 — every `LensRegistryEntry` in `src/v3/compiler/regen.dag` has a -// `src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_.dag` harness evaluated here. -const R3_GATE_87_CEMENTING_HARNESS_PATH_PREFIX: &str = - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_"; -const R3_GATE_87_CEMENTING_HARNESS_PATH_SUFFIX: &str = ".dag"; - -fn lens_name_from_gate_87_harness_path(file: &str) -> String { - file.strip_prefix(R3_GATE_87_CEMENTING_HARNESS_PATH_PREFIX) - .and_then(|rest| rest.strip_suffix(R3_GATE_87_CEMENTING_HARNESS_PATH_SUFFIX)) - .unwrap_or_else(|| { - panic!( - "R3_GATE_87_CEMENTING_REGEN_SUITES: harness path must be \ + // R3 gate #87 — every `LensRegistryEntry` in `src/v3/compiler/regen.dag` has a + // `src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_.dag` harness evaluated here. + const R3_GATE_87_CEMENTING_HARNESS_PATH_PREFIX: &str = + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_"; + const R3_GATE_87_CEMENTING_HARNESS_PATH_SUFFIX: &str = ".dag"; + + fn lens_name_from_gate_87_harness_path(file: &str) -> String { + file.strip_prefix(R3_GATE_87_CEMENTING_HARNESS_PATH_PREFIX) + .and_then(|rest| rest.strip_suffix(R3_GATE_87_CEMENTING_HARNESS_PATH_SUFFIX)) + .unwrap_or_else(|| { + panic!( + "R3_GATE_87_CEMENTING_REGEN_SUITES: harness path must be \ `{prefix}{suffix}`, got `{file}`", - prefix = R3_GATE_87_CEMENTING_HARNESS_PATH_PREFIX, - suffix = R3_GATE_87_CEMENTING_HARNESS_PATH_SUFFIX, - ) - }) - .to_string() -} - -pub const R3_GATE_87_CEMENTING_REGEN_SUITES: &[(&str, &str, &str, &[&str])] = &[ - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_cost.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_cost.dag", - "r3_gate_87_cementing_regen_cost_suite", - &[ - "cementing_regen_cost_merge_sort_lens_output_equals", - "cementing_regen_cost", - ], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_cost_symbolic.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_cost_symbolic.dag", - "r3_gate_87_cementing_regen_cost_symbolic_suite", - &[ - "cementing_regen_cost_symbolic", - "cementing_regen_cost_symbolic_countdown", - ], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_cost_target_realization.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_cost_target_realization.dag", - "r3_gate_87_cementing_regen_cost_target_realization_suite", - &["cementing_regen_cost_target_realization"], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_effect_enumeration.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_effect_enumeration.dag", - "r3_gate_87_cementing_regen_effect_enumeration_suite", - &["cementing_regen_effect_enumeration"], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_infer_helpers.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_infer_helpers.dag", - "r3_gate_87_cementing_regen_infer_helpers_suite", - &["cementing_regen_infer_helpers"], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_lower_helpers.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_lower_helpers.dag", - "r3_gate_87_cementing_regen_lower_helpers_suite", - &["cementing_regen_lower_helpers"], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_provenance.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_provenance.dag", - "r3_gate_87_cementing_regen_provenance_suite", - &["cementing_regen_provenance"], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_structural_resolution.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_structural_resolution.dag", - "r3_gate_87_cementing_regen_structural_resolution_suite", - &["cementing_regen_structural_resolution"], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_unused_parameters.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_unused_parameters.dag", - "r3_gate_87_cementing_regen_unused_parameters_suite", - &["cementing_regen_unused_parameters"], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_variant_payload.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_variant_payload.dag", - "r3_gate_87_cementing_regen_variant_payload_suite", - &["cementing_regen_variant_payload"], - ), -]; + prefix = R3_GATE_87_CEMENTING_HARNESS_PATH_PREFIX, + suffix = R3_GATE_87_CEMENTING_HARNESS_PATH_SUFFIX, + ) + }) + .to_string() + } + pub const R3_GATE_87_CEMENTING_REGEN_SUITES: &[(&str, &str, &str, &[&str])] = &[ + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_cost.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_cost.dag", + "r3_gate_87_cementing_regen_cost_suite", + &[ + "cementing_regen_cost_merge_sort_lens_output_equals", + "cementing_regen_cost", + ], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_cost_symbolic.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_cost_symbolic.dag", + "r3_gate_87_cementing_regen_cost_symbolic_suite", + &[ + "cementing_regen_cost_symbolic", + "cementing_regen_cost_symbolic_countdown", + ], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_cost_target_realization.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_cost_target_realization.dag", + "r3_gate_87_cementing_regen_cost_target_realization_suite", + &["cementing_regen_cost_target_realization"], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_effect_enumeration.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_effect_enumeration.dag", + "r3_gate_87_cementing_regen_effect_enumeration_suite", + &["cementing_regen_effect_enumeration"], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_infer_helpers.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_infer_helpers.dag", + "r3_gate_87_cementing_regen_infer_helpers_suite", + &["cementing_regen_infer_helpers"], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_lower_helpers.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_lower_helpers.dag", + "r3_gate_87_cementing_regen_lower_helpers_suite", + &["cementing_regen_lower_helpers"], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_provenance.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_provenance.dag", + "r3_gate_87_cementing_regen_provenance_suite", + &["cementing_regen_provenance"], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_structural_resolution.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_structural_resolution.dag", + "r3_gate_87_cementing_regen_structural_resolution_suite", + &["cementing_regen_structural_resolution"], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_unused_parameters.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_unused_parameters.dag", + "r3_gate_87_cementing_regen_unused_parameters_suite", + &["cementing_regen_unused_parameters"], + ), + ( + include_str!("../tests/dag/t_r3_gate_87_cementing_regen_variant_payload.dag"), + "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_variant_payload.dag", + "r3_gate_87_cementing_regen_variant_payload_suite", + &["cementing_regen_variant_payload"], + ), + ]; } pub mod gunbc_ci { -//! `gunbc.ci` workflow gate selection — affected-set dispatch substrate. -//! -//! **Authority** (Phase B): `docs/briefs/r3-wave1-s6-slice7-affected-set-impl-worker.md`; -//! fail-closed + obligation closure: `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §3–§4; -//! carrier shape: `dsl/gunbc/ci.dag` (`CIWorkflowDag`, `CIGateEdge`: `from` is a prerequisite of `to`). -//! -//! This module is intentionally **pure**: every decision is a function of -//! [`CiWorkflowDagInput`] + [`CiWorkflowDiff`] only. Git diffs, path-regex, env, -//! and PR #2713 lens receipts are upstream responsibilities; they must be -//! mapped into [`CiWorkflowDiff`] (or a superset-equivalent touch set) before -//! calling [`select_affected_gates`]. -//! -//! **Verifier ratchet witness (Phase C scaffolding)** — `docs/design-ci-workflow-substrate-shape-2026-05-12.md` -//! S5 + prequeue §5.2: monotone **inclusion** under enlarging the touched-id -//! set (smaller touch ⊆ larger touch ⇒ selected subset ⊆ selected superset). -//! See [`selection_subset_under_touch_set_growth`]. -//! -//! **Expansion semantics:** [`select_affected_gates`] walks each `CIGateEdge` **symmetrically** -//! (treat `from → to` as an undirected adjacency) to fixpoint, so a downstream obligation -//! pulls in its sibling prerequisites (e.g. touching `lint` selects `l1-ratchet`, which -//! forces `tests` because both parents are prerequisites of `l1-ratchet`). The returned -//! order is a **topological sort** of that vertex set on the directed prerequisite subgraph. -//! -//! **Global carrier validity:** before expanding any touch seed, [`select_affected_gates`] requires -//! the **entire** directed prerequisite graph on [`CiWorkflowDagInput::gates`] to be acyclic. A cycle -//! confined to an unselected weak component must still fail closed (P3), not succeed in “narrow” mode. - -use std::collections::{BTreeSet, HashMap, HashSet, VecDeque}; - -/// Structural mirror of `CIGate.id` in `dsl/gunbc/ci.dag`. -pub type CiGateId = String; - -/// Structural mirror of `CIGate` in `dsl/gunbc/ci.dag`. -/// -/// [`select_affected_gates`] consults only [`CiGateMeta::id`]. [`CiGateMeta::blocking`] is carried -/// for **carrier parity** with the DSL record so BinaryShim / dispatch wiring can map rows without -/// a forked struct; merge-blocking vs advisory semantics stay **outside** this selection substrate -/// until a consumer reads the field. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CiGateMeta { - pub id: CiGateId, - pub blocking: bool, -} + //! `gunbc.ci` workflow gate selection — affected-set dispatch substrate. + //! + //! **Authority** (Phase B): `docs/briefs/r3-wave1-s6-slice7-affected-set-impl-worker.md`; + //! fail-closed + obligation closure: `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §3–§4; + //! carrier shape: `dsl/gunbc/ci.dag` (`CIWorkflowDag`, `CIGateEdge`: `from` is a prerequisite of `to`). + //! + //! This module is intentionally **pure**: every decision is a function of + //! [`CiWorkflowDagInput`] + [`CiWorkflowDiff`] only. Git diffs, path-regex, env, + //! and PR #2713 lens receipts are upstream responsibilities; they must be + //! mapped into [`CiWorkflowDiff`] (or a superset-equivalent touch set) before + //! calling [`select_affected_gates`]. + //! + //! **Verifier ratchet witness (Phase C scaffolding)** — `docs/design-ci-workflow-substrate-shape-2026-05-12.md` + //! S5 + prequeue §5.2: monotone **inclusion** under enlarging the touched-id + //! set (smaller touch ⊆ larger touch ⇒ selected subset ⊆ selected superset). + //! See [`selection_subset_under_touch_set_growth`]. + //! + //! **Expansion semantics:** [`select_affected_gates`] walks each `CIGateEdge` **symmetrically** + //! (treat `from → to` as an undirected adjacency) to fixpoint, so a downstream obligation + //! pulls in its sibling prerequisites (e.g. touching `lint` selects `l1-ratchet`, which + //! forces `tests` because both parents are prerequisites of `l1-ratchet`). The returned + //! order is a **topological sort** of that vertex set on the directed prerequisite subgraph. + //! + //! **Global carrier validity:** before expanding any touch seed, [`select_affected_gates`] requires + //! the **entire** directed prerequisite graph on [`CiWorkflowDagInput::gates`] to be acyclic. A cycle + //! confined to an unselected weak component must still fail closed (P3), not succeed in “narrow” mode. + + use std::collections::{BTreeSet, HashMap, HashSet, VecDeque}; + + /// Structural mirror of `CIGate.id` in `dsl/gunbc/ci.dag`. + pub type CiGateId = String; + + /// Structural mirror of `CIGate` in `dsl/gunbc/ci.dag`. + /// + /// [`select_affected_gates`] consults only [`CiGateMeta::id`]. [`CiGateMeta::blocking`] is carried + /// for **carrier parity** with the DSL record so BinaryShim / dispatch wiring can map rows without + /// a forked struct; merge-blocking vs advisory semantics stay **outside** this selection substrate + /// until a consumer reads the field. + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct CiGateMeta { + pub id: CiGateId, + pub blocking: bool, + } -/// Structural mirror of `CIWorkflowDag`: gate roster + prerequisite edges. -/// -/// Each edge `(from, to)` means **`from` must complete before `to`** (matches -/// `CIGateEdge { from, to }` in `ci.dag`). -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct CiWorkflowDagInput { - pub gates: Vec, - /// Prerequisite edges: `from` runs before `to`. - pub edges: Vec<(CiGateId, CiGateId)>, -} + /// Structural mirror of `CIWorkflowDag`: gate roster + prerequisite edges. + /// + /// Each edge `(from, to)` means **`from` must complete before `to`** (matches + /// `CIGateEdge { from, to }` in `ci.dag`). + #[derive(Debug, Clone, Default, PartialEq, Eq)] + pub struct CiWorkflowDagInput { + pub gates: Vec, + /// Prerequisite edges: `from` runs before `to`. + pub edges: Vec<(CiGateId, CiGateId)>, + } -// Practice 4 (coproduct checkpoint, `docs/modeling-discipline.md` §4): -// 🟢 GREEN — terminal upstream-policy boundary for gate-id touch semantics before -// `select_affected_gates`: `TouchAll` is the conservative in-DAG superset seed; -// `TouchedGates` is the explicit finite set upstream mapped from lens/git facts. -// Ledger: no third authority here — path-regex, env, and PR #2713 receipts stay -// outside this module (Slice 7 brief + affected-set selection canvas §1). -/// Diff-against-base input projected to **gate ids** (single authority at this boundary). -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CiWorkflowDiff { - /// Every gate id in [`CiWorkflowDagInput::gates`] is treated as directly touched. - TouchAll, - /// Upstream bundle names gate ids with proven or assumed direct impact. - TouchedGates(BTreeSet), -} + // Practice 4 (coproduct checkpoint, `docs/modeling-discipline.md` §4): + // 🟢 GREEN — terminal upstream-policy boundary for gate-id touch semantics before + // `select_affected_gates`: `TouchAll` is the conservative in-DAG superset seed; + // `TouchedGates` is the explicit finite set upstream mapped from lens/git facts. + // Ledger: no third authority here — path-regex, env, and PR #2713 receipts stay + // outside this module (Slice 7 brief + affected-set selection canvas §1). + /// Diff-against-base input projected to **gate ids** (single authority at this boundary). + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum CiWorkflowDiff { + /// Every gate id in [`CiWorkflowDagInput::gates`] is treated as directly touched. + TouchAll, + /// Upstream bundle names gate ids with proven or assumed direct impact. + TouchedGates(BTreeSet), + } -// Practice 4 (coproduct checkpoint, `docs/modeling-discipline.md` §4): -// 🟢 GREEN — terminal typed failure surface for [`select_affected_gates`]: malformed `CIWorkflowDag` -// carrier vs non-acyclic directed prerequisites (full roster graph and/or selected subset; no silent success). -// Ledger: (1) fact placement — not split across consumers; this `Result` boundary owns the `Err` -// coproduct. (2) variant-is-data — rejected; payloads differ structurally. (3) algebraic — not -// `std/` refs. (4) dimensional — not one decomposed axis space; each variant is an irreducible -// outcome at the `dsl/gunbc/ci.dag` structural mirror until `.dag`-owned dispatch absorbs diagnostics. -/// [`select_affected_gates`] failed: malformed `CIWorkflowDag` carrier or non-acyclic prerequisites. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CiAffectedGatesError { - /// The same `CIGate.id` appears more than once in [`CiWorkflowDagInput::gates`] (malformed carrier; must not collapse into a `HashSet` silently). - DuplicateGateRosterId { id: CiGateId }, - /// `CIGateEdge` names a gate id not present in [`CiWorkflowDagInput::gates`] (violates single roster authority in `dsl/gunbc/ci.dag`). - UnknownEdgeEndpoint { from: CiGateId, to: CiGateId }, - /// The directed prerequisite graph is not a DAG: either the **full** roster graph from - /// [`CiWorkflowDagInput::edges`] contains a cycle, or the selected vertex set’s induced subgraph - /// does (broken indegree accounting). - NonAcyclicPrerequisiteGraph, -} + // Practice 4 (coproduct checkpoint, `docs/modeling-discipline.md` §4): + // 🟢 GREEN — terminal typed failure surface for [`select_affected_gates`]: malformed `CIWorkflowDag` + // carrier vs non-acyclic directed prerequisites (full roster graph and/or selected subset; no silent success). + // Ledger: (1) fact placement — not split across consumers; this `Result` boundary owns the `Err` + // coproduct. (2) variant-is-data — rejected; payloads differ structurally. (3) algebraic — not + // `std/` refs. (4) dimensional — not one decomposed axis space; each variant is an irreducible + // outcome at the `dsl/gunbc/ci.dag` structural mirror until `.dag`-owned dispatch absorbs diagnostics. + /// [`select_affected_gates`] failed: malformed `CIWorkflowDag` carrier or non-acyclic prerequisites. + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum CiAffectedGatesError { + /// The same `CIGate.id` appears more than once in [`CiWorkflowDagInput::gates`] (malformed carrier; must not collapse into a `HashSet` silently). + DuplicateGateRosterId { id: CiGateId }, + /// `CIGateEdge` names a gate id not present in [`CiWorkflowDagInput::gates`] (violates single roster authority in `dsl/gunbc/ci.dag`). + UnknownEdgeEndpoint { from: CiGateId, to: CiGateId }, + /// The directed prerequisite graph is not a DAG: either the **full** roster graph from + /// [`CiWorkflowDagInput::edges`] contains a cycle, or the selected vertex set’s induced subgraph + /// does (broken indegree accounting). + NonAcyclicPrerequisiteGraph, + } -/// On success, returns gate ids to execute: the **connected component** of the seed under the symmetric -/// closure of prerequisite edges (iterate `from ↔ to` adjacency to fixpoint), then -/// **topologically sorted** on the directed subgraph (prerequisites first). -/// -/// **Fail-closed** (`design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §3): -/// any touched id not present in `dag.gates` forces **all** gates listed in `dag.gates` -/// into the plan (superset within this DAG). -/// -/// **Empty diff** (`TouchedGates` empty): returns `Ok` of an **empty** plan — callers map that -/// to “no merge-blocking re-verify” vs “full superset” per repository policy **outside** -/// this function (this module does not read policy carriers). -/// -/// **Malformed carrier:** duplicate [`CiGateMeta::id`] values in [`CiWorkflowDagInput::gates`] -/// yield [`CiAffectedGatesError::DuplicateGateRosterId`] (roster must be unique before any `HashSet` -/// view so gates are not dropped silently). Any edge whose `from` or `to` is absent from that roster -/// yields [`CiAffectedGatesError::UnknownEdgeEndpoint`]. Any directed cycle in the **full** -/// prerequisite graph on the roster (not only the touch-selected component) yields -/// [`CiAffectedGatesError::NonAcyclicPrerequisiteGraph`] instead of returning `Ok` in narrow mode. -pub fn select_affected_gates( - dag: &CiWorkflowDagInput, - diff: &CiWorkflowDiff, -) -> Result, CiAffectedGatesError> { - validate_unique_gate_roster(dag)?; - let known: HashSet<&str> = dag.gates.iter().map(|g| g.id.as_str()).collect(); - for (from, to) in &dag.edges { - if !known.contains(from.as_str()) || !known.contains(to.as_str()) { - return Err(CiAffectedGatesError::UnknownEdgeEndpoint { - from: from.clone(), - to: to.clone(), - }); + /// On success, returns gate ids to execute: the **connected component** of the seed under the symmetric + /// closure of prerequisite edges (iterate `from ↔ to` adjacency to fixpoint), then + /// **topologically sorted** on the directed subgraph (prerequisites first). + /// + /// **Fail-closed** (`design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §3): + /// any touched id not present in `dag.gates` forces **all** gates listed in `dag.gates` + /// into the plan (superset within this DAG). + /// + /// **Empty diff** (`TouchedGates` empty): returns `Ok` of an **empty** plan — callers map that + /// to “no merge-blocking re-verify” vs “full superset” per repository policy **outside** + /// this function (this module does not read policy carriers). + /// + /// **Malformed carrier:** duplicate [`CiGateMeta::id`] values in [`CiWorkflowDagInput::gates`] + /// yield [`CiAffectedGatesError::DuplicateGateRosterId`] (roster must be unique before any `HashSet` + /// view so gates are not dropped silently). Any edge whose `from` or `to` is absent from that roster + /// yields [`CiAffectedGatesError::UnknownEdgeEndpoint`]. Any directed cycle in the **full** + /// prerequisite graph on the roster (not only the touch-selected component) yields + /// [`CiAffectedGatesError::NonAcyclicPrerequisiteGraph`] instead of returning `Ok` in narrow mode. + pub fn select_affected_gates( + dag: &CiWorkflowDagInput, + diff: &CiWorkflowDiff, + ) -> Result, CiAffectedGatesError> { + validate_unique_gate_roster(dag)?; + let known: HashSet<&str> = dag.gates.iter().map(|g| g.id.as_str()).collect(); + for (from, to) in &dag.edges { + if !known.contains(from.as_str()) || !known.contains(to.as_str()) { + return Err(CiAffectedGatesError::UnknownEdgeEndpoint { + from: from.clone(), + to: to.clone(), + }); + } } - } - // Fail-closed on the whole carrier: a cycle in an unselected component must not yield `Ok` - // for a disjoint touch seed (codex / P3). - topo_sort_subset(dag, &known)?; + // Fail-closed on the whole carrier: a cycle in an unselected component must not yield `Ok` + // for a disjoint touch seed (codex / P3). + topo_sort_subset(dag, &known)?; - let force_superset = match diff { - CiWorkflowDiff::TouchAll => false, - CiWorkflowDiff::TouchedGates(ids) => ids.iter().any(|id| !known.contains(id.as_str())), - }; + let force_superset = match diff { + CiWorkflowDiff::TouchAll => false, + CiWorkflowDiff::TouchedGates(ids) => ids.iter().any(|id| !known.contains(id.as_str())), + }; - let seed: HashSet<&str> = if force_superset { - known.clone() - } else { - match diff { - CiWorkflowDiff::TouchAll => known.clone(), - CiWorkflowDiff::TouchedGates(ids) => ids.iter().map(|s| s.as_str()).collect(), - } - }; + let seed: HashSet<&str> = if force_superset { + known.clone() + } else { + match diff { + CiWorkflowDiff::TouchAll => known.clone(), + CiWorkflowDiff::TouchedGates(ids) => ids.iter().map(|s| s.as_str()).collect(), + } + }; - if seed.is_empty() { - return Ok(Vec::new()); - } + if seed.is_empty() { + return Ok(Vec::new()); + } - // Symmetric edge closure: expand along `from ↔ to` until stable, then topo-sort - // the induced set on directed prerequisite edges. - let mut selected: HashSet<&str> = seed.clone(); - loop { - let before = selected.len(); - for (from, to) in &dag.edges { - let from = from.as_str(); - let to = to.as_str(); - if selected.contains(from) { - selected.insert(to); + // Symmetric edge closure: expand along `from ↔ to` until stable, then topo-sort + // the induced set on directed prerequisite edges. + let mut selected: HashSet<&str> = seed.clone(); + loop { + let before = selected.len(); + for (from, to) in &dag.edges { + let from = from.as_str(); + let to = to.as_str(); + if selected.contains(from) { + selected.insert(to); + } + if selected.contains(to) { + selected.insert(from); + } } - if selected.contains(to) { - selected.insert(from); + if selected.len() == before { + break; } } - if selected.len() == before { - break; - } - } - topo_sort_subset(dag, &selected) -} - -fn validate_unique_gate_roster(dag: &CiWorkflowDagInput) -> Result<(), CiAffectedGatesError> { - let mut seen: HashSet<&str> = HashSet::new(); - for g in &dag.gates { - if !seen.insert(g.id.as_str()) { - return Err(CiAffectedGatesError::DuplicateGateRosterId { id: g.id.clone() }); - } + topo_sort_subset(dag, &selected) } - Ok(()) -} -/// Set inclusion witness for ratchet tests. -/// -/// Returns whether `select_affected_gates(dag, narrow)` ⊆ `select_affected_gates(dag, wide)` as sets -/// when **both** calls succeed; if either returns `Err`, returns `false`. -/// Intended use: both arguments are [`CiWorkflowDiff::TouchedGates`] with `narrow_ids ⊆ wide_ids`, or -/// `wide` is [`CiWorkflowDiff::TouchAll`] (superset selection). Other pairings are not a refinement -/// partial order and may return `false` even when both plans are individually valid. -pub fn selection_subset_under_touch_set_growth( - dag: &CiWorkflowDagInput, - narrow: &CiWorkflowDiff, - wide: &CiWorkflowDiff, -) -> bool { - match ( - select_affected_gates(dag, narrow), - select_affected_gates(dag, wide), - ) { - (Ok(a), Ok(b)) => { - let a: BTreeSet<_> = a.into_iter().collect(); - let b: BTreeSet<_> = b.into_iter().collect(); - a.is_subset(&b) + fn validate_unique_gate_roster(dag: &CiWorkflowDagInput) -> Result<(), CiAffectedGatesError> { + let mut seen: HashSet<&str> = HashSet::new(); + for g in &dag.gates { + if !seen.insert(g.id.as_str()) { + return Err(CiAffectedGatesError::DuplicateGateRosterId { id: g.id.clone() }); + } } - _ => false, + Ok(()) } -} -fn topo_sort_subset( - dag: &CiWorkflowDagInput, - subset: &HashSet<&str>, -) -> Result, CiAffectedGatesError> { - let mut indegree: HashMap<&str, usize> = HashMap::new(); - for id in subset.iter().copied() { - indegree.entry(id).or_insert(0); - } - for (from, to) in &dag.edges { - let from = from.as_str(); - let to = to.as_str(); - if subset.contains(from) && subset.contains(to) { - *indegree.entry(to).or_insert(0) += 1; + /// Set inclusion witness for ratchet tests. + /// + /// Returns whether `select_affected_gates(dag, narrow)` ⊆ `select_affected_gates(dag, wide)` as sets + /// when **both** calls succeed; if either returns `Err`, returns `false`. + /// Intended use: both arguments are [`CiWorkflowDiff::TouchedGates`] with `narrow_ids ⊆ wide_ids`, or + /// `wide` is [`CiWorkflowDiff::TouchAll`] (superset selection). Other pairings are not a refinement + /// partial order and may return `false` even when both plans are individually valid. + pub fn selection_subset_under_touch_set_growth( + dag: &CiWorkflowDagInput, + narrow: &CiWorkflowDiff, + wide: &CiWorkflowDiff, + ) -> bool { + match ( + select_affected_gates(dag, narrow), + select_affected_gates(dag, wide), + ) { + (Ok(a), Ok(b)) => { + let a: BTreeSet<_> = a.into_iter().collect(); + let b: BTreeSet<_> = b.into_iter().collect(); + a.is_subset(&b) + } + _ => false, } } - let mut queue: VecDeque<&str> = indegree - .iter() - .filter(|(_, d)| **d == 0) - .map(|(n, _)| *n) - .collect::>() - .into_iter() - .collect(); - - let mut out: Vec = Vec::new(); - while let Some(n) = queue.pop_front() { - out.push(n.to_string()); + fn topo_sort_subset( + dag: &CiWorkflowDagInput, + subset: &HashSet<&str>, + ) -> Result, CiAffectedGatesError> { + let mut indegree: HashMap<&str, usize> = HashMap::new(); + for id in subset.iter().copied() { + indegree.entry(id).or_insert(0); + } for (from, to) in &dag.edges { - if from.as_str() != n { - continue; - } + let from = from.as_str(); let to = to.as_str(); - if !subset.contains(to) { - continue; + if subset.contains(from) && subset.contains(to) { + *indegree.entry(to).or_insert(0) += 1; } - if let Some(d) = indegree.get_mut(to) { - *d -= 1; - if *d == 0 { - queue.push_back(to); + } + + let mut queue: VecDeque<&str> = indegree + .iter() + .filter(|(_, d)| **d == 0) + .map(|(n, _)| *n) + .collect::>() + .into_iter() + .collect(); + + let mut out: Vec = Vec::new(); + while let Some(n) = queue.pop_front() { + out.push(n.to_string()); + for (from, to) in &dag.edges { + if from.as_str() != n { + continue; + } + let to = to.as_str(); + if !subset.contains(to) { + continue; + } + if let Some(d) = indegree.get_mut(to) { + *d -= 1; + if *d == 0 { + queue.push_back(to); + } } } } - } - if out.len() != subset.len() { - return Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph); - } - Ok(out) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn demo_ci_dag() -> CiWorkflowDagInput { - CiWorkflowDagInput { - gates: vec![ - CiGateMeta { - id: "compile-gates".into(), - blocking: true, - }, - CiGateMeta { - id: "lint".into(), - blocking: true, - }, - CiGateMeta { - id: "tests".into(), - blocking: true, - }, - CiGateMeta { - id: "l1-ratchet".into(), - blocking: true, - }, - ], - edges: vec![ - ("compile-gates".into(), "lint".into()), - ("compile-gates".into(), "tests".into()), - ("lint".into(), "l1-ratchet".into()), - ("tests".into(), "l1-ratchet".into()), - ], + if out.len() != subset.len() { + return Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph); } + Ok(out) } - #[test] - fn select_affected_gates_empty_diff_returns_empty_plan() { - let dag = demo_ci_dag(); - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::new()); - assert_eq!(select_affected_gates(&dag, &diff), Ok(Vec::new())); - } + #[cfg(test)] + mod tests { + use super::*; + + fn demo_ci_dag() -> CiWorkflowDagInput { + CiWorkflowDagInput { + gates: vec![ + CiGateMeta { + id: "compile-gates".into(), + blocking: true, + }, + CiGateMeta { + id: "lint".into(), + blocking: true, + }, + CiGateMeta { + id: "tests".into(), + blocking: true, + }, + CiGateMeta { + id: "l1-ratchet".into(), + blocking: true, + }, + ], + edges: vec![ + ("compile-gates".into(), "lint".into()), + ("compile-gates".into(), "tests".into()), + ("lint".into(), "l1-ratchet".into()), + ("tests".into(), "l1-ratchet".into()), + ], + } + } - #[test] - fn select_affected_gates_touch_all_runs_full_dag_in_topo_order() { - let dag = demo_ci_dag(); - let got = - select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("demo dag is valid"); - assert_eq!( - got, - vec![ - "compile-gates".to_string(), - "lint".to_string(), - "tests".to_string(), - "l1-ratchet".to_string(), - ] - ); - } + #[test] + fn select_affected_gates_empty_diff_returns_empty_plan() { + let dag = demo_ci_dag(); + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::new()); + assert_eq!(select_affected_gates(&dag, &diff), Ok(Vec::new())); + } - #[test] - fn select_affected_gates_isolated_gate_no_edges() { - let dag = CiWorkflowDagInput { - gates: vec![CiGateMeta { - id: "solo".into(), - blocking: true, - }], - edges: vec![], - }; - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["solo".into()])); - assert_eq!( - select_affected_gates(&dag, &diff), - Ok(vec!["solo".to_string()]) - ); - } + #[test] + fn select_affected_gates_touch_all_runs_full_dag_in_topo_order() { + let dag = demo_ci_dag(); + let got = + select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("demo dag is valid"); + assert_eq!( + got, + vec![ + "compile-gates".to_string(), + "lint".to_string(), + "tests".to_string(), + "l1-ratchet".to_string(), + ] + ); + } - #[test] - fn select_affected_gates_unknown_touch_id_forces_superset() { - let dag = demo_ci_dag(); - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["no-such-gate".into()])); - let got = select_affected_gates(&dag, &diff).expect("superset path is acyclic"); - let expect = select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("demo dag"); - assert_eq!(got, expect); - } + #[test] + fn select_affected_gates_isolated_gate_no_edges() { + let dag = CiWorkflowDagInput { + gates: vec![CiGateMeta { + id: "solo".into(), + blocking: true, + }], + edges: vec![], + }; + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["solo".into()])); + assert_eq!( + select_affected_gates(&dag, &diff), + Ok(vec!["solo".to_string()]) + ); + } - #[test] - fn select_affected_gates_rejects_unknown_edge_endpoint() { - let dag = CiWorkflowDagInput { - gates: vec![CiGateMeta { - id: "solo".into(), - blocking: true, - }], - edges: vec![("solo".into(), "phantom".into())], - }; - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["solo".into()])); - assert_eq!( - select_affected_gates(&dag, &diff), - Err(CiAffectedGatesError::UnknownEdgeEndpoint { - from: "solo".into(), - to: "phantom".into(), - }) - ); - } + #[test] + fn select_affected_gates_unknown_touch_id_forces_superset() { + let dag = demo_ci_dag(); + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["no-such-gate".into()])); + let got = select_affected_gates(&dag, &diff).expect("superset path is acyclic"); + let expect = select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("demo dag"); + assert_eq!(got, expect); + } - #[test] - fn select_affected_gates_rejects_duplicate_gate_roster_ids() { - let dag = CiWorkflowDagInput { - gates: vec![ - CiGateMeta { - id: "dup".into(), + #[test] + fn select_affected_gates_rejects_unknown_edge_endpoint() { + let dag = CiWorkflowDagInput { + gates: vec![CiGateMeta { + id: "solo".into(), blocking: true, - }, - CiGateMeta { - id: "dup".into(), - blocking: false, - }, - ], - edges: vec![], - }; - assert_eq!( - select_affected_gates(&dag, &CiWorkflowDiff::TouchAll), - Err(CiAffectedGatesError::DuplicateGateRosterId { id: "dup".into() }) - ); - } + }], + edges: vec![("solo".into(), "phantom".into())], + }; + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["solo".into()])); + assert_eq!( + select_affected_gates(&dag, &diff), + Err(CiAffectedGatesError::UnknownEdgeEndpoint { + from: "solo".into(), + to: "phantom".into(), + }) + ); + } - #[test] - fn select_affected_gates_rejects_prerequisite_cycle() { - let dag = CiWorkflowDagInput { - gates: vec![ - CiGateMeta { - id: "a".into(), - blocking: true, - }, - CiGateMeta { - id: "b".into(), - blocking: true, - }, - ], - edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], - }; - assert_eq!( - select_affected_gates(&dag, &CiWorkflowDiff::TouchAll), - Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) - ); - } + #[test] + fn select_affected_gates_rejects_duplicate_gate_roster_ids() { + let dag = CiWorkflowDagInput { + gates: vec![ + CiGateMeta { + id: "dup".into(), + blocking: true, + }, + CiGateMeta { + id: "dup".into(), + blocking: false, + }, + ], + edges: vec![], + }; + assert_eq!( + select_affected_gates(&dag, &CiWorkflowDiff::TouchAll), + Err(CiAffectedGatesError::DuplicateGateRosterId { id: "dup".into() }) + ); + } - #[test] - fn select_affected_gates_rejects_global_cycle_even_when_touch_isolated() { - let dag = CiWorkflowDagInput { - gates: vec![ - CiGateMeta { - id: "a".into(), - blocking: true, - }, - CiGateMeta { - id: "b".into(), - blocking: true, - }, - CiGateMeta { - id: "c".into(), - blocking: true, - }, - ], - edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], - }; - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["c".into()])); - assert_eq!( - select_affected_gates(&dag, &diff), - Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) - ); - } + #[test] + fn select_affected_gates_rejects_prerequisite_cycle() { + let dag = CiWorkflowDagInput { + gates: vec![ + CiGateMeta { + id: "a".into(), + blocking: true, + }, + CiGateMeta { + id: "b".into(), + blocking: true, + }, + ], + edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], + }; + assert_eq!( + select_affected_gates(&dag, &CiWorkflowDiff::TouchAll), + Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) + ); + } - #[test] - fn select_affected_gates_empty_touch_rejects_when_global_graph_has_cycle() { - let dag = CiWorkflowDagInput { - gates: vec![ - CiGateMeta { - id: "a".into(), - blocking: true, - }, - CiGateMeta { - id: "b".into(), - blocking: true, - }, - ], - edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], - }; - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::new()); - assert_eq!( - select_affected_gates(&dag, &diff), - Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) - ); - } + #[test] + fn select_affected_gates_rejects_global_cycle_even_when_touch_isolated() { + let dag = CiWorkflowDagInput { + gates: vec![ + CiGateMeta { + id: "a".into(), + blocking: true, + }, + CiGateMeta { + id: "b".into(), + blocking: true, + }, + CiGateMeta { + id: "c".into(), + blocking: true, + }, + ], + edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], + }; + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["c".into()])); + assert_eq!( + select_affected_gates(&dag, &diff), + Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) + ); + } - #[test] - fn select_affected_gates_lint_only_pulls_l1_and_shared_prereqs() { - let dag = demo_ci_dag(); - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])); - let got = select_affected_gates(&dag, &diff).expect("demo dag"); - let set: BTreeSet<_> = got.iter().cloned().collect(); - assert_eq!( - set, - BTreeSet::from([ - "compile-gates".into(), - "lint".into(), - "tests".into(), - "l1-ratchet".into(), - ]) - ); - assert_eq!(got.first().map(String::as_str), Some("compile-gates")); - } + #[test] + fn select_affected_gates_empty_touch_rejects_when_global_graph_has_cycle() { + let dag = CiWorkflowDagInput { + gates: vec![ + CiGateMeta { + id: "a".into(), + blocking: true, + }, + CiGateMeta { + id: "b".into(), + blocking: true, + }, + ], + edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], + }; + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::new()); + assert_eq!( + select_affected_gates(&dag, &diff), + Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) + ); + } - #[test] - fn selection_subset_under_touch_set_growth_holds_on_demo_dag() { - let dag = demo_ci_dag(); - let narrow = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])); - let wide = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into(), "tests".into()])); - assert!(selection_subset_under_touch_set_growth( - &dag, &narrow, &wide - )); - } + #[test] + fn select_affected_gates_lint_only_pulls_l1_and_shared_prereqs() { + let dag = demo_ci_dag(); + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])); + let got = select_affected_gates(&dag, &diff).expect("demo dag"); + let set: BTreeSet<_> = got.iter().cloned().collect(); + assert_eq!( + set, + BTreeSet::from([ + "compile-gates".into(), + "lint".into(), + "tests".into(), + "l1-ratchet".into(), + ]) + ); + assert_eq!(got.first().map(String::as_str), Some("compile-gates")); + } - #[test] - fn selection_subset_empty_diff_is_subset_of_nonempty() { - let dag = demo_ci_dag(); - let empty = CiWorkflowDiff::TouchedGates(BTreeSet::new()); - let wide = CiWorkflowDiff::TouchedGates(BTreeSet::from(["tests".into()])); - assert!(selection_subset_under_touch_set_growth(&dag, &empty, &wide)); - } -} + #[test] + fn selection_subset_under_touch_set_growth_holds_on_demo_dag() { + let dag = demo_ci_dag(); + let narrow = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])); + let wide = + CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into(), "tests".into()])); + assert!(selection_subset_under_touch_set_growth( + &dag, &narrow, &wide + )); + } + #[test] + fn selection_subset_empty_diff_is_subset_of_nonempty() { + let dag = demo_ci_dag(); + let empty = CiWorkflowDiff::TouchedGates(BTreeSet::new()); + let wide = CiWorkflowDiff::TouchedGates(BTreeSet::from(["tests".into()])); + assert!(selection_subset_under_touch_set_growth(&dag, &empty, &wide)); + } + } } use self::integration_rs_wiring_scan::integration_rs_cementing_path_attr_binds_mod_stem; diff --git a/src/v3/compiler/src/gunbc_ci.rs b/src/v3/compiler/src/gunbc_ci.rs deleted file mode 100644 index 723587bc5d8..00000000000 --- a/src/v3/compiler/src/gunbc_ci.rs +++ /dev/null @@ -1,479 +0,0 @@ -//! `gunbc.ci` workflow gate selection — affected-set dispatch substrate. -//! -//! **Authority** (Phase B): `docs/briefs/r3-wave1-s6-slice7-affected-set-impl-worker.md`; -//! fail-closed + obligation closure: `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §3–§4; -//! carrier shape: `dsl/gunbc/ci.dag` (`CIWorkflowDag`, `CIGateEdge`: `from` is a prerequisite of `to`). -//! -//! This module is intentionally **pure**: every decision is a function of -//! [`CiWorkflowDagInput`] + [`CiWorkflowDiff`] only. Git diffs, path-regex, env, -//! and PR #2713 lens receipts are upstream responsibilities; they must be -//! mapped into [`CiWorkflowDiff`] (or a superset-equivalent touch set) before -//! calling [`select_affected_gates`]. -//! -//! **Verifier ratchet witness (Phase C scaffolding)** — `docs/design-ci-workflow-substrate-shape-2026-05-12.md` -//! S5 + prequeue §5.2: monotone **inclusion** under enlarging the touched-id -//! set (smaller touch ⊆ larger touch ⇒ selected subset ⊆ selected superset). -//! See [`selection_subset_under_touch_set_growth`]. -//! -//! **Expansion semantics:** [`select_affected_gates`] walks each `CIGateEdge` **symmetrically** -//! (treat `from → to` as an undirected adjacency) to fixpoint, so a downstream obligation -//! pulls in its sibling prerequisites (e.g. touching `lint` selects `l1-ratchet`, which -//! forces `tests` because both parents are prerequisites of `l1-ratchet`). The returned -//! order is a **topological sort** of that vertex set on the directed prerequisite subgraph. -//! -//! **Global carrier validity:** before expanding any touch seed, [`select_affected_gates`] requires -//! the **entire** directed prerequisite graph on [`CiWorkflowDagInput::gates`] to be acyclic. A cycle -//! confined to an unselected weak component must still fail closed (P3), not succeed in “narrow” mode. - -use std::collections::{BTreeSet, HashMap, HashSet, VecDeque}; - -/// Structural mirror of `CIGate.id` in `dsl/gunbc/ci.dag`. -pub type CiGateId = String; - -/// Structural mirror of `CIGate` in `dsl/gunbc/ci.dag`. -/// -/// [`select_affected_gates`] consults only [`CiGateMeta::id`]. [`CiGateMeta::blocking`] is carried -/// for **carrier parity** with the DSL record so BinaryShim / dispatch wiring can map rows without -/// a forked struct; merge-blocking vs advisory semantics stay **outside** this selection substrate -/// until a consumer reads the field. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CiGateMeta { - pub id: CiGateId, - pub blocking: bool, -} - -/// Structural mirror of `CIWorkflowDag`: gate roster + prerequisite edges. -/// -/// Each edge `(from, to)` means **`from` must complete before `to`** (matches -/// `CIGateEdge { from, to }` in `ci.dag`). -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct CiWorkflowDagInput { - pub gates: Vec, - /// Prerequisite edges: `from` runs before `to`. - pub edges: Vec<(CiGateId, CiGateId)>, -} - -// Practice 4 (coproduct checkpoint, `docs/modeling-discipline.md` §4): -// 🟢 GREEN — terminal upstream-policy boundary for gate-id touch semantics before -// `select_affected_gates`: `TouchAll` is the conservative in-DAG superset seed; -// `TouchedGates` is the explicit finite set upstream mapped from lens/git facts. -// Ledger: no third authority here — path-regex, env, and PR #2713 receipts stay -// outside this module (Slice 7 brief + affected-set selection canvas §1). -/// Diff-against-base input projected to **gate ids** (single authority at this boundary). -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CiWorkflowDiff { - /// Every gate id in [`CiWorkflowDagInput::gates`] is treated as directly touched. - TouchAll, - /// Upstream bundle names gate ids with proven or assumed direct impact. - TouchedGates(BTreeSet), -} - -// Practice 4 (coproduct checkpoint, `docs/modeling-discipline.md` §4): -// 🟢 GREEN — terminal typed failure surface for [`select_affected_gates`]: malformed `CIWorkflowDag` -// carrier vs non-acyclic directed prerequisites (full roster graph and/or selected subset; no silent success). -// Ledger: (1) fact placement — not split across consumers; this `Result` boundary owns the `Err` -// coproduct. (2) variant-is-data — rejected; payloads differ structurally. (3) algebraic — not -// `std/` refs. (4) dimensional — not one decomposed axis space; each variant is an irreducible -// outcome at the `dsl/gunbc/ci.dag` structural mirror until `.dag`-owned dispatch absorbs diagnostics. -/// [`select_affected_gates`] failed: malformed `CIWorkflowDag` carrier or non-acyclic prerequisites. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CiAffectedGatesError { - /// The same `CIGate.id` appears more than once in [`CiWorkflowDagInput::gates`] (malformed carrier; must not collapse into a `HashSet` silently). - DuplicateGateRosterId { id: CiGateId }, - /// `CIGateEdge` names a gate id not present in [`CiWorkflowDagInput::gates`] (violates single roster authority in `dsl/gunbc/ci.dag`). - UnknownEdgeEndpoint { from: CiGateId, to: CiGateId }, - /// The directed prerequisite graph is not a DAG: either the **full** roster graph from - /// [`CiWorkflowDagInput::edges`] contains a cycle, or the selected vertex set’s induced subgraph - /// does (broken indegree accounting). - NonAcyclicPrerequisiteGraph, -} - -/// On success, returns gate ids to execute: the **connected component** of the seed under the symmetric -/// closure of prerequisite edges (iterate `from ↔ to` adjacency to fixpoint), then -/// **topologically sorted** on the directed subgraph (prerequisites first). -/// -/// **Fail-closed** (`design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §3): -/// any touched id not present in `dag.gates` forces **all** gates listed in `dag.gates` -/// into the plan (superset within this DAG). -/// -/// **Empty diff** (`TouchedGates` empty): returns `Ok` of an **empty** plan — callers map that -/// to “no merge-blocking re-verify” vs “full superset” per repository policy **outside** -/// this function (this module does not read policy carriers). -/// -/// **Malformed carrier:** duplicate [`CiGateMeta::id`] values in [`CiWorkflowDagInput::gates`] -/// yield [`CiAffectedGatesError::DuplicateGateRosterId`] (roster must be unique before any `HashSet` -/// view so gates are not dropped silently). Any edge whose `from` or `to` is absent from that roster -/// yields [`CiAffectedGatesError::UnknownEdgeEndpoint`]. Any directed cycle in the **full** -/// prerequisite graph on the roster (not only the touch-selected component) yields -/// [`CiAffectedGatesError::NonAcyclicPrerequisiteGraph`] instead of returning `Ok` in narrow mode. -pub fn select_affected_gates( - dag: &CiWorkflowDagInput, - diff: &CiWorkflowDiff, -) -> Result, CiAffectedGatesError> { - validate_unique_gate_roster(dag)?; - let known: HashSet<&str> = dag.gates.iter().map(|g| g.id.as_str()).collect(); - for (from, to) in &dag.edges { - if !known.contains(from.as_str()) || !known.contains(to.as_str()) { - return Err(CiAffectedGatesError::UnknownEdgeEndpoint { - from: from.clone(), - to: to.clone(), - }); - } - } - - // Fail-closed on the whole carrier: a cycle in an unselected component must not yield `Ok` - // for a disjoint touch seed (codex / P3). - topo_sort_subset(dag, &known)?; - - let force_superset = match diff { - CiWorkflowDiff::TouchAll => false, - CiWorkflowDiff::TouchedGates(ids) => ids.iter().any(|id| !known.contains(id.as_str())), - }; - - let seed: HashSet<&str> = if force_superset { - known.clone() - } else { - match diff { - CiWorkflowDiff::TouchAll => known.clone(), - CiWorkflowDiff::TouchedGates(ids) => ids.iter().map(|s| s.as_str()).collect(), - } - }; - - if seed.is_empty() { - return Ok(Vec::new()); - } - - // Symmetric edge closure: expand along `from ↔ to` until stable, then topo-sort - // the induced set on directed prerequisite edges. - let mut selected: HashSet<&str> = seed.clone(); - loop { - let before = selected.len(); - for (from, to) in &dag.edges { - let from = from.as_str(); - let to = to.as_str(); - if selected.contains(from) { - selected.insert(to); - } - if selected.contains(to) { - selected.insert(from); - } - } - if selected.len() == before { - break; - } - } - - topo_sort_subset(dag, &selected) -} - -fn validate_unique_gate_roster(dag: &CiWorkflowDagInput) -> Result<(), CiAffectedGatesError> { - let mut seen: HashSet<&str> = HashSet::new(); - for g in &dag.gates { - if !seen.insert(g.id.as_str()) { - return Err(CiAffectedGatesError::DuplicateGateRosterId { id: g.id.clone() }); - } - } - Ok(()) -} - -/// Set inclusion witness for ratchet tests. -/// -/// Returns whether `select_affected_gates(dag, narrow)` ⊆ `select_affected_gates(dag, wide)` as sets -/// when **both** calls succeed; if either returns `Err`, returns `false`. -/// Intended use: both arguments are [`CiWorkflowDiff::TouchedGates`] with `narrow_ids ⊆ wide_ids`, or -/// `wide` is [`CiWorkflowDiff::TouchAll`] (superset selection). Other pairings are not a refinement -/// partial order and may return `false` even when both plans are individually valid. -pub fn selection_subset_under_touch_set_growth( - dag: &CiWorkflowDagInput, - narrow: &CiWorkflowDiff, - wide: &CiWorkflowDiff, -) -> bool { - match ( - select_affected_gates(dag, narrow), - select_affected_gates(dag, wide), - ) { - (Ok(a), Ok(b)) => { - let a: BTreeSet<_> = a.into_iter().collect(); - let b: BTreeSet<_> = b.into_iter().collect(); - a.is_subset(&b) - } - _ => false, - } -} - -fn topo_sort_subset( - dag: &CiWorkflowDagInput, - subset: &HashSet<&str>, -) -> Result, CiAffectedGatesError> { - let mut indegree: HashMap<&str, usize> = HashMap::new(); - for id in subset.iter().copied() { - indegree.entry(id).or_insert(0); - } - for (from, to) in &dag.edges { - let from = from.as_str(); - let to = to.as_str(); - if subset.contains(from) && subset.contains(to) { - *indegree.entry(to).or_insert(0) += 1; - } - } - - let mut queue: VecDeque<&str> = indegree - .iter() - .filter(|(_, d)| **d == 0) - .map(|(n, _)| *n) - .collect::>() - .into_iter() - .collect(); - - let mut out: Vec = Vec::new(); - while let Some(n) = queue.pop_front() { - out.push(n.to_string()); - for (from, to) in &dag.edges { - if from.as_str() != n { - continue; - } - let to = to.as_str(); - if !subset.contains(to) { - continue; - } - if let Some(d) = indegree.get_mut(to) { - *d -= 1; - if *d == 0 { - queue.push_back(to); - } - } - } - } - - if out.len() != subset.len() { - return Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph); - } - Ok(out) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn demo_ci_dag() -> CiWorkflowDagInput { - CiWorkflowDagInput { - gates: vec![ - CiGateMeta { - id: "compile-gates".into(), - blocking: true, - }, - CiGateMeta { - id: "lint".into(), - blocking: true, - }, - CiGateMeta { - id: "tests".into(), - blocking: true, - }, - CiGateMeta { - id: "l1-ratchet".into(), - blocking: true, - }, - ], - edges: vec![ - ("compile-gates".into(), "lint".into()), - ("compile-gates".into(), "tests".into()), - ("lint".into(), "l1-ratchet".into()), - ("tests".into(), "l1-ratchet".into()), - ], - } - } - - #[test] - fn select_affected_gates_empty_diff_returns_empty_plan() { - let dag = demo_ci_dag(); - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::new()); - assert_eq!(select_affected_gates(&dag, &diff), Ok(Vec::new())); - } - - #[test] - fn select_affected_gates_touch_all_runs_full_dag_in_topo_order() { - let dag = demo_ci_dag(); - let got = - select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("demo dag is valid"); - assert_eq!( - got, - vec![ - "compile-gates".to_string(), - "lint".to_string(), - "tests".to_string(), - "l1-ratchet".to_string(), - ] - ); - } - - #[test] - fn select_affected_gates_isolated_gate_no_edges() { - let dag = CiWorkflowDagInput { - gates: vec![CiGateMeta { - id: "solo".into(), - blocking: true, - }], - edges: vec![], - }; - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["solo".into()])); - assert_eq!( - select_affected_gates(&dag, &diff), - Ok(vec!["solo".to_string()]) - ); - } - - #[test] - fn select_affected_gates_unknown_touch_id_forces_superset() { - let dag = demo_ci_dag(); - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["no-such-gate".into()])); - let got = select_affected_gates(&dag, &diff).expect("superset path is acyclic"); - let expect = select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("demo dag"); - assert_eq!(got, expect); - } - - #[test] - fn select_affected_gates_rejects_unknown_edge_endpoint() { - let dag = CiWorkflowDagInput { - gates: vec![CiGateMeta { - id: "solo".into(), - blocking: true, - }], - edges: vec![("solo".into(), "phantom".into())], - }; - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["solo".into()])); - assert_eq!( - select_affected_gates(&dag, &diff), - Err(CiAffectedGatesError::UnknownEdgeEndpoint { - from: "solo".into(), - to: "phantom".into(), - }) - ); - } - - #[test] - fn select_affected_gates_rejects_duplicate_gate_roster_ids() { - let dag = CiWorkflowDagInput { - gates: vec![ - CiGateMeta { - id: "dup".into(), - blocking: true, - }, - CiGateMeta { - id: "dup".into(), - blocking: false, - }, - ], - edges: vec![], - }; - assert_eq!( - select_affected_gates(&dag, &CiWorkflowDiff::TouchAll), - Err(CiAffectedGatesError::DuplicateGateRosterId { id: "dup".into() }) - ); - } - - #[test] - fn select_affected_gates_rejects_prerequisite_cycle() { - let dag = CiWorkflowDagInput { - gates: vec![ - CiGateMeta { - id: "a".into(), - blocking: true, - }, - CiGateMeta { - id: "b".into(), - blocking: true, - }, - ], - edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], - }; - assert_eq!( - select_affected_gates(&dag, &CiWorkflowDiff::TouchAll), - Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) - ); - } - - #[test] - fn select_affected_gates_rejects_global_cycle_even_when_touch_isolated() { - let dag = CiWorkflowDagInput { - gates: vec![ - CiGateMeta { - id: "a".into(), - blocking: true, - }, - CiGateMeta { - id: "b".into(), - blocking: true, - }, - CiGateMeta { - id: "c".into(), - blocking: true, - }, - ], - edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], - }; - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["c".into()])); - assert_eq!( - select_affected_gates(&dag, &diff), - Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) - ); - } - - #[test] - fn select_affected_gates_empty_touch_rejects_when_global_graph_has_cycle() { - let dag = CiWorkflowDagInput { - gates: vec![ - CiGateMeta { - id: "a".into(), - blocking: true, - }, - CiGateMeta { - id: "b".into(), - blocking: true, - }, - ], - edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], - }; - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::new()); - assert_eq!( - select_affected_gates(&dag, &diff), - Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) - ); - } - - #[test] - fn select_affected_gates_lint_only_pulls_l1_and_shared_prereqs() { - let dag = demo_ci_dag(); - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])); - let got = select_affected_gates(&dag, &diff).expect("demo dag"); - let set: BTreeSet<_> = got.iter().cloned().collect(); - assert_eq!( - set, - BTreeSet::from([ - "compile-gates".into(), - "lint".into(), - "tests".into(), - "l1-ratchet".into(), - ]) - ); - assert_eq!(got.first().map(String::as_str), Some("compile-gates")); - } - - #[test] - fn selection_subset_under_touch_set_growth_holds_on_demo_dag() { - let dag = demo_ci_dag(); - let narrow = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])); - let wide = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into(), "tests".into()])); - assert!(selection_subset_under_touch_set_growth( - &dag, &narrow, &wide - )); - } - - #[test] - fn selection_subset_empty_diff_is_subset_of_nonempty() { - let dag = demo_ci_dag(); - let empty = CiWorkflowDiff::TouchedGates(BTreeSet::new()); - let wide = CiWorkflowDiff::TouchedGates(BTreeSet::from(["tests".into()])); - assert!(selection_subset_under_touch_set_growth(&dag, &empty, &wide)); - } -} diff --git a/src/v3/compiler/src/integration_rs_wiring_scan.rs b/src/v3/compiler/src/integration_rs_wiring_scan.rs deleted file mode 100644 index 301e3fc9d5d..00000000000 --- a/src/v3/compiler/src/integration_rs_wiring_scan.rs +++ /dev/null @@ -1,362 +0,0 @@ -//! `tests/integration.rs` wiring scanner (Band-C cementing dispatch). -//! -//! SG-0 / INVARIANTS P5: hand-authored Rust under `src/v3/compiler/src/` — keep -//! `EXPECTED_HAND_AUTHORED_NON_TEST` in `tests/integration/sg0_census_test.rs` in lockstep -//! when adding or retiring paths here. -//! -//! **ROADMAP / dissolution:** same **v3 lens capability honesty pass** row as -//! `src/v3/compiler/tests/integration/common/wiring_scanner_test.rs` in `INVARIANTS.md` § SG-0 — -//! Band-C `#[path]` / `mod` wiring is enforced without `syn` until reflected integration facts exist. -//! **Dissolution:** remove this scanner when `tests/integration.rs` cementing module wiring can be -//! validated structurally (no line-oriented scan). Raw/byte string openers in **Code** return -//! `Err` so `CementingDispatchMatchesProjection` surfaces a claim failure instead of aborting the runner. -//! -//! Lives in the compiler crate so `test_runner` can validate -//! `#[path = "integration/cementing/.rs"]` + `mod ;` pairings without -//! duplicating the scanner across test binaries. - -/// `true` when `bytes[i..]` opens a `b"…"`, `br#"…"#`, `r#"…"#`, or `r"…"` literal in -/// Rust source. The wiring scanner does not model these — return an error if they appear -/// in `Code` so Band-C ratchets surface `ClaimResult::Fail` instead of aborting. -fn code_opens_raw_or_byte_string_literal(bytes: &[u8], i: usize) -> bool { - match bytes.get(i) { - Some(b'b') => { - if bytes.get(i + 1) == Some(&b'"') { - return true; - } - if bytes.get(i + 1) == Some(&b'r') { - let mut j = i + 2; - while bytes.get(j) == Some(&b'#') { - j += 1; - } - return bytes.get(j) == Some(&b'"'); - } - false - } - Some(b'r') => { - let mut j = i + 1; - while bytes.get(j) == Some(&b'#') { - j += 1; - } - bytes.get(j) == Some(&b'"') - } - _ => false, - } -} - -/// Byte index after the last byte of a Rust escape sequence starting at `backslash` -/// (`bytes[backslash] == b'\\'`). Matches suffix forms used in character and byte -/// literals (`\n`, `\t`, `\xNN`, `\u{…}`, …). Unicode escapes use **only** the braced -/// `\u{…}` form in modern Rust; there is no separate `\uXXXX` tail to scan. -fn integration_rs_escape_sequence_end(bytes: &[u8], backslash: usize) -> usize { - debug_assert_eq!(bytes.get(backslash), Some(&b'\\')); - let i = backslash + 1; - if i >= bytes.len() { - return bytes.len(); - } - match bytes[i] { - b'x' => (backslash + 4).min(bytes.len()), - b'u' if bytes.get(i + 1) == Some(&b'{') => { - let mut j = i + 2; - while j < bytes.len() && bytes[j] != b'}' { - j += 1; - } - (j + 1).min(bytes.len()) - } - _ => (backslash + 2).min(bytes.len()), - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum IntegrationRsScan { - Code, - LineComment, - BlockComment(u32), - String, - /// Inside `b'…'` after consuming the `b'` prefix. - ByteCharLiteral, - /// Inside `'…'` after consuming the opening `'`. - CharLiteral, -} - -fn integration_rs_reject_raw_string_in_code(bytes: &[u8], i: usize) -> Result<(), String> { - if code_opens_raw_or_byte_string_literal(bytes, i) { - return Err(format!( - "integration_rs wiring scan: raw/byte string literal at byte offset {i} \ - (e.g. r\"…\", r#\"…\"#, b\"…\", br#\"…\"#). Extend IntegrationRsScan before scanning this file." - )); - } - Ok(()) -} - -/// Byte offsets of `needle` in `integration_rs` that occur in **Code** (outside -/// `//`, `/* */`, `"…"` strings, `b'…'` byte literals, and `'…'` character literals). -/// Lifetimes and labels (`'a`, `'static`) are skipped without entering string/literal -/// states. See [`integration_rs_active_line_contains`]. -fn integration_rs_code_substring_positions( - integration_rs: &str, - needle: &str, -) -> Result, String> { - let mut hits = Vec::new(); - if needle.is_empty() { - hits.push(0); - return Ok(hits); - } - let bytes = integration_rs.as_bytes(); - let mut i = 0usize; - let mut state = IntegrationRsScan::Code; - - while i < bytes.len() { - match state { - IntegrationRsScan::Code => { - integration_rs_reject_raw_string_in_code(bytes, i)?; - if bytes[i] == b'/' && i + 1 < bytes.len() { - if bytes[i + 1] == b'/' { - state = IntegrationRsScan::LineComment; - i += 2; - continue; - } - if bytes[i + 1] == b'*' { - state = IntegrationRsScan::BlockComment(1); - i += 2; - continue; - } - } - if bytes.get(i) == Some(&b'b') && bytes.get(i + 1) == Some(&b'\'') { - state = IntegrationRsScan::ByteCharLiteral; - i += 2; - continue; - } - if bytes[i] == b'\'' { - if bytes.get(i + 1) == Some(&b'\\') { - state = IntegrationRsScan::CharLiteral; - i += 1; - continue; - } - if let Some(rest) = integration_rs.get(i + 1..) { - let mut chars = rest.chars(); - if let Some(c) = chars.next() { - let after_char = i + 1 + c.len_utf8(); - if bytes.get(after_char) == Some(&b'\'') { - i = after_char + 1; - continue; - } - } - } - i += 1; - while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') - { - i += 1; - } - continue; - } - if bytes[i] == b'"' { - state = IntegrationRsScan::String; - i += 1; - continue; - } - if integration_rs[i..].starts_with(needle) { - hits.push(i); - } - i += 1; - } - IntegrationRsScan::LineComment => { - if bytes[i] == b'\n' { - state = IntegrationRsScan::Code; - } - i += 1; - } - IntegrationRsScan::BlockComment(depth) => { - if bytes[i] == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'*' { - state = IntegrationRsScan::BlockComment(depth + 1); - i += 2; - continue; - } - if bytes[i] == b'*' && i + 1 < bytes.len() && bytes[i + 1] == b'/' { - let d = depth - 1; - i += 2; - state = if d == 0 { - IntegrationRsScan::Code - } else { - IntegrationRsScan::BlockComment(d) - }; - continue; - } - i += 1; - } - IntegrationRsScan::String => match bytes[i] { - b'\\' => { - i = (i + 2).min(bytes.len()); - } - b'"' => { - i += 1; - state = IntegrationRsScan::Code; - } - _ => { - i += 1; - } - }, - IntegrationRsScan::ByteCharLiteral => { - if bytes.get(i) == Some(&b'\\') { - i = integration_rs_escape_sequence_end(bytes, i); - } else { - i += 1; - } - if bytes.get(i) == Some(&b'\'') { - i += 1; - } - state = IntegrationRsScan::Code; - } - IntegrationRsScan::CharLiteral => { - if bytes.get(i) == Some(&b'\\') { - i = integration_rs_escape_sequence_end(bytes, i); - } else if let Some(rest) = integration_rs.get(i..) { - let mut ci = rest.chars(); - if let Some(c) = ci.next() { - i += c.len_utf8(); - } else { - state = IntegrationRsScan::Code; - continue; - } - } else { - state = IntegrationRsScan::Code; - continue; - } - if bytes.get(i) == Some(&b'\'') { - i += 1; - } - state = IntegrationRsScan::Code; - } - } - } - Ok(hits) -} - -/// Consume one outer `#[ … ]` attribute (handles quoted `"` regions and nested `[` `]`). -fn skip_rust_attribute(s: &str) -> Option<&str> { - if !s.starts_with("#[") { - return None; - } - let bytes = s.as_bytes(); - let mut i = 1usize; - let mut depth = 0usize; - let mut in_string = false; - let mut escape = false; - while i < bytes.len() { - let b = bytes[i]; - if in_string { - if escape { - escape = false; - i += 1; - continue; - } - if b == b'\\' { - escape = true; - i += 1; - continue; - } - if b == b'"' { - in_string = false; - } - i += 1; - continue; - } - match b { - b'"' => { - in_string = true; - i += 1; - } - b'[' => { - depth += 1; - i += 1; - } - b']' => { - depth = depth.saturating_sub(1); - i += 1; - if depth == 0 { - return Some(&s[i..]); - } - } - _ => { - i += 1; - } - } - } - None -} - -/// After a `#[path = "integration/cementing/.rs"]` attribute in Code, the next -/// Rust item must be `mod ;` (optionally separated by whitespace, newlines, or -/// additional `#[…]` attributes). -fn mod_decl_follows_cementing_path_tail(mut tail: &str, stem: &str) -> bool { - let prefix = format!("mod {stem}"); - loop { - tail = tail.trim_start(); - if tail.is_empty() { - return false; - } - if tail.starts_with("#[") { - let Some(rest) = skip_rust_attribute(tail) else { - return false; - }; - tail = rest; - continue; - } - if !tail.starts_with(&prefix) { - return false; - } - let after_prefix = &tail[prefix.len()..]; - if after_prefix - .chars() - .next() - .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_') - { - // e.g. `mod stem` must not match `mod stem_extra`. - return false; - } - let after = after_prefix.trim_start(); - return after.starts_with(';'); - } -} - -/// `true` when `integration_rs` contains `#[path = "integration/cementing/.rs"]` -/// in **Code** and that attribute is immediately followed (per -/// [`mod_decl_follows_cementing_path_tail`]) by `mod ;`. -/// -/// This is stricter than substring checks for `#[path]` and `mod` separately — it -/// rejects a mismatched `#[path]` on one module paired with a stray `mod ;` -/// elsewhere (`TESTING.md` Band-C dispatch). -pub fn integration_rs_cementing_path_attr_binds_mod_stem( - integration_rs: &str, - stem: &str, -) -> Result { - let path_attr = format!(r#"#[path = "integration/cementing/{stem}.rs"]"#); - for pos in integration_rs_code_substring_positions(integration_rs, &path_attr)? { - let Some(tail) = integration_rs.get(pos + path_attr.len()..) else { - continue; - }; - if mod_decl_follows_cementing_path_tail(tail, stem) { - return Ok(true); - } - } - Ok(false) -} - -/// True when `needle` appears in `integration_rs` **outside** Rust line comments -/// (`//` … including `///` / `//!`), **nested** block comments (`/* … */`), normal -/// `"…"` string literals, `b'…'` byte character literals, and `'…'` character -/// literals (excluding lifetimes / labels such as `'a` or `'static`). -/// -/// Band-C wiring ratchets use this so `#[path = …]` / `mod …;` matches cannot -/// false-green on commented-out or string-embedded copies. Needles are ASCII -/// (`#[path`, `mod foo`); the scan is byte-oriented on UTF-8 boundaries. -/// -/// **Not handled:** raw strings (`r#"…"#`), byte strings (`b"…"`, `br#"…"#`). If those appear in -/// scanned **Code**, this returns `Err` with an explicit message (so `TestPredicate` callers can -/// map to `ClaimResult::Fail`) until `IntegrationRsScan` is widened for those forms. -pub fn integration_rs_active_line_contains( - integration_rs: &str, - needle: &str, -) -> Result { - Ok(!integration_rs_code_substring_positions(integration_rs, needle)?.is_empty()) -} diff --git a/src/v3/compiler/src/lens_declaration_apply.rs b/src/v3/compiler/src/lens_declaration_apply.rs deleted file mode 100644 index 55de5b0c4ac..00000000000 --- a/src/v3/compiler/src/lens_declaration_apply.rs +++ /dev/null @@ -1,3058 +0,0 @@ -//! Bounded lens application (T-LensAPI / D1): interpret `ArrowBody::UserDefined` graphs -//! over substrate-shaped [`FieldValue`] — no whole-claim operator recognizers. -//! -//! **R3 gate #5 (`lens_apply_dot_rs_retired`):** the legacy path -//! `src/v3/compiler/src/lens_apply.rs` is **retired** (path absent from the tree). -//! Bounded lens application and `lens_testgen` live here until PB-Runtime / Row-4 -//! readiness dissolves the host shim, per `docs/r3-program-plan.md` §1.8 and -//! `INVARIANTS` P5(b) deferral language on the tracking PR. - -use std::collections::{HashMap, HashSet}; -use std::str::FromStr; - -use num_bigint::BigInt; - -use crate::dag::{ - literal_bits_int, ArrowBody, AtomPayload, Behavior, BindNode, BranchNode, BranchPattern, Dag, - Declaration, DeclarationId, FieldValue, LiteralBits, OperatorKind, PortId, ProducerLookup, - TransformNode, TransformTarget, TypeConnective, ValueBody, -}; -use crate::evaluator::{ - evaluate_body, EvalFrame, EvalStateStack, EvalStrategy, InputEvaluationOrder, Value, -}; -use crate::infer_helpers::resolve_template_argument_value; - -fn is_fold_instantiation(dag: &Dag, decl: &Declaration) -> bool { - matches!( - &decl.connective, - TypeConnective::Instantiation { template, .. } - if dag.std_list_fold_decl() == Some(*template) - ) -} - -const LENS_APPLY_TYPE_WALK_DEPTH: usize = 32; - -/// Structural eligibility for the monomorphized `std.list.fold` → [`eval_fold_step`] fast path. -/// -/// `eval_fold_step` / `eval_std_fold` are type-erased over accumulator and element shape: they -/// bind acc/elt as opaque [`FieldValue`] to the step `Bind`'s last two params and walk the step -/// body via [`EvalCtx::eval_port`]. The interpretability constraint therefore lives on the step -/// body's transitive [`Behavior`] reachability, not on the resolved type arguments. A step body -/// is eligible iff every reachable producer is one the bounded interpreter can evaluate: -/// [`Behavior::Loop`], non-`UserDefined` [`ArrowBody`], parameterized `Bind` producers, and -/// logical operators are all rejected as fail-closed (matching the interpreter's own -/// [`LensApplyError::UnimplementedLoopBound`] / [`LensApplyError::UnsupportedConstruct`] paths). -/// -/// **Dissolution trigger:** delete this predicate when `eval_loop` and the rejected -/// `Behavior::Bind` / `Logical` paths gain bounded semantics — at that point every fold step -/// body is interpretable and the eligibility gate is vacuous. -fn step_body_eligible_for_bounded_eval(dag: &Dag, step_bind: &BindNode) -> bool { - let mut visited: HashSet = HashSet::new(); - eligibility_walk_port(dag, step_bind.value, &mut visited, 0) -} - -fn eligibility_walk_port( - dag: &Dag, - port: PortId, - visited: &mut HashSet, - depth: usize, -) -> bool { - if depth >= LENS_APPLY_TYPE_WALK_DEPTH { - return false; - } - // Parameter ports (acc/elt of the step `Bind`, formals of any callable we walk into) - // have no producer — they're bound by the interpreter at evaluation time. Treat as - // eligible; only producer-backed nodes contribute interpretability constraints. - // Malformed-substrate states (MissingPort / MissingNode / BindCycle) fail-closed: - // INVARIANTS P3 forbids collapsing them into the legitimate-absence path. - let producer = match dag.resolve_producer_lookup(&port) { - ProducerLookup::NoProducer => return true, - ProducerLookup::Found(b) => b, - ProducerLookup::MissingPort { .. } - | ProducerLookup::MissingNode { .. } - | ProducerLookup::BindCycle { .. } => return false, - }; - match producer { - Behavior::Value(_) => true, - Behavior::Transform(t) => eligibility_walk_transform(dag, t, visited, depth + 1), - Behavior::Branch(b) => eligibility_walk_branch(dag, b, visited, depth + 1), - Behavior::Loop(_) => false, - Behavior::Bind(b) => { - if !b.params.is_empty() { - return false; - } - eligibility_walk_port(dag, b.value, visited, depth + 1) - } - } -} - -fn eligibility_walk_transform( - dag: &Dag, - t: &TransformNode, - visited: &mut HashSet, - depth: usize, -) -> bool { - for input in &t.inputs { - if !eligibility_walk_port(dag, *input, visited, depth + 1) { - return false; - } - } - match &t.target { - TransformTarget::Operator(OperatorKind::Logical(_)) => false, - TransformTarget::Operator(_) - | TransformTarget::UnresolvedFieldProject { .. } - | TransformTarget::ResolvedFieldProject { .. } => true, - TransformTarget::Callable(callee) => { - eligibility_walk_callable(dag, *callee, visited, depth + 1) - } - } -} - -fn eligibility_walk_branch( - dag: &Dag, - b: &BranchNode, - visited: &mut HashSet, - depth: usize, -) -> bool { - if !eligibility_walk_port(dag, b.input, visited, depth + 1) { - return false; - } - for path in &b.paths { - match &path.pattern { - BranchPattern::ResolvedVariant(_) => {} - BranchPattern::UnresolvedVariant { .. } => return false, - } - if !eligibility_walk_port(dag, path.output, visited, depth + 1) { - return false; - } - } - true -} - -fn eligibility_walk_callable( - dag: &Dag, - callee: DeclarationId, - visited: &mut HashSet, - depth: usize, -) -> bool { - if depth >= LENS_APPLY_TYPE_WALK_DEPTH { - return false; - } - if !visited.insert(callee) { - return true; - } - let decl = dag.declaration(callee); - match &decl.connective { - TypeConnective::Instantiation { template, .. } => { - // Nested `std.list.fold` instantiations: the interpreter dispatches the same - // bounded path recursively, so eligibility is conditional on the inner step body. - // For now any nested fold is treated as ineligible — its step body would need to - // be located via `find_fold_step_bind_via_instantiation`, which is the same shape - // recursion this predicate already walks at the outer call site. - if dag.std_list_fold_decl() == Some(*template) { - return false; - } - eligibility_walk_callable(dag, *template, visited, depth + 1) - } - TypeConnective::Arrow { body, .. } => { - let ArrowBody::UserDefined(root) = body else { - return false; - }; - let b = (*root).bind(dag); - eligibility_walk_port(dag, b.value, visited, depth + 1) - } - _ => false, - } -} - -fn declaration_is_callable_type(dag: &Dag, current: DeclarationId, depth: usize) -> bool { - if depth >= LENS_APPLY_TYPE_WALK_DEPTH { - return false; - } - match &dag.declaration(current).connective { - TypeConnective::Arrow { .. } => true, - TypeConnective::Instantiation { template, .. } => { - declaration_is_callable_type(dag, *template, depth + 1) - } - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - declaration_is_callable_type(dag, *next, depth + 1) - } - TypeConnective::Atom(AtomPayload::Literal(_)) - | TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(_)) - | TypeConnective::Atom(AtomPayload::TypeParam(_)) - | TypeConnective::Conj { .. } - | TypeConnective::Disj { .. } - | TypeConnective::Cardinality(_) => false, - } -} - -/// Arrow formals of `std.list.fold` whose types are callable — the step `f` slot. -fn fold_template_callable_formals(dag: &Dag, fold_template: DeclarationId) -> Vec { - let decl = dag.declaration(fold_template); - let TypeConnective::Arrow { inputs, .. } = &decl.connective else { - return Vec::new(); - }; - inputs - .iter() - .copied() - .filter(|&i| declaration_is_callable_type(dag, i, 0)) - .collect() -} - -/// Peel `Instantiation` carriers (same contract as [`LensInterpreter::eval_callable`]) until an -/// `Arrow` with `UserDefined` body; return the root `Bind` when it has ≥2 params (step closure). -fn monomorph_callable_bind_root(dag: &Dag, mut decl_id: DeclarationId) -> Option<&BindNode> { - for _ in 0..LENS_APPLY_TYPE_WALK_DEPTH { - let decl = dag.declaration(decl_id); - match &decl.connective { - TypeConnective::Instantiation { template, .. } => { - if dag.std_list_fold_decl() == Some(*template) { - return None; - } - decl_id = *template; - } - TypeConnective::Arrow { body, .. } => { - let ArrowBody::UserDefined(root) = body else { - return None; - }; - let b = (*root).bind(dag); - return (b.params.len() >= 2).then_some(b); - } - _ => return None, - } - } - None -} - -/// Locate the `|acc, x|` step closure lowered as a two-parameter `Bind` for this `fold` site. -/// -/// Walk `Instantiation.arguments` on the monomorphized fold callee — substrate `DeclarationId` -/// keyed to the template's callable formal — then resolve the step `Arrow` root `Bind`. Matches -/// builder/runtime arity: the step is not a `Transform` input port. No span-overlap recovery — -/// that would not be a declared substrate dependency (Facts Flow Forward). -/// -/// **Dissolution:** attach the step as an explicit `Transform` input / behavior edge and delete -/// this `Instantiation`-walk indirection when lowering guarantees a direct edge. -fn find_fold_step_bind_via_instantiation( - dag: &Dag, - fold_callable_id: DeclarationId, -) -> Option<&BindNode> { - let decl = dag.declaration(fold_callable_id); - let TypeConnective::Instantiation { - template, - arguments, - } = &decl.connective - else { - return None; - }; - if dag.std_list_fold_decl() != Some(*template) { - return None; - } - let formals = fold_template_callable_formals(dag, *template); - let depth_budget = LENS_APPLY_TYPE_WALK_DEPTH as i64; - - for arg in arguments { - let resolved = resolve_template_argument_value(&depth_budget, arguments, arg.value); - let Some(b) = monomorph_callable_bind_root(dag, resolved) else { - continue; - }; - if formals.contains(&arg.parameter) { - return Some(b); - } - } - None -} - -/// WIP gate #2374: R2 evaluator path for lenses whose **runtime inputs are scalar literals only** -/// (`FieldValue::Literal`). Matches PB-runtime-shaped `evaluate` / `evaluate_body` semantics on the -/// lowered lens graph; falls back to [`EvalCtx`] when preconditions fail or evaluation returns a -/// non-literal `Value` this bridge cannot yet map back to [`FieldValue`]. -/// -/// **ProducerLookup discipline:** `ProducerLookup::NoProducer` may fall through to [`EvalCtx`] -/// (`Ok(None)`). Malformed-substrate variants (`MissingPort`, `MissingNode`, `BindCycle`) fail -/// closed as [`LensApplyError`] — they must not collapse into the same silent fallback as legitimate -/// absence (INVARIANTS P2/P3). Evaluator frame/build failures and [`evaluate_body`] errors still -/// return `Ok(None)` so [`EvalCtx`] can recover typed diagnostics (overflow, branch miss, loops). -/// -/// **Not full PB-Runtime retirement:** This helper does **not** complete the Row-4 / -/// interpreter-as-data dissolution by itself; canonical shrink of this module waits on -/// §7.1 receipts. Expand coverage until the bounded-lens host can delete entirely. -/// -/// **Merge:** still gated on §7.1 Row-4 green; this is early signal only. -fn try_apply_lens_via_evaluator_literals_only( - lens_program: &Dag, - root_bind: &BindNode, - inputs: &[FieldValue], -) -> Result, LensApplyError> { - if !inputs.iter().all(|a| matches!(a, FieldValue::Literal(_))) { - return Ok(None); - } - let producer = match lens_program.resolve_producer_lookup(&root_bind.value) { - ProducerLookup::Found(b) => b, - ProducerLookup::NoProducer => return Ok(None), - ProducerLookup::MissingPort { .. } => return Err(LensApplyError::UnresolvedPort), - ProducerLookup::MissingNode { .. } | ProducerLookup::BindCycle { .. } => { - return Err(LensApplyError::MalformedLensRoot); - } - }; - let entry = producer.id(); - let bindings: Vec<(PortId, Value)> = root_bind - .params - .iter() - .zip(inputs.iter()) - .map(|(p, fv)| { - let FieldValue::Literal(bits) = fv else { - unreachable!("filtered to literals above") - }; - (*p, Value::LiteralValue(bits.clone())) - }) - .collect(); - let frame = match EvalFrame::from_bindings(bindings) { - Ok(f) => f, - Err(_) => return Ok(None), - }; - let mut state = EvalStateStack::with_root_frame(frame); - let strategy = EvalStrategy::ApplicativeOrder { - input_order: InputEvaluationOrder::LeftFirst, - }; - let out = match evaluate_body(lens_program, entry, &mut state, strategy) { - Ok(v) => v, - Err(_) => return Ok(None), - }; - match out { - Value::LiteralValue(bits) => Ok(Some(FieldValue::Literal(bits))), - _ => Ok(None), - } -} - -/// Apply a named lens (`Arrow` + `UserDefined` body) from `lens_program` to positional -/// `inputs` (left-to-right with the arrow's formal parameters). -/// -/// When `program_under_test` is `Some`, the R3 fixture lens `auto_loop_parallelism_pending_lens` -/// is evaluated natively against that DAG (iteration-parallelism indicator) so the result is -/// derived from substrate `lane2_workflow` installed during [`crate::compile_to_dag`], not from -/// out-of-band test-runner mutation. -/// -/// **Db18 deviation:** this reads `lane2_workflow` that may have been staged by -/// [`crate::r3_fc_lane2_loop_witness`] from a magic comment — a parallel authority to lowering until -/// the loop workflow is authored from surface syntax. Budget + dissolution: `ROADMAP.md` §"R3 -/// second-batch auto-loop scaffold"; SG-0 pairing class **(b)** on census-touching PRs. -pub fn apply_lens_declaration( - lens_program: &Dag, - program_under_test: Option<&Dag>, - lens_decl_id: DeclarationId, - inputs: &[FieldValue], -) -> Result { - let decl = lens_program.declaration(lens_decl_id); - if let Some(pu) = program_under_test { - if decl.name.as_deref() == Some("auto_loop_parallelism_pending_lens") && inputs.len() == 1 { - let v = pu - .workflow_lane2_subject() - .map(|subject| crate::loop_iteration_parallel_emission_indicator(pu, subject)) - .unwrap_or(0); - return Ok(FieldValue::Literal(LiteralBits::Int(v.to_string()))); - } - } - let TypeConnective::Arrow { - inputs: param_tys, - output: _, - body, - } = &decl.connective - else { - return Err(LensApplyError::NotAnArrow); - }; - let ArrowBody::UserDefined(root) = body else { - return Err(LensApplyError::UnsupportedArrowBody); - }; - if param_tys.len() != inputs.len() { - return Err(LensApplyError::ArityMismatch { - expected: param_tys.len(), - got: inputs.len(), - }); - } - let root_bind = (*root).bind(lens_program); - if root_bind.params.len() != inputs.len() { - return Err(LensApplyError::ArityMismatch { - expected: root_bind.params.len(), - got: inputs.len(), - }); - } - // Reflection (`program_under_test: Some`) supplies rich `FieldValue` shapes the literals-only - // evaluator bridge cannot encode yet — keep `EvalCtx` there until the retirement slice widens. - if program_under_test.is_none() { - if let Some(out) = - try_apply_lens_via_evaluator_literals_only(lens_program, root_bind, inputs)? - { - return Ok(out); - } - } - let mut ctx = EvalCtx::new(lens_program); - for (port, arg) in root_bind.params.iter().zip(inputs.iter()) { - ctx.bind_top(*port, arg.clone()); - } - ctx.eval_port(root_bind.value) -} - -/// Build a substrate-shaped `Dag` record (only `nodes` is populated faithfully) from a -/// compiled program [`Dag`], for lenses like `named_function_count` that read `d.nodes`. -/// -/// Reflection is **complete** per [`docs/design-reflection-completeness.md`](../../docs/design-reflection-completeness.md): -/// every substrate-declared field on each [`Behavior`] variant (and nested carriers such as -/// [`WorkflowEffect`]) projects into [`FieldValue`] with no per-consumer narrowing and no -/// execution semantics (structural `NodeId` / `PortId` references are not followed into -/// callee bodies). -/// -/// `source_file` limits nodes to those authored in that compilation unit (the merged -/// bootstrap graph also lives in the same [`Dag`]). -/// -/// **`id_space` (INVARIANTS P2):** `List` / `Behavior` variant [`DeclarationId`]s in the -/// returned [`FieldValue`] are taken from `id_space` — the same [`Dag`] you pass to -/// [`apply_lens_declaration`] for the lens under test. Shapes still come from `program`'s -/// lowered [`Behavior`] nodes (filtered by `source_file`). Pass `program` for both when the -/// lens and claim program share one compile; pass the canonical lens `Dag` as `id_space` -/// when applying `named_function_count` from the same bytes as `build.rs` splices while -/// reflecting nodes from the claim program. -pub fn reflect_program_dag_nodes_in_file( - program: &Dag, - source_file: &str, - id_space: &Dag, -) -> Result { - let nodes: Vec = program - .nodes() - .iter() - .filter(|b| behavior_source_file(b) == source_file) - .cloned() - .collect(); - let nodes = substrate_reflection::reflect_behavior_list(id_space, &nodes)?; - Ok(FieldValue::Record(vec![("nodes".to_string(), nodes)])) -} - -/// Empty `std.list` spine (`Empty` variant) in the substrate shape expected by `fold`. -pub fn empty_substrate_list_value(dag: &Dag) -> Result { - let (empty_id, _) = v3_list_empty_cons_ids(dag)?; - Ok(FieldValue::Variant { - constructor: empty_id, - payload: vec![], - }) -} - -fn behavior_source_file(behavior: &Behavior) -> &str { - match behavior { - Behavior::Value(v) => v.span.file.as_str(), - Behavior::Transform(t) => t.span.file.as_str(), - Behavior::Branch(b) => b.span.file.as_str(), - Behavior::Loop(l) => l.span.file.as_str(), - Behavior::Bind(b) => b.span.file.as_str(), - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum LensApplyError { - NotAnArrow, - UnsupportedArrowBody, - ArityMismatch { - expected: usize, - got: usize, - }, - MalformedLensRoot, - UnsupportedConstruct(&'static str), - TypeMismatch(&'static str), - UnresolvedPort, - UnresolvedBranchPattern, - UnimplementedCallable(String), - UnimplementedLoopBound, - BranchMiss, - BadFieldProject, - BadListShape, - MissingType(&'static str), - MissingValueBody, - /// PR-E: reserved for fold-driver paths that are not yet delegated to - /// [`apply_lens_declaration`] (e.g. `DimensionReport` / lens-instance carriers). - /// Per Q-Reification Option-A (Dag IS the reflected program), the historical - /// `fold_lens_over_reflected_program` reflect-then-apply driver has been retired; - /// callers go through [`apply_lens_declaration`] directly. - UnimplementedLensFold, - /// Substrate → [`FieldValue`] reflection failed (missing sum/variant wiring in id_space). - SubstrateReflect(&'static str), -} - -impl From for LensApplyError { - fn from(e: substrate_reflection::ReflectError) -> Self { - LensApplyError::SubstrateReflect(e.0) - } -} - -/// Lower a declaration [`ValueBody`] into the structural [`FieldValue`] carrier used by the -/// lens interpreter. -/// -/// `fixture_dag` is the graph that will own [`DeclarationId`] edges once structural bodies carry -/// resolvable `Reference` payloads through this helper; today only scalar + shallow structural -/// clone paths are implemented (no `fixture_dag` lookup yet). -pub fn field_value_from_value_body( - #[allow(unused_variables)] fixture_dag: &Dag, - body: &ValueBody, -) -> Result { - match body { - ValueBody::Scalar(bits) => Ok(FieldValue::Literal(bits.clone())), - ValueBody::Structural { fields } => { - let mut out = Vec::with_capacity(fields.len()); - for (label, fv) in fields { - out.push((label.clone(), fv.clone())); - } - Ok(FieldValue::Record(out)) - } - ValueBody::List(values) => Ok(FieldValue::List(values.clone())), - ValueBody::Map(entries) => Ok(FieldValue::Map(entries.clone())), - ValueBody::Unparsed(_) => Err(LensApplyError::UnsupportedConstruct( - "unparsed declaration value body", - )), - } -} - -/// Frame-based evaluation context for D1 lens interpretation. -/// -/// **Complexity / cloning:** port lookups use [`FieldValue::clone`] and list walks -/// clone elements deliberately — bounded R1 gate scope; revisit if lenses iterate -/// over large program DAGs (e.g. T-LaneE). -struct EvalCtx<'a> { - dag: &'a Dag, - frames: Vec>, -} - -impl<'a> EvalCtx<'a> { - fn new(dag: &'a Dag) -> Self { - Self { - dag, - frames: vec![HashMap::new()], - } - } - - fn bind_top(&mut self, port: PortId, value: FieldValue) { - self.frames - .last_mut() - .expect("root frame") - .insert(port.raw(), value); - } - - fn lookup(&self, port: PortId) -> Option { - self.frames - .iter() - .rev() - .find_map(|m| m.get(&port.raw()).cloned()) - } - - fn bind_current(&mut self, port: PortId, value: FieldValue) { - self.frames - .last_mut() - .expect("frame") - .insert(port.raw(), value); - } - - fn push_frame(&mut self) { - self.frames.push(HashMap::new()); - } - - fn pop_frame(&mut self) { - self.frames.pop(); - if self.frames.is_empty() { - panic!("EvalCtx: popped root frame"); - } - } - - fn eval_port(&mut self, port: PortId) -> Result { - if let Some(v) = self.lookup(port) { - return Ok(v); - } - let producer = self - .dag - .resolve_producer_opt(&port) - .ok_or(LensApplyError::UnresolvedPort)?; - let out = match producer { - Behavior::Value(v) => FieldValue::Literal(v.data.clone()), - Behavior::Transform(t) => self.eval_transform(t)?, - Behavior::Branch(b) => self.eval_branch(b, port)?, - Behavior::Loop(l) => self.eval_loop(l)?, - Behavior::Bind(b) => { - // Only top-level lens roots (`apply_lens_declaration`) and fold step - // binds (`eval_fold_step` / `eval_std_fold`) supply parameters; inner - // parameterized `Bind` producers are not interpreted yet. - if b.params.is_empty() { - self.eval_port(b.value)? - } else { - return Err(LensApplyError::UnsupportedConstruct( - "non-root function bind as producer", - )); - } - } - }; - self.bind_current(port, out.clone()); - Ok(out) - } - - fn eval_transform(&mut self, t: &TransformNode) -> Result { - match &t.target { - TransformTarget::Callable(callee) => { - let decl = self.dag.declaration(*callee); - if is_fold_instantiation(self.dag, decl) && t.inputs.len() == 2 { - if let Some(step_bind) = - find_fold_step_bind_via_instantiation(self.dag, *callee) - { - if step_body_eligible_for_bounded_eval(self.dag, step_bind) { - let list = self.eval_port(t.inputs[0])?; - let init = self.eval_port(t.inputs[1])?; - return self.eval_fold_step(list, init, step_bind); - } - } - } - self.eval_callable(*callee, &t.inputs) - } - TransformTarget::Operator(OperatorKind::Arithmetic(op)) => { - if t.inputs.len() != 2 { - return Err(LensApplyError::ArityMismatch { - expected: 2, - got: t.inputs.len(), - }); - } - let a = int_from_value(&self.eval_port(t.inputs[0])?)?; - let b = int_from_value(&self.eval_port(t.inputs[1])?)?; - let n = apply_arithmetic_int(*op, a, b)?; - Ok(FieldValue::Literal(LiteralBits::Int(n.to_string()))) - } - TransformTarget::Operator(OperatorKind::Comparison(op)) => { - if t.inputs.len() != 2 { - return Err(LensApplyError::ArityMismatch { - expected: 2, - got: t.inputs.len(), - }); - } - let lhs = self.eval_port(t.inputs[0])?; - let rhs = self.eval_port(t.inputs[1])?; - let out = match (&lhs, &rhs) { - ( - FieldValue::Literal(LiteralBits::Int(a)), - FieldValue::Literal(LiteralBits::Int(b)), - ) => { - let ai = BigInt::from_str(a).map_err(|_| { - LensApplyError::TypeMismatch("expected decimal Int literal") - })?; - let bi = BigInt::from_str(b).map_err(|_| { - LensApplyError::TypeMismatch("expected decimal Int literal") - })?; - match op { - crate::dag::ComparisonOp::Eq => ai == bi, - crate::dag::ComparisonOp::Ne => ai != bi, - crate::dag::ComparisonOp::Lt => ai < bi, - crate::dag::ComparisonOp::Le => ai <= bi, - crate::dag::ComparisonOp::Gt => ai > bi, - crate::dag::ComparisonOp::Ge => ai >= bi, - } - } - ( - FieldValue::Literal(LiteralBits::String(a)), - FieldValue::Literal(LiteralBits::String(b)), - ) => match op { - crate::dag::ComparisonOp::Eq => a == b, - crate::dag::ComparisonOp::Ne => a != b, - _ => { - return Err(LensApplyError::UnsupportedConstruct( - "string comparison beyond Eq/Ne", - )); - } - }, - _ => { - return Err(LensApplyError::UnsupportedConstruct( - "comparison operands must both be Int literals or both String literals", - )); - } - }; - Ok(FieldValue::Literal(LiteralBits::Bool(out))) - } - TransformTarget::Operator(OperatorKind::Logical(_)) => Err( - LensApplyError::UnsupportedConstruct("logical operator in lens apply"), - ), - TransformTarget::UnresolvedFieldProject { field_label } - | TransformTarget::ResolvedFieldProject { field_label } => { - if t.inputs.len() != 1 { - return Err(LensApplyError::ArityMismatch { - expected: 1, - got: t.inputs.len(), - }); - } - let base = self.eval_port(t.inputs[0])?; - project_field(&base, field_label) - } - } - } - - fn eval_callable( - &mut self, - callee: DeclarationId, - arg_ports: &[PortId], - ) -> Result { - let decl = self.dag.declaration(callee); - if self.dag.std_list_fold_decl() == Some(callee) { - return self.eval_std_fold(arg_ports); - } - if let TypeConnective::Instantiation { template, .. } = &decl.connective { - // Monomorphized user functions (`count_named_bind`, …) lower as `Instantiation` - // carriers; interpretation follows the generic template `Arrow`. - if self.dag.std_list_fold_decl() != Some(*template) { - return self.eval_callable(*template, arg_ports); - } - } - let name = decl.name.clone().unwrap_or_default(); - let TypeConnective::Arrow { - inputs, - output: _, - body, - } = &decl.connective - else { - return Err(LensApplyError::UnimplementedCallable(format!( - "`{name}` (id={}) is not an evaluated callable arrow", - callee.raw() - ))); - }; - let ArrowBody::UserDefined(root) = body else { - return Err(LensApplyError::UnimplementedCallable(format!( - "`{name}` has no UserDefined body" - ))); - }; - if inputs.len() != arg_ports.len() { - return Err(LensApplyError::ArityMismatch { - expected: inputs.len(), - got: arg_ports.len(), - }); - } - let b = (*root).bind(self.dag); - if b.params.len() != arg_ports.len() { - return Err(LensApplyError::ArityMismatch { - expected: b.params.len(), - got: arg_ports.len(), - }); - } - self.push_frame(); - for (param, arg_port) in b.params.iter().zip(arg_ports.iter()) { - let v = self.eval_port(*arg_port)?; - self.bind_current(*param, v); - } - let out = self.eval_port(b.value); - self.pop_frame(); - out - } - - fn eval_fold_step( - &mut self, - list: FieldValue, - init: FieldValue, - step_bind: &BindNode, - ) -> Result { - let n = step_bind.params.len(); - if n < 2 { - return Err(LensApplyError::ArityMismatch { - expected: 2, - got: n, - }); - } - // Monomorphized `fold` lowers the `|acc, x|` lambda with possible leading - // synthesized parameters; the accumulator and element are always the last two - // formal parameter ports. - let acc_param = step_bind.params[n - 2]; - let elt_param = step_bind.params[n - 1]; - let mut acc = init; - for elt in list_elements(self.dag, &list)? { - self.push_frame(); - self.bind_current(acc_param, acc); - self.bind_current(elt_param, elt); - acc = self.eval_port(step_bind.value)?; - self.pop_frame(); - } - Ok(acc) - } - - fn eval_std_fold(&mut self, arg_ports: &[PortId]) -> Result { - if arg_ports.len() != 3 { - return Err(LensApplyError::ArityMismatch { - expected: 3, - got: arg_ports.len(), - }); - } - let list = self.eval_port(arg_ports[0])?; - let init = self.eval_port(arg_ports[1])?; - let step_bind_port = arg_ports[2]; - let step_producer = self - .dag - .resolve_producer_opt(&step_bind_port) - .ok_or(LensApplyError::UnresolvedPort)?; - let Behavior::Bind(step_bind) = step_producer else { - return Err(LensApplyError::UnsupportedConstruct( - "fold step is not a Bind", - )); - }; - let n = step_bind.params.len(); - if n < 2 { - return Err(LensApplyError::ArityMismatch { - expected: 2, - got: n, - }); - } - let acc_param = step_bind.params[n - 2]; - let elt_param = step_bind.params[n - 1]; - let mut acc = init; - for elt in list_elements(self.dag, &list)? { - self.push_frame(); - self.bind_current(acc_param, acc); - self.bind_current(elt_param, elt); - acc = self.eval_port(step_bind.value)?; - self.pop_frame(); - } - Ok(acc) - } - - fn eval_branch( - &mut self, - b: &BranchNode, - out_port: PortId, - ) -> Result { - let disc = self.eval_port(b.input)?; - for path in &b.paths { - let variant_id = match &path.pattern { - BranchPattern::ResolvedVariant(id) => *id, - BranchPattern::UnresolvedVariant { .. } => { - return Err(LensApplyError::UnresolvedBranchPattern); - } - }; - if !variant_matches(self.dag, &disc, variant_id)? { - continue; - } - self.push_frame(); - if let Some(binding) = &path.binding { - let payload = variant_payload_for_binding(self.dag, &disc, variant_id)?; - self.bind_current(binding.payload_port, payload); - } - let v = self.eval_port(path.output)?; - self.pop_frame(); - self.bind_current(out_port, v.clone()); - return Ok(v); - } - Err(LensApplyError::BranchMiss) - } - - /// Loop bodies are intentionally uninterpreted in D1. - /// - /// **Fail-closed receipt:** both `LoopBound::Cardinality` (single-fn recursion) - /// and `LoopBound::Descent` (mutual-recursion clusters) return - /// [`LensApplyError::UnimplementedLoopBound`] until iteration semantics land in - /// this interpreter. - fn eval_loop(&mut self, _l: &crate::dag::LoopNode) -> Result { - Err(LensApplyError::UnimplementedLoopBound) - } -} - -fn int_from_value(v: &FieldValue) -> Result { - match v { - FieldValue::Literal(LiteralBits::Int(s)) => s - .parse::() - .map_err(|_| LensApplyError::TypeMismatch("expected Int literal in i64 range")), - _ => Err(LensApplyError::TypeMismatch("expected Int literal")), - } -} - -/// Checked `Int` arithmetic for the D1 lens interpreter (INVARIANTS P3: no wrapping `+`/`-`/`*`/`/` -/// that could fabricate results or panic in debug under `LensOutputEquals` / gate paths). -fn apply_arithmetic_int( - op: crate::dag::ArithmeticOp, - a: i64, - b: i64, -) -> Result { - const OVERFLOW: LensApplyError = LensApplyError::UnsupportedConstruct("integer overflow"); - match op { - crate::dag::ArithmeticOp::Add => a.checked_add(b).ok_or(OVERFLOW), - crate::dag::ArithmeticOp::Sub => a.checked_sub(b).ok_or(OVERFLOW), - crate::dag::ArithmeticOp::Mul => a.checked_mul(b).ok_or(OVERFLOW), - crate::dag::ArithmeticOp::Div => { - if b == 0 { - return Err(LensApplyError::TypeMismatch("division by zero")); - } - a.checked_div(b).ok_or(OVERFLOW) - } - } -} - -fn project_field(base: &FieldValue, label: &str) -> Result { - let FieldValue::Record(fields) = base else { - return Err(LensApplyError::BadFieldProject); - }; - fields - .iter() - .find(|(l, _)| l == label) - .map(|(_, v)| v.clone()) - .ok_or(LensApplyError::BadFieldProject) -} - -fn variant_matches( - dag: &Dag, - value: &FieldValue, - variant_ty: DeclarationId, -) -> Result { - match value { - FieldValue::Variant { constructor, .. } => Ok(*constructor == variant_ty), - FieldValue::Literal(LiteralBits::Bool(b)) => { - let expected_ty = dag - .bool_runtime_variant_id(*b) - .ok_or(LensApplyError::MissingType("Bool variant"))?; - Ok(expected_ty == variant_ty) - } - _ => Ok(false), - } -} - -fn variant_payload_for_binding( - dag: &Dag, - value: &FieldValue, - variant_ty: DeclarationId, -) -> Result { - if let FieldValue::Literal(LiteralBits::Bool(_)) = value { - if !variant_matches(dag, value, variant_ty)? { - return Err(LensApplyError::BadFieldProject); - } - let conj = dag.declaration(variant_ty); - return match &conj.connective { - TypeConnective::Conj { children } if children.is_empty() => { - Ok(FieldValue::Record(vec![])) - } - TypeConnective::Conj { .. } => Err(LensApplyError::UnsupportedConstruct( - "Bool match arm with payload fields is not supported for Bool literal scrutinee", - )), - _ => Ok(FieldValue::Record(vec![])), - }; - } - let FieldValue::Variant { - constructor, - payload, - } = value - else { - return Err(LensApplyError::BadFieldProject); - }; - if *constructor != variant_ty { - return Err(LensApplyError::BadFieldProject); - } - if payload.len() == 1 { - return Ok(payload[0].clone()); - } - let conj = dag.declaration(variant_ty); - let TypeConnective::Conj { children } = &conj.connective else { - // Unit-like variant (e.g. `True` / `False` / `Empty`). - return Ok(FieldValue::Record(vec![])); - }; - if payload.len() != children.len() { - return Err(LensApplyError::BadFieldProject); - } - let fields: Vec<_> = children - .iter() - .zip(payload.iter()) - .map(|(c, v)| (c.label.clone(), v.clone())) - .collect(); - Ok(FieldValue::Record(fields)) -} - -/// Uncons a substrate `List` spine into owned elements (clones each head). -fn list_elements(dag: &Dag, list: &FieldValue) -> Result, LensApplyError> { - let mut out = Vec::new(); - let mut cur = list; - loop { - let FieldValue::Variant { - constructor, - payload, - } = cur - else { - return Err(LensApplyError::BadListShape); - }; - let label = variant_label(dag, *constructor).ok_or(LensApplyError::BadListShape)?; - match label.as_str() { - "Empty" => break, - "Cons" => { - if payload.len() != 2 { - return Err(LensApplyError::BadListShape); - } - out.push(payload[0].clone()); - cur = &payload[1]; - } - _ => return Err(LensApplyError::BadListShape), - } - } - Ok(out) -} - -// perf: linear scan over all declarations per variant id — fine for R1 gate DAGs; -// revisit if lenses iterate over large programs (prefer declaration-indexed lookup). -fn variant_label(dag: &Dag, variant_id: DeclarationId) -> Option { - dag.declarations() - .iter() - .find_map(|decl| match &decl.connective { - TypeConnective::Disj { variants } => variants - .iter() - .find(|variant| variant.ty == variant_id) - .map(|variant| variant.label.clone()), - _ => None, - }) -} - -// perf: linear scan for `List` disj — same bounded-gate rationale as `variant_label`. -fn v3_list_empty_cons_ids(dag: &Dag) -> Result<(DeclarationId, DeclarationId), LensApplyError> { - let list_decl = dag.declarations().iter().find(|d| { - d.name.as_deref() == Some("List") && matches!(d.connective, TypeConnective::Disj { .. }) - }); - let Some(list_decl) = list_decl else { - return Err(LensApplyError::MissingType("List")); - }; - let TypeConnective::Disj { variants } = &list_decl.connective else { - return Err(LensApplyError::MissingType("List")); - }; - let empty = variants - .iter() - .find(|v| v.label == "Empty") - .ok_or(LensApplyError::MissingType("List.Empty"))? - .ty; - let cons = variants - .iter() - .find(|v| v.label == "Cons") - .ok_or(LensApplyError::MissingType("List.Cons"))? - .ty; - Ok((empty, cons)) -} - -/// Fixed `(a, b, c)` triples for R1 `AlgebraicLaw(Associativity, …)` operational checks. -/// -/// A **single** triple can pass for operations that are not associative (coincidence). The -/// runner requires **every** triple here to satisfy [`int_associativity_holds`] — still not a -/// quantified substrate law proof, but a materially stronger witness than one sample. Magnitudes -/// stay modest so `Int` addition-style lenses do not overflow during witness application. -pub const ASSOCIATIVITY_WITNESS_TRIPLES: &[(i64, i64, i64)] = &[ - (2, 3, 5), - (0, 1, 99), - (-3, 7, 2), - (-1, 0, 1), - (1, 1, 2), - (5, 0, 3), - (10, -4, 7), - (100, 200, 300), -]; - -/// Fixed `(a, b)` pairs for PR-B.3 `AlgebraicLaw(Commutativity, …)` operational checks. -/// -/// These are runner witnesses, not substrate law proofs. The sample table lives next to the -/// associativity table so law-runner scaffolds share one bounded Int witness authority until -/// first-class substrate law witnesses dissolve the runner-side sample checks. -pub const COMMUTATIVITY_WITNESS_PAIRS: &[(i64, i64)] = &[ - (2, 3), - (0, 99), - (-3, 7), - (-1, 0), - (1, 1), - (5, 0), - (10, -4), - (100, 200), -]; - -/// Samples for `AlgebraicLaw(Identity, …)` operational checks: **every** value must appear in at -/// least one [`COMMUTATIVITY_WITNESS_PAIRS`] coordinate so identity witnessing stays materially -/// aligned with the commutativity witness authority (still not a substrate law proof). -pub const IDENTITY_WITNESS_SAMPLES: &[i64] = &[-4, -3, -1, 0, 1, 2, 3, 5, 7, 10, 99, 100, 200]; - -/// Candidate identity elements searched left-to-right; exactly one must satisfy -/// `e ⊕ a = a` and `a ⊕ e = a` for every sample in [`IDENTITY_WITNESS_SAMPLES`]. Multiple matches -/// fail closed (`Ok(false)`) so incidental finite-table coincidences cannot certify an ambiguous op. -/// -/// **High-end redundancy:** keep **at least two** entries **strictly greater** than -/// `max(IDENTITY_WITNESS_SAMPLES)` (currently `200`). With only one such candidate, a binary `Int` -/// lens behaving like `min` can satisfy the finite witness with a unique “top-like” constant even -/// though `Int` has no lattice top (INVARIANTS P1/P3; codex PR #2394). Several candidates also sit -/// below `min(samples)` so `max`-like ops typically hit the same multiplicity fail-closure. -pub const IDENTITY_WITNESS_CANDIDATES: &[i64] = &[ - -300, -200, -100, -99, -10, -7, -5, -4, -3, -2, -1, 0, 1, 2, 3, 5, 7, 10, 99, 100, 200, 250, - 300, 400, -]; - -/// Evaluate `(a ⊕ b) ⊕ c` vs `a ⊕ (b ⊕ c)` for a binary `Int` lens using [`apply_lens_declaration`]. -pub fn int_associativity_holds( - program_dag: &Dag, - lens_decl_id: DeclarationId, - a: i64, - b: i64, - c: i64, -) -> Result { - let int = |n: i64| FieldValue::Literal(literal_bits_int(n)); - let left_ab = apply_lens_declaration(program_dag, None, lens_decl_id, &[int(a), int(b)])?; - let left = apply_lens_declaration(program_dag, None, lens_decl_id, &[left_ab, int(c)])?; - let right_bc = apply_lens_declaration(program_dag, None, lens_decl_id, &[int(b), int(c)])?; - let right = apply_lens_declaration(program_dag, None, lens_decl_id, &[int(a), right_bc])?; - Ok(left == right) -} - -/// True iff [`int_associativity_holds`] succeeds for every triple in `triples`. -pub fn int_associativity_holds_all_triples( - program_dag: &Dag, - lens_decl_id: DeclarationId, - triples: &[(i64, i64, i64)], -) -> Result { - for &(a, b, c) in triples { - if !int_associativity_holds(program_dag, lens_decl_id, a, b, c)? { - return Ok(false); - } - } - Ok(true) -} - -/// True iff exactly one `e` in `candidates` satisfies left/right identity against every `samples` -/// entry via [`apply_lens_declaration`]. -pub fn int_identity_witness_holds( - program_dag: &Dag, - lens_decl_id: DeclarationId, - samples: &[i64], - candidates: &[i64], -) -> Result { - let int = |n: i64| FieldValue::Literal(literal_bits_int(n)); - let mut matching = 0_i32; - for &e in candidates { - let ev = int(e); - let mut ok = true; - for &a in samples { - let left = - apply_lens_declaration(program_dag, None, lens_decl_id, &[ev.clone(), int(a)])?; - let right = - apply_lens_declaration(program_dag, None, lens_decl_id, &[int(a), ev.clone()])?; - if left != int(a) || right != int(a) { - ok = false; - break; - } - } - if ok { - matching += 1; - if matching > 1 { - return Ok(false); - } - } - } - Ok(matching == 1) -} - -const _: () = assert!(IDENTITY_WITNESS_SAMPLES.len() > 1); -const _: () = assert!(IDENTITY_WITNESS_CANDIDATES.len() > 1); - -#[cfg(test)] -mod tests { - use super::*; - use crate::compile_to_dag; - use crate::dag::{Behavior, LoopBound}; - - #[test] - fn named_function_count_on_trivial_program() { - let src = include_str!("../../lenses/named_function_count.dag"); - let lens_dag = - compile_to_dag(src, "src/v3/lenses/named_function_count.dag").expect("lens compiles"); - let prog = compile_to_dag("let x: Int = 1", "lens_apply_prog.v3").expect("prog compiles"); - let lens_id = lens_dag - .declaration_by_name("named_function_count") - .expect("named_function_count") - .id; - let input = reflect_program_dag_nodes_in_file(&prog, "lens_apply_prog.v3", &lens_dag) - .expect("reflect"); - let out = apply_lens_declaration(&lens_dag, None, lens_id, &[input]).expect("apply"); - assert_eq!(out, FieldValue::Literal(literal_bits_int(1))); - } - - #[test] - fn identity_witness_samples_align_with_commutativity_witness_authority() { - for &s in IDENTITY_WITNESS_SAMPLES { - assert!( - COMMUTATIVITY_WITNESS_PAIRS - .iter() - .any(|&(a, b)| a == s || b == s), - "every IDENTITY_WITNESS_SAMPLES entry must appear in COMMUTATIVITY_WITNESS_PAIRS \ - (doc invariant on IDENTITY_WITNESS_SAMPLES); missing sample {s}" - ); - } - } - - #[test] - fn identity_witness_candidate_table_has_redundant_high_and_low_extents() { - let sample_min = *IDENTITY_WITNESS_SAMPLES.iter().min().unwrap(); - let sample_max = *IDENTITY_WITNESS_SAMPLES.iter().max().unwrap(); - let highs: Vec = IDENTITY_WITNESS_CANDIDATES - .iter() - .copied() - .filter(|&c| c > sample_max) - .collect(); - let lows: Vec = IDENTITY_WITNESS_CANDIDATES - .iter() - .copied() - .filter(|&c| c < sample_min) - .collect(); - assert!( - highs.len() >= 2, - "need ≥2 candidates strictly above sample max ({sample_max}) so min-like lenses cannot \ - mint a unique false identity; got {highs:?}" - ); - assert!( - lows.len() >= 2, - "need ≥2 candidates strictly below sample min ({sample_min}) for symmetric fail-closure \ - skew; got {lows:?}" - ); - } - - #[test] - fn int_add_lens_associativity_sample() { - let src = r#"module w -fn lens_composition_op(a: Int, b: Int) -> Int = a + b -"#; - let dag = compile_to_dag(src, "assoc.v3").expect("compiles"); - let id = dag.declaration_by_name("lens_composition_op").unwrap().id; - assert!( - int_associativity_holds_all_triples(&dag, id, ASSOCIATIVITY_WITNESS_TRIPLES) - .expect("assoc witness"), - "Int `+` lens must pass every ASSOCIATIVITY_WITNESS_TRIPLES entry" - ); - } - - #[test] - fn literals_only_evaluator_bridge_agrees_with_eval_ctx_for_int_add() { - let src = r#"module w -fn lens_composition_op(a: Int, b: Int) -> Int = a + b -"#; - let dag = compile_to_dag(src, "eval_bridge_vs_ctx.v3").expect("compiles"); - let lens_decl_id = dag.declaration_by_name("lens_composition_op").unwrap().id; - let decl = dag.declaration(lens_decl_id); - let TypeConnective::Arrow { body, .. } = &decl.connective else { - panic!("expected arrow lens"); - }; - let ArrowBody::UserDefined(root) = body else { - panic!("expected user-defined arrow body"); - }; - let root_bind = (*root).bind(&dag); - let inputs = &[ - FieldValue::Literal(literal_bits_int(11)), - FieldValue::Literal(literal_bits_int(-3)), - ]; - let via_bridge = super::try_apply_lens_via_evaluator_literals_only(&dag, root_bind, inputs) - .expect("evaluator bridge lookup") - .expect("literal Int add must succeed on evaluator bridge"); - let mut ctx = EvalCtx::new(&dag); - for (port, arg) in root_bind.params.iter().zip(inputs.iter()) { - ctx.bind_top(*port, arg.clone()); - } - let via_ctx = ctx.eval_port(root_bind.value).expect("eval ctx"); - assert_eq!(via_bridge, via_ctx); - } - - #[test] - fn literals_only_with_program_under_test_skips_bridge_matches_bare_apply() { - let lens_src = r#"module w -fn lens_composition_op(a: Int, b: Int) -> Int = a + b -"#; - let lens_dag = compile_to_dag(lens_src, "reflect_skip_bridge_lens.v3").expect("compiles"); - let prog_under_test = - compile_to_dag("let x: Int = 1", "reflect_skip_bridge_prog.v3").expect("prog compiles"); - let id = lens_dag - .declaration_by_name("lens_composition_op") - .unwrap() - .id; - let inputs = &[ - FieldValue::Literal(literal_bits_int(5)), - FieldValue::Literal(literal_bits_int(7)), - ]; - let bare = apply_lens_declaration(&lens_dag, None, id, inputs).expect("bare apply"); - let with_put = - apply_lens_declaration(&lens_dag, Some(&prog_under_test), id, inputs).expect("put"); - assert_eq!(bare, with_put); - } - - #[test] - fn int_add_lens_identity_witness_finds_zero_uniquely() { - let src = r#"module w -fn lens_composition_op(a: Int, b: Int) -> Int = a + b -"#; - let dag = compile_to_dag(src, "id_add.v3").expect("compiles"); - let id = dag.declaration_by_name("lens_composition_op").unwrap().id; - assert!( - super::int_identity_witness_holds( - &dag, - id, - IDENTITY_WITNESS_SAMPLES, - IDENTITY_WITNESS_CANDIDATES - ) - .expect("identity witness"), - "Int `+` lens identity must be uniquely 0 on the bounded witness tables" - ); - } - - #[test] - fn int_mul_lens_identity_witness_finds_one_uniquely() { - let src = r#"module w -fn lens_mul_op(a: Int, b: Int) -> Int = a * b -"#; - let dag = compile_to_dag(src, "id_mul.v3").expect("compiles"); - let id = dag.declaration_by_name("lens_mul_op").unwrap().id; - assert!( - super::int_identity_witness_holds( - &dag, - id, - IDENTITY_WITNESS_SAMPLES, - IDENTITY_WITNESS_CANDIDATES - ) - .expect("identity witness"), - "Int `*` lens identity must be uniquely 1 on the bounded witness tables" - ); - } - - #[test] - fn int_min_lens_identity_witness_fails_closed_on_ambiguous_top_candidates() { - let src = r#"module w -fn lens_min_op(a: Int, b: Int) -> Int = if a < b then a else b -"#; - let dag = compile_to_dag(src, "id_min.v3").expect("compiles"); - let id = dag.declaration_by_name("lens_min_op").unwrap().id; - assert!( - !super::int_identity_witness_holds( - &dag, - id, - IDENTITY_WITNESS_SAMPLES, - IDENTITY_WITNESS_CANDIDATES - ) - .expect("identity witness"), - "binary Int min has no identity on bounded samples; bounded witness must fail closed" - ); - } - - #[test] - fn int_add_overflow_returns_lens_apply_error_not_wrapped_value() { - let src = r#"module m -fn f(a: Int, b: Int) -> Int = a + b -"#; - let dag = compile_to_dag(src, "int_ovf.v3").expect("compiles"); - let id = dag.declaration_by_name("f").unwrap().id; - let err = apply_lens_declaration( - &dag, - None, - id, - &[ - FieldValue::Literal(literal_bits_int(i64::MAX)), - FieldValue::Literal(literal_bits_int(1)), - ], - ) - .expect_err("overflow must not yield a wrapped Int"); - assert!( - matches!( - err, - super::LensApplyError::UnsupportedConstruct("integer overflow") - ), - "{err:?}" - ); - } - - #[test] - fn empty_fold_returns_init() { - let src = r#"module m -import std.list { List, fold } - -fn sum(xs: List) -> Int = - fold(xs, 99, |acc, x| acc + x) -"#; - let dag = compile_to_dag(src, "empty_fold.v3").expect("compiles"); - let sum_id = dag.declaration_by_name("sum").expect("sum").id; - let empty = empty_substrate_list_value(&dag).expect("empty list"); - let out = apply_lens_declaration(&dag, None, sum_id, &[empty]).expect("fold empty"); - assert_eq!(out, FieldValue::Literal(literal_bits_int(99))); - } - - #[test] - fn monomorphized_fold_step_bind_recovered_via_instantiation_arguments() { - let src = r#"module m -import std.list { List, fold } - -fn sum(xs: List) -> Int = - fold(xs, 99, |acc, x| acc + x) -"#; - let dag = compile_to_dag(src, "mono_fold_bind_paths.v3").expect("compiles"); - let fold_transform = dag - .nodes() - .iter() - .find_map(|n| { - let Behavior::Transform(t) = n else { - return None; - }; - let TransformTarget::Callable(callee) = &t.target else { - return None; - }; - super::is_fold_instantiation(&dag, dag.declaration(*callee)).then_some(t) - }) - .expect("monomorphized fold transform"); - let TransformTarget::Callable(callee) = &fold_transform.target else { - unreachable!(); - }; - super::find_fold_step_bind_via_instantiation(&dag, *callee).expect("fold step bind"); - } - - #[test] - fn fold_step_lookup_requires_template_formal_edge() { - let src = r#"module m -import std.list { List, fold } - -fn sum(xs: List) -> Int = - fold(xs, 99, |acc, x| acc + x) -"#; - let mut dag = compile_to_dag(src, "mono_fold_bind_no_formal_edge.v3").expect("compiles"); - let fold_callable = dag - .nodes() - .iter() - .find_map(|n| { - let Behavior::Transform(t) = n else { - return None; - }; - let TransformTarget::Callable(callee) = &t.target else { - return None; - }; - super::is_fold_instantiation(&dag, dag.declaration(*callee)).then_some(*callee) - }) - .expect("monomorphized fold transform"); - let TypeConnective::Instantiation { - template, - arguments, - } = &dag.declaration(fold_callable).connective - else { - panic!("fold callable should be an Instantiation"); - }; - let formals = super::fold_template_callable_formals(&dag, *template); - let depth_budget = LENS_APPLY_TYPE_WALK_DEPTH as i64; - let callable_arg_index = arguments - .iter() - .position(|arg| { - let resolved = resolve_template_argument_value(&depth_budget, arguments, arg.value); - super::monomorph_callable_bind_root(&dag, resolved).is_some() - && formals.contains(&arg.parameter) - }) - .expect("callable fold argument"); - let non_formal_parameter = arguments - .iter() - .map(|arg| arg.parameter) - .find(|parameter| !formals.contains(parameter)) - .expect("non-callable fold formal parameter"); - - let TypeConnective::Instantiation { arguments, .. } = - &mut dag.declaration_mut(fold_callable).connective - else { - panic!("fold callable should remain an Instantiation"); - }; - arguments[callable_arg_index].parameter = non_formal_parameter; - - assert!( - super::find_fold_step_bind_via_instantiation(&dag, fold_callable).is_none(), - "unique callable candidates must not be accepted without the template-formal edge" - ); - } - - #[test] - fn comparison_operator_returns_bool_literal_not_sum_variant() { - let src = r#"module m -fn truth() -> Bool = 1 == 1 -"#; - let dag = compile_to_dag(src, "truth.v3").expect("compiles"); - let id = dag.declaration_by_name("truth").unwrap().id; - let out = apply_lens_declaration(&dag, None, id, &[]).expect("apply"); - assert_eq!(out, FieldValue::Literal(LiteralBits::Bool(true))); - } - - #[test] - fn bool_literal_scrutinee_selects_disj_match_arm() { - let src = r#"module m -fn p(b: Bool) -> Int = match b { True => 1, False => 0 } -"#; - let dag = compile_to_dag(src, "bool_lit_match.v3").expect("compiles"); - let id = dag.declaration_by_name("p").unwrap().id; - let out = apply_lens_declaration( - &dag, - None, - id, - &[FieldValue::Literal(LiteralBits::Bool(true))], - ) - .expect("apply"); - assert_eq!(out, FieldValue::Literal(literal_bits_int(1))); - } - - #[test] - fn mutual_recursion_lens_hits_unimplemented_loop_for_descent_cluster() { - let src = r#"module m -fn even(n: Int) -> Bool = if n == 0 then true else odd(n - 1) -fn odd(n: Int) -> Bool = if n == 0 then false else even(n - 1) -fn run_even(n: Int) -> Bool = even(n) -"#; - let dag = compile_to_dag(src, "mutual_lens.v3").expect("compiles"); - assert!( - dag.nodes() - .iter() - .filter_map(Behavior::as_loop) - .any(|l| { matches!(l.bound, LoopBound::Descent { .. }) }), - "expected at least one LoopBound::Descent (mutual cluster)" - ); - let run_even = dag.declaration_by_name("run_even").expect("run_even").id; - let err = apply_lens_declaration( - &dag, - None, - run_even, - &[FieldValue::Literal(literal_bits_int(1))], - ) - .expect_err("loop interpretation is unimplemented"); - assert!( - matches!(err, LensApplyError::UnimplementedLoopBound), - "expected UnimplementedLoopBound, got {err:?}" - ); - } - - #[test] - fn apply_lens_declaration_arity_mismatch() { - let src = r#"module m -fn f(a: Int, b: Int) -> Int = a + b -"#; - let dag = compile_to_dag(src, "arity.v3").expect("compiles"); - let id = dag.declaration_by_name("f").unwrap().id; - let err = - apply_lens_declaration(&dag, None, id, &[FieldValue::Literal(literal_bits_int(1))]) - .expect_err("wrong arity"); - assert!( - matches!( - err, - LensApplyError::ArityMismatch { - expected: 2, - got: 1 - } - ), - "{err:?}" - ); - } - - #[test] - fn eligible_fold_runs_through_bounded_path() { - // Step body is `acc + x` — no Loop, no unsupported construct in transitive call graph. - // The structural eligibility predicate must approve and the bounded fast path must - // produce the sum. - let src = r#"module m -import std.list { List, fold } - -fn sum(xs: List) -> Int = - fold(xs, 0, |acc, x| acc + x) -"#; - let dag = compile_to_dag(src, "eligible_fold.v3").expect("compiles"); - let sum_id = dag.declaration_by_name("sum").expect("sum").id; - let (empty_id, cons_id) = super::v3_list_empty_cons_ids(&dag).expect("list ids"); - let int = |n: i64| FieldValue::Literal(literal_bits_int(n)); - let mut list = FieldValue::Variant { - constructor: empty_id, - payload: vec![], - }; - for n in [3i64, 2, 1] { - list = FieldValue::Variant { - constructor: cons_id, - payload: vec![int(n), list], - }; - } - let out = apply_lens_declaration(&dag, None, sum_id, &[list]).expect("eligible fold runs"); - assert_eq!(out, int(6)); - } - - #[test] - fn ineligible_fold_with_recursive_step_skips_bounded_path() { - // `helper` is self-recursive → lowers to a `LoopNode` in the step body's transitive - // call graph. The structural eligibility predicate must reject; the bounded fast path - // is bypassed; `eval_callable` rejects the monomorphized `Instantiation { template: - // fold }` with `UnimplementedCallable` (fail-closed, vs running and hitting - // `UnimplementedLoopBound` deeper in). - let src = r#"module m -import std.list { List, fold } - -fn helper(n: Int) -> Int = if n == 0 then 0 else helper(n - 1) + 1 -fn sum(xs: List) -> Int = - fold(xs, 0, |acc, x| acc + helper(x)) -"#; - let dag = compile_to_dag(src, "ineligible_fold.v3").expect("compiles"); - let sum_id = dag.declaration_by_name("sum").expect("sum").id; - let (_, cons_id) = super::v3_list_empty_cons_ids(&dag).expect("list ids"); - let empty = empty_substrate_list_value(&dag).expect("empty list"); - let one = FieldValue::Variant { - constructor: cons_id, - payload: vec![FieldValue::Literal(literal_bits_int(1)), empty], - }; - let err = - apply_lens_declaration(&dag, None, sum_id, &[one]).expect_err("ineligible step body"); - assert!( - matches!(err, LensApplyError::UnimplementedCallable(_)), - "{err:?}" - ); - } - - #[test] - fn branch_miss_when_scrutinee_matches_no_arm() { - let src = r#"module m -fn p(b: Bool) -> Int = match b { True => 1, False => 0 } -"#; - let dag = compile_to_dag(src, "branch_miss.v3").expect("compiles"); - let id = dag.declaration_by_name("p").unwrap().id; - let err = - apply_lens_declaration(&dag, None, id, &[FieldValue::Literal(literal_bits_int(42))]) - .expect_err("Int is not True/False"); - assert!(matches!(err, LensApplyError::BranchMiss), "{err:?}"); - } -} - -// Substrate ↔ `FieldValue` structural reflection (design-reflection-completeness). -// SG-0: folded under this file — the census forbids a new sibling `substrate_reflection.rs`. -mod substrate_reflection { - //! Complete structural reflection of computation-substrate [`Behavior`] nodes into - //! lens-input [`FieldValue`] per `docs/design-reflection-completeness.md` (LOCKED). - //! Reflection is static: no execution, no branch-arm selection, no loop iteration. - - use crate::dag::{ - AtomPayload, Behavior, BindEmitParticipation, BindNode, BoolPortRef, BranchArm, - BranchEmitParticipation, BranchNode, BranchPattern, BreakingShape, CardinalityBound, - ClusterId, CreateCause, Dag, DeclarationId, EffectShape, FieldValue, HttpMethodScalar, - IdempotentShape, KeySource, LiteralBits, LoopBound, LoopNode, NodeId, NonSingletonList, - OperationEffect, OperatorKind, Path, PayloadBinding, PortId, TransformNode, - TransformTarget, TypeConnective, ValueNode, WorkflowEffect, - }; - use crate::diagnostics::SourceSpan; - - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct ReflectError(pub &'static str); - - pub type ReflectResult = Result; - - fn err(msg: &'static str) -> ReflectResult { - Err(ReflectError(msg)) - } - - fn disj_variant_ty( - dag: &Dag, - sum_name: &str, - variant_label: &str, - ) -> ReflectResult { - let mut decl_id = dag - .declaration_by_name(sum_name) - .ok_or(ReflectError("missing sum type"))? - .id; - const PEEL_MAX: usize = 64; - for _ in 0..PEEL_MAX { - let decl = dag.declaration(decl_id); - match &decl.connective { - TypeConnective::Instantiation { - template, - arguments, - } if arguments.is_empty() => { - decl_id = *template; - } - TypeConnective::Disj { variants } => { - return variants - .iter() - .find(|v| v.label == variant_label) - .map(|v| v.ty) - .ok_or(ReflectError("missing sum variant")); - } - TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(name)) - if name == variant_label => - { - // Single-variant enum: `Instantiation → Atom(Label)` (no `Disj` row). - return Ok(decl_id); - } - _ => return err("sum type is not Disj or unit Atom"), - } - } - err("Instantiation peel depth exceeded") - } - - fn port_fv(p: PortId) -> FieldValue { - FieldValue::Literal(LiteralBits::Int(i64::from(p.raw()).to_string())) - } - - fn node_fv(n: NodeId) -> FieldValue { - FieldValue::Literal(LiteralBits::Int(i64::from(n.raw()).to_string())) - } - - fn cluster_fv(c: ClusterId) -> FieldValue { - FieldValue::Literal(LiteralBits::Int(i64::from(c.raw()).to_string())) - } - - fn reflect_source_span(span: &SourceSpan) -> FieldValue { - // Field names must match `type SourceSpan` in `dsl/std/types.dag` (`file`, `start`, `end`). - // The Rust carrier still uses `byte_start` / `byte_end` on [`SourceSpan`]. - FieldValue::Record(vec![ - ( - "file".to_string(), - FieldValue::Literal(LiteralBits::String(span.file.clone())), - ), - ( - "start".to_string(), - FieldValue::Literal(LiteralBits::Int(i64::from(span.byte_start).to_string())), - ), - ( - "end".to_string(), - FieldValue::Literal(LiteralBits::Int(i64::from(span.byte_end).to_string())), - ), - ]) - } - - fn v3_list_empty_cons_ids(dag: &Dag) -> ReflectResult<(DeclarationId, DeclarationId)> { - let list_decl = dag.declarations().iter().find(|d| { - d.name.as_deref() == Some("List") && matches!(d.connective, TypeConnective::Disj { .. }) - }); - let Some(list_decl) = list_decl else { - return err("List"); - }; - let TypeConnective::Disj { variants } = &list_decl.connective else { - return err("List"); - }; - let empty = variants - .iter() - .find(|v| v.label == "Empty") - .ok_or(ReflectError("List.Empty"))? - .ty; - let cons = variants - .iter() - .find(|v| v.label == "Cons") - .ok_or(ReflectError("List.Cons"))? - .ty; - Ok((empty, cons)) - } - - fn named_record_type_root(dag: &Dag, name: &str) -> ReflectResult { - let mut decl_id = dag - .declaration_by_name(name) - .ok_or(ReflectError("missing substrate record type"))? - .id; - const PEEL_MAX: usize = 64; - for _ in 0..PEEL_MAX { - match &dag.declaration(decl_id).connective { - TypeConnective::Conj { .. } => return Ok(decl_id), - TypeConnective::Instantiation { - template, - arguments, - } if arguments.is_empty() => { - decl_id = *template; - } - _ => return err("substrate record type is not Conj"), - } - } - err("substrate record peel depth exceeded") - } - - fn conj_field_ty( - dag: &Dag, - conj_decl_id: DeclarationId, - label: &str, - ) -> ReflectResult { - let decl = dag.declaration(conj_decl_id); - let TypeConnective::Conj { children } = &decl.connective else { - return err("expected Conj"); - }; - children - .iter() - .find(|c| c.label == label) - .map(|c| c.ty) - .ok_or(ReflectError("missing Conj field")) - } - - /// [`FieldValue::Variant::payload`] is **positional**: one entry per field of the - /// variant payload [`TypeConnective::Conj`], in `children` order — same contract as - /// `variant_payload_for_binding` in this file. - fn sum_variant_payload( - dag: &Dag, - sum_name: &str, - variant_label: &str, - payload: Vec, - ) -> ReflectResult { - let ctor_ty = disj_variant_ty(dag, sum_name, variant_label)?; - match &dag.declaration(ctor_ty).connective { - TypeConnective::Conj { children } => { - if children.len() != payload.len() { - return err("sum variant payload arity mismatch"); - } - } - _ => { - if payload.len() > 1 { - return err("sum variant payload arity mismatch"); - } - } - } - Ok(FieldValue::Variant { - constructor: ctor_ty, - payload, - }) - } - - fn peel_to_optional_cardinality_decl( - dag: &Dag, - mut ty: DeclarationId, - ) -> ReflectResult { - const PEEL_MAX: usize = 64; - for _ in 0..PEEL_MAX { - match &dag.declaration(ty).connective { - TypeConnective::Cardinality(p) if p.bound() == CardinalityBound::AtMostOne => { - return Ok(ty); - } - TypeConnective::Instantiation { template, .. } => ty = *template, - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => ty = *next, - _ => return err("expected optional T? cardinality"), - } - } - err("optional cardinality peel depth exceeded") - } - - fn optional_some_none_constructor_ids( - dag: &Dag, - cardinality_decl_id: DeclarationId, - ) -> ReflectResult<(DeclarationId, DeclarationId)> { - let disj_id = dag - .optional_match_disj(cardinality_decl_id) - .ok_or(ReflectError("missing optional Some/None sum"))?; - let decl = dag.declaration(disj_id); - let TypeConnective::Disj { variants } = &decl.connective else { - return err("optional match row is not Disj"); - }; - let some_ty = variants - .iter() - .find(|v| v.label == "Some") - .ok_or(ReflectError("Some"))? - .ty; - let none_ty = variants - .iter() - .find(|v| v.label == "None") - .ok_or(ReflectError("None"))? - .ty; - Ok((some_ty, none_ty)) - } - - /// Optional `T?` using the same `Some` / `None` sum as inference (`Cardinality` → match disj). - fn reflect_optional_sum( - dag: &Dag, - cardinality_decl_id: DeclarationId, - opt: Option, - mut reflect_some: impl FnMut(&Dag, T) -> ReflectResult, - ) -> ReflectResult { - let (some_ty, none_ty) = optional_some_none_constructor_ids(dag, cardinality_decl_id)?; - Ok(match opt { - None => FieldValue::Variant { - constructor: none_ty, - payload: vec![], - }, - Some(x) => FieldValue::Variant { - constructor: some_ty, - payload: vec![reflect_some(dag, x)?], - }, - }) - } - - fn reflect_port_id_list(dag: &Dag, ports: &[PortId]) -> ReflectResult { - let (empty_id, cons_id) = v3_list_empty_cons_ids(dag)?; - let mut tail = FieldValue::Variant { - constructor: empty_id, - payload: vec![], - }; - for p in ports.iter().rev() { - tail = FieldValue::Variant { - constructor: cons_id, - payload: vec![port_fv(*p), tail], - }; - } - Ok(tail) - } - - fn reflect_string_list_spine(dag: &Dag, strings: &[String]) -> ReflectResult { - let (empty_id, cons_id) = v3_list_empty_cons_ids(dag)?; - let mut tail = FieldValue::Variant { - constructor: empty_id, - payload: vec![], - }; - for s in strings.iter().rev() { - tail = FieldValue::Variant { - constructor: cons_id, - payload: vec![FieldValue::Literal(LiteralBits::String(s.clone())), tail], - }; - } - Ok(tail) - } - - fn reflect_unit_variant(dag: &Dag, sum_name: &str, label: &str) -> ReflectResult { - sum_variant_payload(dag, sum_name, label, vec![]) - } - - fn reflect_arithmetic_op(dag: &Dag, op: crate::dag::ArithmeticOp) -> ReflectResult { - let label = match op { - crate::dag::ArithmeticOp::Add => "Add", - crate::dag::ArithmeticOp::Sub => "Sub", - crate::dag::ArithmeticOp::Mul => "Mul", - crate::dag::ArithmeticOp::Div => "Div", - }; - reflect_unit_variant(dag, "ArithmeticOp", label) - } - - fn reflect_comparison_op(dag: &Dag, op: crate::dag::ComparisonOp) -> ReflectResult { - let label = match op { - crate::dag::ComparisonOp::Eq => "Eq", - crate::dag::ComparisonOp::Ne => "Ne", - crate::dag::ComparisonOp::Lt => "Lt", - crate::dag::ComparisonOp::Le => "Le", - crate::dag::ComparisonOp::Gt => "Gt", - crate::dag::ComparisonOp::Ge => "Ge", - }; - reflect_unit_variant(dag, "ComparisonOp", label) - } - - fn reflect_logical_op(dag: &Dag, op: crate::dag::LogicalOp) -> ReflectResult { - let label = match op { - crate::dag::LogicalOp::And => "And", - crate::dag::LogicalOp::Or => "Or", - }; - reflect_unit_variant(dag, "LogicalOp", label) - } - - fn reflect_operator_kind(dag: &Dag, k: &OperatorKind) -> ReflectResult { - match k { - OperatorKind::Arithmetic(op) => { - let id = disj_variant_ty(dag, "OperatorKind", "Arithmetic")?; - Ok(FieldValue::Variant { - constructor: id, - payload: vec![reflect_arithmetic_op(dag, *op)?], - }) - } - OperatorKind::Comparison(op) => { - let id = disj_variant_ty(dag, "OperatorKind", "Comparison")?; - Ok(FieldValue::Variant { - constructor: id, - payload: vec![reflect_comparison_op(dag, *op)?], - }) - } - OperatorKind::Logical(op) => { - let id = disj_variant_ty(dag, "OperatorKind", "Logical")?; - Ok(FieldValue::Variant { - constructor: id, - payload: vec![reflect_logical_op(dag, *op)?], - }) - } - } - } - - fn reflect_http_method_scalar(dag: &Dag, m: HttpMethodScalar) -> ReflectResult { - let label = match m { - HttpMethodScalar::Get => "GET", - HttpMethodScalar::Post => "POST", - HttpMethodScalar::Put => "PUT", - HttpMethodScalar::Patch => "PATCH", - HttpMethodScalar::Delete => "DELETE", - HttpMethodScalar::Head => "HEAD", - HttpMethodScalar::Options => "OPTIONS", - }; - reflect_unit_variant(dag, "HttpMethod", label) - } - - fn reflect_create_cause(dag: &Dag, c: &CreateCause) -> ReflectResult { - match c { - CreateCause::PostAlways => { - sum_variant_payload(dag, "CreateCause", "PostAlways", vec![]) - } - CreateCause::KeylessFallback { method } => sum_variant_payload( - dag, - "CreateCause", - "KeylessFallback", - vec![reflect_http_method_scalar(dag, *method)?], - ), - } - } - - fn reflect_key_source(dag: &Dag, ks: &KeySource) -> ReflectResult { - match ks { - KeySource::PathParam { param } => sum_variant_payload( - dag, - "KeySource", - "PathParam", - vec![FieldValue::Literal(LiteralBits::String(param.clone()))], - ), - KeySource::InputField { field } => sum_variant_payload( - dag, - "KeySource", - "InputField", - vec![FieldValue::Literal(LiteralBits::String(field.clone()))], - ), - KeySource::CompositeKey { fields } => sum_variant_payload( - dag, - "KeySource", - "CompositeKey", - vec![reflect_string_list_spine(dag, fields)?], - ), - } - } - - fn reflect_idempotent_shape(dag: &Dag, s: &IdempotentShape) -> ReflectResult { - match s { - IdempotentShape::ReadEffect => { - sum_variant_payload(dag, "IdempotentShape", "ReadEffect", vec![]) - } - IdempotentShape::UpsertEffect { key_source } => sum_variant_payload( - dag, - "IdempotentShape", - "UpsertEffect", - vec![reflect_key_source(dag, key_source)?], - ), - IdempotentShape::DeleteEffect { key_source } => sum_variant_payload( - dag, - "IdempotentShape", - "DeleteEffect", - vec![reflect_key_source(dag, key_source)?], - ), - } - } - - fn reflect_breaking_shape(dag: &Dag, s: &BreakingShape) -> ReflectResult { - match s { - BreakingShape::CreateEffect { cause } => sum_variant_payload( - dag, - "BreakingShape", - "CreateEffect", - vec![reflect_create_cause(dag, cause)?], - ), - BreakingShape::AppendEffect => { - sum_variant_payload(dag, "BreakingShape", "AppendEffect", vec![]) - } - } - } - - fn reflect_effect_shape(dag: &Dag, s: &EffectShape) -> ReflectResult { - match s { - EffectShape::IsIdempotent(inner) => sum_variant_payload( - dag, - "EffectShape", - "IsIdempotent", - vec![reflect_idempotent_shape(dag, inner)?], - ), - EffectShape::IsBreaking(inner) => sum_variant_payload( - dag, - "EffectShape", - "IsBreaking", - vec![reflect_breaking_shape(dag, inner)?], - ), - } - } - - fn reflect_operation_effect(dag: &Dag, op: &OperationEffect) -> ReflectResult { - Ok(FieldValue::Record(vec![ - ( - "operation_name".to_string(), - FieldValue::Literal(LiteralBits::String(op.operation_name.clone())), - ), - ("shape".to_string(), reflect_effect_shape(dag, &op.shape)?), - ])) - } - - fn reflect_operation_effect_vec_spine( - dag: &Dag, - ops: &[OperationEffect], - ) -> ReflectResult { - let (empty_id, cons_id) = v3_list_empty_cons_ids(dag)?; - let mut tail = FieldValue::Variant { - constructor: empty_id, - payload: vec![], - }; - for op in ops.iter().rev() { - let head = reflect_operation_effect(dag, op)?; - tail = FieldValue::Variant { - constructor: cons_id, - payload: vec![head, tail], - }; - } - Ok(tail) - } - - fn reflect_bool_port_ref(_dag: &Dag, r: BoolPortRef) -> ReflectResult { - Ok(FieldValue::Record(vec![( - "port".to_string(), - port_fv(r.port_id()), - )])) - } - - fn reflect_workflow_effect(dag: &Dag, wf: &WorkflowEffect) -> ReflectResult { - match wf { - WorkflowEffect::LinearEffect { ops } => sum_variant_payload( - dag, - "WorkflowEffect", - "LinearEffect", - vec![reflect_operation_effect_vec_spine(dag, ops)?], - ), - WorkflowEffect::BranchEffect { arms } => sum_variant_payload( - dag, - "WorkflowEffect", - "BranchEffect", - vec![reflect_non_singleton_branch_arms(dag, arms)?], - ), - WorkflowEffect::LoopEffect { body } => sum_variant_payload( - dag, - "WorkflowEffect", - "LoopEffect", - vec![reflect_workflow_effect(dag, body)?], - ), - WorkflowEffect::ParallelEffect { branches } => sum_variant_payload( - dag, - "WorkflowEffect", - "ParallelEffect", - vec![reflect_non_singleton_workflow_branches(dag, branches)?], - ), - } - } - - fn reflect_branch_arm_vec_spine(dag: &Dag, arms: &[BranchArm]) -> ReflectResult { - let (empty_id, cons_id) = v3_list_empty_cons_ids(dag)?; - let mut tail = FieldValue::Variant { - constructor: empty_id, - payload: vec![], - }; - for arm in arms.iter().rev() { - tail = FieldValue::Variant { - constructor: cons_id, - payload: vec![reflect_branch_arm(dag, arm)?, tail], - }; - } - Ok(tail) - } - - fn reflect_non_singleton_branch_arms( - dag: &Dag, - arms: &NonSingletonList, - ) -> ReflectResult { - let rest_spine = reflect_branch_arm_vec_spine(dag, &arms.rest)?; - Ok(FieldValue::Record(vec![ - ("first".to_string(), reflect_branch_arm(dag, &arms.first)?), - ("second".to_string(), reflect_branch_arm(dag, &arms.second)?), - ("rest".to_string(), rest_spine), - ])) - } - - fn reflect_branch_arm(dag: &Dag, arm: &BranchArm) -> ReflectResult { - Ok(FieldValue::Record(vec![ - ( - "condition".to_string(), - reflect_bool_port_ref(dag, arm.bool_port())?, - ), - ( - "body".to_string(), - reflect_workflow_effect(dag, arm.body())?, - ), - ])) - } - - fn reflect_workflow_effect_vec_spine( - dag: &Dag, - items: &[Box], - ) -> ReflectResult { - let (empty_id, cons_id) = v3_list_empty_cons_ids(dag)?; - let mut tail = FieldValue::Variant { - constructor: empty_id, - payload: vec![], - }; - for item in items.iter().rev() { - tail = FieldValue::Variant { - constructor: cons_id, - payload: vec![reflect_workflow_effect(dag, item.as_ref())?, tail], - }; - } - Ok(tail) - } - - fn reflect_non_singleton_workflow_branches( - dag: &Dag, - branches: &NonSingletonList>, - ) -> ReflectResult { - let rest_spine = reflect_workflow_effect_vec_spine(dag, &branches.rest)?; - Ok(FieldValue::Record(vec![ - ( - "first".to_string(), - reflect_workflow_effect(dag, branches.first.as_ref())?, - ), - ( - "second".to_string(), - reflect_workflow_effect(dag, branches.second.as_ref())?, - ), - ("rest".to_string(), rest_spine), - ])) - } - - fn reflect_optional_workflow_effect( - dag: &Dag, - cardinality_decl_id: DeclarationId, - opt: Option<&WorkflowEffect>, - ) -> ReflectResult { - reflect_optional_sum(dag, cardinality_decl_id, opt.cloned(), |d, w| { - reflect_workflow_effect(d, &w) - }) - } - - fn reflect_optional_branch_emit( - dag: &Dag, - cardinality_decl_id: DeclarationId, - opt: Option, - ) -> ReflectResult { - reflect_optional_sum(dag, cardinality_decl_id, opt, |d, p| match p { - BranchEmitParticipation::UserMatch => { - reflect_unit_variant(d, "BranchEmitParticipation", "UserMatch") - } - }) - } - - fn reflect_optional_bind_emit( - dag: &Dag, - cardinality_decl_id: DeclarationId, - opt: Option, - ) -> ReflectResult { - reflect_optional_sum(dag, cardinality_decl_id, opt, |d, p| match p { - BindEmitParticipation::UserCallable => { - reflect_unit_variant(d, "BindEmitParticipation", "UserCallable") - } - }) - } - - fn reflect_transform_target(dag: &Dag, t: &TransformTarget) -> ReflectResult { - match t { - TransformTarget::Callable(callee) => sum_variant_payload( - dag, - "TransformTarget", - "Callable", - vec![FieldValue::Reference(*callee)], - ), - TransformTarget::UnresolvedFieldProject { field_label } => sum_variant_payload( - dag, - "TransformTarget", - "UnresolvedFieldProject", - vec![FieldValue::Literal(LiteralBits::String( - field_label.clone(), - ))], - ), - TransformTarget::ResolvedFieldProject { field_label } => sum_variant_payload( - dag, - "TransformTarget", - "ResolvedFieldProject", - vec![FieldValue::Literal(LiteralBits::String( - field_label.clone(), - ))], - ), - TransformTarget::Operator(op) => sum_variant_payload( - dag, - "TransformTarget", - "Operator", - vec![reflect_operator_kind(dag, op)?], - ), - } - } - - fn reflect_branch_pattern(dag: &Dag, p: &BranchPattern) -> ReflectResult { - match p { - BranchPattern::UnresolvedVariant { name, span } => sum_variant_payload( - dag, - "BranchPattern", - "UnresolvedVariant", - vec![ - FieldValue::Literal(LiteralBits::String(name.clone())), - reflect_source_span(span), - ], - ), - BranchPattern::ResolvedVariant(decl) => sum_variant_payload( - dag, - "BranchPattern", - "ResolvedVariant", - vec![FieldValue::Reference(*decl)], - ), - } - } - - fn reflect_optional_payload_binding( - dag: &Dag, - opt: Option<&PayloadBinding>, - ) -> ReflectResult { - let path = named_record_type_root(dag, "BranchPath")?; - let binding_card = - peel_to_optional_cardinality_decl(dag, conj_field_ty(dag, path, "binding")?)?; - reflect_optional_sum(dag, binding_card, opt.cloned(), |_d, b| { - Ok(FieldValue::Record(vec![ - ( - "binding_name".to_string(), - FieldValue::Literal(LiteralBits::String(b.binding_name.clone())), - ), - ("payload_port".to_string(), port_fv(b.payload_port)), - ])) - }) - } - - fn reflect_branch_path(dag: &Dag, p: &Path) -> ReflectResult { - Ok(FieldValue::Record(vec![ - ("body".to_string(), node_fv(p.body)), - ("result_port".to_string(), port_fv(p.result_port())), - ( - "pattern".to_string(), - reflect_branch_pattern(dag, &p.pattern)?, - ), - ( - "binding".to_string(), - reflect_optional_payload_binding(dag, p.binding.as_ref())?, - ), - ])) - } - - fn reflect_branch_paths(dag: &Dag, paths: &[Path]) -> ReflectResult { - let (empty_id, cons_id) = v3_list_empty_cons_ids(dag)?; - let mut tail = FieldValue::Variant { - constructor: empty_id, - payload: vec![], - }; - for p in paths.iter().rev() { - let head = reflect_branch_path(dag, p)?; - tail = FieldValue::Variant { - constructor: cons_id, - payload: vec![head, tail], - }; - } - Ok(tail) - } - - fn reflect_loop_bound(dag: &Dag, b: &LoopBound) -> ReflectResult { - match b { - LoopBound::Cardinality { count } => { - sum_variant_payload(dag, "LoopBound", "Cardinality", vec![port_fv(*count)]) - } - LoopBound::Descent { cluster, measure } => sum_variant_payload( - dag, - "LoopBound", - "Descent", - vec![cluster_fv(*cluster), port_fv(*measure)], - ), - } - } - - fn behavior_variant_id(dag: &Dag, label: &str) -> ReflectResult { - disj_variant_ty(dag, "Behavior", label) - } - - pub fn reflect_behavior(dag: &Dag, behavior: &Behavior) -> ReflectResult { - match behavior { - Behavior::Value(v) => reflect_value(dag, v), - Behavior::Transform(t) => reflect_transform(dag, t), - Behavior::Branch(b) => reflect_branch(dag, b), - Behavior::Loop(l) => reflect_loop(dag, l), - Behavior::Bind(b) => reflect_bind(dag, b), - } - } - - fn reflect_value(dag: &Dag, v: &ValueNode) -> ReflectResult { - let id = behavior_variant_id(dag, "Value")?; - let vn = named_record_type_root(dag, "ValueNode")?; - let lane2_card = - peel_to_optional_cardinality_decl(dag, conj_field_ty(dag, vn, "lane2_workflow")?)?; - let lane2 = reflect_optional_workflow_effect(dag, lane2_card, v.lane2_workflow())?; - let payload = FieldValue::Record(vec![ - ("id".to_string(), node_fv(v.id)), - ("payload".to_string(), FieldValue::Literal(v.data.clone())), - ("result_port".to_string(), port_fv(v.output)), - ("span".to_string(), reflect_source_span(&v.span)), - ("lane2_workflow".to_string(), lane2), - ]); - Ok(FieldValue::Variant { - constructor: id, - payload: vec![payload], - }) - } - - fn reflect_transform(dag: &Dag, t: &TransformNode) -> ReflectResult { - let id = behavior_variant_id(dag, "Transform")?; - let inputs = reflect_port_id_list(dag, &t.inputs)?; - let payload = FieldValue::Record(vec![ - ("id".to_string(), node_fv(t.id)), - ( - "target".to_string(), - reflect_transform_target(dag, &t.target)?, - ), - ("inputs".to_string(), inputs), - ("result_port".to_string(), port_fv(t.output)), - ("span".to_string(), reflect_source_span(&t.span)), - ]); - Ok(FieldValue::Variant { - constructor: id, - payload: vec![payload], - }) - } - - fn reflect_branch(dag: &Dag, b: &BranchNode) -> ReflectResult { - let id = behavior_variant_id(dag, "Branch")?; - let paths = reflect_branch_paths(dag, &b.paths)?; - let br = named_record_type_root(dag, "BranchNode")?; - let emit_card = - peel_to_optional_cardinality_decl(dag, conj_field_ty(dag, br, "emit_participation")?)?; - let emit = reflect_optional_branch_emit(dag, emit_card, b.emit_participation())?; - let payload = FieldValue::Record(vec![ - ("id".to_string(), node_fv(b.id)), - ("input".to_string(), port_fv(b.input)), - ("paths".to_string(), paths), - ("result_port".to_string(), port_fv(b.output)), - ("span".to_string(), reflect_source_span(&b.span)), - ("emit_participation".to_string(), emit), - ]); - Ok(FieldValue::Variant { - constructor: id, - payload: vec![payload], - }) - } - - fn reflect_loop(dag: &Dag, l: &LoopNode) -> ReflectResult { - let id = behavior_variant_id(dag, "Loop")?; - let bound = reflect_loop_bound(dag, &l.bound)?; - let payload = FieldValue::Record(vec![ - ("id".to_string(), node_fv(l.id)), - ("source".to_string(), port_fv(l.source)), - ("init".to_string(), port_fv(l.init)), - ("body".to_string(), node_fv(l.body)), - ("bound".to_string(), bound), - ("result_port".to_string(), port_fv(l.output)), - ("span".to_string(), reflect_source_span(&l.span)), - ]); - Ok(FieldValue::Variant { - constructor: id, - payload: vec![payload], - }) - } - - fn reflect_bind(dag: &Dag, b: &BindNode) -> ReflectResult { - let id = behavior_variant_id(dag, "Bind")?; - let params = reflect_port_id_list(dag, &b.params)?; - let bn = named_record_type_root(dag, "BindNode")?; - let lane2_card = - peel_to_optional_cardinality_decl(dag, conj_field_ty(dag, bn, "lane2_workflow")?)?; - let emit_card = - peel_to_optional_cardinality_decl(dag, conj_field_ty(dag, bn, "emit_participation")?)?; - let lane2 = reflect_optional_workflow_effect(dag, lane2_card, b.lane2_workflow())?; - let emit = reflect_optional_bind_emit(dag, emit_card, b.emit_participation())?; - let payload = FieldValue::Record(vec![ - ("id".to_string(), node_fv(b.id)), - ( - "name".to_string(), - FieldValue::Literal(LiteralBits::String(b.name.clone())), - ), - ("result_port".to_string(), port_fv(b.value)), - ("params".to_string(), params), - ("span".to_string(), reflect_source_span(&b.span)), - ("lane2_workflow".to_string(), lane2), - ("emit_participation".to_string(), emit), - ]); - Ok(FieldValue::Variant { - constructor: id, - payload: vec![payload], - }) - } - - pub fn reflect_behavior_list(dag: &Dag, nodes: &[Behavior]) -> ReflectResult { - let (empty_id, cons_id) = v3_list_empty_cons_ids(dag)?; - let mut tail = FieldValue::Variant { - constructor: empty_id, - payload: vec![], - }; - for behavior in nodes.iter().rev() { - let head = reflect_behavior(dag, behavior)?; - tail = FieldValue::Variant { - constructor: cons_id, - payload: vec![head, tail], - }; - } - Ok(tail) - } - - #[cfg(test)] - mod reflection_tests { - use super::*; - use crate::compile_to_dag; - use crate::dag::{ - Behavior, DeclarationId, FieldValue, LiteralBits, LoopBound, TransformTarget, - TypeConnective, - }; - - fn compile(src: &str, file: &str) -> Dag { - match compile_to_dag(src, file) { - Ok(d) => { - assert!(d.diagnostics().is_empty(), "{file}: {:?}", d.diagnostics()); - d - } - Err(e) => panic!("compile {file}: {e:?}"), - } - } - - fn behavior_inner_record(fv: &FieldValue) -> &[(String, FieldValue)] { - let FieldValue::Variant { payload, .. } = fv else { - panic!("expected Behavior variant, got {fv:?}"); - }; - assert_eq!(payload.len(), 1, "Behavior variant payload"); - let FieldValue::Record(fields) = &payload[0] else { - panic!("expected inner record"); - }; - fields.as_slice() - } - - fn record_get<'a>(rec: &'a [(String, FieldValue)], key: &str) -> &'a FieldValue { - rec.iter() - .find(|(k, _)| k == key) - .map(|(_, v)| v) - .unwrap_or_else(|| panic!("missing field `{key}` in {rec:?}")) - } - - fn list_spine_len(dag: &Dag, list: &FieldValue) -> usize { - let (empty_id, _) = v3_list_empty_cons_ids(dag).expect("List ids"); - let mut n = 0; - let mut cur = list; - loop { - let FieldValue::Variant { - constructor, - payload, - } = cur - else { - panic!("expected List spine"); - }; - if *constructor == empty_id { - break; - } - assert_eq!(payload.len(), 2, "Cons payload"); - n += 1; - cur = &payload[1]; - } - n - } - - fn loop_bound_variant_label(dag: &Dag, variant_constructor_ty: DeclarationId) -> String { - let decl = dag - .declaration_by_name("LoopBound") - .expect("LoopBound declaration"); - let TypeConnective::Disj { variants } = &decl.connective else { - panic!("LoopBound not a Disj"); - }; - variants - .iter() - .find(|v| v.ty == variant_constructor_ty) - .map(|v| v.label.clone()) - .unwrap_or_else(|| { - panic!("constructor {variant_constructor_ty:?} not a LoopBound variant"); - }) - } - - fn optional_carrier_variant_label(dag: &Dag, fv: &FieldValue) -> String { - let FieldValue::Variant { constructor, .. } = fv else { - panic!("expected optional carrier variant, got {fv:?}"); - }; - dag.declarations() - .iter() - .find_map(|decl| match &decl.connective { - TypeConnective::Disj { variants } => variants - .iter() - .find(|v| v.ty == *constructor) - .map(|v| v.label.clone()), - _ => None, - }) - .unwrap_or_else(|| { - panic!("constructor {constructor:?} not a sum variant payload ty") - }) - } - - /// Peel empty `Instantiation` wrappers to the underlying `Conj` (same contract as - /// `substrate_reflection::named_record_type_root`). - fn named_conj_root_id(dag: &Dag, substrate_record_name: &str) -> DeclarationId { - let mut decl_id = dag - .declaration_by_name(substrate_record_name) - .unwrap_or_else(|| panic!("missing declaration `{substrate_record_name}`")) - .id; - const PEEL_MAX: usize = 64; - for _ in 0..PEEL_MAX { - match &dag.declaration(decl_id).connective { - TypeConnective::Conj { .. } => return decl_id, - TypeConnective::Instantiation { - template, - arguments, - } if arguments.is_empty() => { - decl_id = *template; - } - _ => panic!("`{substrate_record_name}` did not peel to a Conj"), - } - } - panic!("peel depth exceeded for `{substrate_record_name}`"); - } - - /// Ratchet: reflected `Record` field **order and labels** match the named substrate `Conj`. - fn assert_record_matches_named_substrate_conj( - dag: &Dag, - rec: &[(String, FieldValue)], - substrate_record_name: &str, - ) { - let conj_id = named_conj_root_id(dag, substrate_record_name); - let TypeConnective::Conj { children } = &dag.declaration(conj_id).connective else { - panic!("`{substrate_record_name}` not a Conj after peel"); - }; - assert_eq!( - rec.len(), - children.len(), - "`{substrate_record_name}`: reflected field count vs declared Conj arity" - ); - for (i, ((ref_label, _), decl_field)) in rec.iter().zip(children.iter()).enumerate() { - assert_eq!( - ref_label, &decl_field.label, - "`{substrate_record_name}`: field {i} label mismatch (reflection vs substrate.dag)" - ); - } - } - - /// Ratchet: `FieldValue::Variant` payload slot count matches the declared variant payload - /// shape (`Conj` children when the constructor is a record; otherwise 0–1 like - /// `sum_variant_payload`). - fn assert_sum_variant_payload_matches_substrate( - dag: &Dag, - sum_decl_name: &str, - fv: &FieldValue, - ) { - let FieldValue::Variant { - constructor, - payload, - } = fv - else { - panic!("expected sum variant, got {fv:?}"); - }; - let ctor_ty = *constructor; - let ctor_decl = dag.declaration(ctor_ty); - let variant_label = { - let sum_decl = dag - .declaration_by_name(sum_decl_name) - .unwrap_or_else(|| panic!("missing sum `{sum_decl_name}`")); - let TypeConnective::Disj { variants } = &sum_decl.connective else { - panic!("`{sum_decl_name}` not a Disj"); - }; - variants - .iter() - .find(|v| v.ty == ctor_ty) - .map(|v| v.label.as_str()) - .unwrap_or_else(|| { - panic!("constructor {ctor_ty:?} is not a variant of `{sum_decl_name}`") - }) - }; - match &ctor_decl.connective { - TypeConnective::Conj { children } => { - assert_eq!( - payload.len(), - children.len(), - "`{sum_decl_name}`::{variant_label}: payload arity vs declared Conj" - ); - } - _ => { - assert!( - payload.len() <= 1, - "`{sum_decl_name}`::{variant_label}: non-Conj constructor allows at most one payload; got {}", - payload.len() - ); - } - } - } - - #[test] - fn reflection_optional_fields_use_some_none_not_list() { - let src = "let x: Int = 7\n"; - let file = "reflect_optional_carrier_value.v3"; - let dag = compile(src, file); - let v = dag - .nodes() - .iter() - .find_map(|b| match b { - Behavior::Value(v) if v.span.file == file => Some(v), - _ => None, - }) - .expect("Value node"); - let fv = reflect_behavior(&dag, &Behavior::Value(v.clone())).expect("reflect"); - let rec = behavior_inner_record(&fv); - let lane2 = record_get(rec, "lane2_workflow"); - assert_eq!(optional_carrier_variant_label(&dag, lane2), "None"); - - let src_bind = "fn g(x: Int) -> Int = x + 1\n"; - let file_bind = "reflect_optional_carrier_bind.v3"; - let dag_b = compile(src_bind, file_bind); - let b = dag_b - .nodes() - .iter() - .find_map(|beh| match beh { - Behavior::Bind(b) if b.span.file == file_bind && !b.params.is_empty() => { - Some(b) - } - _ => None, - }) - .expect("Bind node"); - let fv_b = reflect_behavior(&dag_b, &Behavior::Bind(b.clone())).expect("reflect bind"); - let rec_b = behavior_inner_record(&fv_b); - let emit = record_get(rec_b, "emit_participation"); - assert_eq!(optional_carrier_variant_label(&dag_b, emit), "Some"); - } - - #[test] - fn reflection_value_includes_all_substrate_fields() { - let src = "let x: Int = 7\n"; - let file = "reflect_value.v3"; - let dag = compile(src, file); - let v = dag - .nodes() - .iter() - .find_map(|b| match b { - Behavior::Value(v) if v.span.file == file => Some(v), - _ => None, - }) - .expect("Value node"); - let fv = reflect_behavior(&dag, &Behavior::Value(v.clone())).expect("reflect"); - let rec = behavior_inner_record(&fv); - for key in ["id", "payload", "result_port", "span", "lane2_workflow"] { - let _ = record_get(rec, key); - } - } - - #[test] - fn reflection_transform_includes_target_inputs_and_span() { - let src = "fn f(a: Int, b: Int) -> Int = a + b\n"; - let file = "reflect_transform.v3"; - let dag = compile(src, file); - let t = dag - .nodes() - .iter() - .find_map(|b| match b { - Behavior::Transform(t) if t.span.file == file => Some(t), - _ => None, - }) - .expect("Transform node"); - let fv = reflect_behavior(&dag, &Behavior::Transform(t.clone())).expect("reflect"); - let rec = behavior_inner_record(&fv); - for key in ["id", "target", "inputs", "result_port", "span"] { - let _ = record_get(rec, key); - } - } - - #[test] - fn reflection_bind_includes_params_lane2_emit() { - let src = "fn g(x: Int) -> Int = x + 1\n"; - let file = "reflect_bind.v3"; - let dag = compile(src, file); - let b = dag - .nodes() - .iter() - .find_map(|beh| match beh { - Behavior::Bind(b) if b.span.file == file && !b.params.is_empty() => Some(b), - _ => None, - }) - .expect("Bind node"); - let fv = reflect_behavior(&dag, &Behavior::Bind(b.clone())).expect("reflect"); - let rec = behavior_inner_record(&fv); - for key in [ - "id", - "name", - "result_port", - "params", - "span", - "lane2_workflow", - "emit_participation", - ] { - let _ = record_get(rec, key); - } - } - - #[test] - fn reflection_branch_three_arms_totality() { - let src = "\ - type Color = Red | Green | Blue - fn classify(h: Color) -> Int = match h { Red => 0, Green => 1, Blue => 2 } - "; - let file = "reflect_branch_three.v3"; - let dag = compile(src, file); - let br = dag - .nodes() - .iter() - .find_map(|b| match b { - Behavior::Branch(br) if br.span.file == file => Some(br), - _ => None, - }) - .expect("Branch"); - assert_eq!(br.paths.len(), 3); - let fv = reflect_behavior(&dag, &Behavior::Branch(br.clone())).expect("reflect"); - let rec = behavior_inner_record(&fv); - assert_record_matches_named_substrate_conj(&dag, rec, "BranchNode"); - let paths = record_get(rec, "paths"); - assert_eq!(list_spine_len(&dag, paths), 3); - let FieldValue::Variant { payload, .. } = paths else { - panic!("paths list"); - }; - assert_eq!(payload.len(), 2); - let FieldValue::Record(arm_fields) = &payload[0] else { - panic!("first arm record"); - }; - for key in ["body", "result_port", "pattern", "binding"] { - let _ = record_get(arm_fields, key); - } - } - - #[test] - fn reflection_loop_bound_coproduct_cardinality_vs_descent() { - let src_single = "\ - fn count(n: Int) -> Int = if n == 0 then 0 else 1 + count(n - 1) - let _: Int = count(1) - "; - let file_c = "reflect_loop_card.v3"; - let dag_c = compile(src_single, file_c); - let lp_c = dag_c - .nodes() - .iter() - .find_map(|b| match b { - Behavior::Loop(l) if l.span.file == file_c => Some(l), - _ => None, - }) - .expect("cardinality loop"); - assert!(matches!(lp_c.bound, LoopBound::Cardinality { .. })); - let fv_c = reflect_behavior(&dag_c, &Behavior::Loop(lp_c.clone())).expect("reflect"); - let rec_c = behavior_inner_record(&fv_c); - assert_record_matches_named_substrate_conj(&dag_c, rec_c, "LoopNode"); - let bound_c = record_get(rec_c, "bound"); - let FieldValue::Variant { - constructor: c_ty, .. - } = bound_c - else { - panic!("LoopBound variant"); - }; - assert_eq!(loop_bound_variant_label(&dag_c, *c_ty), "Cardinality"); - - let src_mutual = "\ - fn even(n: Int) -> Bool = if n == 0 then true else odd(n - 1) - fn odd(n: Int) -> Bool = if n == 0 then false else even(n - 1) - "; - let file_d = "reflect_loop_desc.v3"; - let dag_d = compile(src_mutual, file_d); - let lp_d = dag_d - .nodes() - .iter() - .find_map(|b| match b { - Behavior::Loop(l) - if l.span.file == file_d - && matches!(l.bound, LoopBound::Descent { .. }) => - { - Some(l) - } - _ => None, - }) - .expect("descent loop"); - let fv_d = reflect_behavior(&dag_d, &Behavior::Loop(lp_d.clone())).expect("reflect"); - let rec_d = behavior_inner_record(&fv_d); - assert_record_matches_named_substrate_conj(&dag_d, rec_d, "LoopNode"); - let bound_d = record_get(rec_d, "bound"); - let FieldValue::Variant { - constructor: d_ty, .. - } = bound_d - else { - panic!("LoopBound variant"); - }; - assert_eq!(loop_bound_variant_label(&dag_d, *d_ty), "Descent"); - } - - #[test] - fn reflection_value_inner_record_matches_value_node_conj_decl() { - let src = "let x: Int = 7\n"; - let file = "reflect_schema_value.v3"; - let dag = compile(src, file); - let v = dag - .nodes() - .iter() - .find_map(|b| match b { - Behavior::Value(v) if v.span.file == file => Some(v), - _ => None, - }) - .expect("Value node"); - let fv = reflect_behavior(&dag, &Behavior::Value(v.clone())).expect("reflect"); - let rec = behavior_inner_record(&fv); - assert_record_matches_named_substrate_conj(&dag, rec, "ValueNode"); - } - - #[test] - fn reflection_source_span_record_matches_types_dag_conj_decl() { - let src = "let x: Int = 7\n"; - let file = "reflect_schema_source_span.v3"; - let dag = compile(src, file); - let v = dag - .nodes() - .iter() - .find_map(|b| match b { - Behavior::Value(v) if v.span.file == file => Some(v), - _ => None, - }) - .expect("Value node"); - let fv = reflect_behavior(&dag, &Behavior::Value(v.clone())).expect("reflect"); - let rec = behavior_inner_record(&fv); - let span_rec = match record_get(rec, "span") { - FieldValue::Record(fields) => fields.as_slice(), - other => panic!("expected span record, got {other:?}"), - }; - assert_record_matches_named_substrate_conj(&dag, span_rec, "SourceSpan"); - assert_eq!( - record_get(span_rec, "start"), - &FieldValue::Literal(LiteralBits::Int(i64::from(v.span.byte_start).to_string())) - ); - assert_eq!( - record_get(span_rec, "end"), - &FieldValue::Literal(LiteralBits::Int(i64::from(v.span.byte_end).to_string())) - ); - } - - #[test] - fn reflection_transform_inner_record_matches_transform_node_conj_decl() { - let src = "fn f(a: Int, b: Int) -> Int = a + b\n"; - let file = "reflect_schema_transform.v3"; - let dag = compile(src, file); - let t = dag - .nodes() - .iter() - .find_map(|b| match b { - Behavior::Transform(t) if t.span.file == file => Some(t), - _ => None, - }) - .expect("Transform node"); - let fv = reflect_behavior(&dag, &Behavior::Transform(t.clone())).expect("reflect"); - let rec = behavior_inner_record(&fv); - assert_record_matches_named_substrate_conj(&dag, rec, "TransformNode"); - } - - #[test] - fn reflection_bind_inner_record_matches_bind_node_conj_decl() { - let src = "fn g(x: Int) -> Int = x + 1\n"; - let file = "reflect_schema_bind.v3"; - let dag = compile(src, file); - let b = dag - .nodes() - .iter() - .find_map(|beh| match beh { - Behavior::Bind(b) if b.span.file == file && !b.params.is_empty() => Some(b), - _ => None, - }) - .expect("Bind node"); - let fv = reflect_behavior(&dag, &Behavior::Bind(b.clone())).expect("reflect"); - let rec = behavior_inner_record(&fv); - assert_record_matches_named_substrate_conj(&dag, rec, "BindNode"); - } - - #[test] - fn reflection_transform_target_callable_variant_matches_substrate_payload_shape() { - let src = "fn id(x: Int) -> Int = x\nlet _: Int = id(1)\n"; - let file = "reflect_schema_callable.v3"; - let dag = compile(src, file); - let t = dag - .nodes() - .iter() - .find_map(|b| match b { - Behavior::Transform(t) if t.span.file == file => match &t.target { - TransformTarget::Callable(_) => Some(t), - _ => None, - }, - _ => None, - }) - .expect("Callable transform"); - let fv = reflect_behavior(&dag, &Behavior::Transform(t.clone())).expect("reflect"); - let rec = behavior_inner_record(&fv); - let target = record_get(rec, "target"); - assert_sum_variant_payload_matches_substrate(&dag, "TransformTarget", target); - } - - #[test] - fn reflection_transform_target_field_project_variant_matches_substrate_payload_shape() { - let src = "\ -type Point { x: Int y: Int } -fn get_x(point: Point) -> Int = point.x -"; - let file = "reflect_schema_field_proj.v3"; - let dag = compile(src, file); - let t = dag - .nodes() - .iter() - .find_map(|b| match b { - Behavior::Transform(t) if t.span.file == file => match &t.target { - TransformTarget::UnresolvedFieldProject { .. } - | TransformTarget::ResolvedFieldProject { .. } => Some(t), - _ => None, - }, - _ => None, - }) - .expect("FieldProject transform"); - let fv = reflect_behavior(&dag, &Behavior::Transform(t.clone())).expect("reflect"); - let rec = behavior_inner_record(&fv); - let target = record_get(rec, "target"); - assert_sum_variant_payload_matches_substrate(&dag, "TransformTarget", target); - } - } -} - -// PB-0 cycle-2: T-LAS structural carriers nested under `lens_declaration_apply` for SG-0 -// `.rs` path retirement; `lib.rs` re-exports `v3_compiler::lens_t_las_carrier` at crate root. -// Dissolution: emit-time carriers owned by `.dag` / generated substrate without host mirrors. -pub mod lens_t_las_carrier { - //! Structural carriers for Rust emission of T-LAS lens surfaces that reference - //! `v3.std.lens::Lens`, `std.algebra::Monoid`, and `v3.std.lens_application` - //! (`gate #92`). Shapes mirror the `.dag` authorities; they are not used by the - //! compiler runtime outside `emit_rust_module` snapshots linking against - //! `v3_compiler`. - - use std::rc::Rc; - - use crate::dag::{Behavior, Dag, LoopBound}; - use crate::diagnostics::Diagnostic; - use crate::dimension::Witness; - - /// Mirrors `OptionalDiagnostic` in `v3.std.dimensions` for emitted lens code. - #[derive(Clone, Debug)] - pub enum OptionalDiagnostic { - NoDiagnostic, - SomeDiagnostic { value: Diagnostic }, - } - - pub type LensReadFn = dyn Fn(&Dag, &Behavior) -> Witness; - pub type LensValidateFn = dyn Fn(&Dag, T) -> OptionalDiagnostic; - - /// Mirrors `Monoid` in `dsl/std/algebra.dag`. - #[derive(Clone)] - pub struct Monoid { - pub op: Rc T>, - pub identity: T, - } - - /// Mirrors `Lens` in `v3.std.lens`. - #[derive(Clone)] - pub struct Lens { - pub name: String, - pub read: Rc>, - pub sequential: Monoid, - pub branch: Rc T>, - pub iterate: Rc T>, - pub validate: Rc>, - } - - /// Mirrors `LensEnforcement` in `v3.std.lens_application`. - #[derive(Clone)] - pub struct LensEnforcement { - pub project: Rc Projected>, - pub violates: Rc bool>, - } - - /// Mirrors `EnforceableLens` in `v3.std.lens_application`. - #[derive(Clone)] - pub struct EnforceableLens { - pub lens: Lens, - pub enforcement: LensEnforcement, - } -} - -// R3 gate #6 (`lens_testgen_dot_rs_retired`): the standalone `lens_testgen.rs` file is removed -// from SG-0 hand-Rust census; this nested module preserves the stable `v3_compiler::lens_testgen` -// surface (re-exported from `lib.rs`) until PB-Runtime owns testgen end-to-end. -pub mod lens_testgen { - include!("lens_testgen_body.txt"); -} - -pub use substrate_reflection::reflect_behavior_list; diff --git a/src/v3/compiler/src/lens_t_las_carrier.rs b/src/v3/compiler/src/lens_t_las_carrier.rs deleted file mode 100644 index 471841ee57a..00000000000 --- a/src/v3/compiler/src/lens_t_las_carrier.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Structural carriers for Rust emission of T-LAS lens surfaces that reference -//! `v3.std.lens::Lens`, `std.algebra::Monoid`, and `v3.std.lens_application` -//! (`gate #92`). Shapes mirror the `.dag` authorities; they are not used by the -//! compiler runtime outside `emit_rust_module` snapshots linking against -//! `v3_compiler`. - -use std::rc::Rc; - -use crate::dag::{Behavior, Dag, LoopBound}; -use crate::diagnostics::Diagnostic; -use crate::dimension::Witness; - -/// Mirrors `OptionalDiagnostic` in `v3.std.dimensions` for emitted lens code. -#[derive(Clone, Debug)] -pub enum OptionalDiagnostic { - NoDiagnostic, - SomeDiagnostic { value: Diagnostic }, -} - -pub type LensReadFn = dyn Fn(&Dag, &Behavior) -> Witness; -pub type LensValidateFn = dyn Fn(&Dag, T) -> OptionalDiagnostic; - -/// Mirrors `Monoid` in `dsl/std/algebra.dag`. -#[derive(Clone)] -pub struct Monoid { - pub op: Rc T>, - pub identity: T, -} - -/// Mirrors `Lens` in `v3.std.lens`. -#[derive(Clone)] -pub struct Lens { - pub name: String, - pub read: Rc>, - pub sequential: Monoid, - pub branch: Rc T>, - pub iterate: Rc T>, - pub validate: Rc>, -} - -/// Mirrors `LensEnforcement` in `v3.std.lens_application`. -#[derive(Clone)] -pub struct LensEnforcement { - pub project: Rc Projected>, - pub violates: Rc bool>, -} - -/// Mirrors `EnforceableLens` in `v3.std.lens_application`. -#[derive(Clone)] -pub struct EnforceableLens { - pub lens: Lens, - pub enforcement: LensEnforcement, -} diff --git a/src/v3/compiler/src/lib.rs b/src/v3/compiler/src/lib.rs index 40048322783..e43200b8f38 100644 --- a/src/v3/compiler/src/lib.rs +++ b/src/v3/compiler/src/lib.rs @@ -3772,8 +3772,8 @@ pub mod lens_declaration_apply { "/lens_declaration_apply_body.txt" )); } -pub use lens_declaration_apply::lens_testgen; pub use lens_declaration_apply::lens_t_las_carrier; +pub use lens_declaration_apply::lens_testgen; /// Effect-enumeration lens. Authority lives in /// `src/v3/lenses/effect_enumeration.dag`; the Rust projection is diff --git a/src/v3/compiler/src/r3_gate_87_cementing_regen_runner_suites.rs b/src/v3/compiler/src/r3_gate_87_cementing_regen_runner_suites.rs deleted file mode 100644 index 88a856e107f..00000000000 --- a/src/v3/compiler/src/r3_gate_87_cementing_regen_runner_suites.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! R3 gate #87 — PB-B-1 runner table for `tests/dag/t_r3_gate_87_cementing_regen_*.dag` harnesses. -//! -//! **INVARIANTS P2 / single authority:** `R3_GATE_87_CEMENTING_REGEN_SUITES` is the only merge-visible -//! inventory for which harnesses `t_pb_b_1_dag_runner_test` executes. `cementing_dispatch` and -//! `r3_gate_87_lens_cementing_regen_receipts_test` consume the same table so `.dag` Band-C receipts -//! cannot pass `CementingDispatchMatchesProjection` without a matching runner row. - -use std::collections::BTreeSet; -use std::path::Path; - -/// `LensRegistryEntry.name` values implied by [`R3_GATE_87_CEMENTING_REGEN_SUITES`] harness `file` -/// paths (`t_r3_gate_87_cementing_regen_.dag`). **Single authority** for the gate-#87 -/// inventory ratchet: `r3_gate_87_lens_cementing_regen_receipts_test` compares live `regen.dag` -/// names against this set so a new registry row cannot ship without a matching runner row. -pub fn r3_gate_87_cementing_regen_lens_names_for_runner_table() -> BTreeSet { - R3_GATE_87_CEMENTING_REGEN_SUITES - .iter() - .map(|(_, file, _, _)| lens_name_from_gate_87_harness_path(file)) - .collect() -} - -/// Module stems (`tests/dag/.dag`) enumerated in [`R3_GATE_87_CEMENTING_REGEN_SUITES`]. -/// [`crate::cementing_dispatch::evaluate_cementing_dispatch_projection`] requires every -/// `kind == "dag"` receipt in `cementing_dispatch.dag` to use one of these stems so the dispatch -/// list cannot drift from the T-PB-B-1 runner. -pub fn r3_gate_87_cementing_regen_pb_b1_dag_module_stems() -> BTreeSet { - R3_GATE_87_CEMENTING_REGEN_SUITES - .iter() - .map(|(_, file, _, _)| { - Path::new(file) - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or_else(|| { - panic!("R3_GATE_87_CEMENTING_REGEN_SUITES: invalid harness path `{file}`") - }) - .to_string() - }) - .collect() -} - -// R3 gate #87 — every `LensRegistryEntry` in `src/v3/compiler/regen.dag` has a -// `src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_.dag` harness evaluated here. -const R3_GATE_87_CEMENTING_HARNESS_PATH_PREFIX: &str = - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_"; -const R3_GATE_87_CEMENTING_HARNESS_PATH_SUFFIX: &str = ".dag"; - -fn lens_name_from_gate_87_harness_path(file: &str) -> String { - file.strip_prefix(R3_GATE_87_CEMENTING_HARNESS_PATH_PREFIX) - .and_then(|rest| rest.strip_suffix(R3_GATE_87_CEMENTING_HARNESS_PATH_SUFFIX)) - .unwrap_or_else(|| { - panic!( - "R3_GATE_87_CEMENTING_REGEN_SUITES: harness path must be \ - `{prefix}{suffix}`, got `{file}`", - prefix = R3_GATE_87_CEMENTING_HARNESS_PATH_PREFIX, - suffix = R3_GATE_87_CEMENTING_HARNESS_PATH_SUFFIX, - ) - }) - .to_string() -} - -pub const R3_GATE_87_CEMENTING_REGEN_SUITES: &[(&str, &str, &str, &[&str])] = &[ - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_cost.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_cost.dag", - "r3_gate_87_cementing_regen_cost_suite", - &[ - "cementing_regen_cost_merge_sort_lens_output_equals", - "cementing_regen_cost", - ], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_cost_symbolic.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_cost_symbolic.dag", - "r3_gate_87_cementing_regen_cost_symbolic_suite", - &[ - "cementing_regen_cost_symbolic", - "cementing_regen_cost_symbolic_countdown", - ], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_cost_target_realization.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_cost_target_realization.dag", - "r3_gate_87_cementing_regen_cost_target_realization_suite", - &["cementing_regen_cost_target_realization"], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_effect_enumeration.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_effect_enumeration.dag", - "r3_gate_87_cementing_regen_effect_enumeration_suite", - &["cementing_regen_effect_enumeration"], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_infer_helpers.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_infer_helpers.dag", - "r3_gate_87_cementing_regen_infer_helpers_suite", - &["cementing_regen_infer_helpers"], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_lower_helpers.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_lower_helpers.dag", - "r3_gate_87_cementing_regen_lower_helpers_suite", - &["cementing_regen_lower_helpers"], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_provenance.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_provenance.dag", - "r3_gate_87_cementing_regen_provenance_suite", - &["cementing_regen_provenance"], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_structural_resolution.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_structural_resolution.dag", - "r3_gate_87_cementing_regen_structural_resolution_suite", - &["cementing_regen_structural_resolution"], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_unused_parameters.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_unused_parameters.dag", - "r3_gate_87_cementing_regen_unused_parameters_suite", - &["cementing_regen_unused_parameters"], - ), - ( - include_str!("../tests/dag/t_r3_gate_87_cementing_regen_variant_payload.dag"), - "src/v3/compiler/tests/dag/t_r3_gate_87_cementing_regen_variant_payload.dag", - "r3_gate_87_cementing_regen_variant_payload_suite", - &["cementing_regen_variant_payload"], - ), -]; diff --git a/src/v3/compiler/src/test_runner.rs b/src/v3/compiler/src/test_runner.rs index 56b8ad10c25..1cfde961c27 100644 --- a/src/v3/compiler/src/test_runner.rs +++ b/src/v3/compiler/src/test_runner.rs @@ -6336,8 +6336,7 @@ fn sg0_quoted_path_from_line(line: &str) -> Option { fn is_lens_producer_census_path(path: &str) -> bool { matches!( path, - "src/v3/compiler/lens_declaration_apply_body.txt" - | "src/v3/compiler/src/bin/regen_lens.rs" + "src/v3/compiler/lens_declaration_apply_body.txt" | "src/v3/compiler/src/bin/regen_lens.rs" ) } From 3e5e4252e74f696142b8e09af66d0f7ad573e163 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 19:28:16 -0400 Subject: [PATCH 4/8] WIP: PB-0 cycle-2 NON_TEST retirement (msg_84abadad Track A): retire 6 paths --- src/v3/compiler/lens_declaration_apply_body.txt | 11 +++++------ src/v3/compiler/src/cementing_dispatch.rs | 4 ++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/v3/compiler/lens_declaration_apply_body.txt b/src/v3/compiler/lens_declaration_apply_body.txt index 1b759bf8737..57c6b5b84a0 100644 --- a/src/v3/compiler/lens_declaration_apply_body.txt +++ b/src/v3/compiler/lens_declaration_apply_body.txt @@ -1,8 +1,7 @@ -// PB-0 cycle-2 SG-0 scaffold: bounded lens application (`lens_declaration_apply` module body). -// Included from `lib.rs` via `include!` so `src/v3/compiler/src/lens_declaration_apply.rs` retires -// from EXPECTED_HAND_AUTHORED_NON_TEST (same semantics; path-only retirement per gate #5 notes). -// Dissolution: PB-Runtime / Row-4 interpreter-as-data owns reflection + application (R3 §1.8). - +//! PB-0 cycle-2: bounded lens application module body (`lens_declaration_apply_body.txt`), included +//! from `lib.rs` so `src/v3/compiler/src/lens_declaration_apply.rs` retires from SG-0 NON_TEST census +//! (path-only retirement; same semantics per gate #5 notes). Dissolution: PB-Runtime / Row-4. +//! //! Bounded lens application (T-LensAPI / D1): interpret `ArrowBody::UserDefined` graphs //! over substrate-shaped [`FieldValue`] — no whole-claim operator recognizers. //! @@ -3057,7 +3056,7 @@ pub mod lens_t_las_carrier { // from SG-0 hand-Rust census; this nested module preserves the stable `v3_compiler::lens_testgen` // surface (re-exported from `lib.rs`) until PB-Runtime owns testgen end-to-end. pub mod lens_testgen { - include!("lens_testgen_body.txt"); + include!("src/lens_testgen_body.txt"); } pub use substrate_reflection::reflect_behavior_list; diff --git a/src/v3/compiler/src/cementing_dispatch.rs b/src/v3/compiler/src/cementing_dispatch.rs index f5d278e1cdc..e432b4bafa7 100644 --- a/src/v3/compiler/src/cementing_dispatch.rs +++ b/src/v3/compiler/src/cementing_dispatch.rs @@ -1002,6 +1002,10 @@ pub mod gunbc_ci { use self::integration_rs_wiring_scan::integration_rs_cementing_path_attr_binds_mod_stem; use self::r3_gate_87_cementing_regen_runner_suites::r3_gate_87_cementing_regen_pb_b1_dag_module_stems; +use std::collections::{BTreeSet, HashSet}; +use std::path::Path as FsPath; + +use crate::dag::{Dag, FieldValue, LiteralBits, TypeConnective, ValueBody}; fn record_field<'a>(fields: &'a [(String, FieldValue)], label: &str) -> Option<&'a FieldValue> { fields From 8d2133ae3eacd75f4c1eab5830a52f11945fac2a Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 19:29:53 -0400 Subject: [PATCH 5/8] WIP: PB-0 cycle-2 NON_TEST retirement (msg_84abadad Track A): retire 6 paths --- .../compiler/lens_declaration_apply_body.txt | 24 +++++++++---------- src/v3/compiler/src/lib.rs | 3 ++- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/v3/compiler/lens_declaration_apply_body.txt b/src/v3/compiler/lens_declaration_apply_body.txt index 57c6b5b84a0..69db0b649ca 100644 --- a/src/v3/compiler/lens_declaration_apply_body.txt +++ b/src/v3/compiler/lens_declaration_apply_body.txt @@ -1,15 +1,15 @@ -//! PB-0 cycle-2: bounded lens application module body (`lens_declaration_apply_body.txt`), included -//! from `lib.rs` so `src/v3/compiler/src/lens_declaration_apply.rs` retires from SG-0 NON_TEST census -//! (path-only retirement; same semantics per gate #5 notes). Dissolution: PB-Runtime / Row-4. -//! -//! Bounded lens application (T-LensAPI / D1): interpret `ArrowBody::UserDefined` graphs -//! over substrate-shaped [`FieldValue`] — no whole-claim operator recognizers. -//! -//! **R3 gate #5 (`lens_apply_dot_rs_retired`):** the legacy path -//! `src/v3/compiler/src/lens_apply.rs` is **retired** (path absent from the tree). -//! Bounded lens application and `lens_testgen` live here until PB-Runtime / Row-4 -//! readiness dissolves the host shim, per `docs/r3-program-plan.md` §1.8 and -//! `INVARIANTS` P5(b) deferral language on the tracking PR. +// PB-0 cycle-2: bounded lens application module body (`lens_declaration_apply_body.txt`), included +// from `lib.rs` so `src/v3/compiler/src/lens_declaration_apply.rs` retires from SG-0 NON_TEST census +// (path-only retirement; same semantics per gate #5 notes). Dissolution: PB-Runtime / Row-4. +// +// Bounded lens application (T-LensAPI / D1): interpret `ArrowBody::UserDefined` graphs +// over substrate-shaped [`FieldValue`] — no whole-claim operator recognizers. +// +// R3 gate #5 (`lens_apply_dot_rs_retired`): the legacy path +// `src/v3/compiler/src/lens_apply.rs` is retired (path absent from the tree). +// Bounded lens application and `lens_testgen` live here until PB-Runtime / Row-4 +// readiness dissolves the host shim, per `docs/r3-program-plan.md` §1.8 and +// `INVARIANTS` P5(b) deferral language on the tracking PR. use std::collections::{HashMap, HashSet}; use std::str::FromStr; diff --git a/src/v3/compiler/src/lib.rs b/src/v3/compiler/src/lib.rs index e43200b8f38..fbe8aef6a17 100644 --- a/src/v3/compiler/src/lib.rs +++ b/src/v3/compiler/src/lib.rs @@ -3765,7 +3765,8 @@ pub mod evaluator { } mod int_literal_ranges; /// T-LensAPI D1: bounded lens interpreter over substrate-shaped [`FieldValue`] -/// (module body: `lens_declaration_apply_body.txt`, included here for SG-0 census path retirement). +/// (R3 gate #5: `lens_apply.rs` retired; body in `lens_declaration_apply_body.txt`, included here +/// for SG-0 census path retirement until PB-Runtime / Row-4). pub mod lens_declaration_apply { include!(concat!( env!("CARGO_MANIFEST_DIR"), From 87e96fb96b8f235529e4eaaa04305c283b68bcff Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 19:47:43 -0400 Subject: [PATCH 6/8] WIP: PB-0 cycle-2 NON_TEST retirement (msg_84abadad Track A): retire 6 paths --- INVARIANTS.md | 2 +- src/v3/compiler/src/test_runner.rs | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/INVARIANTS.md b/INVARIANTS.md index 55da9950fe1..c808f3d3f55 100644 --- a/INVARIANTS.md +++ b/INVARIANTS.md @@ -351,7 +351,7 @@ Per **Dispatch-Discipline Mechanisms (b)**, each path in `EXPECTED_HAND_AUTHORED | Path | Receipt | |------|---------| -| `src/v3/compiler/src/cementing_dispatch.rs` | **ROADMAP:** `ROADMAP.md` — **v3 lens capability honesty pass** (same posture as `wiring_scanner_test.rs`). **Dissolution (host scan):** reflected wiring facts at the runner edge so dispatch does not read `tests/integration.rs` from disk. **Dissolution (receipt expansion):** move the `expected_cementing_receipt_triples` projection bridge out of Rust into structural `.dag` data keyed from the register ∩ `regen.dag` projection (single roster authority). **Interim ratchet:** `CementingDispatchMatchesProjection` + closed `CementingBandCReceiptKind` in `tests/dag/cementing_dispatch.dag`. **PB-0 cycle-2 path retirement (PR #3046):** nested `pub mod` surfaces `gunbc_ci`, `integration_rs_wiring_scan`, and `r3_gate_87_cementing_regen_runner_suites` live in this file (same dissolution triggers and APIs; `lib.rs` re-exports them at the prior crate paths for SG-0 census). | +| `src/v3/compiler/src/cementing_dispatch.rs` | **Cementing dispatch surface (path `cementing_dispatch.rs` on SG-0 census):** **ROADMAP:** `ROADMAP.md` — **v3 lens capability honesty pass** (same posture as `wiring_scanner_test.rs`). **Dissolution (host scan):** reflected wiring facts at the runner edge so dispatch does not read `tests/integration.rs` from disk. **Dissolution (receipt expansion):** move the `expected_cementing_receipt_triples` projection bridge out of Rust into structural `.dag` data keyed from the register ∩ `regen.dag` projection (single roster authority). **Interim ratchet:** `CementingDispatchMatchesProjection` + closed `CementingBandCReceiptKind` in `tests/dag/cementing_dispatch.dag`. **PB-0 cycle-2 (PR #3046) consolidation:** former standalone files `integration_rs_wiring_scan.rs`, `r3_gate_87_cementing_regen_runner_suites.rs`, and `gunbc_ci.rs` are nested `pub mod` blocks in this module; `lib.rs` re-exports them at the prior crate paths. The following bullets carry forward the **same checkable P5 receipts** that previously lived on separate INVARIANTS rows (Dispatch-Discipline **(b)** — one row per census path, expanded cell per consolidation PR). **Nested `integration_rs_wiring_scan` (retired path `src/v3/compiler/src/integration_rs_wiring_scan.rs`):** **ROADMAP:** same **v3 lens capability honesty pass** bullet as `wiring_scanner_test.rs`. **Dissolution:** remove when `tests/integration.rs` cementing `#[path]` / `mod` wiring can be validated structurally (no line scanner). **Interim ratchet:** `Err` (not panic) on raw/byte string openers in **Code** so `CementingDispatchMatchesProjection` surfaces `ClaimResult::Fail`. **Nested `r3_gate_87_cementing_regen_runner_suites` (retired path `src/v3/compiler/src/r3_gate_87_cementing_regen_runner_suites.rs`):** **ROADMAP:** same **v3 lens capability honesty pass** / gate #87 closure. **Dissolution:** PB-B-1 gate-#87 harness inventory is generated or reflected as substrate facts (no hand-maintained `R3_GATE_87_CEMENTING_REGEN_SUITES` table); `t_pb_b_1_dag_runner_test`, `cementing_dispatch`, and `r3_gate_87_lens_cementing_regen_receipts_test` consume that single authority. **Interim ratchet:** one merge-visible table shared across those consumers (INVARIANTS P2 single authority). **Nested `gunbc_ci` (retired path `src/v3/compiler/src/gunbc_ci.rs`):** **ROADMAP:** `ROADMAP.md` § **Forward-Tracked Lane: T-Workflow-As-Data** (≈L57–L63; substrate lane feeding program gate **#103** `ci_uses_affected_set_selection` per `docs/r3-program-plan.md` + `docs/briefs/r3-wave1-s6-slice7-affected-set-impl-worker.md`). **Dissolution:** remove when `CIWorkflowDag` affected-gate dispatch is owned by `.dag` programs / generated `TestClaim` receipts (or the dag compiler emits selection) with **no** parallel Rust mirror over the `dsl/gunbc/ci.dag` carrier. **Interim ratchet:** `select_affected_gates` + in-module hermetic tests pin symmetric edge-expansion semantics consistent with `CIGateEdge { from, to }` in `dsl/gunbc/ci.dag`. | --- diff --git a/src/v3/compiler/src/test_runner.rs b/src/v3/compiler/src/test_runner.rs index 1cfde961c27..d67110abbdc 100644 --- a/src/v3/compiler/src/test_runner.rs +++ b/src/v3/compiler/src/test_runner.rs @@ -4708,10 +4708,24 @@ impl<'a> TestRunner<'a> { )); } }; - let entries = match sg0_census_list_entries(&list_constant_name) { + let mut entries = match sg0_census_list_entries(&list_constant_name) { Ok(entries) => entries, Err(reason) => return ClaimResult::Fail(reason), }; + // Lens-producer paths may live on `EXPECTED_HAND_AUTHORED_FRAGMENTS` after a path-only + // retirement (e.g. `lens_declaration_apply_body.txt`). `CensusSubsetCount` for + // `lens_producer_files_remaining` still names `expected_hand_authored_non_test` in `.dag` + // claims — union the fragment list so the subset predicate cannot silently drop to zero. + if list_constant_name == "expected_hand_authored_non_test" + && subset_name == "lens_producer_files_subset_predicate" + { + match sg0_census_list_entries("expected_hand_authored_fragments") { + Ok(fragments) => entries.extend(fragments), + Err(reason) => return ClaimResult::Fail(reason), + } + entries.sort(); + entries.dedup(); + } let count = entries.iter().filter(|path| predicate(path)).count() as i64; if count == 0 { ClaimResult::Pass From ab592e841e6b0e070d186bad195e07592ebd92c4 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 23:48:12 +0000 Subject: [PATCH 7/8] fix(test_runner): union FRAGMENTS for lens-producer CensusSubsetCount CensusSubsetCount for lens_producer_files_subset_predicate still reads expected_hand_authored_non_test from .dag claims; after PB-0 the bounded lens host path moved to EXPECTED_HAND_AUTHORED_FRAGMENTS. Merge fragment entries before filtering so the ratchet cannot falsely pass with zero matches. INVARIANTS SG-0 row for cementing_dispatch.rs already carries the full nested gunbc_ci T-Workflow-As-Data / gate #103 P5 receipt from the retired standalone row. Co-authored-by: Cursor --- src/v3/compiler/src/test_runner.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/v3/compiler/src/test_runner.rs b/src/v3/compiler/src/test_runner.rs index d67110abbdc..addfd29a337 100644 --- a/src/v3/compiler/src/test_runner.rs +++ b/src/v3/compiler/src/test_runner.rs @@ -6344,9 +6344,11 @@ fn sg0_quoted_path_from_line(line: &str) -> Option { /// Hand-Rust surfaces counted by T-PB-A `lens_producer_files_remaining` (`CensusSubsetCount`). /// -/// `lens_apply.rs` → `lens_declaration_apply.rs` is a **path** retirement only; the bounded -/// lens host remains a lens-producer residual until PB-Runtime owns application/reflection -/// (Row-4 / §7.1). Omitting this path would falsely show census “progress” after a rename. +/// `lens_apply.rs` → `lens_declaration_apply.rs` → `lens_declaration_apply_body.txt` is a **path** +/// retirement chain only; the bounded lens host remains a lens-producer residual until PB-Runtime +/// owns application/reflection (Row-4 / §7.1). `eval_census_subset_count_shape` unions +/// `EXPECTED_HAND_AUTHORED_FRAGMENTS` when applying this predicate to `expected_hand_authored_non_test` +/// so a fragment move cannot fake census progress. fn is_lens_producer_census_path(path: &str) -> bool { matches!( path, From b9153769c60fbd8075896c655afc4d52d9a7f695 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 19:52:38 -0400 Subject: [PATCH 8/8] WIP: PB-0 cycle-2 NON_TEST retirement (msg_84abadad Track A): retire 6 paths --- src/v3/compiler/lens_declaration_apply_body.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v3/compiler/lens_declaration_apply_body.txt b/src/v3/compiler/lens_declaration_apply_body.txt index 69db0b649ca..2bfee1d218d 100644 --- a/src/v3/compiler/lens_declaration_apply_body.txt +++ b/src/v3/compiler/lens_declaration_apply_body.txt @@ -1152,7 +1152,7 @@ mod tests { #[test] fn named_function_count_on_trivial_program() { - let src = include_str!("../../lenses/named_function_count.dag"); + let src = include_str!("../lenses/named_function_count.dag"); let lens_dag = compile_to_dag(src, "src/v3/lenses/named_function_count.dag").expect("lens compiles"); let prog = compile_to_dag("let x: Int = 1", "lens_apply_prog.v3").expect("prog compiles");