From 2d4a529ec4dd9bf5f5e2add0184fd4e1f430aab9 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 14 May 2026 03:47:19 +0000 Subject: [PATCH 1/9] =?UTF-8?q?Revert=20"PB-0=20cycle=205=20NON=5FTEST=20?= =?UTF-8?q?=E2=80=94=20cementing=5Fdispatch=20diagnostics=20dimension=20pi?= =?UTF-8?q?peline=5Fauthority=20post=5Femit=5Fverifier=20infer=20=E2=80=94?= =?UTF-8?q?=20brief=20docs/briefs/r3-pb0-non-test-retirement-worker-cycle5?= =?UTF-8?q?.md=20Director=20msg=5Fb44f4320=20(#3057)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 7efdd2ba2d99894306996e248dbc9d76f908d950. --- src/v3/compiler/build.rs | 25 +- src/v3/compiler/src/cementing_dispatch.rs | 20 +- src/v3/compiler/src/diagnostics.rs | 6 - src/v3/compiler/src/dimension.rs | 6 - src/v3/compiler/src/infer.rs | 6 - src/v3/compiler/src/pipeline_authority.rs | 6 - src/v3/compiler/src/post_emit_verifier.rs | 6 - .../tests/integration/sg0_census_test.rs | 9 + .../cementing_dispatch.rs.in | 1590 ---- .../diagnostics.rs.in | 1631 ---- .../pb0_cycle4_emit_templates/dimension.rs.in | 539 -- tools/pb0_cycle4_emit_templates/infer.rs.in | 7262 ----------------- .../pipeline_authority.rs.in | 261 - .../post_emit_verifier.rs.in | 463 -- 14 files changed, 17 insertions(+), 11813 deletions(-) delete mode 100644 tools/pb0_cycle4_emit_templates/cementing_dispatch.rs.in delete mode 100644 tools/pb0_cycle4_emit_templates/diagnostics.rs.in delete mode 100644 tools/pb0_cycle4_emit_templates/dimension.rs.in delete mode 100644 tools/pb0_cycle4_emit_templates/infer.rs.in delete mode 100644 tools/pb0_cycle4_emit_templates/pipeline_authority.rs.in delete mode 100644 tools/pb0_cycle4_emit_templates/post_emit_verifier.rs.in diff --git a/src/v3/compiler/build.rs b/src/v3/compiler/build.rs index e0918e824ac..a03b0ee1ff6 100644 --- a/src/v3/compiler/build.rs +++ b/src/v3/compiler/build.rs @@ -436,7 +436,7 @@ fn emit_r1_gates_fixture(manifest_path: &Path, v3_dir: &Path) { /// **Hygiene:** `repeat_string` exists in the bootstrap bundle only via this excerpt — no /// hand-maintained duplicate of `render.dag` (INVARIANTS P2). Edit the marked region in /// `render.dag` only; this output is overwritten on `cargo build -p v3-compiler`. -/// Emit PB-0 host authority modules (former `EXPECTED_HAND_AUTHORED_NON_TEST` paths) from +/// Emit PB-0 cycle-4 host authority modules (former `EXPECTED_HAND_AUTHORED_NON_TEST` paths) from /// checked-in templates so `REGEN_OUTPUTS` / `GENERATED_FILES` stay the sole SG-0 partition /// authority (`sg0_v3_hand_authored_census`). /// @@ -464,10 +464,6 @@ fn emit_pb0_cycle4_host_authority_modules(repo_root: &Path) { ); let tmpl_dir = repo_root.join("tools/pb0_cycle4_emit_templates"); let specs: &[(&str, &str)] = &[ - ( - "cementing_dispatch.rs.in", - "src/v3/compiler/src/cementing_dispatch.rs", - ), ( "complexity_lattice.rs.in", "src/v3/compiler/src/complexity_lattice_generated.rs", @@ -476,8 +472,6 @@ fn emit_pb0_cycle4_host_authority_modules(repo_root: &Path) { "cost_basis_declaration.rs.in", "src/v3/compiler/src/cost_basis_declaration_generated.rs", ), - ("diagnostics.rs.in", "src/v3/compiler/src/diagnostics.rs"), - ("dimension.rs.in", "src/v3/compiler/src/dimension.rs"), ( "emit_collection_ops_method_contract.rs.in", "src/v3/compiler/src/emit/collection_ops_method_contract_generated.rs", @@ -494,19 +488,10 @@ fn emit_pb0_cycle4_host_authority_modules(repo_root: &Path) { "int_literal_ranges.rs.in", "src/v3/compiler/src/int_literal_ranges_generated.rs", ), - ("infer.rs.in", "src/v3/compiler/src/infer.rs"), ( "pb_method_template_projection.rs.in", "src/v3/compiler/src/pb_method_template_projection_generated.rs", ), - ( - "pipeline_authority.rs.in", - "src/v3/compiler/src/pipeline_authority.rs", - ), - ( - "post_emit_verifier.rs.in", - "src/v3/compiler/src/post_emit_verifier.rs", - ), ]; for (tmpl_name, rel_out) in specs { let src_path = tmpl_dir.join(tmpl_name); @@ -769,9 +754,8 @@ fn main() { "src/v3/compiler/src/bootstrap_generated.rs", "src/v3/compiler/src/bootstrap_generated_without_parse_surface.rs", "src/v3/compiler/src/bootstrap_std_generated.rs", - // PB-0: former hand `EXPECTED_HAND_AUTHORED_NON_TEST` modules — templates under + // PB-0 cycle-4: former hand `EXPECTED_HAND_AUTHORED_NON_TEST` modules — templates under // `tools/pb0_cycle4_emit_templates/*.rs.in`, emitted by `emit_pb0_cycle4_host_authority_modules`. - "src/v3/compiler/src/cementing_dispatch.rs", "src/v3/compiler/src/complexity_lattice_generated.rs", // SG-5 substrate / runtime-mirror projections, generated from // `src/v3/std/substrate.dag` + `src/v3/std/parse_surface.dag` @@ -789,13 +773,10 @@ fn main() { "src/v3/compiler/src/dag_lookup_generated.rs", "src/v3/compiler/src/dag_scalar_generated.rs", "src/v3/compiler/src/dag_value_body_generated.rs", - "src/v3/compiler/src/diagnostics.rs", "src/v3/compiler/src/diagnostics_generated.rs", - "src/v3/compiler/src/dimension.rs", "src/v3/compiler/src/emit/collection_ops_method_contract_generated.rs", "src/v3/compiler/src/emit_rust_bin_shim_generated.rs", "src/v3/compiler/src/enforced_lens_application_generated.rs", - "src/v3/compiler/src/infer.rs", "src/v3/compiler/src/infer_helpers_generated.rs", "src/v3/compiler/src/int_literal_ranges_generated.rs", "src/v3/compiler/src/lens_cost_symbolic_generated.rs", @@ -807,8 +788,6 @@ fn main() { "src/v3/compiler/src/lens_unused_parameters_generated.rs", "src/v3/compiler/src/lower_helpers_generated.rs", "src/v3/compiler/src/operators_generated.rs", - "src/v3/compiler/src/pipeline_authority.rs", - "src/v3/compiler/src/post_emit_verifier.rs", "src/v3/compiler/src/pb_method_template_projection_generated.rs", "src/v3/compiler/src/parse_generated.rs", "src/v3/compiler/src/parse_surface_generated.rs", diff --git a/src/v3/compiler/src/cementing_dispatch.rs b/src/v3/compiler/src/cementing_dispatch.rs index 6a9de3c3324..c2d66994603 100644 --- a/src/v3/compiler/src/cementing_dispatch.rs +++ b/src/v3/compiler/src/cementing_dispatch.rs @@ -1,15 +1,8 @@ -// AUTO-GENERATED by src/v3/compiler/build.rs (`emit_pb0_cycle4_host_authority_modules`). -// -// Template: `tools/pb0_cycle4_emit_templates/.rs.in` (workspace root). -// Dissolution receipt: PB-0 NON_TEST partition — `REGEN_OUTPUTS` producer; full `.dag` emit T-LP track. -// - //! Band-C cementing dispatch — [`crate::test_runner::TestPredicate::CementingDispatchMatchesProjection`]. //! -//! SG-0 / INVARIANTS P5: PB-0 generated compiler module. The authoritative body lives in -//! `tools/pb0_cycle4_emit_templates/cementing_dispatch.rs.in`; `build.rs` -//! emits `src/v3/compiler/src/cementing_dispatch.rs` and registers it in -//! `REGEN_OUTPUTS` / `GENERATED_FILES`. +//! SG-0 / INVARIANTS P5: hand-authored `src/v3/compiler/src/` module — register the path in +//! `EXPECTED_HAND_AUTHORED_NON_TEST` inside `tests/integration/sg0_census_test.rs` when this +//! file ships or changes materially. //! //! Projects `LensRegistryEntry` rows from the bootstrapped `regen.dag` authority against //! `std.verification` `lens_capability_register_rows` (closed coproduct status axes per @@ -30,10 +23,9 @@ pub mod integration_rs_wiring_scan { //! `tests/integration.rs` wiring scanner (Band-C cementing dispatch). //! - //! SG-0 / INVARIANTS P5: this scanner is hosted by the PB-0 generated - //! `cementing_dispatch.rs` output. Edit the template under - //! `tools/pb0_cycle4_emit_templates/`; `build.rs` refreshes the emitted - //! compiler module. + //! 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 — diff --git a/src/v3/compiler/src/diagnostics.rs b/src/v3/compiler/src/diagnostics.rs index e22d5bd143c..9bf30646ff6 100644 --- a/src/v3/compiler/src/diagnostics.rs +++ b/src/v3/compiler/src/diagnostics.rs @@ -1,9 +1,3 @@ -// AUTO-GENERATED by src/v3/compiler/build.rs (`emit_pb0_cycle4_host_authority_modules`). -// -// Template: `tools/pb0_cycle4_emit_templates/.rs.in` (workspace root). -// Dissolution receipt: PB-0 NON_TEST partition — `REGEN_OUTPUTS` producer; full `.dag` emit T-LP track. -// - // Diagnostics and the enforced mark_unresolved API. // // The load-bearing guarantee: diff --git a/src/v3/compiler/src/dimension.rs b/src/v3/compiler/src/dimension.rs index ffdc2ee77e9..28427c626c4 100644 --- a/src/v3/compiler/src/dimension.rs +++ b/src/v3/compiler/src/dimension.rs @@ -1,9 +1,3 @@ -// AUTO-GENERATED by src/v3/compiler/build.rs (`emit_pb0_cycle4_host_authority_modules`). -// -// Template: `tools/pb0_cycle4_emit_templates/.rs.in` (workspace root). -// Dissolution receipt: PB-0 NON_TEST partition — `REGEN_OUTPUTS` producer; full `.dag` emit T-LP track. -// - //! DB-3 dimension analysis — Lane 2 Stage 2f. //! //! Type authority: `src/v3/std/dimensions.dag`, spine order: diff --git a/src/v3/compiler/src/infer.rs b/src/v3/compiler/src/infer.rs index b5205eb552a..8e62b4bc523 100644 --- a/src/v3/compiler/src/infer.rs +++ b/src/v3/compiler/src/infer.rs @@ -1,9 +1,3 @@ -// AUTO-GENERATED by src/v3/compiler/build.rs (`emit_pb0_cycle4_host_authority_modules`). -// -// Template: `tools/pb0_cycle4_emit_templates/.rs.in` (workspace root). -// Dissolution receipt: PB-0 NON_TEST partition — `REGEN_OUTPUTS` producer; full `.dag` emit T-LP track. -// - // Type inference: fills Port.state by propagating types forward through // the DAG. Dispatches on TypeConnective (M1_DESIGN.md §4) instead of the // old name-based function signature lookup. diff --git a/src/v3/compiler/src/pipeline_authority.rs b/src/v3/compiler/src/pipeline_authority.rs index b35341b6c71..70f86a7f828 100644 --- a/src/v3/compiler/src/pipeline_authority.rs +++ b/src/v3/compiler/src/pipeline_authority.rs @@ -1,9 +1,3 @@ -// AUTO-GENERATED by src/v3/compiler/build.rs (`emit_pb0_cycle4_host_authority_modules`). -// -// Template: `tools/pb0_cycle4_emit_templates/.rs.in` (workspace root). -// Dissolution receipt: PB-0 NON_TEST partition — `REGEN_OUTPUTS` producer; full `.dag` emit T-LP track. -// - use crate::dag::{Dag, DeclarationId, FieldValue, TypeConnective, ValueBody}; #[cfg(test)] diff --git a/src/v3/compiler/src/post_emit_verifier.rs b/src/v3/compiler/src/post_emit_verifier.rs index 266d0f52727..463c627193d 100644 --- a/src/v3/compiler/src/post_emit_verifier.rs +++ b/src/v3/compiler/src/post_emit_verifier.rs @@ -1,9 +1,3 @@ -// AUTO-GENERATED by src/v3/compiler/build.rs (`emit_pb0_cycle4_host_authority_modules`). -// -// Template: `tools/pb0_cycle4_emit_templates/.rs.in` (workspace root). -// Dissolution receipt: PB-0 NON_TEST partition — `REGEN_OUTPUTS` producer; full `.dag` emit T-LP track. -// - //! E-5 / Lane 1 Stage 1c PR 4 — shared test harness that reads each //! target's `CleanEmissionContract.post_emit_verifier` record and //! invokes the declared verifier against an emitted source file. diff --git a/src/v3/compiler/tests/integration/sg0_census_test.rs b/src/v3/compiler/tests/integration/sg0_census_test.rs index 3668542c448..00424983745 100644 --- a/src/v3/compiler/tests/integration/sg0_census_test.rs +++ b/src/v3/compiler/tests/integration/sg0_census_test.rs @@ -270,18 +270,25 @@ const EXPECTED_HAND_AUTHORED_NON_TEST: &[&str] = &[ "src/v3/compiler/src/bin/self_host_fixed_point.rs", "src/v3/compiler/src/bootstrap.rs", "src/v3/compiler/src/bootstrap_regen_fresh.rs", + // R3 gate #87 / T-Tests-As-Data-Completeness: `CementingDispatchMatchesProjection` host + // evaluator for `tests/dag/cementing_dispatch.dag` (P5 consumer receipt; dissolves when + // predicate substrate owns the walk without host FS coupling). + "src/v3/compiler/src/cementing_dispatch.rs", "src/v3/compiler/src/dag.rs", "src/v3/compiler/src/dag/builder.rs", // Closed Cardinality payload + idempotent target shim (API closure). "src/v3/compiler/src/dag/cardinality_payload.rs", "src/v3/compiler/src/dag/effects.rs", "src/v3/compiler/src/dag/ports.rs", + "src/v3/compiler/src/diagnostics.rs", + "src/v3/compiler/src/dimension.rs", "src/v3/compiler/src/emit.rs", "src/v3/compiler/src/emit/python_target.rs", "src/v3/compiler/src/emit/rust_target.rs", "src/v3/compiler/src/emit_rust.rs", // 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/infer.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` @@ -297,6 +304,8 @@ const EXPECTED_HAND_AUTHORED_NON_TEST: &[&str] = &[ // fix moved it out of `emit.rs`. Dissolves when the equivalent Shape B // `.dag` program owns the OpenAPI artifact projection end-to-end. "src/v3/compiler/src/omni_shape_b_openapi.rs", + "src/v3/compiler/src/pipeline_authority.rs", + "src/v3/compiler/src/post_emit_verifier.rs", // PB-1 Item 5: host mirror of `dsl/std/process.dag` `ProcessExit` for emitted bin shims. "src/v3/compiler/src/process_exit.rs", // R1C-E (T-Emit `.dag` `TestClaim` wrappers): shared `check_*` API the host diff --git a/tools/pb0_cycle4_emit_templates/cementing_dispatch.rs.in b/tools/pb0_cycle4_emit_templates/cementing_dispatch.rs.in deleted file mode 100644 index affd27617d5..00000000000 --- a/tools/pb0_cycle4_emit_templates/cementing_dispatch.rs.in +++ /dev/null @@ -1,1590 +0,0 @@ -//! Band-C cementing dispatch — [`crate::test_runner::TestPredicate::CementingDispatchMatchesProjection`]. -//! -//! SG-0 / INVARIANTS P5: PB-0 generated compiler module. The authoritative body lives in -//! `tools/pb0_cycle4_emit_templates/cementing_dispatch.rs.in`; `build.rs` -//! emits `src/v3/compiler/src/cementing_dispatch.rs` and registers it in -//! `REGEN_OUTPUTS` / `GENERATED_FILES`. -//! -//! Projects `LensRegistryEntry` rows from the bootstrapped `regen.dag` authority against -//! `std.verification` `lens_capability_register_rows` (closed coproduct status axes per -//! `docs/design-tests-as-data-completeness.md` §8.3), then validates the Band-C receipt list -//! in `cementing_dispatch.dag` and on-disk harness artifacts. -//! -//! **Interim host coupling:** validating `temporary-rust` receipts scans `tests/integration.rs` -//! once per predicate evaluation (see `std::fs::read_to_string` below). Dissolution: consume -//! reflected wiring facts at the runner edge so Band-C dispatch does not depend on crate layout -//! + host FS for this check. -//! -//! **Interim projection bridge:** `expected_cementing_receipt_triples` is a fail-closed Rust match -//! from `(registry_name, regen lens_file basename)` to the canonical receipt triples the -//! `cementing_band_c_v2_complete_receipts` list must equal. Dissolution: move that expansion into -//! `.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. - -pub mod integration_rs_wiring_scan { - //! `tests/integration.rs` wiring scanner (Band-C cementing dispatch). - //! - //! SG-0 / INVARIANTS P5: this scanner is hosted by the PB-0 generated - //! `cementing_dispatch.rs` output. Edit the template under - //! `tools/pb0_cycle4_emit_templates/`; `build.rs` refreshes the emitted - //! compiler module. - //! - //! **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 — **Layer 1** affected-set dispatch substrate. - //! - //! **Authority** (Phase B): `docs/briefs/r3-wave1-s6-slice7-affected-set-impl-worker.md`; - //! fail-closed behavior aligned with - //! `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §3 (superset - //! table) applied to **gate-id** touch seeds (see §4 for how end-state runners join - //! `NodeRef` / dimension receipts to obligations — **not implemented in this module**); - //! carrier shape: `dsl/gunbc/ci.dag` (`CIWorkflowDag`, `CIGateEdge`: `from` is a prerequisite of `to`). - //! - //! **Layer 1 vs canvas §7 / ROADMAP gate #103:** program row `ci_uses_affected_set_selection` - //! eventually requires the Slice-5 **BinaryShim** runner to wire PR #2713 affected-set - //! receipts through `CIWorkflowDag` / `TestClaim` metadata (canvas §§1.1–1.3, §4, §7). - //! This crate surface **does not** close that hand-off: there is no typed PR #2713 - //! receipt type here and no runner entry mapping `Set` into these APIs. - //! What lands here is the **pure graph planner** — [`select_affected_gates`] — plus a - //! thin [`CiBinaryShimAffectedSetReceipt`] adapter for **gate-id seeds only** (tests + - //! future runner glue once upstream produces `CIGate.id` touches). - //! - //! This module is intentionally **pure**: every decision is a function of - //! [`CiWorkflowDagInput`] + [`CiWorkflowDiff`] only. Git diffs, path-regex, env, - //! and PR #2713 lens output must be **mapped upstream** 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 runner-side dispatch 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 lens receipts stay - // outside this module (Slice 7 brief + canvas §§1.1–4 end-state runner; Layer 1 only here). - /// 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, - } - } - - /// **Layer 1** gate-id touch seed for [`select_affected_gates_for_binary_shim`]. - /// - /// Canvas `design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §1.1 - /// steps 3–5 and §4 require the **runner** to consume PR #2713 structured output, join - /// it with `CIWorkflowDag` / `TestClaim` metadata, and **then** emit executable work. - /// This struct is **not** that receipt: it only carries `CIGate.id` strings (and a - /// narrowing flag) **after** any `NodeRef` → gate-id mapping has happened elsewhere. - /// - /// When `narrowing_available` is `false`, selection fails closed to the full gate - /// roster ([`CiWorkflowDiff::TouchAll`]; canvas §3: unknown dimension / missing - /// receipt / unbuildable DAG pair). - #[derive(Debug, Clone, PartialEq, Eq, Default)] - pub struct CiBinaryShimAffectedSetReceipt { - /// `false` forces [`CiWorkflowDiff::TouchAll`] regardless of `proven_direct_gate_touches`. - pub narrowing_available: bool, - /// Gate ids proven directly touched **after** upstream mapping (seed before symmetric expansion). - pub proven_direct_gate_touches: BTreeSet, - } - - /// Layer 1 entry: map a **gate-id-only** [`CiBinaryShimAffectedSetReceipt`] into [`select_affected_gates`]. - pub fn select_affected_gates_for_binary_shim( - dag: &CiWorkflowDagInput, - receipt: &CiBinaryShimAffectedSetReceipt, - ) -> Result, CiAffectedGatesError> { - let diff = if receipt.narrowing_available { - CiWorkflowDiff::TouchedGates(receipt.proven_direct_gate_touches.clone()) - } else { - CiWorkflowDiff::TouchAll - }; - select_affected_gates(dag, &diff) - } - - 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)); - } - - #[test] - fn binary_shim_receipt_unknown_narrowing_matches_touch_all() { - let dag = demo_ci_dag(); - let receipt = CiBinaryShimAffectedSetReceipt { - narrowing_available: false, - proven_direct_gate_touches: BTreeSet::from(["lint".into()]), - }; - let got = select_affected_gates_for_binary_shim(&dag, &receipt).expect("valid dag"); - let expect = select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("valid dag"); - assert_eq!(got, expect); - } - - #[test] - fn binary_shim_receipt_narrow_lint_matches_touched_gates_lint() { - let dag = demo_ci_dag(); - let receipt = CiBinaryShimAffectedSetReceipt { - narrowing_available: true, - proven_direct_gate_touches: BTreeSet::from(["lint".into()]), - }; - let got = select_affected_gates_for_binary_shim(&dag, &receipt).expect("valid dag"); - let expect = select_affected_gates( - &dag, - &CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])), - ) - .expect("valid dag"); - assert_eq!(got, expect); - } - - #[test] - fn binary_shim_receipt_empty_narrow_seed_yields_empty_plan() { - let dag = demo_ci_dag(); - let receipt = CiBinaryShimAffectedSetReceipt { - narrowing_available: true, - proven_direct_gate_touches: BTreeSet::new(), - }; - assert_eq!( - select_affected_gates_for_binary_shim(&dag, &receipt), - Ok(Vec::new()) - ); - } - } -} - -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 - .iter() - .find(|(candidate, _)| candidate == label) - .map(|(_, value)| value) -} - -fn string_field(fields: &[(String, FieldValue)], label: &str) -> Result { - match record_field(fields, label) { - Some(FieldValue::Literal(LiteralBits::String(value))) => Ok(value.clone()), - Some(other) => Err(format!("`{label}` must be a string literal, got {other:?}")), - None => Err(format!("record is missing string field `{label}`")), - } -} - -fn disj_variants( - dag: &Dag, - type_name: &str, -) -> Result, String> { - let sum_decl = dag.declaration_by_name(type_name).ok_or_else(|| { - format!( - "merged DAG missing nullary sum `{type_name}` — expected `std.verification` for lens \ - capability axes, or `tests/dag/cementing_dispatch.dag` for `CementingBandCReceiptKind`" - ) - })?; - let TypeConnective::Disj { variants } = &sum_decl.connective else { - return Err(format!("`{type_name}` must be a Disj coproduct")); - }; - Ok(variants.iter().map(|v| (v.label.clone(), v.ty)).collect()) -} - -fn decode_nullary_sum_variant( - dag: &Dag, - sum_type_name: &str, - value: &FieldValue, - field_label: &str, - record_role: &str, -) -> Result { - let variants = disj_variants(dag, sum_type_name)?; - let allowed: HashSet = variants.iter().map(|(_, id)| *id).collect(); - let FieldValue::Variant { - constructor, - payload, - } = value - else { - return Err(format!( - "{record_role} `{field_label}` must be a closed sum variant of `{sum_type_name}`; got {value:?}" - )); - }; - if !payload.is_empty() { - return Err(format!( - "{record_role} `{field_label}`: unexpected variant payload (expected nullary `{sum_type_name}` arm)" - )); - } - if !allowed.contains(constructor) { - return Err(format!( - "{record_role} `{field_label}`: constructor id {:?} is not a declared variant of `{sum_type_name}`", - constructor - )); - } - variants - .iter() - .find(|(_, id)| id == constructor) - .map(|(label, _)| label.clone()) - .ok_or_else(|| { - format!( - "{record_role} `{field_label}`: internal mismatch resolving `{sum_type_name}` variant" - ) - }) -} - -/// Maps `CementingBandCReceiptKind` variant labels to the stable wire tags used in -/// `expected_cementing_receipt_triples` and on-disk dispatch branches. -fn cementing_receipt_kind_wire_tag(sum_variant_label: &str) -> Result<&'static str, String> { - match sum_variant_label { - "DagHarness" => Ok("dag"), - "TemporaryRustModule" => Ok("temporary-rust"), - other => Err(format!( - "cementing receipt `kind`: unknown `CementingBandCReceiptKind` variant `{other}`" - )), - } -} - -fn decode_cementing_band_c_receipt_kind( - dag: &Dag, - value: &FieldValue, - field_label: &str, -) -> Result<&'static str, String> { - let label = decode_nullary_sum_variant( - dag, - "CementingBandCReceiptKind", - value, - field_label, - "cementing receipt row", - )?; - cementing_receipt_kind_wire_tag(&label) -} - -fn record_fields(value: &FieldValue) -> Option<&[(String, FieldValue)]> { - match value { - FieldValue::Record(fields) => Some(fields.as_slice()), - _ => None, - } -} - -fn resolve_declaration_ref_id( - value: &FieldValue, - field_label: &str, -) -> Result { - match value { - FieldValue::Reference(id) => Ok(*id), - FieldValue::Record(fields) if fields.is_empty() => Err(format!( - "`{field_label}`: DeclarationRef is the empty record literal {{}} — use an identifier \ - so lowering emits FieldValue::Reference(DeclarationId), not an empty record", - )), - other => Err(format!( - "`{field_label}`: expected FieldValue::Reference(DeclarationId) \ - for a DeclarationRef edge, got {other:?}" - )), - } -} - -fn list_items_of_declaration( - dag: &Dag, - id: crate::dag::DeclarationId, - role: &str, -) -> Result, String> { - let decl = dag.declaration(id); - match &decl.value_body { - Some(ValueBody::List(items)) => Ok(items.clone()), - other => Err(format!( - "`{role}` must resolve to a `List<…>` data declaration; got {:?} for `{}`", - other, - decl.name.as_deref().unwrap_or("") - )), - } -} - -fn read_lens_registry_name_lens_file_pairs(dag: &Dag) -> Result, String> { - let entry_type_id = dag - .declaration_by_name("LensRegistryEntry") - .map(|decl| decl.id) - .ok_or_else(|| "bootstrap must declare `LensRegistryEntry` (regen.dag)".to_string())?; - - let mut pairs = Vec::new(); - for decl in dag.declarations() { - if decl.meta_tag != Some(entry_type_id) { - continue; - } - let Some(ValueBody::Structural { fields }) = &decl.value_body else { - return Err(format!( - "lens registry entry `{}` must carry a structural value body", - decl.name.as_deref().unwrap_or("") - )); - }; - let binding = decl - .name - .clone() - .unwrap_or_else(|| "".to_string()); - pairs.push(( - string_field(fields, "name") - .map_err(|e| format!("lens registry entry `{binding}`: {e}"))?, - string_field(fields, "lens_file") - .map_err(|e| format!("lens registry entry `{binding}`: {e}"))?, - )); - } - Ok(pairs) -} - -/// Closed roster of Band-C `(registry_name, module_stem, kind)` receipts implied by the -/// current `regen.dag` × v2-complete register projection. When a new v2-complete lens -/// lands in `regen.dag`, extend this expansion in the same PR as `cementing_dispatch.dag` -/// receipt rows — otherwise the dispatch predicate fail-closes with an explicit error. -fn expected_cementing_receipt_triples( - registry_pairs: &[(String, String)], - basenames: &BTreeSet, -) -> Result, String> { - let mut out = BTreeSet::new(); - for (name, lens_file) in registry_pairs { - let basename = FsPath::new(lens_file) - .file_name() - .and_then(|s| s.to_str()) - .ok_or_else(|| { - format!("registry entry `{name}` has lens_file without basename: {lens_file}") - })? - .to_string(); - if !basenames.contains(&basename) { - continue; - } - match (name.as_str(), basename.as_str()) { - ("cost", "complexity.dag") => { - out.insert(( - name.clone(), - "t_r3_gate_87_cementing_regen_cost".to_string(), - "dag".to_string(), - )); - out.insert(( - name.clone(), - "complexity_lens_behavioral_completion".to_string(), - "temporary-rust".to_string(), - )); - } - ("cost_symbolic", "cost.dag") => { - out.insert(( - name.clone(), - "t_r3_gate_87_cementing_regen_cost_symbolic".to_string(), - "dag".to_string(), - )); - out.insert(( - name.clone(), - "cost_lens_symbolic_consumer_test".to_string(), - "temporary-rust".to_string(), - )); - } - _ => { - return Err(format!( - "Band-C v2 cementing projection includes `LensRegistryEntry` \ - (`name={name}`, `lens_file` basename `{basename}`) but \ - `expected_cementing_receipt_triples` has no receipt expansion — \ - land matching `cementing_band_c_v2_complete_receipts` rows in \ - `cementing_dispatch.dag` and extend the expansion table in \ - `cementing_dispatch.rs` in the same PR." - )); - } - } - } - Ok(out) -} - -fn v2_cementing_basenames_from_capability_rows( - dag: &Dag, - capability_rows: &[FieldValue], -) -> Result, String> { - let mut basenames = BTreeSet::new(); - for row in capability_rows { - let Some(fields) = record_fields(row) else { - return Err(format!( - "capability_register list: expected record row, got {row:?}" - )); - }; - let lens_basename = string_field(fields, "lens_basename")?; - let structural = record_field(fields, "structural").ok_or_else(|| { - "capability_register row: missing `structural` field (expected \ - `LensCapabilityStructuralStatus` variant)" - .to_string() - })?; - let behavioral = record_field(fields, "behavioral").ok_or_else(|| { - "capability_register row: missing `behavioral` field (expected \ - `LensCapabilityBehavioralStatus` variant)" - .to_string() - })?; - let v2 = record_field(fields, "v2_counterpart").ok_or_else(|| { - "capability_register row: missing `v2_counterpart` field (expected \ - `LensCapabilityV2Counterpart` variant)" - .to_string() - })?; - decode_nullary_sum_variant( - dag, - "LensCapabilityStructuralStatus", - structural, - "structural", - "capability_register row", - )?; - let behavioral_label = decode_nullary_sum_variant( - dag, - "LensCapabilityBehavioralStatus", - behavioral, - "behavioral", - "capability_register row", - )?; - let v2_label = decode_nullary_sum_variant( - dag, - "LensCapabilityV2Counterpart", - v2, - "v2_counterpart", - "capability_register row", - )?; - if behavioral_label == "LensCapabilityBehavioralComplete" - && v2_label == "LensCapabilityV2RealV2" - { - basenames.insert(lens_basename); - } - } - Ok(basenames) -} - -/// Lens basenames that participate in Band-C v2 cementing: `LensCapabilityBehavioralComplete` -/// plus `LensCapabilityV2RealV2` in the canonical `std.verification` `lens_capability_register_rows` -/// list (same projection `CementingDispatchMatchesProjection` uses before intersecting `regen.dag`). -/// -/// Exposed for integration tests that mechanically ratchet the markdown capability table against -/// this structural authority (`lens_register_correspondence_test`). -pub fn lens_capability_register_v2_cementing_basenames( - dag: &Dag, -) -> Result, String> { - let reg_decl = dag - .declaration_by_name("lens_capability_register_rows") - .ok_or_else(|| { - "bootstrap must declare `lens_capability_register_rows` (std.verification)".to_string() - })?; - let capability_rows = - list_items_of_declaration(dag, reg_decl.id, "lens_capability_register_rows")?; - v2_cementing_basenames_from_capability_rows(dag, &capability_rows) -} - -fn cementing_dispatch_two_refs( - payload: &[FieldValue], -) -> Result<(&FieldValue, &FieldValue), String> { - match payload { - [a, b] => Ok((a, b)), - [FieldValue::Record(fields)] => { - let a = record_field(fields, "capability_register").ok_or_else(|| { - "CementingDispatchMatchesProjection record missing `capability_register`" - .to_string() - })?; - let b = record_field(fields, "cementing_receipts").ok_or_else(|| { - "CementingDispatchMatchesProjection record missing `cementing_receipts`".to_string() - })?; - Ok((a, b)) - } - _ => Err(format!( - "CementingDispatchMatchesProjection payload should be two DeclarationRef edges \ - or a record {{ capability_register, cementing_receipts }}; got {} slot(s)", - payload.len() - )), - } -} - -/// Evaluate `CementingDispatchMatchesProjection` for a claim declared in `declaration_file`. -/// -/// Returns `Ok(())` on pass; `Err(reason)` maps to [`crate::test_runner::ClaimResult::Fail`]. -pub(crate) fn evaluate_cementing_dispatch_projection( - dag: &Dag, - declaration_file: &str, - payload: &[FieldValue], -) -> Result<(), String> { - const CEMENTING_DISPATCH_DECL_PATH: &str = "cementing_dispatch.dag"; - if !declaration_file.ends_with(CEMENTING_DISPATCH_DECL_PATH) { - return Err(format!( - "CementingDispatchMatchesProjection is only accepted for claims declared in `**/{CEMENTING_DISPATCH_DECL_PATH}`; got `{declaration_file}`" - )); - } - - let (reg_ref, recv_ref) = cementing_dispatch_two_refs(payload)?; - let reg_id = resolve_declaration_ref_id(reg_ref, "capability_register")?; - let recv_id = resolve_declaration_ref_id(recv_ref, "cementing_receipts")?; - - let capability_rows = list_items_of_declaration(dag, reg_id, "capability_register")?; - let receipt_rows = list_items_of_declaration(dag, recv_id, "cementing_receipts")?; - - let basenames = v2_cementing_basenames_from_capability_rows(dag, &capability_rows)?; - - let registry_pairs = read_lens_registry_name_lens_file_pairs(dag)?; - - let mut matched_basenames = BTreeSet::new(); - for (name, lens_file) in ®istry_pairs { - let Some(basename) = FsPath::new(lens_file).file_name().and_then(|s| s.to_str()) else { - return Err(format!( - "registry entry `{name}` has lens_file without basename: {lens_file}" - )); - }; - if basenames.contains(basename) { - matched_basenames.insert(basename.to_string()); - } - } - - let missing: Vec<_> = basenames.difference(&matched_basenames).cloned().collect(); - if !missing.is_empty() { - return Err(format!( - "`lens_capability_register` escalates v2 cementing for lens basenames {missing:?}, \ - but no `LensRegistryEntry` in bootstrap names those files — fix the register data or add registry entries." - )); - } - - let mut receipt_triples = BTreeSet::new(); - - for row in &receipt_rows { - let Some(fields) = record_fields(row) else { - return Err(format!( - "cementing_receipts list: expected record row, got {row:?}" - )); - }; - let registry_name = string_field(fields, "registry_name")?; - let module_stem = string_field(fields, "module_stem")?; - let kind_field = record_field(fields, "kind").ok_or_else(|| { - "cementing receipt row: missing `kind` field (expected `CementingBandCReceiptKind` \ - variant)" - .to_string() - })?; - let kind_str = decode_cementing_band_c_receipt_kind(dag, kind_field, "kind")?; - let triple = ( - registry_name.clone(), - module_stem.clone(), - kind_str.to_string(), - ); - if !receipt_triples.insert(triple.clone()) { - return Err(format!( - "cementing_receipts contains duplicate receipt identity {triple:?}; each \ - `(registry_name, module_stem, kind)` must be unique." - )); - } - } - - let expected_triples = expected_cementing_receipt_triples(®istry_pairs, &basenames)?; - if receipt_triples != expected_triples { - return Err(format!( - "cementing_receipts `(registry_name, module_stem, kind)` triples must exactly match \ - the Band-C expansion of the register ∩ regen projection — expected {expected_triples:?}, \ - got {receipt_triples:?}" - )); - } - - let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let pb_b1_gate87_dag_stems = r3_gate_87_cementing_regen_pb_b1_dag_module_stems(); - let integration_rs = match std::fs::read_to_string(manifest_dir.join("tests/integration.rs")) { - Ok(text) => text, - Err(err) => { - return Err(format!( - "read tests/integration.rs for cementing wiring ratchet: {err}" - )); - } - }; - - for row in &receipt_rows { - let Some(fields) = record_fields(row) else { - return Err(format!( - "cementing_receipts list: expected record row, got {row:?}" - )); - }; - let registry_name = string_field(fields, "registry_name")?; - let module_stem = string_field(fields, "module_stem")?; - let kind_field = record_field(fields, "kind").ok_or_else(|| { - "cementing receipt row: missing `kind` field (expected `CementingBandCReceiptKind` \ - variant)" - .to_string() - })?; - let kind_str = decode_cementing_band_c_receipt_kind(dag, kind_field, "kind")?; - match kind_str { - "dag" => { - if !pb_b1_gate87_dag_stems.contains(&module_stem) { - return Err(format!( - "registry lens `{registry_name}` lists v2-complete cementing receipt `{module_stem}` \ - as a `.dag` harness, but that stem is not wired in \ - `r3_gate_87_cementing_regen_runner_suites::R3_GATE_87_CEMENTING_REGEN_SUITES` \ - (T-PB-B-1 runner) — extend the shared table or fix the receipt list." - )); - } - let path = manifest_dir - .join("tests") - .join("dag") - .join(format!("{module_stem}.dag")); - if !path.is_file() { - return Err(format!( - "registry lens `{registry_name}` lists v2-complete cementing receipt `{module_stem}` \ - as a `.dag` harness; expected file {}", - path.display() - )); - } - } - "temporary-rust" => { - let path = manifest_dir - .join("tests") - .join("integration") - .join("cementing") - .join(format!("{module_stem}.rs")); - if !path.is_file() { - return Err(format!( - "registry lens `{registry_name}` lists temporary Rust cementing receipt `{module_stem}`; \ - expected file {}", - path.display() - )); - } - if !integration_rs_cementing_path_attr_binds_mod_stem( - &integration_rs, - &module_stem, - )? { - let expected = format!(r#"#[path = "integration/cementing/{module_stem}.rs"]"#); - return Err(format!( - "registry lens `{registry_name}` lists temporary Rust cementing stem `{module_stem}` but \ - tests/integration.rs does not bind `{expected}` to `mod {module_stem};` in the same item \ - (Band-C dispatch)." - )); - } - } - _ => { - return Err( - "cementing receipt `kind`: internal error — wire tag not `dag` or `temporary-rust`" - .to_string(), - ); - } - } - } - - Ok(()) -} diff --git a/tools/pb0_cycle4_emit_templates/diagnostics.rs.in b/tools/pb0_cycle4_emit_templates/diagnostics.rs.in deleted file mode 100644 index 9bf30646ff6..00000000000 --- a/tools/pb0_cycle4_emit_templates/diagnostics.rs.in +++ /dev/null @@ -1,1631 +0,0 @@ -// Diagnostics and the enforced mark_unresolved API. -// -// The load-bearing guarantee: -// Port.value_type == None iff DiagnosticTable contains PortId -// -// Enforcement is structural: Dag::clear_port_type is pub(crate) and -// is ONLY called from Dag::mark_unresolved (which also writes the -// diagnostic entry atomically). No other call path can null a port. -// -// G2 guardrail: SourceSpan always carries a non-empty `file` field. -// Cross-artifact diagnostics (M2+ multi-file, multi-language) rely -// on spans being addressable across files. Do NOT replace this with -// a bare (line, col) pair — that forecloses the thesis goal. -// -// ──────────────────────────────────────────────────────────────── -// DEFERRED DISSOLUTION: Diagnostic enum is a scaffold. -// -// Current variants (M0.5): TokenizerError, ParseError, TypeMismatch, -// ArityMismatch, ResolveError. The v3 target shape per -// docs/v3-modeling-analysis.md §CompilerDiagnostic is a 5-field -// record: -// -// Diagnostic { -// span: SourceSpan, -// category: DiagnosticCategory, // Type | Cardinality | ... -// subject: DiagnosticSubject, // typed ref to thing flagged -// detail: DiagnosticDetail, // category-specific payload -// correction: Correction, // live fixed code or explicit deferral -// producing_node: Option, -// } -// -// The `correction` field is the thesis claim "show the correct code" -// (THESIS.md §"Error handling"). Every diagnostic that can structurally -// compute a fix must carry one. Design: SELF_HOSTING.md §14.6. -// Roundtrip test pattern: break → diagnose → apply correction → -// recompile → zero diagnostics. Lands at L1.5. -// -// TRIGGER for dissolution: when typed DeclarationId / FieldRef references -// replace the name + span carriers below. M1(2.5) delivered the -// DeclarationId substrate; the subject/detail refactor is next. -// -// Extension from 3→5 variants at M0.5 is deferred dissolution with -// justification: the fail-closed invariant (C-8) requires that every -// detectable failure produces a diagnostic, and M0's infer + lower -// stages reach failure categories (arity, resolve) that TypeMismatch -// does not cover. Until Subject/Detail infrastructure exists, -// extension is the least-bad interim form. -// -// DO NOT add a severity field. C-8: all diagnostics are errors. -// DO NOT add a warning variant. DO NOT add a "maybe wrong" state. -// If a condition is detected and it's wrong, emit the diagnostic -// and fail. If it's not wrong, emit nothing. There is no middle. -// ──────────────────────────────────────────────────────────────── - -use std::collections::HashMap; - -use crate::dag::{ - AtomPayload, CardinalityBound, Dag, DeclarationId, Field, FieldValue, PortId, TypeConnective, - ValueBody, -}; -use crate::types::TypeShape; - -// `SourceSpan` / `Correction` are generated mirrors of std-owned -// substrate/diagnostic carriers. The compiler-local `Diagnostic` -// taxonomy below remains a narrow host shim until the staged compiler -// adopts the shared std diagnostic record directly. -include!("diagnostics_generated.rs"); - -impl Correction { - pub fn live( - description: impl Into, - span: SourceSpan, - new_source: impl Into, - ) -> Self { - Self::LiveCorrection { - witness: CorrectionWitness { - description: description.into(), - span, - new_source: new_source.into(), - }, - } - } - - pub fn deferred( - reason: impl Into, - owner: impl Into, - exit_condition: impl Into, - ) -> Self { - Self::DeferredCorrection { - reason: reason.into(), - retirement_plan: RetirementPlan { - owner: owner.into(), - exit_condition: exit_condition.into(), - }, - } - } - - pub fn deferred_for_diagnostic_class(class: &'static str) -> Self { - Self::deferred( - format!("{class} does not yet compute a source-level live correction witness"), - "R3 Gap 9 row #106 diagnostic roundtrip coverage", - format!( - "replace this deferral with a generated/data-backed LiveCorrection for {class}" - ), - ) - } - - pub fn live_witness(&self) -> Result<&CorrectionWitness, CorrectionApplyError> { - match self { - Correction::LiveCorrection { witness } => Ok(witness), - Correction::DeferredCorrection { reason, .. } => Err(CorrectionApplyError::Deferred { - reason: reason.clone(), - }), - } - } - - pub fn description(&self) -> &str { - match self { - Correction::LiveCorrection { witness } => &witness.description, - Correction::DeferredCorrection { reason, .. } => reason, - } - } - - pub fn span(&self) -> Option<&SourceSpan> { - match self { - Correction::LiveCorrection { witness } => Some(&witness.span), - Correction::DeferredCorrection { .. } => None, - } - } - - pub fn new_source(&self) -> Option<&str> { - match self { - Correction::LiveCorrection { witness } => Some(&witness.new_source), - Correction::DeferredCorrection { .. } => None, - } - } -} - -/// Stable `ResolveError.name` for a negative `PositiveIntervalWidth.UnitCount.units` payload when -/// lowering bypasses the normal `Nat` literal gate (e.g. tests calling `enforce_non_negative_unit_count_payload` directly). -/// -/// Wording is centralized for tests that assert on [`Diagnostic::ResolveError`] directly (see repo -/// `TESTING.md`, behavior-driven). Primary authoring rejects `-1` via [`Diagnostic::MagnitudeOutOfRange`] -/// against `Nat`'s decimal range facts. -#[doc(hidden)] -pub fn positive_interval_width_unit_count_requires_nonnegative_units_literal_message( - units: &str, -) -> String { - format!("PositiveIntervalWidth.UnitCount requires nonnegative `units` literal; got {units}") -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CorrectionApplyError { - InvalidSpan { - start: u32, - end: u32, - source_len: usize, - }, - NonCharBoundary { - offset: u32, - }, - Deferred { - reason: String, - }, -} - -#[derive(Debug, Clone)] -pub enum CorrectionValidationError { - Apply(CorrectionApplyError), - Tokenize(Diagnostic), - Parse(Diagnostic), -} - -pub fn apply_correction( - source: &str, - correction: &Correction, -) -> Result { - let witness = correction.live_witness()?; - let start = witness.span.byte_start as usize; - let end = witness.span.byte_end as usize; - if start > end || end > source.len() { - return Err(CorrectionApplyError::InvalidSpan { - start: witness.span.byte_start, - end: witness.span.byte_end, - source_len: source.len(), - }); - } - if !source.is_char_boundary(start) { - return Err(CorrectionApplyError::NonCharBoundary { - offset: witness.span.byte_start, - }); - } - if !source.is_char_boundary(end) { - return Err(CorrectionApplyError::NonCharBoundary { - offset: witness.span.byte_end, - }); - } - - let mut updated = String::with_capacity(source.len() + witness.new_source.len()); - updated.push_str(&source[..start]); - updated.push_str(&witness.new_source); - updated.push_str(&source[end..]); - Ok(updated) -} - -pub fn apply_correction_and_reparse( - source: &str, - file: &str, - correction: &Correction, -) -> Result { - let updated = apply_correction(source, correction).map_err(CorrectionValidationError::Apply)?; - let tokens = - crate::tokenize::tokenize(&updated, file).map_err(CorrectionValidationError::Tokenize)?; - crate::parse::parse(&tokens, file).map_err(CorrectionValidationError::Parse)?; - Ok(updated) -} - -#[derive(Debug, Clone)] -pub enum Diagnostic { - TokenizerError { - message: String, - span: SourceSpan, - correction: Correction, - }, - ParseError { - message: String, - span: SourceSpan, - correction: Correction, - }, - TypeMismatch { - expected: TypeShape, - actual: TypeShape, - span: SourceSpan, - correction: Correction, - }, - ArityMismatch { - function: String, - expected: usize, - actual: usize, - span: SourceSpan, - correction: Correction, - }, - ResolveError { - name: String, - span: SourceSpan, - correction: Correction, - }, - UnitMismatch { - operator: String, - parameter: String, - expected: TypeShape, - actual: TypeShape, - span: SourceSpan, - correction: Correction, - }, - BranchConditionNotBool { - port: PortId, - actual_type: Option, - span: SourceSpan, - correction: Correction, - }, - MagnitudeOutOfRange { - literal: String, - target: String, - range_min_inclusive: String, - range_max_inclusive: String, - expected: TypeShape, - span: SourceSpan, - correction: Correction, - }, - MalformedIntegerRangeFact { - message: String, - span: SourceSpan, - correction: Correction, - }, - NominalOpacityViolation { - declaration: DeclarationId, - accessor: Option, - span: SourceSpan, - correction: Correction, - }, -} - -/// Layer-1 compiler diagnostic kind labels in **declaration order** of [`Diagnostic`]. -/// -/// This is the **only** source of label strings for [`Diagnostic::layer1_kind_label`], which -/// indexes this slice using an exhaustive per-variant index match (no wildcard). Crate tests assert -/// the declaration-order fixture matches those indices; integration tests ratchet -/// `CompilerDiagnosticKind` / `std.verification.DiagnosticKind` in the bootstrap DAG against this -/// slice. When adding a [`Diagnostic`] variant, extend this list, add an index arm next to -/// `layer1_kind_label`, and update the `.dag` closed sums — lens-instance kinds stay on the -/// Layer-2 path (Q6.5), not here. -pub const LAYER1_DIAGNOSTIC_KIND_LABELS: &[&str] = &[ - "TokenizerError", - "ParseError", - "TypeMismatch", - "ArityMismatch", - "ResolveError", - "UnitMismatch", - "BranchConditionNotBool", - "MagnitudeOutOfRange", - "MalformedIntegerRangeFact", - "NominalOpacityViolation", -]; - -impl Diagnostic { - /// Zero-based declaration index for Layer-1 kind strings (same order as [`Diagnostic`] and - /// [`LAYER1_DIAGNOSTIC_KIND_LABELS`]). - /// - /// This match is **exhaustive with no wildcard**: adding a [`Diagnostic`] variant is a compile - /// error until a new arm is authored here **and** [`LAYER1_DIAGNOSTIC_KIND_LABELS`] is extended - /// by one entry. [`Diagnostic::layer1_kind_label`] reads only from that slice, so the label - /// strings have a single authority. - fn layer1_declaration_index(&self) -> usize { - match self { - Diagnostic::TokenizerError { .. } => 0, - Diagnostic::ParseError { .. } => 1, - Diagnostic::TypeMismatch { .. } => 2, - Diagnostic::ArityMismatch { .. } => 3, - Diagnostic::ResolveError { .. } => 4, - Diagnostic::UnitMismatch { .. } => 5, - Diagnostic::BranchConditionNotBool { .. } => 6, - Diagnostic::MagnitudeOutOfRange { .. } => 7, - Diagnostic::MalformedIntegerRangeFact { .. } => 8, - Diagnostic::NominalOpacityViolation { .. } => 9, - } - } - - /// Stable Layer-1 kind name for substrate mirrors and test predicates (Q6.5). - /// - /// Labels come only from [`LAYER1_DIAGNOSTIC_KIND_LABELS`]; an exhaustive per-variant index - /// match selects the row so new enum variants cannot compile without extending the const slice - /// and substrate closed sums. - pub fn layer1_kind_label(&self) -> &'static str { - LAYER1_DIAGNOSTIC_KIND_LABELS[self.layer1_declaration_index()] - } - - pub fn span(&self) -> &SourceSpan { - match self { - Diagnostic::TokenizerError { span, .. } - | Diagnostic::ParseError { span, .. } - | Diagnostic::TypeMismatch { span, .. } - | Diagnostic::ArityMismatch { span, .. } - | Diagnostic::ResolveError { span, .. } - | Diagnostic::UnitMismatch { span, .. } - | Diagnostic::BranchConditionNotBool { span, .. } - | Diagnostic::MagnitudeOutOfRange { span, .. } - | Diagnostic::MalformedIntegerRangeFact { span, .. } - | Diagnostic::NominalOpacityViolation { span, .. } => span, - } - } - - pub fn correction(&self) -> &Correction { - match self { - Diagnostic::TokenizerError { correction, .. } - | Diagnostic::ParseError { correction, .. } - | Diagnostic::TypeMismatch { correction, .. } - | Diagnostic::ArityMismatch { correction, .. } - | Diagnostic::ResolveError { correction, .. } - | Diagnostic::UnitMismatch { correction, .. } - | Diagnostic::BranchConditionNotBool { correction, .. } - | Diagnostic::MagnitudeOutOfRange { correction, .. } - | Diagnostic::MalformedIntegerRangeFact { correction, .. } - | Diagnostic::NominalOpacityViolation { correction, .. } => correction, - } - } - - pub fn message(&self) -> String { - match self { - Diagnostic::TokenizerError { message, .. } | Diagnostic::ParseError { message, .. } => { - message.clone() - } - Diagnostic::TypeMismatch { - expected, actual, .. - } => format!("expected {expected:?}, got {actual:?}"), - Diagnostic::ArityMismatch { - function, - expected, - actual, - .. - } => format!("{function} expected {expected}, got {actual}"), - Diagnostic::ResolveError { name, .. } => name.clone(), - Diagnostic::UnitMismatch { - operator, - parameter, - expected, - actual, - .. - } => format!( - "unit mismatch for operator `{operator}` on phantom parameter `{parameter}`: abelian-group closure requires matching units (expected {expected:?}, got {actual:?})" - ), - Diagnostic::BranchConditionNotBool { actual_type, .. } => match actual_type { - Some(ty) => format!("branch condition port is not Bool (got {ty:?})"), - None => "branch condition port is not Bool (type not resolved)".to_string(), - }, - Diagnostic::MagnitudeOutOfRange { - literal, - target, - range_min_inclusive, - range_max_inclusive, - expected, - .. - } => format!( - "integer literal `{literal}` is out of range for `{target}` (expected {range_min_inclusive}..={range_max_inclusive}, declared type {expected:?}); use a wider target" - ), - Diagnostic::MalformedIntegerRangeFact { message, .. } => message.clone(), - Diagnostic::NominalOpacityViolation { - declaration, - accessor, - .. - } => match accessor { - Some(accessor) => format!( - "nominal-opaque declaration `{declaration:?}` may only be structurally accessed through permitted accessor `{accessor:?}`" - ), - None => format!( - "nominal-opaque declaration `{declaration:?}` cannot be structurally descended without a permitted accessor" - ), - }, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DiagnosticStyleTarget { - Rust, - Go, - Python, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum DiagnosticRenderError { - MissingLanguageSpec(&'static str), - MissingCleanEmissionContract(&'static str), - MalformedCleanEmissionContract { - declaration: DeclarationId, - detail: &'static str, - }, - MissingCorrectionStyle(&'static str), - MalformedCorrectionStyle { - declaration: DeclarationId, - detail: &'static str, - }, -} -#[derive(Debug, Clone, PartialEq, Eq)] -struct CorrectionStyleBinding { - indent_unit: String, - line_ending: String, - string_quote: String, - trailing_semicolon: bool, -} - -impl CorrectionStyleBinding { - fn build(dag: &Dag, declaration: DeclarationId) -> Result { - let fields = match dag.declaration(declaration).value_body.as_ref() { - Some(crate::dag::ValueBody::Structural { fields }) => fields, - _ => { - return Err(DiagnosticRenderError::MalformedCorrectionStyle { - declaration, - detail: "correction_style declaration must carry a Structural value_body", - }); - } - }; - Ok(Self { - indent_unit: require_string(fields, "indent_unit", declaration)?, - line_ending: require_string(fields, "line_ending", declaration)?, - string_quote: require_string(fields, "string_quote", declaration)?.replace("%Q", "\""), - trailing_semicolon: require_bool(fields, "trailing_semicolon", declaration)?, - }) - } -} - -pub fn render_diagnostic_for_target( - dag: &Dag, - target: DiagnosticStyleTarget, - diagnostic: &Diagnostic, -) -> Result { - let language_spec = language_spec_for_target(dag, target)?; - render_diagnostic_for_language_spec(dag, language_spec, diagnostic) -} - -pub fn render_diagnostic_for_language_spec( - dag: &Dag, - language_spec: DeclarationId, - diagnostic: &Diagnostic, -) -> Result { - let declaration = correction_style_for_language_spec(dag, language_spec)?; - let style = CorrectionStyleBinding::build(dag, declaration)?; - Ok(render_diagnostic_with_style(diagnostic, &style)) -} - -fn language_spec_for_target( - dag: &Dag, - target: DiagnosticStyleTarget, -) -> Result { - let (language_spec, missing_name) = match target { - DiagnosticStyleTarget::Rust => (dag.rust_language_spec(), "rust_language"), - DiagnosticStyleTarget::Go => (dag.go_language_spec(), "go_language"), - DiagnosticStyleTarget::Python => (dag.python_language_spec(), "python_language"), - }; - language_spec.ok_or(DiagnosticRenderError::MissingLanguageSpec(missing_name)) -} - -fn correction_style_for_language_spec( - dag: &Dag, - language_spec: DeclarationId, -) -> Result { - let clean_emission_decl = dag - .target_syntax_bundle_for_language(language_spec) - .map(|bundle| bundle.clean_emission_spec) - .ok_or(DiagnosticRenderError::MissingCleanEmissionContract( - "clean_emission.correction_style", - ))?; - let Some(ValueBody::Structural { fields }) = &dag.declaration(clean_emission_decl).value_body - else { - return Err(DiagnosticRenderError::MalformedCleanEmissionContract { - declaration: clean_emission_decl, - detail: "clean emission declaration must carry a structural value_body", - }); - }; - let value = fields - .iter() - .find(|(label, _)| label == "correction_style") - .map(|(_, value)| value) - .ok_or(DiagnosticRenderError::MissingCorrectionStyle( - "clean_emission.correction_style", - ))?; - match value { - FieldValue::Reference(declaration) => Ok(*declaration), - _ => Err(DiagnosticRenderError::MalformedCleanEmissionContract { - declaration: clean_emission_decl, - detail: "clean emission correction_style field must be a declaration reference", - }), - } -} - -fn render_diagnostic_with_style(diagnostic: &Diagnostic, style: &CorrectionStyleBinding) -> String { - let mut lines = vec![format!( - "ERROR at {}:{}-{}: {}", - diagnostic.span().file, - diagnostic.span().byte_start, - diagnostic.span().byte_end, - diagnostic.message() - )]; - if let Correction::LiveCorrection { .. } = diagnostic.correction() { - lines.push(format!("FIX: {}", diagnostic.correction().description())); - lines.push(render_correction_source(diagnostic.correction(), style)); - } else { - lines.push(format!( - "DEFERRED CORRECTION: {}", - diagnostic.correction().description() - )); - } - lines.join(&style.line_ending) -} - -fn render_correction_source(fix: &Correction, style: &CorrectionStyleBinding) -> String { - let Ok(witness) = fix.live_witness() else { - return String::new(); - }; - let suffix = if style.trailing_semicolon { ";" } else { "" }; - let normalized = witness.new_source.replace("\r\n", "\n").replace('\r', "\n"); - if !normalized.contains('\n') { - return format!( - "{}{}{}{}{}", - style.indent_unit, - style.string_quote, - escape_for_string_literal(&normalized, &style.string_quote), - style.string_quote, - suffix - ); - } - - let body = normalized - .split('\n') - .map(|line| { - format!( - "{}{}", - style.indent_unit, - escape_for_string_literal(line, &style.string_quote) - ) - }) - .collect::>() - .join(&style.line_ending); - format!( - "{}{}{}{}", - style.string_quote, style.line_ending, body, style.string_quote - ) + suffix -} - -fn escape_for_string_literal(source: &str, string_quote: &str) -> String { - let escaped = source.replace('\\', "\\\\"); - if string_quote.is_empty() { - return escaped; - } - escaped.replace(string_quote, &format!("\\{string_quote}")) -} - -fn require_string( - fields: &[(String, FieldValue)], - label: &'static str, - declaration: DeclarationId, -) -> Result { - let value = fields - .iter() - .find(|(field_label, _)| field_label == label) - .map(|(_, value)| value) - .ok_or(DiagnosticRenderError::MalformedCorrectionStyle { - declaration, - detail: "correction_style is missing a required String field", - })?; - match value { - FieldValue::Literal(crate::dag::LiteralBits::String(value)) => Ok(value.clone()), - _ => Err(DiagnosticRenderError::MalformedCorrectionStyle { - declaration, - detail: "correction_style String field must be a string literal", - }), - } -} - -fn require_bool( - fields: &[(String, FieldValue)], - label: &'static str, - declaration: DeclarationId, -) -> Result { - let value = fields - .iter() - .find(|(field_label, _)| field_label == label) - .map(|(_, value)| value) - .ok_or(DiagnosticRenderError::MalformedCorrectionStyle { - declaration, - detail: "correction_style is missing a required Bool field", - })?; - match value { - FieldValue::Literal(crate::dag::LiteralBits::Bool(value)) => Ok(*value), - _ => Err(DiagnosticRenderError::MalformedCorrectionStyle { - declaration, - detail: "correction_style Bool field must be a bool literal", - }), - } -} - -pub(crate) fn declaration_display_name(dag: &Dag, declaration: DeclarationId) -> String { - dag.declaration(declaration) - .name - .clone() - .unwrap_or_else(|| format!("declaration#{}", declaration.raw())) -} - -pub(crate) fn example_source_for_decl(dag: &Dag, declaration: DeclarationId) -> Option { - example_source_for_decl_inner(dag, declaration, 0) -} - -fn example_source_for_decl_inner( - dag: &Dag, - declaration: DeclarationId, - depth: usize, -) -> Option { - if depth >= 8 { - return None; - } - if dag.declaration(declaration).refinement.is_some() { - return None; - } - if let Some(example) = builtin_example_source(dag, declaration, depth) { - return Some(example); - } - let decl = dag.declaration(declaration); - match &decl.connective { - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - example_source_for_decl_inner(dag, *next, depth + 1) - } - TypeConnective::Instantiation { template, .. } => { - example_source_for_decl_inner(dag, *template, depth + 1) - } - TypeConnective::Conj { children } => { - let fields: Option> = children - .iter() - .map(|field| { - Some(format!( - "{}: {}", - field.label, - example_source_for_decl_inner(dag, field.ty, depth + 1)? - )) - }) - .collect(); - Some(format!("{{ {} }}", fields?.join(", "))) - } - TypeConnective::Disj { variants } => variants - .iter() - .find_map(|variant| render_variant_witness(dag, variant, depth + 1)), - TypeConnective::Cardinality(p) if p.bound() == CardinalityBound::AtMostOne => { - Some("None".to_string()) - } - _ => None, - } -} - -fn builtin_example_source(dag: &Dag, declaration: DeclarationId, depth: usize) -> Option { - if dag.int_shape().is_some_and(|shape| { - decl_matches_example_identity(dag, declaration, shape.declaration, depth) - }) { - return Some("1".to_string()); - } - if dag.bool_shape().is_some_and(|shape| { - decl_matches_example_identity(dag, declaration, shape.declaration, depth) - }) { - return Some("true".to_string()); - } - if dag.string_shape().is_some_and(|shape| { - decl_matches_example_identity(dag, declaration, shape.declaration, depth) - }) { - return Some("\"x\"".to_string()); - } - if dag.list_template().is_some_and(|list_template| { - decl_matches_example_identity(dag, declaration, list_template, depth) - }) { - return Some("[]".to_string()); - } - None -} - -fn canonical_decl_for_example( - dag: &Dag, - declaration: DeclarationId, - depth: usize, -) -> Option { - if depth >= 8 { - return None; - } - match &dag.declaration(declaration).connective { - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - canonical_decl_for_example(dag, *next, depth + 1) - } - TypeConnective::Instantiation { template, .. } => { - canonical_decl_for_example(dag, *template, depth + 1) - } - _ => Some(declaration), - } -} - -fn decl_matches_example_identity( - dag: &Dag, - actual: DeclarationId, - expected: DeclarationId, - depth: usize, -) -> bool { - canonical_decl_for_example(dag, actual, depth) - == canonical_decl_for_example(dag, expected, depth) -} - -fn render_variant_witness(dag: &Dag, variant: &Field, depth: usize) -> Option { - if dag.declaration(variant.ty).refinement.is_some() { - return None; - } - let TypeConnective::Conj { children } = &dag.declaration(variant.ty).connective else { - return None; - }; - if children.is_empty() { - return Some(variant.label.clone()); - } - // Constructor invocation in the current `.dag` surface is - // positional (`Variant(arg0, arg1)`), even when the payload - // declaration's fields are named. Preserve declaration-derived - // child order here, but do not invent an unsupported - // `Variant { field: value }` surface until lowering accepts that - // shape structurally. - let payload: Option> = children - .iter() - .map(|field| example_source_for_decl_inner(dag, field.ty, depth + 1)) - .collect(); - Some(format!("{}({})", variant.label, payload?.join(", "))) -} - -pub(crate) fn witness_correction_for_decl( - dag: &Dag, - declaration: DeclarationId, - span: SourceSpan, - description: impl Into, -) -> Option { - if dag.declaration(declaration).refinement.is_some() { - return None; - } - Some(Correction::live( - description, - span, - example_source_for_decl(dag, declaration)?, - )) -} - -/// Opaque, validated witness to a path in the substrate -/// `bootstrap_authority` set declared by -/// `src/v3/std/bootstrap_authority.dag`. Held internally as a -/// `&'static str` because the only minters are the bootstrap loader -/// (`bootstrap_regen_fresh::parse_fixture`) and bootstrap kernel-patch -/// helpers (`bootstrap::patch_kernel_bool_boolean_algebra_inhabits`), -/// both of which iterate the `&'static`-keyed authority arrays -/// produced by `build.rs` (`STAGED_FILES`/`V3_SPECS`/`COMPILER_FILES`/ -/// `EXTDEPS_FILES`) — derivations of the same substrate -/// `bootstrap_authority` set. -/// -/// The constructor is deliberately `pub(crate)`. Consumers receive -/// minted keys through [`DiagnosticAttribution::BootstrapAuthority`] -/// and dispatch on witness identity (equality / hash). They MUST NOT -/// rebuild a `BootstrapAuthorityKey` from a `SourceSpan.file` string -/// to recover attribution — that would defeat the witness contract. -/// `path()` is exposed for diagnostic display only. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct BootstrapAuthorityKey(&'static str); - -impl BootstrapAuthorityKey { - /// Mint a key from a `&'static str` known to be a row in the - /// substrate `bootstrap_authority` set. Bootstrap-only constructor - /// — see type-level doc for the witness contract. - pub(crate) fn new(path: &'static str) -> Self { - Self(path) - } - - /// Typed witness for the kernel-`Bool` `inhabits` patch authority - /// (`dsl/std/types.dag` row of the substrate `bootstrap_authority` - /// set, classified `StdAuthority`). Encapsulates the path-string so - /// `bootstrap.rs` consumers do not name `dsl/std/types.dag` directly - /// — audit-row #2 retirement (bootstrap.rs slice 1 of 2). Reviewers - /// must treat new `for_*` accessors as additions to the witness - /// surface; each one represents a hand-Rust authority site. - pub(crate) fn for_kernel_bool() -> Self { - Self::new("dsl/std/types.dag") - } - - /// Typed witness for the pipeline-authority row - /// (`src/v3/compiler/pipeline.dag` of the substrate - /// `bootstrap_authority` set, classified `CompilerAuthority`). - /// Encapsulates the path-string so `bootstrap.rs` consumers do not - /// name `src/v3/compiler/pipeline.dag` directly — audit-row #6 - /// retirement (bootstrap.rs slice 1 of 2). - pub(crate) fn for_pipeline_authority() -> Self { - Self::new("src/v3/compiler/pipeline.dag") - } - - /// Canonical bootstrap-authority path. **Display only.** Consumers - /// dispatching on attribution must use witness equality, not this - /// string, to decide bootstrap membership; a string compare against - /// `SourceSpan.file` is precisely the path-string bridge this type - /// dissolves. - /// - /// **Egress-only contract.** This accessor is intentionally a - /// one-way door: a consumer may read the path for diagnostic - /// rendering, but the only way *back* to a `BootstrapAuthorityKey` - /// witness is the `pub(crate)` [`Self::new`] constructor that the - /// bootstrap loader owns. Round-tripping `path()` through some - /// other public constructor is a contract-violation that the type - /// system cannot block on its own; reviewers should treat any new - /// `pub fn from_path` / `From<&str>` impl on this type as the same - /// path-string bridge re-introduced and reject it. - pub fn path(&self) -> &'static str { - self.0 - } -} - -/// Where a diagnostic originated in the bootstrap-vs-user dimension. -/// -/// `Unattributed` is the default for diagnostics raised against -/// user-compile sources or against not-yet-classified internal -/// failures. `BootstrapAuthority` carries an opaque witness that the -/// diagnostic was attached while loading or patching one of the -/// substrate `bootstrap_authority` rows. Verification consumers (e.g. -/// the row-82 `diagnostics_empty_after_bootstrap` ratchet) dispatch on -/// this discriminant rather than scanning `Diagnostic.span().file`. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum DiagnosticAttribution { - Unattributed, - BootstrapAuthority(BootstrapAuthorityKey), -} - -impl DiagnosticAttribution { - /// `true` iff this diagnostic was attached against a known - /// bootstrap-authority row. Verification surfaces should prefer - /// this over `span.file == "src/v3/std/..."` predicates. - pub fn is_bootstrap(&self) -> bool { - matches!(self, Self::BootstrapAuthority(_)) - } - - /// Borrow the bootstrap-authority witness, when present. - pub fn as_bootstrap_authority(&self) -> Option<&BootstrapAuthorityKey> { - match self { - Self::BootstrapAuthority(key) => Some(key), - Self::Unattributed => None, - } - } -} - -#[derive(Debug, Default, Clone)] -pub struct DiagnosticTable { - entries: HashMap, -} - -impl DiagnosticTable { - pub fn new() -> Self { - Self { - entries: HashMap::new(), - } - } - - pub fn is_empty(&self) -> bool { - self.entries.is_empty() - } - - pub fn contains(&self, port: PortId) -> bool { - self.entries.contains_key(&port) - } - - pub fn get(&self, port: PortId) -> Option<&Diagnostic> { - self.entries.get(&port).map(|(d, _)| d) - } - - /// Borrow the structural attribution recorded alongside a - /// diagnostic. `Some(Unattributed)` for ordinary user-compile - /// failures; `Some(BootstrapAuthority(_))` for diagnostics attached - /// while loading or patching a substrate bootstrap-authority row. - pub fn attribution(&self, port: PortId) -> Option<&DiagnosticAttribution> { - self.entries.get(&port).map(|(_, a)| a) - } - - pub fn len(&self) -> usize { - self.entries.len() - } - - /// Iterate `(port, diagnostic)` pairs. Used by tests and callers - /// that need to scan all diagnostics for a specific kind. For - /// attribution-aware iteration use [`Self::iter_attributed`]. - pub fn iter(&self) -> impl Iterator { - self.entries.iter().map(|(p, (d, _))| (*p, d)) - } - - /// Iterate `(port, diagnostic, attribution)` triples. Verification - /// consumers that need to count or filter by bootstrap attribution - /// without touching `SourceSpan.file` use this surface. - pub fn iter_attributed( - &self, - ) -> impl Iterator { - self.entries.iter().map(|(p, (d, a))| (*p, d, a)) - } - - /// Insert a diagnostic entry for a port. pub(crate) because the - /// only callers are Dag::mark_unresolved* (which atomically also - /// clears the port's value_type) and should never be called - /// independently from outside the crate. - pub(crate) fn insert( - &mut self, - port: PortId, - diagnostic: Diagnostic, - attribution: DiagnosticAttribution, - ) { - self.entries.insert(port, (diagnostic, attribution)); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::types::TypeShape; - - fn layer1_diagnostic_examples_in_declaration_order() -> Vec { - let span = SourceSpan::new("layer1_kind_label_ratchets.v3", 0, 0); - let ty = TypeShape::new(DeclarationId::test_raw(0)); - vec![ - Diagnostic::TokenizerError { - message: String::new(), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("TokenizerError"), - }, - Diagnostic::ParseError { - message: String::new(), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("ParseError"), - }, - Diagnostic::TypeMismatch { - expected: ty, - actual: ty, - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("TypeMismatch"), - }, - Diagnostic::ArityMismatch { - function: String::new(), - expected: 0, - actual: 0, - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("ArityMismatch"), - }, - Diagnostic::ResolveError { - name: String::new(), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("ResolveError"), - }, - Diagnostic::UnitMismatch { - operator: String::new(), - parameter: String::new(), - expected: ty, - actual: ty, - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("UnitMismatch"), - }, - Diagnostic::BranchConditionNotBool { - port: PortId::test_raw(0), - actual_type: None, - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("BranchConditionNotBool"), - }, - Diagnostic::MagnitudeOutOfRange { - literal: String::new(), - target: String::new(), - range_min_inclusive: String::new(), - range_max_inclusive: String::new(), - expected: ty, - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("MagnitudeOutOfRange"), - }, - Diagnostic::MalformedIntegerRangeFact { - message: String::new(), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("MalformedIntegerRangeFact"), - }, - Diagnostic::NominalOpacityViolation { - declaration: DeclarationId::test_raw(0), - accessor: None, - span, - correction: Correction::deferred_for_diagnostic_class("NominalOpacityViolation"), - }, - ] - } - - #[test] - fn layer1_ordered_labels_match_layer1_kind_label() { - let examples = layer1_diagnostic_examples_in_declaration_order(); - assert_eq!( - examples.len(), - LAYER1_DIAGNOSTIC_KIND_LABELS.len(), - "declaration-order fixture must list every Diagnostic variant exactly once" - ); - let mut seen = std::collections::HashSet::new(); - for (i, diag) in examples.iter().enumerate() { - let idx = diag.layer1_declaration_index(); - assert_eq!( - idx, i, - "fixture must stay in declaration order and match layer1_declaration_index()" - ); - let label = diag.layer1_kind_label(); - assert_eq!(label, LAYER1_DIAGNOSTIC_KIND_LABELS[i]); - assert!(seen.insert(label), "duplicate layer1 label: {label}"); - } - } - - #[test] - fn apply_correction_replaces_the_requested_span() { - let updated = apply_correction( - "let x: Int = 1\n", - &Correction::live( - "replace the literal", - SourceSpan::new("apply_fix.v3", 13, 14), - "2", - ), - ) - .expect("correction should apply"); - assert_eq!(updated, "let x: Int = 2\n"); - } - - #[test] - fn apply_correction_uses_span_offsets_not_file_participation() { - let updated = apply_correction( - "let x: Int = 1\n", - &Correction::live( - "replace the literal", - SourceSpan::new("expected.v3", 13, 14), - "2", - ), - ) - .expect("correction application is offset-bounded, not gated by a separate file string"); - assert_eq!(updated, "let x: Int = 2\n"); - } - - #[test] - fn apply_correction_rejects_non_char_boundaries() { - let source = "let x = \"é\"\n"; - let accent = source.find('é').expect("fixture contains accent") as u32; - let error = apply_correction( - source, - &Correction::live( - "split the accent", - SourceSpan::new("utf8_fix.v3", accent, accent + 1), - "e", - ), - ) - .expect_err("mid-codepoint correction must fail closed"); - assert_eq!( - error, - CorrectionApplyError::NonCharBoundary { offset: accent + 1 } - ); - } - - #[test] - fn apply_correction_and_reparse_surfaces_parse_failures() { - let error = apply_correction_and_reparse( - "let x: Int = 1\n", - "broken_fix.v3", - &Correction::live( - "make the expression malformed", - SourceSpan::new("broken_fix.v3", 13, 14), - ")", - ), - ) - .expect_err("malformed correction should fail closed"); - assert!( - matches!(error, CorrectionValidationError::Parse(_)), - "expected parse failure carrier, got {error:?}" - ); - } - - #[test] - fn clean_emission_contracts_reference_named_correction_styles() { - let dag = Dag::new(); - let assert_reference = - |clean_decl: DeclarationId, expected_style_name: &str, expected_name: &str| { - let fields = match dag - .declaration(clean_decl) - .value_body - .as_ref() - .expect("clean emission carries value_body") - { - crate::dag::ValueBody::Structural { fields } => fields, - other => panic!("clean emission must be structural, got {other:?}"), - }; - let value = fields - .iter() - .find(|(label, _)| label == "correction_style") - .map(|(_, value)| value) - .expect("correction_style field exists"); - match value { - FieldValue::Reference(actual) => assert_eq!( - dag.declaration(*actual).name.as_deref(), - Some(expected_style_name), - "{expected_name} should point at its named correction style" - ), - other => panic!("correction_style must be a Reference, got {other:?}"), - } - }; - assert_reference( - dag.rust_clean_emission_spec().expect("rust clean emission"), - "rust_correction_style", - "rust_clean_emission", - ); - assert_reference( - dag.go_clean_emission_spec().expect("go clean emission"), - "go_correction_style", - "go_clean_emission", - ); - assert_reference( - dag.python_clean_emission_spec() - .expect("python clean emission"), - "python_correction_style", - "python_clean_emission", - ); - } - - #[test] - fn target_syntax_bundles_pair_each_language_with_its_clean_emission_contract() { - let dag = Dag::new(); - let assert_bundle = |language_spec: DeclarationId, - expected_clean_emission: DeclarationId| { - let bundle = dag - .target_syntax_bundle_for_language(language_spec) - .expect("bundle should exist for cached language spec"); - assert_eq!(bundle.language_spec, language_spec); - assert_eq!(bundle.clean_emission_spec, expected_clean_emission); - }; - assert_bundle( - dag.rust_language_spec().expect("rust language"), - dag.rust_clean_emission_spec().expect("rust clean emission"), - ); - assert_bundle( - dag.go_language_spec().expect("go language"), - dag.go_clean_emission_spec().expect("go clean emission"), - ); - assert_bundle( - dag.python_language_spec().expect("python language"), - dag.python_clean_emission_spec() - .expect("python clean emission"), - ); - } - - #[test] - fn example_source_for_decl_uses_cached_structural_identities_not_std_names() { - let mut dag = Dag::new(); - let int_decl = dag.int_shape().expect("int shape").declaration; - let bool_decl = dag.bool_shape().expect("bool shape").declaration; - let string_decl = dag.string_shape().expect("string shape").declaration; - let list_decl = dag.list_template().expect("list template"); - - dag.declaration_mut(int_decl).name = Some("WholeNumber".to_string()); - dag.declaration_mut(bool_decl).name = Some("TruthValue".to_string()); - dag.declaration_mut(string_decl).name = Some("Text".to_string()); - dag.declaration_mut(list_decl).name = Some("Sequence".to_string()); - - let list_instantiation = dag.alloc_declaration_id(); - dag.push_declaration(crate::dag::Declaration { - id: list_instantiation, - name: Some("SequenceOfInt".to_string()), - connective: TypeConnective::Instantiation { - template: list_decl, - arguments: Vec::new(), - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("diagnostics_test.v3", 0, 1), - }); - - assert_eq!( - example_source_for_decl(&dag, int_decl).as_deref(), - Some("1") - ); - assert_eq!( - example_source_for_decl(&dag, bool_decl).as_deref(), - Some("true") - ); - assert_eq!( - example_source_for_decl(&dag, string_decl).as_deref(), - Some("\"x\"") - ); - assert_eq!( - example_source_for_decl(&dag, list_decl).as_deref(), - Some("[]") - ); - assert_eq!( - example_source_for_decl(&dag, list_instantiation).as_deref(), - Some("[]") - ); - } - - #[test] - fn example_source_for_decl_fails_closed_on_refined_record_children() { - let mut dag = Dag::new(); - let int_decl = dag.int_shape().expect("int shape").declaration; - let bool_decl = dag.bool_shape().expect("bool shape").declaration; - - let refined_child = dag.alloc_declaration_id(); - dag.push_declaration(crate::dag::Declaration { - id: refined_child, - name: Some("BigInt".to_string()), - connective: TypeConnective::Atom(AtomPayload::ResolvedByStructure(int_decl)), - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: Some(bool_decl), - nominal_opacity: None, - span: SourceSpan::new("diagnostics_test.v3", 0, 1), - }); - - let record_decl = dag.alloc_declaration_id(); - dag.push_declaration(crate::dag::Declaration { - id: record_decl, - name: Some("NeedsBig".to_string()), - connective: TypeConnective::Conj { - children: vec![Field { - label: "value".to_string(), - ty: refined_child, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("diagnostics_test.v3", 0, 1), - }); - - assert_eq!(example_source_for_decl(&dag, record_decl), None); - } - - #[test] - fn example_source_for_decl_fails_closed_on_refined_sum_payloads() { - let mut dag = Dag::new(); - let int_decl = dag.int_shape().expect("int shape").declaration; - let bool_decl = dag.bool_shape().expect("bool shape").declaration; - - let refined_child = dag.alloc_declaration_id(); - dag.push_declaration(crate::dag::Declaration { - id: refined_child, - name: Some("BigInt".to_string()), - connective: TypeConnective::Atom(AtomPayload::ResolvedByStructure(int_decl)), - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: Some(bool_decl), - nominal_opacity: None, - span: SourceSpan::new("diagnostics_test.v3", 0, 1), - }); - - let payload_decl = dag.alloc_declaration_id(); - dag.push_declaration(crate::dag::Declaration { - id: payload_decl, - name: None, - connective: TypeConnective::Conj { - children: vec![Field { - label: "value".to_string(), - ty: refined_child, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("diagnostics_test.v3", 0, 1), - }); - - let sum_decl = dag.alloc_declaration_id(); - dag.push_declaration(crate::dag::Declaration { - id: sum_decl, - name: Some("MaybeBig".to_string()), - connective: TypeConnective::Disj { - variants: vec![Field { - label: "Some".to_string(), - ty: payload_decl, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("diagnostics_test.v3", 0, 1), - }); - - assert_eq!(example_source_for_decl(&dag, sum_decl), None); - } - - /// Regression: happy-path payloaded disjunct. Fills the coverage gap - /// the PR #554 loop-health meta-review called out — the existing - /// witness tests cover unpayloaded primitives and fail-closed refined - /// payloads, but no test pinned "Disj with a positional-payload variant - /// renders as `Variant(witness_arg0, witness_arg1, …)`". The walk goes - /// through `render_variant_witness` → `example_source_for_decl_inner` - /// on each payload field, so the assertion exercises the full - /// Disj → Conj → primitive recursion path structurally. - #[test] - fn example_source_for_decl_renders_positional_payload_variant() { - let mut dag = Dag::new(); - let int_decl = dag.int_shape().expect("int shape").declaration; - let bool_decl = dag.bool_shape().expect("bool shape").declaration; - - // Positional payload: fields `_0: Int`, `_1: Bool` — mirrors the - // shape lowering produces for `Cons(Int, Bool)` before any named- - // field surface grammar lands. - let payload_decl = dag.alloc_declaration_id(); - dag.push_declaration(crate::dag::Declaration { - id: payload_decl, - name: None, - connective: TypeConnective::Conj { - children: vec![ - Field { - label: "_0".to_string(), - ty: int_decl, - }, - Field { - label: "_1".to_string(), - ty: bool_decl, - }, - ], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("diagnostics_test.v3", 0, 1), - }); - - let sum_decl = dag.alloc_declaration_id(); - dag.push_declaration(crate::dag::Declaration { - id: sum_decl, - name: Some("Pair".to_string()), - connective: TypeConnective::Disj { - variants: vec![Field { - label: "Paired".to_string(), - ty: payload_decl, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("diagnostics_test.v3", 0, 1), - }); - - assert_eq!( - example_source_for_decl(&dag, sum_decl).as_deref(), - Some("Paired(1, true)"), - "payloaded variant witness must recurse structurally through each payload field" - ); - } - - #[test] - fn render_rust_diagnostic_uses_rust_correction_style() { - let dag = Dag::new(); - let rendered = render_diagnostic_for_target( - &dag, - DiagnosticStyleTarget::Rust, - &Diagnostic::ResolveError { - name: "field `c` does not exist on `Point`".to_string(), - span: SourceSpan::new("field.v3", 12, 19), - correction: Correction::live( - "did you mean `point.a`?", - SourceSpan::new("field.v3", 12, 19), - "point.a", - ), - }, - ) - .expect("render"); - assert!(rendered.contains("FIX: did you mean `point.a`?")); - assert!(rendered.contains("\n \"point.a\";")); - } - - #[test] - fn render_deferred_correction_does_not_emit_fix_header() { - let dag = Dag::new(); - let rendered = render_diagnostic_for_target( - &dag, - DiagnosticStyleTarget::Rust, - &Diagnostic::ParseError { - message: "not yet live-correctable".to_string(), - span: SourceSpan::new("deferred.v3", 0, 1), - correction: Correction::deferred_for_diagnostic_class("ParseError"), - }, - ) - .expect("render"); - assert!(rendered.contains("ERROR at deferred.v3:0-1")); - assert!( - !rendered.contains("FIX (option"), - "deferred corrections are explicit residual carriers, not user-facing fixes: {rendered}" - ); - assert!( - rendered.contains("DEFERRED CORRECTION: ParseError does not yet compute"), - "deferred corrections should remain visible without rendering as applyable fixes: {rendered}" - ); - } - - #[test] - fn render_go_diagnostic_uses_go_correction_style() { - let dag = Dag::new(); - let rendered = render_diagnostic_for_target( - &dag, - DiagnosticStyleTarget::Go, - &Diagnostic::ResolveError { - name: "field `c` does not exist on `Point`".to_string(), - span: SourceSpan::new("field.v3", 12, 19), - correction: Correction::live( - "did you mean `point.a`?", - SourceSpan::new("field.v3", 12, 19), - "point.a", - ), - }, - ) - .expect("render"); - assert!(rendered.contains("FIX: did you mean `point.a`?")); - assert!(rendered.contains("\n\t\"point.a\"")); - assert!(!rendered.contains("\n\t\"point.a\";")); - } - - #[test] - fn render_python_diagnostic_uses_python_correction_style() { - let dag = Dag::new(); - let rendered = render_diagnostic_for_target( - &dag, - DiagnosticStyleTarget::Python, - &Diagnostic::ResolveError { - name: "field `c` does not exist on `Point`".to_string(), - span: SourceSpan::new("field.v3", 12, 19), - correction: Correction::live( - "did you mean `point.a`?", - SourceSpan::new("field.v3", 12, 19), - "point.a", - ), - }, - ) - .expect("render"); - assert!(rendered.contains("FIX: did you mean `point.a`?")); - assert!(rendered.contains("\n \"point.a\"")); - assert!(!rendered.contains("\n \"point.a\";")); - } - - #[test] - fn render_rust_diagnostic_escapes_quotes_and_backslashes_in_fix_source() { - let dag = Dag::new(); - let rendered = render_diagnostic_for_target( - &dag, - DiagnosticStyleTarget::Rust, - &Diagnostic::ResolveError { - name: "field `path` does not exist on `Config`".to_string(), - span: SourceSpan::new("field.v3", 12, 19), - correction: Correction::live( - "did you mean `config.path`?", - SourceSpan::new("field.v3", 12, 19), - "config[\"path\\\\name\"]", - ), - }, - ) - .expect("render"); - assert!(rendered.contains("\n \"config[\\\"path\\\\\\\\name\\\"]\";")); - } - - #[test] - fn render_multiline_fix_escapes_quotes_and_backslashes_in_each_line() { - let dag = Dag::new(); - let rendered = render_diagnostic_for_target( - &dag, - DiagnosticStyleTarget::Python, - &Diagnostic::ResolveError { - name: "field `path` does not exist on `Config`".to_string(), - span: SourceSpan::new("field.v3", 12, 19), - correction: Correction::live( - "did you mean `config.path`?", - SourceSpan::new("field.v3", 12, 19), - "config[\n\"path\\\\name\"\n]", - ), - }, - ) - .expect("render"); - assert!(rendered.contains("\"\n config[\n \\\"path\\\\\\\\name\\\"\n ]\"")); - } - - #[test] - fn diagnostic_attribution_default_is_unattributed_and_distinguishes_bootstrap() { - let key_a = BootstrapAuthorityKey::new("src/v3/std/induction.dag"); - let key_b = BootstrapAuthorityKey::new("dsl/std/types.dag"); - - let unattributed = DiagnosticAttribution::Unattributed; - let bootstrap_a = DiagnosticAttribution::BootstrapAuthority(key_a.clone()); - let bootstrap_a_again = DiagnosticAttribution::BootstrapAuthority(key_a.clone()); - let bootstrap_b = DiagnosticAttribution::BootstrapAuthority(key_b.clone()); - - assert!(!unattributed.is_bootstrap()); - assert_eq!(unattributed.as_bootstrap_authority(), None); - - assert!(bootstrap_a.is_bootstrap()); - assert_eq!(bootstrap_a.as_bootstrap_authority(), Some(&key_a)); - - // Witness identity (==) is the consumer dispatch path — no - // `span.file` string compare. - assert_eq!(bootstrap_a, bootstrap_a_again); - assert_ne!(bootstrap_a, bootstrap_b); - assert_ne!(bootstrap_a, unattributed); - } - - #[test] - fn diagnostic_table_round_trips_bootstrap_attribution_per_port() { - // Drive through the public dag attach surface so we exercise - // the same biconditional (`Unresolved port iff diagnostic`) as - // production. `attach_diagnostic` is `pub(crate)`, so this - // test stays in-crate. - let mut dag = Dag::empty(); - let key = BootstrapAuthorityKey::new("src/v3/std/python_method_template_contracts.dag"); - let span = SourceSpan::new("src/v3/std/python_method_template_contracts.dag", 0, 0); - - dag.attach_diagnostic(Diagnostic::ResolveError { - name: "user".to_string(), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("ResolveError"), - }); - dag.attach_bootstrap_diagnostic( - key.clone(), - Diagnostic::ResolveError { - name: "bootstrap".to_string(), - span, - correction: Correction::deferred_for_diagnostic_class("ResolveError"), - }, - ); - - let mut unattr_seen = 0usize; - let mut bootstrap_seen = 0usize; - for (_port, diag, attribution) in dag.diagnostics().iter_attributed() { - let Diagnostic::ResolveError { name, .. } = diag else { - panic!("unexpected diagnostic kind: {diag:?}"); - }; - match (name.as_str(), attribution) { - ("user", DiagnosticAttribution::Unattributed) => unattr_seen += 1, - ("bootstrap", DiagnosticAttribution::BootstrapAuthority(k)) => { - assert_eq!(k, &key); - bootstrap_seen += 1; - } - other => panic!("unexpected (name, attribution) pair: {other:?}"), - } - } - assert_eq!(unattr_seen, 1); - assert_eq!(bootstrap_seen, 1); - - let bootstrap_count = dag - .diagnostics() - .iter_attributed() - .filter(|(_, _, a)| a.is_bootstrap()) - .count(); - assert_eq!(bootstrap_count, 1); - } -} diff --git a/tools/pb0_cycle4_emit_templates/dimension.rs.in b/tools/pb0_cycle4_emit_templates/dimension.rs.in deleted file mode 100644 index 28427c626c4..00000000000 --- a/tools/pb0_cycle4_emit_templates/dimension.rs.in +++ /dev/null @@ -1,539 +0,0 @@ -//! DB-3 dimension analysis — Lane 2 Stage 2f. -//! -//! Type authority: `src/v3/std/dimensions.dag`, spine order: -//! [`behavior_spine_in_node_order`] (`src/v3/std/workflows.dag`). -//! -//! Symbolic-cost dimension is the first migrated lens: it reuses -//! [`crate::lens_cost_symbolic::symbolic_cost_of`] for per-Behavior -//! witnesses and reports the composed carrier at `workflow_root`'s result -//! port (same query the cost lens answers today). -//! -//! Witnesses are collected only for behaviors **reachable backward** from -//! `workflow_root`'s result port (plus `workflow_root` itself). The symbolic -//! cost lens fold only resolves costs along producer chains in `Dag.nodes` -//! construction order; bootstrap declarations outside the active workflow -//! slice routinely carry `Lookup::Miss` on their result ports even though the -//! user program is well-costed — whole-DAG iteration would false-fail. - -use std::collections::HashSet; - -use crate::dag::{Behavior, Dag, NodeId, PortId, SymbolicCost}; -use crate::diagnostics::{Correction, Diagnostic, SourceSpan}; -use crate::lens_cost_symbolic::{symbolic_cost_of, SymbolicCostLookup}; - -fn behavior_result_port(b: &Behavior) -> PortId { - match b { - Behavior::Value(v) => v.result_port(), - Behavior::Transform(t) => t.result_port(), - Behavior::Branch(br) => br.result_port(), - Behavior::Loop(l) => l.result_port(), - Behavior::Bind(bind) => bind.result_port(), - } -} - -fn behavior_span(at: &Behavior) -> SourceSpan { - match at { - Behavior::Value(v) => v.span.clone(), - Behavior::Transform(t) => t.span.clone(), - Behavior::Branch(b) => b.span.clone(), - Behavior::Loop(l) => l.span.clone(), - Behavior::Bind(bind) => bind.span.clone(), - } -} - -/// Evidence partition — mirrors `Witness` in `std/dimensions.dag`. -#[derive(Debug, Clone)] -pub enum Witness { - Inhabits(C), - Violates { reason: String, at: Behavior }, -} - -/// Report carrier — mirrors `DimensionReport` in `std/dimensions.dag`. -/// -/// 🟢 TERMINAL (aggregate from DB-3 dimension evaluation; see `dimensions.dag`). -/// Pass/fail partition: success carries `composed`; failure carries `violations` -/// and must not fabricate a carrier when witnesses violate or root composition -/// misses (R2 fail-closed). -#[derive(Debug, Clone)] -pub enum DimensionReport { - DimensionOk { - dimension_name: String, - composed: C, - witnesses: Vec>, - }, - DimensionFail { - dimension_name: String, - violations: Vec, - witnesses: Vec>, - }, -} - -/// Same `Dag.nodes` order as `lenses/cost.dag::compute_symbolic_costs` / -/// `std/workflows.dag::behavior_spine`. -pub fn behavior_spine_in_node_order(d: &Dag) -> &[Behavior] { - d.nodes() -} - -/// Behaviors on the backward dataflow slice from `workflow_root`'s result -/// port, including `workflow_root` itself (the bind / root may not appear as -/// the `produced_by` target of its own result port). -fn workflow_reachable_behavior_ids(d: &Dag, workflow_root: NodeId) -> HashSet { - let mut visited_nodes = HashSet::new(); - let mut visited_ports: HashSet = HashSet::new(); - let mut frontier: Vec = Vec::new(); - - let root_behavior = d.node(workflow_root); - frontier.push(behavior_result_port(root_behavior)); - - while let Some(port) = frontier.pop() { - if !visited_ports.insert(port) { - continue; - } - let Some(producer) = d.port_opt(&port).and_then(|p| p.produced_by) else { - continue; - }; - expand_behavior_backward(d, producer, &mut frontier, &mut visited_nodes); - } - - // Always include the workflow root itself for dimension witnesses (e.g. a - // `Bind` is never the `produced_by` of its own `value` port). Do **not** - // seed `workflow_root` into `visited_nodes` before the walk: when the root - // is a `Transform`, its result port's producer is that same node — an early - // insert would make `expand_behavior_backward` return without enqueueing the - // transform's inputs, shrinking the slice to `{root}` only. - visited_nodes.insert(workflow_root); - visited_nodes -} - -fn expand_behavior_backward( - d: &Dag, - node: NodeId, - frontier: &mut Vec, - visited_nodes: &mut HashSet, -) { - if !visited_nodes.insert(node) { - return; - } - let Some(behavior) = d.node_opt(&node) else { - return; - }; - match behavior { - Behavior::Value(_) => {} - Behavior::Transform(transform) => { - for input in &transform.inputs { - frontier.push(*input); - } - } - Behavior::Branch(branch) => { - frontier.push(branch.input); - for path in &branch.paths { - expand_behavior_backward(d, path.body, frontier, visited_nodes); - frontier.push(path.output); - } - } - Behavior::Loop(loop_node) => { - frontier.push(loop_node.source); - frontier.push(loop_node.init); - if let Some(count) = loop_node.bound.count_port() { - frontier.push(count); - } - expand_behavior_backward(d, loop_node.body, frontier, visited_nodes); - } - Behavior::Bind(bind) => { - for param in &bind.params { - frontier.push(*param); - } - frontier.push(bind.value); - } - } -} - -/// Symbolic-cost dimension analysis — DB-3 dispatch over the cost lens algebra. -/// -/// On success ([`DimensionReport::DimensionOk`]), `composed` is the asymptotic -/// bound at `workflow_root`'s result port. Witnesses walk every reachable -/// [`Behavior`] in [`behavior_spine_in_node_order`] (construction order, -/// filtered to the backward slice from the workflow root). Failure is only -/// [`DimensionReport::DimensionFail`]: there is no fabricated carrier. -pub fn analyze_symbolic_cost_dimension( - d: &Dag, - workflow_root: NodeId, -) -> DimensionReport { - const DIMENSION_NAME: &str = "symbolic_cost"; - let scope = workflow_reachable_behavior_ids(d, workflow_root); - let mut witnesses = Vec::new(); - for behavior in d.nodes() { - if !scope.contains(&behavior.id()) { - continue; - } - let port = behavior_result_port(behavior); - match symbolic_cost_of(d, &port) { - SymbolicCostLookup::Miss => witnesses.push(Witness::Violates { - reason: "missing symbolic cost for behavior result port".into(), - at: behavior.clone(), - }), - SymbolicCostLookup::Hit(cost) => witnesses.push(Witness::Inhabits(cost)), - } - } - - let root = d.node(workflow_root); - let root_lookup = symbolic_cost_of(d, &behavior_result_port(root)); - - let witness_failure = witnesses - .iter() - .any(|w| matches!(w, Witness::Violates { .. })); - - if !witness_failure { - if let SymbolicCostLookup::Hit(composed) = root_lookup { - return DimensionReport::DimensionOk { - dimension_name: DIMENSION_NAME.to_string(), - composed, - witnesses, - }; - } - } - - let mut violations: Vec = witnesses - .iter() - .filter_map(|w| { - let Witness::Violates { reason, at } = w else { - return None; - }; - Some(Diagnostic::ParseError { - message: format!("symbolic_cost dimension: {reason}"), - span: behavior_span(at), - correction: Correction::deferred_for_diagnostic_class( - "SymbolicCostDimensionDiagnostic", - ), - }) - }) - .collect(); - - // Root `Miss` normally duplicates a `Witness::Violates` on the workflow root - // (same port as `behavior_result_port(root)`). Only synthesize a diagnostic - // here if the witness spine and root lookup ever disagree (fail-closed). - if matches!(root_lookup, SymbolicCostLookup::Miss) && violations.is_empty() { - violations.push(Diagnostic::ParseError { - message: "symbolic_cost dimension: missing symbolic cost for workflow root result port" - .into(), - span: behavior_span(root), - correction: Correction::deferred_for_diagnostic_class( - "SymbolicCostDimensionDiagnostic", - ), - }); - } - - DimensionReport::DimensionFail { - dimension_name: DIMENSION_NAME.to_string(), - violations, - witnesses, - } -} - -/// PR-E E7 symbolic-cost-only first executable slice: public -/// complexity-analysis entrypoint per -/// `docs/briefs/r3-evaluator-e7-symbolic-cost-only-follow-on-readiness.md`. -/// -/// **Single authority.** Thin wrapper that delegates to -/// [`analyze_symbolic_cost_dimension`] — the lens-spine path that walks -/// reachable behaviors from `workflow_root` via -/// `workflow_reachable_behavior_ids` and consumes -/// [`crate::lens_cost_symbolic::symbolic_cost_of`] for each behavior's -/// result port. The wrapper exists so the E7 public surface is named -/// the way the dispatch brief locks it (`analyze_complexity` / -/// `analyze_tenant_flow` / `analyze_ifc`) without introducing a -/// parallel analyzer. -/// -/// **Lens-spine, not body-evaluator-driven.** This entrypoint does -/// **not** depend on `eval_node` / `evaluate_body` at all — the -/// symbolic-cost lens (`lens_cost_symbolic_generated.rs`) walks the -/// program DAG structurally and produces `SymbolicCostLookup::Hit(cost)` -/// for every reachable behavior including `Loop`. Even now that -/// `eval_node` dispatches `Behavior::Loop` (E5 landed), this wrapper -/// remains the single-authority complexity entrypoint until -/// `analyze_with_evaluator` (the body-evaluator-driven form) lands; -/// at that point this lens-spine wrapper either dissolves or stays as -/// the not-evaluator-driven path for cost specifically. -/// -/// **Diagnostics.** `DimensionFail.violations` carries typed -/// [`Diagnostic`] entries; tests must assert by typed pattern match, -/// never by parsing `Witness::Violates.reason` strings. -pub fn analyze_complexity(dag: &Dag, workflow_root: NodeId) -> DimensionReport { - analyze_symbolic_cost_dimension(dag, workflow_root) -} - -#[cfg(test)] -mod analyze_complexity_tests { - use super::*; - use crate::dag::{literal_bits_int, Dag, TransformTarget}; - use crate::operators::{ArithmeticOp, OperatorKind}; - - fn span() -> SourceSpan { - SourceSpan::new("analyze_complexity_test", 0, 0) - } - - fn int_add_dag() -> (Dag, NodeId) { - let mut dag = Dag::new(); - let lhs = dag.push_value(literal_bits_int(1), span()); - let rhs = dag.push_value(literal_bits_int(2), span()); - let add_out = dag.push_transform( - TransformTarget::Operator(OperatorKind::Arithmetic(ArithmeticOp::Add)), - vec![lhs, rhs], - span(), - ); - let root = dag.push_bind("sum", add_out, vec![], span()); - (dag, root) - } - - // E7 §test 1 — single-authority: analyze_complexity must produce - // the same DimensionReport as the live analyze_symbolic_cost_dimension - // for the same (dag, workflow_root). Pins that the wrapper does not - // introduce a parallel analyzer. - #[test] - fn analyze_complexity_matches_analyze_symbolic_cost_dimension_for_known_workflow() { - let (dag, root) = int_add_dag(); - - let wrapped = analyze_complexity(&dag, root); - let direct = analyze_symbolic_cost_dimension(&dag, root); - - match (&wrapped, &direct) { - ( - DimensionReport::DimensionOk { - dimension_name: w_name, - composed: w_composed, - witnesses: w_witnesses, - }, - DimensionReport::DimensionOk { - dimension_name: d_name, - composed: d_composed, - witnesses: d_witnesses, - }, - ) => { - assert_eq!(w_name, d_name); - assert_eq!(w_composed, d_composed); - assert_eq!(w_witnesses.len(), d_witnesses.len()); - } - other => { - panic!("expected both branches DimensionOk with matching content, got {other:?}") - } - } - } - - // E7 §test 1 (returns_ok shape check) — DimensionOk arm carries the - // expected dimension_name and a SymbolicCost composed value. - #[test] - fn analyze_complexity_returns_ok_for_bounded_int_add_workflow() { - let (dag, root) = int_add_dag(); - - let report = analyze_complexity(&dag, root); - - let DimensionReport::DimensionOk { - dimension_name, - composed: _, - witnesses, - } = report - else { - panic!("expected DimensionOk for bounded int-add, got {report:?}"); - }; - assert_eq!(dimension_name, "symbolic_cost"); - assert!( - witnesses - .iter() - .all(|w| !matches!(w, Witness::Violates { .. })), - "no Violates witness expected on the happy path", - ); - } - - // E7 §test 2 — fail-closed on missing cost: program with a - // ghost-input transform produces DimensionFail with at least one - // typed Diagnostic::ParseError in violations. Asserted by typed - // pattern match on the Diagnostic enum, never by string parsing. - #[test] - fn analyze_complexity_fails_closed_with_typed_diagnostic_on_missing_cost() { - let mut dag = Dag::new(); - let int_shape = dag.int_shape().expect("bootstrap Int"); - let lhs = dag.push_value(literal_bits_int(1), span()); - let ghost_input = dag.alloc_port_with_shape(int_shape); - let bad_add = dag.push_transform( - TransformTarget::Operator(OperatorKind::Arithmetic(ArithmeticOp::Add)), - vec![lhs, ghost_input], - span(), - ); - let root = dag.push_bind("x", bad_add, vec![], span()); - - let report = analyze_complexity(&dag, root); - - let DimensionReport::DimensionFail { - dimension_name, - violations, - witnesses: _, - } = report - else { - panic!("expected DimensionFail when the cost lens misses a port, got {report:?}"); - }; - assert_eq!(dimension_name, "symbolic_cost"); - assert!( - !violations.is_empty(), - "DimensionFail must carry at least one typed diagnostic", - ); - assert!( - violations - .iter() - .all(|d| matches!(d, Diagnostic::ParseError { .. })), - "every violation must be a typed Diagnostic enum variant, got {violations:?}", - ); - } - - // E7 §test 3 — DimensionFail does not fabricate a `composed` - // carrier. Pattern-matching the `DimensionFail` arm is enough to - // assert this: the field is unreachable on that arm by - // construction. The wrapper preserves the partition. Reuses the - // ghost-input transform pattern from the existing - // `missing_symbolic_cost_surfaces_as_dimension_fail_with_violates_witnesses` - // test so the workflow is well-formed enough for the lens walk. - #[test] - fn analyze_complexity_fail_arm_has_no_composed_field() { - let mut dag = Dag::new(); - let int_shape = dag.int_shape().expect("bootstrap Int"); - let lhs = dag.push_value(literal_bits_int(1), span()); - let ghost_input = dag.alloc_port_with_shape(int_shape); - let bad_add = dag.push_transform( - TransformTarget::Operator(OperatorKind::Arithmetic(ArithmeticOp::Add)), - vec![lhs, ghost_input], - span(), - ); - let root = dag.push_bind("y", bad_add, vec![], span()); - - let report = analyze_complexity(&dag, root); - - match report { - DimensionReport::DimensionFail { .. } => { - // Exhaustive match on this arm: the only fields are - // dimension_name / violations / witnesses. There is no - // `composed` field to fabricate. Pattern coverage IS - // the assertion. - } - DimensionReport::DimensionOk { .. } => { - panic!("expected DimensionFail on a ghost-input workflow, not DimensionOk") - } - } - } - - // E7 §test 6 — typed-diagnostic discipline: every entry in - // DimensionFail.violations must be inspectable as a typed - // Diagnostic enum variant. No to_string / format / regex on the - // human-facing message field beyond non-empty checks. (Test #5 - // from the brief is a discipline rule enforced by reviewer - // convention, not a runtime assertion.) - #[test] - fn analyze_complexity_violation_entries_are_typed_diagnostic_enums() { - let mut dag = Dag::new(); - let int_shape = dag.int_shape().expect("bootstrap Int"); - let lhs = dag.push_value(literal_bits_int(1), span()); - let ghost_input = dag.alloc_port_with_shape(int_shape); - let bad_add = dag.push_transform( - TransformTarget::Operator(OperatorKind::Arithmetic(ArithmeticOp::Add)), - vec![lhs, ghost_input], - span(), - ); - let root = dag.push_bind("z", bad_add, vec![], span()); - - let report = analyze_complexity(&dag, root); - let DimensionReport::DimensionFail { violations, .. } = report else { - panic!("expected DimensionFail for ghost-port workflow"); - }; - - for diagnostic in &violations { - // Exhaustive enum match: every variant of Diagnostic is a - // structural type the test can pattern-match on without - // parsing message content. Non-`ParseError` variants are - // also acceptable typed inhabitants. - let typed = match diagnostic { - Diagnostic::ParseError { message, .. } => !message.is_empty(), - _ => true, - }; - assert!( - typed, - "Diagnostic must carry a non-empty human-facing message; got {diagnostic:?}", - ); - } - } -} - -#[cfg(test)] -mod fail_closed_tests { - use super::*; - use crate::dag::{literal_bits_int, Dag, TransformTarget}; - use crate::operators::{ArithmeticOp, OperatorKind}; - - #[test] - fn missing_symbolic_cost_surfaces_as_dimension_fail_with_violates_witnesses() { - let mut dag = Dag::new(); - let span = SourceSpan::new("dimension_fail_closed_test", 0, 0); - let int_shape = dag.int_shape().expect("bootstrap Int"); - let lhs = dag.push_value(literal_bits_int(1), span.clone()); - let ghost_input = dag.alloc_port_with_shape(int_shape); - let bad_add = dag.push_transform( - TransformTarget::Operator(OperatorKind::Arithmetic(ArithmeticOp::Add)), - vec![lhs, ghost_input], - span.clone(), - ); - let bind_id = dag.push_bind("x", bad_add, vec![], span); - - let report = analyze_symbolic_cost_dimension(&dag, bind_id); - let DimensionReport::DimensionFail { - violations, - witnesses, - .. - } = report - else { - panic!("expected DimensionFail when cost lens misses an input port, got {report:?}"); - }; - - assert!( - violations - .iter() - .any(|v| matches!(v, Diagnostic::ParseError { .. })), - "expected ParseError diagnostics from dimension proof failure, got {violations:?}" - ); - assert!( - witnesses - .iter() - .any(|w| matches!(w, Witness::Violates { .. })), - "expected at least one Violates witness, got {witnesses:?}" - ); - assert!( - witnesses - .iter() - .all(|w| !matches!(w, Witness::Inhabits(SymbolicCost::UnknownCost { .. }))), - "dimension witnesses must not fabricate UnknownCost carriers" - ); - } - - #[test] - fn transform_workflow_root_still_backward_reaches_operand_ports() { - let mut dag = Dag::new(); - let span = SourceSpan::new("transform_root_reach_test", 0, 0); - let lhs = dag.push_value(literal_bits_int(1), span.clone()); - let rhs = dag.push_value(literal_bits_int(2), span.clone()); - let add_out = dag.push_transform( - TransformTarget::Operator(OperatorKind::Arithmetic(ArithmeticOp::Add)), - vec![lhs, rhs], - span, - ); - let transform_root = dag - .port(add_out) - .produced_by - .expect("transform output port wired to its node"); - - let report = analyze_symbolic_cost_dimension(&dag, transform_root); - let DimensionReport::DimensionOk { witnesses, .. } = report else { - panic!("expected DimensionOk for well-wired Int add, got {report:?}"); - }; - assert!( - witnesses.len() >= 3, - "expected transform + two literal witnesses, got {}", - witnesses.len() - ); - } -} diff --git a/tools/pb0_cycle4_emit_templates/infer.rs.in b/tools/pb0_cycle4_emit_templates/infer.rs.in deleted file mode 100644 index 8e62b4bc523..00000000000 --- a/tools/pb0_cycle4_emit_templates/infer.rs.in +++ /dev/null @@ -1,7262 +0,0 @@ -// Type inference: fills Port.state by propagating types forward through -// the DAG. Dispatches on TypeConnective (M1_DESIGN.md §4) instead of the -// old name-based function signature lookup. -// -// Transform.target is a DeclarationId. Inference reads the target's -// Declaration and: -// - Arrow { inputs, output, body }: direct signature from the input and -// output declarations, converted to port-level TypeShape via the -// `declaration_to_type_shape` bridge. -// - Atom(Identifier { name, resolved }): follow the resolved link if set, -// otherwise look up the name in the Dag's declaration table -// (§8.9 inhabitance walk, reduced at M1(2.5) to direct primitive lookup -// pending the full algebra bootstrap in Phase 5). -// - Anything else: not callable. -// -// Fail-closed (invariant C-8): every detectable problem goes through -// Dag::mark_unresolved. After inference, a post-sweep drives any remaining -// Uninferred ports to Unresolved with a generic diagnostic so the -// biconditional -// state == Unresolved iff diagnostics.contains(port_id) -// and the post-invariant -// state != Uninferred after infer completes -// both hold. -// -// **M1 extension — inference mutates `Dag.declarations`:** operator / algebra -// resolution may `push_declaration` for anonymous `Instantiation` shapes (e.g. -// `Result` for totalizing `div`); `find_equivalent_anonymous_instantiation` -// deduplicates so fixpoint growth stays bounded. `decide` takes `&mut Dag` and clones -// each `Behavior` before matching so borrows stay sound — inference is not read-only -// over the declaration table. - -use std::collections::HashSet; - -use crate::dag::{ - substrate_div_error_type_decl_suppressed_for_emit, - substrate_result_type_decl_suppressed_for_emit, ArithmeticOp, ArrowBody, AtomPayload, Behavior, - BindNode, BindNodeId, CardinalityBound, Dag, Declaration, DeclarationId, Field, LiteralBits, - Lookup, NominalOpacity, PhantomParameter, PortId, PortState, TemplateArgument, TransformNode, - TransformTarget, TypeConnective, -}; -use crate::diagnostics::{ - declaration_display_name, example_source_for_decl, witness_correction_for_decl, Correction, - Diagnostic, SourceSpan, -}; -use crate::infer_helpers::{ - behavior_output_port, generated_template_arguments_match, normalize_instantiation_arguments, - payload_binding_span as generated_payload_binding_span, - push_template_argument_binding as generated_push_template_argument_binding, - resolve_template_argument_value as generated_resolve_template_argument_value, - template_argument_value as generated_template_argument_value, NormalizedInstantiationArgs, - TemplateArgumentBinding, TemplateArgumentsMatch, -}; -use std::str::FromStr; - -use num_bigint::BigInt; - -use crate::int_literal_ranges::{ - int_literal_fits_expected_type, integer_range_for_decl, literal_bigint_at, - magnitude_out_of_range_for_interval, IntegerRangeLookup, -}; -use crate::lower::{clone_predicate_body, outer_predicate_slots}; -use crate::operators::{LogicalOp, OperatorKind}; -use crate::types::TypeShape; - -/// Outcome of [`try_reconcile_int_literal_decision_set`]. -enum IntLiteralSetReconciliation { - /// Reconciled without changing the port (narrow annotation already wins over default `Int`). - Keep, - /// Applied `set_port_type` to narrow a default-`Int` literal port to `ty`. - SetNarrow, - /// `mark_unresolved` with a magnitude or other diagnostic. - Marked, -} - -/// Reconcile `Decision::Set` for ports backed by an integer literal when the proposed -/// and existing [`TypeShape`] would otherwise `TypeMismatch`: (a) `let`/`data` -/// pre-seed vs default `Int`, (b) call-sites that propose a callee's narrow -/// type over a default-`Int` argument. -fn try_reconcile_int_literal_decision_set( - dag: &mut Dag, - port: PortId, - existing: TypeShape, - ty: TypeShape, -) -> Option { - let default_int = dag.int_shape()?; - let literal = literal_bigint_at(dag, port)?; - if type_shapes_equivalent(dag, &ty, &default_int) { - match int_literal_fits_expected_type(dag, &literal, existing.declaration) { - Ok(Some(true)) => Some(IntLiteralSetReconciliation::Keep), - Ok(Some(false)) => { - if let IntegerRangeLookup::Found(range) = - integer_range_for_decl(dag, existing.declaration) - { - let span = node_span_for_port(dag, port).unwrap_or_else(synthetic_span); - dag.mark_unresolved( - port, - magnitude_out_of_range_for_interval(&literal, existing, range, span), - ); - Some(IntLiteralSetReconciliation::Marked) - } else { - None - } - } - Ok(None) => None, - Err(diag) => { - if !matches!(dag.port(port).state(), PortState::Unresolved) { - dag.mark_unresolved(port, diag); - } - Some(IntLiteralSetReconciliation::Marked) - } - } - } else if type_shapes_equivalent(dag, &existing, &default_int) { - match int_literal_fits_expected_type(dag, &literal, ty.declaration) { - Ok(Some(true)) => { - dag.set_port_type(port, ty); - Some(IntLiteralSetReconciliation::SetNarrow) - } - Ok(Some(false)) => { - if let IntegerRangeLookup::Found(range) = - integer_range_for_decl(dag, ty.declaration) - { - let span = node_span_for_port(dag, port).unwrap_or_else(synthetic_span); - dag.mark_unresolved( - port, - magnitude_out_of_range_for_interval(&literal, ty, range, span), - ); - Some(IntLiteralSetReconciliation::Marked) - } else { - None - } - } - Ok(None) => None, - Err(diag) => { - if !matches!(dag.port(port).state(), PortState::Unresolved) { - dag.mark_unresolved(port, diag); - } - Some(IntLiteralSetReconciliation::Marked) - } - } - } else { - None - } -} - -pub fn infer(dag: &mut Dag) { - // Fixpoint loop. Runs decide for every node, then pattern - // resolution + exhaustiveness + uniqueness for every Branch. - // Pattern resolution is folded into the loop (not run after it) - // so a Branch that gets flipped to Unresolved by - // non-exhaustive/duplicate-arm checks propagates to downstream - // consumers: the next iteration's decide pass sees the upstream - // Unresolved and cascades Decision::Fail through Transform / - // Branch / Loop / Bind consumers. Running pattern resolution - // after the main loop would leave downstream types stale and - // violate FAIL-CLOSED. - loop { - ensure_all_optional_match_disjs(dag); - let mut changed = false; - let node_count = dag.nodes().len(); - for i in 0..node_count { - match decide(dag, i) { - Decision::Set(port, ty) => { - if matches!(dag.port(port).state(), PortState::Unresolved) { - continue; - } - match dag.port(port).state().clone() { - PortState::Uninferred => { - dag.set_port_type(port, ty); - changed = true; - } - PortState::Resolved(existing) - if type_shapes_equivalent(dag, &existing, &ty) => {} - PortState::Resolved(existing) => { - match int_literal_magnitude_narrow_merge(dag, port, &existing, &ty) { - IntLiteralNarrowMerge::Merge => { - dag.set_port_type(port, ty); - changed = true; - continue; - } - IntLiteralNarrowMerge::Reject(diag) => { - dag.mark_unresolved(port, *diag); - changed = true; - continue; - } - IntLiteralNarrowMerge::NotApplicable => {} - } - if is_retryable_generic_decl(dag, existing.declaration) - || is_retryable_generic_decl(dag, ty.declaration) - { - dag.set_port_type(port, ty); - changed = true; - continue; - } - if let Some(outcome) = - try_reconcile_int_literal_decision_set(dag, port, existing, ty) - { - match outcome { - IntLiteralSetReconciliation::Keep => continue, - IntLiteralSetReconciliation::SetNarrow - | IntLiteralSetReconciliation::Marked => { - changed = true; - continue; - } - } - } - let span = node_span_for_port(dag, port).unwrap_or_else(synthetic_span); - let correction = witness_correction_for_decl( - dag, - existing.declaration, - span.clone(), - format!( - "replace this expression with a `{}` value", - declaration_display_name(dag, existing.declaration) - ), - ) - .unwrap_or_else(|| { - Correction::deferred_for_diagnostic_class("InferenceDiagnostic") - }); - let diag = Diagnostic::TypeMismatch { - expected: existing, - actual: ty, - span, - correction, - }; - dag.mark_unresolved(port, diag); - changed = true; - } - PortState::Unresolved => unreachable!("guarded above"), - } - } - Decision::Fail(port, diag) => { - if !matches!(dag.port(port).state(), PortState::Unresolved) { - dag.mark_unresolved(port, *diag); - changed = true; - } - } - Decision::Retry => {} - } - } - if resolve_branch_patterns(dag) { - changed = true; - } - if resolve_branch_payload_bindings(dag) { - changed = true; - } - if resolve_field_project_targets(dag) { - changed = true; - } - if resolve_callable_targets(dag) { - changed = true; - } - if materialize_callable_signature_instantiations(dag) { - changed = true; - } - if resolve_lambda_parameter_types(dag) { - changed = true; - } - if validate_user_defined_function_signatures(dag) { - changed = true; - } - if !changed { - break; - } - } - - // Post-sweep: any port still Uninferred means decide() missed a case. - let uninferred_ports: Vec = dag - .all_ports() - .filter(|p| matches!(p.state(), PortState::Uninferred)) - .map(|p| p.id()) - .collect(); - for port in uninferred_ports { - let span = node_span_for_port(dag, port).unwrap_or_else(synthetic_span); - dag.mark_unresolved( - port, - Diagnostic::ResolveError { - name: "(inference did not resolve this port)".to_string(), - span, - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }, - ); - } - - crate::enforced_lens_application::check_enforced_lens_applications(dag); -} - -/// Walk a declaration through `Instantiation` / `ResolvedIdentifier` -/// edges until it reaches a declaration whose connective is -/// `Disj`. Returns the `DeclarationId` of that Disj declaration — -/// or `None` if no Disj is reachable within `WALK_DEPTH_LIMIT` -/// steps, or the chain terminates at some other connective. -/// -/// Used by both the `decide_transform` Branch gate and the -/// post-infer `resolve_branch_patterns` pass so that aliased or -/// instantiated sum types (`type Hue = Color` where -/// `Color = Red | Green | Blue`) resolve to the same underlying -/// `Disj` fact whether their immediate connective is the Disj -/// itself, an `Instantiation` pointing at it, or an -/// `Atom(ResolvedIdentifier(...))` chain of aliases. The -/// alternative — reading `decl.connective` directly — would -/// reject aliases even though the underlying sum fact survives -/// in the declaration graph, violating Facts Flow Forward. -fn walk_to_disj_decl(dag: &Dag, start: DeclarationId) -> Option { - let mut current = start; - for _ in 0..WALK_DEPTH_LIMIT { - let decl = dag.declaration(current); - match &decl.connective { - TypeConnective::Disj { .. } => return Some(current), - TypeConnective::Cardinality(p) - if p.bound() == crate::dag::CardinalityBound::AtMostOne => - { - return existing_optional_match_disj_decl(dag, current); - } - TypeConnective::Instantiation { template, .. } => { - current = *template; - } - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - current = *next; - } - _ => return None, - } - } - None -} - -fn existing_optional_match_disj_decl( - dag: &Dag, - cardinality_decl_id: DeclarationId, -) -> Option { - dag.optional_match_disj(cardinality_decl_id) -} - -fn walk_to_optional_cardinality_decl(dag: &Dag, start: DeclarationId) -> Option { - let mut current = start; - for _ in 0..WALK_DEPTH_LIMIT { - match &dag.declaration(current).connective { - TypeConnective::Cardinality(p) - if p.bound() == crate::dag::CardinalityBound::AtMostOne => - { - return Some(current); - } - TypeConnective::Instantiation { template, .. } => current = *template, - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => current = *next, - _ => return None, - } - } - None -} - -pub(crate) fn ensure_optional_match_disj( - dag: &mut Dag, - cardinality_decl_id: DeclarationId, -) -> Option { - if let Some(existing) = existing_optional_match_disj_decl(dag, cardinality_decl_id) { - return Some(existing); - } - let (element, span) = match dag.declaration(cardinality_decl_id).connective.clone() { - TypeConnective::Cardinality(p) if p.bound() == crate::dag::CardinalityBound::AtMostOne => ( - p.element(), - dag.declaration(cardinality_decl_id).span.clone(), - ), - _ => return None, - }; - - let some_payload_id = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: some_payload_id, - name: None, - connective: TypeConnective::Conj { - children: vec![Field { - label: "_0".to_string(), - ty: element, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - - let none_payload_id = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: none_payload_id, - name: None, - connective: TypeConnective::Conj { - children: Vec::new(), - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - - let disj_id = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: disj_id, - name: None, - connective: TypeConnective::Disj { - variants: vec![ - Field { - label: "Some".to_string(), - ty: some_payload_id, - }, - Field { - label: "None".to_string(), - ty: none_payload_id, - }, - ], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span, - }); - dag.set_optional_match_disj(cardinality_decl_id, disj_id); - - Some(disj_id) -} - -/// Materialize `Some`/`None` match sums for every `Cardinality(_, AtMostOne)` row so -/// downstream structural consumers (substrate reflection, lenses) can use -/// `Dag::optional_match_disj` without requiring an optional scrutinee on a Branch. -pub(crate) fn ensure_all_optional_match_disjs(dag: &mut Dag) { - let decl_ids: Vec = dag.declarations().iter().map(|d| d.id).collect(); - for decl_id in decl_ids { - if matches!( - &dag.declaration(decl_id).connective, - TypeConnective::Cardinality(p) if p.bound() == CardinalityBound::AtMostOne - ) && existing_optional_match_disj_decl(dag, decl_id).is_none() - { - ensure_optional_match_disj(dag, decl_id).expect( - "AtMostOne cardinality without optional_match_disj should materialize a Some/None disj", - ); - } - } -} - -/// For every Branch node, resolve each Path's `BranchPattern` by -/// matching the arm's variant name against the scrutinee's Disj -/// children and verify exhaustiveness + uniqueness. Mutates the -/// Path in place for successful rewrites, marks the Branch's -/// output port Unresolved (via `Dag::mark_unresolved`) for -/// failures. Skips Branches whose input port is not yet Resolved — -/// those will be revisited on the next fixpoint iteration. -/// -/// Aliases / instantiations of sum types normalize through -/// `walk_to_disj_decl` before variant resolution, so `type Hue = -/// Color` and `Color = Red | Green | Blue` both resolve arms -/// against `Color`'s variants. -/// -/// Returns `true` if any state (Path.pattern rewrite or Branch -/// output port transition) changed, so the calling fixpoint loop -/// knows to keep iterating. Pattern-resolution changes and -/// coverage-check failures both propagate downstream through the -/// normal `decide_transform` cascade in subsequent iterations. -fn resolve_branch_patterns(dag: &mut Dag) -> bool { - let mut changed = false; - let optional_scrutinees: HashSet = dag - .nodes() - .iter() - .filter_map(|node| { - let Behavior::Branch(branch) = node else { - return None; - }; - let PortState::Resolved(ty) = dag.port(branch.input).state() else { - return None; - }; - walk_to_optional_cardinality_decl(dag, ty.declaration) - }) - .collect(); - for decl_id in optional_scrutinees { - if existing_optional_match_disj_decl(dag, decl_id).is_none() - && ensure_optional_match_disj(dag, decl_id).is_some() - { - changed = true; - } - } - // Collect the rewrites first (immutable borrow of nodes + ports + - // declarations), then apply them (mutable borrow of nodes). This - // two-phase pattern avoids borrow conflicts while still reading - // declaration bodies. - struct Rewrite { - node_index: usize, - path_index: usize, - result: Result, - output_port: PortId, - } - let mut rewrites: Vec = Vec::new(); - // Coverage check: for each Branch, after resolving all paths, - // verify the resolved set equals the scrutinee's Disj variant set - // exactly (every variant covered by exactly one arm). Missing or - // duplicated arms are fail-closed diagnostics on the Branch's - // output port. - struct CoverageCheck { - output_port: PortId, - span: SourceSpan, - expected: Vec<(String, DeclarationId)>, - arm_outputs: Vec, - // Parallel vec of (resolved_decl_id, arm_name_for_diagnostic) - // collected in path order. Paths whose resolution failed are - // not included — their error already fires, so coverage is - // reported only on the subset that resolved. - resolved_arms: Vec<(DeclarationId, String)>, - } - let mut coverage_checks: Vec = Vec::new(); - for (node_index, node) in dag.nodes().iter().enumerate() { - let Behavior::Branch(b) = node else { - continue; - }; - let scrutinee_ty = match dag.port(b.input).state() { - PortState::Resolved(ty) => *ty, - PortState::Unresolved | PortState::Uninferred => continue, - }; - // Walk the scrutinee through alias / instantiation edges to - // the underlying Disj. `type Hue = Color` and - // `Color = Red | Green | Blue` both resolve arms against - // `Color`'s variants via this walk. - let Some(disj_decl_id) = walk_to_disj_decl(dag, scrutinee_ty.declaration).or_else(|| { - walk_to_optional_cardinality_decl(dag, scrutinee_ty.declaration) - .and_then(|cardinality| existing_optional_match_disj_decl(dag, cardinality)) - }) else { - // Scrutinee doesn't resolve to a Disj — the main infer - // pass caught this as a TypeMismatch already. Skip. - continue; - }; - let disj_variants: Vec<(String, DeclarationId)> = - match &dag.declaration(disj_decl_id).connective { - TypeConnective::Disj { variants } => { - variants.iter().map(|f| (f.label.clone(), f.ty)).collect() - } - _ => unreachable!("walk_to_disj_decl returned a non-Disj declaration"), - }; - let mut resolved_arms: Vec<(DeclarationId, String)> = Vec::new(); - for (path_index, path) in b.paths.iter().enumerate() { - let result = match &path.pattern { - crate::dag::BranchPattern::ResolvedVariant(id) => { - // Already resolved (e.g., if/else paths on a - // second infer pass). Record for coverage check - // and skip the rewrite. - let arm_name = disj_variants - .iter() - .find(|(_, variant_id)| variant_id == id) - .map(|(label, _)| label.clone()) - .unwrap_or_else(|| format!("declaration#{}", id.raw())); - resolved_arms.push((*id, arm_name)); - continue; - } - crate::dag::BranchPattern::UnresolvedVariant { name, span } => { - match disj_variants.iter().find(|(label, _)| label == name) { - Some((_, ty)) => { - resolved_arms.push((*ty, name.clone())); - Ok(*ty) - } - None => Err(Diagnostic::ResolveError { - name: format!( - "variant `{name}` is not a constructor of this match's scrutinee type" - ), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }), - } - } - }; - rewrites.push(Rewrite { - node_index, - path_index, - result, - output_port: b.output, - }); - } - coverage_checks.push(CoverageCheck { - output_port: b.output, - span: b.span.clone(), - expected: disj_variants, - arm_outputs: b.paths.iter().map(|path| path.output).collect(), - resolved_arms, - }); - } - for rewrite in rewrites { - match rewrite.result { - Ok(variant_id) => { - if let Behavior::Branch(b) = &mut dag.nodes_mut()[rewrite.node_index] { - b.paths[rewrite.path_index].pattern = - crate::dag::BranchPattern::ResolvedVariant(variant_id); - changed = true; - } - } - Err(diag) => { - if !matches!(dag.port(rewrite.output_port).state(), PortState::Unresolved) { - dag.mark_unresolved(rewrite.output_port, diag); - changed = true; - } - } - } - } - // Coverage pass: for each Branch whose paths resolved (fully or - // partially), verify exhaustiveness (every variant covered) and - // uniqueness (no variant duplicated). Skip if the Branch's output - // already has a diagnostic from a pattern resolution failure — - // coverage is meaningless on a partially-resolved arm set. - for check in coverage_checks { - if matches!(dag.port(check.output_port).state(), PortState::Unresolved) { - continue; - } - // Uniqueness: each resolved DeclarationId appears at most once. - let mut seen: HashSet = HashSet::new(); - let mut duplicate: Option = None; - for (id, name) in &check.resolved_arms { - if !seen.insert(*id) { - duplicate = Some(name.clone()); - break; - } - } - if let Some(name) = duplicate { - dag.mark_unresolved( - check.output_port, - Diagnostic::ResolveError { - name: format!( - "duplicate match arm for variant `{name}` — each variant of a sum type must match exactly once" - ), - span: check.span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }, - ); - changed = true; - continue; - } - // Exhaustiveness: every expected variant must be in the - // resolved set. Surface the first missing variant's name as - // the diagnostic label. - let missing: Vec<&str> = check - .expected - .iter() - .filter(|(_, id)| !seen.contains(id)) - .map(|(label, _)| label.as_str()) - .collect(); - if !missing.is_empty() { - let missing_list = missing.join(", "); - let arm_prefix = if check.resolved_arms.is_empty() { - "" - } else { - ", " - }; - let body = branch_output_example_source(dag, check.output_port, &check.arm_outputs); - let insert_at = check.span.byte_end.saturating_sub(1); - let correction_span = SourceSpan::new(check.span.file.clone(), insert_at, insert_at); - let arms = missing - .iter() - .map(|variant| format!("{variant} => {body}")) - .collect::>() - .join(", "); - dag.mark_unresolved( - check.output_port, - Diagnostic::ResolveError { - name: format!( - "non-exhaustive match: missing arm(s) for variant(s) `{missing_list}` — every constructor of the scrutinee's sum type must be covered" - ), - span: check.span, - correction: Correction::live( - format!("add missing arm(s) for `{missing_list}`"), - correction_span, - format!("{arm_prefix}{arms}"), - ), - }, - ); - changed = true; - } - } - changed -} - -fn branch_output_example_source(dag: &Dag, output_port: PortId, arm_outputs: &[PortId]) -> String { - std::iter::once(output_port) - .chain(arm_outputs.iter().copied()) - .find_map(|port| match dag.port(port).state() { - PortState::Resolved(output_ty) => example_source_for_decl(dag, output_ty.declaration), - PortState::Uninferred | PortState::Unresolved => None, - }) - .unwrap_or_else(|| "()".to_string()) -} - -fn branch_payload_binding_span(path: &crate::dag::Path, branch_span: &SourceSpan) -> SourceSpan { - let extent = generated_payload_binding_span(path, branch_span); - let file = match &path.pattern { - crate::dag::BranchPattern::UnresolvedVariant { span, .. } => span.file.clone(), - crate::dag::BranchPattern::ResolvedVariant(_) => branch_span.file.clone(), - }; - SourceSpan::new(file, extent.byte_start, extent.byte_end) -} - -fn resolve_branch_payload_bindings(dag: &mut Dag) -> bool { - struct BindingRewrite { - port: PortId, - span: SourceSpan, - result: Result, - } - - let mut rewrites: Vec = Vec::new(); - - for node in dag.nodes() { - let Behavior::Branch(b) = node else { - continue; - }; - - match dag.port(b.input).state() { - PortState::Uninferred => continue, - PortState::Unresolved => { - for path in &b.paths { - let Some(binding) = &path.binding else { - continue; - }; - rewrites.push(BindingRewrite { - port: binding.payload_port, - span: branch_payload_binding_span(path, &b.span), - result: Err(Diagnostic::ResolveError { - name: "(upstream failure in match payload binding)".to_string(), - span: branch_payload_binding_span(path, &b.span), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }), - }); - } - } - PortState::Resolved(scrutinee_ty) => { - let mut subst = SubstStack::new(); - let disj_decl_id = - walk_to_disj_decl_with_subst(dag, scrutinee_ty.declaration, &mut subst); - if disj_decl_id.is_none() - && is_retryable_generic_decl(dag, scrutinee_ty.declaration) - { - continue; - } - let variants = disj_decl_id.map(|id| match &dag.declaration(id).connective { - TypeConnective::Disj { variants } => variants - .iter() - .map(|field| (field.label.clone(), field.ty)) - .collect::>(), - _ => { - unreachable!("walk_to_disj_decl_with_subst returned a non-Disj declaration") - } - }); - - for path in &b.paths { - let Some(binding) = &path.binding else { - continue; - }; - let span = branch_payload_binding_span(path, &b.span); - let result = match &variants { - None => Err(Diagnostic::ResolveError { - name: format!( - "cannot bind payload `{}` because the match scrutinee does not walk to a Disj type", - binding.binding_name - ), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }), - Some(variants) => { - let variant = match &path.pattern { - crate::dag::BranchPattern::ResolvedVariant(id) => { - variants.iter().find(|(_, ty)| *ty == *id) - } - crate::dag::BranchPattern::UnresolvedVariant { name, .. } => { - variants.iter().find(|(label, _)| label == name) - } - }; - match variant { - Some((variant_name, variant_id)) => resolve_payload_binding_type( - dag, - *variant_id, - &subst, - variant_name, - &binding.binding_name, - &span, - ), - None => { - let arm_name = match &path.pattern { - crate::dag::BranchPattern::ResolvedVariant(id) => variants - .iter() - .find(|(_, ty)| *ty == *id) - .map(|(label, _)| label.clone()) - .unwrap_or_else(|| format!("declaration#{}", id.raw())), - crate::dag::BranchPattern::UnresolvedVariant { name, .. } => { - name.clone() - } - }; - Err(Diagnostic::ResolveError { - name: format!( - "variant `{arm_name}` is not a constructor of this match's scrutinee type" - ), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }) - } - } - } - }; - rewrites.push(BindingRewrite { - port: binding.payload_port, - span, - result, - }); - } - } - } - } - - let mut changed = false; - for rewrite in rewrites { - match rewrite.result { - Ok(resolution) => { - let ty = match resolution { - PayloadBindingResolution::Direct(ty) => ty, - PayloadBindingResolution::SpecializedRecord { - variant_decl_id, - subst, - } => materialize_specialized_payload_record(dag, variant_decl_id, &subst), - }; - match dag.port(rewrite.port).state().clone() { - PortState::Unresolved => {} - PortState::Uninferred => { - dag.set_port_type(rewrite.port, ty); - changed = true; - } - PortState::Resolved(existing) - if type_shapes_equivalent(dag, &existing, &ty) => {} - PortState::Resolved(existing) => { - dag.mark_unresolved( - rewrite.port, - Diagnostic::TypeMismatch { - expected: existing, - actual: ty, - span: rewrite.span, - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }, - ); - changed = true; - } - } - } - Err(diag) => { - if !matches!(dag.port(rewrite.port).state(), PortState::Unresolved) { - dag.mark_unresolved(rewrite.port, diag); - changed = true; - } - } - } - } - - changed -} - -/// T-Modeling int-lit / PR #806 consumer: allow `i64` default-typed int -/// literal output ports to refine to a range-backed fixed-width `IntegerPrimitive` -/// when the magnitude fits, without treating it as a conflicting resolution. -#[derive(Debug)] -enum IntLiteralNarrowMerge { - /// Narrow to the fixed-width type — caller should `set_port_type` to `to`. - Merge, - /// Not a default-`Int` literal → fixed-width call-site narrow case. - NotApplicable, - /// Preserve typed diagnostics (e.g. [`Diagnostic::MagnitudeOutOfRange`]) like the - /// annotated-`let` value-node path, not a silent fallback to `TypeMismatch`. - Reject(Box), -} - -impl IntLiteralNarrowMerge { - fn reject(diagnostic: Diagnostic) -> Self { - Self::Reject(Box::new(diagnostic)) - } -} - -fn int_literal_magnitude_narrow_merge( - dag: &Dag, - port: PortId, - from: &TypeShape, - to: &TypeShape, -) -> IntLiteralNarrowMerge { - let Some(lit) = literal_bigint_at(dag, port) else { - return IntLiteralNarrowMerge::NotApplicable; - }; - let Some(int_shape) = dag.int_shape() else { - return IntLiteralNarrowMerge::NotApplicable; - }; - if !type_shapes_equivalent(dag, from, &int_shape) { - return IntLiteralNarrowMerge::NotApplicable; - } - let span = node_span_for_port(dag, port).unwrap_or_else(synthetic_span); - match int_literal_fits_expected_type(dag, &lit, to.declaration) { - Ok(Some(true)) => IntLiteralNarrowMerge::Merge, - Ok(Some(false)) => match integer_range_for_decl(dag, to.declaration) { - IntegerRangeLookup::Found(bound) => IntLiteralNarrowMerge::reject( - magnitude_out_of_range_for_interval(&lit, *to, bound, span), - ), - IntegerRangeLookup::Invalid(diag) => IntLiteralNarrowMerge::reject(diag), - IntegerRangeLookup::Missing => { - IntLiteralNarrowMerge::reject(Diagnostic::ResolveError { - name: "(internal: integer literal out of range but no range fact)".to_string(), - span, - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }) - } - }, - Err(diag) => IntLiteralNarrowMerge::reject(diag), - Ok(None) => IntLiteralNarrowMerge::NotApplicable, - } -} - -enum Decision { - Set(PortId, TypeShape), - Fail(PortId, Box), - Retry, -} - -impl Decision { - fn fail(port: PortId, diagnostic: Diagnostic) -> Self { - Self::Fail(port, Box::new(diagnostic)) - } -} - -fn decide(dag: &mut Dag, index: usize) -> Decision { - // Clone before matching so `decide_transform` can take `&mut Dag` without - // holding an immutable borrow of `dag.nodes()` across the call. - let behavior = dag.nodes()[index].clone(); - match &behavior { - Behavior::Value(v) => { - // `let x: UInt8 = 5` seeds the value port to `UInt8` after lowering; - // the Int literal Value node would otherwise re-stamp `Int64` here and - // clobber host narrowing. If the port is already a non-`Int` type, only - // defer (Retry) when range facts prove the literal fits that type. - // Otherwise fail closed with `MagnitudeOutOfRange` (same contract as - // call-site narrowing) or fall through so normal mismatch handling - // applies when there is no range-backed check. - if let LiteralBits::Int(lit_str) = &v.data { - let Some(literal) = BigInt::from_str(lit_str.as_str()).ok() else { - return Decision::fail( - v.output, - Diagnostic::ResolveError { - name: "internal: malformed decimal integer literal".to_string(), - span: v.span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }, - ); - }; - if let PortState::Resolved(existing) = dag.port(v.output).state() { - if let Some(int_sh) = dag.int_shape() { - if !type_shapes_equivalent(dag, existing, &int_sh) { - match int_literal_fits_expected_type( - dag, - &literal, - existing.declaration, - ) { - Ok(Some(true)) => return Decision::Retry, - Ok(Some(false)) => { - match integer_range_for_decl(dag, existing.declaration) { - IntegerRangeLookup::Found(range) => { - return Decision::fail( - v.output, - magnitude_out_of_range_for_interval( - &literal, - *existing, - range, - v.span.clone(), - ), - ); - } - IntegerRangeLookup::Invalid(diag) => { - return Decision::fail(v.output, diag); - } - IntegerRangeLookup::Missing => { - return Decision::fail( - v.output, - Diagnostic::ResolveError { - name: "(internal: integer literal out of range but no range fact)" - .to_string(), - span: v.span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }, - ); - } - } - } - Err(diag) => return Decision::fail(v.output, diag), - Ok(None) => {} - } - } - } - } - } - let shape_and_name = match &v.data { - // Unconstrained integer literals keep the existing explicit - // default to Int64. Expected-type narrowing happens at - // reconciliation sites below, against range facts. - LiteralBits::Int(_) => (dag.int_shape(), "Int"), - LiteralBits::Bool(_) => (dag.bool_shape(), "Bool"), - LiteralBits::String(_) => (dag.string_shape(), "String"), - }; - let Some(ty) = shape_and_name.0 else { - return Decision::fail( - v.output, - Diagnostic::ResolveError { - name: format!( - "primitive `{}` missing from declaration table — bootstrap failed", - shape_and_name.1 - ), - span: v.span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }, - ); - }; - Decision::Set(v.output, ty) - } - Behavior::Transform(t) => decide_transform(dag, t), - Behavior::Branch(b) => { - // Branch dispatches on a sum type. Its input must - // walk through alias/instantiation edges to a - // declaration whose connective is - // `TypeConnective::Disj { .. }`. Bool and Classical are - // Disj declarations (True | False), so `if` lowers here - // naturally. User-defined sums like - // `type Sign = Plus | Minus` also pass. Aliased sums - // (`type Hue = Color` where Color is a sum) pass - // via `walk_to_disj_decl`. String, Int, Float, etc. - // fail this check — the previous M0 "must be Bool" - // rule was the degenerate case of this broader rule. - match dag.port(b.input).state() { - PortState::Uninferred => return Decision::Retry, - PortState::Unresolved => { - return Decision::fail( - b.output, - Diagnostic::ResolveError { - name: "(upstream failure in branch condition)".to_string(), - span: b.span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }, - ); - } - PortState::Resolved(ty) => { - if walk_to_disj_decl(dag, ty.declaration).is_none() - && walk_to_optional_cardinality_decl(dag, ty.declaration).is_none() - { - if is_retryable_generic_decl(dag, ty.declaration) { - return Decision::Retry; - } - let Some(bool_ty) = dag.bool_shape() else { - return Decision::fail( - b.output, - Diagnostic::ResolveError { - name: "primitive `Bool` missing from declaration table — bootstrap failed" - .to_string(), - span: b.span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }, - ); - }; - return Decision::fail( - b.output, - Diagnostic::TypeMismatch { - expected: bool_ty, - actual: *ty, - span: b.span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }, - ); - } - } - } - - let mut iter = b.paths.iter(); - let Some(first_path) = iter.next() else { - return Decision::Retry; - }; - let first_type = match dag.port(first_path.output).state() { - PortState::Uninferred => return Decision::Retry, - PortState::Unresolved => { - return Decision::fail( - b.output, - Diagnostic::ResolveError { - name: "(upstream failure in branch path)".to_string(), - span: b.span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }, - ); - } - PortState::Resolved(t) => *t, - }; - for path in iter { - match dag.port(path.output).state() { - PortState::Uninferred => return Decision::Retry, - PortState::Unresolved => { - return Decision::fail( - b.output, - Diagnostic::ResolveError { - name: "(upstream failure in branch path)".to_string(), - span: b.span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }, - ); - } - PortState::Resolved(other) - if type_shapes_equivalent(dag, other, &first_type) => {} - PortState::Resolved(other) => { - return Decision::fail( - b.output, - Diagnostic::TypeMismatch { - expected: first_type, - actual: *other, - span: b.span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }, - ); - } - } - } - Decision::Set(b.output, first_type) - } - Behavior::Loop(l) => { - if l.body == l.id { - return Decision::Retry; - } - let body_node = dag.node(l.body); - let body_output = behavior_output_port(body_node); - match dag.port(body_output).state() { - PortState::Uninferred => Decision::Retry, - PortState::Unresolved => Decision::fail( - l.output, - Diagnostic::ResolveError { - name: "function body is unresolved".to_string(), - span: l.span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }, - ), - PortState::Resolved(body_ty) => Decision::Set(l.output, *body_ty), - } - } - Behavior::Bind(_) => Decision::Retry, - } -} - -fn decide_transform(dag: &mut Dag, t: &TransformNode) -> Decision { - // Structural dispatch on `TransformTarget`. `Operator` carries the - // resolved `OperatorKind` directly and produces its signature from - // the LHS operand type (arithmetic returns operand type, comparison - // returns Bool). `Callable` points at a DeclarationId and walks - // `resolve_arrow` through any Instantiation / ResolvedIdentifier - // chain to recover the concrete Arrow signature. - let signature = match &t.target { - TransformTarget::UnresolvedFieldProject { field_label } => { - return decide_field_project(dag, t, field_label); - } - TransformTarget::ResolvedFieldProject { field_label } => { - return decide_field_project(dag, t, field_label); - } - TransformTarget::Operator(op_kind) => { - let lhs_type = match t.inputs.first() { - None => { - return Decision::fail( - t.output, - Diagnostic::ArityMismatch { - function: crate::operators::symbol(*op_kind), - expected: 2, - actual: 0, - span: t.span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }, - ); - } - Some(port) => match dag.port(*port).state() { - PortState::Uninferred => return Decision::Retry, - PortState::Unresolved => { - return Decision::fail( - t.output, - Diagnostic::ResolveError { - name: format!( - "(upstream failure in {})", - crate::operators::symbol(*op_kind) - ), - span: t.span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }, - ); - } - PortState::Resolved(ty) => *ty, - }, - }; - if is_retryable_generic_decl(dag, lhs_type.declaration) { - return Decision::Retry; - } - match resolve_operator_arrow(dag, *op_kind, &lhs_type) { - Some(sig) => sig, - None => { - if is_retryable_generic_decl(dag, lhs_type.declaration) { - return Decision::Retry; - } - return Decision::fail( - t.output, - Diagnostic::ResolveError { - name: format!( - "cannot dispatch operator `{}` on {lhs_type:?}", - crate::operators::symbol(*op_kind) - ), - span: t.span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }, - ); - } - } - } - TransformTarget::Callable(target_id) => { - match resolve_callable_target(dag, *target_id, &t.inputs, &t.span) { - CallableTargetResolution::Retry => return Decision::Retry, - CallableTargetResolution::Fail(diag) => return Decision::fail(t.output, diag), - CallableTargetResolution::Resolved { signature, .. } => signature, - } - } - }; - - // Arrow-body state check. The five variants partition into the - // "check something" cases (UserDefined → Bind.value port state, - // ExternalRealization → realization shape) and the "no body to - // walk" cases (Pending/NoBody/Unparsed). Each failure surfaces as - // a fail-closed diagnostic on the call site's output port. - match &signature.body { - ArrowBody::UserDefined(bind_id) => { - // User function: the call site's signature is only trustworthy - // once the function's Bind.value port has reached Resolved. - // Uninferred → Retry; Unresolved → fail the call site. - match dag.port((*bind_id).bind(dag).value).state() { - PortState::Uninferred => return Decision::Retry, - PortState::Unresolved => { - let name = transform_target_display_name(dag, &t.target); - return Decision::fail( - t.output, - Diagnostic::ResolveError { - name: format!("function `{name}` has an invalid body"), - span: t.span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }, - ); - } - PortState::Resolved(_) => {} - } - } - ArrowBody::ExternalRealization(realization_id) => { - // Primitive realization: the target declaration must be a - // Conj whose `meta_tag` edge points at the cached - // `Realization` meta-type (populated at bootstrap). The - // typed-edge check was asserted at bootstrap construction - // (`assert_realization_shape`); re-validating here catches - // drift from any future mutation path that bypasses the - // construction-time invariant. - if !is_realization_shape(dag, *realization_id) { - let name = transform_target_display_name(dag, &t.target); - return Decision::fail( - t.output, - Diagnostic::ResolveError { - name: format!( - "arrow `{name}` carries an ExternalRealization body whose target is not a realization declaration" - ), - span: t.span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }, - ); - } - } - ArrowBody::Pending | ArrowBody::NoBody => { - // Both shapes mean "no executable body to walk." - // `Pending` is the remaining transient scaffold from - // `seed_function_signature` during lowering; `NoBody` - // is the terminal "no body by construction" form used - // by arrow types and other non-executable carriers. - // `decide_transform` treats them identically: signature - // inhabitance accepts; body walking is skipped. The - // variant distinction exists so - // `lens_structural_resolution` can treat any surviving - // `Arrow(Pending)` in the final Dag as an R13-class body- - // patching regression. - } - ArrowBody::Unparsed(_) => { - // Surface-grammar scaffold: signature type-checks, - // body is not yet parseable under the M1(2.7) grammar. - // Callers can dispatch against the signature; the body - // source span is preserved for M2+ parser extensions. - // Dissolves when block bodies become fully parseable. - } - } - - if signature.inputs.len() != t.inputs.len() { - return Decision::fail( - t.output, - Diagnostic::ArityMismatch { - function: transform_target_display_name(dag, &t.target), - expected: signature.inputs.len(), - actual: t.inputs.len(), - span: t.span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }, - ); - } - for (input_port, expected_ty) in t.inputs.iter().zip(signature.inputs.iter()) { - match dag.port(*input_port).state() { - PortState::Uninferred => return Decision::Retry, - PortState::Unresolved => { - return Decision::fail( - t.output, - Diagnostic::ResolveError { - name: format!( - "(upstream failure in {})", - transform_target_display_name(dag, &t.target) - ), - span: t.span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }, - ); - } - PortState::Resolved(actual) => { - let types_equivalent = type_shapes_equivalent(dag, actual, expected_ty); - if !types_equivalent - && (is_retryable_generic_decl(dag, actual.declaration) - || is_retryable_generic_decl(dag, expected_ty.declaration)) - { - return Decision::Retry; - } - if let Some(diag) = phantom_unit_mismatch( - dag, - transform_target_display_name(dag, &t.target), - expected_ty, - actual, - &t.span, - ) { - return Decision::fail(t.output, diag); - } - if !types_equivalent { - if let Some(literal) = literal_bigint_at(dag, *input_port) { - match int_literal_fits_expected_type(dag, &literal, expected_ty.declaration) - { - Ok(Some(true)) => { - if let Some(diag) = check_refinement_discharge( - dag, - actual, - expected_ty, - &t.target, - &t.span, - ) { - return Decision::fail(t.output, diag); - } - // Default literal shape is `Int`; align the argument port with - // the callee's narrow range-backed type (same as `let` / `data` - // pre-seed + `Decision::Set` reunion for annotated literals). - return Decision::Set(*input_port, *expected_ty); - } - Ok(Some(false)) | Ok(None) => {} - Err(diag) => return Decision::fail(t.output, diag), - } - match integer_range_for_decl(dag, expected_ty.declaration) { - IntegerRangeLookup::Found(range) => { - return Decision::fail( - *input_port, - magnitude_out_of_range_for_interval( - &literal, - *expected_ty, - range, - t.span.clone(), - ), - ); - } - IntegerRangeLookup::Invalid(diag) => { - return Decision::fail(t.output, diag); - } - IntegerRangeLookup::Missing => {} - } - } - return Decision::fail( - t.output, - Diagnostic::TypeMismatch { - expected: *expected_ty, - actual: *actual, - span: t.span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }, - ); - } - // DB-11 (3a.3) refinement discharge. Structural type - // equivalence just passed; now check that any refinement - // declared on the callee's parameter type is also carried - // by the argument's port type, with structurally-equal - // predicate expression DAGs. No SMT, no implication — if - // the predicates don't walk equal, the caller must - // introduce a narrowing Branch that does satisfy the - // expected refinement. - if let Some(diag) = - check_refinement_discharge(dag, actual, expected_ty, &t.target, &t.span) - { - return Decision::fail(t.output, diag); - } - } - } - } - Decision::Set(t.output, signature.output) -} - -fn phantom_unit_mismatch( - dag: &Dag, - context: String, - expected_ty: &TypeShape, - actual_ty: &TypeShape, - span: &SourceSpan, -) -> Option { - let expected_inst = instantiation_parts(dag, expected_ty.declaration)?; - let actual_inst = instantiation_parts(dag, actual_ty.declaration)?; - let expected_template = dag.declaration(expected_inst.template); - for phantom in &expected_template.phantom_params { - if let Err(diag) = - require_abelian_group_phantom_algebra(dag, phantom.algebra, phantom.parameter, span) - { - return Some(diag); - } - if !expected_template.type_params.contains(&phantom.parameter) { - return Some(malformed_phantom_parameter_diagnostic( - dag, - expected_inst.template, - phantom.parameter, - span, - )); - } - let Some(actual_phantom) = - mapped_phantom_parameter(dag, expected_inst.template, phantom, actual_inst.template) - else { - continue; - }; - if let Err(diag) = require_abelian_group_phantom_algebra( - dag, - actual_phantom.algebra, - actual_phantom.parameter, - span, - ) { - return Some(diag); - } - if !phantom_algebras_equivalent(dag, phantom.algebra, actual_phantom.algebra) { - return Some(unsupported_phantom_algebra_diagnostic( - dag, - actual_phantom.algebra, - actual_phantom.parameter, - span, - )); - } - let Some(expected_arg) = expected_inst - .arguments - .iter() - .find(|arg| arg.parameter == phantom.parameter) - else { - return Some(missing_phantom_argument_diagnostic( - dag, - expected_inst.template, - phantom.parameter, - "expected", - span, - )); - }; - let Some(actual_arg) = actual_inst - .arguments - .iter() - .find(|arg| arg.parameter == actual_phantom.parameter) - else { - return Some(missing_phantom_argument_diagnostic( - dag, - actual_inst.template, - actual_phantom.parameter, - "actual", - span, - )); - }; - if expected_arg.value != actual_arg.value { - return Some(Diagnostic::UnitMismatch { - operator: context, - parameter: phantom_parameter_display_name(dag, phantom.parameter), - expected: TypeShape::new(expected_arg.value), - actual: TypeShape::new(actual_arg.value), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }); - } - } - None -} - -fn malformed_phantom_parameter_diagnostic( - dag: &Dag, - template: DeclarationId, - parameter: DeclarationId, - span: &SourceSpan, -) -> Diagnostic { - Diagnostic::ResolveError { - name: format!( - "malformed phantom parameter {} on {}: parameter is not in type_params", - phantom_parameter_display_name(dag, parameter), - declaration_display_name(dag, template), - ), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - } -} - -fn missing_phantom_argument_diagnostic( - dag: &Dag, - template: DeclarationId, - parameter: DeclarationId, - side: &str, - span: &SourceSpan, -) -> Diagnostic { - Diagnostic::ResolveError { - name: format!( - "malformed phantom instantiation for {}: missing {side} argument for phantom parameter {}", - declaration_display_name(dag, template), - phantom_parameter_display_name(dag, parameter), - ), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - } -} - -fn unsupported_phantom_algebra_diagnostic( - dag: &Dag, - algebra: DeclarationId, - parameter: DeclarationId, - span: &SourceSpan, -) -> Diagnostic { - Diagnostic::ResolveError { - name: format!( - "unsupported phantom algebra {} for phantom parameter {}", - declaration_display_name(dag, algebra), - phantom_parameter_display_name(dag, parameter), - ), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - } -} - -fn require_abelian_group_phantom_algebra( - dag: &Dag, - algebra: DeclarationId, - parameter: DeclarationId, - span: &SourceSpan, -) -> Result<(), Diagnostic> { - let Some(canonical_abelian_group) = dag.abelian_group_decl() else { - return Err(unsupported_phantom_algebra_diagnostic( - dag, algebra, parameter, span, - )); - }; - let mut current = algebra; - for _ in 0..WALK_DEPTH_LIMIT { - let decl = dag.declaration(current); - if current == canonical_abelian_group { - return Ok(()); - } - match &decl.connective { - TypeConnective::Instantiation { template, .. } => { - current = *template; - } - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - current = *next; - } - _ => { - return Err(unsupported_phantom_algebra_diagnostic( - dag, algebra, parameter, span, - )); - } - } - } - Err(unsupported_phantom_algebra_diagnostic( - dag, algebra, parameter, span, - )) -} - -fn phantom_algebras_equivalent( - dag: &Dag, - lhs_algebra: DeclarationId, - rhs_algebra: DeclarationId, -) -> bool { - lhs_algebra == rhs_algebra || declaration_shapes_equivalent(dag, lhs_algebra, rhs_algebra, 0) -} - -fn mapped_phantom_parameter( - dag: &Dag, - source_template: DeclarationId, - source_phantom: &PhantomParameter, - target_template: DeclarationId, -) -> Option { - let source_decl = dag.declaration(source_template); - let target_decl = dag.declaration(target_template); - let source_index = source_decl - .type_params - .iter() - .position(|parameter| *parameter == source_phantom.parameter)?; - let target_parameter = target_decl.type_params.get(source_index).copied()?; - target_decl - .phantom_params - .iter() - .find(|target_phantom| target_phantom.parameter == target_parameter) - .cloned() -} - -struct InstantiationParts<'a> { - template: DeclarationId, - arguments: &'a [crate::dag::TemplateArgument], -} - -fn instantiation_parts(dag: &Dag, declaration: DeclarationId) -> Option> { - match &dag.declaration(declaration).connective { - TypeConnective::Instantiation { - template, - arguments, - } => Some(InstantiationParts { - template: *template, - arguments, - }), - _ => None, - } -} - -fn phantom_parameter_display_name(dag: &Dag, parameter: DeclarationId) -> String { - match &dag.declaration(parameter).connective { - TypeConnective::Atom(AtomPayload::TypeParam(name)) => name.clone(), - _ => declaration_display_name(dag, parameter), - } -} - -/// DB-11 (3a.3) call-site refinement discharge. Returns `None` when -/// the expected parameter has no refinement (unconditionally OK), or -/// when the actual argument's top-level refinement discharges the -/// expected predicate. Returns a `Diagnostic` otherwise. Pure -/// structural walk — no SMT, no entailment beyond conjunction-member -/// matching against the canonical composite form. -/// -/// Composite-canonical invariant (post-narrowing refactor): every -/// refinement in the DAG is a single predicate Declaration. Narrowing -/// produces a composite `outer_body && new_body` rather than an alias -/// chain of unary refinements, so discharge only has to look at the -/// top-level refinement on `actual.declaration` — no chain walk. The -/// composite-vs-single asymmetry is handled by -/// `predicate_discharges`, which accepts either a full structural -/// match against the expected predicate or a match against a -/// conjunct of `actual`'s composite body. -fn check_refinement_discharge( - dag: &Dag, - actual: &TypeShape, - expected: &TypeShape, - target: &TransformTarget, - span: &SourceSpan, -) -> Option { - let expected_pred = dag.declaration(expected.declaration).refinement?; - let actual_pred = dag.declaration(actual.declaration).refinement; - match actual_pred { - Some(actual_pred) if predicate_discharges(dag, actual_pred, expected_pred, 0) => None, - _ => { - let callee = transform_target_display_name(dag, target); - let name = if actual_pred.is_some() { - format!( - "argument to `{callee}` does not satisfy the expected \ - `where` refinement (caller's predicate does not walk \ - structurally equal to the callee's, and no conjunct \ - of the caller's composite matches)" - ) - } else { - format!( - "argument to `{callee}` does not satisfy the expected \ - `where` refinement — no narrowing branch in scope" - ) - }; - Some(Diagnostic::ResolveError { - name, - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }) - } - } -} - -/// Structural discharge check between an actual and expected -/// predicate declaration. Succeeds when: -/// -/// - The two predicates are the same declaration id; OR -/// - The actual predicate's body walks structurally equal to the -/// expected predicate's body (param-paired); OR -/// - The actual predicate's body is a `Transform(Logical(And), [a, b])` -/// and either `a` or `b` (as a virtual sub-predicate sharing the -/// actual's `Bind.params[0]`) discharges the expected predicate. -/// -/// The composite conjunct case is the load-bearing piece that lets -/// the post-narrowing composite `outer_body && new_body` match a -/// callee whose refinement is just `outer_body`. Pure structural, -/// depth-bounded. -fn predicate_discharges( - dag: &Dag, - actual_pred_decl: DeclarationId, - expected_pred_decl: DeclarationId, - depth: usize, -) -> bool { - if depth >= WALK_DEPTH_LIMIT { - return false; - } - if actual_pred_decl == expected_pred_decl { - return true; - } - let Some((actual_param, actual_body)) = predicate_info(dag, actual_pred_decl) else { - return false; - }; - let Some((expected_param, expected_body)) = predicate_info(dag, expected_pred_decl) else { - return false; - }; - body_discharges( - dag, - actual_body, - expected_body, - actual_param, - expected_param, - ) -} - -/// Flatten-and-subset discharge check. A predicate body is treated -/// as a CONJUNCTION OF LEAVES — the maximal multiset of non-`And` -/// operand ports reachable by recursively unfolding every -/// `Transform(Logical(And), [lhs, rhs])` root. Actual discharges -/// expected iff every expected leaf is walk-equal (param-paired) to -/// some actual leaf. -/// -/// This makes conjunction associativity / grouping irrelevant to -/// discharge: `a && (b && c)`, `(a && b) && c`, and `a && b && c` -/// all flatten to the same leaf set `{a, b, c}` and therefore -/// discharge each other symmetrically. Without the flattening, -/// `body_discharges` could only recurse into one root conjunct of -/// the actual side at a time, so an asymmetric grouping would fail -/// to discharge a syntax-equivalent callee contract (the -/// substrate-level blocker ChatGPT's R5 review called out on -/// `31a3709d`). -/// -/// Still pure structural — no SMT, no absorption laws, no ordering -/// reasoning. The "canonical form" is the leaf multiset; two bodies -/// representing the same conjunction share one leaf multiset up to -/// reordering, and the subset check tolerates the multiset shape. -fn body_discharges( - dag: &Dag, - actual_body: PortId, - expected_body: PortId, - actual_param: PortId, - expected_param: PortId, -) -> bool { - let mut actual_leaves: Vec = Vec::new(); - collect_conjunct_leaves(dag, actual_body, &mut actual_leaves, 0); - let mut expected_leaves: Vec = Vec::new(); - collect_conjunct_leaves(dag, expected_body, &mut expected_leaves, 0); - expected_leaves.into_iter().all(|expected_leaf| { - actual_leaves.iter().any(|&actual_leaf| { - refinement_ports_equal( - dag, - actual_leaf, - expected_leaf, - actual_param, - expected_param, - 0, - ) - }) - }) -} - -/// Recursively unfold `Transform(Logical(And), [lhs, rhs])` roots -/// and collect the non-`And` leaves. Depth-bounded by -/// `WALK_DEPTH_LIMIT`; on overflow the current port is pushed as a -/// leaf so discharge degrades to the pre-flatten shape rather than -/// silently dropping conjuncts. -fn collect_conjunct_leaves(dag: &Dag, body_port: PortId, out: &mut Vec, depth: usize) { - if depth >= WALK_DEPTH_LIMIT { - out.push(body_port); - return; - } - if let Some(node_id) = dag.port(body_port).produced_by { - if let Behavior::Transform(t) = dag.node(node_id) { - if matches!( - &t.target, - TransformTarget::Operator(OperatorKind::Logical(LogicalOp::And)) - ) && t.inputs.len() == 2 - { - collect_conjunct_leaves(dag, t.inputs[0], out, depth + 1); - collect_conjunct_leaves(dag, t.inputs[1], out, depth + 1); - return; - } - } - } - out.push(body_port); -} - -fn predicate_info(dag: &Dag, pred_decl: DeclarationId) -> Option<(PortId, PortId)> { - if let TypeConnective::Arrow { - body: ArrowBody::UserDefined(bind_id), - .. - } = &dag.declaration(pred_decl).connective - { - let bind = (*bind_id).bind(dag); - if let Some(param_port) = bind.params.first() { - return Some((*param_port, bind.value)); - } - } - None -} - -fn refinement_ports_equal( - dag: &Dag, - lhs_port: PortId, - rhs_port: PortId, - lhs_param: PortId, - rhs_param: PortId, - depth: usize, -) -> bool { - if depth >= WALK_DEPTH_LIMIT { - return false; - } - let lhs_is_param = lhs_port == lhs_param; - let rhs_is_param = rhs_port == rhs_param; - if lhs_is_param || rhs_is_param { - return lhs_is_param && rhs_is_param; - } - let Some(lhs_nid) = dag.port(lhs_port).produced_by else { - return lhs_port == rhs_port; - }; - let Some(rhs_nid) = dag.port(rhs_port).produced_by else { - return false; - }; - match (dag.node(lhs_nid), dag.node(rhs_nid)) { - (Behavior::Value(lv), Behavior::Value(rv)) => lv.data == rv.data, - (Behavior::Transform(lt), Behavior::Transform(rt)) => { - refinement_targets_equal(dag, <.target, &rt.target) - && lt.inputs.len() == rt.inputs.len() - && lt.inputs.iter().zip(rt.inputs.iter()).all(|(l, r)| { - refinement_ports_equal(dag, *l, *r, lhs_param, rhs_param, depth + 1) - }) - } - _ => false, - } -} - -fn refinement_targets_equal(dag: &Dag, lhs: &TransformTarget, rhs: &TransformTarget) -> bool { - match (lhs, rhs) { - // DB-11 (3a.3) structural callable identity. Call lowering - // materializes a fresh `Instantiation` declaration per - // call-site when the callee has retained template arguments - // (see `retained_template_arguments_for_target` in lower.rs). - // Two syntactically identical calls to a generic predicate - // (e.g., `is_eq(d, 0)`) from two different refinement - // contexts therefore carry different `Callable(DeclarationId)` - // targets even though their template + argument values match. - // Nominal id equality would reject those as distinct; the - // authoritative identity is template + substituted arguments, - // which `declaration_shapes_equivalent` already walks through - // `Instantiation`/`ResolvedIdentifier` edges. - (TransformTarget::Callable(a), TransformTarget::Callable(b)) => { - declaration_shapes_equivalent(dag, *a, *b, 0) - } - (TransformTarget::Operator(a), TransformTarget::Operator(b)) => a == b, - ( - TransformTarget::UnresolvedFieldProject { - field_label: a_label, - }, - TransformTarget::UnresolvedFieldProject { - field_label: b_label, - }, - ) => a_label == b_label, - ( - TransformTarget::ResolvedFieldProject { - field_label: a_label, - }, - TransformTarget::ResolvedFieldProject { - field_label: b_label, - }, - ) => a_label == b_label, - _ => false, - } -} - -struct ResolvedArrow { - inputs: Vec, - output: TypeShape, - body: ArrowBody, -} - -struct ResolvedArrowDecl { - inputs: Vec, - output: DeclarationId, -} - -#[derive(Clone)] -struct PortTypeContext { - decl: DeclarationId, - subst: SubstStack, -} - -struct CallableSignatureContext { - inputs: Vec, - output: PortTypeContext, -} - -enum CallableTargetResolution { - Retry, - Fail(Diagnostic), - Resolved { - template: DeclarationId, - arguments: Vec, - signature: ResolvedArrow, - }, -} - -enum CallableBindingResolution { - Resolved, - Retry, - Conflict, -} - -/// Lazy substitution stack for `Instantiation` walks. When inference -/// descends into an `Instantiation { template, arguments }`, it pushes -/// `arguments` onto this stack; when a downstream `TypeParam` reference -/// is encountered, the stack is consulted top-down to find the bound -/// `DeclarationId`. Pop on Instantiation exit keeps the stack balanced. -/// See M1_DESIGN.md §4 Q4 / §5 for the walk semantics. -#[derive(Clone)] -pub(crate) struct SubstStack { - frames: Vec>, -} - -impl SubstStack { - pub(crate) fn new() -> Self { - Self { frames: Vec::new() } - } - - pub(crate) fn push(&mut self, args: Vec) { - self.frames.push(args); - } - - pub(crate) fn pop(&mut self) { - self.frames.pop(); - } - - pub(crate) fn lookup(&self, param_id: DeclarationId) -> Option { - for frame in self.frames.iter().rev() { - for arg in frame { - if arg.parameter == param_id { - if arg.value == param_id { - return None; - } - return Some(arg.value); - } - } - } - None - } -} - -enum PayloadBindingResolution { - Direct(TypeShape), - SpecializedRecord { - variant_decl_id: DeclarationId, - subst: SubstStack, - }, -} - -fn declaration_is_callable(dag: &Dag, current: DeclarationId, depth: usize) -> bool { - if depth >= WALK_DEPTH_LIMIT { - return false; - } - match &dag.declaration(current).connective { - TypeConnective::Arrow { .. } => true, - TypeConnective::Instantiation { template, .. } => { - declaration_is_callable(dag, *template, depth + 1) - } - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - declaration_is_callable(dag, *next, depth + 1) - } - TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(_)) - | TypeConnective::Atom(AtomPayload::TypeParam(_)) - | TypeConnective::Atom(AtomPayload::Literal(_)) - | TypeConnective::Conj { .. } - | TypeConnective::Disj { .. } - | TypeConnective::Cardinality(_) => false, - } -} - -fn is_retryable_generic_decl(dag: &Dag, current: DeclarationId) -> bool { - let mut visiting = Vec::new(); - is_retryable_generic_decl_walk(dag, current, 0, &mut visiting) -} - -fn is_retryable_generic_decl_walk( - dag: &Dag, - current: DeclarationId, - depth: usize, - visiting: &mut Vec, -) -> bool { - if depth >= WALK_DEPTH_LIMIT { - return false; - } - if visiting.contains(¤t) { - return false; - } - visiting.push(current); - let retryable = match &dag.declaration(current).connective { - TypeConnective::Atom(AtomPayload::TypeParam(_)) => true, - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - is_retryable_generic_decl_walk(dag, *next, depth + 1, visiting) - } - TypeConnective::Instantiation { arguments, .. } => arguments - .iter() - .any(|arg| is_retryable_generic_decl_walk(dag, arg.value, depth + 1, visiting)), - TypeConnective::Cardinality(p) => { - is_retryable_generic_decl_walk(dag, p.element(), depth + 1, visiting) - } - TypeConnective::Conj { children } => children - .iter() - .any(|field| is_retryable_generic_decl_walk(dag, field.ty, depth + 1, visiting)), - TypeConnective::Disj { variants } => variants - .iter() - .any(|field| is_retryable_generic_decl_walk(dag, field.ty, depth + 1, visiting)), - TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(_)) - | TypeConnective::Atom(AtomPayload::Literal(_)) - | TypeConnective::Arrow { .. } => false, - }; - visiting.pop(); - retryable -} - -fn callable_template_arguments( - dag: &Dag, - target: DeclarationId, -) -> (DeclarationId, Vec) { - match &dag.declaration(target).connective { - TypeConnective::Instantiation { - template, - arguments, - } => (*template, arguments.clone()), - _ => (target, Vec::new()), - } -} - -fn template_argument_value( - arguments: &[TemplateArgument], - parameter: DeclarationId, -) -> Option { - match generated_template_argument_value(arguments, ¶meter) { - Lookup::Hit(value) => Some(value), - Lookup::Miss => None, - } -} - -fn resolve_template_argument_value( - arguments: &[TemplateArgument], - current: DeclarationId, - depth: usize, -) -> DeclarationId { - generated_resolve_template_argument_value( - &(WALK_DEPTH_LIMIT.saturating_sub(depth) as i64), - arguments, - current, - ) -} - -fn retained_template_arguments_for_target( - dag: &Dag, - template: DeclarationId, - arguments: &[TemplateArgument], -) -> Vec { - let mut allowed: HashSet = dag - .declaration(template) - .type_params - .iter() - .copied() - .collect(); - if let Some(enclosing_disj) = enclosing_disj_for_variant(dag, template) { - allowed.extend(dag.declaration(enclosing_disj).type_params.iter().copied()); - } - if let Some(raw_arrow) = resolve_arrow_decl_walk(dag, template, &mut SubstStack::new(), 0) { - for input in raw_arrow.inputs { - if declaration_is_callable(dag, input, 0) { - allowed.insert(input); - } - } - } - - let mut retained: Vec = Vec::new(); - for argument in arguments { - if !allowed.contains(&argument.parameter) { - continue; - } - let resolved_value = resolve_template_argument_value(arguments, argument.value, 0); - if let Some(existing) = retained - .iter_mut() - .find(|existing| existing.parameter == argument.parameter) - { - existing.value = resolved_value; - continue; - } - retained.push(TemplateArgument { - parameter: argument.parameter, - value: resolved_value, - }); - } - retained -} - -fn push_template_argument_binding( - arguments: &mut Vec, - parameter: DeclarationId, - value: DeclarationId, -) -> bool { - match generated_push_template_argument_binding(arguments, ¶meter, value) { - TemplateArgumentBinding::Conflict => false, - TemplateArgumentBinding::NoOp => true, - TemplateArgumentBinding::Append => { - arguments.push(TemplateArgument { parameter, value }); - true - } - TemplateArgumentBinding::ReplaceAt { - _0: index, - _1: updated, - } => { - let Some(existing) = arguments.get_mut(index as usize) else { - return false; - }; - existing.value = updated; - true - } - } -} - -fn resolve_arrow_decl_walk( - dag: &Dag, - current: DeclarationId, - subst: &mut SubstStack, - depth: usize, -) -> Option { - if depth >= WALK_DEPTH_LIMIT { - return None; - } - let decl = dag.declaration(current); - match &decl.connective { - TypeConnective::Arrow { inputs, output, .. } => Some(ResolvedArrowDecl { - inputs: inputs.clone(), - output: *output, - }), - TypeConnective::Instantiation { - template, - arguments, - } => { - subst.push(arguments.clone()); - let result = resolve_arrow_decl_walk(dag, *template, subst, depth + 1); - subst.pop(); - result - } - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - resolve_arrow_decl_walk(dag, *next, subst, depth + 1) - } - TypeConnective::Atom(AtomPayload::TypeParam(_)) => { - let bound = subst.lookup(current)?; - resolve_arrow_decl_walk(dag, bound, subst, depth + 1) - } - TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(_)) - | TypeConnective::Atom(AtomPayload::Literal(_)) - | TypeConnective::Conj { .. } - | TypeConnective::Disj { .. } - | TypeConnective::Cardinality(_) => None, - } -} - -fn port_type_context(dag: &Dag, port: PortId) -> Option { - let resolved_decl = match dag.port(port).state() { - PortState::Resolved(ty) => ty.declaration, - PortState::Uninferred | PortState::Unresolved => return None, - }; - let Some(produced_by) = dag.port(port).produced_by else { - return Some(PortTypeContext { - decl: resolved_decl, - subst: SubstStack::new(), - }); - }; - match dag.node(produced_by) { - // Use the port's resolved declaration, not the literal's default - // substrate type (`Int` for all int literals). After range narrowing the - // literal port may already be `UInt8` / etc.; `literal_decl_id`-style - // `Int` here makes `resolve_callable_target` think the argument is still - // the default `Int`, producing spurious implicit-template conflicts against - // a `UInt8` parameter (e.g. `id_u8(7)`). - Behavior::Value(_) => Some(PortTypeContext { - decl: resolved_decl, - subst: SubstStack::new(), - }), - Behavior::Transform(t) => match &t.target { - TransformTarget::Callable(target) => { - let CallableTargetResolution::Resolved { - template, - arguments, - .. - } = resolve_callable_target(dag, *target, &t.inputs, &t.span) - else { - return Some(PortTypeContext { - decl: resolved_decl, - subst: SubstStack::new(), - }); - }; - let mut subst = SubstStack::new(); - subst.push(arguments); - let Some(arrow) = resolve_arrow_decl_walk(dag, template, &mut subst, 0) else { - if let Some(enclosing_disj) = enclosing_disj_for_variant(dag, template) { - return Some(PortTypeContext { - decl: enclosing_disj, - subst, - }); - } - return Some(PortTypeContext { - decl: resolved_decl, - subst: SubstStack::new(), - }); - }; - Some(PortTypeContext { - decl: arrow.output, - subst, - }) - } - TransformTarget::UnresolvedFieldProject { field_label } - | TransformTarget::ResolvedFieldProject { field_label } => { - field_project_port_type_context(dag, t, field_label) - } - TransformTarget::Operator(_) => Some(PortTypeContext { - decl: resolved_decl, - subst: SubstStack::new(), - }), - }, - Behavior::Branch(_) | Behavior::Loop(_) | Behavior::Bind(_) => Some(PortTypeContext { - decl: resolved_decl, - subst: SubstStack::new(), - }), - } -} - -fn field_project_port_type_context( - dag: &Dag, - t: &TransformNode, - field_label: &str, -) -> Option { - if t.inputs.len() != 1 { - return None; - } - let input_ty = match dag.port(t.inputs[0]).state() { - PortState::Resolved(ty) => *ty, - PortState::Uninferred | PortState::Unresolved => return None, - }; - let mut subst = SubstStack::new(); - let conj_id = walk_to_conj_decl_with_subst(dag, input_ty.declaration, &mut subst)?; - let TypeConnective::Conj { children } = &dag.declaration(conj_id).connective else { - return None; - }; - let field_decl = children - .iter() - .find(|field| field.label == field_label) - .map(|field| field.ty)?; - Some(PortTypeContext { - decl: field_decl, - subst, - }) -} - -fn resolve_binding_decl( - dag: &Dag, - current: DeclarationId, - subst: &SubstStack, - depth: usize, -) -> Option { - if depth >= WALK_DEPTH_LIMIT { - return None; - } - match &dag.declaration(current).connective { - TypeConnective::Atom(AtomPayload::TypeParam(_)) => subst - .lookup(current) - .and_then(|bound| resolve_binding_decl(dag, bound, subst, depth + 1)) - .or_else(|| { - walk_to_type_shape(dag, current, subst, depth + 1).map(|ty| ty.declaration) - }), - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - resolve_binding_decl(dag, *next, subst, depth + 1) - } - _ => walk_to_type_shape(dag, current, subst, depth + 1).map(|ty| ty.declaration), - } -} - -fn callable_signature_context( - dag: &Dag, - callable: DeclarationId, -) -> Option { - let decl = dag.declaration(callable); - if let TypeConnective::Arrow { - inputs, - body: ArrowBody::UserDefined(bind_id), - .. - } = &decl.connective - { - let bind = (*bind_id).bind(dag); - if bind.params.len() < inputs.len() { - return None; - } - let start = bind.params.len() - inputs.len(); - let mut input_contexts = Vec::with_capacity(inputs.len()); - for port in &bind.params[start..] { - input_contexts.push(port_type_context(dag, *port)?); - } - return Some(CallableSignatureContext { - inputs: input_contexts, - output: port_type_context(dag, bind.value)?, - }); - } - - let (template, arguments) = callable_template_arguments(dag, callable); - let mut subst = SubstStack::new(); - subst.push(arguments); - let raw_arrow = resolve_arrow_decl_walk(dag, template, &mut subst, 0)?; - Some(CallableSignatureContext { - inputs: raw_arrow - .inputs - .into_iter() - .map(|decl| PortTypeContext { - decl, - subst: subst.clone(), - }) - .collect(), - output: PortTypeContext { - decl: raw_arrow.output, - subst, - }, - }) -} - -fn bind_expected_callable_to_actual( - dag: &Dag, - expected_callable: DeclarationId, - actual_callable: DeclarationId, - arguments: &mut Vec, -) -> CallableBindingResolution { - let mut expected_subst = SubstStack::new(); - expected_subst.push(arguments.clone()); - let Some(expected_arrow) = - resolve_arrow_decl_walk(dag, expected_callable, &mut expected_subst, 0) - else { - return CallableBindingResolution::Conflict; - }; - let Some(actual_signature) = callable_signature_context(dag, actual_callable) else { - return CallableBindingResolution::Retry; - }; - if expected_arrow.inputs.len() != actual_signature.inputs.len() { - return CallableBindingResolution::Conflict; - } - for (expected_input, actual_input) in expected_arrow - .inputs - .into_iter() - .zip(actual_signature.inputs.iter()) - { - if !bind_expected_decl_to_actual_context(dag, expected_input, actual_input, arguments, 0) { - return CallableBindingResolution::Conflict; - } - } - if !bind_expected_decl_to_actual_context( - dag, - expected_arrow.output, - &actual_signature.output, - arguments, - 0, - ) { - return CallableBindingResolution::Conflict; - } - CallableBindingResolution::Resolved -} - -fn bind_expected_decl_to_actual_context( - dag: &Dag, - expected: DeclarationId, - actual: &PortTypeContext, - arguments: &mut Vec, - depth: usize, -) -> bool { - if depth >= WALK_DEPTH_LIMIT { - return false; - } - - match &dag.declaration(expected).connective { - TypeConnective::Atom(AtomPayload::TypeParam(_)) => { - if let Some(bound) = template_argument_value(arguments, expected) { - if bound != expected { - let expected_ctx = PortTypeContext { - decl: bound, - subst: SubstStack::new(), - }; - return bind_expected_decl_to_actual_context( - dag, - expected_ctx.decl, - actual, - arguments, - depth + 1, - ); - } - } - let Some(value) = resolve_binding_decl(dag, actual.decl, &actual.subst, depth + 1) - else { - return false; - }; - push_template_argument_binding(arguments, expected, value) - } - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - bind_expected_decl_to_actual_context(dag, *next, actual, arguments, depth + 1) - } - TypeConnective::Instantiation { - template, - arguments: expected_args, - } => { - let actual_decl = match &dag.declaration(actual.decl).connective { - TypeConnective::Atom(AtomPayload::TypeParam(_)) => { - let Some(bound) = actual.subst.lookup(actual.decl) else { - return false; - }; - return bind_expected_decl_to_actual_context( - dag, - expected, - &PortTypeContext { - decl: bound, - subst: actual.subst.clone(), - }, - arguments, - depth + 1, - ); - } - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - return bind_expected_decl_to_actual_context( - dag, - expected, - &PortTypeContext { - decl: *next, - subst: actual.subst.clone(), - }, - arguments, - depth + 1, - ); - } - _ => actual.decl, - }; - let TypeConnective::Instantiation { - template: actual_template, - arguments: actual_args, - } = &dag.declaration(actual_decl).connective - else { - if actual_decl != *template { - return false; - } - let template_params = dag.declaration(*template).type_params.clone(); - if template_params.len() != expected_args.len() { - return false; - } - for (expected_arg, param_id) in expected_args.iter().zip(template_params) { - let Some(actual_value) = actual.subst.lookup(param_id) else { - return false; - }; - if !bind_expected_decl_to_actual_context( - dag, - expected_arg.value, - &PortTypeContext { - decl: actual_value, - subst: actual.subst.clone(), - }, - arguments, - depth + 1, - ) { - return false; - } - } - return true; - }; - if *actual_template == expected && expected_args.len() == actual_args.len() { - for (expected_arg, actual_arg) in expected_args.iter().zip(actual_args.iter()) { - if !bind_expected_decl_to_actual_context( - dag, - expected_arg.value, - &PortTypeContext { - decl: actual_arg.value, - subst: actual.subst.clone(), - }, - arguments, - depth + 1, - ) { - return false; - } - } - return true; - } - if template != actual_template || expected_args.len() != actual_args.len() { - return false; - } - for (expected_arg, actual_arg) in expected_args.iter().zip(actual_args.iter()) { - if !bind_expected_decl_to_actual_context( - dag, - expected_arg.value, - &PortTypeContext { - decl: actual_arg.value, - subst: actual.subst.clone(), - }, - arguments, - depth + 1, - ) { - return false; - } - } - true - } - TypeConnective::Cardinality(p) => { - let element = p.element(); - let bound = p.bound(); - let actual_decl = match &dag.declaration(actual.decl).connective { - TypeConnective::Atom(AtomPayload::TypeParam(_)) => { - let Some(bound) = actual.subst.lookup(actual.decl) else { - return false; - }; - return bind_expected_decl_to_actual_context( - dag, - expected, - &PortTypeContext { - decl: bound, - subst: actual.subst.clone(), - }, - arguments, - depth + 1, - ); - } - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - return bind_expected_decl_to_actual_context( - dag, - expected, - &PortTypeContext { - decl: *next, - subst: actual.subst.clone(), - }, - arguments, - depth + 1, - ); - } - _ => actual.decl, - }; - let TypeConnective::Cardinality(actual_p) = &dag.declaration(actual_decl).connective - else { - return false; - }; - if bound != actual_p.bound() { - return false; - } - bind_expected_decl_to_actual_context( - dag, - element, - &PortTypeContext { - decl: actual_p.element(), - subst: actual.subst.clone(), - }, - arguments, - depth + 1, - ) - } - _ => { - let Some(expected_ty) = - walk_to_type_shape(dag, expected, &SubstStack::new(), depth + 1) - else { - return false; - }; - let Some(actual_ty) = walk_to_type_shape(dag, actual.decl, &actual.subst, depth + 1) - else { - return false; - }; - expected_ty == actual_ty - } - } -} - -fn callable_instantiation_conflict( - dag: &Dag, - target: DeclarationId, - expected: DeclarationId, - actual: &PortTypeContext, - span: &SourceSpan, -) -> Diagnostic { - let expected_name = target_display_name(dag, expected); - let actual_name = resolve_binding_decl(dag, actual.decl, &actual.subst, 0) - .map(|id| target_display_name(dag, id)) - .unwrap_or_else(|| format!("declaration#{}", actual.decl.raw())); - Diagnostic::ResolveError { - name: format!( - "implicit template binding for `{}` conflicts while matching `{expected_name}` against `{actual_name}`", - target_display_name(dag, target) - ), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - } -} - -/// `port_type_context` reports the default `Int` type shape for integer literals, so -/// [`bind_expected_decl_to_actual_context`] can reject a callee that expects a range-backed -/// narrow type (`UInt8`, …). The main transform decision path already allows in-range -/// literal narrowing; mirror that here so [`resolve_callable_target`] can succeed. Out-of-range -/// literals surface as [`Diagnostic::MagnitudeOutOfRange`] to match -/// `decide(Transform)`-style call checks. -fn int_literal_implicit_bind_tolerated_for_expected( - dag: &Dag, - expected: DeclarationId, - input_port: PortId, - span: &SourceSpan, -) -> Result { - let Some(literal) = literal_bigint_at(dag, input_port) else { - return Ok(false); - }; - match int_literal_fits_expected_type(dag, &literal, expected) { - Ok(Some(true)) => Ok(true), - Ok(Some(false)) => match integer_range_for_decl(dag, expected) { - IntegerRangeLookup::Found(range) => Err(magnitude_out_of_range_for_interval( - &literal, - TypeShape::new(expected), - range, - span.clone(), - )), - IntegerRangeLookup::Invalid(diag) => Err(diag), - IntegerRangeLookup::Missing => Ok(false), - }, - Ok(None) => Ok(false), - Err(diag) => Err(diag), - } -} - -fn resolve_callable_target( - dag: &Dag, - target: DeclarationId, - runtime_inputs: &[PortId], - span: &SourceSpan, -) -> CallableTargetResolution { - let (template, mut arguments) = callable_template_arguments(dag, target); - arguments = retained_template_arguments_for_target(dag, template, &arguments); - let Some(signature) = resolve_direct_target_signature(dag, target, &arguments) else { - let name = target_display_name(dag, target); - return CallableTargetResolution::Fail(Diagnostic::ResolveError { - name, - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }); - }; - let mut raw_subst = SubstStack::new(); - raw_subst.push(arguments.clone()); - let raw_arrow_inputs = - resolve_arrow_decl_walk(dag, template, &mut raw_subst, 0).map(|arrow| arrow.inputs); - - let expected_runtime_arity = raw_arrow_inputs - .as_ref() - .map(|inputs| { - inputs - .iter() - .filter(|input| !declaration_is_callable(dag, **input, 0)) - .count() - }) - .unwrap_or_else(|| signature.inputs.len()); - if expected_runtime_arity != runtime_inputs.len() { - return CallableTargetResolution::Fail(Diagnostic::ArityMismatch { - function: target_display_name(dag, target), - expected: expected_runtime_arity, - actual: runtime_inputs.len(), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }); - } - - let mut runtime_iter = runtime_inputs.iter(); - if let Some(raw_inputs) = raw_arrow_inputs { - for expected_input in raw_inputs { - if declaration_is_callable(dag, expected_input, 0) { - let Some(actual_callable) = template_argument_value(&arguments, expected_input) - else { - return CallableTargetResolution::Retry; - }; - match bind_expected_callable_to_actual( - dag, - expected_input, - actual_callable, - &mut arguments, - ) { - CallableBindingResolution::Resolved => {} - CallableBindingResolution::Retry => { - return CallableTargetResolution::Retry; - } - CallableBindingResolution::Conflict => { - return CallableTargetResolution::Fail( - Diagnostic::ResolveError { - name: format!( - "callable argument to `{}` does not match the expected function type", - target_display_name(dag, target) - ), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }, - ); - } - } - continue; - } - let Some(input_port) = runtime_iter.next() else { - break; - }; - match dag.port(*input_port).state() { - PortState::Uninferred => return CallableTargetResolution::Retry, - PortState::Unresolved => { - return CallableTargetResolution::Fail(Diagnostic::ResolveError { - name: format!("(upstream failure in {})", target_display_name(dag, target)), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }); - } - PortState::Resolved(_) => {} - } - let Some(actual_ctx) = port_type_context(dag, *input_port) else { - return CallableTargetResolution::Retry; - }; - if !bind_expected_decl_to_actual_context( - dag, - expected_input, - &actual_ctx, - &mut arguments, - 0, - ) { - match int_literal_implicit_bind_tolerated_for_expected( - dag, - expected_input, - *input_port, - span, - ) { - Ok(true) => {} - Ok(false) => { - return CallableTargetResolution::Fail(callable_instantiation_conflict( - dag, - target, - expected_input, - &actual_ctx, - span, - )); - } - Err(diag) => return CallableTargetResolution::Fail(diag), - } - } - } - } else { - for expected_input in &signature.inputs { - if declaration_is_callable(dag, expected_input.declaration, 0) { - let Some(actual_callable) = - template_argument_value(&arguments, expected_input.declaration) - else { - return CallableTargetResolution::Retry; - }; - match bind_expected_callable_to_actual( - dag, - expected_input.declaration, - actual_callable, - &mut arguments, - ) { - CallableBindingResolution::Resolved => {} - CallableBindingResolution::Retry => { - return CallableTargetResolution::Retry; - } - CallableBindingResolution::Conflict => { - return CallableTargetResolution::Fail(Diagnostic::ResolveError { - name: format!( - "callable argument to `{}` does not match the expected function type", - target_display_name(dag, target) - ), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }); - } - } - continue; - } - let Some(actual_callable) = - template_argument_value(&arguments, expected_input.declaration) - else { - let Some(input_port) = runtime_iter.next() else { - break; - }; - match dag.port(*input_port).state() { - PortState::Uninferred => return CallableTargetResolution::Retry, - PortState::Unresolved => { - return CallableTargetResolution::Fail(Diagnostic::ResolveError { - name: format!( - "(upstream failure in {})", - target_display_name(dag, target) - ), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }); - } - PortState::Resolved(_) => {} - } - let Some(actual_ctx) = port_type_context(dag, *input_port) else { - return CallableTargetResolution::Retry; - }; - if !bind_expected_decl_to_actual_context( - dag, - expected_input.declaration, - &actual_ctx, - &mut arguments, - 0, - ) { - match int_literal_implicit_bind_tolerated_for_expected( - dag, - expected_input.declaration, - *input_port, - span, - ) { - Ok(true) => {} - Ok(false) => { - return CallableTargetResolution::Fail( - callable_instantiation_conflict( - dag, - target, - expected_input.declaration, - &actual_ctx, - span, - ), - ); - } - Err(diag) => return CallableTargetResolution::Fail(diag), - } - } - continue; - }; - match bind_expected_callable_to_actual( - dag, - expected_input.declaration, - actual_callable, - &mut arguments, - ) { - CallableBindingResolution::Resolved => {} - CallableBindingResolution::Retry => { - return CallableTargetResolution::Retry; - } - CallableBindingResolution::Conflict => { - return CallableTargetResolution::Fail(Diagnostic::ResolveError { - name: format!( - "callable argument to `{}` does not match the expected function type", - target_display_name(dag, target) - ), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ), - }); - } - } - } - } - - let Some(signature) = resolve_direct_target_signature(dag, target, &arguments) else { - let name = target_display_name(dag, target); - return CallableTargetResolution::Fail(Diagnostic::ResolveError { - name, - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }); - }; - CallableTargetResolution::Resolved { - template, - arguments, - signature, - } -} - -fn resolve_direct_target_signature( - dag: &Dag, - target: DeclarationId, - arguments: &[TemplateArgument], -) -> Option { - let (template, _) = callable_template_arguments(dag, target); - let mut subst = SubstStack::new(); - subst.push(arguments.to_vec()); - if let Some(arrow) = resolve_arrow_walk(dag, template, &mut subst, 0) { - return Some(arrow); - } - - let TypeConnective::Conj { children } = &dag.declaration(template).connective else { - return None; - }; - let inputs = children - .iter() - .map(|child| signature_type_shape(dag, child.ty, &subst, 0)) - .collect::>>()?; - Some(ResolvedArrow { - inputs, - output: TypeShape::new( - instantiated_enclosing_disj_for_variant(dag, template, arguments) - .or_else(|| enclosing_disj_for_variant(dag, template)) - .unwrap_or(target), - ), - // Variant constructor synthesis: `Variant(payload)` is direct - // construction, not a function call with an executable body. - // `NoBody` rather than `Pending` so the synthesized signature - // semantically matches the "no body by construction" cases in - // declarations, even though this `ResolvedArrow` is transient - // inference state (never stored in `Dag.declarations`). - body: ArrowBody::NoBody, - }) -} - -fn instantiated_enclosing_disj_for_variant( - dag: &Dag, - variant_decl_id: DeclarationId, - arguments: &[TemplateArgument], -) -> Option { - let enclosing_disj = enclosing_disj_for_variant(dag, variant_decl_id)?; - let params = &dag.declaration(enclosing_disj).type_params; - if params.is_empty() { - return Some(enclosing_disj); - } - let disj_arguments = params - .iter() - .map(|parameter| { - arguments - .iter() - .find(|argument| argument.parameter == *parameter) - .cloned() - }) - .collect::>>()?; - find_equivalent_anonymous_instantiation(dag, enclosing_disj, &disj_arguments, None) -} - -fn resolve_callable_targets(dag: &mut Dag) -> bool { - struct Rewrite { - node_index: usize, - template: DeclarationId, - arguments: Vec, - } - - let mut rewrites: Vec = Vec::new(); - for (node_index, node) in dag.nodes().iter().enumerate() { - let Behavior::Transform(t) = node else { - continue; - }; - let TransformTarget::Callable(target) = t.target else { - continue; - }; - let CallableTargetResolution::Resolved { - template, - arguments, - .. - } = resolve_callable_target(dag, target, &t.inputs, &t.span) - else { - continue; - }; - let (current_template, current_arguments) = callable_template_arguments(dag, target); - if current_template == template - && matches!( - generated_template_arguments_match(¤t_arguments, &arguments), - TemplateArgumentsMatch::Match, - ) - { - continue; - } - if arguments.is_empty() && current_template == template { - continue; - } - rewrites.push(Rewrite { - node_index, - template, - arguments, - }); - } - - let mut changed = false; - for rewrite in rewrites { - let new_target = if rewrite.arguments.is_empty() { - rewrite.template - } else { - let instantiation_id = dag.alloc_declaration_id(); - dag.push_declaration(crate::dag::Declaration { - id: instantiation_id, - name: None, - connective: TypeConnective::Instantiation { - template: rewrite.template, - arguments: rewrite.arguments, - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: synthetic_span(), - }); - instantiation_id - }; - let Behavior::Transform(t) = &mut dag.nodes_mut()[rewrite.node_index] else { - continue; - }; - let TransformTarget::Callable(target) = &mut t.target else { - continue; - }; - if *target != new_target { - *target = new_target; - changed = true; - } - } - changed -} - -fn materialize_callable_signature_instantiations(dag: &mut Dag) -> bool { - let requests: Vec<(DeclarationId, Vec)> = dag - .nodes() - .iter() - .filter_map(|node| { - let Behavior::Transform(t) = node else { - return None; - }; - let TransformTarget::Callable(target) = t.target else { - return None; - }; - let (template, arguments) = callable_template_arguments(dag, target); - (!arguments.is_empty()).then_some((template, arguments)) - }) - .collect(); - - let initial_count = dag.declarations().len(); - for (template, arguments) in requests { - let mut subst = SubstStack::new(); - subst.push(arguments); - let Some(raw_arrow) = resolve_arrow_decl_walk(dag, template, &mut subst, 0) else { - continue; - }; - for input in raw_arrow.inputs { - let _ = concretize_decl_with_subst(dag, input, &subst, 0); - } - let _ = concretize_decl_with_subst(dag, raw_arrow.output, &subst, 0); - } - dag.declarations().len() != initial_count -} - -fn resolve_lambda_parameter_types(dag: &mut Dag) -> bool { - struct Rewrite { - port: PortId, - ty: TypeShape, - } - - let mut rewrites: Vec = Vec::new(); - - for node in dag.nodes() { - let Behavior::Transform(t) = node else { - continue; - }; - let TransformTarget::Callable(target) = t.target else { - continue; - }; - let (template, arguments) = match resolve_callable_target(dag, target, &t.inputs, &t.span) { - CallableTargetResolution::Resolved { - template, - arguments, - .. - } => (template, arguments), - CallableTargetResolution::Retry => { - bind_non_callable_target_arguments(dag, target, &t.inputs) - .unwrap_or_else(|| callable_template_arguments(dag, target)) - } - CallableTargetResolution::Fail(_) => continue, - }; - let mut outer_subst = SubstStack::new(); - outer_subst.push(arguments.clone()); - let Some(raw_arrow) = resolve_arrow_decl_walk(dag, template, &mut SubstStack::new(), 0) - else { - continue; - }; - for expected_input in raw_arrow.inputs { - if !declaration_is_callable(dag, expected_input, 0) { - continue; - } - let Some(actual_callable) = template_argument_value(&arguments, expected_input) else { - continue; - }; - let Some(expected_signature) = - resolve_arrow_walk(dag, expected_input, &mut outer_subst.clone(), 0) - else { - continue; - }; - let TypeConnective::Arrow { body, .. } = &dag.declaration(actual_callable).connective - else { - continue; - }; - let ArrowBody::UserDefined(bind_id) = body else { - continue; - }; - let bind = (*bind_id).bind(dag); - if bind.params.len() < expected_signature.inputs.len() { - continue; - } - let start = bind.params.len() - expected_signature.inputs.len(); - for (port, ty) in bind.params[start..] - .iter() - .copied() - .zip(expected_signature.inputs.iter().copied()) - { - rewrites.push(Rewrite { port, ty }); - } - } - } - - let mut changed = false; - for rewrite in rewrites { - let current = dag.port(rewrite.port).state().clone(); - if matches!(current, PortState::Resolved(existing) if existing == rewrite.ty) { - continue; - } - if matches!(current, PortState::Unresolved) { - continue; - } - dag.set_port_type(rewrite.port, rewrite.ty); - changed = true; - } - changed -} - -fn validate_user_defined_function_signatures(dag: &mut Dag) -> bool { - struct Failure { - port: PortId, - diagnostic: Diagnostic, - } - - let user_defined_arrows: Vec<(DeclarationId, Vec, DeclarationId, BindNodeId)> = - dag.declarations() - .iter() - .filter_map(|decl| match &decl.connective { - TypeConnective::Arrow { - inputs, - output, - body: ArrowBody::UserDefined(bind_id), - } => Some((decl.id, inputs.clone(), *output, *bind_id)), - _ => None, - }) - .collect(); - - let mut failures = Vec::new(); - - 'declarations: for (decl_id, inputs, output, bind_id) in user_defined_arrows { - let bind = bind_id.bind(dag); - if matches!(dag.port(bind.value).state(), PortState::Unresolved) { - continue; - } - if bind.params.len() < inputs.len() { - failures.push(Failure { - port: bind.value, - diagnostic: Diagnostic::ResolveError { - name: format!( - "function `{}` body does not satisfy its declared signature", - dag.declaration(decl_id) - .name - .as_deref() - .unwrap_or("") - ), - span: bind.span.clone(), - correction: witness_correction_for_decl( - dag, - output, - bind.span.clone(), - format!( - "replace the function body with a `{}` value", - declaration_display_name(dag, output) - ), - ) - .unwrap_or_else(|| { - crate::diagnostics::Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ) - }), - }, - }); - continue; - } - - let start = bind.params.len() - inputs.len(); - let mut arguments = Vec::new(); - for (index, (port, expected_decl)) in bind.params[start..] - .iter() - .copied() - .zip(inputs.iter().copied()) - .enumerate() - { - if declaration_is_callable(dag, expected_decl, 0) { - continue; - } - match dag.port(port).state() { - PortState::Uninferred | PortState::Unresolved => continue 'declarations, - PortState::Resolved(_) => {} - } - let Some(actual_ctx) = port_type_context(dag, port) else { - continue 'declarations; - }; - if bind_expected_decl_to_actual_context( - dag, - expected_decl, - &actual_ctx, - &mut arguments, - 0, - ) { - continue; - } - - failures.push(Failure { - port: bind.value, - diagnostic: Diagnostic::ResolveError { - name: format!( - "function `{}` parameter {} does not satisfy its declared signature", - dag.declaration(decl_id) - .name - .as_deref() - .unwrap_or(""), - index + 1 - ), - span: bind.span.clone(), - correction: witness_correction_for_decl( - dag, - expected_decl, - bind.span.clone(), - format!( - "replace the parameter use with a `{}` value", - declaration_display_name(dag, expected_decl) - ), - ) - .unwrap_or_else(|| { - crate::diagnostics::Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ) - }), - }, - }); - continue 'declarations; - } - - match dag.port(bind.value).state() { - PortState::Uninferred | PortState::Unresolved => continue, - PortState::Resolved(_) => {} - } - if declaration_is_callable(dag, output, 0) { - continue; - } - let Some(actual_ctx) = port_type_context(dag, bind.value) else { - continue; - }; - if bind_expected_decl_to_actual_context(dag, output, &actual_ctx, &mut arguments, 0) { - continue; - } - - failures.push(Failure { - port: bind.value, - diagnostic: Diagnostic::ResolveError { - name: format!( - "function `{}` body does not satisfy its declared return type", - dag.declaration(decl_id) - .name - .as_deref() - .unwrap_or("") - ), - span: bind.span.clone(), - correction: witness_correction_for_decl( - dag, - output, - bind.span.clone(), - format!( - "replace the function body with a `{}` value", - declaration_display_name(dag, output) - ), - ) - .unwrap_or_else(|| { - crate::diagnostics::Correction::deferred_for_diagnostic_class( - "InferenceDiagnostic", - ) - }), - }, - }); - } - - let mut changed = false; - for failure in failures { - if matches!(dag.port(failure.port).state(), PortState::Unresolved) { - continue; - } - dag.mark_unresolved(failure.port, failure.diagnostic); - changed = true; - } - changed -} - -fn bind_non_callable_target_arguments( - dag: &Dag, - target: DeclarationId, - runtime_inputs: &[PortId], -) -> Option<(DeclarationId, Vec)> { - let (template, mut arguments) = callable_template_arguments(dag, target); - let signature = resolve_direct_target_signature(dag, target, &arguments)?; - let mut raw_subst = SubstStack::new(); - raw_subst.push(arguments.clone()); - let raw_arrow_inputs = - resolve_arrow_decl_walk(dag, template, &mut raw_subst, 0).map(|arrow| arrow.inputs); - - let expected_runtime_arity = raw_arrow_inputs - .as_ref() - .map(|inputs| { - inputs - .iter() - .filter(|input| !declaration_is_callable(dag, **input, 0)) - .count() - }) - .unwrap_or_else(|| signature.inputs.len()); - if expected_runtime_arity != runtime_inputs.len() { - return None; - } - - let mut runtime_iter = runtime_inputs.iter(); - if let Some(raw_inputs) = raw_arrow_inputs { - for expected_input in raw_inputs { - if declaration_is_callable(dag, expected_input, 0) { - continue; - } - let input_port = *runtime_iter.next()?; - let actual_ctx = port_type_context(dag, input_port)?; - if !bind_expected_decl_to_actual_context( - dag, - expected_input, - &actual_ctx, - &mut arguments, - 0, - ) { - return None; - } - } - } else { - for expected_input in &signature.inputs { - if declaration_is_callable(dag, expected_input.declaration, 0) { - continue; - } - let input_port = *runtime_iter.next()?; - let actual_ctx = port_type_context(dag, input_port)?; - if !bind_expected_decl_to_actual_context( - dag, - expected_input.declaration, - &actual_ctx, - &mut arguments, - 0, - ) { - return None; - } - } - } - - Some((template, arguments)) -} - -fn walk_to_conj_decl_with_subst( - dag: &Dag, - start: DeclarationId, - subst: &mut SubstStack, -) -> Option { - match walk_to_conj_decl_with_subst_or_opacity(dag, start, subst, false) { - ConjWalkResult::Conj(id) => Some(id), - ConjWalkResult::NoConj => None, - ConjWalkResult::NominalOpaque(_) => { - unreachable!("opacity check is disabled for ordinary Conj walks") - } - } -} - -enum ConjWalkResult { - Conj(DeclarationId), - NominalOpaque(DeclarationId), - NoConj, -} - -fn walk_to_conj_decl_with_subst_or_opacity( - dag: &Dag, - start: DeclarationId, - subst: &mut SubstStack, - stop_at_nominal_opacity: bool, -) -> ConjWalkResult { - let mut current = start; - for _ in 0..WALK_DEPTH_LIMIT { - let decl = dag.declaration(current); - if stop_at_nominal_opacity && decl.nominal_opacity.is_some() { - return ConjWalkResult::NominalOpaque(current); - } - match &decl.connective { - TypeConnective::Conj { .. } => return ConjWalkResult::Conj(current), - TypeConnective::Instantiation { - template, - arguments, - } => { - subst.push(arguments.clone()); - current = *template; - } - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - current = *next; - } - TypeConnective::Atom(AtomPayload::TypeParam(_)) => { - let Some(next) = subst.lookup(current) else { - return ConjWalkResult::NoConj; - }; - current = next; - } - _ => return ConjWalkResult::NoConj, - } - } - ConjWalkResult::NoConj -} - -fn walk_to_disj_decl_with_subst( - dag: &Dag, - start: DeclarationId, - subst: &mut SubstStack, -) -> Option { - let mut current = start; - for _ in 0..WALK_DEPTH_LIMIT { - let decl = dag.declaration(current); - match &decl.connective { - TypeConnective::Disj { .. } => return Some(current), - TypeConnective::Cardinality(p) - if p.bound() == crate::dag::CardinalityBound::AtMostOne => - { - return existing_optional_match_disj_decl(dag, current); - } - TypeConnective::Instantiation { - template, - arguments, - } => { - subst.push(arguments.clone()); - current = *template; - } - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - current = *next; - } - TypeConnective::Atom(AtomPayload::TypeParam(_)) => { - current = subst.lookup(current)?; - } - _ => return None, - } - } - None -} - -fn enclosing_disj_for_variant(dag: &Dag, variant_decl_id: DeclarationId) -> Option { - dag.declarations().iter().find_map(|decl| { - let TypeConnective::Disj { variants } = &decl.connective else { - return None; - }; - variants - .iter() - .find(|variant| variant.ty == variant_decl_id) - .map(|_| decl.id) - }) -} - -fn resolve_payload_binding_type( - dag: &Dag, - variant_decl_id: DeclarationId, - subst: &SubstStack, - variant_name: &str, - binding_name: &str, - span: &SourceSpan, -) -> Result { - match &dag.declaration(variant_decl_id).connective { - TypeConnective::Conj { children } if children.is_empty() => { - Err(Diagnostic::ResolveError { - name: format!( - "variant `{variant_name}` does not carry a payload and cannot bind `{binding_name}`" - ), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }) - } - TypeConnective::Conj { children } - if children.len() == 1 && children[0].label.as_str() == "_0" => - { - let payload_type_decl = children[0].ty; - walk_to_type_shape(dag, payload_type_decl, subst, 0) - .map(PayloadBindingResolution::Direct) - .ok_or_else(|| Diagnostic::ResolveError { - name: format!( - "variant `{variant_name}` payload does not resolve to a port type for binding `{binding_name}`" - ), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }) - } - TypeConnective::Conj { .. } => Ok(PayloadBindingResolution::SpecializedRecord { - variant_decl_id, - subst: subst.clone(), - }), - _ => { - Err(Diagnostic::ResolveError { - name: format!( - "variant `{variant_name}` does not lower to a payload Conj and cannot bind `{binding_name}`" - ), - span: span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }) - } - } -} - -fn materialize_specialized_payload_record( - dag: &mut Dag, - variant_decl_id: DeclarationId, - subst: &SubstStack, -) -> TypeShape { - let variant_decl = dag.declaration(variant_decl_id).clone(); - let TypeConnective::Conj { children } = variant_decl.connective else { - return TypeShape::new(variant_decl_id); - }; - let specialized_children: Vec = children - .into_iter() - .map(|field| Field { - label: field.label, - ty: concretize_decl_with_subst(dag, field.ty, subst, 0), - }) - .collect(); - if let Some(existing) = find_equivalent_anonymous_conj(dag, &specialized_children) { - return TypeShape::new(existing); - } - let id = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id, - name: None, - connective: TypeConnective::Conj { - children: specialized_children, - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: variant_decl.span, - }); - TypeShape::new(id) -} - -pub(crate) fn concretize_decl_with_subst( - dag: &mut Dag, - current: DeclarationId, - subst: &SubstStack, - depth: usize, -) -> DeclarationId { - if depth >= WALK_DEPTH_LIMIT { - return current; - } - let decl = dag.declaration(current).clone(); - // DB-16 (3a.3 closure): refinement-bearing declarations whose base - // requires substitution materialize a fresh substituted-refined - // carrier — structurally identical to what `lower_parameter_refinement` - // would produce if the user had authored the refinement on the - // concrete type directly. Runs before the connective match so the - // refinement edge is preserved across the walk. - if decl.refinement.is_some() && refinement_base_requires_substitution(dag, current, subst) { - return materialize_substituted_refined_decl(dag, current, subst); - } - match decl.connective { - TypeConnective::Atom(AtomPayload::TypeParam(_)) => subst.lookup(current).unwrap_or(current), - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - concretize_decl_with_subst(dag, next, subst, depth + 1) - } - TypeConnective::Instantiation { - template, - arguments, - } => { - let specialized_arguments: Vec = arguments - .into_iter() - .map(|arg| TemplateArgument { - parameter: arg.parameter, - value: concretize_decl_with_subst(dag, arg.value, subst, depth + 1), - }) - .collect(); - if let Some(existing) = find_equivalent_anonymous_instantiation( - dag, - template, - &specialized_arguments, - decl.nominal_opacity.as_ref(), - ) { - return existing; - } - let id = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id, - name: None, - connective: TypeConnective::Instantiation { - template, - arguments: specialized_arguments, - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: decl.nominal_opacity.clone(), - span: decl.span, - }); - id - } - TypeConnective::Cardinality(p) => { - let element = p.element(); - let bound = p.bound(); - let specialized_element = concretize_decl_with_subst(dag, element, subst, depth + 1); - if let Some(existing) = - find_equivalent_anonymous_cardinality(dag, specialized_element, &bound) - { - return existing; - } - dag.alloc_cardinality_decl(specialized_element, bound, decl.span.clone()) - } - _ => current, - } -} - -/// DB-16 (3a.3 closure): gate used by both `concretize_decl_with_subst` -/// (construction side) and `signature_type_shape` (consumer side) to -/// decide whether a refinement-bearing declaration needs substitution -/// before its refinement edge can be meaningfully consumed. -/// -/// Returns `true` iff walking the refined carrier's base through -/// `Atom(ResolvedIdentifier(_))` hops lands on either: -/// - a `TypeParam` declaration whose id is bound in `subst`, or -/// - an `Instantiation` whose arguments reference substitution-bound -/// TypeParams (i.e., substitution would materially change the -/// resolved base). -/// -/// For concrete refined carriers (`Int where pred(x)`) the walk -/// short-circuits to `false`; DB-11's identity-terminator fires -/// unchanged. For refined TypeParams with no active binding (earlier -/// iteration retry case) the walk also returns `false`. -fn refinement_base_requires_substitution( - dag: &Dag, - current: DeclarationId, - subst: &SubstStack, -) -> bool { - let decl = dag.declaration(current); - let TypeConnective::Atom( - AtomPayload::ResolvedByStructure(base) | AtomPayload::ResolvedByName(base), - ) = &decl.connective - else { - return false; - }; - refinement_base_walk(dag, *base, subst, 0) -} - -fn refinement_base_walk( - dag: &Dag, - current: DeclarationId, - subst: &SubstStack, - depth: usize, -) -> bool { - if depth >= WALK_DEPTH_LIMIT { - return false; - } - let decl = dag.declaration(current); - match &decl.connective { - TypeConnective::Atom(AtomPayload::TypeParam(_)) => subst.lookup(current).is_some(), - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - refinement_base_walk(dag, *next, subst, depth + 1) - } - TypeConnective::Instantiation { arguments, .. } => arguments - .iter() - .any(|arg| refinement_base_walk(dag, arg.value, subst, depth + 1)), - TypeConnective::Cardinality(p) => refinement_base_walk(dag, p.element(), subst, depth + 1), - _ => false, - } -} - -/// DB-16 (3a.3 closure): the sole construction site for substituted -/// refined carriers. Called from `concretize_decl_with_subst`'s -/// refinement branch during the `materialize_callable_signature_instantiations` -/// phase. Produces a fresh anonymous `Declaration` structurally -/// identical to what `lower_parameter_refinement` would produce if -/// the user had authored the refinement on the concrete substituted -/// base directly. -/// -/// Dedups against existing substituted-refined carriers in the Dag -/// before allocating — at fixpoint iteration N+1, if iteration N -/// already produced a carrier for this (template, subst), the scan -/// finds it and no new allocation happens. -/// -/// Fail-closed per C-8: substrate-integrity violations register -/// diagnostics rather than silently degrading to the template carrier. -fn materialize_substituted_refined_decl( - dag: &mut Dag, - template_refined: DeclarationId, - subst: &SubstStack, -) -> DeclarationId { - // Dedup: if a matching substituted-refined carrier already exists - // (e.g., from an earlier fixpoint iteration), reuse it. - if let Some(existing) = find_equivalent_substituted_refined_decl(dag, template_refined, subst) { - return existing; - } - - let template_decl = dag.declaration(template_refined).clone(); - let Some(template_pred_decl_id) = template_decl.refinement else { - // Caller (concretize_decl_with_subst's refinement branch) checks - // `decl.refinement.is_some()` before entering. Reaching this - // point means the caller contract was violated — surface that - // rather than masking it with a silent fallthrough. - unreachable!( - "materialize_substituted_refined_decl entered on declaration {:?} without refinement edge", - template_refined - ); - }; - let TypeConnective::Atom( - AtomPayload::ResolvedByStructure(template_base) - | AtomPayload::ResolvedByName(template_base), - ) = template_decl.connective - else { - // Caller also checks `refinement_base_requires_substitution`, - // whose first step returns false for any connective other than - // `Atom(ResolvedIdentifier(_))`. Reaching this point means the - // caller contract was violated. - unreachable!( - "materialize_substituted_refined_decl entered on declaration {:?} whose connective is not Atom(ResolvedIdentifier(_))", - template_refined - ); - }; - let template_span = template_decl.span.clone(); - - // Step 1: resolve the substituted base. - let Some(substituted_base) = resolve_decl_with_subst(dag, template_base, subst, 0) else { - dag.attach_diagnostic(Diagnostic::ResolveError { - name: "refined-generic substitution: substituted base did not resolve".to_string(), - span: template_span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }); - return template_refined; - }; - - // Step 2: extract original predicate slots. - let Some((original_param_port, original_body_port)) = - outer_predicate_slots(dag, template_pred_decl_id) - else { - let pred_span = dag.declaration(template_pred_decl_id).span.clone(); - dag.attach_diagnostic(Diagnostic::ResolveError { - name: "refined-generic substitution: malformed predicate shape".to_string(), - span: pred_span, - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }); - return template_refined; - }; - - // Step 3: allocate fresh composite param port typed as substituted base. - let fresh_param_port = dag.alloc_port(None); - dag.set_port_type(fresh_param_port, TypeShape::new(substituted_base)); - - // Step 4: clone the predicate body, routing Transform targets - // through the active substitution stack. - let Some(cloned_body_port) = clone_predicate_body( - dag, - original_body_port, - original_param_port, - fresh_param_port, - subst, - 0, - ) else { - dag.attach_diagnostic(Diagnostic::ResolveError { - name: "refined-generic substitution: out-of-fragment predicate body reached materialization" - .to_string(), - span: template_span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }); - return template_refined; - }; - - // Step 5: wrap cloned body in a fresh Bind. - let bind_id = dag.alloc_node_id(); - dag.push_node(Behavior::Bind(BindNode { - id: bind_id, - name: "".to_string(), - value: cloned_body_port, - params: vec![fresh_param_port], - span: template_span.clone(), - lane2_workflow: None, - emit_participation: None, - })); - - // Step 5 (cont.): build the fresh predicate-Arrow Declaration. - // Inherit the original predicate's output type (Bool) — reading it - // from the original Arrow keeps this module independent of the - // Bool-decl lookup pattern `lower_parameter_refinement` uses. - let TypeConnective::Arrow { - output: bool_decl_id, - .. - } = &dag.declaration(template_pred_decl_id).connective - else { - // `outer_predicate_slots` rejected non-Arrow predicates at - // step 2 above; reaching this point means the predicate - // declaration's connective mutated between step 2 and step 5, - // which is impossible under `&mut Dag` exclusive access. - unreachable!( - "predicate declaration {:?} connective is not Arrow despite outer_predicate_slots succeeding", - template_pred_decl_id - ); - }; - let bool_decl_id = *bool_decl_id; - - let fresh_pred_decl_id = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: fresh_pred_decl_id, - name: None, - connective: TypeConnective::Arrow { - inputs: vec![substituted_base], - output: bool_decl_id, - body: ArrowBody::UserDefined( - BindNodeId::from_bind_node(dag, bind_id) - .expect("UserDefined Arrow body bind id must point at a Bind"), - ), - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: template_span.clone(), - }); - - // Step 6: allocate the substituted-refined carrier Declaration. - let fresh_refined_id = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: fresh_refined_id, - name: None, - connective: TypeConnective::Atom(AtomPayload::ResolvedByStructure(substituted_base)), - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: Some(fresh_pred_decl_id), - nominal_opacity: None, - span: template_span, - }); - - fresh_refined_id -} - -/// DB-16 (3a.3 closure): read-only lookup used by both -/// `concretize_decl_with_subst` (pre-allocation dedup) and -/// `signature_type_shape` (consumer-side lookup gate). -/// -/// Given a template refined carrier and an active substitution, scans -/// `dag.declarations()` for an already-materialized substituted-refined -/// carrier whose base matches the substituted base AND whose predicate -/// body walks structurally equal to what cloning the template's body -/// with the given subst WOULD produce. Mirrors -/// `find_equivalent_anonymous_instantiation`'s dedup pattern. -/// -/// Pure read; no allocation. `&Dag`. -fn find_equivalent_substituted_refined_decl( - dag: &Dag, - template_refined: DeclarationId, - subst: &SubstStack, -) -> Option { - let template_decl = dag.declaration(template_refined); - let TypeConnective::Atom( - AtomPayload::ResolvedByStructure(template_base) - | AtomPayload::ResolvedByName(template_base), - ) = &template_decl.connective - else { - return None; - }; - let template_pred_id = template_decl.refinement?; - - let substituted_base = resolve_decl_with_subst(dag, *template_base, subst, 0)?; - if substituted_base == *template_base { - return None; - } - - let (template_param, template_body) = outer_predicate_slots(dag, template_pred_id)?; - - for decl in dag.declarations() { - if decl.name.is_some() { - continue; - } - if decl.id == template_refined { - continue; - } - let TypeConnective::Atom( - AtomPayload::ResolvedByStructure(cand_base) | AtomPayload::ResolvedByName(cand_base), - ) = &decl.connective - else { - continue; - }; - if *cand_base != substituted_base { - continue; - } - let Some(cand_pred_id) = decl.refinement else { - continue; - }; - let Some((cand_param, cand_body)) = outer_predicate_slots(dag, cand_pred_id) else { - continue; - }; - if predicate_bodies_equal_under_subst( - dag, - cand_body, - cand_param, - template_body, - template_param, - subst, - 0, - ) { - return Some(decl.id); - } - } - None -} - -/// DB-16 (3a.3 closure): lockstep structural equality on two predicate -/// bodies, threading the active substitution stack through the -/// template side's Transform target decl-level references. Used by -/// `find_equivalent_substituted_refined_decl` to compare a candidate -/// materialized body against what cloning the template would produce. -/// -/// Pure read; walks Value/Transform nodes, treats the parameter-slot -/// on each side as equivalent. Mirrors `clone_predicate_body`'s shape -/// exactly so the comparison is faithful to what cloning emits. -fn predicate_bodies_equal_under_subst( - dag: &Dag, - cand_port: PortId, - cand_param: PortId, - template_port: PortId, - template_param: PortId, - subst: &SubstStack, - depth: usize, -) -> bool { - if depth >= WALK_DEPTH_LIMIT { - return false; - } - let cand_is_param = cand_port == cand_param; - let template_is_param = template_port == template_param; - if cand_is_param != template_is_param { - return false; - } - if cand_is_param { - return true; - } - - let cand_node_id = dag.port(cand_port).produced_by; - let template_node_id = dag.port(template_port).produced_by; - match (cand_node_id, template_node_id) { - (None, None) => cand_port == template_port, - (Some(cand_id), Some(template_id)) => match (dag.node(cand_id), dag.node(template_id)) { - (Behavior::Value(cand_v), Behavior::Value(template_v)) => { - cand_v.data == template_v.data - } - (Behavior::Transform(cand_t), Behavior::Transform(template_t)) => { - if !transform_targets_equal_under_subst( - dag, - &cand_t.target, - &template_t.target, - subst, - ) { - return false; - } - if cand_t.inputs.len() != template_t.inputs.len() { - return false; - } - cand_t - .inputs - .iter() - .zip(template_t.inputs.iter()) - .all(|(c, t)| { - predicate_bodies_equal_under_subst( - dag, - *c, - cand_param, - *t, - template_param, - subst, - depth + 1, - ) - }) - } - _ => false, - }, - _ => false, - } -} - -fn transform_targets_equal_under_subst( - dag: &Dag, - cand: &TransformTarget, - template: &TransformTarget, - subst: &SubstStack, -) -> bool { - match (cand, template) { - (TransformTarget::Operator(a), TransformTarget::Operator(b)) => a == b, - (TransformTarget::Callable(cand_id), TransformTarget::Callable(template_id)) => { - callable_decls_equal_under_subst(dag, *cand_id, *template_id, subst) - } - ( - TransformTarget::UnresolvedFieldProject { - field_label: cand_label, - }, - TransformTarget::UnresolvedFieldProject { - field_label: template_label, - }, - ) => cand_label == template_label, - ( - TransformTarget::ResolvedFieldProject { - field_label: cand_label, - }, - TransformTarget::ResolvedFieldProject { - field_label: template_label, - }, - ) => cand_label == template_label, - _ => false, - } -} - -/// DB-16 (3a.3 closure): structural equivalence between two declaration -/// references under an active substitution stack. Used by -/// `transform_targets_equal_under_subst` to compare Callable / -/// FieldProject target decl-level references when one side is a -/// template-context Instantiation (possibly carrying extra bindings -/// for outer TypeParams) and the other is a concrete site -/// Instantiation (only bindings for the callee's own TypeParams). -/// -/// Strategy: normalize both sides to their template-own-type-param -/// bindings (filtering out substitution-stack artifacts from the -/// template side), resolve through the subst stack on the template -/// side, then compare. Falls back to -/// `declaration_shapes_equivalent` when either side isn't an -/// Instantiation (direct decl id match). -fn callable_decls_equal_under_subst( - dag: &Dag, - cand_id: DeclarationId, - template_id: DeclarationId, - subst: &SubstStack, -) -> bool { - if cand_id == template_id { - return true; - } - // Fast path: resolve template through subst and compare directly. - let template_resolved = - resolve_decl_with_subst(dag, template_id, subst, 0).unwrap_or(template_id); - if cand_id == template_resolved - || declaration_shapes_equivalent(dag, cand_id, template_resolved, 0) - { - return true; - } - // Fallback: both are Instantiations, compare by template own-type-params. - let (Some(cand_norm), Some(template_norm)) = ( - normalized_instantiation_args(dag, cand_id), - normalized_instantiation_args(dag, template_id), - ) else { - return false; - }; - if cand_norm.template != template_norm.template { - return false; - } - if cand_norm.args.len() != template_norm.args.len() { - return false; - } - cand_norm - .args - .iter() - .zip(template_norm.args.iter()) - .all(|(c_arg, t_arg)| { - c_arg.parameter == t_arg.parameter && { - let t_val = - resolve_decl_with_subst(dag, t_arg.value, subst, 0).unwrap_or(t_arg.value); - c_arg.value == t_val || declaration_shapes_equivalent(dag, c_arg.value, t_val, 0) - } - }) -} - -struct NormalizedInstantiation { - template: DeclarationId, - args: Vec, -} - -/// DB-16 (3a.3 closure): normalize an `Instantiation` declaration by -/// stripping **only** self-bindings (`arg.parameter == arg.value`). -/// -/// Self-bindings are reattachment artifacts produced by -/// `resolve_callable_target`'s unification when a generic call site -/// is encountered under an outer generic scope: the outer TypeParam -/// binds to itself in the callee's argument list because no concrete -/// value has been inferred yet. Those self-bindings are no-op under -/// substitution (`SubstStack::lookup` short-circuits to `None` on -/// them) but inflate argument-length comparisons in strict -/// structural checks. -/// -/// Non-self bindings are NEVER stripped: retained callable-argument -/// identities (per `retained_template_arguments_for_target`) carry -/// semantic meaning that must survive DB-16's equivalence walk. Two -/// Instantiations that differ only by a non-self retained binding -/// are structurally distinct and must compare unequal. -fn normalized_instantiation_args( - dag: &Dag, - decl: DeclarationId, -) -> Option { - match normalize_instantiation_arguments(&dag.declaration(decl).connective) { - NormalizedInstantiationArgs::NotInstantiation => None, - NormalizedInstantiationArgs::Normalized { template, args } => { - Some(NormalizedInstantiation { template, args }) - } - } -} - -fn find_equivalent_anonymous_conj(dag: &Dag, children: &[Field]) -> Option { - dag.declarations().iter().find_map(|decl| { - if decl.name.is_some() { - return None; - } - let TypeConnective::Conj { children: existing } = &decl.connective else { - return None; - }; - (existing.len() == children.len() - && existing - .iter() - .zip(children.iter()) - .all(|(lhs, rhs)| lhs.label == rhs.label && lhs.ty == rhs.ty)) - .then_some(decl.id) - }) -} - -fn find_equivalent_anonymous_instantiation( - dag: &Dag, - template: DeclarationId, - arguments: &[TemplateArgument], - nominal_opacity: Option<&NominalOpacity>, -) -> Option { - dag.declarations().iter().find_map(|decl| { - if decl.name.is_some() { - return None; - } - let TypeConnective::Instantiation { - template: existing_template, - arguments: existing_arguments, - } = &decl.connective - else { - return None; - }; - (template == *existing_template - && decl.nominal_opacity.as_ref() == nominal_opacity - && matches!( - generated_template_arguments_match(existing_arguments, arguments), - TemplateArgumentsMatch::Match, - )) - .then_some(decl.id) - }) -} - -fn find_equivalent_anonymous_cardinality( - dag: &Dag, - element: DeclarationId, - bound: &crate::dag::CardinalityBound, -) -> Option { - dag.declarations().iter().find_map(|decl| { - if decl.name.is_some() { - return None; - } - let TypeConnective::Cardinality(existing) = &decl.connective else { - return None; - }; - (element == existing.element() && existing.bound() == *bound).then_some(decl.id) - }) -} - -enum FieldProjectResolution { - Retry, - Fail(Box), - Resolved { output_ty: TypeShape }, -} - -impl FieldProjectResolution { - fn fail(diagnostic: Diagnostic) -> Self { - Self::Fail(Box::new(diagnostic)) - } -} - -fn resolve_field_project( - dag: &Dag, - t: &TransformNode, - field_label: &str, -) -> FieldProjectResolution { - if t.inputs.len() != 1 { - return FieldProjectResolution::fail(Diagnostic::ArityMismatch { - function: format!(".{field_label}"), - expected: 1, - actual: t.inputs.len(), - span: t.span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }); - } - - let input_ty = match dag.port(t.inputs[0]).state() { - PortState::Uninferred => return FieldProjectResolution::Retry, - PortState::Unresolved => { - return FieldProjectResolution::fail(Diagnostic::ResolveError { - name: format!("(upstream failure in field `{field_label}`)"), - span: t.span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }) - } - PortState::Resolved(ty) => *ty, - }; - - let mut subst = SubstStack::new(); - if let Some(opaque_decl) = first_nominal_opaque_on_conj_walk(dag, input_ty.declaration, &subst) - { - return FieldProjectResolution::fail(Diagnostic::NominalOpacityViolation { - declaration: opaque_decl, - accessor: None, - span: t.span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }); - } - let Some(actual_conj_id) = walk_to_conj_decl_with_subst(dag, input_ty.declaration, &mut subst) - else { - return FieldProjectResolution::fail(Diagnostic::ResolveError { - name: format!( - "field `{field_label}` cannot be projected from `{}` because it does not walk to a Conj type", - target_display_name(dag, input_ty.declaration), - ), - span: t.span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }); - }; - - let children = match &dag.declaration(actual_conj_id).connective { - TypeConnective::Conj { children } => children, - _ => unreachable!("walk_to_conj_decl returned a non-Conj declaration"), - }; - let Some(field_decl_id) = children - .iter() - .find(|field| field.label == field_label) - .map(|field| field.ty) - else { - let field_start = t.span.byte_end.saturating_sub(field_label.len() as u32); - let correction = match children.as_slice() { - [field] => Correction::live( - format!("did you mean field `{}`?", field.label), - SourceSpan::new(t.span.file.clone(), field_start, t.span.byte_end), - field.label.clone(), - ), - fields => { - let candidates = fields - .iter() - .map(|field| field.label.as_str()) - .collect::>() - .join(", "); - Correction::deferred( - format!( - "field `{field_label}` has multiple valid replacements: {candidates}" - ), - "R3 Gap 9 row #106 field-choice correction workflow", - "model structured alternative corrections so each candidate can be surfaced without choosing arbitrarily", - ) - } - }; - return FieldProjectResolution::fail(Diagnostic::ResolveError { - name: format!( - "field `{field_label}` does not exist on `{}`", - target_display_name(dag, input_ty.declaration), - ), - span: t.span.clone(), - correction, - }); - }; - let Some(output_ty) = walk_to_type_shape(dag, field_decl_id, &subst, 0) else { - return FieldProjectResolution::fail(Diagnostic::ResolveError { - name: format!( - "field `{field_label}` on `{}` does not resolve to a port type", - target_display_name(dag, input_ty.declaration), - ), - span: t.span.clone(), - correction: Correction::deferred_for_diagnostic_class("InferenceDiagnostic"), - }); - }; - - FieldProjectResolution::Resolved { output_ty } -} - -fn first_nominal_opaque_on_conj_walk( - dag: &Dag, - start: DeclarationId, - subst: &SubstStack, -) -> Option { - let mut subst = subst.clone(); - match walk_to_conj_decl_with_subst_or_opacity(dag, start, &mut subst, true) { - ConjWalkResult::NominalOpaque(id) => Some(id), - ConjWalkResult::Conj(_) | ConjWalkResult::NoConj => None, - } -} - -fn decide_field_project(dag: &Dag, t: &TransformNode, field_label: &str) -> Decision { - match resolve_field_project(dag, t, field_label) { - FieldProjectResolution::Retry => Decision::Retry, - FieldProjectResolution::Fail(diag) => Decision::fail(t.output, *diag), - FieldProjectResolution::Resolved { output_ty, .. } => Decision::Set(t.output, output_ty), - } -} - -fn resolve_field_project_targets(dag: &mut Dag) -> bool { - let mut rewrites: Vec<(usize, String)> = Vec::new(); - for (node_index, node) in dag.nodes().iter().enumerate() { - let Behavior::Transform(t) = node else { - continue; - }; - let TransformTarget::UnresolvedFieldProject { field_label } = &t.target else { - continue; - }; - let FieldProjectResolution::Resolved { .. } = resolve_field_project(dag, t, field_label) - else { - continue; - }; - rewrites.push((node_index, field_label.clone())); - } - - let mut changed = false; - for (node_index, field_label) in rewrites { - let Behavior::Transform(t) = &mut dag.nodes_mut()[node_index] else { - continue; - }; - let target = &mut t.target; - let TransformTarget::UnresolvedFieldProject { .. } = target else { - continue; - }; - *target = TransformTarget::ResolvedFieldProject { field_label }; - changed = true; - } - changed -} - -const WALK_DEPTH_LIMIT: usize = 32; -/// §8.9 operator dispatch via structural algebra walk. -/// -/// Walks the LHS type's declaration chain (following `Instantiation` -/// and `ResolvedIdentifier` edges) until it reaches an algebra -/// `Conj` declaration, then looks up the operator's field by name -/// (via `OperatorKind::algebra_field_name`). The field's Arrow -/// signature is read from the declaration graph — the compiler -/// consumes `std/algebra.dag` as authority rather than fabricating -/// signatures in Rust. -/// -/// **Receiver substitution rule.** The algebra's first type -/// parameter is the "receiver" — it represents "the inhabitant of -/// this algebra." When the algebra field's Arrow has `(T, T) -> T`, -/// the resolved signature substitutes `T → source_id` (the user's -/// `lhs_type` declaration, e.g., `Int`), not `T → Word64` (the -/// algebra instantiation's template argument). This keeps port -/// types consistent with user-facing primitives. Non-receiver -/// positions (`Bool` in the comparison arrows, `Ordering` in -/// `compare`) stay as-is. -/// -/// **Current coverage.** Works for scalar algebras whose receiver -/// is the first type parameter used directly: `OrderedRing`, -/// `Ring`, `Semiring`, `Field`, `Lattice`, -/// `BooleanAlgebra`. This is sufficient for `Int`, `Float`, and -/// any user type that aliases an `OrderedRing`/`Ring`/`Field` -/// instantiation. -/// -/// **Not yet covered** (tracked in DOWNSTREAM_REQUIREMENTS.md as -/// class-5 gaps): -/// - `Bool`: ~~no structural link~~ now uses `Declaration.inhabits` on -/// the kernel `Bool` sum in `dsl/std/types.dag`, populated at bootstrap -/// (`bootstrap::patch_kernel_bool_boolean_algebra_inhabits`) so v2 -/// keeps a plain `type Bool = True | False` surface. The walk follows -/// `Declaration.inhabits` when the connective chain hits a `Disj` -/// without an inline algebra. -/// - `String` / collection-level algebras whose receiver is -/// `FreeMonoid` / `Set` / `Map` (the whole -/// instantiation, not just `T`): needs a refinement to the -/// substitution rule. -/// -/// For those cases the resolver falls back to a Rust-side bridge -/// with a `(T, T) -> T` / `(T, T) -> Bool` signature. The fallback -/// is explicit about being a scaffold (see OperatorKind's -/// dissolution receipt). -/// -/// **Dedup invariant:** anonymous `Result<…, DivError>` instantiations -/// must reuse `find_equivalent_anonymous_instantiation` on every pass; -/// a missed structural match would mint fresh `DeclarationId`s each -/// iteration and the fixpoint loop would not terminate. -fn div_total_result_output_shape(dag: &mut Dag, base_lhs: TypeShape) -> Option { - let result_template = canonical_result_decl(dag)?.id; - let div_error = canonical_div_error_decl(dag)?.id; - let (t_ok, t_err, span) = { - let rdecl = dag.declaration(result_template); - ( - rdecl.type_params.first().copied()?, - rdecl.type_params.get(1).copied()?, - rdecl.span.clone(), - ) - }; - let args = vec![ - TemplateArgument { - parameter: t_ok, - value: base_lhs.declaration, - }, - TemplateArgument { - parameter: t_err, - value: div_error, - }, - ]; - if let Some(existing) = - find_equivalent_anonymous_instantiation(dag, result_template, &args, None) - { - return Some(TypeShape::new(existing)); - } - let id = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id, - name: None, - connective: TypeConnective::Instantiation { - template: result_template, - arguments: args, - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span, - }); - Some(TypeShape::new(id)) -} - -fn resolve_operator_arrow( - dag: &mut Dag, - op_kind: OperatorKind, - lhs_type: &TypeShape, -) -> Option { - // DB-11 (3a.3) operand normalization. Primitive operators (`+`, - // `>`, `!=`, etc.) are structurally over BASE types — refinements - // are surface-level facts about values, not part of the operator's - // arrow contract. Mirroring a refined lhs like `Int where d != 0` - // onto both operand positions (as the old fallback did) made the - // call-site refinement-discharge pass treat the refinement as a - // real requirement on every operand — so a literal `10` in - // `d > 10` failed discharge because literals carry no refinement. - // Strip refinements once up front; algebra-Conj walks and the - // primitive fallback both operate on the base. - let source_id = strip_refinement_to_base(dag, lhs_type.declaration); - let base_lhs = TypeShape::new(source_id); - // Walk the source type's declaration chain to find the algebra - // Conj. We follow Instantiation/ResolvedIdentifier edges; at - // each step the template argument bindings are intentionally - // discarded — see the receiver substitution rule in the doc - // comment. - let mut current = source_id; - let mut saw_algebra_conj = false; - for _ in 0..WALK_DEPTH_LIMIT { - let decl = dag.declaration(current).clone(); - match &decl.connective { - TypeConnective::Conj { children } => { - saw_algebra_conj = true; - // Found the algebra. Look up the operator's field by - // name. - let field_name = crate::operators::algebra_field_name(op_kind); - if let Some(field) = children.iter().find(|f| f.label == field_name) { - return read_algebra_field(dag, &decl, field.ty, source_id, op_kind, &base_lhs); - } - // Algebra doesn't declare this operator's field — - // fall back to the Rust-side scaffold bridge below. - break; - } - TypeConnective::Instantiation { template, .. } => { - current = *template; - } - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - current = *next; - } - // Terminal connectives: try `Declaration.inhabits` (e.g. Bool - // → BooleanAlgebra) before giving up. - TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(_)) - | TypeConnective::Atom(AtomPayload::TypeParam(_)) - | TypeConnective::Atom(AtomPayload::Literal(_)) - | TypeConnective::Disj { .. } - | TypeConnective::Arrow { .. } - | TypeConnective::Cardinality(_) => { - if let Some(inh) = decl.inhabits { - current = inh; - } else { - break; - } - } - } - } - // Fallback: Rust-side scaffold bridge for primitives whose - // structural walk doesn't terminate at an algebra Conj (Bool, - // Classical; collection-level algebras). Documented class-5 gap. - // - // Logical operators are Bool-monomorphic: both operands and the - // output are always Bool, independent of `lhs_type`. An Int lhs - // on `&&` / `||` must surface a type mismatch, not propagate - // through the operand slots the way Arithmetic / Comparison do. - // - // Fail-closed for LHS that the walk never reached any Conj for: - // every loop exit saw a terminal connective with no `inhabits`, - // an Arrow / Disj / Cardinality with no `inhabits`, or depth - // exhaustion. The pre-fix fallback fabricated `(T,T)->T` / - // `(T,T)->Bool` / `(Bool,Bool)->Bool` / `(T,T)->Result` - // for any such LHS — inventing an arrow contract for a type that - // declares none. Refuse synthesis here so the caller surfaces a - // typed unsupported-operator diagnostic. The Conj-reached-but- - // missing-field path (`saw_algebra_conj == true`) keeps its - // existing scaffold; tightening that further (distinguishing - // algebra carriers from arbitrary product structs) requires a - // structural signal beyond "is a Conj" — see PR #1329 STOP+PING. - if !saw_algebra_conj { - return None; - } - let (inputs, output) = match op_kind { - // Division leaves the class-5 scaffold: total shape matches algebra `div`. - OperatorKind::Arithmetic(ArithmeticOp::Div) => ( - vec![base_lhs, base_lhs], - div_total_result_output_shape(dag, base_lhs)?, - ), - OperatorKind::Arithmetic(_) => (vec![base_lhs, base_lhs], base_lhs), - OperatorKind::Comparison(_) => (vec![base_lhs, base_lhs], dag.bool_shape()?), - OperatorKind::Logical(_) => { - let bool_shape = dag.bool_shape()?; - (vec![bool_shape, bool_shape], bool_shape) - } - }; - Some(ResolvedArrow { - inputs, - output, - body: ArrowBody::Pending, - }) -} - -/// DB-11 (3a.3) refinement-strip helper. Walks the -/// `Atom(ResolvedIdentifier(...))` chain, skipping past any -/// declaration that carries a `refinement` edge, until it reaches a -/// declaration with no refinement. Used by `resolve_operator_arrow` -/// to normalize primitive-operator inputs to their base types — -/// refinements are surface-level facts about values, not part of an -/// operator's arrow contract. -/// -/// Also used by [`crate::emit::operator_carrier_realization`] so emit -/// and infer share one refinement-stripping rule (no drift). -/// -/// Terminates at the first un-refined declaration OR when the chain -/// can no longer be followed (any non-ResolvedIdentifier connective). -/// Depth-bounded by `WALK_DEPTH_LIMIT`. -pub(crate) fn strip_refinement_to_base(dag: &Dag, decl_id: DeclarationId) -> DeclarationId { - let mut current = decl_id; - for _ in 0..WALK_DEPTH_LIMIT { - let decl = dag.declaration(current); - if decl.refinement.is_none() { - return current; - } - match &decl.connective { - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - current = *next; - } - _ => return current, - } - } - current -} - -/// Read an algebra field's Arrow signature and substitute the -/// algebra's receiver type parameter to the source declaration. -/// -/// The algebra field `field_ty` points at an Arrow declaration -/// emitted by `type_to_declaration_id` when lowering the algebra's -/// record type (e.g., `OrderedRing`'s `add: fn(T, T) -> T`). -/// The Arrow's input/output DeclarationIds reference the algebra's -/// receiver type parameter directly. Substitution replaces those -/// references with `source_id`; non-receiver positions walk to -/// their named anchor via the same walk `walk_to_type_shape` does. -fn read_algebra_field( - dag: &mut Dag, - algebra_decl: &crate::dag::Declaration, - field_ty: DeclarationId, - source_id: DeclarationId, - op_kind: OperatorKind, - lhs_type: &TypeShape, -) -> Option { - // The algebra's first type parameter is the receiver. For - // `OrderedRing` this is T's DeclarationId. - let receiver_param = algebra_decl.type_params.first().copied(); - // Unwrap the field's connective to the underlying Arrow. The - // field declaration was emitted by `type_to_declaration_id`'s - // Arrow arm as `TypeConnective::Arrow { inputs, output, body }`. - let field_decl = dag.declaration(field_ty); - let (arrow_inputs, arrow_output, arrow_body) = match &field_decl.connective { - TypeConnective::Arrow { - inputs, - output, - body, - } => (inputs.clone(), *output, body.clone()), - // Algebra field that isn't an Arrow (e.g., the `zero: T` - // constant). Not a callable operator — fall back. - _ => return None, - }; - // Translate each input / output DeclarationId through the - // receiver-substitution rule. - let input_shapes: Vec = arrow_inputs - .iter() - .map(|id| substitute_receiver(dag, *id, receiver_param, source_id).map(TypeShape::new)) - .collect::>()?; - let output_shape = - substitute_receiver(dag, arrow_output, receiver_param, source_id).map(TypeShape::new)?; - // Sanity check: the arity is always 2 for binary operators. - // If algebra.dag ever declares a field under one of our - // operator names with a different arity, the check downstream - // in `decide_transform` catches the mismatch as a - // `Diagnostic::ArityMismatch`, but we also want to preserve - // `lhs_type` in the signature as a debug fallback if the walk - // succeeds but the shape is unexpectedly wrong. - let _ = lhs_type; - let _ = op_kind; - Some(ResolvedArrow { - inputs: input_shapes, - output: output_shape, - body: arrow_body, - }) -} - -/// Substitute a declaration id through the receiver-substitution -/// rule: if the id matches the algebra's receiver type parameter, -/// return the source type; otherwise walk to the nearest named -/// declaration (same model as `walk_to_type_shape` but with the -/// receiver case short-circuited). -fn substitute_receiver( - dag: &mut Dag, - current: DeclarationId, - receiver_param: Option, - source_id: DeclarationId, -) -> Option { - if Some(current) == receiver_param { - return Some(source_id); - } - let decl = dag.declaration(current).clone(); - match &decl.connective { - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - substitute_receiver(dag, *next, receiver_param, source_id) - } - TypeConnective::Instantiation { - template, - arguments, - } => { - let mut new_args: Vec = Vec::with_capacity(arguments.len()); - let mut any_change = false; - for arg in arguments { - let new_val = substitute_receiver(dag, arg.value, receiver_param, source_id)?; - if new_val != arg.value { - any_change = true; - } - new_args.push(TemplateArgument { - parameter: arg.parameter, - value: new_val, - }); - } - if !any_change { - return Some(current); - } - // Same dedup contract as `div_total_result_output_shape`: false negatives - // here allocate unbounded anonymous instantiations across fixpoint iterations. - if let Some(existing) = find_equivalent_anonymous_instantiation( - dag, - *template, - &new_args, - decl.nominal_opacity.as_ref(), - ) { - return Some(existing); - } - let id = dag.alloc_declaration_id(); - let span = decl.span.clone(); - dag.push_declaration(Declaration { - id, - name: None, - connective: TypeConnective::Instantiation { - template: *template, - arguments: new_args, - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: decl.nominal_opacity.clone(), - span, - }); - Some(id) - } - TypeConnective::Atom(AtomPayload::TypeParam(_)) - | TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(_)) => None, - _ => { - if decl.name.is_some() { - Some(current) - } else { - None - } - } - } -} - -fn canonical_result_decl(dag: &Dag) -> Option<&Declaration> { - dag.declarations() - .iter() - .find(|decl| substrate_result_type_decl_suppressed_for_emit(dag, decl)) -} - -fn canonical_div_error_decl(dag: &Dag) -> Option<&Declaration> { - dag.declarations() - .iter() - .find(|decl| substrate_div_error_type_decl_suppressed_for_emit(dag, decl)) -} - -/// Dispatch-time invariant check on an `ExternalRealization` target: -/// the linked declaration must be a `Conj` with a non-`None` -/// `meta_tag` edge. The meta_tag IS the realization marker — no -/// further name/id comparison is required. -/// -/// **Round-10 correction.** Earlier revisions compared `meta_tag` -/// against a cached `Dag::realization_meta_id()` pointing at a -/// `Realization` declaration. Production bootstrap doesn't load a -/// `Realization` declaration (realization facts live in -/// `dsl/extdeps/languages/*` per the thesis, not in the std/ set -/// the M1(2.7) bootstrap consumes), so the cache was always `None` -/// and the check always failed. The shape check is now purely -/// structural: "Conj with a meta_tag" is the realization marker. -/// The structural shape is what `bootstrap::assert_realization_shape` -/// and the `#[cfg(test)]` realization smoke test already validate -/// at construction time. Any drift is caught at both construction -/// and dispatch. -fn is_realization_shape(dag: &Dag, realization_id: DeclarationId) -> bool { - let decl = dag.declaration(realization_id); - if !matches!(decl.connective, TypeConnective::Conj { .. }) { - return false; - } - decl.meta_tag.is_some() -} - -/// Substrate walk: given a DeclarationId, descend through -/// `Instantiation` (pushing subst frames), `Atom(Identifier { resolved -/// })` (following the link), and `Atom(TypeParam)` (looking up the -/// subst stack) until an `Arrow` is reached. At the Arrow, substitute -/// each input and output DeclarationId through the subst stack and -/// bridge to `TypeShape` via `walk_to_type_shape`. -fn resolve_arrow_walk( - dag: &Dag, - current: DeclarationId, - subst: &mut SubstStack, - depth: usize, -) -> Option { - if depth >= WALK_DEPTH_LIMIT { - return None; - } - let decl = dag.declaration(current); - match &decl.connective { - TypeConnective::Arrow { - inputs, - output, - body, - } => { - let mut input_shapes: Vec = Vec::new(); - for id in inputs { - if subst.lookup(*id).is_some() && declaration_is_callable(dag, *id, depth + 1) { - continue; - } - input_shapes.push(signature_type_shape(dag, *id, subst, depth + 1)?); - } - let output_shape = signature_type_shape(dag, *output, subst, depth + 1)?; - Some(ResolvedArrow { - inputs: input_shapes, - output: output_shape, - body: body.clone(), - }) - } - TypeConnective::Instantiation { - template, - arguments, - } => { - subst.push(arguments.clone()); - let result = resolve_arrow_walk(dag, *template, subst, depth + 1); - subst.pop(); - result - } - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - resolve_arrow_walk(dag, *next, subst, depth + 1) - } - // Terminal non-Arrow cases. Each is unreachable in well-formed - // user code; enumerated explicitly (rather than `_ => None`) so - // that any future `TypeConnective` or `AtomPayload` variant - // forces consideration here instead of silently falling through. - TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(_)) => None, - TypeConnective::Atom(AtomPayload::TypeParam(_)) => None, - TypeConnective::Atom(AtomPayload::Literal(_)) => None, - TypeConnective::Conj { .. } => None, - TypeConnective::Disj { .. } => None, - TypeConnective::Cardinality(_) => None, - } -} - -/// Walk a type declaration down to a port-level `TypeShape`. -/// -/// `TypeShape` is a newtype around `DeclarationId` — port types ARE -/// declaration identities. The walk descends through anonymous -/// declarations until it finds a stable type identity: -/// -/// - named top-level declarations keep their own `DeclarationId` -/// - anonymous instantiations keep THEIR instantiation id so -/// `List` and bare `List` do not collapse to the same shape -/// - type params resolve through the substitution stack when bound -/// -/// There is no name-keyed bridge back to a coarse primitive tag — the -/// declaration graph IS the type identity. -fn walk_to_type_shape( - dag: &Dag, - current: DeclarationId, - subst: &SubstStack, - depth: usize, -) -> Option { - if depth >= WALK_DEPTH_LIMIT { - return None; - } - let decl = dag.declaration(current); - // Named top-level declaration: this is the port's type identity. - if decl.name.is_some() { - return Some(TypeShape::new(current)); - } - // Anonymous declaration: follow the chain through the substrate. - match &decl.connective { - TypeConnective::Atom(AtomPayload::TypeParam(_)) => { - if let Some(bound) = subst.lookup(current) { - walk_to_type_shape(dag, bound, subst, depth + 1) - } else { - Some(TypeShape::new(current)) - } - } - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - walk_to_type_shape(dag, *next, subst, depth + 1) - } - TypeConnective::Instantiation { .. } => { - resolve_decl_with_subst(dag, current, subst, depth + 1) - .map(TypeShape::new) - .or_else(|| Some(TypeShape::new(current))) - } - // Terminal non-follow cases. An anonymous `UnresolvedIdentifier` - // means the sweep did not resolve the reference — the phantom - // diagnostic is already attached, and this walk fails so the - // caller can surface it. Anonymous optionals still need port - // identities today because reflected substrate fields such as - // `DagPort.produced_by: NodeId?` are legal field-project outputs. - // The broader anonymous structural cases stay fail-closed until - // port-type extension work admits them deliberately. - // Enumerated explicitly (rather than `_ => None`) so any - // future variant forces consideration here. - TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(_)) => None, - TypeConnective::Atom(AtomPayload::Literal(_)) => None, - TypeConnective::Conj { .. } => None, - TypeConnective::Disj { .. } => None, - TypeConnective::Arrow { .. } => None, - TypeConnective::Cardinality(_) => Some(TypeShape::new(current)), - } -} - -fn signature_type_shape( - dag: &Dag, - current: DeclarationId, - subst: &SubstStack, - depth: usize, -) -> Option { - if depth >= WALK_DEPTH_LIMIT { - return None; - } - let decl = dag.declaration(current); - if decl.name.is_some() { - return Some(TypeShape::new(current)); - } - // DB-16 (3a.3 closure): read-only lookup gate. When the refinement - // base requires substitution, check whether - // `materialize_callable_signature_instantiations` has already - // produced a substituted-refined carrier for this - // (template_refined, subst) combination. If so, return that - // carrier. Construction lives at the phase; this gate reads. - if decl.refinement.is_some() && refinement_base_requires_substitution(dag, current, subst) { - if let Some(materialized) = find_equivalent_substituted_refined_decl(dag, current, subst) { - return Some(TypeShape::new(materialized)); - } - // Lookup miss: the phase didn't materialize for this - // combination (TypeParam unbound at phase time, or call-site - // outside the phase walk). Fall through to the DB-11 - // identity-terminator; downstream `is_retryable_generic_decl` - // classifies as retry. - } - // DB-11 (3a.3): refinement-bearing declarations are identity - // terminators for signature walks. Without this, a refined param - // type `Int where d != 0` (connective: ResolvedIdentifier(Int), - // refinement: Some(pred)) would be walked to its base `Int`, and - // the call-site refinement-discharge check in `decide_transform` - // would never see the predicate edge on the callee side. - if decl.refinement.is_some() { - return Some(TypeShape::new(current)); - } - match &decl.connective { - TypeConnective::Instantiation { .. } => { - resolve_decl_with_subst(dag, current, subst, depth + 1) - .map(TypeShape::new) - .or_else(|| Some(TypeShape::new(current))) - } - TypeConnective::Atom(AtomPayload::TypeParam(_)) => { - if let Some(bound) = subst.lookup(current) { - signature_type_shape(dag, bound, subst, depth + 1) - } else { - Some(TypeShape::new(current)) - } - } - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - signature_type_shape(dag, *next, subst, depth + 1) - } - TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(_)) => None, - TypeConnective::Atom(AtomPayload::Literal(_)) => None, - TypeConnective::Conj { .. } => None, - TypeConnective::Disj { .. } => None, - TypeConnective::Arrow { .. } => None, - // Cardinality-typed signatures (e.g., `fn port(d, id) -> DagPort?`) - // keep the anonymous Cardinality declaration id as the port's type - // identity. Mirrors `walk_to_type_shape`'s Cardinality case — - // optional returns are legal throughout the substrate. - TypeConnective::Cardinality(_) => Some(TypeShape::new(current)), - } -} - -fn resolve_decl_with_subst( - dag: &Dag, - current: DeclarationId, - subst: &SubstStack, - depth: usize, -) -> Option { - if depth >= WALK_DEPTH_LIMIT { - return None; - } - let decl = dag.declaration(current); - match &decl.connective { - TypeConnective::Atom(AtomPayload::TypeParam(_)) => subst - .lookup(current) - .and_then(|bound| resolve_decl_with_subst(dag, bound, subst, depth + 1)) - .or(Some(current)), - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - resolve_decl_with_subst(dag, *next, subst, depth + 1) - } - TypeConnective::Instantiation { - template, - arguments, - } => { - let specialized_arguments: Vec = arguments - .iter() - .map(|arg| { - Some(TemplateArgument { - parameter: arg.parameter, - value: resolve_decl_with_subst(dag, arg.value, subst, depth + 1)?, - }) - }) - .collect::>()?; - if specialized_arguments - .iter() - .zip(arguments.iter()) - .all(|(lhs, rhs)| lhs.parameter == rhs.parameter && lhs.value == rhs.value) - { - return Some(current); - } - find_equivalent_decl_instantiation(dag, *template, &specialized_arguments) - .or(Some(current)) - } - TypeConnective::Cardinality(p) => { - let element = p.element(); - let bound = p.bound(); - let specialized_element = resolve_decl_with_subst(dag, element, subst, depth + 1)?; - if bound == crate::dag::CardinalityBound::AtMostOne { - if let Some(idem) = - crate::dag::cardinality_idempotent_target(dag, specialized_element, bound) - { - return Some(idem); - } - } - if specialized_element == element { - return Some(current); - } - find_equivalent_decl_cardinality(dag, specialized_element, &bound).or(Some(current)) - } - _ => Some(current), - } -} - -fn find_equivalent_decl_instantiation( - dag: &Dag, - template: DeclarationId, - arguments: &[TemplateArgument], -) -> Option { - dag.declarations().iter().find_map(|decl| { - let TypeConnective::Instantiation { - template: existing_template, - arguments: existing_arguments, - } = &decl.connective - else { - return None; - }; - (template == *existing_template - && existing_arguments.len() == arguments.len() - && existing_arguments - .iter() - .zip(arguments.iter()) - .all(|(lhs, rhs)| lhs.parameter == rhs.parameter && lhs.value == rhs.value)) - .then_some(decl.id) - }) -} - -fn find_equivalent_decl_cardinality( - dag: &Dag, - element: DeclarationId, - bound: &crate::dag::CardinalityBound, -) -> Option { - dag.declarations().iter().find_map(|decl| { - let TypeConnective::Cardinality(existing) = &decl.connective else { - return None; - }; - (element == existing.element() && existing.bound() == *bound).then_some(decl.id) - }) -} - -/// Best-effort human-readable name for a Transform.target DeclarationId, -/// used in diagnostics. Walks through resolved Identifier atoms to find -/// something nameable. -fn target_display_name(dag: &Dag, target: DeclarationId) -> String { - let decl = dag.declaration(target); - if let Some(name) = &decl.name { - return name.clone(); - } - match &decl.connective { - TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(name)) => name.clone(), - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - target_display_name(dag, *next) - } - _ => format!("declaration#{}", target.raw()), - } -} - -/// Best-effort human-readable name for a `TransformTarget`. Dispatches -/// on the variant: `Callable` goes through `target_display_name`, -/// `Operator` renders the source symbol directly. -fn transform_target_display_name(dag: &Dag, target: &TransformTarget) -> String { - match target { - TransformTarget::Callable(id) => target_display_name(dag, *id), - TransformTarget::UnresolvedFieldProject { field_label } => format!(".{field_label}"), - TransformTarget::ResolvedFieldProject { field_label } => format!(".{field_label}"), - TransformTarget::Operator(op_kind) => crate::operators::symbol(*op_kind), - } -} - -fn node_span_for_port(dag: &Dag, port: PortId) -> Option { - dag.port(port) - .produced_by - .map(|node_id| dag.node(node_id).span().clone()) -} - -fn synthetic_span() -> SourceSpan { - SourceSpan::new("", 0, 0) -} - -pub(crate) fn type_shapes_equivalent(dag: &Dag, lhs: &TypeShape, rhs: &TypeShape) -> bool { - if lhs == rhs { - return true; - } - declaration_shapes_equivalent(dag, lhs.declaration, rhs.declaration, 0) -} - -fn declaration_shapes_equivalent( - dag: &Dag, - lhs: DeclarationId, - rhs: DeclarationId, - depth: usize, -) -> bool { - if lhs == rhs { - return true; - } - if depth >= WALK_DEPTH_LIMIT { - return false; - } - let lhs_decl = dag.declaration(lhs); - let rhs_decl = dag.declaration(rhs); - match (&lhs_decl.connective, &rhs_decl.connective) { - ( - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)), - _, - ) => declaration_shapes_equivalent(dag, *next, rhs, depth + 1), - ( - _, - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)), - ) => declaration_shapes_equivalent(dag, lhs, *next, depth + 1), - ( - TypeConnective::Instantiation { - template: lhs_template, - arguments: lhs_arguments, - }, - TypeConnective::Instantiation { - template: rhs_template, - arguments: rhs_arguments, - }, - ) => { - declaration_shapes_equivalent(dag, *lhs_template, *rhs_template, depth + 1) - && phantom_arguments_equivalent_for_shape( - dag, - *lhs_template, - lhs_arguments, - *rhs_template, - rhs_arguments, - ) - && lhs_arguments.len() == rhs_arguments.len() - && lhs_arguments - .iter() - .zip(rhs_arguments.iter()) - .all(|(lhs_arg, rhs_arg)| { - declaration_shapes_equivalent(dag, lhs_arg.value, rhs_arg.value, depth + 1) - }) - } - (TypeConnective::Cardinality(lhs_p), TypeConnective::Cardinality(rhs_p)) => { - lhs_p.bound() == rhs_p.bound() - && declaration_shapes_equivalent(dag, lhs_p.element(), rhs_p.element(), depth + 1) - } - ( - TypeConnective::Conj { - children: lhs_children, - }, - TypeConnective::Conj { - children: rhs_children, - }, - ) => { - lhs_children.len() == rhs_children.len() - && lhs_children - .iter() - .zip(rhs_children.iter()) - .all(|(lhs_child, rhs_child)| { - lhs_child.label == rhs_child.label - && declaration_shapes_equivalent( - dag, - lhs_child.ty, - rhs_child.ty, - depth + 1, - ) - }) - } - ( - TypeConnective::Disj { - variants: lhs_variants, - }, - TypeConnective::Disj { - variants: rhs_variants, - }, - ) => { - lhs_variants.len() == rhs_variants.len() - && lhs_variants - .iter() - .zip(rhs_variants.iter()) - .all(|(lhs_variant, rhs_variant)| { - lhs_variant.label == rhs_variant.label - && declaration_shapes_equivalent( - dag, - lhs_variant.ty, - rhs_variant.ty, - depth + 1, - ) - }) - } - _ => false, - } -} - -fn phantom_arguments_equivalent_for_shape( - dag: &Dag, - lhs_template: DeclarationId, - lhs_arguments: &[TemplateArgument], - rhs_template: DeclarationId, - rhs_arguments: &[TemplateArgument], -) -> bool { - phantom_arguments_preserved_from_template( - dag, - lhs_template, - lhs_arguments, - rhs_template, - rhs_arguments, - ) && phantom_arguments_preserved_from_template( - dag, - rhs_template, - rhs_arguments, - lhs_template, - lhs_arguments, - ) -} - -fn phantom_arguments_preserved_from_template( - dag: &Dag, - source_template: DeclarationId, - source_arguments: &[TemplateArgument], - target_template: DeclarationId, - target_arguments: &[TemplateArgument], -) -> bool { - let source_decl = dag.declaration(source_template); - for phantom in &source_decl.phantom_params { - if require_abelian_group_phantom_algebra( - dag, - phantom.algebra, - phantom.parameter, - &synthetic_span(), - ) - .is_err() - { - return false; - } - let Some(target_phantom) = - mapped_phantom_parameter(dag, source_template, phantom, target_template) - else { - return false; - }; - if require_abelian_group_phantom_algebra( - dag, - target_phantom.algebra, - target_phantom.parameter, - &synthetic_span(), - ) - .is_err() - { - return false; - } - if !phantom_algebras_equivalent(dag, phantom.algebra, target_phantom.algebra) { - return false; - } - let Some(source_arg) = source_arguments - .iter() - .find(|arg| arg.parameter == phantom.parameter) - else { - return false; - }; - let Some(target_arg) = target_arguments - .iter() - .find(|arg| arg.parameter == target_phantom.parameter) - else { - return false; - }; - if source_arg.value != target_arg.value { - return false; - } - } - true -} - -#[cfg(test)] -mod bool_logical_operator_arrow_tests { - use super::*; - use crate::dag::PhantomParameter; - use crate::infer_helpers::filter_non_self_template_arguments; - use crate::operators::{ArithmeticOp, ComparisonOp}; - - #[test] - fn nominal_opaque_field_project_fails_closed_before_structural_descent() { - let mut dag = Dag::new(); - let span = SourceSpan::new("", 0, 1); - let int = dag.int_shape().expect("bootstrap Int").declaration; - - let payload = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: payload, - name: Some("OpaquePayload".to_string()), - connective: TypeConnective::Conj { - children: vec![Field { - label: "value".to_string(), - ty: int, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - - let opaque = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: opaque, - name: Some("OpaqueWrapper".to_string()), - connective: TypeConnective::Atom(AtomPayload::ResolvedByStructure(payload)), - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: Some(NominalOpacity { - permitted_accessors: Vec::new(), - }), - span: span.clone(), - }); - - let input = dag.alloc_port_with_shape(TypeShape::new(opaque)); - let output = dag.push_transform( - TransformTarget::UnresolvedFieldProject { - field_label: "value".to_string(), - }, - vec![input], - span, - ); - - infer(&mut dag); - - assert!(matches!( - dag.diagnostics().get(output), - Some(Diagnostic::NominalOpacityViolation { - declaration, - accessor: None, - .. - }) if *declaration == opaque - )); - assert!( - matches!(dag.port(output).state(), PortState::Unresolved), - "opacity violation must leave the projected output unresolved" - ); - } - - #[test] - fn nominal_opaque_field_project_preserves_instantiation_substitution() { - let mut dag = Dag::new(); - let span = SourceSpan::new("", 0, 1); - let int = dag.int_shape().expect("bootstrap Int").declaration; - - let payload = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: payload, - name: Some("SecretPayloadForSubst".to_string()), - connective: TypeConnective::Conj { - children: vec![Field { - label: "value".to_string(), - ty: int, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let secret = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: secret, - name: Some("SecretForSubst".to_string()), - connective: TypeConnective::Atom(AtomPayload::ResolvedByStructure(payload)), - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: Some(NominalOpacity { - permitted_accessors: Vec::new(), - }), - span: span.clone(), - }); - let box_param = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: box_param, - name: None, - connective: TypeConnective::Atom(AtomPayload::TypeParam("T".to_string())), - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let box_alias = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: box_alias, - name: Some("BoxAlias".to_string()), - connective: TypeConnective::Atom(AtomPayload::ResolvedByStructure(box_param)), - type_params: vec![box_param], - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let boxed_secret = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: boxed_secret, - name: Some("BoxedSecret".to_string()), - connective: TypeConnective::Instantiation { - template: box_alias, - arguments: vec![TemplateArgument { - parameter: box_param, - value: secret, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - - let input = dag.alloc_port_with_shape(TypeShape::new(boxed_secret)); - let output = dag.push_transform( - TransformTarget::UnresolvedFieldProject { - field_label: "value".to_string(), - }, - vec![input], - span, - ); - - infer(&mut dag); - - assert!(matches!( - dag.diagnostics().get(output), - Some(Diagnostic::NominalOpacityViolation { - declaration, - accessor: None, - .. - }) if *declaration == secret - )); - } - - #[test] - fn field_project_without_nominal_opacity_remains_structural() { - let mut dag = Dag::new(); - let span = SourceSpan::new("", 0, 1); - let int = dag.int_shape().expect("bootstrap Int").declaration; - - let record = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: record, - name: Some("PlainPayload".to_string()), - connective: TypeConnective::Conj { - children: vec![Field { - label: "value".to_string(), - ty: int, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - - let input = dag.alloc_port_with_shape(TypeShape::new(record)); - let output = dag.push_transform( - TransformTarget::UnresolvedFieldProject { - field_label: "value".to_string(), - }, - vec![input], - span, - ); - - infer(&mut dag); - - assert!( - dag.diagnostics().get(output).is_none(), - "plain structural projection should not emit opacity diagnostics" - ); - assert!(matches!( - dag.port(output).state(), - PortState::Resolved(shape) if shape.declaration == int - )); - } - - #[test] - fn bootstrapped_secret_is_marked_nominal_opaque() { - let dag = Dag::new(); - let secret = dag - .declaration_by_name("Secret") - .expect("bootstrap Secret declaration"); - assert!( - secret.nominal_opacity.is_some(), - "Secret must consume the nominal-opacity carrier before Modeling dispatch" - ); - } - - #[test] - fn arithmetic_on_non_algebra_lhs_fails_closed_instead_of_synthesizing_arrow() { - // Director audit (post-merge finding): pre-fix, when the - // structural walk in `resolve_operator_arrow` did not encounter - // any algebra Conj on the LHS (e.g. an Arrow / Disj / Atom with - // no `inhabits` link), the fallback fabricated `(T,T)->T` for - // arithmetic and `(T,T)->Bool` for comparison. That invented - // an operator-rule arrow contract for a type that declares - // none. With the fail-closed check in place, the resolver - // returns None so the call site emits a typed - // unsupported-operator diagnostic. - let mut dag = Dag::new(); - let span = SourceSpan::new("", 0, 1); - let int = dag.int_shape().expect("bootstrap Int").declaration; - - // A non-algebra LHS: an anonymous Arrow declaration. No - // `inhabits`, no Conj on the walk path. - let arrow_decl = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: arrow_decl, - name: None, - connective: TypeConnective::Arrow { - inputs: vec![int], - output: int, - body: ArrowBody::NoBody, - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let lhs = TypeShape::new(arrow_decl); - - assert!( - resolve_operator_arrow(&mut dag, OperatorKind::Arithmetic(ArithmeticOp::Add), &lhs,) - .is_none(), - "`+` on a non-algebra LHS must fail-closed: the pre-fix \ - fallback fabricated `(Arrow,Arrow)->Arrow` instead of \ - surfacing an unsupported-operator diagnostic" - ); - assert!( - resolve_operator_arrow(&mut dag, OperatorKind::Comparison(ComparisonOp::Lt), &lhs,) - .is_none(), - "`<` on a non-algebra LHS must fail-closed: the pre-fix \ - fallback fabricated `(Arrow,Arrow)->Bool` instead of \ - surfacing an unsupported-operator diagnostic" - ); - assert!( - resolve_operator_arrow(&mut dag, OperatorKind::Arithmetic(ArithmeticOp::Div), &lhs,) - .is_none(), - "`/` on a non-algebra LHS must fail-closed: the pre-fix \ - Div fallback synthesized `(Arrow,Arrow)->Result`" - ); - assert!( - resolve_operator_arrow(&mut dag, OperatorKind::Logical(LogicalOp::And), &lhs,) - .is_none(), - "`&&` on a non-algebra LHS must fail-closed: the pre-fix \ - logical fallback produced `(Bool,Bool)->Bool` regardless \ - of LHS, inventing a carrier the type does not declare" - ); - } - - #[test] - fn bool_logical_and_resolves_via_boolean_algebra_meet_not_pending_fallback() { - let mut dag = Dag::new(); - let bool_shape = dag.bool_shape().expect("bootstrap Bool"); - let sig = - resolve_operator_arrow(&mut dag, OperatorKind::Logical(LogicalOp::And), &bool_shape) - .expect("&& should resolve on Bool"); - assert!( - !matches!(sig.body, ArrowBody::Pending), - "expected Bool && via inhabits → BooleanAlgebra.meet, not Pending scaffold; got {:?}", - sig.body - ); - assert_eq!(sig.inputs.len(), 2); - assert_eq!(sig.inputs[0].declaration, bool_shape.declaration); - assert_eq!(sig.inputs[1].declaration, bool_shape.declaration); - assert_eq!(sig.output.declaration, bool_shape.declaration); - } - - #[test] - fn bool_logical_or_resolves_via_boolean_algebra_join_not_pending_fallback() { - let mut dag = Dag::new(); - let bool_shape = dag.bool_shape().expect("bootstrap Bool"); - let sig = - resolve_operator_arrow(&mut dag, OperatorKind::Logical(LogicalOp::Or), &bool_shape) - .expect("|| should resolve on Bool"); - assert!( - !matches!(sig.body, ArrowBody::Pending), - "expected Bool || via inhabits → BooleanAlgebra.join, not Pending scaffold; got {:?}", - sig.body - ); - assert_eq!(sig.inputs.len(), 2); - assert_eq!(sig.inputs[0].declaration, bool_shape.declaration); - assert_eq!(sig.inputs[1].declaration, bool_shape.declaration); - assert_eq!(sig.output.declaration, bool_shape.declaration); - } - - #[test] - fn arithmetic_operator_checks_abelian_phantom_unit_closure_before_type_mismatch() { - let mut dag = Dag::new(); - let span = SourceSpan::new("", 0, 1); - let int = dag.int_shape().expect("bootstrap Int").declaration; - let abelian_group = dag - .declaration_by_name("AbelianGroup") - .expect("bootstrap AbelianGroup") - .id; - - let currency = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: currency, - name: Some("Currency".to_string()), - connective: TypeConnective::Conj { children: vec![] }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let usd = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: usd, - name: Some("USD".to_string()), - connective: TypeConnective::Instantiation { - template: currency, - arguments: vec![], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let eur = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: eur, - name: Some("EUR".to_string()), - connective: TypeConnective::Instantiation { - template: currency, - arguments: vec![], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let currency_abelian_group = dag.alloc_declaration_id(); - let abelian_receiver = dag - .declaration(abelian_group) - .type_params - .first() - .copied() - .expect("AbelianGroup receiver type parameter"); - dag.push_declaration(Declaration { - id: currency_abelian_group, - name: Some("CurrencyAbelianGroup".to_string()), - connective: TypeConnective::Instantiation { - template: abelian_group, - arguments: vec![TemplateArgument { - parameter: abelian_receiver, - value: currency, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let int_abelian_group = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: int_abelian_group, - name: Some("IntAbelianGroup".to_string()), - connective: TypeConnective::Instantiation { - template: abelian_group, - arguments: vec![TemplateArgument { - parameter: abelian_receiver, - value: int, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let money = dag.alloc_declaration_id(); - let c_param = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: money, - name: Some("Money".to_string()), - connective: TypeConnective::Conj { - children: vec![Field { - label: "amount".to_string(), - ty: int, - }], - }, - type_params: vec![c_param], - phantom_params: vec![PhantomParameter { - parameter: c_param, - algebra: currency_abelian_group, - }], - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - dag.push_declaration(Declaration { - id: c_param, - name: None, - connective: TypeConnective::Atom(AtomPayload::TypeParam("C".to_string())), - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - - let money_usd = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: money_usd, - name: Some("MoneyUSD".to_string()), - connective: TypeConnective::Instantiation { - template: money, - arguments: vec![TemplateArgument { - parameter: c_param, - value: usd, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let money_eur = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: money_eur, - name: Some("MoneyEUR".to_string()), - connective: TypeConnective::Instantiation { - template: money, - arguments: vec![TemplateArgument { - parameter: c_param, - value: eur, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let money_c = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: money_c, - name: Some("MoneyC".to_string()), - connective: TypeConnective::Instantiation { - template: money, - arguments: vec![TemplateArgument { - parameter: c_param, - value: c_param, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let alt_c_param = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: alt_c_param, - name: None, - connective: TypeConnective::Atom(AtomPayload::TypeParam("AltC".to_string())), - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let alt_money = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: alt_money, - name: Some("AltMoney".to_string()), - connective: TypeConnective::Conj { - children: vec![Field { - label: "amount".to_string(), - ty: int, - }], - }, - type_params: vec![alt_c_param], - phantom_params: vec![PhantomParameter { - parameter: alt_c_param, - algebra: currency_abelian_group, - }], - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let alt_money_usd = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: alt_money_usd, - name: Some("AltMoneyUSD".to_string()), - connective: TypeConnective::Instantiation { - template: alt_money, - arguments: vec![TemplateArgument { - parameter: alt_c_param, - value: usd, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let alt_money_eur = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: alt_money_eur, - name: Some("AltMoneyEUR".to_string()), - connective: TypeConnective::Instantiation { - template: alt_money, - arguments: vec![TemplateArgument { - parameter: alt_c_param, - value: eur, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let other_algebra_money = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: other_algebra_money, - name: Some("OtherAlgebraMoney".to_string()), - connective: TypeConnective::Conj { - children: vec![Field { - label: "amount".to_string(), - ty: int, - }], - }, - type_params: vec![alt_c_param], - phantom_params: vec![PhantomParameter { - parameter: alt_c_param, - algebra: int_abelian_group, - }], - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let other_algebra_money_usd = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: other_algebra_money_usd, - name: Some("OtherAlgebraMoneyUSD".to_string()), - connective: TypeConnective::Instantiation { - template: other_algebra_money, - arguments: vec![TemplateArgument { - parameter: alt_c_param, - value: usd, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - - let lhs = dag.alloc_port_with_shape(TypeShape::new(money_usd)); - let rhs_same = dag.alloc_port_with_shape(TypeShape::new(money_usd)); - let rhs_other = dag.alloc_port_with_shape(TypeShape::new(money_eur)); - let cross_template_rhs_other = dag.alloc_port_with_shape(TypeShape::new(alt_money_eur)); - let ok_output = dag.push_transform( - TransformTarget::Operator(OperatorKind::Arithmetic(ArithmeticOp::Add)), - vec![lhs, rhs_same], - span.clone(), - ); - let bad_output = dag.push_transform( - TransformTarget::Operator(OperatorKind::Arithmetic(ArithmeticOp::Add)), - vec![lhs, rhs_other], - span.clone(), - ); - let cross_template_bad_output = dag.push_transform( - TransformTarget::Operator(OperatorKind::Arithmetic(ArithmeticOp::Add)), - vec![lhs, cross_template_rhs_other], - span.clone(), - ); - assert!( - type_shapes_equivalent( - &dag, - &TypeShape::new(money_usd), - &TypeShape::new(alt_money_usd) - ), - "structural equivalence should map phantom axes across equivalent templates" - ); - assert!( - !type_shapes_equivalent( - &dag, - &TypeShape::new(money_usd), - &TypeShape::new(other_algebra_money_usd), - ), - "structural equivalence must compare mapped phantom algebra authority" - ); - let money_missing_arg = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: money_missing_arg, - name: Some("MoneyMissingArg".to_string()), - connective: TypeConnective::Instantiation { - template: money, - arguments: Vec::new(), - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let malformed_lhs = dag.alloc_port_with_shape(TypeShape::new(money_missing_arg)); - let malformed_rhs = dag.alloc_port_with_shape(TypeShape::new(money_missing_arg)); - let malformed_output = dag.push_transform( - TransformTarget::Operator(OperatorKind::Arithmetic(ArithmeticOp::Add)), - vec![malformed_lhs, malformed_rhs], - span.clone(), - ); - let rogue_param = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: rogue_param, - name: None, - connective: TypeConnective::Atom(AtomPayload::TypeParam("Rogue".to_string())), - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let bad_money = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: bad_money, - name: Some("BadMoney".to_string()), - connective: TypeConnective::Conj { - children: vec![Field { - label: "amount".to_string(), - ty: int, - }], - }, - type_params: vec![c_param], - phantom_params: vec![PhantomParameter { - parameter: rogue_param, - algebra: currency_abelian_group, - }], - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let bad_money_usd = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: bad_money_usd, - name: Some("BadMoneyUSD".to_string()), - connective: TypeConnective::Instantiation { - template: bad_money, - arguments: vec![TemplateArgument { - parameter: rogue_param, - value: usd, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span, - }); - let invalid_lhs = dag.alloc_port_with_shape(TypeShape::new(bad_money_usd)); - let invalid_rhs = dag.alloc_port_with_shape(TypeShape::new(bad_money_usd)); - let invalid_output = dag.push_transform( - TransformTarget::Operator(OperatorKind::Arithmetic(ArithmeticOp::Add)), - vec![invalid_lhs, invalid_rhs], - SourceSpan::new("", 1, 2), - ); - let unsupported_algebra = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: unsupported_algebra, - name: Some("CurrencyMonoid".to_string()), - connective: TypeConnective::Conj { children: vec![] }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("", 2, 3), - }); - let unsupported_money = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: unsupported_money, - name: Some("UnsupportedMoney".to_string()), - connective: TypeConnective::Conj { - children: vec![Field { - label: "amount".to_string(), - ty: int, - }], - }, - type_params: vec![c_param], - phantom_params: vec![PhantomParameter { - parameter: c_param, - algebra: unsupported_algebra, - }], - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("", 2, 3), - }); - let unsupported_money_usd = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: unsupported_money_usd, - name: Some("UnsupportedMoneyUSD".to_string()), - connective: TypeConnective::Instantiation { - template: unsupported_money, - arguments: vec![TemplateArgument { - parameter: c_param, - value: usd, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("", 2, 3), - }); - let unsupported_lhs = dag.alloc_port_with_shape(TypeShape::new(unsupported_money_usd)); - let unsupported_rhs = dag.alloc_port_with_shape(TypeShape::new(unsupported_money_usd)); - let unsupported_output = dag.push_transform( - TransformTarget::Operator(OperatorKind::Arithmetic(ArithmeticOp::Add)), - vec![unsupported_lhs, unsupported_rhs], - SourceSpan::new("", 2, 3), - ); - let fake_abelian_group = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: fake_abelian_group, - name: Some("AbelianGroup".to_string()), - connective: TypeConnective::Conj { children: vec![] }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("", 3, 4), - }); - let fake_algebra_money = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: fake_algebra_money, - name: Some("FakeAlgebraMoney".to_string()), - connective: TypeConnective::Conj { - children: vec![Field { - label: "amount".to_string(), - ty: int, - }], - }, - type_params: vec![c_param], - phantom_params: vec![PhantomParameter { - parameter: c_param, - algebra: fake_abelian_group, - }], - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("", 3, 4), - }); - let fake_algebra_money_usd = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: fake_algebra_money_usd, - name: Some("FakeAlgebraMoneyUSD".to_string()), - connective: TypeConnective::Instantiation { - template: fake_algebra_money, - arguments: vec![TemplateArgument { - parameter: c_param, - value: usd, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("", 3, 4), - }); - let fake_lhs = dag.alloc_port_with_shape(TypeShape::new(fake_algebra_money_usd)); - let fake_rhs = dag.alloc_port_with_shape(TypeShape::new(fake_algebra_money_usd)); - let fake_output = dag.push_transform( - TransformTarget::Operator(OperatorKind::Arithmetic(ArithmeticOp::Add)), - vec![fake_lhs, fake_rhs], - SourceSpan::new("", 3, 4), - ); - let accept_eur = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: accept_eur, - name: Some("accept_eur".to_string()), - connective: TypeConnective::Arrow { - inputs: vec![money_eur], - output: money_eur, - body: ArrowBody::NoBody, - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("", 3, 4), - }); - let callable_lhs = dag.alloc_port_with_shape(TypeShape::new(money_usd)); - let callable_output = dag.push_transform( - TransformTarget::Callable(accept_eur), - vec![callable_lhs], - SourceSpan::new("", 3, 4), - ); - let generic_accept_money = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: generic_accept_money, - name: Some("accept_money".to_string()), - connective: TypeConnective::Arrow { - inputs: vec![money_c], - output: money_c, - body: ArrowBody::NoBody, - }, - type_params: vec![c_param], - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("", 4, 5), - }); - let generic_callable_lhs = dag.alloc_port_with_shape(TypeShape::new(money_usd)); - let generic_callable_output = dag.push_transform( - TransformTarget::Callable(generic_accept_money), - vec![generic_callable_lhs], - SourceSpan::new("", 4, 5), - ); - let untracked_param = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: untracked_param, - name: None, - connective: TypeConnective::Atom(AtomPayload::TypeParam("U".to_string())), - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("", 4, 5), - }); - let untracked_money = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: untracked_money, - name: Some("UntrackedMoney".to_string()), - connective: TypeConnective::Conj { - children: vec![Field { - label: "amount".to_string(), - ty: int, - }], - }, - type_params: vec![untracked_param], - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("", 4, 5), - }); - let untracked_money_usd = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: untracked_money_usd, - name: Some("UntrackedMoneyUSD".to_string()), - connective: TypeConnective::Instantiation { - template: untracked_money, - arguments: vec![TemplateArgument { - parameter: untracked_param, - value: usd, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: SourceSpan::new("", 4, 5), - }); - let untracked_callable_lhs = dag.alloc_port_with_shape(TypeShape::new(untracked_money_usd)); - let untracked_callable_output = dag.push_transform( - TransformTarget::Callable(accept_eur), - vec![untracked_callable_lhs], - SourceSpan::new("", 4, 5), - ); - assert!( - !type_shapes_equivalent( - &dag, - &TypeShape::new(untracked_money_usd), - &TypeShape::new(money_eur), - ), - "structural equivalence must preserve phantom axes from both templates" - ); - - infer(&mut dag); - - assert_eq!( - dag.port(ok_output).state(), - &PortState::Resolved(TypeShape::new(money_usd)) - ); - let bad_diag = dag - .diagnostics() - .get(bad_output) - .expect("mismatched phantom unit should fail on operator output"); - assert!( - matches!( - bad_diag, - Diagnostic::UnitMismatch { - parameter, - expected, - actual, - .. - } if parameter == "C" - && expected.declaration == usd - && actual.declaration == eur - ), - "unexpected diagnostic: {bad_diag:?}" - ); - let cross_template_bad_diag = dag - .diagnostics() - .get(cross_template_bad_output) - .expect("mapped phantom units should fail across structurally equivalent templates"); - assert!( - matches!( - cross_template_bad_diag, - Diagnostic::UnitMismatch { - parameter, - expected, - actual, - .. - } if parameter == "C" - && expected.declaration == usd - && actual.declaration == eur - ), - "unexpected diagnostic: {cross_template_bad_diag:?}" - ); - let malformed_diag = dag - .diagnostics() - .get(malformed_output) - .expect("missing phantom instantiation argument should fail closed"); - assert!( - matches!( - malformed_diag, - Diagnostic::ResolveError { name, .. } - if name.contains("missing expected argument for phantom parameter C") - ), - "unexpected diagnostic: {malformed_diag:?}" - ); - let invalid_diag = dag - .diagnostics() - .get(invalid_output) - .expect("phantom parameter outside type_params should fail closed"); - assert!( - matches!( - invalid_diag, - Diagnostic::ResolveError { name, .. } - if name.contains("parameter is not in type_params") - ), - "unexpected diagnostic: {invalid_diag:?}" - ); - let unsupported_diag = dag - .diagnostics() - .get(unsupported_output) - .expect("unsupported phantom algebra should fail closed"); - assert!( - matches!( - unsupported_diag, - Diagnostic::ResolveError { name, .. } - if name.contains("unsupported phantom algebra CurrencyMonoid") - ), - "unexpected diagnostic: {unsupported_diag:?}" - ); - let fake_diag = dag - .diagnostics() - .get(fake_output) - .expect("non-canonical declaration named AbelianGroup should fail closed"); - assert!( - matches!( - fake_diag, - Diagnostic::ResolveError { name, .. } - if name.contains("unsupported phantom algebra AbelianGroup") - ), - "unexpected diagnostic: {fake_diag:?}" - ); - let callable_diag = dag - .diagnostics() - .get(callable_output) - .expect("callable boundary should enforce phantom units"); - assert!( - matches!( - callable_diag, - Diagnostic::UnitMismatch { - operator, - parameter, - expected, - actual, - .. - } if operator == "accept_eur" - && parameter == "C" - && expected.declaration == eur - && actual.declaration == usd - ), - "unexpected diagnostic: {callable_diag:?}" - ); - assert!( - matches!( - dag.port(generic_callable_output).state(), - PortState::Resolved(_) - ), - "generic callable should not fail phantom unit checking before C can bind" - ); - assert!( - !matches!( - dag.diagnostics().get(generic_callable_output), - Some(Diagnostic::UnitMismatch { .. }) - ), - "generic callable must not emit UnitMismatch before C can bind" - ); - let untracked_callable_diag = dag - .diagnostics() - .get(untracked_callable_output) - .expect("structural wrapper without phantom axis should not satisfy phantom wrapper"); - assert!( - matches!( - untracked_callable_diag, - Diagnostic::TypeMismatch { .. } | Diagnostic::ResolveError { .. } - ), - "unexpected diagnostic: {untracked_callable_diag:?}" - ); - } - - #[test] - fn template_argument_reconciliation_helpers_match_and_update_consistently() { - let dag = Dag::new(); - let p0 = dag.bool_shape().expect("bootstrap Bool").declaration; - let p1 = dag.int_shape().expect("bootstrap Int").declaration; - let v0 = dag.string_shape().expect("bootstrap String").declaration; - let v1 = dag.bool_shape().expect("bootstrap Bool").declaration; - let mut arguments = vec![ - TemplateArgument { - parameter: p0, - value: p0, - }, - TemplateArgument { - parameter: p1, - value: v1, - }, - ]; - - assert!(push_template_argument_binding(&mut arguments, p0, v0)); - assert!(matches!( - generated_template_arguments_match( - &arguments, - &[ - TemplateArgument { - parameter: p0, - value: v0, - }, - TemplateArgument { - parameter: p1, - value: v1, - }, - ] - ), - TemplateArgumentsMatch::Match, - )); - assert!(!push_template_argument_binding(&mut arguments, p1, v0)); - } - - #[test] - fn template_arguments_match_generated_projection_agrees_across_length_and_value_cases() { - let dag = Dag::new(); - let p0 = dag.bool_shape().expect("bootstrap Bool").declaration; - let p1 = dag.int_shape().expect("bootstrap Int").declaration; - let v0 = dag.string_shape().expect("bootstrap String").declaration; - let v1 = dag.bool_shape().expect("bootstrap Bool").declaration; - let pair = |parameter, value| TemplateArgument { parameter, value }; - let is_match = |lhs: &[TemplateArgument], rhs: &[TemplateArgument]| -> bool { - matches!( - generated_template_arguments_match(lhs, rhs), - TemplateArgumentsMatch::Match, - ) - }; - - let empty: Vec = vec![]; - assert!(is_match(&empty, &empty)); - assert!(!is_match(&empty, &[pair(p0, v0)])); - assert!(!is_match(&[pair(p0, v0)], &empty)); - - let two = vec![pair(p0, v0), pair(p1, v1)]; - assert!(is_match(&two, &two.clone())); - assert!(!is_match(&two, &[pair(p0, v0), pair(p1, v0)])); - assert!(!is_match(&two, &[pair(p0, v0), pair(p0, v1)])); - } - - #[test] - fn filter_non_self_template_arguments_matches_handwritten_filter() { - let dag = Dag::new(); - let p0 = dag.bool_shape().expect("bootstrap Bool").declaration; - let p1 = dag.int_shape().expect("bootstrap Int").declaration; - let args = vec![ - TemplateArgument { - parameter: p0, - value: p0, - }, - TemplateArgument { - parameter: p1, - value: p0, - }, - TemplateArgument { - parameter: p1, - value: p1, - }, - ]; - let expected: Vec = args - .iter() - .filter(|a| a.parameter != a.value) - .cloned() - .collect(); - let actual = filter_non_self_template_arguments(&args); - assert_eq!(actual.len(), expected.len()); - for (a, e) in actual.iter().zip(expected.iter()) { - assert_eq!(a.parameter, e.parameter); - assert_eq!(a.value, e.value); - } - } - - /// SG-4b (callable-instantiation normalization cluster): exercise - /// `find_equivalent_anonymous_instantiation` through the real - /// declaration-table path so the generated template-argument - /// comparison is the one doing the dedup. - #[test] - fn find_equivalent_anonymous_instantiation_dedups_via_generated_comparison() { - let mut dag = Dag::new(); - let template = dag.bool_shape().expect("bootstrap Bool").declaration; - let p0 = dag.int_shape().expect("bootstrap Int").declaration; - let p1 = dag.string_shape().expect("bootstrap String").declaration; - let v0 = dag.int_shape().expect("bootstrap Int").declaration; - let v1 = dag.bool_shape().expect("bootstrap Bool").declaration; - - let args = vec![ - TemplateArgument { - parameter: p0, - value: v0, - }, - TemplateArgument { - parameter: p1, - value: v1, - }, - ]; - - let anon_id = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: anon_id, - name: None, - connective: TypeConnective::Instantiation { - template, - arguments: args.clone(), - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: synthetic_span(), - }); - - // Exact arguments match → dedup hit. - assert_eq!( - find_equivalent_anonymous_instantiation(&dag, template, &args, None), - Some(anon_id), - ); - - // One value differs → no hit. - let mut value_diff = args.clone(); - value_diff[1].value = v0; - assert_eq!( - find_equivalent_anonymous_instantiation(&dag, template, &value_diff, None), - None, - ); - - // Length differs → no hit. - assert_eq!( - find_equivalent_anonymous_instantiation(&dag, template, &args[..1], None), - None, - ); - - let opaque = NominalOpacity { - permitted_accessors: Vec::new(), - }; - assert_eq!( - find_equivalent_anonymous_instantiation(&dag, template, &args, Some(&opaque)), - None, - "anonymous instantiation interning must not collapse distinct nominal-opacity carriers" - ); - - let opaque_anon_id = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: opaque_anon_id, - name: None, - connective: TypeConnective::Instantiation { - template, - arguments: args.clone(), - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: Some(opaque.clone()), - span: synthetic_span(), - }); - assert_eq!( - find_equivalent_anonymous_instantiation(&dag, template, &args, Some(&opaque)), - Some(opaque_anon_id), - ); - let opaque_with_accessor = NominalOpacity { - permitted_accessors: vec![p0], - }; - assert_eq!( - find_equivalent_anonymous_instantiation( - &dag, - template, - &args, - Some(&opaque_with_accessor), - ), - None, - "permitted-accessor changes are part of the nominal-opacity interning key" - ); - - // Self-binding normalization: a `parameter == value` entry is a - // real structural distinction at this call site — - // `find_equivalent_anonymous_instantiation` does not strip self - // bindings itself; `normalized_instantiation_args` owns that. - // An instantiation with a self-binding added does not dedup - // against one without it, confirming the comparison is strict - // pairwise and the generated helper's length check fires. - let mut with_self = args.clone(); - with_self.push(TemplateArgument { - parameter: p0, - value: p0, - }); - assert_eq!( - find_equivalent_anonymous_instantiation(&dag, template, &with_self, None), - None, - ); - } - - #[test] - fn arithmetic_division_uses_totalized_result_shape() { - let mut dag = Dag::new(); - let int = dag.int_shape().expect("bootstrap Int").declaration; - let lhs = TypeShape::new(int); - let first = - resolve_operator_arrow(&mut dag, OperatorKind::Arithmetic(ArithmeticOp::Div), &lhs) - .expect("division must resolve"); - let after_first = dag.declarations().len(); - let second = - resolve_operator_arrow(&mut dag, OperatorKind::Arithmetic(ArithmeticOp::Div), &lhs) - .expect("division must resolve"); - - assert_eq!(first.output.declaration, second.output.declaration); - assert_eq!( - after_first, - dag.declarations().len(), - "div resolution should dedup anonymous instantiation", - ); - - let result_shape_decl = dag.declaration(first.output.declaration); - let TypeConnective::Instantiation { - template, - arguments, - } = &result_shape_decl.connective - else { - panic!("division output must remain an instantiation"); - }; - let std_result = canonical_result_decl(&dag).expect("std Result").id; - let std_div_error = canonical_div_error_decl(&dag).expect("std DivError").id; - assert_eq!(*template, std_result); - assert_eq!(arguments.len(), 2); - assert_eq!(arguments[0].value, int); - assert_eq!(arguments[1].value, std_div_error); - } - - #[test] - fn arithmetic_division_canonical_result_and_diverror_are_structural_not_path_keyed() { - let mut dag = Dag::new(); - let int = dag.int_shape().expect("bootstrap Int").declaration; - let result = canonical_result_decl(&dag).expect("bootstrap Result").id; - let div_error = canonical_div_error_decl(&dag) - .expect("bootstrap DivError") - .id; - - dag.declaration_mut(result).span.file = "renamed/std/errors.dag".to_string(); - dag.declaration_mut(div_error).span.file = "renamed/std/errors.dag".to_string(); - - let resolved = resolve_operator_arrow( - &mut dag, - OperatorKind::Arithmetic(ArithmeticOp::Div), - &TypeShape::new(int), - ) - .expect("division must resolve through structural error primitives"); - let TypeConnective::Instantiation { - template, - arguments, - } = &dag.declaration(resolved.output.declaration).connective - else { - panic!("division output must remain an instantiation"); - }; - - assert_eq!(*template, result); - assert_eq!(arguments.len(), 2); - assert_eq!(arguments[0].value, int); - assert_eq!(arguments[1].value, div_error); - assert_eq!( - canonical_result_decl(&dag).expect("structural Result").id, - result - ); - assert_eq!( - canonical_div_error_decl(&dag) - .expect("structural DivError") - .id, - div_error - ); - } - - #[test] - fn arithmetic_division_ignores_user_shadowed_result_carriers() { - let mut dag = Dag::new(); - let int = dag.int_shape().expect("bootstrap Int").declaration; - let span = synthetic_span(); - - let shadow_ok = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: shadow_ok, - name: None, - connective: TypeConnective::Atom(AtomPayload::TypeParam("Ok".to_string())), - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let shadow_err = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: shadow_err, - name: None, - connective: TypeConnective::Atom(AtomPayload::TypeParam("Err".to_string())), - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let shadow_result = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: shadow_result, - name: Some("Result".to_string()), - connective: TypeConnective::Disj { - variants: vec![ - Field { - label: "Ok".to_string(), - ty: shadow_ok, - }, - Field { - label: "Err".to_string(), - ty: shadow_err, - }, - ], - }, - type_params: vec![shadow_ok, shadow_err], - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let shadow_div_error = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: shadow_div_error, - name: Some("DivError".to_string()), - connective: TypeConnective::Disj { - variants: vec![ - Field { - label: "DivideByZero".to_string(), - ty: int, - }, - Field { - label: "Overflow".to_string(), - ty: int, - }, - ], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span, - }); - assert!( - !substrate_div_error_type_decl_suppressed_for_emit( - &dag, - dag.declaration(shadow_div_error) - ), - "payload-bearing DivError twin must not match the canonical unit-variant fingerprint", - ); - - let resolved = resolve_operator_arrow( - &mut dag, - OperatorKind::Arithmetic(ArithmeticOp::Div), - &TypeShape::new(int), - ) - .expect("division must resolve through std carrier"); - let TypeConnective::Instantiation { - template, - arguments, - } = &dag.declaration(resolved.output.declaration).connective - else { - panic!("division output must remain an instantiation"); - }; - - assert_ne!(*template, shadow_result); - assert_ne!(arguments[1].value, shadow_div_error); - assert_eq!( - *template, - canonical_result_decl(&dag).expect("std Result").id - ); - assert_eq!( - arguments[1].value, - canonical_div_error_decl(&dag).expect("std DivError").id - ); - } - - #[test] - fn arithmetic_division_prefers_bootstrap_error_primitives_over_exact_twins() { - let mut dag = Dag::new(); - let int = dag.int_shape().expect("bootstrap Int").declaration; - let std_result = canonical_result_decl(&dag).expect("std Result").id; - let std_div_error = canonical_div_error_decl(&dag).expect("std DivError").id; - let span = synthetic_span(); - - let twin_ok = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: twin_ok, - name: None, - connective: TypeConnective::Atom(AtomPayload::TypeParam("ok".to_string())), - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let twin_err = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: twin_err, - name: None, - connective: TypeConnective::Atom(AtomPayload::TypeParam("err".to_string())), - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let twin_ok_payload = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: twin_ok_payload, - name: None, - connective: TypeConnective::Conj { - children: vec![Field { - label: "value".to_string(), - ty: twin_ok, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let twin_err_payload = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: twin_err_payload, - name: None, - connective: TypeConnective::Conj { - children: vec![Field { - label: "value".to_string(), - ty: twin_err, - }], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let twin_result = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: twin_result, - name: Some("Result".to_string()), - connective: TypeConnective::Disj { - variants: vec![ - Field { - label: "Ok".to_string(), - ty: twin_ok_payload, - }, - Field { - label: "Err".to_string(), - ty: twin_err_payload, - }, - ], - }, - type_params: vec![twin_ok, twin_err], - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let twin_divide_by_zero = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: twin_divide_by_zero, - name: None, - connective: TypeConnective::Conj { children: vec![] }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let twin_overflow = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: twin_overflow, - name: None, - connective: TypeConnective::Conj { children: vec![] }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - let twin_div_error = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: twin_div_error, - name: Some("DivError".to_string()), - connective: TypeConnective::Disj { - variants: vec![ - Field { - label: "DivideByZero".to_string(), - ty: twin_divide_by_zero, - }, - Field { - label: "Overflow".to_string(), - ty: twin_overflow, - }, - ], - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span, - }); - - assert!(substrate_result_type_decl_suppressed_for_emit( - &dag, - dag.declaration(twin_result) - )); - assert!(substrate_div_error_type_decl_suppressed_for_emit( - &dag, - dag.declaration(twin_div_error) - )); - assert_eq!(canonical_result_decl(&dag).expect("Result").id, std_result); - assert_eq!( - canonical_div_error_decl(&dag).expect("DivError").id, - std_div_error - ); - - let resolved = resolve_operator_arrow( - &mut dag, - OperatorKind::Arithmetic(ArithmeticOp::Div), - &TypeShape::new(int), - ) - .expect("division must resolve through bootstrap error primitives"); - let TypeConnective::Instantiation { - template, - arguments, - } = &dag.declaration(resolved.output.declaration).connective - else { - panic!("division output must remain an instantiation"); - }; - assert_eq!(*template, std_result); - assert_eq!(arguments[1].value, std_div_error); - assert_ne!(*template, twin_result); - assert_ne!(arguments[1].value, twin_div_error); - } - - #[test] - fn read_algebra_field_fails_closed_on_unresolved_instantiation_argument() { - let mut dag = Dag::new(); - let span = synthetic_span(); - let int = dag.int_shape().expect("bootstrap Int").declaration; - - let receiver_param = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: receiver_param, - name: None, - connective: TypeConnective::Atom(AtomPayload::TypeParam("Self".to_string())), - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - - let unresolved = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: unresolved, - name: None, - connective: TypeConnective::Atom(AtomPayload::UnresolvedIdentifier( - "Missing".to_string(), - )), - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - - let arithmetic_field = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: arithmetic_field, - name: None, - connective: TypeConnective::Arrow { - inputs: vec![receiver_param, unresolved], - output: int, - body: ArrowBody::NoBody, - }, - type_params: Vec::new(), - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - - let algebra = dag.alloc_declaration_id(); - dag.push_declaration(Declaration { - id: algebra, - name: Some("DivProbeAlgebra".to_string()), - connective: TypeConnective::Conj { - children: vec![Field { - label: "div".to_string(), - ty: arithmetic_field, - }], - }, - type_params: vec![receiver_param], - phantom_params: Vec::new(), - meta_tag: None, - specialization_parent: None, - inhabits: None, - value_body: None, - refinement: None, - nominal_opacity: None, - span: span.clone(), - }); - - let before = dag.declarations().len(); - let algebra_decl = dag.declaration(algebra).clone(); - assert!(read_algebra_field( - &mut dag, - &algebra_decl, - arithmetic_field, - int, - OperatorKind::Arithmetic(ArithmeticOp::Div), - &TypeShape::new(int), - ) - .is_none()); - assert_eq!(dag.declarations().len(), before); - } -} diff --git a/tools/pb0_cycle4_emit_templates/pipeline_authority.rs.in b/tools/pb0_cycle4_emit_templates/pipeline_authority.rs.in deleted file mode 100644 index 70f86a7f828..00000000000 --- a/tools/pb0_cycle4_emit_templates/pipeline_authority.rs.in +++ /dev/null @@ -1,261 +0,0 @@ -use crate::dag::{Dag, DeclarationId, FieldValue, TypeConnective, ValueBody}; - -#[cfg(test)] -pub(crate) const PIPELINE_AUTHORITY_FILE: &str = "src/v3/compiler/pipeline.dag"; - -const PIPELINE_STAGE_BINDING_TYPE: &str = "PipelineStageBinding"; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum PipelineSnapshotKind { - Surface, - Dag, - Text, -} - -#[derive(Debug, Clone)] -pub(crate) struct PipelineStageAuthority { - // Read by `bootstrap::materialize_pipeline_realizations` when feature - // `bootstrap-regen-fresh` is enabled. - #[cfg_attr(not(feature = "bootstrap-regen-fresh"), allow(dead_code))] - pub(crate) stage: DeclarationId, - pub(crate) stage_name: String, - #[cfg_attr(not(feature = "bootstrap-regen-fresh"), allow(dead_code))] - pub(crate) realization: DeclarationId, - #[cfg_attr(not(feature = "bootstrap-regen-fresh"), allow(dead_code))] - pub(crate) realization_name: String, - pub(crate) snapshot_kind: PipelineSnapshotKind, -} - -/// Pipeline stage order, read structurally from `PipelineStageBinding` -/// declarations in the Dag. The declaration order of the bindings in -/// `pipeline.dag` is the **sole runtime ordering authority** on this path. -/// -/// **DB-16 case 2c / T-Bridge-Retirement disposition (PB review, PR #1171, -/// 2026-04-29):** `fn compile { … }` still lowers to `ArrowBody::Unparsed` — the -/// lowered Dag does **not** carry an ordered list of stage names inside the -/// compile arrow. Without that structural witness, a fail-closed cross-check -/// against the human-readable `compile` body cannot use the substrate query -/// surface alone. Prior attempts used either `include_str!("../pipeline.dag")` -/// (compile-time embed) or `std::fs::read_to_string` + span slicing (runtime -/// source-text side channel); both are rejected for `bridge_include_str_side_channels_retired` -/// in this lane. **Compile-body vs binding drift detection is therefore -/// suspended** until derivation provides a single authored carrier or a -/// lowered structural compile-body representation. Callers must keep -/// `PipelineStageBinding` declaration order and the `compile` orchestrator body -/// in sync by review/regen discipline until then. -pub(crate) fn ordered_pipeline_stages(dag: &Dag) -> Result, String> { - let binding_type_id = dag - .declaration_by_name(PIPELINE_STAGE_BINDING_TYPE) - .map(|decl| decl.id) - .ok_or_else(|| { - format!("missing pipeline authority type `{PIPELINE_STAGE_BINDING_TYPE}`") - })?; - - let mut ordered: Vec = Vec::new(); - let mut seen_stages: Vec = Vec::new(); - - for declaration in dag.declarations() { - if declaration.meta_tag != Some(binding_type_id) { - continue; - } - let Some(ValueBody::Structural { fields }) = &declaration.value_body else { - let binding_name = declaration - .name - .as_deref() - .unwrap_or(""); - return Err(format!( - "pipeline stage binding `{binding_name}` must carry a structural value body" - )); - }; - - let stage = require_decl_ref(fields, "stage", declaration.name.as_deref())?; - let realization = require_decl_ref(fields, "realization", declaration.name.as_deref())?; - let snapshot_kind = require_snapshot_kind(dag, fields, declaration.name.as_deref())?; - - let stage_name = dag.declaration(stage).name.clone().ok_or_else(|| { - format!( - "pipeline stage binding `{}` points at unnamed stage declaration `{}`", - declaration - .name - .as_deref() - .unwrap_or(""), - stage.raw() - ) - })?; - let realization_name = dag.declaration(realization).name.clone().ok_or_else(|| { - format!( - "pipeline stage binding `{}` points at unnamed realization declaration `{}`", - declaration - .name - .as_deref() - .unwrap_or(""), - realization.raw() - ) - })?; - - if seen_stages.contains(&stage) { - return Err(format!( - "pipeline stage `{stage_name}` has multiple stage bindings" - )); - } - seen_stages.push(stage); - - ordered.push(PipelineStageAuthority { - stage, - stage_name, - realization, - realization_name, - snapshot_kind, - }); - } - - if ordered.is_empty() { - return Err("pipeline authority contains no stage bindings".to_string()); - } - - Ok(ordered) -} - -/// Pipeline stage names in authority order. Thin wrapper over -/// `ordered_pipeline_stages` for callers that only need the names. -pub(crate) fn pipeline_compile_order_names(dag: &Dag) -> Result, String> { - Ok(ordered_pipeline_stages(dag)? - .into_iter() - .map(|stage| stage.stage_name) - .collect()) -} - -fn require_decl_ref( - fields: &[(String, FieldValue)], - label: &str, - binding_name: Option<&str>, -) -> Result { - fields - .iter() - .find(|(field, _)| field == label) - .and_then(|(_, value)| match value { - FieldValue::Reference(id) => Some(*id), - _ => None, - }) - .ok_or_else(|| { - format!( - "pipeline stage binding `{}` is missing required DeclarationRef field `{label}`", - binding_name.unwrap_or("") - ) - }) -} - -fn require_snapshot_kind( - dag: &Dag, - fields: &[(String, FieldValue)], - binding_name: Option<&str>, -) -> Result { - let snapshot_type_id = dag - .declaration_by_name("PipelineSnapshotKind") - .map(|decl| decl.id) - .ok_or_else(|| "missing pipeline authority type `PipelineSnapshotKind`".to_string())?; - let value = fields - .iter() - .find(|(field, _)| field == "snapshot") - .map(|(_, value)| value) - .ok_or_else(|| { - format!( - "pipeline stage binding `{}` is missing required `snapshot` field", - binding_name.unwrap_or("") - ) - })?; - - let constructor = match value { - FieldValue::Variant { - constructor, - payload, - } => { - if !payload.is_empty() { - return Err(format!( - "pipeline stage binding `{}` has non-empty snapshot payload", - binding_name.unwrap_or("") - )); - } - *constructor - } - _ => { - return Err(format!( - "pipeline stage binding `{}` must use a PipelineSnapshotKind variant for `snapshot`", - binding_name.unwrap_or("") - )) - } - }; - - let TypeConnective::Disj { variants } = &dag.declaration(snapshot_type_id).connective else { - return Err( - "pipeline authority type `PipelineSnapshotKind` must lower to a sum".to_string(), - ); - }; - let label = variants - .iter() - .find(|variant| variant.ty == constructor) - .map(|variant| variant.label.as_str()) - .ok_or_else(|| { - format!( - "pipeline stage binding `{}` uses snapshot constructor `{}` that does not belong to `PipelineSnapshotKind`", - binding_name.unwrap_or(""), - constructor.raw() - ) - })?; - - match label { - "SnapshotSurface" => Ok(PipelineSnapshotKind::Surface), - "SnapshotDag" => Ok(PipelineSnapshotKind::Dag), - "SnapshotText" => Ok(PipelineSnapshotKind::Text), - other => Err(format!( - "pipeline stage binding `{}` uses unknown snapshot constructor `{other}`", - binding_name.unwrap_or("") - )), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::dag::ArrowBody; - - const PIPELINE_COMPILE_FN: &str = "compile"; - - #[test] - fn ordered_pipeline_stages_authority_is_pipeline_stage_binding_only() { - let dag = Dag::new(); - let names = pipeline_compile_order_names(&dag).expect("bootstrap pipeline stages"); - assert_eq!( - names, - vec![ - "parse".to_string(), - "lower".to_string(), - "infer".to_string(), - "compute_ownership".to_string(), - "lens_complexity".to_string(), - "emit".to_string(), - ], - "ordering is structural declaration order of PipelineStageBinding rows; update this test when pipeline.dag stage set changes" - ); - } - - #[test] - fn pipeline_compile_body_remains_unparsed_blocking_structural_retirement() { - let dag = Dag::new(); - let compile = dag - .declarations() - .iter() - .find(|decl| { - decl.name.as_deref() == Some(PIPELINE_COMPILE_FN) - && decl.span.file == PIPELINE_AUTHORITY_FILE - }) - .expect("pipeline `compile` declaration"); - let TypeConnective::Arrow { body, .. } = &compile.connective else { - panic!("`compile` must be an arrow declaration"); - }; - assert!( - matches!(body, ArrowBody::Unparsed(_)), - "case 2c: `compile` has no lowered ordered stage list — R3 bridge_include_str_side_channels_retired cannot be satisfied here without a new substrate fact; see module doc on ordered_pipeline_stages" - ); - } -} diff --git a/tools/pb0_cycle4_emit_templates/post_emit_verifier.rs.in b/tools/pb0_cycle4_emit_templates/post_emit_verifier.rs.in deleted file mode 100644 index 463c627193d..00000000000 --- a/tools/pb0_cycle4_emit_templates/post_emit_verifier.rs.in +++ /dev/null @@ -1,463 +0,0 @@ -//! E-5 / Lane 1 Stage 1c PR 4 — shared test harness that reads each -//! target's `CleanEmissionContract.post_emit_verifier` record and -//! invokes the declared verifier against an emitted source file. -//! -//! Before this module, each pilot roundtrip test (`emit_rust_…`, -//! `emit_go_…`, `emit_python_…`) hardcoded the verifier command + -//! args in Rust. That divergence is exactly what the contract is -//! supposed to eliminate — the spec file declares the verifier -//! config; the harness reads it; nothing drifts. Adding a new target -//! (Swift, Kotlin, Verilog) requires only a `CleanEmissionContract` -//! data item, not a new Rust roundtrip helper. -//! -//! The harness is deliberately minimal: -//! - `parse_post_emit_verifier` walks the declared structural fields -//! of a `python_clean_emission` / `rust_clean_emission` / -//! `go_clean_emission` declaration and extracts the four -//! consumed fields (`command`, `args`, `expected_exit_code`, -//! `output_policy`) of the `PostEmitVerifier` record into a typed -//! binding. (`syntax_only` is authored on the spec but not yet -//! consumed by any Rust-side consumer — see -//! `PostEmitVerifierBinding` doc.) -//! - `run_post_emit_verifier` invokes `Command::new(binding.command) -//! .args(&binding.args).arg(source_path)`, collects stdout/stderr, -//! and applies the binding's `expected_exit_code` + -//! `output_policy` as the verdict. -//! -//! Scope caveat — calling convention: the harness assumes the -//! verifier takes the source file as its final positional argument. -//! That shape fits every target declared today (`rustc `, -//! `gofmt -l `, `python3 -m py_compile `), but the -//! `PostEmitVerifier` record does not yet encode argument-placement -//! structurally. A verifier that wants stdin, a fixed `-o` slot, or -//! a non-terminal source-path position cannot be expressed without -//! extending the contract shape. When such a target concretely -//! arrives, the fix is a structural extension to `PostEmitVerifier` -//! (e.g. `source_position: SourcePositionPolicy`), not a bespoke -//! Rust branch inside this runner — same discipline as -//! `pattern_bindings` / `output_policy`. - -use std::path::Path; -use std::process::Command; - -use crate::dag::{DeclarationId, FieldValue, LiteralBits}; -use crate::Dag; - -/// Typed read of `data _clean_emission.post_emit_verifier`. -/// Every field here is a structural declaration on the contract that -/// the runner actively consults — no defaults, no fallbacks; a -/// missing or malformed field fires `VerifierParseError::MalformedSpec` -/// at parse time so the spec-file drift surfaces loudly rather than -/// silently hand the runner a partially-filled binding. -/// -/// The authored `PostEmitVerifier` record has a fifth field, -/// `syntax_only: Bool`, which the current runner does not consume — -/// it is metadata about the verifier's depth (shallow syntax check -/// vs full compile) that downstream schedulers (per-PR CI vs -/// nightly) will dispatch on. Per E-6, a spec field lands only with -/// a same-PR consumer; this binding therefore does NOT parse -/// `syntax_only` yet. The field remains in `std/clean_emission.dag` -/// because the three existing target spec files already declare it -/// (landed in PRs 1/2/3 alongside the rest of the contract shape); -/// a follow-up PR that adds the first downstream consumer will -/// add the parse + storage here atomically. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PostEmitVerifierBinding { - pub command: String, - pub args: Vec, - pub expected_exit_code: i64, - pub output_policy: VerifierOutputPolicyBinding, -} - -/// Typed mirror of `std.clean_emission.VerifierOutputPolicy`. Only -/// closed variants — adding a new policy is a spec extension, not a -/// runner override. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VerifierOutputPolicyBinding { - IgnoreVerifierOutput, - RequireEmptyStdout, - RequireEmptyStderr, - RequireEmptyStdoutAndStderr, -} - -/// Contract-parse failures. Callers that want to surface the -/// underlying declaration in a diagnostic propagate this error -/// directly — the `DeclarationId` points at the -/// `*_clean_emission` data item so spec-authors can find the -/// offending record. -#[derive(Debug, Clone)] -pub enum VerifierParseError { - MissingDeclaration, - MalformedSpec { - declaration: DeclarationId, - detail: &'static str, - }, -} - -/// Runner failures. `InvocationFailed` fires when the OS cannot -/// spawn the verifier process (binary missing from PATH, etc.); -/// `WrongExitCode` and `PolicyViolation` fire after a successful -/// spawn when the verdict does not match the contract. -#[derive(Debug)] -pub enum VerifierRunError { - InvocationFailed { - command: String, - io_error: String, - }, - WrongExitCode { - expected: i64, - actual: Option, - stdout: String, - stderr: String, - }, - PolicyViolation { - policy: VerifierOutputPolicyBinding, - stdout: String, - stderr: String, - }, -} - -/// Parse `.post_emit_verifier` into a typed -/// binding. `spec_declaration` is the id of a -/// `*_clean_emission: CleanEmissionContract` data item (obtained via -/// `dag.rust_clean_emission_spec()` / `dag.go_clean_emission_spec()` -/// / `dag.python_clean_emission_spec()`). -pub fn parse_post_emit_verifier( - dag: &Dag, - spec_declaration: DeclarationId, -) -> Result { - let decl = dag.declaration(spec_declaration); - let Some(crate::dag::ValueBody::Structural { fields }) = &decl.value_body else { - return Err(VerifierParseError::MalformedSpec { - declaration: spec_declaration, - detail: "clean_emission spec must be a structural data item", - }); - }; - let verifier_value = fields - .iter() - .find(|(label, _)| label == "post_emit_verifier") - .map(|(_, value)| value) - .ok_or(VerifierParseError::MalformedSpec { - declaration: spec_declaration, - detail: "clean_emission is missing required `post_emit_verifier` field", - })?; - let FieldValue::Record(verifier_fields) = verifier_value else { - return Err(VerifierParseError::MalformedSpec { - declaration: spec_declaration, - detail: "clean_emission.post_emit_verifier must be a structural record", - }); - }; - - let command = require_string(verifier_fields, "command", spec_declaration)?; - let args = require_string_list(verifier_fields, "args", spec_declaration)?; - let expected_exit_code = require_int(verifier_fields, "expected_exit_code", spec_declaration)?; - let output_policy = - parse_output_policy(dag, verifier_fields, "output_policy", spec_declaration)?; - - Ok(PostEmitVerifierBinding { - command, - args, - expected_exit_code, - output_policy, - }) -} - -/// Invoke the verifier against `source_path` and apply the -/// contract's verdict semantics. The source file path is appended -/// as the final positional argument — every declared verifier -/// today expects the file as its last argument -/// (`rustc ` / `gofmt -l ` / `python3 -m py_compile -/// `). -pub fn run_post_emit_verifier( - binding: &PostEmitVerifierBinding, - source_path: &Path, -) -> Result<(), VerifierRunError> { - let mut command = Command::new(&binding.command); - command.args(&binding.args).arg(source_path); - // Rustc (and py_compile to a lesser extent) produce artifacts - // next to / in cwd. Pinning cwd to the source's parent directory - // contains those artifacts inside the caller's tmp dir instead - // of dropping them into the workspace root. - // - // `Path::parent()` returns `Some("")` for single-component - // relative paths (e.g. `main.rs`); `current_dir("")` fails the - // spawn on most platforms. Filter empties so relative paths in - // the current directory fall through to the inherited cwd - // instead of rejecting valid inputs at spawn time. - if let Some(parent) = source_path - .parent() - .filter(|parent| !parent.as_os_str().is_empty()) - { - command.current_dir(parent); - } - let output = command - .output() - .map_err(|err| VerifierRunError::InvocationFailed { - command: binding.command.clone(), - io_error: err.to_string(), - })?; - - let exit_code = output.status.code(); - let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); - let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); - - if exit_code.map(i64::from) != Some(binding.expected_exit_code) { - return Err(VerifierRunError::WrongExitCode { - expected: binding.expected_exit_code, - actual: exit_code, - stdout, - stderr, - }); - } - - let policy = binding.output_policy; - let stdout_empty = stdout.trim().is_empty(); - let stderr_empty = stderr.trim().is_empty(); - let ok = match policy { - VerifierOutputPolicyBinding::IgnoreVerifierOutput => true, - VerifierOutputPolicyBinding::RequireEmptyStdout => stdout_empty, - VerifierOutputPolicyBinding::RequireEmptyStderr => stderr_empty, - VerifierOutputPolicyBinding::RequireEmptyStdoutAndStderr => stdout_empty && stderr_empty, - }; - if !ok { - return Err(VerifierRunError::PolicyViolation { - policy, - stdout, - stderr, - }); - } - Ok(()) -} - -fn require_string( - fields: &[(String, FieldValue)], - name: &'static str, - declaration: DeclarationId, -) -> Result { - fields - .iter() - .find(|(label, _)| label == name) - .and_then(|(_, value)| match value { - FieldValue::Literal(LiteralBits::String(s)) => Some(s.clone()), - _ => None, - }) - .ok_or(VerifierParseError::MalformedSpec { - declaration, - detail: "required string field missing or malformed", - }) -} - -fn require_int( - fields: &[(String, FieldValue)], - name: &'static str, - declaration: DeclarationId, -) -> Result { - fields - .iter() - .find(|(label, _)| label == name) - .and_then(|(_, value)| match value { - FieldValue::Literal(LiteralBits::Int(s)) => s.parse::().ok(), - _ => None, - }) - .ok_or(VerifierParseError::MalformedSpec { - declaration, - detail: "required int field missing or malformed", - }) -} - -fn require_string_list( - fields: &[(String, FieldValue)], - name: &'static str, - declaration: DeclarationId, -) -> Result, VerifierParseError> { - let value = fields - .iter() - .find(|(label, _)| label == name) - .map(|(_, value)| value) - .ok_or(VerifierParseError::MalformedSpec { - declaration, - detail: "required list field missing", - })?; - let FieldValue::List(entries) = value else { - return Err(VerifierParseError::MalformedSpec { - declaration, - detail: "required field must be a list", - }); - }; - let mut out = Vec::with_capacity(entries.len()); - for entry in entries { - let FieldValue::Literal(LiteralBits::String(s)) = entry else { - return Err(VerifierParseError::MalformedSpec { - declaration, - detail: "list entries must be string literals", - }); - }; - out.push(s.clone()); - } - Ok(out) -} - -fn parse_output_policy( - dag: &Dag, - fields: &[(String, FieldValue)], - name: &'static str, - declaration: DeclarationId, -) -> Result { - let value = fields - .iter() - .find(|(label, _)| label == name) - .map(|(_, value)| value) - .ok_or(VerifierParseError::MalformedSpec { - declaration, - detail: "required variant field missing", - })?; - let FieldValue::Variant { - constructor, - payload, - } = value - else { - return Err(VerifierParseError::MalformedSpec { - declaration, - detail: "output_policy must be a VerifierOutputPolicy variant", - }); - }; - if !payload.is_empty() { - return Err(VerifierParseError::MalformedSpec { - declaration, - detail: "VerifierOutputPolicy variants must not carry payload fields", - }); - } - // Variant resolution dispatches on the cached - // `VerifierOutputPolicyVariants` typed handles — the Dag - // resolves the Disj's variant ids once at bootstrap end and - // every downstream consumer compares `DeclarationId`s here. No - // variant.label string lookups at parse time; INVARIANTS.md - // §Layer opacity / §Semantic authority forbids post-lowering - // consumers from reaching for names. Mirrors the PR 2.5 - // `PatternBindingRuleVariants` structure. - let variants = dag.verifier_output_policy_variants(); - let ignore_output = variants - .ignore_output - .ok_or(VerifierParseError::MalformedSpec { - declaration, - detail: "VerifierOutputPolicy.IgnoreVerifierOutput declaration was not found", - })?; - let require_empty_stdout = - variants - .require_empty_stdout - .ok_or(VerifierParseError::MalformedSpec { - declaration, - detail: "VerifierOutputPolicy.RequireEmptyStdout declaration was not found", - })?; - let require_empty_stderr = - variants - .require_empty_stderr - .ok_or(VerifierParseError::MalformedSpec { - declaration, - detail: "VerifierOutputPolicy.RequireEmptyStderr declaration was not found", - })?; - let require_empty_stdout_and_stderr = - variants - .require_empty_stdout_and_stderr - .ok_or(VerifierParseError::MalformedSpec { - declaration, - detail: - "VerifierOutputPolicy.RequireEmptyStdoutAndStderr declaration was not found", - })?; - if *constructor == ignore_output { - Ok(VerifierOutputPolicyBinding::IgnoreVerifierOutput) - } else if *constructor == require_empty_stdout { - Ok(VerifierOutputPolicyBinding::RequireEmptyStdout) - } else if *constructor == require_empty_stderr { - Ok(VerifierOutputPolicyBinding::RequireEmptyStderr) - } else if *constructor == require_empty_stdout_and_stderr { - Ok(VerifierOutputPolicyBinding::RequireEmptyStdoutAndStderr) - } else { - Err(VerifierParseError::MalformedSpec { - declaration, - detail: "output_policy constructor is not a known VerifierOutputPolicy variant", - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Parse the Rust contract and assert the structural fields - /// match the declared values in `src/v3/spec/rust.dag`. Proves - /// the parser consumes the shared substrate correctly without - /// hardcoding any of the target-specific command / args values. - #[test] - fn parses_rust_post_emit_verifier_contract() { - let dag = crate::dag::Dag::new(); - let spec = dag - .rust_clean_emission_spec() - .expect("rust_clean_emission cached"); - let binding = parse_post_emit_verifier(&dag, spec).expect("parse"); - assert_eq!(binding.command, "rustc"); - assert_eq!(binding.args, vec!["--edition=2021", "-D", "warnings"]); - assert_eq!(binding.expected_exit_code, 0); - assert_eq!( - binding.output_policy, - VerifierOutputPolicyBinding::IgnoreVerifierOutput - ); - } - - #[test] - fn parses_go_post_emit_verifier_contract() { - let dag = crate::dag::Dag::new(); - let spec = dag - .go_clean_emission_spec() - .expect("go_clean_emission cached"); - let binding = parse_post_emit_verifier(&dag, spec).expect("parse"); - assert_eq!(binding.command, "gofmt"); - assert_eq!(binding.args, vec!["-l"]); - assert_eq!(binding.expected_exit_code, 0); - assert_eq!( - binding.output_policy, - VerifierOutputPolicyBinding::RequireEmptyStdout - ); - } - - #[test] - fn parses_python_post_emit_verifier_contract() { - let dag = crate::dag::Dag::new(); - let spec = dag - .python_clean_emission_spec() - .expect("python_clean_emission cached"); - let binding = parse_post_emit_verifier(&dag, spec).expect("parse"); - assert_eq!(binding.command, "python3"); - assert_eq!(binding.args, vec!["-m", "py_compile"]); - assert_eq!(binding.expected_exit_code, 0); - assert_eq!( - binding.output_policy, - VerifierOutputPolicyBinding::IgnoreVerifierOutput - ); - } - - /// Regression: `Path::parent()` returns `Some("")` for - /// single-component relative paths like `main.rs`. Setting - /// `current_dir("")` fails the spawn on most platforms, so the - /// runner used to reject valid inputs whenever callers passed - /// a bare relative filename. The fix filters empty parents so - /// the spawn inherits the ambient cwd instead. This test uses - /// an environment-probe binary that exists on every - /// POSIX-shaped test runner and expects exit 0 regardless of - /// the path passed; the specific file does not need to exist - /// because the probe doesn't read it. - #[cfg(unix)] - #[test] - fn run_accepts_single_component_relative_source_path() { - use std::path::PathBuf; - let binding = PostEmitVerifierBinding { - command: "true".to_string(), - args: Vec::new(), - expected_exit_code: 0, - output_policy: VerifierOutputPolicyBinding::IgnoreVerifierOutput, - }; - let relative = PathBuf::from("main.rs"); - run_post_emit_verifier(&binding, &relative) - .expect("single-component relative source path must not fail the spawn"); - } -} From 4f3fb00fb3e9d22f20d1e47dbe0af256c0686374 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 14 May 2026 03:47:35 +0000 Subject: [PATCH 2/9] =?UTF-8?q?Revert=20"PB-0=20cycle-4=20NON=5FTEST=20ret?= =?UTF-8?q?irement=20(msg=5F5d7f3491):=207=20paths=20=E2=80=94=20complexit?= =?UTF-8?q?y=5Flattice.rs;=20cost=5Fbasis=5Fdeclaration.rs;=20enforced=5Fl?= =?UTF-8?q?ens=5Fapplication.rs;=20int=5Fliteral=5Franges.rs;=20pb=5Fmetho?= =?UTF-8?q?d=5Ftemplate=5Fprojection.rs;=20emit/collection=5Fops=5Fmethod?= =?UTF-8?q?=5Fcontract.rs;=20emit=5Frust=5Fbin=5Fshim.rs=20=E2=80=94=20bri?= =?UTF-8?q?ef=20docs/briefs/r3-pb0-non-test-retire=20(#3048)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit b1c8017f99f81ffa04c6588a48230ee95f1902db. --- dsl/gunbc/compiler.dag | 3 + src/v3/compiler/build.rs | 109 +- .../v3/compiler/src/complexity_lattice.rs | 0 .../src/complexity_lattice_generated.rs | 115 -- .../v3/compiler/src/cost_basis_declaration.rs | 0 .../src/cost_basis_declaration_generated.rs | 171 -- src/v3/compiler/src/emit.rs | 1 - .../emit/collection_ops_method_contract.rs | 0 ...ollection_ops_method_contract_generated.rs | 189 --- .../v3/compiler/src/emit_rust_bin_shim.rs | 12 +- .../src/emit_rust_bin_shim_generated.rs | 100 -- .../compiler/src/enforced_lens_application.rs | 0 .../enforced_lens_application_generated.rs | 1314 --------------- .../v3/compiler/src/int_literal_ranges.rs | 0 .../src/int_literal_ranges_generated.rs | 1026 ------------ src/v3/compiler/src/lib.rs | 6 - .../src/pb_method_template_projection.rs | 0 ...pb_method_template_projection_generated.rs | 1453 ----------------- src/v3/compiler/src/process_exit.rs | 7 +- .../tests/integration/sg0_census_test.rs | 20 +- 20 files changed, 29 insertions(+), 4497 deletions(-) rename tools/pb0_cycle4_emit_templates/complexity_lattice.rs.in => src/v3/compiler/src/complexity_lattice.rs (100%) delete mode 100644 src/v3/compiler/src/complexity_lattice_generated.rs rename tools/pb0_cycle4_emit_templates/cost_basis_declaration.rs.in => src/v3/compiler/src/cost_basis_declaration.rs (100%) delete mode 100644 src/v3/compiler/src/cost_basis_declaration_generated.rs rename tools/pb0_cycle4_emit_templates/emit_collection_ops_method_contract.rs.in => src/v3/compiler/src/emit/collection_ops_method_contract.rs (100%) delete mode 100644 src/v3/compiler/src/emit/collection_ops_method_contract_generated.rs rename tools/pb0_cycle4_emit_templates/emit_rust_bin_shim.rs.in => src/v3/compiler/src/emit_rust_bin_shim.rs (86%) delete mode 100644 src/v3/compiler/src/emit_rust_bin_shim_generated.rs rename tools/pb0_cycle4_emit_templates/enforced_lens_application.rs.in => src/v3/compiler/src/enforced_lens_application.rs (100%) delete mode 100644 src/v3/compiler/src/enforced_lens_application_generated.rs rename tools/pb0_cycle4_emit_templates/int_literal_ranges.rs.in => src/v3/compiler/src/int_literal_ranges.rs (100%) delete mode 100644 src/v3/compiler/src/int_literal_ranges_generated.rs rename tools/pb0_cycle4_emit_templates/pb_method_template_projection.rs.in => src/v3/compiler/src/pb_method_template_projection.rs (100%) delete mode 100644 src/v3/compiler/src/pb_method_template_projection_generated.rs diff --git a/dsl/gunbc/compiler.dag b/dsl/gunbc/compiler.dag index fb90e9fb17f..43410c76848 100644 --- a/dsl/gunbc/compiler.dag +++ b/dsl/gunbc/compiler.dag @@ -74,6 +74,7 @@ data stage0: GeneratedCrate = { source_dir: "src/v3/compiler/src", hand_maintained_src: [ "bootstrap.rs", + "cost_basis_declaration.rs", "dag.rs", "diagnostics.rs", "dimension.rs", @@ -81,6 +82,8 @@ data stage0: GeneratedCrate = { "emit_go.rs", "emit_python.rs", "emit_rust.rs", + // PB-1 Item 5: hand-authored `main.rs` shell formatter (not emitted into src/). + "emit_rust_bin_shim.rs", "python_target.rs", "rust_target.rs", "infer.rs", diff --git a/src/v3/compiler/build.rs b/src/v3/compiler/build.rs index a03b0ee1ff6..8a22d46a99b 100644 --- a/src/v3/compiler/build.rs +++ b/src/v3/compiler/build.rs @@ -436,97 +436,6 @@ fn emit_r1_gates_fixture(manifest_path: &Path, v3_dir: &Path) { /// **Hygiene:** `repeat_string` exists in the bootstrap bundle only via this excerpt — no /// hand-maintained duplicate of `render.dag` (INVARIANTS P2). Edit the marked region in /// `render.dag` only; this output is overwritten on `cargo build -p v3-compiler`. -/// Emit PB-0 cycle-4 host authority modules (former `EXPECTED_HAND_AUTHORED_NON_TEST` paths) from -/// checked-in templates so `REGEN_OUTPUTS` / `GENERATED_FILES` stay the sole SG-0 partition -/// authority (`sg0_v3_hand_authored_census`). -/// -/// **Templates** live under `tools/pb0_cycle4_emit_templates/*.rs.in` (workspace root — not under -/// `src/v3/compiler/`, so they are outside the SG-0 `.rs` walk and not misread as hand-authored -/// compiler sources). **Full substrate dissolution** (`.dag` / lens-producer authority replacing -/// this body) is the separate T-LP-Retirement program; this path is the bounded PB-0 census -/// partition receipt per `docs/briefs/r3-pb0-non-test-retirement-worker.md`. -/// -/// **Checked-in outputs:** the emitted `*_generated.rs` paths are tracked in git like other -/// `REGEN_OUTPUTS` modules (not gitignored). `cargo build` / `cargo check` runs this `build.rs` -/// hook before `rustc` sees `lib.rs`, and `cargo:rerun-if-changed` on the template directory -/// forces a rebuild when `.rs.in` edits land — refresh committed `*_generated.rs` in the same PR -/// as template edits (CI exercises the full path). -/// -/// Do not hand-edit `*_generated.rs`; edit the `.rs.in` templates and rebuild. -fn emit_pb0_cycle4_host_authority_modules(repo_root: &Path) { - const HEADER: &str = concat!( - "// AUTO-GENERATED by src/v3/compiler/build.rs (`emit_pb0_cycle4_host_authority_modules`).\n", - "//\n", - "// Template: `tools/pb0_cycle4_emit_templates/.rs.in` (workspace root).\n", - "// Dissolution receipt: PB-0 NON_TEST partition — `REGEN_OUTPUTS` producer; full `.dag` emit T-LP track.\n", - "//\n", - "\n", - ); - let tmpl_dir = repo_root.join("tools/pb0_cycle4_emit_templates"); - let specs: &[(&str, &str)] = &[ - ( - "complexity_lattice.rs.in", - "src/v3/compiler/src/complexity_lattice_generated.rs", - ), - ( - "cost_basis_declaration.rs.in", - "src/v3/compiler/src/cost_basis_declaration_generated.rs", - ), - ( - "emit_collection_ops_method_contract.rs.in", - "src/v3/compiler/src/emit/collection_ops_method_contract_generated.rs", - ), - ( - "emit_rust_bin_shim.rs.in", - "src/v3/compiler/src/emit_rust_bin_shim_generated.rs", - ), - ( - "enforced_lens_application.rs.in", - "src/v3/compiler/src/enforced_lens_application_generated.rs", - ), - ( - "int_literal_ranges.rs.in", - "src/v3/compiler/src/int_literal_ranges_generated.rs", - ), - ( - "pb_method_template_projection.rs.in", - "src/v3/compiler/src/pb_method_template_projection_generated.rs", - ), - ]; - for (tmpl_name, rel_out) in specs { - let src_path = tmpl_dir.join(tmpl_name); - println!("cargo:rerun-if-changed={}", src_path.display()); - let body = fs::read_to_string(&src_path).unwrap_or_else(|e| { - panic!( - "emit_pb0_cycle4_host_authority_modules: read template {}: {e}", - src_path.display() - ) - }); - let out_path = repo_root.join(rel_out); - if let Some(parent) = out_path.parent() { - fs::create_dir_all(parent).unwrap_or_else(|e| { - panic!( - "emit_pb0_cycle4_host_authority_modules: mkdir {}: {e}", - parent.display() - ) - }); - } - let generated = format!("{HEADER}{body}"); - let needs_write = match fs::read_to_string(&out_path) { - Ok(existing) => existing != generated, - Err(_) => true, - }; - if needs_write { - fs::write(&out_path, generated).unwrap_or_else(|e| { - panic!( - "emit_pb0_cycle4_host_authority_modules: write {}: {e}", - out_path.display() - ) - }); - } - } -} - fn emit_render_repeat_string_bootstrap(repo_root: &Path) { const BEGIN: &str = "// GUNBC_BOOTSTRAP_EMIT_BEGIN\n"; const END: &str = "// GUNBC_BOOTSTRAP_EMIT_END"; @@ -620,7 +529,6 @@ fn main() { let src_dir = v3_dir.parent().expect("v3 dir has parent"); let repo_root = src_dir.parent().expect("src dir has parent"); emit_render_repeat_string_bootstrap(repo_root); - emit_pb0_cycle4_host_authority_modules(repo_root); let std_dir = v3_dir.join("std"); let spec_dir = v3_dir.join("spec"); let compiler_dir = manifest_path.clone(); @@ -637,10 +545,6 @@ fn main() { println!("cargo:rerun-if-changed={}", extdeps_dir.display()); println!("cargo:rerun-if-changed={}", gunbc_dir.display()); println!("cargo:rerun-if-changed={}", dsl_std_dir.display()); - println!( - "cargo:rerun-if-changed={}", - repo_root.join("tools/pb0_cycle4_emit_templates").display() - ); // Structural-recursion termination analysis walks a recursing // argument back to its declared Disj connective (see @@ -754,9 +658,6 @@ fn main() { "src/v3/compiler/src/bootstrap_generated.rs", "src/v3/compiler/src/bootstrap_generated_without_parse_surface.rs", "src/v3/compiler/src/bootstrap_std_generated.rs", - // PB-0 cycle-4: former hand `EXPECTED_HAND_AUTHORED_NON_TEST` modules — templates under - // `tools/pb0_cycle4_emit_templates/*.rs.in`, emitted by `emit_pb0_cycle4_host_authority_modules`. - "src/v3/compiler/src/complexity_lattice_generated.rs", // SG-5 substrate / runtime-mirror projections, generated from // `src/v3/std/substrate.dag` + `src/v3/std/parse_surface.dag` // and consumed by hand-authored Rust via `include!(...)`. @@ -765,20 +666,15 @@ fn main() { // outputs, so the content header alone never masquerades as // generated (SG-0's `sg0_generated_partition_is_producer_owned` // invariant). - "src/v3/compiler/src/complexity_lens_generated.rs", - "src/v3/compiler/src/cost_basis_declaration_generated.rs", "src/v3/compiler/src/dag_branch_generated.rs", "src/v3/compiler/src/dag_cluster_generated.rs", - "src/v3/compiler/src/dag_cost_generated.rs", "src/v3/compiler/src/dag_lookup_generated.rs", + "src/v3/compiler/src/dag_cost_generated.rs", "src/v3/compiler/src/dag_scalar_generated.rs", "src/v3/compiler/src/dag_value_body_generated.rs", "src/v3/compiler/src/diagnostics_generated.rs", - "src/v3/compiler/src/emit/collection_ops_method_contract_generated.rs", - "src/v3/compiler/src/emit_rust_bin_shim_generated.rs", - "src/v3/compiler/src/enforced_lens_application_generated.rs", "src/v3/compiler/src/infer_helpers_generated.rs", - "src/v3/compiler/src/int_literal_ranges_generated.rs", + "src/v3/compiler/src/complexity_lens_generated.rs", "src/v3/compiler/src/lens_cost_symbolic_generated.rs", "src/v3/compiler/src/lens_cost_target_realization_generated.rs", "src/v3/compiler/src/lens_effect_enumeration_generated.rs", @@ -788,7 +684,6 @@ fn main() { "src/v3/compiler/src/lens_unused_parameters_generated.rs", "src/v3/compiler/src/lower_helpers_generated.rs", "src/v3/compiler/src/operators_generated.rs", - "src/v3/compiler/src/pb_method_template_projection_generated.rs", "src/v3/compiler/src/parse_generated.rs", "src/v3/compiler/src/parse_surface_generated.rs", "src/v3/compiler/src/parse_tables_generated.rs", diff --git a/tools/pb0_cycle4_emit_templates/complexity_lattice.rs.in b/src/v3/compiler/src/complexity_lattice.rs similarity index 100% rename from tools/pb0_cycle4_emit_templates/complexity_lattice.rs.in rename to src/v3/compiler/src/complexity_lattice.rs diff --git a/src/v3/compiler/src/complexity_lattice_generated.rs b/src/v3/compiler/src/complexity_lattice_generated.rs deleted file mode 100644 index 0ccf6156b69..00000000000 --- a/src/v3/compiler/src/complexity_lattice_generated.rs +++ /dev/null @@ -1,115 +0,0 @@ -// AUTO-GENERATED by src/v3/compiler/build.rs (`emit_pb0_cycle4_host_authority_modules`). -// -// Template: `tools/pb0_cycle4_emit_templates/.rs.in` (workspace root). -// Dissolution receipt: PB-0 NON_TEST partition — `REGEN_OUTPUTS` producer; full `.dag` emit T-LP track. -// - -//! **`complexity_enforcement_budget_dominates`** — executable **T-LAS enforcement** -//! ordering on `AsymptoticClass` for `complexity_enforcement_violates` / -//! `EnforcedApplication` (gate #92). It matches `v3.std.algebra::asymptotic_dominates` -//! on every arm including `ClassPolynomial×ClassPolynomial` (Peano `degree` compare). -//! -//! **`asymptotic_dominates`** is a backward-compatible alias for tests and older -//! call sites; new enforcement code should call [`complexity_enforcement_budget_dominates`]. -//! -//! Stay aligned with `complexity.dag` + `complexity_enforcement_violates` comments. - -use crate::dag::{positive_descent_count, AsymptoticClass}; - -pub fn complexity_enforcement_budget_dominates(a: &AsymptoticClass, b: &AsymptoticClass) -> bool { - use AsymptoticClass::*; - match a { - ClassUnknown => true, - ClassExponential => match b { - ClassUnknown => false, - ClassExponential - | ClassPolynomial { .. } // Any `ClassPolynomial` tier is below exponential. - | ClassQuadratic - | ClassLinearithmic - | ClassLinear - | ClassLog - | ClassConstant => true, - }, - ClassPolynomial { degree: da } => match b { - ClassUnknown | ClassExponential => false, - ClassPolynomial { degree: db } => { - positive_descent_count(da) >= positive_descent_count(db) - } - ClassQuadratic | ClassLinearithmic | ClassLinear | ClassLog | ClassConstant => true, - }, - ClassQuadratic => match b { - ClassConstant | ClassLog | ClassLinear | ClassLinearithmic | ClassQuadratic => true, - ClassPolynomial { .. } | ClassExponential | ClassUnknown => false, - }, - ClassLinearithmic => match b { - ClassConstant | ClassLog | ClassLinear | ClassLinearithmic => true, - ClassQuadratic | ClassPolynomial { .. } | ClassExponential | ClassUnknown => false, - }, - ClassLinear => match b { - ClassConstant | ClassLog | ClassLinear => true, - ClassLinearithmic - | ClassQuadratic - | ClassPolynomial { .. } - | ClassExponential - | ClassUnknown => false, - }, - ClassLog => match b { - ClassConstant | ClassLog => true, - ClassLinear - | ClassLinearithmic - | ClassQuadratic - | ClassPolynomial { .. } - | ClassExponential - | ClassUnknown => false, - }, - ClassConstant => match b { - ClassConstant => true, - ClassLog - | ClassLinear - | ClassLinearithmic - | ClassQuadratic - | ClassPolynomial { .. } - | ClassExponential - | ClassUnknown => false, - }, - } -} - -/// Alias of [`complexity_enforcement_budget_dominates`]. Prefer the named helper for -/// new call sites so enforcement-grade poly refinement is not confused with substrate -/// `algebra.dag::asymptotic_dominates` at the documentation layer. -#[inline] -pub fn asymptotic_dominates(a: &AsymptoticClass, b: &AsymptoticClass) -> bool { - complexity_enforcement_budget_dominates(a, b) -} - -#[cfg(test)] -mod asymptotic_dominates_tests { - use super::{asymptotic_dominates, complexity_enforcement_budget_dominates}; - use crate::dag::{positive_amount_from_i64, AsymptoticClass}; - - fn poly(k: i64) -> AsymptoticClass { - AsymptoticClass::ClassPolynomial { - degree: positive_amount_from_i64(k).expect("test degree in range"), - } - } - - #[test] - fn polynomial_degree_orders_within_class_polynomial() { - let p5 = poly(5); - let p3 = poly(3); - assert!(complexity_enforcement_budget_dominates(&p5, &p3)); - assert!(!complexity_enforcement_budget_dominates(&p3, &p5)); - assert!(complexity_enforcement_budget_dominates(&p3, &p3)); - } - - #[test] - fn alias_matches_budget_dominates() { - let p3 = poly(3); - let p5 = poly(5); - assert_eq!( - asymptotic_dominates(&p5, &p3), - complexity_enforcement_budget_dominates(&p5, &p3) - ); - } -} diff --git a/tools/pb0_cycle4_emit_templates/cost_basis_declaration.rs.in b/src/v3/compiler/src/cost_basis_declaration.rs similarity index 100% rename from tools/pb0_cycle4_emit_templates/cost_basis_declaration.rs.in rename to src/v3/compiler/src/cost_basis_declaration.rs diff --git a/src/v3/compiler/src/cost_basis_declaration_generated.rs b/src/v3/compiler/src/cost_basis_declaration_generated.rs deleted file mode 100644 index f5c14ac8b4a..00000000000 --- a/src/v3/compiler/src/cost_basis_declaration_generated.rs +++ /dev/null @@ -1,171 +0,0 @@ -// AUTO-GENERATED by src/v3/compiler/build.rs (`emit_pb0_cycle4_host_authority_modules`). -// -// Template: `tools/pb0_cycle4_emit_templates/.rs.in` (workspace root). -// Dissolution receipt: PB-0 NON_TEST partition — `REGEN_OUTPUTS` producer; full `.dag` emit T-LP track. -// - -//! Materialize [`CostBasisDeclaration`](crate::lens_cost_symbolic::CostBasisDeclaration) from -//! lowered [`Dag`](crate::dag::Dag) facts — interim **consumer boundary** for gate **#93** -//! (`crdt_cost_basis_demonstrated`) **substrate / symbolic-cost slice**; folding authored basis -//! rows into [`compute_symbolic_costs`](crate::lens_cost_symbolic::compute_symbolic_costs) remains follow-on on the same gate. -//! -//! Persisted `.dag` `data` rows and folding declarations into [`compute_symbolic_costs`](crate::lens_cost_symbolic::compute_symbolic_costs) remain follow-on; embedders MUST NOT -//! hand-assemble `CostBasisDeclaration` beside this helper when deriving the same evidence -//! from a compiled DAG (INVARIANTS.md P2). - -use std::fmt; - -use crate::dag::{ - ArrowBody, BindNode, Dag, DeclarationId, Lookup, PortId, SectionRef, SizeVariable, - SymbolicCost, TypeConnective, -}; -use crate::lens_cost_symbolic::{compute_symbolic_costs, CostBasisDeclaration, CostBasisKind}; - -/// [`try_build_per_write_log_cost_basis_declaration`] stops on missing DAG facts instead of -/// panicking (fail-closed consumer boundary; codex gate on PR #2299). -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CostBasisDeclarationBuildError { - DeclarationNotFound { - name: String, - }, - MergeStepMissingParams { - merge_step_declaration_name: String, - }, - /// Declaration must be a function (`TypeConnective::Arrow`) so the body bind is - /// reachable via [`ArrowBody::UserDefined`] (single authority per P2). - WorkflowDeclarationNotArrow { - name: String, - }, - /// Arrow declaration body is not yet lowered to `UserDefined(BindNodeId)` — cannot read - /// the authoritative body [`BindNode`] for parameters / span / structural parity. - WorkflowBodyNotUserDefinedBind { - name: String, - }, - /// `compute_symbolic_costs` has no `Hit(LogCost)` row keyed to the merge step’s first - /// parameter port — cannot fabricate a per-write log basis without lens evidence of a - /// divide on that port (P2 / fail-closed). - MergeStepLacksLogCostWitness { - merge_step_declaration_name: String, - }, -} - -impl fmt::Display for CostBasisDeclarationBuildError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - CostBasisDeclarationBuildError::DeclarationNotFound { name } => { - write!(f, "declaration `{name}` not found") - } - CostBasisDeclarationBuildError::MergeStepMissingParams { - merge_step_declaration_name, - } => write!( - f, - "declaration `{merge_step_declaration_name}` should declare at least one parameter on its body bind" - ), - CostBasisDeclarationBuildError::WorkflowDeclarationNotArrow { name } => write!( - f, - "declaration `{name}` must have Arrow connective (callable)" - ), - CostBasisDeclarationBuildError::WorkflowBodyNotUserDefinedBind { name } => write!( - f, - "declaration `{name}` body is not ArrowBody::UserDefined(BindNodeId); cannot resolve authoritative bind node" - ), - CostBasisDeclarationBuildError::MergeStepLacksLogCostWitness { - merge_step_declaration_name, - } => write!( - f, - "declaration `{merge_step_declaration_name}` first param port has no LogCost witness in `compute_symbolic_costs` (expected divide-on-dividend lowering)" - ), - } - } -} - -impl std::error::Error for CostBasisDeclarationBuildError {} - -fn user_defined_arrow_body_bind<'a>( - dag: &'a Dag, - declaration_name: &str, -) -> Result<(&'a BindNode, DeclarationId), CostBasisDeclarationBuildError> { - let decl = dag.declaration_by_name(declaration_name).ok_or_else(|| { - CostBasisDeclarationBuildError::DeclarationNotFound { - name: declaration_name.to_string(), - } - })?; - let subject = decl.id; - let bind = match &decl.connective { - TypeConnective::Arrow { body, .. } => match body { - ArrowBody::UserDefined(bind_id) => bind_id.bind_opt(dag).ok_or_else(|| { - CostBasisDeclarationBuildError::WorkflowBodyNotUserDefinedBind { - name: declaration_name.to_string(), - } - })?, - _ => { - return Err( - CostBasisDeclarationBuildError::WorkflowBodyNotUserDefinedBind { - name: declaration_name.to_string(), - }, - ); - } - }, - _ => { - return Err( - CostBasisDeclarationBuildError::WorkflowDeclarationNotArrow { - name: declaration_name.to_string(), - }, - ); - } - }; - Ok((bind, subject)) -} - -fn symbolic_table_includes_log_on_port(dag: &Dag, port: PortId) -> bool { - compute_symbolic_costs(dag).iter().any(|e| { - matches!( - &e.cost, - Lookup::Hit(SymbolicCost::LogCost { _0: sv }) if sv.source_port == port - ) - }) -} - -/// **`LogCost` witness:** returns [`MergeStepLacksLogCostWitness`] unless -/// [`compute_symbolic_costs`] already **`Hit`s `LogCost(merge_param_port)`** (divide-on-dividend -/// lowering); does not fabricate basis without lens table evidence. -/// **Subject** is `SectionRef::DeclarationScope { declaration: … }` wrapping the workflow -/// [`DeclarationId`] (T-LAS / audit structural authority — expression-scoped bases use -/// `NodeScope` when authors need it). -/// `merge_step_declaration_name` is the **`fn` declaration id** (same string the author uses for -/// the merge helper, e.g. `crdt_merge_step`); its body [`BindNode`] is read only via -/// [`TypeConnective::Arrow`] → [`ArrowBody::UserDefined`] like the workflow (P2 single authority -/// for both callables). -pub fn try_build_per_write_log_cost_basis_declaration( - dag: &Dag, - workflow_declaration_name: &str, - merge_step_declaration_name: &str, -) -> Result { - let (workflow_bind, subject) = user_defined_arrow_body_bind(dag, workflow_declaration_name)?; - let (merge_bind, _) = user_defined_arrow_body_bind(dag, merge_step_declaration_name)?; - let merge_replicas_port = *merge_bind.params.first().ok_or_else(|| { - CostBasisDeclarationBuildError::MergeStepMissingParams { - merge_step_declaration_name: merge_step_declaration_name.to_string(), - } - })?; - if !symbolic_table_includes_log_on_port(dag, merge_replicas_port) { - return Err( - CostBasisDeclarationBuildError::MergeStepLacksLogCostWitness { - merge_step_declaration_name: merge_step_declaration_name.to_string(), - }, - ); - } - Ok(CostBasisDeclaration { - subject: SectionRef::DeclarationScope { - declaration: subject, - }, - kind: CostBasisKind::PerWrite, - cost: SymbolicCost::LogCost { - _0: SizeVariable { - source_port: merge_replicas_port, - display_name: None, - }, - }, - span: workflow_bind.span.clone(), - }) -} diff --git a/src/v3/compiler/src/emit.rs b/src/v3/compiler/src/emit.rs index 2a80baeda32..d1420cf68e2 100644 --- a/src/v3/compiler/src/emit.rs +++ b/src/v3/compiler/src/emit.rs @@ -15,7 +15,6 @@ //! target-private carriers stay inside their target module and no //! cross-target code should read them. -#[path = "emit/collection_ops_method_contract_generated.rs"] pub(crate) mod collection_ops_method_contract; pub(crate) mod python_target; pub(crate) mod rust_target; diff --git a/tools/pb0_cycle4_emit_templates/emit_collection_ops_method_contract.rs.in b/src/v3/compiler/src/emit/collection_ops_method_contract.rs similarity index 100% rename from tools/pb0_cycle4_emit_templates/emit_collection_ops_method_contract.rs.in rename to src/v3/compiler/src/emit/collection_ops_method_contract.rs diff --git a/src/v3/compiler/src/emit/collection_ops_method_contract_generated.rs b/src/v3/compiler/src/emit/collection_ops_method_contract_generated.rs deleted file mode 100644 index 5b68d67e0f0..00000000000 --- a/src/v3/compiler/src/emit/collection_ops_method_contract_generated.rs +++ /dev/null @@ -1,189 +0,0 @@ -// AUTO-GENERATED by src/v3/compiler/build.rs (`emit_pb0_cycle4_host_authority_modules`). -// -// Template: `tools/pb0_cycle4_emit_templates/.rs.in` (workspace root). -// Dissolution receipt: PB-0 NON_TEST partition — `REGEN_OUTPUTS` producer; full `.dag` emit T-LP track. -// - -//! `CollectionOps.*_contract` fields cross `DeclarationRef` boundaries. Emit -//! consumers must fail closed on the **algebra identity** (P1): each referenced -//! declaration must be a `MethodTemplateContract` data binding whose -//! `dag_method.decl` resolves to the expected registry `*_method` row. - -use super::method_emit_template_variant_label; -use crate::dag::{Dag, DeclarationId, FieldValue, LiteralBits, TypeConnective, ValueBody}; - -/// 🟢 TERMINAL at the CollectionOps contract-consumer projection boundary. -/// This mirrors the substrate `MethodEmitTemplate` coproduct exactly so Rust -/// CollectionOps can choose the HO inline rendering without flattening or -/// copying template text into a third authority. -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum MethodTemplateContractEmitTemplate { - SingleTemplate(String), - HigherOrderTemplates { - inline_template: String, - fn_ref_template: String, - }, -} - -/// Fail closed unless `contract_decl` is a `MethodTemplateContract` instance -/// whose `dag_method.decl` resolves to `expected_method_decl`. -pub(crate) fn require_method_template_contract_dag_method( - dag: &Dag, - contract_decl: DeclarationId, - collection_ops_field: &'static str, - expected_method_decl: DeclarationId, -) -> Result<(), &'static str> { - let fields = method_template_contract_fields(dag, contract_decl)?; - require_method_template_contract_fields_dag_method( - fields, - collection_ops_field, - expected_method_decl, - ) -} - -pub(crate) fn method_template_contract_decl_emit_template( - dag: &Dag, - contract_decl: DeclarationId, - collection_ops_field: &'static str, - expected_method_decl: DeclarationId, -) -> Result { - let fields = method_template_contract_fields(dag, contract_decl)?; - require_method_template_contract_fields_dag_method( - fields, - collection_ops_field, - expected_method_decl, - )?; - method_template_contract_fields_emit_template(dag, fields) -} - -fn method_template_contract_fields( - dag: &Dag, - contract_decl: DeclarationId, -) -> Result<&[(String, FieldValue)], &'static str> { - let mtc_type_id = dag - .method_template_contract_decl() - .ok_or("internal: MethodTemplateContract type missing from dag")?; - - let decl = dag.declaration(contract_decl); - let template = match &decl.connective { - TypeConnective::Instantiation { template, .. } => *template, - _ => { - return Err( - "CollectionOps contract must reference a MethodTemplateContract data binding", - ); - } - }; - if template != mtc_type_id { - return Err("CollectionOps contract must instantiate MethodTemplateContract"); - } - - let Some(body) = decl.value_body.as_ref() else { - return Err( - "CollectionOps contract referenced declaration must carry a structural value body", - ); - }; - let fields = match body { - ValueBody::Structural { fields } => fields, - _ => { - return Err( - "CollectionOps contract referenced declaration must be structural MethodTemplateContract data", - ); - } - }; - Ok(fields) -} - -fn require_method_template_contract_fields_dag_method( - fields: &[(String, FieldValue)], - collection_ops_field: &'static str, - expected_method_decl: DeclarationId, -) -> Result<(), &'static str> { - let method_id = method_template_contract_fields_dag_method(fields)?; - if method_id != expected_method_decl { - return Err(match collection_ops_field { - "fold_contract" => "CollectionOps.fold_contract must target fold_method", - "concat_contract" => "CollectionOps.concat_contract must target concat_method", - "length_contract" => "CollectionOps.length_contract must target length_method", - "filter_contract" => "CollectionOps.filter_contract must target filter_method", - "flat_map_contract" => "CollectionOps.flat_map_contract must target flat_map_method", - "any_contract" => "CollectionOps.any_contract must target any_method", - "all_contract" => "CollectionOps.all_contract must target all_method", - "map_contract" => "CollectionOps.map_contract must target map_method", - "is_empty_contract" => { - "CollectionOps.is_empty_contract must target is_empty_method (FreeMonoid.is_empty -> Bool)" - } - _ => "CollectionOps contract dag_method does not match expected registry method", - }); - } - - Ok(()) -} - -fn method_template_contract_fields_dag_method( - fields: &[(String, FieldValue)], -) -> Result { - let dag_method = fields - .iter() - .find(|(l, _)| l == "dag_method") - .map(|(_, v)| v) - .ok_or("MethodTemplateContract missing dag_method field")?; - - let FieldValue::Record(method_ref_fields) = dag_method else { - return Err("MethodTemplateContract.dag_method must be a MethodRef record"); - }; - - let method_id = match method_ref_fields.as_slice() { - [(label, FieldValue::Reference(id))] if label == "decl" => *id, - _ => { - return Err( - "MethodTemplateContract.dag_method must be exactly `{ decl: DeclarationRef }` (MethodRef)", - ); - } - }; - Ok(method_id) -} - -fn method_template_contract_fields_emit_template( - dag: &Dag, - fields: &[(String, FieldValue)], -) -> Result { - let emit_value = fields - .iter() - .find(|(label, _)| label == "emit_template") - .map(|(_, v)| v) - .ok_or("MethodTemplateContract missing emit_template field")?; - let FieldValue::Variant { - constructor, - payload, - } = emit_value - else { - return Err("MethodTemplateContract.emit_template must be a sum variant"); - }; - let ctor_name = method_emit_template_variant_label(dag, *constructor).ok_or( - "MethodTemplateContract.emit_template variant not found under MethodEmitTemplate disj", - )?; - match ctor_name { - "SingleTemplate" => { - let [FieldValue::Literal(LiteralBits::String(template))] = payload.as_slice() else { - return Err("SingleTemplate must carry exactly one string template payload"); - }; - Ok(MethodTemplateContractEmitTemplate::SingleTemplate( - template.replace("%Q", "\""), - )) - } - "HigherOrderTemplates" => { - let [FieldValue::Literal(LiteralBits::String(inline_template)), FieldValue::Literal(LiteralBits::String(fn_ref_template))] = - payload.as_slice() - else { - return Err( - "HigherOrderTemplates must carry inline_template and fn_ref_template strings", - ); - }; - Ok(MethodTemplateContractEmitTemplate::HigherOrderTemplates { - inline_template: inline_template.replace("%Q", "\""), - fn_ref_template: fn_ref_template.replace("%Q", "\""), - }) - } - _ => Err("MethodTemplateContract.emit_template uses an unknown MethodEmitTemplate variant"), - } -} diff --git a/tools/pb0_cycle4_emit_templates/emit_rust_bin_shim.rs.in b/src/v3/compiler/src/emit_rust_bin_shim.rs similarity index 86% rename from tools/pb0_cycle4_emit_templates/emit_rust_bin_shim.rs.in rename to src/v3/compiler/src/emit_rust_bin_shim.rs index 5db3e863f06..3168e8fe4a3 100644 --- a/tools/pb0_cycle4_emit_templates/emit_rust_bin_shim.rs.in +++ b/src/v3/compiler/src/emit_rust_bin_shim.rs @@ -1,12 +1,10 @@ //! PB-1 — Rust `main.rs` shell for generated `[[bin]]` shims (Item 5). //! -//! **SG-0 / bounded-seed receipt:** this body is spliced from -//! `tools/pb0_cycle4_emit_templates/emit_rust_bin_shim.rs.in` by `build.rs` -//! (`emit_pb0_cycle4_host_authority_modules`); the output path is in -//! `GENERATED_FILES` / `REGEN_OUTPUTS` (not `EXPECTED_HAND_AUTHORED_NON_TEST`). -//! Narrow host helper for §4.2 shell text only; see -//! `docs/invariants/bounded-substrate-seed.md` (no new substrate primitive — -//! mirrors existing `std.process.ProcessExit`). +//! **SG-0 / bounded-seed receipt:** hand-authored path is enumerated in +//! `EXPECTED_HAND_AUTHORED_NON_TEST` in `sg0_census_test.rs` (not +//! `GENERATED_FILES` / `build.rs` output). Narrow host helper for §4.2 shell +//! text only; see `docs/invariants/bounded-substrate-seed.md` (no new +//! substrate primitive — mirrors existing `std.process.ProcessExit`). //! //! Authority: `docs/design-pb-runtime-interpreter.md` §4.2 (`main` → //! `ProcessExit` → `ExitCode`). This module owns **parameterized Rust text** diff --git a/src/v3/compiler/src/emit_rust_bin_shim_generated.rs b/src/v3/compiler/src/emit_rust_bin_shim_generated.rs deleted file mode 100644 index 33f4e66cc7c..00000000000 --- a/src/v3/compiler/src/emit_rust_bin_shim_generated.rs +++ /dev/null @@ -1,100 +0,0 @@ -// AUTO-GENERATED by src/v3/compiler/build.rs (`emit_pb0_cycle4_host_authority_modules`). -// -// Template: `tools/pb0_cycle4_emit_templates/.rs.in` (workspace root). -// Dissolution receipt: PB-0 NON_TEST partition — `REGEN_OUTPUTS` producer; full `.dag` emit T-LP track. -// - -//! PB-1 — Rust `main.rs` shell for generated `[[bin]]` shims (Item 5). -//! -//! **SG-0 / bounded-seed receipt:** this body is spliced from -//! `tools/pb0_cycle4_emit_templates/emit_rust_bin_shim.rs.in` by `build.rs` -//! (`emit_pb0_cycle4_host_authority_modules`); the output path is in -//! `GENERATED_FILES` / `REGEN_OUTPUTS` (not `EXPECTED_HAND_AUTHORED_NON_TEST`). -//! Narrow host helper for §4.2 shell text only; see -//! `docs/invariants/bounded-substrate-seed.md` (no new substrate primitive — -//! mirrors existing `std.process.ProcessExit`). -//! -//! Authority: `docs/design-pb-runtime-interpreter.md` §4.2 (`main` → -//! `ProcessExit` → `ExitCode`). This module owns **parameterized Rust text** -//! only: it does not resolve `BinShim.entry` `DeclarationRef` targets (row-#1 -//! / host bridge gap — STOP there) and does not wire `cargo`/build.rs. - -/// Formats the `main.rs` wrapper for a `.dag`-authored bin shim. -/// -/// `source_dag_path` is emitted verbatim in the `AUTO-GENERATED` header (one -/// line). `description` is split into `//` comment lines. `entry_fn_qname` is -/// the Rust-qualified symbol for the `.dag` entry function returning -/// [`crate::process_exit::ProcessExit`]. -pub fn format_bin_shim_main_rs( - source_dag_path: &str, - description: &str, - entry_fn_qname: &str, -) -> String { - let mut out = String::new(); - out.push_str("// AUTO-GENERATED from "); - out.push_str(source_dag_path); - out.push_str(" — DO NOT EDIT.\n//\n"); - if description.is_empty() { - out.push_str("//\n"); - } else { - for line in description.lines() { - out.push_str("// "); - out.push_str(line); - out.push('\n'); - } - } - out.push_str( - r#" -use std::io::Write; -use std::process::ExitCode; - -use v3_compiler::dag::Dag; -use v3_compiler::process_exit::ProcessExit; - -fn main() -> ExitCode { - match "#, - ); - out.push_str(entry_fn_qname); - out.push_str( - r#"(&Dag::new()) { - ProcessExit::ExitSuccess => ExitCode::SUCCESS, - ProcessExit::ExitFailure { code, reason } => { - let _ = writeln!(std::io::stderr(), "{reason}"); - // Fail-closed: `ExitFailure` must not map to a successful host exit. - // Non-positive or out-of-range codes remap into 1..=255 (see dsl/std/process.dag). - ExitCode::from((code.max(1).min(255)) as u8) - } - } -} -"#, - ); - out -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn format_bin_shim_main_rs_includes_header_entry_and_process_exit() { - let out = format_bin_shim_main_rs( - "dsl/std/runtime/bin_shims/regen_lens.dag", - "Unified lens-regen driver.", - "std::runtime::bin_shims::regen_lens::regen_lens_main", - ); - assert!(out.contains( - "// AUTO-GENERATED from dsl/std/runtime/bin_shims/regen_lens.dag — DO NOT EDIT." - )); - assert!(out.contains("// Unified lens-regen driver.")); - assert!(out.contains("use v3_compiler::process_exit::ProcessExit;")); - assert!(out.contains("code.max(1).min(255)")); - assert!(!out.contains("code.clamp(0, 255)")); - assert!(out.contains("std::runtime::bin_shims::regen_lens::regen_lens_main(&Dag::new())")); - } - - #[test] - fn format_bin_shim_main_rs_multiline_description() { - let out = format_bin_shim_main_rs("path/to/shim.dag", "line1\nline2", "m::f"); - assert!(out.contains("// line1\n// line2\n")); - } -} diff --git a/tools/pb0_cycle4_emit_templates/enforced_lens_application.rs.in b/src/v3/compiler/src/enforced_lens_application.rs similarity index 100% rename from tools/pb0_cycle4_emit_templates/enforced_lens_application.rs.in rename to src/v3/compiler/src/enforced_lens_application.rs diff --git a/src/v3/compiler/src/enforced_lens_application_generated.rs b/src/v3/compiler/src/enforced_lens_application_generated.rs deleted file mode 100644 index cea3c52dbb3..00000000000 --- a/src/v3/compiler/src/enforced_lens_application_generated.rs +++ /dev/null @@ -1,1314 +0,0 @@ -// AUTO-GENERATED by src/v3/compiler/build.rs (`emit_pb0_cycle4_host_authority_modules`). -// -// Template: `tools/pb0_cycle4_emit_templates/.rs.in` (workspace root). -// Dissolution receipt: PB-0 NON_TEST partition — `REGEN_OUTPUTS` producer; full `.dag` emit T-LP track. -// - -//! Enforced lens applications (`EnforcedApplication` from `lens_application.dag`). -//! -//! Gate #91 (`enforce_violation_routing_landed`): budget-violation diagnostics are -//! routed through the authored `diagnostic_severity` field — today the single -//! substrate variant `DiagnosticSeverity::Error` maps to [`Diagnostic::ParseError`] -//! (compile-fail / fail-closed), per `lens_application.dag`, design §3, and -//! INVARIANTS C-8. -//! -//! Future extra `DiagnosticSeverity` constructors stay fail-closed at this consumer until -//! an intentional substrate + routing co-update assigns their `Diagnostic::*` shape. -//! -//! Gate #92 (`complexity_violation_compile_error_demonstrated`): when a program -//! authors `EnforcedApplication` referencing -//! `complexity_enforceable`, infer checks the named section using the **same** -//! `LensEnforcement` projection + violation relation as `complexity.dag` -//! (`complexity_enforcement_project` / `complexity_enforcement_violates`, surfaced -//! through `complexity_lens_generated`) and attaches a compile diagnostic on violation. -//! -//! Gate #58 (`apply_lens_self_application_demonstrated`): when a program authors -//! `EnforcedApplication` referencing `timing_enforceable` -//! (`v3.std.timing_lens`), infer reads the **lowered** `TimingMeasurement` from the -//! `DeclarationScope` subject's structural `measurement` field **only after** verifying the -//! subject row's **nominal** `data …: RowTy = …` type declares `measurement: TimingMeasurement` -//! on `RowTy` (API-level contract; avoids unrelated records that merely spell a `measurement` -//! label). The same usage ceiling as `timing_enforcement_project` / `timing_enforcement_violates` -//! in `timing_lens.dag` applies: non-`Observed` reports are enforced via a **variant-shaped** -//! fault path (never compared as ordinary wall-clock `Nat` against the fault sentinel), while -//! `Observed` wall-clock nanoseconds use the same strict `>` edge as gate #94 against -//! `TimingBudget.max`. - -use std::collections::HashMap; - -use crate::dag::{ - literal_decimal_i64, positive_descent_count, ArrowBody, AsymptoticClass, Behavior, Dag, - DeclarationId, FieldValue, LiteralBits, Lookup, PortId, PositiveDescentAmount, TypeConnective, - ValueBody, -}; -use crate::diagnostics::{Diagnostic, SourceSpan}; -use crate::lens_cost::{ - complexity_enforcement_project, complexity_enforcement_violates, complexity_of, -}; - -fn diagnostic_severity_substrate_disj(dag: &Dag) -> Option { - dag.declarations() - .iter() - .find(|d| { - d.name.as_deref() == Some("DiagnosticSeverity") - && d.span.file.ends_with("lens_application.dag") - }) - .map(|d| d.id) -} - -// Fail-closed when `lens_application.dag` does not expose the expected `DiagnosticSeverity` -// substrate row — factored so unit tests can pin the diagnostic without staging a full -// complexity-enforcement DAG (`Dag::new()` omits `complexity_enforceable`). -fn attach_missing_diagnostic_severity_substrate_diagnostic( - dag: &mut Dag, - enforced_template: DeclarationId, -) { - dag.attach_diagnostic(Diagnostic::ParseError { - message: "lens enforcement: could not resolve substrate `DiagnosticSeverity` from \ - `lens_application.dag` (modeled authority missing; fail-closed)" - .to_string(), - span: dag.declaration(enforced_template).span.clone(), - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }); -} - -fn timing_lens_gate_58_retirement_correction() -> crate::diagnostics::Correction { - crate::diagnostics::Correction::deferred( - "Gate #58 timing enforcement does not yet retain a source span for a safe timing witness rewrite", - "R3 Gap 9 row #106 timing-lens diagnostic roundtrip", - "thread timing measurement/budget source spans through gate #58 enforcement and replace this scaffold diagnostic with a generated/data-backed LiveCorrection", - ) -} - -/// Reads `timing_enforcement_fault_sentinel_count` from the lowered `timing_lens.dag` declaration -/// body (nullary `fn` → bind → [`ValueNode`]), matching substrate authority instead of duplicating -/// the decimal literal in Rust. -fn timing_enforcement_fault_sentinel_ns_from_substrate(dag: &Dag) -> Option { - let decl = dag.declarations().iter().find(|d| { - d.name.as_deref() == Some("timing_enforcement_fault_sentinel_count") - && d.span.file.ends_with("timing_lens.dag") - })?; - let TypeConnective::Arrow { - body: ArrowBody::UserDefined(bind_id), - .. - } = &decl.connective - else { - return None; - }; - let bind = (*bind_id).bind_opt(dag)?; - let producer = dag.resolve_producer_opt(&bind.value)?; - let Behavior::Value(vn) = producer else { - return None; - }; - let LiteralBits::Int(s) = &vn.data else { - return None; - }; - s.parse().ok() -} - -fn timing_measurement_sum_type_decl_id(dag: &Dag) -> Option { - dag.declarations() - .iter() - .find(|d| { - d.name.as_deref() == Some("TimingMeasurement") - && d.span.file.ends_with("timing_lens.dag") - }) - .map(|d| d.id) -} - -fn peel_type_declaration_head(dag: &Dag, mut id: DeclarationId) -> DeclarationId { - const MAX: usize = 64; - for _ in 0..MAX { - match &dag.declaration(id).connective { - TypeConnective::Instantiation { - template, - arguments, - } if arguments.is_empty() => { - id = *template; - } - TypeConnective::Atom(ap) => { - let Some(next) = ap.resolved_id() else { - break; - }; - id = next; - } - _ => break, - } - } - id -} - -/// Returns the nominal `RowTy` declaration id for a lowered `data row: RowTy = …` value row. -fn data_row_nominal_type_decl_id(dag: &Dag, value_decl_id: DeclarationId) -> Option { - match &dag.declaration(value_decl_id).connective { - TypeConnective::Instantiation { - template, - arguments, - } if arguments.is_empty() => Some(*template), - _ => None, - } -} - -/// True when `RowTy` (after alias peel) is a conj that declares `measurement` with type peeling -/// to the substrate `TimingMeasurement` sum at `tm_disj`. -fn timing_section_row_type_declares_measurement_tm( - dag: &Dag, - section_value_decl_id: DeclarationId, - tm_disj: DeclarationId, -) -> bool { - let Some(row_ty) = data_row_nominal_type_decl_id(dag, section_value_decl_id) else { - return false; - }; - let ty_head = peel_type_declaration_head(dag, row_ty); - let TypeConnective::Conj { children } = &dag.declaration(ty_head).connective else { - return false; - }; - let tm_head = peel_type_declaration_head(dag, tm_disj); - children - .iter() - .any(|f| f.label == "measurement" && peel_type_declaration_head(dag, f.ty) == tm_head) -} - -/// Lowered `TimingMeasurement` usage for timing `EnforcedApplication` enforcement. -/// -/// Separates non-evidence variants from `Observed` wall-clock nanoseconds so a legitimate -/// `Observed` duration equal to the substrate fault sentinel `Nat` cannot be conflated with -/// `Unobserved` / `Ambiguous` / `Stale` (INVARIANTS P2 / modeling-discipline Practice 2). -/// -/// Practice 4 (`docs/modeling-discipline.md`, coproduct checkpoint): **GREEN (terminal)** — mirrors -/// `timing_enforcement_project` / `timing_enforcement_violates` in `timing_lens.dag` on the lowered -/// `TimingMeasurement` witness (substrate `LensEnforcement.violates` now takes `TimingMeasurement` -/// × `TimingBudget` per `lens_application.dag`; gate #58 / codex 10994). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum TimingMeasurementEnforcementUsage { - NonObservedFault, - ObservedWallNs(u64), -} - -/// Interprets a lowered `TimingMeasurement` value the same way `timing_enforcement_project` does -/// on the substrate (`timing_lens.dag`): `Observed` → wall-clock nanoseconds; other report variants -/// classify as [`TimingMeasurementEnforcementUsage::NonObservedFault`] (not as a raw `Nat`). -fn timing_measurement_enforcement_usage( - dag: &Dag, - tm_disj: DeclarationId, - value: &FieldValue, -) -> Option { - let TypeConnective::Disj { variants } = &dag.declaration(tm_disj).connective else { - return None; - }; - let FieldValue::Variant { - constructor, - payload, - } = value - else { - return None; - }; - let variant = variants.iter().find(|v| v.ty == *constructor)?; - match variant.label.as_str() { - "Observed" => observed_timing_payload_max_ns(payload) - .map(TimingMeasurementEnforcementUsage::ObservedWallNs), - "Unobserved" | "Ambiguous" | "Stale" => { - Some(TimingMeasurementEnforcementUsage::NonObservedFault) - } - _ => None, - } -} - -/// Same reflexive edge as `timing_enforcement_violates` in `timing_lens.dag` on the lowered witness, -/// using the same `>` edge as substrate `Observed` enforcement (no `TimingBudget` sentinel equality -/// on the observed path). -pub(crate) fn timing_enforcement_violates( - declared_max_ns: u64, - usage: TimingMeasurementEnforcementUsage, -) -> bool { - match usage { - TimingMeasurementEnforcementUsage::NonObservedFault => true, - TimingMeasurementEnforcementUsage::ObservedWallNs(ns) => ns > declared_max_ns, - } -} - -fn observed_timing_payload_max_ns(payload: &[FieldValue]) -> Option { - let FieldValue::Record(parts) = payload.first()? else { - return None; - }; - if let Some((_, v)) = parts.iter().find(|(label, _)| label == "duration") { - return field_value_nat_magnitude(v); - } - field_value_nat_magnitude(&FieldValue::Record(parts.clone())) -} - -fn field_value_nat_magnitude(value: &FieldValue) -> Option { - match value { - FieldValue::Literal(LiteralBits::Int(s)) => s.parse().ok(), - FieldValue::Record(parts) => parts - .iter() - .find(|(label, _)| label == "count") - .and_then(|(_, v)| field_value_nat_magnitude(v)), - _ => None, - } -} - -fn field_value_timing_budget_max_ns(value: &FieldValue) -> Option { - let FieldValue::Record(parts) = value else { - return None; - }; - let max = parts - .iter() - .find(|(label, _)| label == "max") - .map(|(_, v)| v)?; - field_value_nat_magnitude(max) -} - -fn is_timing_enforceable_lens_value( - value: &FieldValue, - timing_enforceable_id: Option, - timing_lens_id: Option, - timing_enforcement_id: Option, -) -> bool { - if let (Some(bundle), FieldValue::Reference(id)) = (timing_enforceable_id, value) { - if *id == bundle { - return true; - } - } - let (Some(lens_id), Some(enforcement_id)) = (timing_lens_id, timing_enforcement_id) else { - return false; - }; - let FieldValue::Record(parts) = value else { - return false; - }; - let mut lens_ref: Option = None; - let mut enforcement_ref: Option = None; - for (label, field) in parts { - match (label.as_str(), field) { - ("lens", FieldValue::Reference(r)) => lens_ref = Some(*r), - ("enforcement", FieldValue::Reference(r)) => enforcement_ref = Some(*r), - _ => {} - } - } - lens_ref == Some(lens_id) && enforcement_ref == Some(enforcement_id) -} - -/// Fail-closed check for landed complexity + timing enforcement applications. -/// -/// Exported at the crate root (`v3_compiler::check_enforced_lens_applications`) so integration -/// tests can re-invoke the same post-infer pass as `infer::infer` without treating declaration -/// presence as proof the probe ran (gate #58 receipt). -pub fn check_enforced_lens_applications(dag: &mut Dag) { - let Some(enforced_template) = dag - .declarations() - .iter() - .find(|d| { - d.name.as_deref() == Some("EnforcedApplication") - && d.span.file.ends_with("lens_application.dag") - }) - .map(|d| d.id) - else { - return; - }; - let complexity_enforceable_id = dag - .declarations() - .iter() - .find(|d| { - d.name.as_deref() == Some("complexity_enforceable") - && d.span.file.ends_with("complexity.dag") - }) - .map(|d| d.id); - let timing_enforceable_id = dag - .declarations() - .iter() - .find(|d| { - d.name.as_deref() == Some("timing_enforceable") - && d.span.file.ends_with("timing_lens.dag") - }) - .map(|d| d.id); - let timing_lens_id = dag - .declarations() - .iter() - .find(|d| { - d.name.as_deref() == Some("timing_lens") && d.span.file.ends_with("timing_lens.dag") - }) - .map(|d| d.id); - let timing_enforcement_id = dag - .declarations() - .iter() - .find(|d| { - d.name.as_deref() == Some("timing_enforcement") - && d.span.file.ends_with("timing_lens.dag") - }) - .map(|d| d.id); - let asymptotic_disj = dag - .declarations() - .iter() - .find(|d| { - d.name.as_deref() == Some("AsymptoticClass") && d.span.file.ends_with("algebra.dag") - }) - .map(|d| d.id); - let Some(section_ref_disj) = dag - .declarations() - .iter() - .find(|d| { - d.name.as_deref() == Some("SectionRef") && d.span.file.ends_with("lens_application.dag") - }) - .map(|d| d.id) - else { - return; - }; - let Some(declaration_scope_conj) = declaration_scope_payload_conj(dag, section_ref_disj) else { - return; - }; - let Some(diagnostic_severity_disj) = diagnostic_severity_substrate_disj(dag) else { - attach_missing_diagnostic_severity_substrate_diagnostic(dag, enforced_template); - return; - }; - let positive_descent_disj = dag - .declaration_by_name("PositiveDescentAmount") - .map(|d| d.id); - - let mut violations: Vec = Vec::new(); - - for decl in dag.declarations() { - let Some(body) = decl.value_body.as_ref() else { - continue; - }; - let ValueBody::Structural { fields } = body else { - continue; - }; - let Some(meta_id) = decl.meta_tag else { - continue; - }; - if !matches_enforced_application_instantiation(dag, meta_id, enforced_template) { - continue; - } - let fm = field_map(fields); - let enforceable_lens = match fm.get("enforceable_lens") { - Some(v) => v, - _ => continue, - }; - let section = match fm.get("section") { - Some(v) => *v, - _ => continue, - }; - let budget_val = match fm.get("budget") { - Some(v) => *v, - _ => continue, - }; - let span = match fm.get("span") { - Some(FieldValue::Record(span_fields)) => record_as_source_span(span_fields), - _ => None, - } - .unwrap_or_else(|| decl.span.clone()); - - if let (Some(cid), FieldValue::Reference(ref_id)) = - (complexity_enforceable_id, enforceable_lens) - { - if *ref_id == cid { - let Some(asymptotic_disj) = asymptotic_disj else { - violations.push(Diagnostic::ParseError { - message: - "lens enforcement: could not resolve substrate `AsymptoticClass` from \ - `algebra.dag` (modeled authority missing; fail-closed)" - .to_string(), - span: decl.span.clone(), - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }); - continue; - }; - let Some(fn_decl) = resolve_declaration_scope_fn( - dag, - section, - section_ref_disj, - declaration_scope_conj, - ) else { - violations.push(Diagnostic::ParseError { - message: - "lens enforcement: could not resolve function declaration for section" - .to_string(), - span: decl.span.clone(), - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }); - continue; - }; - let Some(port) = fn_result_port(dag, fn_decl) else { - violations.push(Diagnostic::ParseError { - message: - "lens enforcement: could not resolve function result port for section" - .to_string(), - span: decl.span.clone(), - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }); - continue; - }; - let Some(budget_class) = field_value_asymptotic_class( - dag, - asymptotic_disj, - positive_descent_disj, - budget_val, - ) else { - violations.push(Diagnostic::ParseError { - message: - "lens enforcement: could not read complexity budget `AsymptoticClass` \ - (ill-formed `ClassPolynomial` budgets must use Peano `degree` ≥ 3; \ - use `ClassLinear` / `ClassQuadratic` for sub-cubic tiers)" - .to_string(), - span: decl.span.clone(), - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }); - continue; - }; - - let summary = match complexity_of(dag, &port) { - Lookup::Hit(s) => s, - Lookup::Miss => { - violations.push(Diagnostic::ParseError { - message: - "lens enforcement: complexity lens returned Miss for section — \ - cannot enforce budget" - .to_string(), - span: span.clone(), - correction: - crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }); - continue; - } - }; - if !complexity_enforcement_violates(&summary, &budget_class) { - continue; - } - let observed = complexity_enforcement_project(&summary); - let severity_val = match fm.get("diagnostic_severity") { - Some(v) => *v, - _ => { - violations.push(Diagnostic::ParseError { - message: - "lens enforcement: `EnforcedApplication` missing `diagnostic_severity`" - .to_string(), - span: decl.span.clone(), - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class("EnforcedLensApplicationDiagnostic"), - }); - continue; - } - }; - let violation_message = format!( - "lens enforcement violation: complexity budget {budget_class:?} exceeded \ - by observed class {observed:?} (declared `EnforcedApplication` at {})", - decl.name.as_deref().unwrap_or("?") - ); - violations.push(enforced_violation_diagnostic( - dag, - diagnostic_severity_disj, - severity_val, - violation_message, - span, - )); - continue; - } - } - - if is_timing_enforceable_lens_value( - enforceable_lens, - timing_enforceable_id, - timing_lens_id, - timing_enforcement_id, - ) { - let Some(tm_disj) = timing_measurement_sum_type_decl_id(dag) else { - violations.push(Diagnostic::ParseError { - message: - "lens enforcement: could not resolve substrate `TimingMeasurement` from \ - `timing_lens.dag` (modeled authority missing; fail-closed)" - .to_string(), - span: decl.span.clone(), - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }); - continue; - }; - let Some(section_decl_id) = - resolve_declaration_scope_declaration_id(section, declaration_scope_conj) - else { - violations.push(Diagnostic::ParseError { - message: "lens enforcement: could not resolve `DeclarationScope` for timing \ - `EnforcedApplication` section" - .to_string(), - span: decl.span.clone(), - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }); - continue; - }; - let section_decl = dag.declaration(section_decl_id); - if !timing_section_row_type_declares_measurement_tm(dag, section_decl_id, tm_disj) { - violations.push(Diagnostic::ParseError { - message: format!( - "lens enforcement: timing `EnforcedApplication` section `{}` must be a \ - lowered `data …: RowTy = …` row whose nominal `RowTy` declares \ - `measurement: TimingMeasurement` (same substrate contract as \ - `timing_enforcement_project`)", - section_decl.name.as_deref().unwrap_or("?") - ), - span: decl.span.clone(), - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }); - continue; - } - let Some(body) = section_decl.value_body.as_ref() else { - violations.push(Diagnostic::ParseError { - message: format!( - "lens enforcement: timing section `{}` has no lowered value body", - section_decl.name.as_deref().unwrap_or("?") - ), - span: decl.span.clone(), - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }); - continue; - }; - let ValueBody::Structural { fields } = body else { - violations.push(Diagnostic::ParseError { - message: format!( - "lens enforcement: timing section `{}` must lower to a structural record body", - section_decl.name.as_deref().unwrap_or("?") - ), - span: decl.span.clone(), - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class("EnforcedLensApplicationDiagnostic"), - }); - continue; - }; - let section_fm = field_map(fields); - let Some(measurement) = section_fm.get("measurement") else { - violations.push(Diagnostic::ParseError { - message: format!( - "lens enforcement: timing section `{}` is missing required `measurement` field", - section_decl.name.as_deref().unwrap_or("?") - ), - span: decl.span.clone(), - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class("EnforcedLensApplicationDiagnostic"), - }); - continue; - }; - let Some(usage_projection) = - timing_measurement_enforcement_usage(dag, tm_disj, measurement) - else { - violations.push(Diagnostic::ParseError { - message: format!( - "lens enforcement: could not interpret `measurement` as lowered \ - `TimingMeasurement` for section `{}`", - section_decl.name.as_deref().unwrap_or("?") - ), - span: decl.span.clone(), - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }); - continue; - }; - let Some(declared_max_ns) = field_value_timing_budget_max_ns(budget_val) else { - violations.push(Diagnostic::ParseError { - message: "lens enforcement: could not read timing budget `TimingBudget.max` \ - nanoseconds (expected `TimingBudget { max: Nanoseconds { count } }`)" - .to_string(), - span: decl.span.clone(), - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }); - continue; - }; - if !timing_enforcement_violates(declared_max_ns, usage_projection) { - continue; - }; - let usage_max_ns_for_message = match usage_projection { - TimingMeasurementEnforcementUsage::ObservedWallNs(ns) => ns, - TimingMeasurementEnforcementUsage::NonObservedFault => { - let Some(s) = timing_enforcement_fault_sentinel_ns_from_substrate(dag) else { - violations.push(Diagnostic::ParseError { - message: - "lens enforcement: could not resolve substrate `timing_enforcement_fault_sentinel_count` \ - from `timing_lens.dag` (needed for fault-path diagnostic text; fail-closed)" - .to_string(), - span: decl.span.clone(), - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class("EnforcedLensApplicationDiagnostic"), - }); - continue; - }; - s - } - }; - let severity_val = match fm.get("diagnostic_severity") { - Some(v) => *v, - _ => { - violations.push(Diagnostic::ParseError { - message: - "lens enforcement: `EnforcedApplication` missing `diagnostic_severity`" - .to_string(), - span: decl.span.clone(), - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }); - continue; - } - }; - let violation_message = format!( - "lens enforcement violation: timing budget ceiling max_ns={declared_max_ns} exceeded \ - by projected wall-clock usage max_ns={usage_max_ns_for_message} (declared `EnforcedApplication` \ - at {})", - decl.name.as_deref().unwrap_or("?") - ); - violations.push(enforced_violation_diagnostic( - dag, - diagnostic_severity_disj, - severity_val, - violation_message, - span, - )); - } - } - - for diagnostic in violations { - dag.attach_diagnostic(diagnostic); - } -} - -#[doc(hidden)] -fn gate_58_raise_observed_duration_ns_in_measurement( - value: &mut FieldValue, - duration_ns: u64, -) -> bool { - match value { - FieldValue::Variant { payload, .. } => { - let Some(FieldValue::Record(parts)) = payload.first_mut() else { - return false; - }; - if let Some((_, FieldValue::Record(duration_parts))) = - parts.iter_mut().find(|(label, _)| label == "duration") - { - return gate_58_set_count_field_to_ns(duration_parts, duration_ns); - } - // PB-1 bootstrap lowering: `Observed` payload can flatten to `{ count: Nat }` without - // a nested `duration` record (authoring uses `Observed { duration: { count } }`). - gate_58_set_count_field_to_ns(parts, duration_ns) - } - _ => false, - } -} - -fn gate_58_set_count_field_to_ns(parts: &mut [(String, FieldValue)], duration_ns: u64) -> bool { - for (dlabel, dfield) in parts.iter_mut() { - if dlabel == "count" { - if let FieldValue::Literal(LiteralBits::Int(s)) = dfield { - *s = duration_ns.to_string(); - return true; - } - return false; - } - } - false -} - -/// Integration receipt helper: mutates the PB-1 gate #58 witness row in a bootstrap [`Dag`]. -/// -/// **Not a supported production API** — only the `v3-compiler` integration test -/// `t_gate_58_apply_lens_self_application_test` should call this (exposed so that test binary can -/// link against `v3_compiler` without `cfg(test)` coupling). -#[doc(hidden)] -pub fn gate_58_test_raise_modeled_ci_timing_measurement_duration_ns( - dag: &mut Dag, - duration_ns: u64, -) -> Result<(), &'static str> { - let id = dag - .declarations() - .iter() - .find(|d| { - d.name.as_deref() == Some("gate_58_modeled_ci_timing_measurement") - && d.span.file.ends_with("t_ci_workflow_as_data_demo.dag") - }) - .map(|d| d.id) - .ok_or("missing gate_58_modeled_ci_timing_measurement declaration")?; - let decl = dag.declaration_mut(id); - let Some(ValueBody::Structural { fields }) = decl.value_body.as_mut() else { - return Err("gate_58 witness missing structural value_body"); - }; - for (label, value) in fields.iter_mut() { - if label == "measurement" - && gate_58_raise_observed_duration_ns_in_measurement(value, duration_ns) - { - return Ok(()); - } - } - Err("measurement field missing or not Observed with duration.count shape") -} - -/// Parses `(declared_max_ns, projected_usage_max_ns)` from a timing lens **budget-ceiling** -/// violation message produced by [`check_enforced_lens_applications`]. -/// -/// The template is owned next to [`timing_enforcement_violates`] / `violation_message` formatting -/// in this module (`max_ns=` appears exactly twice: declared ceiling, then projected usage). -/// Integration tests use this instead of pinning English prose (see repo `TESTING.md` anti-pattern). -#[doc(hidden)] -pub fn gate_58_test_parse_timing_budget_violation_max_ns_pair(message: &str) -> Option<(u64, u64)> { - const NEEDLE: &str = "max_ns="; - let mut found: Vec = Vec::with_capacity(2); - let mut rest = message; - while let Some(pos) = rest.find(NEEDLE) { - let after = pos + NEEDLE.len(); - let tail = &rest[after..]; - let digit_len = tail - .chars() - .take_while(|c| c.is_ascii_digit()) - .map(|c| c.len_utf8()) - .sum::(); - if digit_len == 0 { - return None; - } - let n: u64 = tail[..digit_len].parse().ok()?; - found.push(n); - rest = &tail[digit_len..]; - } - if found.len() == 2 { - Some((found[0], found[1])) - } else { - None - } -} - -fn field_map(fields: &[(String, FieldValue)]) -> HashMap<&str, &FieldValue> { - fields.iter().map(|(k, v)| (k.as_str(), v)).collect() -} - -/// Routes an enforce-mode **budget violation** through `EnforcedApplication.diagnostic_severity`. -/// -/// Substrate policy (C-8): only `DiagnosticSeverity::Error` is a valid steady-state choice; unknown -/// constructors, non-nullary `Error` payload, or malformed values fail closed with an explanatory -/// diagnostic. -fn enforced_violation_diagnostic( - dag: &Dag, - diagnostic_severity_disj: DeclarationId, - severity_val: &FieldValue, - violation_message: String, - span: SourceSpan, -) -> Diagnostic { - let TypeConnective::Disj { variants } = &dag.declaration(diagnostic_severity_disj).connective - else { - return Diagnostic::ParseError { - message: "lens enforcement: internal error (DiagnosticSeverity is not a sum type)" - .to_string(), - span, - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }; - }; - let Some(error_ctor) = variants.iter().find(|v| v.label == "Error").map(|v| v.ty) else { - return Diagnostic::ParseError { - message: "lens enforcement: internal error (DiagnosticSeverity lacks `Error` variant)" - .to_string(), - span, - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }; - }; - let FieldValue::Variant { - constructor, - payload, - } = severity_val - else { - return Diagnostic::ParseError { - message: "lens enforcement: `diagnostic_severity` must be a `DiagnosticSeverity` \ - variant value" - .to_string(), - span, - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }; - }; - if *constructor != error_ctor { - return Diagnostic::ParseError { - message: "lens enforcement: `diagnostic_severity` on `EnforcedApplication` must be \ - `Error` (INVARIANTS C-8; fail-closed discipline)" - .to_string(), - span, - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }; - } - // `lens_application.dag`: `type DiagnosticSeverity = Error` — the lone variant is nullary. - if !payload.is_empty() { - return Diagnostic::ParseError { - message: "lens enforcement: `DiagnosticSeverity::Error` must be nullary (INVARIANTS \ - P1; malformed variant payload)" - .to_string(), - span, - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "EnforcedLensApplicationDiagnostic", - ), - }; - } - Diagnostic::ParseError { - message: violation_message, - span, - correction: timing_lens_gate_58_retirement_correction(), - } -} - -fn matches_enforced_application_instantiation( - dag: &Dag, - mut meta: DeclarationId, - enforced_template: DeclarationId, -) -> bool { - for _ in 0..16 { - match &dag.declaration(meta).connective { - TypeConnective::Instantiation { - template, - arguments, - } => { - if *template == enforced_template && arguments.len() == 3 { - return true; - } - if arguments.is_empty() { - meta = *template; - continue; - } - return false; - } - _ => return false, - } - } - false -} - -fn declaration_scope_payload_conj( - dag: &Dag, - section_ref_disj: DeclarationId, -) -> Option { - let TypeConnective::Disj { variants } = &dag.declaration(section_ref_disj).connective else { - return None; - }; - let v = variants.iter().find(|f| f.label == "DeclarationScope")?; - Some(v.ty) -} - -fn resolve_declaration_scope_declaration_id( - section: &FieldValue, - declaration_scope_conj: DeclarationId, -) -> Option { - let FieldValue::Variant { - constructor, - payload, - } = section - else { - return None; - }; - if *constructor != declaration_scope_conj { - return None; - } - match payload.as_slice() { - [FieldValue::Reference(id)] => Some(*id), - [FieldValue::Record(parts)] => { - let mut decl_id: Option = None; - for (label, value) in parts { - if label == "declaration" { - if let FieldValue::Reference(id) = value { - decl_id = Some(*id); - } - } - } - decl_id - } - _ => None, - } -} - -fn resolve_declaration_scope_fn( - dag: &Dag, - section: &FieldValue, - section_ref_disj: DeclarationId, - declaration_scope_conj: DeclarationId, -) -> Option { - let _ = section_ref_disj; - let id = resolve_declaration_scope_declaration_id(section, declaration_scope_conj)?; - matches!(dag.declaration(id).connective, TypeConnective::Arrow { .. }).then_some(id) -} - -fn fn_result_port(dag: &Dag, fn_decl: DeclarationId) -> Option { - let decl = dag.declaration(fn_decl); - let TypeConnective::Arrow { - body: ArrowBody::UserDefined(bind_node), - .. - } = &decl.connective - else { - return None; - }; - let bind = bind_node.bind(dag); - Some(bind.result_port()) -} - -fn record_as_source_span(fields: &[(String, FieldValue)]) -> Option { - let mut file: Option = None; - let mut byte_start: Option = None; - let mut byte_end: Option = None; - for (label, value) in fields { - match (label.as_str(), value) { - ("file", FieldValue::Literal(LiteralBits::String(s))) => file = Some(s.clone()), - ("byte_start", FieldValue::Literal(LiteralBits::Int(n))) => { - byte_start = literal_decimal_i64(n.as_str()).and_then(|v| u32::try_from(v).ok()); - } - ("byte_end", FieldValue::Literal(LiteralBits::Int(n))) => { - byte_end = literal_decimal_i64(n.as_str()).and_then(|v| u32::try_from(v).ok()); - } - ("start", FieldValue::Literal(LiteralBits::Int(n))) => { - byte_start = literal_decimal_i64(n.as_str()).and_then(|v| u32::try_from(v).ok()); - } - ("end", FieldValue::Literal(LiteralBits::Int(n))) => { - byte_end = literal_decimal_i64(n.as_str()).and_then(|v| u32::try_from(v).ok()); - } - _ => {} - } - } - Some(SourceSpan::new(file?, byte_start?, byte_end?)) -} - -fn field_value_asymptotic_class( - dag: &Dag, - ac_disj: DeclarationId, - pda_disj: Option, - value: &FieldValue, -) -> Option { - let FieldValue::Variant { - constructor, - payload, - } = value - else { - return None; - }; - let TypeConnective::Disj { variants } = &dag.declaration(ac_disj).connective else { - return None; - }; - let variant = variants.iter().find(|v| v.ty == *constructor)?; - match variant.label.as_str() { - "ClassConstant" => Some(AsymptoticClass::ClassConstant), - "ClassLog" => Some(AsymptoticClass::ClassLog), - "ClassLinear" => Some(AsymptoticClass::ClassLinear), - "ClassLinearithmic" => Some(AsymptoticClass::ClassLinearithmic), - "ClassQuadratic" => Some(AsymptoticClass::ClassQuadratic), - "ClassExponential" => Some(AsymptoticClass::ClassExponential), - "ClassUnknown" => Some(AsymptoticClass::ClassUnknown), - "ClassPolynomial" => { - let pda = pda_disj?; - let degree = decode_positive_descent_amount(dag, pda, payload)?; - // `AsymptoticClass::ClassPolynomial` in `algebra.dag` is k ≥ 3; Peano - // counts 1–2 are `ClassLinear` / `ClassQuadratic` territory — reject as - // an invalid budget shape (fail closed; openai-pro / P1). - if positive_descent_count(°ree) < 3 { - return None; - } - Some(AsymptoticClass::ClassPolynomial { degree }) - } - _ => None, - } -} - -fn decode_positive_descent_amount( - dag: &Dag, - pda_disj: DeclarationId, - poly_payload: &[FieldValue], -) -> Option { - match poly_payload { - [FieldValue::Record(parts)] => { - let value = parts.iter().find(|(k, _)| k == "degree").map(|(_, v)| v)?; - decode_positive_descent_variant(dag, pda_disj, value) - } - [single] => decode_positive_descent_variant(dag, pda_disj, single), - _ => None, - } -} - -fn decode_positive_descent_variant( - dag: &Dag, - pda_disj: DeclarationId, - value: &FieldValue, -) -> Option { - let FieldValue::Variant { - constructor, - payload, - } = value - else { - return None; - }; - let TypeConnective::Disj { variants } = &dag.declaration(pda_disj).connective else { - return None; - }; - let variant = variants.iter().find(|v| v.ty == *constructor)?; - match variant.label.as_str() { - "OneStep" => Some(PositiveDescentAmount::OneStep), - "AdditionalStep" => { - let FieldValue::Record(parts) = payload.first()? else { - return None; - }; - let prev = parts - .iter() - .find(|(k, _)| k == "previous") - .map(|(_, v)| v)?; - let inner = decode_positive_descent_variant(dag, pda_disj, prev)?; - Some(PositiveDescentAmount::AdditionalStep { - previous: Box::new(inner), - }) - } - _ => None, - } -} - -#[cfg(test)] -mod diagnostic_severity_fail_closed_tests { - use super::*; - use crate::dag::{LiteralBits, TypeConnective}; - use crate::diagnostics::Diagnostic; - - fn assert_parse_error_at(diag: Diagnostic, expected_span: &SourceSpan) { - match diag { - Diagnostic::ParseError { - span, correction, .. - } => { - assert_eq!(&span, expected_span); - assert!(matches!( - correction, - crate::diagnostics::Correction::DeferredCorrection { .. } - )); - } - other => panic!("expected ParseError, got {other:?}"), - } - } - - #[test] - fn missing_diagnostic_severity_substrate_records_fail_closed_diagnostic() { - let mut dag = Dag::new(); - let Some(enforced_template) = dag - .declarations() - .iter() - .find(|d| { - d.name.as_deref() == Some("EnforcedApplication") - && d.span.file.ends_with("lens_application.dag") - }) - .map(|d| d.id) - else { - panic!("bootstrap should declare EnforcedApplication in lens_application.dag"); - }; - let expected_span = dag.declaration(enforced_template).span.clone(); - let Some(ds_id) = super::diagnostic_severity_substrate_disj(&dag) else { - panic!("bootstrap should declare DiagnosticSeverity in lens_application.dag"); - }; - dag.declaration_mut(ds_id).name = Some("DiagnosticSeverity__test_unresolvable".to_string()); - assert!( - super::diagnostic_severity_substrate_disj(&dag).is_none(), - "Expected DiagnosticSeverity substrate row to be unresolvable after rename" - ); - super::attach_missing_diagnostic_severity_substrate_diagnostic(&mut dag, enforced_template); - let mut it = dag.diagnostics().iter(); - let (_, d) = it.next().expect("fail-closed diagnostic"); - assert!(it.next().is_none(), "expected exactly one diagnostic"); - assert_parse_error_at(d.clone(), &expected_span); - } - - #[test] - fn enforced_violation_diagnostic_rejects_non_error_severity_constructor() { - let dag = Dag::new(); - let Some(ds_disj) = dag - .declarations() - .iter() - .find(|d| { - d.name.as_deref() == Some("DiagnosticSeverity") - && d.span.file.ends_with("lens_application.dag") - }) - .map(|d| d.id) - else { - panic!("bootstrap should declare DiagnosticSeverity in lens_application.dag"); - }; - let wrong_ctor = dag - .bool_runtime_variant_id(true) - .expect("bootstrap True variant"); - let expected_span = SourceSpan::new("t.v3", 0, 1); - let diag = enforced_violation_diagnostic( - &dag, - ds_disj, - &FieldValue::Variant { - constructor: wrong_ctor, - payload: Vec::new(), - }, - "violation".to_string(), - expected_span.clone(), - ); - assert_parse_error_at(diag, &expected_span); - } - - #[test] - fn enforced_violation_diagnostic_rejects_non_variant_severity_value() { - let dag = Dag::new(); - let Some(ds_disj) = dag - .declarations() - .iter() - .find(|d| { - d.name.as_deref() == Some("DiagnosticSeverity") - && d.span.file.ends_with("lens_application.dag") - }) - .map(|d| d.id) - else { - panic!("bootstrap should declare DiagnosticSeverity in lens_application.dag"); - }; - let expected_span = SourceSpan::new("t.v3", 0, 1); - let diag = enforced_violation_diagnostic( - &dag, - ds_disj, - &FieldValue::Literal(LiteralBits::Int("0".to_string())), - "violation".to_string(), - expected_span.clone(), - ); - assert_parse_error_at(diag, &expected_span); - } - - #[test] - fn enforced_violation_diagnostic_rejects_error_severity_with_non_nullary_payload() { - let dag = Dag::new(); - let Some(ds_disj) = dag - .declarations() - .iter() - .find(|d| { - d.name.as_deref() == Some("DiagnosticSeverity") - && d.span.file.ends_with("lens_application.dag") - }) - .map(|d| d.id) - else { - panic!("bootstrap should declare DiagnosticSeverity in lens_application.dag"); - }; - let TypeConnective::Disj { variants } = &dag.declaration(ds_disj).connective else { - panic!("bootstrap DiagnosticSeverity should be a sum type"); - }; - let error_ctor = variants - .iter() - .find(|v| v.label == "Error") - .map(|v| v.ty) - .expect("bootstrap DiagnosticSeverity should have Error variant"); - let expected_span = SourceSpan::new("t.v3", 0, 1); - let diag = enforced_violation_diagnostic( - &dag, - ds_disj, - &FieldValue::Variant { - constructor: error_ctor, - payload: vec![FieldValue::Literal(LiteralBits::Int("0".to_string()))], - }, - "violation".to_string(), - expected_span.clone(), - ); - assert_parse_error_at(diag, &expected_span); - } -} - -#[cfg(test)] -mod polynomial_budget_class_policy_tests { - use crate::dag::{positive_amount_from_i64, positive_descent_count, PositiveDescentAmount}; - - fn polynomial_budget_peano_is_admissible(degree: &PositiveDescentAmount) -> bool { - positive_descent_count(degree) >= 3 - } - - #[test] - fn class_polynomial_enforcement_budget_rejects_sub_cubic_peano_degrees() { - assert_eq!(positive_descent_count(&PositiveDescentAmount::OneStep), 1); - assert!(!polynomial_budget_peano_is_admissible( - &PositiveDescentAmount::OneStep - )); - let deg2 = PositiveDescentAmount::AdditionalStep { - previous: Box::new(PositiveDescentAmount::OneStep), - }; - assert_eq!(positive_descent_count(°2), 2); - assert!(!polynomial_budget_peano_is_admissible(°2)); - let deg3 = positive_amount_from_i64(3).expect("synthetic degree 3"); - assert!(polynomial_budget_peano_is_admissible(°3)); - } -} - -#[cfg(test)] -mod gate_58_timing_enforcement_unit_tests { - use super::{ - gate_58_test_parse_timing_budget_violation_max_ns_pair, - timing_enforcement_fault_sentinel_ns_from_substrate, timing_enforcement_violates, - TimingMeasurementEnforcementUsage, - }; - use crate::dag::Dag; - - #[test] - fn gate_58_parse_max_ns_pair_matches_timing_budget_violation_template() { - let msg = "lens enforcement violation: timing budget ceiling max_ns=1 exceeded \ - by projected wall-clock usage max_ns=999 (declared `EnforcedApplication` \ - at gate_58_enforcement_budget_violation)"; - assert_eq!( - gate_58_test_parse_timing_budget_violation_max_ns_pair(msg), - Some((1, 999)) - ); - } - - #[test] - fn non_observed_fault_always_violates_under_any_finite_budget() { - assert!(timing_enforcement_violates( - 1, - TimingMeasurementEnforcementUsage::NonObservedFault - )); - assert!(timing_enforcement_violates( - u64::MAX - 1, - TimingMeasurementEnforcementUsage::NonObservedFault - )); - } - - #[test] - fn observed_usage_strictly_exceeding_budget_violates() { - assert!(timing_enforcement_violates( - 500, - TimingMeasurementEnforcementUsage::ObservedWallNs(1000) - )); - } - - #[test] - fn observed_usage_within_or_equal_budget_is_clean() { - assert!(!timing_enforcement_violates( - 1000, - TimingMeasurementEnforcementUsage::ObservedWallNs(500) - )); - assert!(!timing_enforcement_violates( - 1000, - TimingMeasurementEnforcementUsage::ObservedWallNs(1000) - )); - } - - #[test] - fn observed_wall_at_substrate_fault_sentinel_is_not_fault_short_circuit() { - let dag = Dag::new(); - let s = timing_enforcement_fault_sentinel_ns_from_substrate(&dag) - .expect("bootstrap should carry timing_lens fault sentinel"); - assert!(!timing_enforcement_violates( - s + 1, - TimingMeasurementEnforcementUsage::ObservedWallNs(s) - )); - assert!(timing_enforcement_violates( - s - 1, - TimingMeasurementEnforcementUsage::ObservedWallNs(s) - )); - } -} diff --git a/tools/pb0_cycle4_emit_templates/int_literal_ranges.rs.in b/src/v3/compiler/src/int_literal_ranges.rs similarity index 100% rename from tools/pb0_cycle4_emit_templates/int_literal_ranges.rs.in rename to src/v3/compiler/src/int_literal_ranges.rs diff --git a/src/v3/compiler/src/int_literal_ranges_generated.rs b/src/v3/compiler/src/int_literal_ranges_generated.rs deleted file mode 100644 index c0df8ddceda..00000000000 --- a/src/v3/compiler/src/int_literal_ranges_generated.rs +++ /dev/null @@ -1,1026 +0,0 @@ -// AUTO-GENERATED by src/v3/compiler/build.rs (`emit_pb0_cycle4_host_authority_modules`). -// -// Template: `tools/pb0_cycle4_emit_templates/.rs.in` (workspace root). -// Dissolution receipt: PB-0 NON_TEST partition — `REGEN_OUTPUTS` producer; full `.dag` emit T-LP track. -// - -//! Integer literal magnitude vs declared integer types — **Q1 consumer**. -//! -//! Authoritative modeling: [`docs/design-emission-model.md`](../../../../docs/design-emission-model.md) -//! §Q1 — `BoundDeclaration = StaticBound(Interval) | PlatformDependent`, with asymmetric -//! match (target `Unbounded` universal-accept; target `ExactInterval` exact `lo`/`hi` equality at -//! the fold). This module implements only the **static** side needed for literal narrowing: -//! substrate range facts [`range_min_inclusive` / `range_max_inclusive`](../../../../dsl/extdeps/languages/rust/primitives.dag) -//! on [`rust_pilot_primitives`](crate::dag::Dag::rust_pilot_primitives) supply -//! `StaticBound(Interval)` as [`IntervalInt::ExactInterval`] (decimal endpoints + host -//! `BigInt` comparison; widened from `i128` per R3 Phase A so `u128::MAX` and any future wider -//! primitive is representable structurally). [`IntervalInt::Unbounded`] exists so Q1’s interval -//! algebra is representable when a -//! target declares an unbounded value domain (pilot `IntegerPrimitive` rows are all exact today). -//! [`PlatformDependent`] is out of scope for i64-bounded literal narrowing (deferred targets). -//! -//! ## Downstream consumers (range-facts + narrowing) -//! -//! | Location | Behavior | -//! | --- | --- | -//! | `infer::try_reconcile_int_literal_decision_set` | `let` / `data` pre-seed vs default `Int64` literal; in-range narrow; OOB → `MagnitudeOutOfRange`. | -//! | `infer::decide_transform` (calls) | Parameter-narrow type vs default-`Int` argument literal; narrow or OOB. | -//! | `infer::int_literal_implicit_bind_tolerated_for_expected` | Callable template binding when structural binding fails on int literal. | -//! | `lower` scalar literal lowering | Early reject for OOB literals before inference reunion. | -//! -//! ### R2 downstream audit ([`docs/briefs/r2-modeling-int-lit-magnitude-worker.md`](../../../../docs/briefs/r2-modeling-int-lit-magnitude-worker.md) §Slice 2) -//! -//! Post–[#1227](https://github.com/gunb-ai/gunbc/pull/1227) (**MethodEmitTemplate** Phase 1.5): no shared symbols with `emit_model.dag` method-template row lists -//! or `*_method_template_contracts` — those paths do **not** consult `rust_pilot_primitives` / this module. -//! -//! | Consumer | Range-facts / Q1 magnitude | -//! | --- | --- | -//! | `infer` (`try_reconcile_int_literal_decision_set`, `decide_transform`, `int_literal_implicit_bind_tolerated_for_expected`, transform/`PortUnion` narrow paths) | **Yes** — `integer_range_for_decl` + `magnitude_out_of_range_for_interval`. | -//! | `lower` (`lower_scalar_literal` / scalar literal outcome) | **Yes** — same facts for early OOB. | -//! | `emit::*` (e.g. `rust_target`) | **Indirect** — emits **resolved** shapes after inference; no parallel range-fact walk. | -//! | Method-template / emit-model carriers (**#1227**) | **N/A** — template contracts; unrelated to int-literal narrowing. | - -use std::collections::HashSet; -use std::str::FromStr; - -use num_bigint::BigInt; - -use crate::dag::{ - AtomPayload, Behavior, Dag, DeclarationId, FieldValue, LiteralBits, PortId, TemplateArgument, - TypeConnective, ValueBody, -}; -use crate::diagnostics::{Diagnostic, SourceSpan}; -use crate::types::TypeShape; - -/// Q1 `Interval` instance carried from String-decimal range facts (not `LiteralBits::Int` -/// widening — producer brief). -/// -/// **Host repr:** `min` / `max` are arbitrary-precision `BigInt` so `u128::MAX` (and any future -/// wider-than-i128 primitive) is representable structurally without per-width host-repr variant -/// explosion. Per Director Path A RATIFIED at gunbc#1739 #issuecomment-4392731264 (R3 Substrate -/// Rust-primitive-full-coverage bundled brief). The previous narrow `i128` host-repr deferred the -/// `u128` row in `dsl/extdeps/languages/rust/primitives.dag` because `u128::MAX` exceeds `i128` -/// range; that gap is closed by the `BigInt` host repr here. -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum IntervalInt { - /// Closed interval — substrate `range_*_inclusive` facts for a fixed-width target primitive. - ExactInterval { - target_name: String, - min_decimal: String, - max_decimal: String, - min: BigInt, - max: BigInt, - }, - /// Value-domain unbounded integer (e.g. arbitrary-precision target). Universal-accept for any - /// tokenized decimal literal magnitude (through `u128::MAX` / signed `|m| ≤ 2^127`). - /// - /// **Dissolution trigger (when this variant is constructed from [`integer_range_for_decl`]):** - /// a `rust_pilot_primitives` `IntegerPrimitive` row (or successor multi-target table) is - /// authored for a target whose Q1 `BoundDeclaration` is `StaticBound(Unbounded)` at magnitude - /// check — e.g. Python `int` per [`docs/design-emission-model.md`](../../../../docs/design-emission-model.md) - /// fold example (T-Ground cross-target / language `primitives.dag` work, not this consumer). - /// Until that producer exists, only [`IntervalInt::ExactInterval`] is returned from the pilot - /// list; [`Unbounded`] remains for `contains_bigint` / Q1 algebra completeness and unit tests. - #[allow(dead_code)] - Unbounded, -} - -/// Decimal endpoints for a **fixed** integer target — the payload of [`MagnitudeOutOfRange`]. -/// -/// **API split:** only [`magnitude_out_of_range`] takes this type. When you have a full -/// [`IntervalInt`] from [`integer_range_for_decl`], call [`magnitude_out_of_range_for_interval`] -/// (it uses [`IntervalInt::exact_interval_facts`] and never passes an unbounded domain into the -/// magnitude diagnostic). -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct ExactIntIntervalFacts { - pub(crate) target_name: String, - pub(crate) min_decimal: String, - pub(crate) max_decimal: String, -} - -impl IntervalInt { - /// Range check for a surface literal magnitude carried as decimal [`LiteralBits::Int`]. - pub(crate) fn contains_bigint(&self, value: &BigInt) -> bool { - match self { - IntervalInt::Unbounded => true, - IntervalInt::ExactInterval { min, max, .. } => min <= value && value <= max, - } - } - - /// Fixed-width pilot rows: `Some` for [`IntervalInt::ExactInterval`]; `None` for - /// [`IntervalInt::Unbounded`] (no decimal range to quote in `MagnitudeOutOfRange`). - pub(crate) fn exact_interval_facts(&self) -> Option { - match self { - IntervalInt::ExactInterval { - target_name, - min_decimal, - max_decimal, - .. - } => Some(ExactIntIntervalFacts { - target_name: target_name.clone(), - min_decimal: min_decimal.clone(), - max_decimal: max_decimal.clone(), - }), - IntervalInt::Unbounded => None, - } - } -} - -pub(crate) enum IntegerRangeLookup { - Found(IntervalInt), - Missing, - Invalid(Diagnostic), -} - -/// Structural witness for routing integer literals: `IntegerAlgebra` and -/// `TargetCarrier` variant **payload type** ids (the `constructor` field on -/// `FieldValue::Variant`), derived from std `OrderedRing` / `Semiring` -/// **or** `Compose>` (R3 gate #19 refinement form) -/// by resolving to the same pilot `(OrderedRing|Semiring, Word*)` routing key. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct IntegerRoutingWitness { - pub(crate) algebra_variant_ty: DeclarationId, - pub(crate) carrier_variant_ty: DeclarationId, -} - -/// Integration / structural tests: witness for `decl`'s resolved integer -/// instantiation (`OrderedRing` / `Semiring` + word carrier), if any. -pub(crate) fn integer_routing_witness_for_decl( - dag: &Dag, - decl: DeclarationId, -) -> Option { - integer_routing_witness_walk(dag, decl, 0) -} - -/// `Nat` (`dsl/std/nat.dag`: `Semiring`). Nonnegative literals share the tokenizer's -/// `u128` magnitude carrier; narrowing still rejects negative values structurally. -fn type_is_nat(dag: &Dag, mut decl: DeclarationId) -> bool { - let Some(nat_id) = dag.declaration_by_name("Nat").map(|d| d.id) else { - return false; - }; - let Some(semiring_template) = dag.declaration_by_name("Semiring").map(|d| d.id) else { - return false; - }; - let Some(magnitude_id) = dag.declaration_by_name("Magnitude").map(|d| d.id) else { - return false; - }; - for _ in 0..32 { - if decl == nat_id { - return true; - } - let declaration = dag.declaration(decl); - match &declaration.connective { - TypeConnective::Instantiation { - template, - arguments, - } if *template == semiring_template - && arguments.len() == 1 - && arguments[0].value == magnitude_id => - { - return true; - } - TypeConnective::Instantiation { template, .. } => decl = *template, - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => decl = *next, - _ => return false, - } - } - false -} - -fn nat_decimal_literal_interval() -> IntervalInt { - IntervalInt::ExactInterval { - target_name: "Nat".to_string(), - min_decimal: "0".to_string(), - max_decimal: u128::MAX.to_string(), - min: BigInt::from(0u8), - max: BigInt::from(u128::MAX), - } -} - -pub(crate) fn integer_range_for_decl(dag: &Dag, decl: DeclarationId) -> IntegerRangeLookup { - if type_is_nat(dag, decl) { - return IntegerRangeLookup::Found(nat_decimal_literal_interval()); - } - let Some(witness) = integer_routing_witness_walk(dag, decl, 0) else { - return IntegerRangeLookup::Missing; - }; - let Some(pilot) = dag.rust_pilot_primitives() else { - return IntegerRangeLookup::Missing; - }; - let Some(body) = pilot.value_body.as_ref() else { - return IntegerRangeLookup::Missing; - }; - let ValueBody::List(elements) = body else { - return IntegerRangeLookup::Missing; - }; - - let integer_primitive_ctor = match rust_primitive_integer_variant_ty(dag) { - Some(id) => id, - None => { - return IntegerRangeLookup::Invalid(malformed_integer_range_fact( - "bootstrap: RustPrimitive.IntegerPrimitive variant type is unavailable".to_string(), - pilot.span.clone(), - )); - } - }; - - let mut matches: Vec = Vec::new(); - for element in elements { - let FieldValue::Variant { - constructor, - payload, - } = element - else { - continue; - }; - if *constructor != integer_primitive_ctor { - continue; - } - match pilot_integer_row(witness, payload, pilot.span.clone()) { - Ok(Some(m)) => matches.push(m), - Ok(None) => {} - Err(diag) => return IntegerRangeLookup::Invalid(diag), - } - } - - if matches.is_empty() { - return IntegerRangeLookup::Missing; - } - if let Some(row) = matches.iter().find(|row| row.range.is_none()) { - return IntegerRangeLookup::Invalid(malformed_integer_range_fact( - format!( - "malformed rust_pilot_primitives IntegerPrimitive row for routing witness {:?}; integer literal range narrowing is unavailable", - (witness.algebra_variant_ty, witness.carrier_variant_ty) - ), - row.span.clone(), - )); - } - if matches.len() > 1 { - return IntegerRangeLookup::Invalid(malformed_integer_range_fact( - format!( - "duplicate rust_pilot_primitives IntegerPrimitive rows for routing witness {:?}; integer literal range narrowing is ambiguous", - (witness.algebra_variant_ty, witness.carrier_variant_ty) - ), - matches[1].span.clone(), - )); - } - let row = matches.remove(0); - match row.range { - Some(range) => IntegerRangeLookup::Found(range), - None => unreachable!("malformed matching rows are handled before duplicate detection"), - } -} - -struct PilotIntegerMatch { - range: Option, - span: SourceSpan, -} - -fn integer_routing_witness_walk( - dag: &Dag, - decl: DeclarationId, - depth: usize, -) -> Option { - if depth >= 32 { - return None; - } - let declaration = dag.declaration(decl); - match &declaration.connective { - TypeConnective::Atom(AtomPayload::ResolvedByName(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) => { - integer_routing_witness_walk(dag, *next, depth + 1) - } - TypeConnective::Instantiation { - template, - arguments, - } => { - if arguments.is_empty() { - return integer_routing_witness_walk(dag, *template, depth + 1); - } - if let Some(compose_id) = dag.declaration_by_name("Compose").map(|d| d.id) { - if *template == compose_id && arguments.len() == 2 { - if let Some(w) = compose_integer_routing_witness(dag, arguments.as_slice()) { - return Some(w); - } - } - } - let carrier = arguments.first()?.value; - integer_instantiation_witness(dag, *template, carrier) - } - _ => None, - } -} - -/// Gate #60 Phase 2.1: `MachineWidth`'s phantom argument may be a literal decimal -/// (`MachineWidth<64>`) naming the Nat magnitude; map to the existing `std.bit` width -/// authority (`Byte` / `Word16` / …) for the same integer-routing witness as Word*-spelled rows. -fn machine_width_inner_to_std_width_carrier_decl( - dag: &Dag, - inner_decl: DeclarationId, -) -> Option { - if std_word_carrier_to_target_carrier_variant_ty(dag, inner_decl).is_some() { - return Some(inner_decl); - } - match &dag.declaration(inner_decl).connective { - TypeConnective::Atom(AtomPayload::Literal(LiteralBits::Int(s))) => { - let n: u32 = s.parse().ok()?; - match n { - 8 => Some(dag.declaration_by_name("Byte")?.id), - 16 => Some(dag.declaration_by_name("Word16")?.id), - 32 => Some(dag.declaration_by_name("Word32")?.id), - 64 => Some(dag.declaration_by_name("Word64")?.id), - 128 => Some(dag.declaration_by_name("Word128")?.id), - _ => None, - } - } - _ => None, - } -} - -/// Fixed-width integers after gate #19: `Compose>` (signed) or -/// `Compose>` (unsigned). Maps to the same `(OrderedRing|Semiring, Word*)` -/// pilot routing key as the legacy `OrderedRing` / `Semiring` instantiations. -fn compose_integer_routing_witness( - dag: &Dag, - arguments: &[TemplateArgument], -) -> Option { - let int_id = dag.declaration_by_name("Int").map(|d| d.id)?; - let uint_id = dag.declaration_by_name("UInt").map(|d| d.id)?; - let machine_width = dag.declaration_by_name("MachineWidth").map(|d| d.id)?; - - let mut signed: Option = None; - let mut word_carrier: Option = None; - - for arg in arguments { - let v = arg.value; - if v == int_id { - signed = Some(true); - } else if v == uint_id { - signed = Some(false); - } else if let TypeConnective::Instantiation { - template, - arguments: mw_args, - } = &dag.declaration(v).connective - { - if *template == machine_width && mw_args.len() == 1 { - let inner = mw_args[0].value; - word_carrier = Some(machine_width_inner_to_std_width_carrier_decl(dag, inner)?); - } - } - } - - let signed = signed?; - let carrier_decl = word_carrier?; - let algebra_variant_ty = if signed { - disj_variant_payload_ty(dag, "IntegerAlgebra", "OrderedRingAlgebra")? - } else { - disj_variant_payload_ty(dag, "IntegerAlgebra", "SemiringAlgebra")? - }; - let carrier_variant_ty = std_word_carrier_to_target_carrier_variant_ty(dag, carrier_decl)?; - Some(IntegerRoutingWitness { - algebra_variant_ty, - carrier_variant_ty, - }) -} - -fn integer_instantiation_witness( - dag: &Dag, - template: DeclarationId, - carrier_decl: DeclarationId, -) -> Option { - let ordered_ring = dag.declaration_by_name("OrderedRing")?.id; - let semiring = dag.declaration_by_name("Semiring")?.id; - let algebra_variant_ty = if template == ordered_ring { - disj_variant_payload_ty(dag, "IntegerAlgebra", "OrderedRingAlgebra")? - } else if template == semiring { - disj_variant_payload_ty(dag, "IntegerAlgebra", "SemiringAlgebra")? - } else { - return None; - }; - let carrier_variant_ty = std_word_carrier_to_target_carrier_variant_ty(dag, carrier_decl)?; - Some(IntegerRoutingWitness { - algebra_variant_ty, - carrier_variant_ty, - }) -} - -fn disj_variant_payload_ty( - dag: &Dag, - sum_name: &str, - variant_label: &str, -) -> Option { - let decl = dag.declaration_by_name(sum_name)?; - let TypeConnective::Disj { variants } = &decl.connective else { - return None; - }; - variants - .iter() - .find(|v| v.label == variant_label) - .map(|v| v.ty) -} - -fn std_word_carrier_to_target_carrier_variant_ty( - dag: &Dag, - carrier_decl: DeclarationId, -) -> Option { - let byte = dag.declaration_by_name("Byte")?.id; - let word16 = dag.declaration_by_name("Word16")?.id; - let word32 = dag.declaration_by_name("Word32")?.id; - let word64 = dag.declaration_by_name("Word64")?.id; - let word128 = dag.declaration_by_name("Word128")?.id; - let label = if carrier_decl == byte { - "ByteCarrier" - } else if carrier_decl == word16 { - "Word16Carrier" - } else if carrier_decl == word32 { - "Word32Carrier" - } else if carrier_decl == word64 { - "Word64Carrier" - } else if carrier_decl == word128 { - "Word128Carrier" - } else { - return None; - }; - disj_variant_payload_ty(dag, "TargetCarrier", label) -} - -fn rust_primitive_integer_variant_ty(dag: &Dag) -> Option { - let rust_primitive = dag.declaration_by_name("RustPrimitive")?; - let TypeConnective::Disj { variants } = &rust_primitive.connective else { - return None; - }; - variants - .iter() - .find(|v| v.label == "IntegerPrimitive") - .map(|v| v.ty) -} - -fn pilot_integer_row( - witness: IntegerRoutingWitness, - payload: &[FieldValue], - default_span: SourceSpan, -) -> Result, Diagnostic> { - // IntegerPrimitive field order: target_name, algebra, carrier, range_*, is_copy, overflow - if payload.len() < 5 { - return Ok(None); - } - let FieldValue::Variant { - constructor: algebra_ctor, - .. - } = &payload[1] - else { - return Err(malformed_integer_range_fact( - "rust_pilot_primitives IntegerPrimitive `algebra` must be a variant value".to_string(), - default_span.clone(), - )); - }; - let FieldValue::Variant { - constructor: carrier_ctor, - .. - } = &payload[2] - else { - return Err(malformed_integer_range_fact( - "rust_pilot_primitives IntegerPrimitive `carrier` must be a variant value".to_string(), - default_span.clone(), - )); - }; - if *algebra_ctor != witness.algebra_variant_ty || *carrier_ctor != witness.carrier_variant_ty { - return Ok(None); - } - - let range = (|| { - let min_decimal = literal_string(payload.get(3)?)?; - let max_decimal = literal_string(payload.get(4)?)?; - let min = min_decimal.parse().ok()?; - let max = max_decimal.parse().ok()?; - if min > max { - return None; - } - Some(IntervalInt::ExactInterval { - target_name: literal_string(payload.first()?)?, - min_decimal, - max_decimal, - min, - max, - }) - })(); - - Ok(Some(PilotIntegerMatch { - range, - span: default_span, - })) -} - -fn literal_string(value: &FieldValue) -> Option { - match value { - FieldValue::Literal(LiteralBits::String(value)) => Some(value.clone()), - _ => None, - } -} - -pub(crate) fn literal_bigint_at(dag: &Dag, port: PortId) -> Option { - let s = match dag.resolve_producer_opt(&port)? { - Behavior::Value(value) => match &value.data { - LiteralBits::Int(s) => s.as_str(), - _ => return None, - }, - _ => return None, - }; - BigInt::from_str(s).ok() -} - -pub(crate) fn int_literal_fits_expected_type( - dag: &Dag, - literal: &BigInt, - expected: DeclarationId, -) -> Result, Diagnostic> { - match integer_range_for_decl(dag, expected) { - IntegerRangeLookup::Found(bound) => Ok(Some(bound.contains_bigint(literal))), - IntegerRangeLookup::Missing => Ok(None), - IntegerRangeLookup::Invalid(diag) => Err(diag), - } -} - -/// Build [`Diagnostic::MagnitudeOutOfRange`] from **exact** decimal range facts only. -/// -/// For a bound that may be [`IntervalInt::Unbounded`], use [`magnitude_out_of_range_for_interval`] -/// instead — this function does not accept [`IntervalInt`]. -pub(crate) fn magnitude_out_of_range( - literal: &BigInt, - expected: TypeShape, - facts: ExactIntIntervalFacts, - span: SourceSpan, -) -> Diagnostic { - Diagnostic::MagnitudeOutOfRange { - literal: literal.to_string(), - target: facts.target_name, - range_min_inclusive: facts.min_decimal, - range_max_inclusive: facts.max_decimal, - expected, - span, - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "IntegerLiteralRangeDiagnostic", - ), - } -} - -/// OOB diagnostic when the only available model is [`IntervalInt`] (e.g. from -/// [`integer_range_for_decl`]). Unbounded domains never produce `MagnitudeOutOfRange` for -/// finite literals; if that combination appears, fail closed without `unreachable!`. -pub(crate) fn magnitude_out_of_range_for_interval( - literal: &BigInt, - expected: TypeShape, - bound: IntervalInt, - span: SourceSpan, -) -> Diagnostic { - match bound.exact_interval_facts() { - Some(facts) => magnitude_out_of_range(literal, expected, facts, span), - None => Diagnostic::ResolveError { - name: "internal: integer literal failed range check but target has no exact interval facts" - .to_string(), - span, - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class("IntegerLiteralRangeDiagnostic"), - }, - } -} - -fn malformed_integer_range_fact(message: String, span: SourceSpan) -> Diagnostic { - Diagnostic::MalformedIntegerRangeFact { - message, - span, - correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( - "IntegerLiteralRangeDiagnostic", - ), - } -} - -/// Bootstrap-only gate: walk **every** `IntegerPrimitive` row in -/// `rust_pilot_primitives` and fail closed if any row is structurally -/// ill-formed, range strings do not parse as decimal `BigInt`, `min > max`, -/// or `(algebra, carrier)` witness pairs collide. -/// -/// Call this once when constructing the extdeps-including bootstrapped `Dag` -/// so drift or corruption in the pilot list surfaces at `Dag::new()`, -/// not only when a particular std type is queried. -pub(crate) fn validate_rust_pilot_integer_primitives(dag: &mut Dag) { - // R3 Phase B (Director Path A RATIFIED at gunbc#1739 #issuecomment-4392731264; - // Option (ii) at #issuecomment-4393145631; Phase B-1 at commit `59511503e`): - // pilot carries 10 IntegerPrimitive rows (i8..i64, i128, u8..u64, u128). - // u128 was unblocked by Phase A `IntervalInt::ExactInterval` BigInt host - // repr widening (commit `e7ba022c6`); the prior `T-Int128 Slice B1` / - // "Slice B2" deferral marker is resolved. - const EXPECTED_INTEGER_ROWS: usize = 10; - const INTEGER_PRIMITIVE_FIELD_COUNT: usize = 7; - - enum PilotListSnapshot { - List(Vec), - MissingBody, - NotList, - } - - // Authority file for span when the declaration is absent (extdeps fixture). - const RUST_PILOT_PRIMITIVES_AUTHORITY: &str = "dsl/extdeps/languages/rust/primitives.dag"; - - let (default_span, pilot_elements) = { - let Some(pilot) = dag.rust_pilot_primitives() else { - dag.attach_diagnostic(malformed_integer_range_fact( - "bootstrap: `rust_pilot_primitives` is missing from the extdeps fixture; \ - integer range authority is unavailable (expected extdeps `primitives.dag` load)" - .to_string(), - SourceSpan::new(RUST_PILOT_PRIMITIVES_AUTHORITY, 0, 0), - )); - return; - }; - let sp = pilot.span.clone(); - let snap = match pilot.value_body.as_ref() { - None => PilotListSnapshot::MissingBody, - Some(ValueBody::List(els)) => PilotListSnapshot::List(els.clone()), - Some(_) => PilotListSnapshot::NotList, - }; - (sp, snap) - }; - - let elements: &[FieldValue] = match &pilot_elements { - PilotListSnapshot::List(els) => els, - PilotListSnapshot::MissingBody => { - dag.attach_diagnostic(malformed_integer_range_fact( - "bootstrap: rust_pilot_primitives is missing a value body".to_string(), - default_span, - )); - return; - } - PilotListSnapshot::NotList => { - dag.attach_diagnostic(malformed_integer_range_fact( - "bootstrap: rust_pilot_primitives must be ValueBody::List".to_string(), - default_span, - )); - return; - } - }; - - let Some(integer_ctor) = rust_primitive_integer_variant_ty(dag) else { - dag.attach_diagnostic(malformed_integer_range_fact( - "bootstrap: RustPrimitive.IntegerPrimitive variant is unavailable".to_string(), - default_span, - )); - return; - }; - - let Some(ord_algebra) = disj_variant_payload_ty(dag, "IntegerAlgebra", "OrderedRingAlgebra") - else { - dag.attach_diagnostic(malformed_integer_range_fact( - "bootstrap: IntegerAlgebra.OrderedRingAlgebra is unavailable for pilot validation" - .to_string(), - default_span, - )); - return; - }; - let Some(sem_algebra) = disj_variant_payload_ty(dag, "IntegerAlgebra", "SemiringAlgebra") - else { - dag.attach_diagnostic(malformed_integer_range_fact( - "bootstrap: IntegerAlgebra.SemiringAlgebra is unavailable for pilot validation" - .to_string(), - default_span, - )); - return; - }; - let mut allowed_carriers: HashSet = HashSet::new(); - for label in [ - "ByteCarrier", - "Word16Carrier", - "Word32Carrier", - "Word64Carrier", - "Word128Carrier", - ] { - if let Some(c) = disj_variant_payload_ty(dag, "TargetCarrier", label) { - allowed_carriers.insert(c); - } - } - if allowed_carriers.is_empty() { - dag.attach_diagnostic(malformed_integer_range_fact( - "bootstrap: no TargetCarrier word variant payload types for pilot validation" - .to_string(), - default_span, - )); - return; - } - - let mut witnesses: HashSet<(DeclarationId, DeclarationId)> = HashSet::new(); - let mut integer_rows: usize = 0; - - for element in elements { - let FieldValue::Variant { - constructor, - payload, - } = element - else { - continue; - }; - if *constructor != integer_ctor { - continue; - } - integer_rows += 1; - - if payload.len() < INTEGER_PRIMITIVE_FIELD_COUNT { - dag.attach_diagnostic(malformed_integer_range_fact( - format!( - "rust_pilot_primitives IntegerPrimitive row has {} fields; expected {}", - payload.len(), - INTEGER_PRIMITIVE_FIELD_COUNT - ), - default_span.clone(), - )); - continue; - } - - let FieldValue::Variant { - constructor: algebra_ctor, - .. - } = &payload[1] - else { - dag.attach_diagnostic(malformed_integer_range_fact( - "rust_pilot_primitives IntegerPrimitive `algebra` must be a variant value" - .to_string(), - default_span.clone(), - )); - continue; - }; - if *algebra_ctor != ord_algebra && *algebra_ctor != sem_algebra { - dag.attach_diagnostic(malformed_integer_range_fact( - "rust_pilot_primitives IntegerPrimitive `algebra` must be OrderedRingAlgebra or SemiringAlgebra (variant payload type id)".to_string(), - default_span.clone(), - )); - continue; - } - - let FieldValue::Variant { - constructor: carrier_ctor, - .. - } = &payload[2] - else { - dag.attach_diagnostic(malformed_integer_range_fact( - "rust_pilot_primitives IntegerPrimitive `carrier` must be a variant value" - .to_string(), - default_span.clone(), - )); - continue; - }; - if !allowed_carriers.contains(carrier_ctor) { - dag.attach_diagnostic(malformed_integer_range_fact( - "rust_pilot_primitives IntegerPrimitive `carrier` must be a word TargetCarrier (Byte/Word16/Word32/Word64/Word128) variant payload type id".to_string(), - default_span.clone(), - )); - continue; - } - - if literal_string(&payload[0]).is_none() { - dag.attach_diagnostic(malformed_integer_range_fact( - "rust_pilot_primitives IntegerPrimitive `target_name` must be a string literal" - .to_string(), - default_span.clone(), - )); - continue; - } - - let (min_s, max_s) = match (literal_string(&payload[3]), literal_string(&payload[4])) { - (Some(a), Some(b)) => (a, b), - _ => { - dag.attach_diagnostic(malformed_integer_range_fact( - "rust_pilot_primitives IntegerPrimitive range bounds must be string literals" - .to_string(), - default_span.clone(), - )); - continue; - } - }; - - if !matches!(payload[5], FieldValue::Literal(LiteralBits::Bool(_))) { - dag.attach_diagnostic(malformed_integer_range_fact( - "rust_pilot_primitives IntegerPrimitive `is_copy` must be a bool literal" - .to_string(), - default_span.clone(), - )); - continue; - } - - if !matches!(&payload[6], FieldValue::Variant { .. }) { - dag.attach_diagnostic(malformed_integer_range_fact( - "rust_pilot_primitives IntegerPrimitive `overflow` must be a variant value" - .to_string(), - default_span.clone(), - )); - continue; - } - - let (min_n, max_n) = match (min_s.parse::(), max_s.parse::()) { - (Ok(mn), Ok(mx)) => (mn, mx), - _ => { - dag.attach_diagnostic(malformed_integer_range_fact( - format!( - "rust_pilot_primitives IntegerPrimitive range [{min_s}, {max_s}] must parse as a decimal integer" - ), - default_span.clone(), - )); - continue; - } - }; - if min_n > max_n { - dag.attach_diagnostic(malformed_integer_range_fact( - format!( - "rust_pilot_primitives IntegerPrimitive range order invalid: min {min_s} > max {max_s}" - ), - default_span.clone(), - )); - continue; - } - - if !witnesses.insert((*algebra_ctor, *carrier_ctor)) { - dag.attach_diagnostic(malformed_integer_range_fact( - format!( - "duplicate rust_pilot_primitives IntegerPrimitive (algebra, carrier) witness: ({algebra_ctor:?}, {carrier_ctor:?})" - ), - default_span.clone(), - )); - } - } - - if integer_rows != EXPECTED_INTEGER_ROWS { - dag.attach_diagnostic(malformed_integer_range_fact( - format!( - "rust_pilot_primitives must list exactly {EXPECTED_INTEGER_ROWS} IntegerPrimitive rows (pilot int scope); found {integer_rows}" - ), - default_span, - )); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::types::TypeShape; - - #[test] - fn magnitude_out_of_range_accepts_only_exact_int_interval_facts() { - let dag = Dag::new(); - let u8_decl = dag.declaration_by_name("UInt8").expect("UInt8").id; - let d = magnitude_out_of_range( - &BigInt::from(256), - TypeShape::new(u8_decl), - ExactIntIntervalFacts { - target_name: "u8".to_string(), - min_decimal: "0".to_string(), - max_decimal: "255".to_string(), - }, - SourceSpan::new("t.v3", 0, 0), - ); - assert!( - matches!( - d, - Diagnostic::MagnitudeOutOfRange { - ref literal, - ref target, - .. - } if literal == "256" && target == "u8" - ), - "expected MagnitudeOutOfRange, got {d:?}" - ); - } - - #[test] - fn magnitude_out_of_range_unbounded_target_fails_closed_with_resolve_error() { - let dag = Dag::new(); - let int_decl = dag.declaration_by_name("Int").expect("Int in bootstrap").id; - let d = magnitude_out_of_range_for_interval( - &BigInt::from(0), - TypeShape::new(int_decl), - IntervalInt::Unbounded, - SourceSpan::new("t.v3", 0, 0), - ); - assert!( - matches!(d, Diagnostic::ResolveError { .. }), - "expected fail-closed ResolveError, got {d:?}" - ); - } - - #[test] - fn interval_int_unbounded_accepts_sample_literals() { - assert!(IntervalInt::Unbounded.contains_bigint(&BigInt::from(i64::MIN))); - assert!(IntervalInt::Unbounded.contains_bigint(&BigInt::from(i64::MAX))); - } - - #[test] - fn int128_witness_matches_i128_pilot_row_constructors() { - // T-Int128 Slice B1: signed Int128 -> i128 pilot row via Word128Carrier. - let dag = Dag::new(); - assert!( - dag.diagnostics().is_empty(), - "bootstrap diagnostics: {:?}", - dag.diagnostics() - ); - let int128 = dag - .declaration_by_name("Int128") - .expect("Int128 in bootstrap") - .id; - let witness = integer_routing_witness_for_decl(&dag, int128).expect("Int128 witness"); - let pilot = dag.rust_pilot_primitives().expect("pilot"); - let ValueBody::List(elements) = pilot.value_body.as_ref().expect("list body") else { - panic!("expected list"); - }; - let integer_primitive_ctor = rust_primitive_integer_variant_ty(&dag).expect("ctor"); - let mut matched = 0usize; - for element in elements { - let FieldValue::Variant { - constructor, - payload, - } = element - else { - continue; - }; - if *constructor != integer_primitive_ctor { - continue; - } - let FieldValue::Variant { constructor: a, .. } = &payload[1] else { - continue; - }; - let FieldValue::Variant { constructor: c, .. } = &payload[2] else { - continue; - }; - if *a == witness.algebra_variant_ty && *c == witness.carrier_variant_ty { - matched += 1; - } - } - assert_eq!( - matched, 1, - "exactly one pilot IntegerPrimitive row for Int128 witness" - ); - } - - #[test] - fn int128_range_lookup_accepts_all_i64_magnitudes() { - // i128 row's range covers all i64 magnitudes; reconciliation passes - // typical literals through `contains_bigint`. - let dag = Dag::new(); - let int128 = dag - .declaration_by_name("Int128") - .expect("Int128 in bootstrap") - .id; - match integer_range_for_decl(&dag, int128) { - IntegerRangeLookup::Found(bound) => { - assert!(bound.contains_bigint(&BigInt::from(i64::MIN))); - assert!(bound.contains_bigint(&BigInt::from(0))); - assert!(bound.contains_bigint(&BigInt::from(i64::MAX))); - } - IntegerRangeLookup::Missing => panic!("expected Found range for Int128, got Missing"), - IntegerRangeLookup::Invalid(d) => { - panic!("expected Found range for Int128, got Invalid: {d:?}") - } - } - } - - #[test] - fn uint8_witness_matches_u8_pilot_row_constructors() { - let dag = Dag::new(); - assert!( - dag.diagnostics().is_empty(), - "bootstrap diagnostics: {:?}", - dag.diagnostics() - ); - let uint8 = dag - .declaration_by_name("UInt8") - .expect("UInt8 in bootstrap") - .id; - let witness = integer_routing_witness_for_decl(&dag, uint8).expect("UInt8 witness"); - let pilot = dag.rust_pilot_primitives().expect("pilot"); - let ValueBody::List(elements) = pilot.value_body.as_ref().expect("list body") else { - panic!("expected list"); - }; - let integer_primitive_ctor = rust_primitive_integer_variant_ty(&dag).expect("ctor"); - let mut matched = 0usize; - for element in elements { - let FieldValue::Variant { - constructor, - payload, - } = element - else { - continue; - }; - if *constructor != integer_primitive_ctor { - continue; - } - let FieldValue::Variant { constructor: a, .. } = &payload[1] else { - continue; - }; - let FieldValue::Variant { constructor: c, .. } = &payload[2] else { - continue; - }; - if *a == witness.algebra_variant_ty && *c == witness.carrier_variant_ty { - matched += 1; - } - } - assert_eq!( - matched, 1, - "exactly one pilot IntegerPrimitive row for UInt8 witness" - ); - } -} diff --git a/src/v3/compiler/src/lib.rs b/src/v3/compiler/src/lib.rs index dcd2374e27c..e7d83de6400 100644 --- a/src/v3/compiler/src/lib.rs +++ b/src/v3/compiler/src/lib.rs @@ -19,11 +19,9 @@ 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; -#[path = "complexity_lattice_generated.rs"] pub mod complexity_lattice; pub mod dag; pub mod diagnostics; -#[path = "enforced_lens_application_generated.rs"] mod enforced_lens_application; pub use enforced_lens_application::check_enforced_lens_applications; // Gate #58 integration receipts (`tests/integration/t_gate_58_apply_lens_self_application_test.rs`) @@ -35,7 +33,6 @@ 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; -#[path = "pb_method_template_projection_generated.rs"] pub mod pb_method_template_projection; mod regen_bootstrap_emit; pub mod regen_tokenize; @@ -55,7 +52,6 @@ pub mod generated_files { pub mod emit; pub mod emit_rust; -#[path = "emit_rust_bin_shim_generated.rs"] pub mod emit_rust_bin_shim; pub mod omni_shape_b_openapi; pub mod process_exit; @@ -3773,7 +3769,6 @@ pub mod evaluator { } } } -#[path = "int_literal_ranges_generated.rs"] mod int_literal_ranges; /// T-LensAPI D1: bounded lens interpreter over substrate-shaped [`FieldValue`] /// (R3 gate #5: `lens_apply.rs` retired; body in `lens_declaration_apply_body.txt`, included here @@ -4996,7 +4991,6 @@ pub use bootstrap_regen_fresh::{ compile_full_bootstrap_without_parse_surface_dag_from_std_seed, compile_std_bootstrap_dag, }; -#[path = "cost_basis_declaration_generated.rs"] mod cost_basis_declaration; mod dimension; mod infer; diff --git a/tools/pb0_cycle4_emit_templates/pb_method_template_projection.rs.in b/src/v3/compiler/src/pb_method_template_projection.rs similarity index 100% rename from tools/pb0_cycle4_emit_templates/pb_method_template_projection.rs.in rename to src/v3/compiler/src/pb_method_template_projection.rs diff --git a/src/v3/compiler/src/pb_method_template_projection_generated.rs b/src/v3/compiler/src/pb_method_template_projection_generated.rs deleted file mode 100644 index fd37aea1286..00000000000 --- a/src/v3/compiler/src/pb_method_template_projection_generated.rs +++ /dev/null @@ -1,1453 +0,0 @@ -// AUTO-GENERATED by src/v3/compiler/build.rs (`emit_pb0_cycle4_host_authority_modules`). -// -// Template: `tools/pb0_cycle4_emit_templates/.rs.in` (workspace root). -// Dissolution receipt: PB-0 NON_TEST partition — `REGEN_OUTPUTS` producer; full `.dag` emit T-LP track. -// - -//! PB-Bootstrap-Process consumer hook: project `MethodTemplateContract` rows -//! from the full bootstrap `Dag`. -//! -//! Implements **R3 row 85 / PB #1560 Gap 4** per the decision in -//! `docs/decisions/r3-row85-method-template-read-surface.md`. The single -//! row authority remains `src/v3/std/{rust,python,go}_method_template_contracts.dag`; -//! this module exposes a target-keyed structural projection of those rows -//! over the lowered bootstrap `Dag` so PB-zero / v2-retirement consumers can -//! select rows by target + `MethodRef` without: -//! -//! - importing `v3.std.*` into v2 crates (Gap 4 acceptance A1), -//! - introducing a second `Map` template-text authority (A2), -//! - copying any template strings into v2 sources (A3). -//! -//! All five `MethodTemplateContract` fields (`dag_method`, `runtime_template`, -//! `emit_template`, `wraps_result`, `placeholder_convention`) are preserved -//! by typed Rust mirrors below (A4). `emit_template` and -//! `placeholder_convention` are sum carriers; their substrate identity is -//! preserved as constructor-tagged enums, not flattened to strings. -//! -//! Gap 4 only — `LanguageSpec.method_templates` rewrite (Gap 5) and v2 leaf -//! emit migration are sequenced strictly after this hook lands. No v2 -//! consumer is migrated by this module. - -use std::collections::HashMap; - -use crate::dag::{Dag, DeclarationId, FieldValue, LiteralBits, TypeConnective, ValueBody}; - -/// Target language whose `MethodTemplateContract` row list is being projected. -/// -/// 🟢 TERMINAL at the per-target row-list scope. The three inhabitants -/// mirror the three closed list-declaration authorities under -/// `src/v3/std/{rust,python,go}_method_template_contracts.dag`. New -/// targets land by adding a row-list authority + a constructor here -/// together; no other dissolution path applies. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum MethodTemplateTarget { - Rust, - Python, - Go, -} - -impl MethodTemplateTarget { - /// Per-target list-declaration name in the bootstrap `Dag`. The strings - /// here mirror the data declaration names in - /// `src/v3/std/{rust,python,go}_method_template_contracts.dag` and are - /// the *names of structural declarations*, not template text. - fn list_declaration_name(self) -> &'static str { - match self { - MethodTemplateTarget::Rust => "rust_method_template_contracts", - MethodTemplateTarget::Python => "python_method_template_contracts", - MethodTemplateTarget::Go => "go_method_template_contracts", - } - } -} - -/// Projected `MethodEmitTemplate` (substrate sum at -/// `src/v3/std/emit_model.dag:469-474`). -/// -/// 🟢 TERMINAL at target method-emission-template scope, inheriting the -/// terminality of the substrate carrier (`MethodEmitTemplate` is marked -/// 🟢 TERMINAL at `emit_model.dag:453`). The two-variant split between -/// inline-lambda and function-reference renderings is the closed semantic -/// case the substrate already locks; new variants here would mean the -/// substrate carrier itself moved. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MethodEmitTemplateProjection { - Single { - template: String, - }, - HigherOrder { - inline_template: String, - fn_ref_template: String, - }, -} - -/// Projected `PlaceholderConvention` (substrate sum at -/// `src/v3/std/emit_model.dag:449-451`). -/// -/// 🟢 TERMINAL: nullary two-inhabitant sum (`IndexedArgs | NamedArg`) that -/// mirrors the closed substrate carrier. New conventions land by editing -/// the substrate sum first; this projection follows. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PlaceholderConventionProjection { - IndexedArgs, - NamedArg, -} - -/// Projected single `MethodTemplateContract` row. -/// -/// `dag_method` is the `DeclarationId` of the `MethodDeclaration` that the -/// row's `MethodRef.decl` field references (the typed, fail-closed substrate -/// identity for the method). Consumers select rows by target + -/// `dag_method` to recover the `(target, method)` template pair. -#[derive(Debug, Clone)] -pub struct MethodTemplateContractRow { - pub dag_method: DeclarationId, - pub runtime_template: String, - pub emit_template: MethodEmitTemplateProjection, - pub wraps_result: bool, - pub placeholder_convention: PlaceholderConventionProjection, -} - -/// Fail-closed projection error. Every observable mismatch between the -/// bootstrap `Dag` shape and the `MethodTemplateContract` substrate carrier -/// surfaces here as a typed value (per `INVARIANTS.md` C-8). -/// -/// 🟢 TERMINAL at the projection's bootstrap-mismatch scope. Each variant -/// names a specific structural class the row authorities cannot satisfy; -/// new variants only land if substrate shape grows a new sub-fact (which -/// would itself follow §P1). The carrier is therefore a closed -/// substrate-mismatch enumeration, not a transitional grab-bag. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MethodTemplateProjectionError { - /// Per-target row-list declaration is absent from the bootstrap `Dag`. - /// Should be impossible if `BOOTSTRAP_FIXTURE_PATH_KEYS` (`bootstrap.rs`) - /// stays in lockstep with `bootstrap_fixture_authority` and - /// `regen_bootstrap --verify` is green; surfaced rather than panicked - /// so consumers can act on it. - ListDeclarationMissing { name: &'static str }, - /// List declaration has no value body (i.e., not a `data` declaration). - ListDeclarationLacksValueBody { name: &'static str }, - /// List declaration's value body is not `ValueBody::List` — violates the - /// `bootstrap_method_template_contracts_lower_to_list_with_empty_diagnostics` - /// gate per the row-85 decision doc. - ListDeclarationValueBodyNotList { name: &'static str }, - /// Row at the given index in the named list is not a record literal. - RowNotRecord { - list: &'static str, - row_index: usize, - }, - /// Row is a declaration reference, but the referenced declaration is not - /// present in the bootstrap `Dag`. - RowReferenceMissing { - list: &'static str, - row_index: usize, - decl_id: DeclarationId, - }, - /// Row is a declaration reference, but the referenced declaration is not a - /// `data` declaration carrying a structural `MethodTemplateContract` body. - RowReferenceNotStructural { - list: &'static str, - row_index: usize, - decl_id: DeclarationId, - }, - /// Row is a declaration reference, but the referenced declaration does - /// not instantiate the `MethodTemplateContract` carrier. A structural - /// lookalike row is still not a contract row authority. - RowReferenceNotMethodTemplateContract { - list: &'static str, - row_index: usize, - decl_id: DeclarationId, - }, - /// Row is missing one of the five required fields. - RowMissingField { - list: &'static str, - row_index: usize, - field: &'static str, - }, - /// Row carries a duplicate field — two entries share a label. The - /// substrate-side `MethodTemplateContract` (and `MethodRef`) carriers - /// declare each field exactly once; duplicates would mean two - /// authorities for the same row coordinate (P2 single-authority). - RowDuplicateField { - list: &'static str, - row_index: usize, - record: &'static str, - field: &'static str, - first_field_index: usize, - duplicate_field_index: usize, - }, - /// Row carries a field whose label is not one of the closed schema - /// (`MethodTemplateContract` = five fields; `MethodRef` = one field). - /// Extra substrate fields land via the substrate carrier, not via - /// projection-side acceptance, so unknown labels surface here rather - /// than being silently ignored (P2 / C-8). - RowUnknownField { - list: &'static str, - row_index: usize, - record: &'static str, - field: String, - field_index: usize, - }, - /// `dag_method` field is not the `MethodRef { decl }` record shape. - DagMethodNotMethodRefRecord { - list: &'static str, - row_index: usize, - }, - /// `MethodRef.decl` is not a typed substrate reference. - MethodRefDeclNotReference { - list: &'static str, - row_index: usize, - }, - /// `MethodRef.decl` references a declaration that does not satisfy - /// the substrate-side `MethodDeclaration` *data-binding* contract - /// (`src/v3/std/methods.dag` + `dsl/std/methods.dag`). Mirrors the - /// Stratum A registry gate - /// (`src/v3/grounding_tests/src/stratum_a.rs::method_registry_name`): - /// the binding must (a) instantiate `MethodDeclaration`, (b) carry - /// a `Structural` value body, (c) have a closed single-`name` field - /// schema, (d) and the `name` value must be a string literal. The - /// `reason` payload identifies which sub-check failed. - MethodRefDeclNotMethodDeclaration { - list: &'static str, - row_index: usize, - decl_id: DeclarationId, - reason: MethodDeclarationBindingViolation, - }, - /// `method_template_contract_row` was called with a `dag_method` - /// `DeclarationId` that does not satisfy the - /// `MethodDeclaration` data-binding contract (same shape and - /// `reason` taxonomy as `MethodRefDeclNotMethodDeclaration`). The - /// helper validates at entry so callers cannot conflate - /// "valid method, no row" with "caller handed in a non-method - /// declaration." - LookupKeyNotMethodDeclaration { - decl_id: DeclarationId, - reason: MethodDeclarationBindingViolation, - }, - /// `MethodDeclaration` itself is missing from the bootstrap `Dag`. - /// Should be impossible if the std fixtures lower cleanly; surfaced - /// rather than panicked so consumers can act on it. - MethodDeclarationCarrierMissing, - /// `runtime_template` field is not a string literal. - RuntimeTemplateNotString { - list: &'static str, - row_index: usize, - }, - /// `wraps_result` field is not a bool literal. - WrapsResultNotBool { - list: &'static str, - row_index: usize, - }, - /// `placeholder_convention` field is not a substrate variant constructor. - PlaceholderConventionNotVariant { - list: &'static str, - row_index: usize, - }, - /// `placeholder_convention` constructor name is unknown. - PlaceholderConventionUnknown { - list: &'static str, - row_index: usize, - constructor: Option, - }, - /// `placeholder_convention` is a nullary sum (`IndexedArgs | NamedArg`), - /// but the variant payload is non-empty. Mirrors the substrate-side - /// canonical-row check - /// `placeholder_convention_canonical_rejects_payload_for_nullary_indexed_args` - /// in `src/v3/grounding_tests/src/stratum_a.rs`. - PlaceholderConventionPayloadNotEmpty { - list: &'static str, - row_index: usize, - constructor: &'static str, - payload_len: usize, - }, - /// `emit_template` field is not a substrate variant constructor. - EmitTemplateNotVariant { - list: &'static str, - row_index: usize, - }, - /// `emit_template` constructor name is unknown. - EmitTemplateUnknownConstructor { - list: &'static str, - row_index: usize, - constructor: Option, - }, - /// `emit_template` `SingleTemplate` payload shape mismatch (expected one - /// string-typed `template` field). - EmitTemplateSinglePayloadShape { - list: &'static str, - row_index: usize, - }, - /// `emit_template` `HigherOrderTemplates` payload shape mismatch - /// (expected two string-typed fields: `inline_template`, `fn_ref_template`). - EmitTemplateHigherOrderPayloadShape { - list: &'static str, - row_index: usize, - }, - /// The `(target, dag_method)` direct lookup helper found more than one row - /// for the same `MethodDeclaration` in the per-target list. Per-target - /// uniqueness by `dag_method` is the substrate-side claim - /// (`method_template_contract_per_target_dag_method_unique`); the helper - /// surfaces a violation rather than silently selecting the first row - /// (P2 single-authority + P3 fail-closed at the public lookup boundary). - DuplicateMethodTemplateRow { - list: &'static str, - first_row_index: usize, - duplicate_row_index: usize, - }, -} - -/// Granular reason for a `MethodDeclaration` data-binding contract failure. -/// -/// 🟢 TERMINAL at the substrate-side data-binding contract scope -/// (`src/v3/std/methods.dag` + Stratum A `method_registry_name`). Each -/// variant maps one of the four sub-checks the contract requires; the -/// substrate-side contract is closed (`MethodDeclaration` carries a -/// single `name: String` field), so this enumeration is closed too. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MethodDeclarationBindingViolation { - /// Connective is not `TypeConnective::Instantiation { template, .. }`. - /// Rules out type / sum / record declarations. - ConnectiveNotInstantiation, - /// `Instantiation` connective, but `template` does not match - /// `MethodDeclaration`'s declaration id. Rules out instantiations of - /// unrelated targets. - InstantiationTemplateNotMethodDeclaration, - /// `value_body` is `None`. Rules out type aliases of the form - /// `type Foo = MethodDeclaration`. - ValueBodyMissing, - /// `value_body` is `Some(_)` but not `ValueBody::Structural`. Rules - /// out `List` / `Map` / scalar bodies that would be wrong-shaped for - /// a `MethodDeclaration` data binding. - ValueBodyNotStructural, - /// `Structural` body, but its field set does not match the closed - /// `MethodDeclaration` schema (single `name` field). Mirrors - /// `enforce_closed_record_schema` in Stratum A. - StructuralFieldsNotClosedNameOnly { observed_labels: Vec }, - /// Closed-schema match, but the `name` field's `FieldValue` is not - /// `Literal(LiteralBits::String(_))`. - NameNotStringLiteral, -} - -/// Project the `MethodTemplateContract` rows for `target` from the full -/// bootstrap `Dag` snapshot. -/// -/// The expected source for `dag` is `generated_full_bootstrap_dag()` (or its -/// equivalent under PB-1-e dissolution-first per #1558 — see the -/// row-85 decision doc §"Non-Fork Ratchet"). Calling on a partially-built -/// `Dag` that is missing the per-target list authorities surfaces -/// [`MethodTemplateProjectionError::ListDeclarationMissing`] rather than -/// panicking. -/// -/// **Boundary contract:** this is the canonical PB-zero consumer hook for -/// row text. v2-side migrations consume `Vec` and -/// must not store or carry parallel `Map` template tables; -/// the structural carrier is the typed projection above. -pub fn method_template_contract_rows( - dag: &Dag, - target: MethodTemplateTarget, -) -> Result, MethodTemplateProjectionError> { - let list_name = target.list_declaration_name(); - let decl = dag - .declaration_by_name(list_name) - .ok_or(MethodTemplateProjectionError::ListDeclarationMissing { name: list_name })?; - let body = decl - .value_body - .as_ref() - .ok_or(MethodTemplateProjectionError::ListDeclarationLacksValueBody { name: list_name })?; - let ValueBody::List(rows) = body else { - return Err( - MethodTemplateProjectionError::ListDeclarationValueBodyNotList { name: list_name }, - ); - }; - - let projected: Vec = rows - .iter() - .enumerate() - .map(|(row_index, row)| project_row(dag, list_name, row_index, row)) - .collect::>()?; - - // Per-target uniqueness by `dag_method` is the substrate-side claim - // (`method_template_contract_per_target_dag_method_unique`). Enforce it - // at the canonical row-list boundary so v2-retirement consumers that - // build their own keyed tables from the projection cannot silently - // observe two authorities for the same `(target, MethodRef)` pair. - let mut first_seen: HashMap = HashMap::with_capacity(projected.len()); - for (index, row) in projected.iter().enumerate() { - if let Some(prior) = first_seen.get(&row.dag_method) { - return Err(MethodTemplateProjectionError::DuplicateMethodTemplateRow { - list: list_name, - first_row_index: *prior, - duplicate_row_index: index, - }); - } - first_seen.insert(row.dag_method, index); - } - - Ok(projected) -} - -/// Direct `(target, dag_method)` lookup helper for Gap-5 / leaf-emit -/// consumers. Returns the row whose `dag_method` matches `dag_method`, or -/// `None` if the per-target list does not contain a row keyed by that -/// `MethodDeclaration`. -/// -/// **Fail-closed on non-method keys.** `dag_method` must instantiate -/// `MethodDeclaration` — the helper validates this at entry so a caller -/// cannot conflate two distinct states: "valid `MethodDeclaration` with -/// no row for this target" (returns `Ok(None)`) versus "caller supplied -/// a declaration that is not a `MethodDeclaration` at all" (returns -/// [`MethodTemplateProjectionError::LookupKeyNotMethodDeclaration`]). -/// This mirrors the per-row `MethodRef.decl` check the projection -/// already enforces; until `DeclarationRef` -/// refinement-typing lands, this is the only place the boundary can -/// hold. -/// -/// Other typed projection failures — including -/// [`MethodTemplateProjectionError::DuplicateMethodTemplateRow`], which is -/// enforced at the canonical row-list boundary in -/// [`method_template_contract_rows`] — bubble through. -pub fn method_template_contract_row( - dag: &Dag, - target: MethodTemplateTarget, - dag_method: DeclarationId, -) -> Result, MethodTemplateProjectionError> { - let method_declaration_id = method_declaration_carrier_id(dag)?; - if let Err(reason) = - validate_method_declaration_data_binding(dag, dag_method, method_declaration_id) - { - return Err( - MethodTemplateProjectionError::LookupKeyNotMethodDeclaration { - decl_id: dag_method, - reason, - }, - ); - } - let rows = method_template_contract_rows(dag, target)?; - Ok(rows.into_iter().find(|row| row.dag_method == dag_method)) -} - -/// Locate the `MethodDeclaration` carrier declaration in the bootstrap -/// `Dag` once. Surfaces [`MethodTemplateProjectionError::MethodDeclarationCarrierMissing`] -/// when the carrier is absent rather than panicking. -pub(crate) fn method_declaration_carrier_id( - dag: &Dag, -) -> Result { - Ok(dag - .declaration_by_name("MethodDeclaration") - .ok_or(MethodTemplateProjectionError::MethodDeclarationCarrierMissing)? - .id) -} - -/// Validate that `decl_id` references a `MethodDeclaration` *data binding* -/// per the substrate-side contract (`src/v3/std/methods.dag` + -/// `dsl/std/methods.dag`). -/// -/// Mirrors `src/v3/grounding_tests/src/stratum_a.rs::method_registry_name` -/// so projection-side and Stratum A-side enforcement of the same gate -/// stay in lockstep. Four sub-checks must all pass: -/// -/// 1. **Connective is `Instantiation`.** Rules out type / sum / record -/// declarations. -/// 2. **`Instantiation.template == MethodDeclaration`.** Rules out -/// instantiations of unrelated targets. -/// 3. **`value_body` is `Some(ValueBody::Structural { .. })`.** Rules -/// out type aliases (`type Foo = MethodDeclaration`, no value body) -/// and wrong-shaped data bindings (`List` / `Map` / scalar bodies). -/// 4. **`Structural` fields are exactly `[name]` with a string literal -/// value.** Closed schema check — mirrors -/// `enforce_closed_record_schema` in Stratum A. Rules out extra -/// fields, missing `name`, duplicate `name`, and non-string-literal -/// `name` values. -/// -/// On success returns the projected method name (`name` field's string -/// literal). On failure returns the typed -/// [`MethodDeclarationBindingViolation`] reason. -/// -/// Both `project_dag_method` (per-row) and `method_template_contract_row` -/// (lookup helper) consume this helper so the data-binding identity is -/// factored once. -pub(crate) fn validate_method_declaration_data_binding( - dag: &Dag, - decl_id: DeclarationId, - method_declaration_id: DeclarationId, -) -> Result { - let referenced = dag.declaration(decl_id); - - // (1) + (2) Connective + template. - let template = match &referenced.connective { - TypeConnective::Instantiation { template, .. } => *template, - _ => return Err(MethodDeclarationBindingViolation::ConnectiveNotInstantiation), - }; - if template != method_declaration_id { - return Err(MethodDeclarationBindingViolation::InstantiationTemplateNotMethodDeclaration); - } - - // (3) Value body present and Structural. - let value_body = referenced - .value_body - .as_ref() - .ok_or(MethodDeclarationBindingViolation::ValueBodyMissing)?; - let ValueBody::Structural { fields } = value_body else { - return Err(MethodDeclarationBindingViolation::ValueBodyNotStructural); - }; - - // (4) Closed `[name]` schema with string-literal value. Reject any - // duplicate or unknown label, require `name` to be present exactly - // once. Mirrors Stratum A's enforce_closed_record_schema. - const EXPECTED: &[&str] = &["name"]; - let name_count = fields.iter().filter(|(label, _)| label == "name").count(); - let any_unknown = fields - .iter() - .any(|(label, _)| !EXPECTED.contains(&label.as_str())); - if fields.len() != EXPECTED.len() || name_count != 1 || any_unknown { - let observed_labels: Vec = fields.iter().map(|(label, _)| label.clone()).collect(); - return Err( - MethodDeclarationBindingViolation::StructuralFieldsNotClosedNameOnly { - observed_labels, - }, - ); - } - let (_, name_value) = fields - .iter() - .find(|(label, _)| label == "name") - .expect("closed-schema check above guarantees a single `name` field"); - match name_value { - FieldValue::Literal(LiteralBits::String(s)) => Ok(s.clone()), - _ => Err(MethodDeclarationBindingViolation::NameNotStringLiteral), - } -} - -fn project_row( - dag: &Dag, - list: &'static str, - row_index: usize, - row: &FieldValue, -) -> Result { - let fields = match row { - FieldValue::Record(fields) => fields, - FieldValue::Reference(decl_id) => { - let decl = dag.declaration_opt(decl_id).ok_or( - MethodTemplateProjectionError::RowReferenceMissing { - list, - row_index, - decl_id: *decl_id, - }, - )?; - let method_template_contract = dag.method_template_contract_decl().ok_or( - MethodTemplateProjectionError::RowReferenceNotMethodTemplateContract { - list, - row_index, - decl_id: *decl_id, - }, - )?; - let TypeConnective::Instantiation { template, .. } = &decl.connective else { - return Err( - MethodTemplateProjectionError::RowReferenceNotMethodTemplateContract { - list, - row_index, - decl_id: *decl_id, - }, - ); - }; - if *template != method_template_contract { - return Err( - MethodTemplateProjectionError::RowReferenceNotMethodTemplateContract { - list, - row_index, - decl_id: *decl_id, - }, - ); - } - match decl.value_body.as_ref() { - Some(ValueBody::Structural { fields }) => fields, - _ => { - return Err(MethodTemplateProjectionError::RowReferenceNotStructural { - list, - row_index, - decl_id: *decl_id, - }); - } - } - } - _ => return Err(MethodTemplateProjectionError::RowNotRecord { list, row_index }), - }; - - const EXPECTED: &[&str] = &[ - "dag_method", - "runtime_template", - "emit_template", - "wraps_result", - "placeholder_convention", - ]; - let lookup = closed_record_lookup(list, row_index, "MethodTemplateContract", fields, EXPECTED)?; - - let dag_method_value = lookup_required(list, row_index, &lookup, "dag_method")?; - let dag_method = project_dag_method(dag, list, row_index, dag_method_value)?; - - let runtime_template_value = lookup_required(list, row_index, &lookup, "runtime_template")?; - let runtime_template = match runtime_template_value { - FieldValue::Literal(LiteralBits::String(s)) => s.clone(), - _ => { - return Err(MethodTemplateProjectionError::RuntimeTemplateNotString { - list, - row_index, - }); - } - }; - - let emit_template_value = lookup_required(list, row_index, &lookup, "emit_template")?; - let emit_template = project_emit_template(dag, list, row_index, emit_template_value)?; - - let wraps_result_value = lookup_required(list, row_index, &lookup, "wraps_result")?; - let wraps_result = match wraps_result_value { - FieldValue::Literal(LiteralBits::Bool(b)) => *b, - _ => return Err(MethodTemplateProjectionError::WrapsResultNotBool { list, row_index }), - }; - - let placeholder_value = lookup_required(list, row_index, &lookup, "placeholder_convention")?; - let placeholder_convention = project_placeholder(dag, list, row_index, placeholder_value)?; - - Ok(MethodTemplateContractRow { - dag_method, - runtime_template, - emit_template, - wraps_result, - placeholder_convention, - }) -} - -/// Validate that `fields` carries exactly the closed set of `expected` -/// labels — no duplicates, no unknowns. Missing labels are not reported -/// here (the per-field `lookup_required` calls below surface those as -/// `RowMissingField` so the error names *which* field is missing). -/// -/// Returns a label-to-`FieldValue` map for the present fields; collisions -/// surface as `RowDuplicateField` and unknown labels surface as -/// `RowUnknownField`. `record` names the carrier (e.g. -/// `"MethodTemplateContract"` or `"MethodRef"`) for diagnostic context. -fn closed_record_lookup<'a>( - list: &'static str, - row_index: usize, - record: &'static str, - fields: &'a [(String, FieldValue)], - expected: &[&'static str], -) -> Result, MethodTemplateProjectionError> { - let mut lookup: HashMap<&'static str, &FieldValue> = HashMap::with_capacity(expected.len()); - let mut first_seen_index: HashMap<&'static str, usize> = HashMap::with_capacity(expected.len()); - for (field_index, (label, value)) in fields.iter().enumerate() { - // Resolve the row-side `String` label to its `&'static str` in - // `expected`. Holding the static reference avoids re-scanning the - // expected slice (and the `.expect()` re-borrow) on the duplicate - // path; non-membership surfaces as `RowUnknownField`. - let Some(static_label) = expected.iter().copied().find(|exp| *exp == label.as_str()) else { - return Err(MethodTemplateProjectionError::RowUnknownField { - list, - row_index, - record, - field: label.clone(), - field_index, - }); - }; - if let Some(prior_index) = first_seen_index.get(static_label) { - return Err(MethodTemplateProjectionError::RowDuplicateField { - list, - row_index, - record, - field: static_label, - first_field_index: *prior_index, - duplicate_field_index: field_index, - }); - } - lookup.insert(static_label, value); - first_seen_index.insert(static_label, field_index); - } - Ok(lookup) -} - -fn lookup_required<'a>( - list: &'static str, - row_index: usize, - lookup: &HashMap<&'a str, &'a FieldValue>, - field: &'static str, -) -> Result<&'a FieldValue, MethodTemplateProjectionError> { - lookup - .get(field) - .copied() - .ok_or(MethodTemplateProjectionError::RowMissingField { - list, - row_index, - field, - }) -} - -fn project_dag_method( - dag: &Dag, - list: &'static str, - row_index: usize, - value: &FieldValue, -) -> Result { - let FieldValue::Record(method_ref_fields) = value else { - return Err(MethodTemplateProjectionError::DagMethodNotMethodRefRecord { list, row_index }); - }; - // `MethodRef` is a single-field carrier (`{ decl: DeclarationRef }`) - // per `src/v3/std/methods.dag`. Apply the same closed-schema check - // as the outer row so duplicates (`{ decl: a, decl: b }`) and - // unknown extras (`{ decl: ..., extra: ... }`) surface as typed - // errors instead of being silently ignored. - const METHOD_REF_FIELDS: &[&str] = &["decl"]; - let lookup = closed_record_lookup( - list, - row_index, - "MethodRef", - method_ref_fields, - METHOD_REF_FIELDS, - )?; - let decl_value = lookup - .get("decl") - .copied() - .ok_or(MethodTemplateProjectionError::DagMethodNotMethodRefRecord { list, row_index })?; - let decl_id = match decl_value { - FieldValue::Reference(decl_id) => *decl_id, - _ => { - return Err(MethodTemplateProjectionError::MethodRefDeclNotReference { - list, - row_index, - }); - } - }; - // `src/v3/std/methods.dag` calls out the boundary contract: today's - // substrate grammar can't express `DeclarationRef`, - // so the projection enforces fail-closed at the boundary that - // `decl` references a `MethodDeclaration`-instantiating data binding. - // Pattern mirrors `method_registry_test.rs::method_registry_covers_*`. - let method_declaration_id = method_declaration_carrier_id(dag)?; - if let Err(reason) = - validate_method_declaration_data_binding(dag, decl_id, method_declaration_id) - { - return Err( - MethodTemplateProjectionError::MethodRefDeclNotMethodDeclaration { - list, - row_index, - decl_id, - reason, - }, - ); - } - Ok(decl_id) -} - -fn project_emit_template( - dag: &Dag, - list: &'static str, - row_index: usize, - value: &FieldValue, -) -> Result { - let FieldValue::Variant { - constructor, - payload, - } = value - else { - return Err(MethodTemplateProjectionError::EmitTemplateNotVariant { list, row_index }); - }; - let constructor_label = disj_variant_label(dag, "MethodEmitTemplate", *constructor); - match constructor_label { - Some("SingleTemplate") => { - // Payload shape (positional, per FieldValue::Variant): one - // `template: String` field. Lowering produces a single - // record-shaped payload entry whose only field is `template`. - let template = single_template_payload(list, row_index, payload)?; - Ok(MethodEmitTemplateProjection::Single { template }) - } - Some("HigherOrderTemplates") => { - let (inline_template, fn_ref_template) = - higher_order_payload(list, row_index, payload)?; - Ok(MethodEmitTemplateProjection::HigherOrder { - inline_template, - fn_ref_template, - }) - } - other => Err( - MethodTemplateProjectionError::EmitTemplateUnknownConstructor { - list, - row_index, - constructor: other.map(str::to_string), - }, - ), - } -} - -/// Recover a sum-variant label by walking the parent `Disj` declaration's -/// variant list. Variant constructor declarations themselves do not carry -/// `name`; identity flows through the parent's `variants[i].label`. Mirrors -/// the pattern used in `tests/integration/method_template_contract_test.rs` -/// (`method_emit_template_variant_label`). -fn disj_variant_label<'a>( - dag: &'a Dag, - parent_name: &str, - constructor: DeclarationId, -) -> Option<&'a str> { - let parent = dag.declaration_by_name(parent_name)?; - let TypeConnective::Disj { variants } = &parent.connective else { - return None; - }; - variants - .iter() - .find(|variant| variant.ty == constructor) - .map(|variant| variant.label.as_str()) -} - -fn single_template_payload( - list: &'static str, - row_index: usize, - payload: &[FieldValue], -) -> Result { - // `Variant.payload` is positional per the substrate carrier - // (`FieldValue::Variant` doc-comment). `SingleTemplate { template: String }` - // therefore lowers to a single-element payload carrying the string literal. - match payload { - [FieldValue::Literal(LiteralBits::String(s))] => Ok(s.clone()), - _ => Err(MethodTemplateProjectionError::EmitTemplateSinglePayloadShape { list, row_index }), - } -} - -fn higher_order_payload( - list: &'static str, - row_index: usize, - payload: &[FieldValue], -) -> Result<(String, String), MethodTemplateProjectionError> { - // `HigherOrderTemplates { inline_template: String, fn_ref_template: String }` - // lowers to a two-element positional payload carrying the two string - // literals in declaration order. - match payload { - [FieldValue::Literal(LiteralBits::String(inline)), FieldValue::Literal(LiteralBits::String(fn_ref))] => { - Ok((inline.clone(), fn_ref.clone())) - } - _ => Err( - MethodTemplateProjectionError::EmitTemplateHigherOrderPayloadShape { list, row_index }, - ), - } -} - -fn project_placeholder( - dag: &Dag, - list: &'static str, - row_index: usize, - value: &FieldValue, -) -> Result { - let FieldValue::Variant { - constructor, - payload, - } = value - else { - return Err( - MethodTemplateProjectionError::PlaceholderConventionNotVariant { list, row_index }, - ); - }; - let constructor_label = disj_variant_label(dag, "PlaceholderConvention", *constructor); - // `PlaceholderConvention` is a nullary sum (`IndexedArgs | NamedArg`) - // per `src/v3/std/emit_model.dag:449-451`. A non-empty payload would - // mean a malformed bootstrap row (e.g., `IndexedArgs(false)`) that the - // projection must surface as a typed error rather than silently - // discarding. Mirrors the substrate-side canonical-row check in - // `src/v3/grounding_tests/src/stratum_a.rs`. - let (label, projection) = match constructor_label { - Some("IndexedArgs") => ("IndexedArgs", PlaceholderConventionProjection::IndexedArgs), - Some("NamedArg") => ("NamedArg", PlaceholderConventionProjection::NamedArg), - other => { - return Err( - MethodTemplateProjectionError::PlaceholderConventionUnknown { - list, - row_index, - constructor: other.map(str::to_string), - }, - ); - } - }; - if !payload.is_empty() { - return Err( - MethodTemplateProjectionError::PlaceholderConventionPayloadNotEmpty { - list, - row_index, - constructor: label, - payload_len: payload.len(), - }, - ); - } - Ok(projection) -} - -#[cfg(test)] -mod tests { - //! Unit tests for projection-internal failure modes that require - //! crate-private `Dag` mutation to construct (e.g., injecting a - //! malformed value body that the on-disk row authorities cannot - //! produce). Behavior tests over the green bootstrap Dag live in - //! `tests/integration/pb_method_template_projection_test.rs`. - - use super::*; - use crate::generated_full_bootstrap_dag; - - /// Inject a non-empty payload onto the `placeholder_convention` variant - /// of the row at `row_index` of the named per-target list. Mutates only - /// the in-memory `Dag`; the row authority sources on disk are - /// untouched. - fn inject_extra_placeholder_payload(dag: &mut Dag, list_name: &str, row_index: usize) { - let decl_id = dag - .declaration_by_name(list_name) - .unwrap_or_else(|| panic!("`{list_name}` missing from bootstrap")) - .id; - let decl = dag.declaration_mut(decl_id); - let body = decl - .value_body - .as_mut() - .unwrap_or_else(|| panic!("`{list_name}` has no value body")); - let ValueBody::List(rows) = body else { - panic!("`{list_name}` value body is not a List"); - }; - let row = rows - .get_mut(row_index) - .unwrap_or_else(|| panic!("row {row_index} missing from `{list_name}`")); - let FieldValue::Record(fields) = row else { - panic!("row {row_index} is not a Record"); - }; - let placeholder_value = fields - .iter_mut() - .find(|(label, _)| label == "placeholder_convention") - .map(|(_, value)| value) - .expect("placeholder_convention field"); - let FieldValue::Variant { payload, .. } = placeholder_value else { - panic!("placeholder_convention must be a Variant"); - }; - payload.push(FieldValue::Literal(LiteralBits::Bool(false))); - } - - /// Duplicate the row at `row_index` of the named per-target list by - /// pushing a clone onto the end of the list. The two rows share a - /// `dag_method`, breaking per-target uniqueness — the substrate-side - /// claim `method_template_contract_per_target_dag_method_unique`. - /// Mutates only the in-memory `Dag`. - fn duplicate_row(dag: &mut Dag, list_name: &str, row_index: usize) { - let decl_id = dag - .declaration_by_name(list_name) - .unwrap_or_else(|| panic!("`{list_name}` missing from bootstrap")) - .id; - let decl = dag.declaration_mut(decl_id); - let body = decl - .value_body - .as_mut() - .unwrap_or_else(|| panic!("`{list_name}` has no value body")); - let ValueBody::List(rows) = body else { - panic!("`{list_name}` value body is not a List"); - }; - let clone = rows - .get(row_index) - .unwrap_or_else(|| panic!("row {row_index} missing from `{list_name}`")) - .clone(); - rows.push(clone); - } - - fn replace_row_with_reference( - dag: &mut Dag, - list_name: &str, - row_index: usize, - replacement: DeclarationId, - ) { - let decl_id = dag.declaration_by_name(list_name).expect("list").id; - let decl = dag.declaration_mut(decl_id); - let body = decl.value_body.as_mut().expect("value body"); - let ValueBody::List(rows) = body else { - panic!("not a list"); - }; - let row = rows.get_mut(row_index).expect("row"); - *row = FieldValue::Reference(replacement); - } - - /// Mutate the row at `row_index` of `list_name` by adding a second - /// field with `field_label` (cloned from the first occurrence). Used - /// by the closed-schema duplicate-field tests below. - fn duplicate_field_on_row(dag: &mut Dag, list_name: &str, row_index: usize, field_label: &str) { - let decl_id = dag.declaration_by_name(list_name).expect("list").id; - let decl = dag.declaration_mut(decl_id); - let body = decl.value_body.as_mut().expect("value body"); - let ValueBody::List(rows) = body else { - panic!("not a list"); - }; - let row = rows.get_mut(row_index).expect("row"); - let FieldValue::Record(fields) = row else { - panic!("row not record"); - }; - let (orig_label, orig_value) = fields - .iter() - .find(|(label, _)| label == field_label) - .map(|(label, value)| (label.clone(), value.clone())) - .expect("field to clone"); - fields.push((orig_label, orig_value)); - } - - /// Replace the row at `row_index` of `list_name` so that, after the - /// existing fields, an unknown-labelled field is appended (used by - /// the closed-schema unknown-field tests below). - fn append_unknown_field_on_row( - dag: &mut Dag, - list_name: &str, - row_index: usize, - unknown_label: &str, - ) { - let decl_id = dag.declaration_by_name(list_name).expect("list").id; - let decl = dag.declaration_mut(decl_id); - let body = decl.value_body.as_mut().expect("value body"); - let ValueBody::List(rows) = body else { - panic!("not a list"); - }; - let row = rows.get_mut(row_index).expect("row"); - let FieldValue::Record(fields) = row else { - panic!("row not record"); - }; - fields.push(( - unknown_label.to_string(), - FieldValue::Literal(LiteralBits::Bool(false)), - )); - } - - #[test] - fn duplicate_field_on_row_surfaces_typed_error() { - let mut dag = generated_full_bootstrap_dag(); - duplicate_field_on_row( - &mut dag, - "rust_method_template_contracts", - 0, - "runtime_template", - ); - let result = method_template_contract_rows(&dag, MethodTemplateTarget::Rust); - match result { - Err(MethodTemplateProjectionError::RowDuplicateField { - record, - field, - first_field_index, - duplicate_field_index, - .. - }) => { - assert_eq!(record, "MethodTemplateContract"); - assert_eq!(field, "runtime_template"); - assert_ne!(first_field_index, duplicate_field_index); - } - other => panic!("expected RowDuplicateField, got {other:?}"), - } - } - - #[test] - fn unknown_field_on_row_surfaces_typed_error() { - let mut dag = generated_full_bootstrap_dag(); - append_unknown_field_on_row( - &mut dag, - "rust_method_template_contracts", - 0, - "renamed_field", - ); - let result = method_template_contract_rows(&dag, MethodTemplateTarget::Rust); - match result { - Err(MethodTemplateProjectionError::RowUnknownField { record, field, .. }) => { - assert_eq!(record, "MethodTemplateContract"); - assert_eq!(field, "renamed_field"); - } - other => panic!("expected RowUnknownField, got {other:?}"), - } - } - - #[test] - fn reference_row_requires_method_template_contract_instance() { - let mut dag = generated_full_bootstrap_dag(); - let non_contract_structural = dag - .declaration_by_name("count_method") - .expect("count_method MethodDeclaration") - .id; - replace_row_with_reference( - &mut dag, - "rust_method_template_contracts", - 0, - non_contract_structural, - ); - - let result = method_template_contract_rows(&dag, MethodTemplateTarget::Rust); - match result { - Err(MethodTemplateProjectionError::RowReferenceNotMethodTemplateContract { - list, - row_index, - decl_id, - }) => { - assert_eq!(list, "rust_method_template_contracts"); - assert_eq!(row_index, 0); - assert_eq!(decl_id, non_contract_structural); - } - other => panic!( - "expected RowReferenceNotMethodTemplateContract for structural lookalike row, got {other:?}" - ), - } - } - - /// Inject a duplicate or unknown field into the inner `MethodRef` - /// record at `dag_method` for the row at `row_index`. - fn mutate_method_ref( - dag: &mut Dag, - list_name: &str, - row_index: usize, - kind: MethodRefMutation<'_>, - ) { - let decl_id = dag.declaration_by_name(list_name).expect("list").id; - let decl = dag.declaration_mut(decl_id); - let body = decl.value_body.as_mut().expect("value body"); - let ValueBody::List(rows) = body else { - panic!("not a list"); - }; - let row = rows.get_mut(row_index).expect("row"); - let FieldValue::Record(fields) = row else { - panic!("row not record"); - }; - let (_, dag_method_value) = fields - .iter_mut() - .find(|(label, _)| label == "dag_method") - .expect("dag_method field"); - let FieldValue::Record(method_ref_fields) = dag_method_value else { - panic!("dag_method not record"); - }; - match kind { - MethodRefMutation::Duplicate => { - let (label, value) = method_ref_fields - .iter() - .find(|(label, _)| label == "decl") - .map(|(label, value)| (label.clone(), value.clone())) - .expect("decl field"); - method_ref_fields.push((label, value)); - } - MethodRefMutation::AppendUnknown(label) => { - method_ref_fields.push(( - label.to_string(), - FieldValue::Literal(LiteralBits::Bool(false)), - )); - } - } - } - - /// 🟢 TERMINAL at the test-local `MethodRef` mutation scope. Names the - /// two ways an in-memory `Dag` row can break the closed `MethodRef` - /// schema: duplicate the existing `decl` field, or append a field with - /// an unknown label. Both modes drive the closed-schema check in - /// `closed_record_lookup`. Test-only; not part of the projection's - /// public surface. - enum MethodRefMutation<'a> { - Duplicate, - AppendUnknown(&'a str), - } - - #[test] - fn method_ref_decl_alias_without_value_body_surfaces_typed_error() { - // `src/v3/std/methods.dag` requires a *data binding* - // (`data _method: MethodDeclaration = { name: "..." }`), - // not just any declaration whose connective resolves to - // `MethodDeclaration`. A type alias like - // `type Foo = MethodDeclaration` lowers to an `Instantiation` - // connective with **no `value_body`**; without the value-body - // guard, that shape would pass the connective check alone. - // - // Simulate the alias-shape by stripping `value_body` from a real - // `MethodDeclaration` data binding (`count_method`) and asserting - // the typed error fires for both the per-row check - // (`MethodRefDeclNotMethodDeclaration`) and the lookup helper - // (`LookupKeyNotMethodDeclaration`). - let mut dag = generated_full_bootstrap_dag(); - let count_method_id = dag - .declaration_by_name("count_method") - .expect("count_method MethodDeclaration in bootstrap") - .id; - dag.declaration_mut(count_method_id).value_body = None; - - // Per-row path: row 0 of `rust_method_template_contracts` is the - // `count_method` row, so projecting it should now surface - // `MethodRefDeclNotMethodDeclaration` with `ValueBodyMissing` - // reason instead of returning a row. - let row_result = method_template_contract_rows(&dag, MethodTemplateTarget::Rust); - match row_result { - Err(MethodTemplateProjectionError::MethodRefDeclNotMethodDeclaration { - decl_id, - reason, - .. - }) => { - assert_eq!(decl_id, count_method_id); - assert_eq!(reason, MethodDeclarationBindingViolation::ValueBodyMissing); - } - other => panic!( - "expected MethodRefDeclNotMethodDeclaration for alias-shaped target, got {other:?}" - ), - } - - // Lookup-helper path: same key, same shape; the helper validates - // at entry and surfaces `LookupKeyNotMethodDeclaration` with the - // same reason. - let helper_result = - method_template_contract_row(&dag, MethodTemplateTarget::Rust, count_method_id); - match helper_result { - Err(MethodTemplateProjectionError::LookupKeyNotMethodDeclaration { - decl_id, - reason, - }) => { - assert_eq!(decl_id, count_method_id); - assert_eq!(reason, MethodDeclarationBindingViolation::ValueBodyMissing); - } - other => { - panic!("expected LookupKeyNotMethodDeclaration for alias-shaped key, got {other:?}") - } - } - } - - #[test] - fn method_ref_decl_not_method_declaration_surfaces_typed_error() { - // `src/v3/std/methods.dag` enforces fail-closed at the projection - // boundary that `MethodRef.decl` instantiates `MethodDeclaration`, - // because today's substrate grammar can't express - // `DeclarationRef`. Mutate the first row's - // `MethodRef.decl` to point at a declaration that is not a - // `MethodDeclaration` instance (`MethodTemplateContract` itself - // is a type declaration, not a `MethodDeclaration` data binding) - // and assert the typed error. - let mut dag = generated_full_bootstrap_dag(); - let non_method_id = dag - .declaration_by_name("MethodTemplateContract") - .expect("MethodTemplateContract type") - .id; - - let list_decl_id = dag - .declaration_by_name("rust_method_template_contracts") - .expect("list") - .id; - let decl = dag.declaration_mut(list_decl_id); - let body = decl.value_body.as_mut().expect("value body"); - let ValueBody::List(rows) = body else { - panic!("not a list"); - }; - let row = rows.get_mut(0).expect("row 0"); - let FieldValue::Record(fields) = row else { - panic!("row not record"); - }; - let dag_method_value = fields - .iter_mut() - .find(|(label, _)| label == "dag_method") - .map(|(_, value)| value) - .expect("dag_method field"); - let FieldValue::Record(method_ref_fields) = dag_method_value else { - panic!("dag_method not record"); - }; - let decl_field_value = method_ref_fields - .iter_mut() - .find(|(label, _)| label == "decl") - .map(|(_, value)| value) - .expect("decl field"); - *decl_field_value = FieldValue::Reference(non_method_id); - - let result = method_template_contract_rows(&dag, MethodTemplateTarget::Rust); - match result { - Err(MethodTemplateProjectionError::MethodRefDeclNotMethodDeclaration { - list, - row_index, - decl_id, - reason, - }) => { - assert_eq!(list, "rust_method_template_contracts"); - assert_eq!(row_index, 0); - assert_eq!(decl_id, non_method_id); - // `MethodTemplateContract` is a type declaration (Conj), - // not an Instantiation — connective sub-check fails first. - assert_eq!( - reason, - MethodDeclarationBindingViolation::ConnectiveNotInstantiation - ); - } - other => panic!( - "expected MethodRefDeclNotMethodDeclaration for non-MethodDeclaration target, got {other:?}" - ), - } - } - - #[test] - fn method_ref_decl_extra_field_surfaces_closed_schema_violation() { - // A MethodDeclaration data binding with an extra field (beyond the - // closed `[name]` schema) must surface - // `StructuralFieldsNotClosedNameOnly`. Mirrors Stratum A's - // `enforce_closed_record_schema` gate. - let mut dag = generated_full_bootstrap_dag(); - let count_method_id = dag - .declaration_by_name("count_method") - .expect("count_method") - .id; - let decl = dag.declaration_mut(count_method_id); - let body = decl.value_body.as_mut().expect("value body"); - let ValueBody::Structural { fields } = body else { - panic!("expected Structural body"); - }; - fields.push(( - "alias".to_string(), - FieldValue::Literal(LiteralBits::String("count_alias".to_string())), - )); - - let result = method_template_contract_rows(&dag, MethodTemplateTarget::Rust); - match result { - Err(MethodTemplateProjectionError::MethodRefDeclNotMethodDeclaration { - reason: - MethodDeclarationBindingViolation::StructuralFieldsNotClosedNameOnly { - observed_labels, - }, - .. - }) => { - assert!(observed_labels.contains(&"name".to_string())); - assert!(observed_labels.contains(&"alias".to_string())); - } - other => { - panic!("expected StructuralFieldsNotClosedNameOnly for extra field, got {other:?}") - } - } - } - - #[test] - fn method_ref_decl_name_not_string_literal_surfaces_typed_error() { - // A MethodDeclaration data binding whose `name` value is not a - // string literal must surface - // `MethodDeclarationBindingViolation::NameNotStringLiteral`. - let mut dag = generated_full_bootstrap_dag(); - let count_method_id = dag - .declaration_by_name("count_method") - .expect("count_method") - .id; - let decl = dag.declaration_mut(count_method_id); - let body = decl.value_body.as_mut().expect("value body"); - let ValueBody::Structural { fields } = body else { - panic!("expected Structural body"); - }; - let (_, name_value) = fields - .iter_mut() - .find(|(label, _)| label == "name") - .expect("name field"); - // Replace with a Bool literal — wrong shape for the contract. - *name_value = FieldValue::Literal(LiteralBits::Bool(true)); - - let result = method_template_contract_rows(&dag, MethodTemplateTarget::Rust); - match result { - Err(MethodTemplateProjectionError::MethodRefDeclNotMethodDeclaration { - reason: MethodDeclarationBindingViolation::NameNotStringLiteral, - .. - }) => {} - other => panic!("expected NameNotStringLiteral for non-string name, got {other:?}"), - } - } - - #[test] - fn duplicate_field_on_method_ref_surfaces_typed_error() { - let mut dag = generated_full_bootstrap_dag(); - mutate_method_ref( - &mut dag, - "rust_method_template_contracts", - 0, - MethodRefMutation::Duplicate, - ); - let result = method_template_contract_rows(&dag, MethodTemplateTarget::Rust); - match result { - Err(MethodTemplateProjectionError::RowDuplicateField { record, field, .. }) => { - assert_eq!(record, "MethodRef"); - assert_eq!(field, "decl"); - } - other => panic!("expected RowDuplicateField for MethodRef, got {other:?}"), - } - } - - #[test] - fn unknown_field_on_method_ref_surfaces_typed_error() { - let mut dag = generated_full_bootstrap_dag(); - mutate_method_ref( - &mut dag, - "rust_method_template_contracts", - 0, - MethodRefMutation::AppendUnknown("nickname"), - ); - let result = method_template_contract_rows(&dag, MethodTemplateTarget::Rust); - match result { - Err(MethodTemplateProjectionError::RowUnknownField { record, field, .. }) => { - assert_eq!(record, "MethodRef"); - assert_eq!(field, "nickname"); - } - other => panic!("expected RowUnknownField for MethodRef, got {other:?}"), - } - } - - #[test] - fn duplicate_row_surfaces_typed_error_at_canonical_row_list_api() { - // Per-target uniqueness by `dag_method` is the substrate-side claim - // `method_template_contract_per_target_dag_method_unique`. The - // canonical row-list API enforces this at the public boundary so - // every consumer (direct row-list consumer or `(target, dag_method)` - // lookup helper) observes the same fail-closed behavior. Gap 5 / - // leaf-emit consumers building their own keyed tables from the - // projection cannot silently observe two authorities for the same - // `(target, MethodRef)` pair. - let mut dag = generated_full_bootstrap_dag(); - duplicate_row(&mut dag, "rust_method_template_contracts", 0); - - // Canonical row-list API surfaces the duplicate. - let list_result = method_template_contract_rows(&dag, MethodTemplateTarget::Rust); - match list_result { - Err(MethodTemplateProjectionError::DuplicateMethodTemplateRow { - list, - first_row_index, - duplicate_row_index, - }) => { - assert_eq!(list, "rust_method_template_contracts"); - assert_ne!(first_row_index, duplicate_row_index); - } - other => panic!( - "expected DuplicateMethodTemplateRow at row-list API for duplicate (target, dag_method), got {other:?}" - ), - } - - // Lookup helper inherits the fail-closed shape via the row-list - // API; no separate uniqueness path on the helper. - let count_method_id = dag - .declaration_by_name("count_method") - .expect("count_method MethodDeclaration in bootstrap Dag") - .id; - let helper_result = - method_template_contract_row(&dag, MethodTemplateTarget::Rust, count_method_id); - assert!( - matches!( - helper_result, - Err(MethodTemplateProjectionError::DuplicateMethodTemplateRow { .. }) - ), - "lookup helper must inherit duplicate-row fail-closed via row-list API; got {helper_result:?}" - ); - } - - #[test] - fn malformed_placeholder_payload_surfaces_typed_error() { - // PlaceholderConvention is a nullary sum (`IndexedArgs | NamedArg`). - // A malformed bootstrap row carrying a non-empty placeholder payload - // must surface as `PlaceholderConventionPayloadNotEmpty`, not - // silently project to `IndexedArgs` / `NamedArg` with the payload - // discarded. Mirrors the substrate-side canonical-row check - // `placeholder_convention_canonical_rejects_payload_for_nullary_indexed_args` - // in `src/v3/grounding_tests/src/stratum_a.rs`. - let mut dag = generated_full_bootstrap_dag(); - inject_extra_placeholder_payload(&mut dag, "rust_method_template_contracts", 0); - - let result = method_template_contract_rows(&dag, MethodTemplateTarget::Rust); - match result { - Err(MethodTemplateProjectionError::PlaceholderConventionPayloadNotEmpty { - list, - row_index, - payload_len, - .. - }) => { - assert_eq!(list, "rust_method_template_contracts"); - assert_eq!(row_index, 0); - assert!(payload_len >= 1, "expected non-empty payload to be reported"); - } - other => panic!( - "expected PlaceholderConventionPayloadNotEmpty for malformed placeholder, got {other:?}" - ), - } - } -} diff --git a/src/v3/compiler/src/process_exit.rs b/src/v3/compiler/src/process_exit.rs index 1b4d013b55d..d4b83262d3e 100644 --- a/src/v3/compiler/src/process_exit.rs +++ b/src/v3/compiler/src/process_exit.rs @@ -4,10 +4,9 @@ //! `EXPECTED_HAND_AUTHORED_NON_TEST` in `sg0_census_test.rs` (not //! `GENERATED_FILES` / `build.rs` output). //! -//! Used by PB-1 emitted bin-shim `main.rs` shells (the `emit_rust_bin_shim` module -//! backed by `emit_rust_bin_shim_generated.rs` in `GENERATED_FILES`) so generated -//! sources can `match` on the same structural shape the `.dag` substrate declares, -//! without inventing a parallel exit carrier. +//! Used by PB-1 emitted bin-shim `main.rs` shells (`emit_rust_bin_shim`) so +//! generated sources can `match` on the same structural shape the `.dag` +//! substrate declares, without inventing a parallel exit carrier. // Practice 4 (coproduct checkpoint, `docs/modeling-discipline.md` §P1): 🟢 GREEN — // terminal host mirror of `dsl/std/process.dag` `ProcessExit`; no extra variants diff --git a/src/v3/compiler/tests/integration/sg0_census_test.rs b/src/v3/compiler/tests/integration/sg0_census_test.rs index 00424983745..afc018b00bf 100644 --- a/src/v3/compiler/tests/integration/sg0_census_test.rs +++ b/src/v3/compiler/tests/integration/sg0_census_test.rs @@ -217,7 +217,7 @@ fn emit_production_code_has_no_declaration_by_name_calls() { // `dag.rs` itself replaces these submodules simultaneously. // // T-Substrate cardinality subset for int literals (2026-04-25): the -// range-comparison shim in `int_literal_ranges_generated.rs` (build.rs-spliced +// range-comparison shim in `int_literal_ranges.rs` is host-side // reconciliation glue over already-declared String-decimal range facts // while `rust_pilot_primitives.value_body` remains an unparsed top-level // list. It intentionally compares only source literals that already fit @@ -274,6 +274,8 @@ const EXPECTED_HAND_AUTHORED_NON_TEST: &[&str] = &[ // evaluator for `tests/dag/cementing_dispatch.dag` (P5 consumer receipt; dissolves when // predicate substrate owns the walk without host FS coupling). "src/v3/compiler/src/cementing_dispatch.rs", + "src/v3/compiler/src/complexity_lattice.rs", + "src/v3/compiler/src/cost_basis_declaration.rs", "src/v3/compiler/src/dag.rs", "src/v3/compiler/src/dag/builder.rs", // Closed Cardinality payload + idempotent target shim (API closure). @@ -283,12 +285,17 @@ const EXPECTED_HAND_AUTHORED_NON_TEST: &[&str] = &[ "src/v3/compiler/src/diagnostics.rs", "src/v3/compiler/src/dimension.rs", "src/v3/compiler/src/emit.rs", + // CollectionOps `*_contract` → `MethodTemplateContract` identity gate (PR #1577 / #1602). + "src/v3/compiler/src/emit/collection_ops_method_contract.rs", "src/v3/compiler/src/emit/python_target.rs", "src/v3/compiler/src/emit/rust_target.rs", "src/v3/compiler/src/emit_rust.rs", + "src/v3/compiler/src/emit_rust_bin_shim.rs", // 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", "src/v3/compiler/src/infer.rs", + "src/v3/compiler/src/int_literal_ranges.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` @@ -304,6 +311,11 @@ const EXPECTED_HAND_AUTHORED_NON_TEST: &[&str] = &[ // fix moved it out of `emit.rs`. Dissolves when the equivalent Shape B // `.dag` program owns the OpenAPI artifact projection end-to-end. "src/v3/compiler/src/omni_shape_b_openapi.rs", + // R3 row 85 / PB #1560 Gap 4: target-keyed projection of the + // `MethodTemplateContract` rows from the full bootstrap `Dag` for + // PB-zero / v2-retirement consumers (decision in + // `docs/decisions/r3-row85-method-template-read-surface.md`). + "src/v3/compiler/src/pb_method_template_projection.rs", "src/v3/compiler/src/pipeline_authority.rs", "src/v3/compiler/src/post_emit_verifier.rs", // PB-1 Item 5: host mirror of `dsl/std/process.dag` `ProcessExit` for emitted bin shims. @@ -341,13 +353,13 @@ const EXPECTED_HAND_AUTHORED_NON_TEST: &[&str] = &[ // "ported-but-still-listed" or "pending-port" set); the ratchet's whole point is that one // monotonically-shrinking authority tracks the Rust→`.dag` migration. const EXPECTED_HAND_AUTHORED_TEST: &[&str] = &[ - "src/v3/compiler/tests/boundary/l5_cross_target_consistency.rs", "src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs", "src/v3/compiler/tests/boundary/m1_3_emit_rust_test.rs", "src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs", "src/v3/compiler/tests/boundary/m1_5_emit_omni_demo_test.rs", "src/v3/compiler/tests/boundary/m2_emit_multi_field_struct_variant_test.rs", "src/v3/compiler/tests/determinism_test.rs", + "src/v3/compiler/tests/boundary/l5_cross_target_consistency.rs", "src/v3/compiler/tests/integration.rs", "src/v3/compiler/tests/integration/anthropic_messages_callable_test.rs", // R3 gate #68 (`anthropic_wire_demonstration`): hermetic typed request/response @@ -1437,7 +1449,7 @@ fn sg0_stage0_hand_maintained_src_covers_emit_subtree_companions() { "hand_maintained_src should exclude emit/rust_target.rs from recursive freshness drift" ); assert!( - list.contains("\"process_exit.rs\""), - "hand_maintained_src should exclude process_exit.rs (PB-1 host mirror) from recursive freshness drift" + list.contains("\"emit_rust_bin_shim.rs\""), + "hand_maintained_src should exclude emit_rust_bin_shim.rs (PB-1 shell helper) from recursive freshness drift" ); } From 97b9d3e9702cdc133ba95f356e93e9637d6f665d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 14 May 2026 03:48:13 +0000 Subject: [PATCH 3/9] =?UTF-8?q?Revert=20"PB-0=20cycle-2=20NON=5FTEST=20ret?= =?UTF-8?q?irement=20(msg=5F84abadad=20Track=20A):=20retire=206=20paths=20?= =?UTF-8?q?=E2=80=94=20integration=5Frs=5Fwiring=5Fscan.rs;=20cementing=5F?= =?UTF-8?q?dispatch.rs;=20r3=5Fgate=5F87=5Fcementing=5Fregen=5Frunner=5Fsu?= =?UTF-8?q?ites.rs;=20gunbc=5Fci.rs;=20lens=5Fdeclaration=5Fapply.rs;=20le?= =?UTF-8?q?ns=5Ft=5Flas=5Fcarrier.rs=20=E2=80=94=20brief=20docs/briefs/r3-?= =?UTF-8?q?pb0-non-test-retirement-worker-cycle2.md=20+=20(#3046)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 1e3651a79ebc38405f489bf69418084d82d6c768. --- INVARIANTS.md | 7 +- dsl/gunbc/compiler.dag | 3 +- src/v3/compiler/src/cementing_dispatch.rs | 1068 +---------------- src/v3/compiler/src/gunbc_ci.rs | 479 ++++++++ .../src/integration_rs_wiring_scan.rs | 362 ++++++ .../lens_declaration_apply.rs} | 84 +- src/v3/compiler/src/lens_t_las_carrier.rs | 53 + src/v3/compiler/src/lib.rs | 18 +- ...3_gate_87_cementing_regen_runner_suites.rs | 128 ++ src/v3/compiler/src/test_runner.rs | 26 +- .../tests/integration/sg0_census_test.rs | 19 +- 11 files changed, 1063 insertions(+), 1184 deletions(-) create mode 100644 src/v3/compiler/src/gunbc_ci.rs create mode 100644 src/v3/compiler/src/integration_rs_wiring_scan.rs rename src/v3/compiler/{lens_declaration_apply_body.txt => src/lens_declaration_apply.rs} (97%) create mode 100644 src/v3/compiler/src/lens_t_las_carrier.rs create mode 100644 src/v3/compiler/src/r3_gate_87_cementing_regen_runner_suites.rs diff --git a/INVARIANTS.md b/INVARIANTS.md index c808f3d3f55..5a34d29e258 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 `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/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/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,7 +351,10 @@ Per **Dispatch-Discipline Mechanisms (b)**, each path in `EXPECTED_HAND_AUTHORED | Path | Receipt | |------|---------| -| `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`. | +| `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). | --- diff --git a/dsl/gunbc/compiler.dag b/dsl/gunbc/compiler.dag index 43410c76848..b0f66d98b3b 100644 --- a/dsl/gunbc/compiler.dag +++ b/dsl/gunbc/compiler.dag @@ -88,8 +88,7 @@ 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_body.txt` at crate root, `include!`d from `lib.rs`). Must stay in - // `hand_maintained_src` so stage0 recursive + // (`lens_declaration_apply.rs` `include!`). 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 c2d66994603..a73da408865 100644 --- a/src/v3/compiler/src/cementing_dispatch.rs +++ b/src/v3/compiler/src/cementing_dispatch.rs @@ -20,1076 +20,12 @@ //! `.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. -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 — **Layer 1** affected-set dispatch substrate. - //! - //! **Authority** (Phase B): `docs/briefs/r3-wave1-s6-slice7-affected-set-impl-worker.md`; - //! fail-closed behavior aligned with - //! `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §3 (superset - //! table) applied to **gate-id** touch seeds (see §4 for how end-state runners join - //! `NodeRef` / dimension receipts to obligations — **not implemented in this module**); - //! carrier shape: `dsl/gunbc/ci.dag` (`CIWorkflowDag`, `CIGateEdge`: `from` is a prerequisite of `to`). - //! - //! **Layer 1 vs canvas §7 / ROADMAP gate #103:** program row `ci_uses_affected_set_selection` - //! eventually requires the Slice-5 **BinaryShim** runner to wire PR #2713 affected-set - //! receipts through `CIWorkflowDag` / `TestClaim` metadata (canvas §§1.1–1.3, §4, §7). - //! This crate surface **does not** close that hand-off: there is no typed PR #2713 - //! receipt type here and no runner entry mapping `Set` into these APIs. - //! What lands here is the **pure graph planner** — [`select_affected_gates`] — plus a - //! thin [`CiBinaryShimAffectedSetReceipt`] adapter for **gate-id seeds only** (tests + - //! future runner glue once upstream produces `CIGate.id` touches). - //! - //! This module is intentionally **pure**: every decision is a function of - //! [`CiWorkflowDagInput`] + [`CiWorkflowDiff`] only. Git diffs, path-regex, env, - //! and PR #2713 lens output must be **mapped upstream** 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 runner-side dispatch 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 lens receipts stay - // outside this module (Slice 7 brief + canvas §§1.1–4 end-state runner; Layer 1 only here). - /// 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, - } - } - - /// **Layer 1** gate-id touch seed for [`select_affected_gates_for_binary_shim`]. - /// - /// Canvas `design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §1.1 - /// steps 3–5 and §4 require the **runner** to consume PR #2713 structured output, join - /// it with `CIWorkflowDag` / `TestClaim` metadata, and **then** emit executable work. - /// This struct is **not** that receipt: it only carries `CIGate.id` strings (and a - /// narrowing flag) **after** any `NodeRef` → gate-id mapping has happened elsewhere. - /// - /// When `narrowing_available` is `false`, selection fails closed to the full gate - /// roster ([`CiWorkflowDiff::TouchAll`]; canvas §3: unknown dimension / missing - /// receipt / unbuildable DAG pair). - #[derive(Debug, Clone, PartialEq, Eq, Default)] - pub struct CiBinaryShimAffectedSetReceipt { - /// `false` forces [`CiWorkflowDiff::TouchAll`] regardless of `proven_direct_gate_touches`. - pub narrowing_available: bool, - /// Gate ids proven directly touched **after** upstream mapping (seed before symmetric expansion). - pub proven_direct_gate_touches: BTreeSet, - } - - /// Layer 1 entry: map a **gate-id-only** [`CiBinaryShimAffectedSetReceipt`] into [`select_affected_gates`]. - pub fn select_affected_gates_for_binary_shim( - dag: &CiWorkflowDagInput, - receipt: &CiBinaryShimAffectedSetReceipt, - ) -> Result, CiAffectedGatesError> { - let diff = if receipt.narrowing_available { - CiWorkflowDiff::TouchedGates(receipt.proven_direct_gate_touches.clone()) - } else { - CiWorkflowDiff::TouchAll - }; - select_affected_gates(dag, &diff) - } - - 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)); - } - - #[test] - fn binary_shim_receipt_unknown_narrowing_matches_touch_all() { - let dag = demo_ci_dag(); - let receipt = CiBinaryShimAffectedSetReceipt { - narrowing_available: false, - proven_direct_gate_touches: BTreeSet::from(["lint".into()]), - }; - let got = select_affected_gates_for_binary_shim(&dag, &receipt).expect("valid dag"); - let expect = select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("valid dag"); - assert_eq!(got, expect); - } - - #[test] - fn binary_shim_receipt_narrow_lint_matches_touched_gates_lint() { - let dag = demo_ci_dag(); - let receipt = CiBinaryShimAffectedSetReceipt { - narrowing_available: true, - proven_direct_gate_touches: BTreeSet::from(["lint".into()]), - }; - let got = select_affected_gates_for_binary_shim(&dag, &receipt).expect("valid dag"); - let expect = select_affected_gates( - &dag, - &CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])), - ) - .expect("valid dag"); - assert_eq!(got, expect); - } - - #[test] - fn binary_shim_receipt_empty_narrow_seed_yields_empty_plan() { - let dag = demo_ci_dag(); - let receipt = CiBinaryShimAffectedSetReceipt { - narrowing_available: true, - proven_direct_gate_touches: BTreeSet::new(), - }; - assert_eq!( - select_affected_gates_for_binary_shim(&dag, &receipt), - Ok(Vec::new()) - ); - } - } -} - -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}; +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; fn record_field<'a>(fields: &'a [(String, FieldValue)], label: &str) -> Option<&'a FieldValue> { fields diff --git a/src/v3/compiler/src/gunbc_ci.rs b/src/v3/compiler/src/gunbc_ci.rs new file mode 100644 index 00000000000..723587bc5d8 --- /dev/null +++ b/src/v3/compiler/src/gunbc_ci.rs @@ -0,0 +1,479 @@ +//! `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 new file mode 100644 index 00000000000..301e3fc9d5d --- /dev/null +++ b/src/v3/compiler/src/integration_rs_wiring_scan.rs @@ -0,0 +1,362 @@ +//! `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/lens_declaration_apply_body.txt b/src/v3/compiler/src/lens_declaration_apply.rs similarity index 97% rename from src/v3/compiler/lens_declaration_apply_body.txt rename to src/v3/compiler/src/lens_declaration_apply.rs index 334ec1545f9..2b5f44d6057 100644 --- a/src/v3/compiler/lens_declaration_apply_body.txt +++ b/src/v3/compiler/src/lens_declaration_apply.rs @@ -1,15 +1,11 @@ -// 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. +//! 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; @@ -1152,7 +1148,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"); @@ -2993,71 +2989,11 @@ 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. - #[allow(clippy::large_enum_variant)] - #[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!("src/lens_testgen_body.txt"); + 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 new file mode 100644 index 00000000000..471841ee57a --- /dev/null +++ b/src/v3/compiler/src/lens_t_las_carrier.rs @@ -0,0 +1,53 @@ +//! 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 e7d83de6400..29dcfa7670d 100644 --- a/src/v3/compiler/src/lib.rs +++ b/src/v3/compiler/src/lib.rs @@ -16,9 +16,6 @@ // 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; @@ -33,7 +30,10 @@ 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; @@ -3771,15 +3771,8 @@ pub mod evaluator { } mod int_literal_ranges; /// T-LensAPI D1: bounded lens interpreter over substrate-shaped [`FieldValue`] -/// (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"), - "/lens_declaration_apply_body.txt" - )); -} -pub use lens_declaration_apply::lens_t_las_carrier; +/// (see module docs in `lens_declaration_apply.rs`). +pub mod lens_declaration_apply; pub use lens_declaration_apply::lens_testgen; /// Effect-enumeration lens. Authority lives in @@ -4052,6 +4045,7 @@ 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/r3_gate_87_cementing_regen_runner_suites.rs b/src/v3/compiler/src/r3_gate_87_cementing_regen_runner_suites.rs new file mode 100644 index 00000000000..88a856e107f --- /dev/null +++ b/src/v3/compiler/src/r3_gate_87_cementing_regen_runner_suites.rs @@ -0,0 +1,128 @@ +//! 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 87b43b56db5..e3dc6caf83d 100644 --- a/src/v3/compiler/src/test_runner.rs +++ b/src/v3/compiler/src/test_runner.rs @@ -4708,24 +4708,10 @@ impl<'a> TestRunner<'a> { )); } }; - let mut entries = match sg0_census_list_entries(&list_constant_name) { + let 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 @@ -6412,15 +6398,13 @@ 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` → `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. +/// `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. 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/src/lens_declaration_apply.rs" | "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 afc018b00bf..7e218533a7c 100644 --- a/src/v3/compiler/tests/integration/sg0_census_test.rs +++ b/src/v3/compiler/tests/integration/sg0_census_test.rs @@ -294,14 +294,17 @@ 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", - // 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). + // 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", "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). @@ -326,6 +329,9 @@ 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", @@ -773,7 +779,6 @@ 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 289d10d62b195cca6b78c14a8eb66a98ca862e86 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 23:52:58 -0400 Subject: [PATCH 4/9] =?UTF-8?q?WIP:=20R3=20Debt-Paydown=20Mgr=20=E2=80=94?= =?UTF-8?q?=20lane=20through=20R3=20close?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/v3/compiler/src/gunbc_ci.rs | 73 +++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/v3/compiler/src/gunbc_ci.rs b/src/v3/compiler/src/gunbc_ci.rs index 723587bc5d8..7011ce54d0d 100644 --- a/src/v3/compiler/src/gunbc_ci.rs +++ b/src/v3/compiler/src/gunbc_ci.rs @@ -201,6 +201,38 @@ pub fn selection_subset_under_touch_set_growth( } } +/// **Layer 1** gate-id touch seed for [`select_affected_gates_for_binary_shim`]. +/// +/// Canvas `design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §1.1 +/// steps 3–5 and §4 require the **runner** to consume PR #2713 structured output, join +/// it with `CIWorkflowDag` / `TestClaim` metadata, and **then** emit executable work. +/// This struct is **not** that receipt: it only carries `CIGate.id` strings (and a +/// narrowing flag) **after** any `NodeRef` → gate-id mapping has happened elsewhere. +/// +/// When `narrowing_available` is `false`, selection fails closed to the full gate +/// roster ([`CiWorkflowDiff::TouchAll`]; canvas §3: unknown dimension / missing +/// receipt / unbuildable DAG pair). +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct CiBinaryShimAffectedSetReceipt { + /// `false` forces [`CiWorkflowDiff::TouchAll`] regardless of `proven_direct_gate_touches`. + pub narrowing_available: bool, + /// Gate ids proven directly touched **after** upstream mapping (seed before symmetric expansion). + pub proven_direct_gate_touches: BTreeSet, +} + +/// Layer 1 entry: map a **gate-id-only** [`CiBinaryShimAffectedSetReceipt`] into [`select_affected_gates`]. +pub fn select_affected_gates_for_binary_shim( + dag: &CiWorkflowDagInput, + receipt: &CiBinaryShimAffectedSetReceipt, +) -> Result, CiAffectedGatesError> { + let diff = if receipt.narrowing_available { + CiWorkflowDiff::TouchedGates(receipt.proven_direct_gate_touches.clone()) + } else { + CiWorkflowDiff::TouchAll + }; + select_affected_gates(dag, &diff) +} + fn topo_sort_subset( dag: &CiWorkflowDagInput, subset: &HashSet<&str>, @@ -476,4 +508,45 @@ mod tests { let wide = CiWorkflowDiff::TouchedGates(BTreeSet::from(["tests".into()])); assert!(selection_subset_under_touch_set_growth(&dag, &empty, &wide)); } + + #[test] + fn binary_shim_receipt_unknown_narrowing_matches_touch_all() { + let dag = demo_ci_dag(); + let receipt = CiBinaryShimAffectedSetReceipt { + narrowing_available: false, + proven_direct_gate_touches: BTreeSet::from(["lint".into()]), + }; + let got = select_affected_gates_for_binary_shim(&dag, &receipt).expect("valid dag"); + let expect = select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("valid dag"); + assert_eq!(got, expect); + } + + #[test] + fn binary_shim_receipt_narrow_lint_matches_touched_gates_lint() { + let dag = demo_ci_dag(); + let receipt = CiBinaryShimAffectedSetReceipt { + narrowing_available: true, + proven_direct_gate_touches: BTreeSet::from(["lint".into()]), + }; + let got = select_affected_gates_for_binary_shim(&dag, &receipt).expect("valid dag"); + let expect = select_affected_gates( + &dag, + &CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])), + ) + .expect("valid dag"); + assert_eq!(got, expect); + } + + #[test] + fn binary_shim_receipt_empty_narrow_seed_yields_empty_plan() { + let dag = demo_ci_dag(); + let receipt = CiBinaryShimAffectedSetReceipt { + narrowing_available: true, + proven_direct_gate_touches: BTreeSet::new(), + }; + assert_eq!( + select_affected_gates_for_binary_shim(&dag, &receipt), + Ok(Vec::new()) + ); + } } From 2cbcfb475e2dfa2dbbd57021624aca0c1b3eaf2d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 14 May 2026 04:10:04 +0000 Subject: [PATCH 5/9] fix(sg0): sync stage0 hand_maintained_src + sort TEST census (PR #3059) - dsl/gunbc/compiler.dag: extend stage0.hand_maintained_src to cover every root-level src/v3/compiler/src/*.rs row in EXPECTED_HAND_AUTHORED_NON_TEST so stage0 clean/copy/diff cannot treat restored hand files as generated. - sg0_census_test.rs: move boundary/l5_cross_target_consistency.rs before determinism_test.rs (ASCII order for sg0_expected_list_is_sorted_and_unique). Addresses codex BLOCKING review on #3059 (hand_maintained / sorted-list). Co-authored-by: Cursor --- dsl/gunbc/compiler.dag | 32 +++++++++++++++++-- .../tests/integration/sg0_census_test.rs | 4 ++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/dsl/gunbc/compiler.dag b/dsl/gunbc/compiler.dag index b0f66d98b3b..590f86aa6d0 100644 --- a/dsl/gunbc/compiler.dag +++ b/dsl/gunbc/compiler.dag @@ -72,8 +72,15 @@ type GeneratedCrate { data stage0: GeneratedCrate = { package_name: "v3-compiler", source_dir: "src/v3/compiler/src", + // SG-0 / stage0: every root-level `source_dir` `*.rs` hand file must appear here so + // `clean_stage0_generated_command` / copy / freshness diffs cannot treat census-listed + // hand paths as generated (INVARIANTS P2; keep in lockstep with + // `EXPECTED_HAND_AUTHORED_NON_TEST` root `src/v3/compiler/src/*.rs` rows). hand_maintained_src: [ "bootstrap.rs", + "bootstrap_regen_fresh.rs", + "cementing_dispatch.rs", + "complexity_lattice.rs", "cost_basis_declaration.rs", "dag.rs", "diagnostics.rs", @@ -84,20 +91,39 @@ data stage0: GeneratedCrate = { "emit_rust.rs", // PB-1 Item 5: hand-authored `main.rs` shell formatter (not emitted into src/). "emit_rust_bin_shim.rs", - "python_target.rs", - "rust_target.rs", + "emit_rust_roundtrip_fixtures.rs", + "enforced_lens_application.rs", + "gunbc_ci.rs", "infer.rs", + "int_literal_ranges.rs", + "integration_rs_wiring_scan.rs", + "lens_declaration_apply.rs", + "lens_t_las_carrier.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 // freshness diff does not treat it as generated drift (P2 boundary discipline). "lens_testgen_body.txt", "lib.rs", "lower.rs", + "memory_peak_cost.rs", + "omni_shape_b_openapi.rs", + "pb_method_template_projection.rs", "pipeline_authority.rs", "post_emit_verifier.rs", // PB-1 Item 5: host mirror of `dsl/std/process.dag` `ProcessExit` for emitted shells. "process_exit.rs", - "regen_parse_emit.rs" + "python_target.rs", + "r1c_e_gates.rs", + "r3_fc_lane2_loop_witness.rs", + "r3_gate_87_cementing_regen_runner_suites.rs", + "regen_bootstrap_emit.rs", + "regen_parse_emit.rs", + "regen_parse_tables_emit.rs", + "regen_tokenize.rs", + "rust_target.rs", + "self_host_receipt_p0.rs", + "test_runner.rs", + "wall_clock_ratchet_manifest.rs", ] } diff --git a/src/v3/compiler/tests/integration/sg0_census_test.rs b/src/v3/compiler/tests/integration/sg0_census_test.rs index 7e218533a7c..0f73d914170 100644 --- a/src/v3/compiler/tests/integration/sg0_census_test.rs +++ b/src/v3/compiler/tests/integration/sg0_census_test.rs @@ -359,13 +359,15 @@ const EXPECTED_HAND_AUTHORED_NON_TEST: &[&str] = &[ // "ported-but-still-listed" or "pending-port" set); the ratchet's whole point is that one // monotonically-shrinking authority tracks the Rust→`.dag` migration. const EXPECTED_HAND_AUTHORED_TEST: &[&str] = &[ + // `tests/boundary/` before `tests/determinism_test.rs` (ASCII `boundary/` < `determinism_`); + // `l5_*` before `m1_*` / `m2_*` within boundary/. + "src/v3/compiler/tests/boundary/l5_cross_target_consistency.rs", "src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs", "src/v3/compiler/tests/boundary/m1_3_emit_rust_test.rs", "src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs", "src/v3/compiler/tests/boundary/m1_5_emit_omni_demo_test.rs", "src/v3/compiler/tests/boundary/m2_emit_multi_field_struct_variant_test.rs", "src/v3/compiler/tests/determinism_test.rs", - "src/v3/compiler/tests/boundary/l5_cross_target_consistency.rs", "src/v3/compiler/tests/integration.rs", "src/v3/compiler/tests/integration/anthropic_messages_callable_test.rs", // R3 gate #68 (`anthropic_wire_demonstration`): hermetic typed request/response From 13bad298e9e9a653ee984f1f58ad8bda5a75d5e2 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 14 May 2026 04:20:56 +0000 Subject: [PATCH 6/9] chore(ci): SG-0 PR-body append for #3059 (+17 hand-path, pairing (b)) Co-authored-by: Cursor --- scripts/ci-merge/sg0-pr-body-append.3059.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 scripts/ci-merge/sg0-pr-body-append.3059.txt diff --git a/scripts/ci-merge/sg0-pr-body-append.3059.txt b/scripts/ci-merge/sg0-pr-body-append.3059.txt new file mode 100644 index 00000000000..317afd32fad --- /dev/null +++ b/scripts/ci-merge/sg0-pr-body-append.3059.txt @@ -0,0 +1,2 @@ +SG-0 hand-path delta: +17 +SG-0 pairing: (b) Director corrective revert + SG-0 census restoration after rescinded PB-0 template-relocation cycle (#3057/#3048/#3046). https://github.com/gunb-ai/gunbc/pull/3059 From e86f98a5191303f2f3737339fb47b83fbb9cef7b Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 14 May 2026 05:08:19 +0000 Subject: [PATCH 7/9] docs(ci): tighten SG-0 pairing (b) P5 receipt for #3059 append Co-authored-by: Cursor --- scripts/ci-merge/sg0-pr-body-append.3059.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/ci-merge/sg0-pr-body-append.3059.txt b/scripts/ci-merge/sg0-pr-body-append.3059.txt index 317afd32fad..464286dc331 100644 --- a/scripts/ci-merge/sg0-pr-body-append.3059.txt +++ b/scripts/ci-merge/sg0-pr-body-append.3059.txt @@ -1,2 +1,3 @@ SG-0 hand-path delta: +17 -SG-0 pairing: (b) Director corrective revert + SG-0 census restoration after rescinded PB-0 template-relocation cycle (#3057/#3048/#3046). https://github.com/gunb-ai/gunbc/pull/3059 +SG-0 pairing: (b) P5 checkable receipt (INVARIANTS §P5 Dispatch-Discipline (b)): +17 restores honest `EXPECTED_HAND_AUTHORED_*` rows after operator corrective revert; paired deletions vs `origin/main` remove rescinded PB-0 codegen/template scaffolds — rep. `tools/pb0_cycle4_emit_templates/cementing_dispatch.rs.in`, `tools/pb0_cycle4_emit_templates/pipeline_authority.rs.in`, `src/v3/compiler/src/complexity_lattice_generated.rs`, `src/v3/compiler/src/emit_rust_bin_shim_generated.rs` (full `D\t` set in this PR diff). ROADMAP.md **SG-0 PR-window net-shrink discipline** (Director course-correction 2026-05-05) frames the PR-body pairing requirement this line satisfies. Director/operator corrective budget URL: +https://github.com/gunb-ai/gunbc/pull/3059 From c7d9ce08ec9752a980a936ac3fe278a4fda6fa25 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 14 May 2026 05:15:46 +0000 Subject: [PATCH 8/9] docs(invariants): P5 receipts for all SG-0 NON_TEST census paths (PR #3059) Co-authored-by: Cursor --- INVARIANTS.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/INVARIANTS.md b/INVARIANTS.md index 5a34d29e258..6bf9e09065c 100644 --- a/INVARIANTS.md +++ b/INVARIANTS.md @@ -345,17 +345,67 @@ Per **Dispatch-Discipline Mechanisms (b)** above, each **new** path added to `EX | `src/v3/compiler/tests/integration/r3_gate_60_phase2_width_nat_parser_test.rs` | **ROADMAP:** `ROADMAP.md` — **`substrate_gap_parser_grammar_closed`** / R3 gate **#60** (Phase 2.1 parser slice: angle-only width nat in `<…>`, `TypeAngleArg` substrate split, `Compose>` lowering with literal phantom width). **Dissolution:** remove when gate #60 parse/lower/routing receipts are carried by `.dag` `TestClaim` / generated harness without this hand-authored `compile_to_dag` + `parse_for_test` module (per `docs/audit/r3-gate-60-decomposition.md` follow-on slices). **Interim ratchet:** `gate_60_phase2_parse_accepts_algebra_angle_width_nat`, `gate_60_phase2_int_64_lowers_to_compose_int_machine_width_literal`, `gate_60_phase2_nat_8_lowers_via_uint_slot`, `gate_60_integer_routing_witness_accepts_literal_nat_machine_width`, `gate_60_bare_numeric_type_is_parse_rejected`, `gate_60_int_disallowed_width_fails_closed_without_malformed_template_args`. | | `src/v3/compiler/tests/integration/t_gate_58_apply_lens_self_application_test.rs` | **ROADMAP:** `ROADMAP.md` → `### Nine lanes` → **T-PB-B** / `pb_rust_tests_outside_residual_zero`. **Plan:** `docs/r3-program-plan.md` §1.8 gate **#58** `apply_lens_self_application_demonstrated` — lane **T-Lens-Self-Application** (`EnforcedApplication` bootstrap receipt over `gate_58_apply_lens_self_application_pass` + typed witness `gate_58_modeled_ci_timing_measurement` (`gate_58_ci_workflow_timing_row` with `workflow: modeled_gunbc_ci_workflow`) in `src/v3/std/t_ci_workflow_as_data_demo.dag`). **Dissolution:** remove when a `.dag` `TestClaim` / runner receipt asserts the same `generated_full_bootstrap_dag()` facts (empty bootstrap diagnostics + witness declarations) without this Rust harness. **Interim ratchet:** `apply_lens_self_application_demonstrated_bootstrap_receipt` + `gate_58_modeled_ci_timing_measurement` presence checks. **Co-receipt (same PR, P5 single surface):** expanded `src/v3/compiler/src/enforced_lens_application.rs` timing consumer + `src/v3/compiler/build.rs` default-rank std `STAGED_FILES` ordering from parsed `import v3.std.*` edges (Kahn topo among co-ranked files; single authority with module imports) are documented in `scripts/ci-merge/sg0-pr-body-append.2827.txt` (prepended to the PR body in CI) with explicit ROADMAP rows + dissolution triggers per INVARIANTS §P5 Mechanism **(b)**. | -### SG-0 hand-authored compiler non-test paths (`src/v3/compiler/src/`) +### SG-0 hand-authored compiler non-test paths (`EXPECTED_HAND_AUTHORED_NON_TEST`) -Per **Dispatch-Discipline Mechanisms (b)**, each path in `EXPECTED_HAND_AUTHORED_NON_TEST` inside `src/v3/compiler/tests/integration/sg0_census_test.rs` must carry a matching dissolution-bearing receipt **in the same PR** as the census line. The following rows are the planning home-of-record for Band-C gate #87 seams that are not integration-test binaries. +Per **Dispatch-Discipline Mechanisms (b)**, each path in `EXPECTED_HAND_AUTHORED_NON_TEST` inside `src/v3/compiler/tests/integration/sg0_census_test.rs` must carry a matching dissolution-bearing receipt **in the same PR** as the census line. The table below is the planning home-of-record for **every** listed non-test hand-Rust path (crate sources, `build.rs`, regen `bin/` entrypoints, and bench harnesses), not only Band-C gate #87 seams. | Path | Receipt | |------|---------| +| `src/v3/compiler/benches/tier3_mirror_perf.rs` | **ROADMAP:** `ROADMAP.md` — **Tier-3 perf-budget Phase-1** / T-Tier3-Dissolution (`docs/briefs/r3-pb-tier3-perf-budget-worker.md`; parent brief #1331). **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when Tier3 host mirrors dissolve per `docs/r3-structure.md` §Acceptance (Criterion harness no longer targets hot Rust substrate entrypoints). **Interim ratchet:** frozen `tier3_baseline.json` + bench stays `--locked` clean under CI. | +| `src/v3/compiler/build.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor`; **authority:** `dsl/gunbc/compiler.dag` **stage0** (`stage0.hand_maintained_src` / regen edges surfaced to Cargo). **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** `build.rs` mirror retires when stage ordering + copy/hash contracts are enforced only via `.dag` emit programs (`SELF_HOSTING.md` §2). **Interim ratchet:** bootstrap snapshot + fmt/ci gates keep `build.rs` aligned with `compiler.dag`. | +| `src/v3/compiler/src/bin/gunbc_ci.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** + **T-Workflow-As-Data** — `bin/gunbc_ci` host shim for modeled CI (`dsl/gunbc/ci.dag`). **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when CI selection + receipts are emitted/invoked without a hand `main` (`SELF_HOSTING.md` §2.2). **Interim ratchet:** `gunbc_ci` integration tests + SG-0 census. | +| `src/v3/compiler/src/bin/r1c_e_emit_gates.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** / R1C-E emit gates (`docs/r3-program-plan.md` T-Emit `.dag` `TestClaim` lane). **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when emit-gate checks are invoked only from generated/bootstrap harness without this bin. **Interim ratchet:** `r1c_e_gates.rs` shared checks + integration harness. | +| `src/v3/compiler/src/bin/regen_bootstrap.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** — regen **bootstrap** stage host entry (`dsl/gunbc/regen*.dag` family). **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when bootstrap regen is a single declared pipeline command with no parallel Rust `main`. **Interim ratchet:** `regen_bootstrap_emit.rs` + bootstrap snapshot CI. | +| `src/v3/compiler/src/bin/regen_lens.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** — regen **lens** stage host entry. **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when lens regen is pipeline-only. **Interim ratchet:** `regen_lens` + lens regen integration tests. | +| `src/v3/compiler/src/bin/regen_parse.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** — regen **parse** stage host entry. **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when parse regen is pipeline-only. **Interim ratchet:** `regen_parse_emit.rs` + parse regen tests. | +| `src/v3/compiler/src/bin/regen_parse_tables.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** — regen **parse tables** stage host entry. **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when parse-tables regen is pipeline-only. **Interim ratchet:** `regen_parse_tables_emit.rs` + tables regen tests. | +| `src/v3/compiler/src/bin/regen_tokenize.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** — regen **tokenize** stage host entry. **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when tokenize regen is pipeline-only. **Interim ratchet:** `src/v3/compiler/src/regen_tokenize.rs` + tokenizer tests. | +| `src/v3/compiler/src/bin/regen_v3.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** — regen **v3** umbrella host entry. **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when full v3 regen is pipeline-only. **Interim ratchet:** fmt/ci bootstrap gates. | +| `src/v3/compiler/src/bin/self_host_fixed_point.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** / `pb_self_compile_fixed_point` — self-host fixed-point trend reader bin. **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when fixed-point receipts are `.dag`/generated consumers only. **Interim ratchet:** `self_host_receipt_p0.rs` JSON field stability tests. | +| `src/v3/compiler/src/bootstrap.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** / compiler bootstrap (`generated_full_bootstrap_dag` host seam). **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when bootstrap construction is generated-only without hand orchestration. **Interim ratchet:** bootstrap integration tests + snapshot ratchet. | +| `src/v3/compiler/src/bootstrap_regen_fresh.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** — bootstrap regen freshness / copy discipline host module. **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when freshness checks live in pipeline `.dag` programs only. **Interim ratchet:** `compiler.dag` stage0 + CI `--verify` gate. | | `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/complexity_lattice.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/cost_basis_declaration.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/dag.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/dag/builder.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/dag/cardinality_payload.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/dag/effects.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/dag/ports.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/diagnostics.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/dimension.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/emit.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/emit/collection_ops_method_contract.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/emit/python_target.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/emit/rust_target.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/emit_rust.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/emit_rust_bin_shim.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/emit_rust_roundtrip_fixtures.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/enforced_lens_application.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | | `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/infer.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/int_literal_ranges.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | | `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/lens_declaration_apply.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/lens_t_las_carrier.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/lib.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/lower.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/memory_peak_cost.rs` | **ROADMAP:** `ROADMAP.md` / `docs/r3-program-plan.md` §1.8 — **R3 gate #94** cost-lens memory-peak compose + enforcement (`dominant` / max_path). **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when memory-peak enforcement is substrate-owned without this hand module. **Interim ratchet:** cost-lens integration + cementing tests referencing gate #94. | +| `src/v3/compiler/src/omni_shape_b_openapi.rs` | **ROADMAP:** `ROADMAP.md` — **T-Omni-Shape-B** transitional surface (`docs/briefs/` brief #1 / PR #2251). **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when Shape B `.dag` program owns OpenAPI projection end-to-end. **Interim ratchet:** omni emit boundary tests. | +| `src/v3/compiler/src/pb_method_template_projection.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** / R3 row 85 — `MethodTemplateContract` read surface (`docs/decisions/r3-row85-method-template-read-surface.md`). **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when projection is generated from `.dag` authority only. **Interim ratchet:** method-template contract tests. | +| `src/v3/compiler/src/pipeline_authority.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/post_emit_verifier.rs` | **ROADMAP:** `ROADMAP.md` § **Nine lanes** — **T-PB-A** / `pb_hand_rust_at_shim_floor` (SG-0 non-test census in `sg0_census_test.rs`; `docs/design-pure-bootstrap-zero.md`). **PR receipt (P5 Mechanism (b)):** this INVARIANTS row + matching `EXPECTED_HAND_AUTHORED_NON_TEST` literal land in the same PR. **Dissolution:** delete when this file's contract is carried only by `.dag` substrate + pipeline-emitted Rust per `SELF_HOSTING.md` §2 (no hand-maintained parallel authority). **Interim ratchet:** `cargo fmt` / `ci` / `v3` + SG-0 census tests keep the path stable. | +| `src/v3/compiler/src/process_exit.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** — PB-1 host mirror of `dsl/std/process.dag` `ProcessExit` for emitted bin shims. **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when process exit typing is emitted, not hand-mirrored. **Interim ratchet:** emit shim tests. | +| `src/v3/compiler/src/r1c_e_gates.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** / R1C-E — shared `check_*` API for emit-gate `.dag` `TestClaim` wrappers + `r1c_e_emit_gates` bin. **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when gates are generated-only. **Interim ratchet:** R1C-E integration harness. | +| `src/v3/compiler/src/r3_fc_lane2_loop_witness.rs` | **ROADMAP:** `ROADMAP.md` — **R3 T-Free-Consequences** / db18 divergence (`docs/design-db18-workflow-effect-carrier.md`); directive scanner until lowering owns `lane2_workflow`. **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete per ROADMAP row when lowering attaches loop workflow effects. **Interim ratchet:** auto-loop lens claims in `r3_fc_*` tests. | | `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/regen_bootstrap_emit.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** — bootstrap regen emit host module. **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when emit output is pipeline-only. **Interim ratchet:** regen bootstrap integration tests. | +| `src/v3/compiler/src/regen_parse_emit.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** — parse regen emit host module. **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when emit output is pipeline-only. **Interim ratchet:** parse regen tests. | +| `src/v3/compiler/src/regen_parse_tables_emit.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** — parse-tables regen emit host module. **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when emit output is pipeline-only. **Interim ratchet:** parse-tables regen tests. | +| `src/v3/compiler/src/regen_tokenize.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** — tokenizer regen emit host module. **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when emit output is pipeline-only. **Interim ratchet:** tokenizer regen tests. | +| `src/v3/compiler/src/self_host_receipt_p0.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** / T-FixedPoint P0 — stable JSON receipt keys for `self_host_fixed_point` trends (DB-8 hand receipt). **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when receipt schema is `.dag`/generated-owned. **Interim ratchet:** field-name unit tests in-module. | +| `src/v3/compiler/src/test_runner.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** / **T-PB-B** — integration test runner host (`ExecuteCommand` / claim evaluation seam). **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when runner is generated or substrate-owned without hand orchestration. **Interim ratchet:** integration + cementing suites. | +| `src/v3/compiler/src/wall_clock_ratchet_manifest.rs` | **ROADMAP:** `ROADMAP.md` § **T-PB-A** — wall-clock warn manifest for CI/binary shims (timing discipline). **PR receipt (P5 Mechanism (b)):** this row + census line same PR. **Dissolution:** delete when timing manifest is `.dag` data consumed by codegen. **Interim ratchet:** modeled CI workflow tests. | --- ## Appendix: ID index From 5fe874a8fe6fa74bd3a158f55bce3f4e4e869d1e Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 14 May 2026 06:14:41 +0000 Subject: [PATCH 9/9] fix(v3-compiler): box OptionalDiagnostic payload for clippy large_enum_variant Co-authored-by: Cursor --- src/v3/compiler/src/lens_t_las_carrier.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v3/compiler/src/lens_t_las_carrier.rs b/src/v3/compiler/src/lens_t_las_carrier.rs index 471841ee57a..39c8f105bbe 100644 --- a/src/v3/compiler/src/lens_t_las_carrier.rs +++ b/src/v3/compiler/src/lens_t_las_carrier.rs @@ -14,7 +14,7 @@ use crate::dimension::Witness; #[derive(Clone, Debug)] pub enum OptionalDiagnostic { NoDiagnostic, - SomeDiagnostic { value: Diagnostic }, + SomeDiagnostic { value: Box }, } pub type LensReadFn = dyn Fn(&Dag, &Behavior) -> Witness;