diff --git a/TESTING.md b/TESTING.md index fd4d377985a..a35a32d0ff9 100644 --- a/TESTING.md +++ b/TESTING.md @@ -331,6 +331,63 @@ See `docs/design-test-infra.md` and ROADMAP §Lane 2 Stage 2c for the DB-15 trajectory. This document is the near-term discipline for Rust-side tests while that runtime matures. +## Cementing tests (Band C — lens subsumption) + +**Where this applies.** Whenever a brief, roadmap line, or review +claims that v3 **subsumes** or is **behaviorally equivalent** to a +v2 analysis for the same program text, that claim is a Band-C +statement about the *function* of the lens, not about whether the +`.dag` file is structurally terminal. The authoritative inventory +is `docs/v3-lens-capability-register.md` (behavioral axis + v2 +counterpart column). Any `LensRegistryEntry` in +`src/v3/compiler/regen.dag` whose row moves to +`BEHAVIORALLY COMPLETE` while still naming a real v2 counterpart +(not `None (v3-native)` / not `N/A`) inherits the same obligation. + +**What “cementing” means.** A cementing test is a **behavioral** +regression that would fail if v3 silently changed the analysis +while prose still claimed parity. Concretely: + +- **v2 counterpart exists:** run the same minimal fixture through + both implementations (v2 oracle + v3 lens output) and assert + semantic equality on the published carrier shape — or assert a + documented, reviewed projection when the types differ but the + claim is about a specific homomorphism. +- **No v2 counterpart (`N/A` / v3-native) but the register marks + `BEHAVIORALLY COMPLETE`:** pin the lens’s published behavioral + contract on **minimal constructed `Dag` shapes** (or a single + tiny `compile_to_dag` fixture when the contract genuinely spans + the pipeline). This is still a cementing test: it cements the + `COMPLETE` row against accidental semantic drift. + +**Where tests live.** Add modules under +`src/v3/compiler/tests/integration/cementing/` and register each +file from `src/v3/compiler/tests/integration.rs` via `#[path = +"integration/cementing/.rs"]`. Prefer one focused module per +lens (or per v2 parity claim), not a kitchen-sink file. + +**Dispatch (same PR as the claim).** Editing +`src/v3/compiler/regen.dag` or promoting a register row to +`COMPLETE` with v2 parity is not complete until the matching +cementing module exists and `tests/integration.rs` wires it. Extend +`CEMENTING_MODULES_FOR_V2_COMPLETE_CLAIMS` in +`tests/integration/cementing/cementing_lens_registry_dispatch_test.rs` +when the register row is upgraded; the test +`cementing_escalation_slice_matches_capability_register` derives the +required registry `name` keys from `docs/v3-lens-capability-register.md` +plus `regen.dag` and asserts the slice matches **exactly** (the slice +is a projection, not a parallel inventory). The test +`cementing_test_modules_exist_for_escalated_v2_complete_registry_claims` +then requires each listed stem to resolve to an on-disk `.rs` file under +`cementing/` **and** to appear in `tests/integration.rs` behind an +exact `#[path = "integration/cementing/.rs"]` attribute (so an +orphan file cannot satisfy the ratchet). + +**Anti-scope.** This section is not a mandate to prove full lens +equivalence for every `.dag` file, and it does not require new +substrate accessors or emitters. It only governs **explicit** +subsumption or `COMPLETE`-with-counterpart claims. + ## Related - `INVARIANTS.md` — project-level invariants; some have direct @@ -339,3 +396,5 @@ for Rust-side tests while that runtime matures. the modeled concepts, not rebuild them - `docs/design-test-infra.md` (DB-15) — the `.dag`-native test infrastructure this document is the stopgap for +- `docs/v3-lens-capability-register.md` — Band-C behavioral axis; + cross-check before dispatching “v3 replaces v2 X” briefs diff --git a/docs/v3-lens-capability-register.md b/docs/v3-lens-capability-register.md index 647af16e539..a261526a139 100644 --- a/docs/v3-lens-capability-register.md +++ b/docs/v3-lens-capability-register.md @@ -78,6 +78,7 @@ The register is a tool, not a document. If it is stale, it is worse than not exi 3. **Review at every ROADMAP dispatch.** Before queueing a brief that depends on a lens being equivalent to its v2 counterpart (e.g., "compare v3 complexity output against v2 complexity output"), check the row. If the row says `PROXY` or `STUB`, the brief is premature and must either wait for substrate/emit work or narrow its scope to what the lens actually computes. 4. **Downgrades are first-class.** A lens that regresses from `COMPLETE` to `PROXY` (e.g., because a cementing test failed and was weakened rather than fixed) belongs in this register immediately. Silent downgrade is the failure mode this document exists to catch. 5. **The two axes do not trade off.** `STRUCTURALLY TERMINAL` + `BEHAVIORALLY PROXY` is a valid, honest state — a well-formed file computing a weaker function than its name implies. It is not a contradiction. Claiming `COMPLETE` because the file is structurally clean is the mistake this register is named against. +6. **Cementing tests have a canonical home in-tree.** In-repo dispatch (fixture layout, v2-oracle vs minimal-`Dag` contract tests, and the `regen.dag` → register → test-module ratchet) lives in `TESTING.md` under *Cementing tests (Band C — lens subsumption)* and in `src/v3/compiler/tests/integration/cementing/cementing_lens_registry_dispatch_test.rs`. `cementing_escalation_slice_matches_capability_register` mechanically aligns `CEMENTING_MODULES_FOR_V2_COMPLETE_CLAIMS` with this table plus `regen.dag` (the slice is a projection, not a second authority). The follow-on ratchet checks both an on-disk `cementing/.rs` and a matching `#[path = ...]` line in `tests/integration.rs` for each escalated claim. Promoting a row to `BEHAVIORALLY COMPLETE` with a non-`N/A` v2 counterpart without landing the cementing module in the same PR is a process failure, not a modeling disagreement. ## Related docs diff --git a/src/v3/compiler/regen.dag b/src/v3/compiler/regen.dag index 1ba63e9e8e6..cf47ecf9ecf 100644 --- a/src/v3/compiler/regen.dag +++ b/src/v3/compiler/regen.dag @@ -24,6 +24,16 @@ import std.types { FilePath, NonEmptyStr } // means empty-string entries cannot be lowered into the registry, // closing the "illegal states representable" hole that a raw // `String` would leave open across all three fields. +// +// **Band-C cementing dispatch.** Each new `LensRegistryEntry` must +// update `docs/v3-lens-capability-register.md` (see its Discipline +// section). If the register row is `BEHAVIORALLY COMPLETE` and names +// a concrete v2 counterpart, add a matching behavioral cementing +// module under `src/v3/compiler/tests/integration/cementing/`, wire it +// in `tests/integration.rs` via `#[path = "integration/cementing/.rs"]`, +// and extend the ratchet slice in +// `cementing_lens_registry_dispatch_test.rs` in the same PR. See +// `TESTING.md` ("Cementing tests (Band C — lens subsumption)"). type LensRegistryEntry { name: NonEmptyStr diff --git a/src/v3/compiler/src/bootstrap_generated.rs b/src/v3/compiler/src/bootstrap_generated.rs index c9a184f4a47..993f33ab532 100644 --- a/src/v3/compiler/src/bootstrap_generated.rs +++ b/src/v3/compiler/src/bootstrap_generated.rs @@ -1037,15 +1037,15 @@ pub(crate) fn bootstrapped_fixture_dag() -> Dag { Declaration { id: DeclarationId(1029), name: Some("from_symbol".to_string()), connective: TypeConnective::Arrow { inputs: vec![DeclarationId(182)], output: DeclarationId(1400), body: ArrowBody::Unparsed(SourceSpan::new("src/v3/compiler/operators.dag", 156, 771)) }, type_params: vec![], meta_tag: None, inhabits: None, value_body: None, refinement: None, span: SourceSpan::new("src/v3/compiler/operators.dag", 108, 771) }, Declaration { id: DeclarationId(1030), name: Some("symbol".to_string()), connective: TypeConnective::Arrow { inputs: vec![DeclarationId(565)], output: DeclarationId(182), body: ArrowBody::UserDefined(NodeId(243)) }, type_params: vec![], meta_tag: None, inhabits: None, value_body: None, refinement: None, span: SourceSpan::new("src/v3/compiler/operators.dag", 773, 1214) }, Declaration { id: DeclarationId(1031), name: Some("algebra_field_name".to_string()), connective: TypeConnective::Arrow { inputs: vec![DeclarationId(565)], output: DeclarationId(182), body: ArrowBody::UserDefined(NodeId(260)) }, type_params: vec![], meta_tag: None, inhabits: None, value_body: None, refinement: None, span: SourceSpan::new("src/v3/compiler/operators.dag", 1216, 2017) }, - Declaration { id: DeclarationId(1032), name: Some("LensRegistryEntry".to_string()), connective: TypeConnective::Conj { children: vec![Field { label: "name".to_string(), ty: DeclarationId(107) }, Field { label: "lens_file".to_string(), ty: DeclarationId(116) }, Field { label: "generated_file".to_string(), ty: DeclarationId(116) }] }, type_params: vec![], meta_tag: None, inhabits: None, value_body: None, refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 1355, 1450) }, - Declaration { id: DeclarationId(1033), name: Some("lens_cost_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("cost".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/complexity.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_cost_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 1452, 1619) }, - Declaration { id: DeclarationId(1034), name: Some("lens_cost_symbolic_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("cost_symbolic".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/cost.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_cost_symbolic_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 1621, 1809) }, - Declaration { id: DeclarationId(1035), name: Some("lens_provenance_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("provenance".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/provenance.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_provenance_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 1811, 1996) }, - Declaration { id: DeclarationId(1036), name: Some("lens_structural_resolution_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("structural_resolution".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/structural_resolution.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_structural_resolution_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 1998, 2227) }, - Declaration { id: DeclarationId(1037), name: Some("lens_unused_parameters_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("unused_parameters".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/unused_parameters.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_unused_parameters_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 2229, 2442) }, - Declaration { id: DeclarationId(1038), name: Some("lens_infer_helpers_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("infer_helpers".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/infer_helpers.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/infer_helpers_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 3069, 3261) }, - Declaration { id: DeclarationId(1039), name: Some("lens_variant_payload_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("variant_payload".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/variant_payload.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/variant_payload_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 3263, 3463) }, - Declaration { id: DeclarationId(1040), name: Some("lens_lower_helpers_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("lower_helpers".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/lower_helpers.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lower_helpers_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 3713, 3905) }, + Declaration { id: DeclarationId(1032), name: Some("LensRegistryEntry".to_string()), connective: TypeConnective::Conj { children: vec![Field { label: "name".to_string(), ty: DeclarationId(107) }, Field { label: "lens_file".to_string(), ty: DeclarationId(116) }, Field { label: "generated_file".to_string(), ty: DeclarationId(116) }] }, type_params: vec![], meta_tag: None, inhabits: None, value_body: None, refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 1950, 2045) }, + Declaration { id: DeclarationId(1033), name: Some("lens_cost_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("cost".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/complexity.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_cost_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 2047, 2214) }, + Declaration { id: DeclarationId(1034), name: Some("lens_cost_symbolic_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("cost_symbolic".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/cost.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_cost_symbolic_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 2216, 2404) }, + Declaration { id: DeclarationId(1035), name: Some("lens_provenance_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("provenance".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/provenance.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_provenance_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 2406, 2591) }, + Declaration { id: DeclarationId(1036), name: Some("lens_structural_resolution_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("structural_resolution".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/structural_resolution.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_structural_resolution_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 2593, 2822) }, + Declaration { id: DeclarationId(1037), name: Some("lens_unused_parameters_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("unused_parameters".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/unused_parameters.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_unused_parameters_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 2824, 3037) }, + Declaration { id: DeclarationId(1038), name: Some("lens_infer_helpers_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("infer_helpers".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/infer_helpers.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/infer_helpers_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 3664, 3856) }, + Declaration { id: DeclarationId(1039), name: Some("lens_variant_payload_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("variant_payload".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/variant_payload.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/variant_payload_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 3858, 4058) }, + Declaration { id: DeclarationId(1040), name: Some("lens_lower_helpers_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("lower_helpers".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/lower_helpers.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lower_helpers_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 4308, 4500) }, Declaration { id: DeclarationId(1041), name: Some("DagDifference".to_string()), connective: TypeConnective::Conj { children: vec![Field { label: "detail".to_string(), ty: DeclarationId(182) }] }, type_params: vec![], meta_tag: None, inhabits: None, value_body: None, refinement: None, span: SourceSpan::new("src/v3/compiler/runtime_mirrors.dag", 115, 154) }, Declaration { id: DeclarationId(1042), name: Some("SurfaceModule".to_string()), connective: TypeConnective::Conj { children: vec![Field { label: "items".to_string(), ty: DeclarationId(1401) }] }, type_params: vec![], meta_tag: None, inhabits: None, value_body: None, refinement: None, span: SourceSpan::new("src/v3/compiler/runtime_mirrors.dag", 580, 629) }, Declaration { id: DeclarationId(1043), name: Some("SurfaceParam".to_string()), connective: TypeConnective::Conj { children: vec![Field { label: "name".to_string(), ty: DeclarationId(182) }, Field { label: "ty".to_string(), ty: DeclarationId(1047) }, Field { label: "refinement".to_string(), ty: DeclarationId(1402) }] }, type_params: vec![], meta_tag: None, inhabits: None, value_body: None, refinement: None, span: SourceSpan::new("src/v3/compiler/runtime_mirrors.dag", 631, 712) }, diff --git a/src/v3/compiler/src/bootstrap_generated_without_runtime_mirrors.rs b/src/v3/compiler/src/bootstrap_generated_without_runtime_mirrors.rs index 5416a6ffdd6..ac2a7fcc25e 100644 --- a/src/v3/compiler/src/bootstrap_generated_without_runtime_mirrors.rs +++ b/src/v3/compiler/src/bootstrap_generated_without_runtime_mirrors.rs @@ -1037,15 +1037,15 @@ pub(crate) fn bootstrapped_fixture_without_runtime_mirrors_dag() -> Dag { Declaration { id: DeclarationId(1029), name: Some("from_symbol".to_string()), connective: TypeConnective::Arrow { inputs: vec![DeclarationId(182)], output: DeclarationId(1386), body: ArrowBody::Unparsed(SourceSpan::new("src/v3/compiler/operators.dag", 156, 771)) }, type_params: vec![], meta_tag: None, inhabits: None, value_body: None, refinement: None, span: SourceSpan::new("src/v3/compiler/operators.dag", 108, 771) }, Declaration { id: DeclarationId(1030), name: Some("symbol".to_string()), connective: TypeConnective::Arrow { inputs: vec![DeclarationId(565)], output: DeclarationId(182), body: ArrowBody::UserDefined(NodeId(243)) }, type_params: vec![], meta_tag: None, inhabits: None, value_body: None, refinement: None, span: SourceSpan::new("src/v3/compiler/operators.dag", 773, 1214) }, Declaration { id: DeclarationId(1031), name: Some("algebra_field_name".to_string()), connective: TypeConnective::Arrow { inputs: vec![DeclarationId(565)], output: DeclarationId(182), body: ArrowBody::UserDefined(NodeId(260)) }, type_params: vec![], meta_tag: None, inhabits: None, value_body: None, refinement: None, span: SourceSpan::new("src/v3/compiler/operators.dag", 1216, 2017) }, - Declaration { id: DeclarationId(1032), name: Some("LensRegistryEntry".to_string()), connective: TypeConnective::Conj { children: vec![Field { label: "name".to_string(), ty: DeclarationId(107) }, Field { label: "lens_file".to_string(), ty: DeclarationId(116) }, Field { label: "generated_file".to_string(), ty: DeclarationId(116) }] }, type_params: vec![], meta_tag: None, inhabits: None, value_body: None, refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 1355, 1450) }, - Declaration { id: DeclarationId(1033), name: Some("lens_cost_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("cost".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/complexity.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_cost_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 1452, 1619) }, - Declaration { id: DeclarationId(1034), name: Some("lens_cost_symbolic_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("cost_symbolic".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/cost.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_cost_symbolic_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 1621, 1809) }, - Declaration { id: DeclarationId(1035), name: Some("lens_provenance_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("provenance".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/provenance.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_provenance_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 1811, 1996) }, - Declaration { id: DeclarationId(1036), name: Some("lens_structural_resolution_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("structural_resolution".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/structural_resolution.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_structural_resolution_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 1998, 2227) }, - Declaration { id: DeclarationId(1037), name: Some("lens_unused_parameters_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("unused_parameters".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/unused_parameters.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_unused_parameters_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 2229, 2442) }, - Declaration { id: DeclarationId(1038), name: Some("lens_infer_helpers_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("infer_helpers".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/infer_helpers.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/infer_helpers_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 3069, 3261) }, - Declaration { id: DeclarationId(1039), name: Some("lens_variant_payload_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("variant_payload".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/variant_payload.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/variant_payload_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 3263, 3463) }, - Declaration { id: DeclarationId(1040), name: Some("lens_lower_helpers_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("lower_helpers".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/lower_helpers.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lower_helpers_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 3713, 3905) }, + Declaration { id: DeclarationId(1032), name: Some("LensRegistryEntry".to_string()), connective: TypeConnective::Conj { children: vec![Field { label: "name".to_string(), ty: DeclarationId(107) }, Field { label: "lens_file".to_string(), ty: DeclarationId(116) }, Field { label: "generated_file".to_string(), ty: DeclarationId(116) }] }, type_params: vec![], meta_tag: None, inhabits: None, value_body: None, refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 1950, 2045) }, + Declaration { id: DeclarationId(1033), name: Some("lens_cost_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("cost".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/complexity.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_cost_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 2047, 2214) }, + Declaration { id: DeclarationId(1034), name: Some("lens_cost_symbolic_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("cost_symbolic".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/cost.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_cost_symbolic_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 2216, 2404) }, + Declaration { id: DeclarationId(1035), name: Some("lens_provenance_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("provenance".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/provenance.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_provenance_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 2406, 2591) }, + Declaration { id: DeclarationId(1036), name: Some("lens_structural_resolution_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("structural_resolution".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/structural_resolution.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_structural_resolution_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 2593, 2822) }, + Declaration { id: DeclarationId(1037), name: Some("lens_unused_parameters_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("unused_parameters".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/unused_parameters.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lens_unused_parameters_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 2824, 3037) }, + Declaration { id: DeclarationId(1038), name: Some("lens_infer_helpers_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("infer_helpers".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/infer_helpers.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/infer_helpers_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 3664, 3856) }, + Declaration { id: DeclarationId(1039), name: Some("lens_variant_payload_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("variant_payload".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/variant_payload.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/variant_payload_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 3858, 4058) }, + Declaration { id: DeclarationId(1040), name: Some("lens_lower_helpers_entry".to_string()), connective: TypeConnective::Instantiation { template: DeclarationId(1032), arguments: vec![] }, type_params: vec![], meta_tag: Some(DeclarationId(1032)), inhabits: None, value_body: Some(ValueBody::Structural { fields: vec![("name".to_string(), FieldValue::Literal(LiteralBits::String("lower_helpers".to_string()))), ("lens_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/lenses/lower_helpers.dag".to_string()))), ("generated_file".to_string(), FieldValue::Literal(LiteralBits::String("src/v3/compiler/src/lower_helpers_generated.rs".to_string())))] }), refinement: None, span: SourceSpan::new("src/v3/compiler/regen.dag", 4308, 4500) }, Declaration { id: DeclarationId(1041), name: None, connective: TypeConnective::Instantiation { template: DeclarationId(455), arguments: vec![TemplateArgument { parameter: DeclarationId(456), value: DeclarationId(458) }] }, type_params: vec![], meta_tag: None, inhabits: None, value_body: None, refinement: None, span: SourceSpan::new("src/v3/std/list.dag", 3259, 3272) }, Declaration { id: DeclarationId(1042), name: None, connective: TypeConnective::Instantiation { template: DeclarationId(455), arguments: vec![TemplateArgument { parameter: DeclarationId(456), value: DeclarationId(460) }] }, type_params: vec![], meta_tag: None, inhabits: None, value_body: None, refinement: None, span: SourceSpan::new("src/v3/std/list.dag", 3421, 3434) }, Declaration { id: DeclarationId(1043), name: None, connective: TypeConnective::Instantiation { template: DeclarationId(455), arguments: vec![TemplateArgument { parameter: DeclarationId(456), value: DeclarationId(460) }] }, type_params: vec![], meta_tag: None, inhabits: None, value_body: None, refinement: None, span: SourceSpan::new("src/v3/std/list.dag", 3439, 3452) }, diff --git a/src/v3/compiler/tests/integration.rs b/src/v3/compiler/tests/integration.rs index fb3bd31848b..8c674eff29d 100644 --- a/src/v3/compiler/tests/integration.rs +++ b/src/v3/compiler/tests/integration.rs @@ -15,13 +15,13 @@ //! test-binary production. //! //! **Module discipline.** Each file under `tests/integration/*.rs`, -//! `tests/boundary/*.rs`, or `tests/unit/*.rs` is a sibling module at this -//! crate root, reached via `#[path]` because Rust's default module -//! resolution for a crate-root file looks in the containing directory -//! (`tests/`) rather than a same-named subdirectory. Shared helpers live -//! under `tests/integration/common/`. Inside a test module, -//! `use crate::common::…` reaches those helpers; there is no per-file -//! `mod common;` declaration. +//! `tests/integration/cementing/*.rs`, `tests/boundary/*.rs`, or +//! `tests/unit/*.rs` is a sibling module at this crate root, reached via +//! `#[path]` because Rust's default module resolution for a crate-root file +//! looks in the containing directory (`tests/`) rather than a same-named +//! subdirectory. Shared helpers live under `tests/integration/common/`. +//! Inside a test module, `use crate::common::…` reaches those helpers; there +//! is no per-file `mod common;` declaration. //! //! **Layer taxonomy (TESTING.md § test layers).** Files are partitioned //! by directory: @@ -37,6 +37,8 @@ #[path = "integration/common/mod.rs"] mod common; +#[path = "integration/cementing/cementing_lens_registry_dispatch_test.rs"] +mod cementing_lens_registry_dispatch_test; #[path = "integration/four_fixture_regression_test.rs"] mod four_fixture_regression_test; #[path = "integration/l1_5_fixed_point_test.rs"] diff --git a/src/v3/compiler/tests/integration/cementing/cementing_lens_registry_dispatch_test.rs b/src/v3/compiler/tests/integration/cementing/cementing_lens_registry_dispatch_test.rs new file mode 100644 index 00000000000..627f36c0832 --- /dev/null +++ b/src/v3/compiler/tests/integration/cementing/cementing_lens_registry_dispatch_test.rs @@ -0,0 +1,479 @@ +//! **Layer:** integration +//! +//! Band-C cementing dispatch for lenses enumerated in `regen.dag` and +//! described in `docs/v3-lens-capability-register.md`. +//! +//! - `TESTING.md` — *Cementing tests (Band C — lens subsumption)* +//! - `src/v3/compiler/regen.dag` — header comment on cementing dispatch +//! +//! **Authority cross-check.** `CEMENTING_MODULES_FOR_V2_COMPLETE_CLAIMS` is not a +//! second source of truth: `cementing_escalation_slice_matches_capability_register` +//! derives the required registry `name` keys from `docs/v3-lens-capability-register.md` +//! (capability table) plus `regen.dag` (`LensRegistryEntry` rows) and asserts the +//! slice matches exactly. +//! +//! **Provenance cementing split.** The `compile_to_dag` exemplar below compares +//! `origin_of` to a **local** five-way `Behavior → Origin` projection that mirrors +//! `provenance.dag`’s emitted `origin_for_behavior` (kept crate-private). That keeps +//! the public `lens_provenance` surface to `origin_of` / `Origin` only. This is a +//! **seam** check (`port` → `produced_by` → `node` → classification) on the live +//! lowering path; the `Behavior → Origin` table is also pinned under +//! `#[cfg(test)]` in `lib.rs::lens_provenance::tests` with minimal hand-built `Dag` +//! shapes (see `TESTING.md` — v3-native `COMPLETE` path). Regenerate / update the +//! local mirror when `provenance.dag` changes that table. + +use std::collections::BTreeSet; +use std::path::{Path, PathBuf}; + +use crate::common::{ + integration_rs_active_line_contains, integration_rs_cementing_path_attr_binds_mod_stem, +}; +use v3_compiler::compile_to_dag; +use v3_compiler::dag::{Behavior, Dag, Declaration, FieldValue, LiteralBits, ValueBody}; +use v3_compiler::lens_provenance::{origin_of, Origin}; + +/// Structural equality for the full published `Origin` carrier (every variant). +/// `Origin` is generated without `PartialEq`; this is the integration-test oracle. +fn assert_origin_carriers_equal(a: &Origin, b: &Origin, context: &str) { + match (a, b) { + (Origin::NoProducer, Origin::NoProducer) => {} + (Origin::MissingPort, Origin::MissingPort) => {} + (Origin::MissingBehavior, Origin::MissingBehavior) => {} + (Origin::Source { _0: x }, Origin::Source { _0: y }) => { + assert_eq!(x, y, "{context}: Source NodeId mismatch") + } + (Origin::Computed { _0: x }, Origin::Computed { _0: y }) => { + assert_eq!(x, y, "{context}: Computed NodeId mismatch") + } + (Origin::Selected { _0: x }, Origin::Selected { _0: y }) => { + assert_eq!(x, y, "{context}: Selected NodeId mismatch") + } + (Origin::Accumulated { _0: x }, Origin::Accumulated { _0: y }) => { + assert_eq!(x, y, "{context}: Accumulated NodeId mismatch") + } + _ => panic!("{context}: full Origin carrier mismatch\n got: {a:?}\n exp: {b:?}"), + } +} + +/// `origin_of` must agree with the provenance lens’s `Behavior → Origin` table on +/// the value port's producer after the `produced_by` walk. +/// +/// The table is duplicated here (not re-exported from the crate) so integration +/// tests do not widen the public API with `origin_for_behavior`. Keep this match in +/// lockstep with `src/v3/lenses/provenance.dag` / `lens_provenance_generated.rs`. +/// Independent minimal-`Dag` pins live in `lens_provenance::tests`. +fn assert_provenance_origin_matches_lens_authority(dag: &Dag, bind_name: &str, context: &str) { + let port = find_bind_value_port(dag, bind_name); + let got = origin_of(dag, &port); + let produced_by = dag + .port_opt(&port) + .unwrap_or_else(|| panic!("{context}: missing port for bind `{bind_name}`")) + .produced_by + .unwrap_or_else(|| { + panic!("{context}: bind `{bind_name}` value port has no producer (expected one)") + }); + let behavior = dag + .nodes() + .iter() + .find(|b| b.id() == produced_by) + .unwrap_or_else(|| panic!("{context}: missing producer node {produced_by:?}")); + let expected = expected_origin_from_producer_behavior(behavior); + assert_origin_carriers_equal(&got, &expected, context); +} + +/// Mirrors `origin_for_behavior` from `provenance.dag` (see `lens_provenance_generated.rs`). +fn expected_origin_from_producer_behavior(behavior: &Behavior) -> Origin { + match behavior { + Behavior::Value(v) => Origin::Source { _0: v.id }, + Behavior::Transform(t) => Origin::Computed { _0: t.id }, + Behavior::Branch(b) => Origin::Selected { _0: b.id }, + Behavior::Loop(l) => Origin::Accumulated { _0: l.id }, + Behavior::Bind(bind) => Origin::Source { _0: bind.id }, + } +} + +/// Pairs of (`regen_lens --lens ` registry key, cementing module stem +/// under `tests/integration/cementing/` without `.rs`). +/// +/// **Must match register + regen mechanically** — see +/// `cementing_escalation_slice_matches_capability_register`. Append when the +/// capability table row is plain `COMPLETE` (not `**PROXY**` / `**STUB**` / `N/A`) +/// **and** the v2 counterpart cell names a real v2 path (not `None (v3-native)` / +/// not `N/A` per `TESTING.md` Band-C). Land the new +/// `cementing/.rs` module and a `#[path = ...]` line in +/// `tests/integration.rs` in the same PR. +const CEMENTING_MODULES_FOR_V2_COMPLETE_CLAIMS: &[(&str, &str)] = &[]; + +fn workspace_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(3) + .expect("expected src/v3/compiler -> workspace root") + .to_path_buf() +} + +fn md_table_cells(line: &str) -> Vec { + // Capability rows escape `|` inside cells as `\|` (see v3 output column). + let tmp = line.replace("\\|", "\u{241f}"); + tmp.split('|') + .map(|s| s.replace('\u{241f}', "|").trim().to_string()) + .collect() +} + +/// Band-C register column: v2 counterpart exists for cross-implementation cementing. +/// Must match `TESTING.md` (Cementing tests): treat `None (v3-native)` and `N/A` +/// as absent — same policy as prose, not a second authority. +fn register_row_has_real_v2_counterpart(v2_cell: &str) -> bool { + let v2 = v2_cell.trim().trim_matches('`'); + !(v2.contains("None (v3-native)") || v2 == "N/A") +} + +#[test] +fn register_row_has_real_v2_counterpart_matches_testing_md_band_c() { + assert!(!register_row_has_real_v2_counterpart("None (v3-native)")); + assert!(!register_row_has_real_v2_counterpart( + " None (v3-native) " + )); + assert!(!register_row_has_real_v2_counterpart("N/A")); + assert!(!register_row_has_real_v2_counterpart(" `N/A` ")); + assert!(register_row_has_real_v2_counterpart( + "src/v2/complexity.dag (5488L)" + )); +} + +#[test] +fn integration_rs_active_line_contains_rejects_commented_cementing_path() { + let src = r#" +// #[path = "integration/cementing/ghost.rs"] +// mod ghost; +#[path = "integration/cementing/real.rs"] +mod real; +"#; + assert!(!integration_rs_active_line_contains( + src, + r#"#[path = "integration/cementing/ghost.rs"]"#, + )); + assert!(integration_rs_active_line_contains( + src, + r#"#[path = "integration/cementing/real.rs"]"#, + )); +} + +#[test] +fn integration_rs_active_line_contains_rejects_block_commented_cementing_path() { + let src = r#"/* +#[path = "integration/cementing/ghost.rs"] +mod ghost; +*/ +#[path = "integration/cementing/real.rs"] +mod real; +"#; + assert!(!integration_rs_active_line_contains( + src, + r#"#[path = "integration/cementing/ghost.rs"]"#, + )); + assert!(integration_rs_active_line_contains( + src, + r#"#[path = "integration/cementing/real.rs"]"#, + )); +} + +#[test] +#[should_panic(expected = "raw/byte string literal")] +fn integration_rs_active_line_contains_panics_on_raw_string_in_code() { + let src = concat!("fn f() { r#\"", "hi", "\"#; }"); + integration_rs_active_line_contains(src, "nope"); +} + +#[test] +fn integration_rs_cementing_path_attr_binds_mod_accepts_multiline() { + let src = concat!( + "#[path = \"integration/cementing/real.rs\"]\n", + "mod real;\n", + ); + assert!(integration_rs_cementing_path_attr_binds_mod_stem( + src, "real" + )); +} + +#[test] +fn integration_rs_cementing_path_attr_binds_mod_accepts_same_line() { + let src = "#[path = \"integration/cementing/real.rs\"] mod real;\n"; + assert!(integration_rs_cementing_path_attr_binds_mod_stem( + src, "real" + )); +} + +#[test] +fn integration_rs_cementing_path_attr_binds_mod_accepts_interleaved_attribute() { + let src = concat!( + "#[path = \"integration/cementing/real.rs\"]\n", + "#[allow(dead_code)]\n", + "mod real;\n", + ); + assert!(integration_rs_cementing_path_attr_binds_mod_stem( + src, "real" + )); +} + +#[test] +fn integration_rs_cementing_path_attr_binds_mod_rejects_mismatched_mod_name() { + let src = concat!( + "#[path = \"integration/cementing/real.rs\"]\n", + "mod decoy;\n\n", + "mod real;\n", + ); + assert!(!integration_rs_cementing_path_attr_binds_mod_stem( + src, "real" + )); +} + +#[test] +fn integration_rs_cementing_path_attr_binds_mod_rejects_stem_extra_mod_name() { + let src = concat!( + "#[path = \"integration/cementing/stem.rs\"]\n", + "mod stem_extra;\n", + ); + assert!(!integration_rs_cementing_path_attr_binds_mod_stem( + src, "stem" + )); +} + +#[test] +fn integration_rs_active_line_ignores_needle_inside_string_literal() { + // Tombstone text appears only inside a `"..."` string — must not satisfy the ratchet. + let src = concat!( + "let _ = \"#[path = \\\"integration/cementing/decoy.rs\\\"]\";\n", + "#[path = \"integration/cementing/real.rs\"]\n", + "mod real;\n", + ); + assert!(!integration_rs_active_line_contains( + src, + r#"#[path = "integration/cementing/decoy.rs"]"#, + )); + assert!(integration_rs_active_line_contains( + src, + r#"#[path = "integration/cementing/real.rs"]"#, + )); +} + +#[test] +fn md_table_cells_preserves_escaped_pipes_for_register_capability_rows() { + // Pin the markdown-as-data split: cementing escalation reads + // `docs/v3-lens-capability-register.md` through this helper (see review on #638). + let row = "| a.dag | TERMINAL | COMPLETE | v2 path | FoundCost(Int) \\| MissingCost | note |"; + let cells = md_table_cells(row); + assert!( + cells.len() >= 5, + "capability ratchet expects Lens, Structural, Behavioral, v2, … columns; got {cells:?}" + ); + assert_eq!(cells[1].trim(), "a.dag"); + assert_eq!(cells[3].trim(), "COMPLETE"); + assert_eq!(cells[4].trim(), "v2 path"); + assert_eq!( + cells[5].trim(), + "FoundCost(Int) | MissingCost", + "cell-internal `|` must survive `\\|` escaping (v3 output column)" + ); +} + +/// Lens basenames (e.g. `complexity.dag`) whose register row is `COMPLETE` with a +/// real v2 counterpart (excludes `None (v3-native)` and `N/A` — `TESTING.md`). +fn lens_basenames_requiring_v2_cementing_from_register_md() -> BTreeSet { + let md_path = workspace_root().join("docs/v3-lens-capability-register.md"); + let md = std::fs::read_to_string(&md_path) + .unwrap_or_else(|e| panic!("read {}: {e}", md_path.display())); + let mut basenames = BTreeSet::new(); + for raw in md.lines() { + let line = raw.trim(); + if !line.starts_with('|') || line.starts_with("|---") || line.contains("---|---") { + continue; + } + let cells = md_table_cells(line); + if cells.len() < 5 { + continue; + } + let lens_cell = cells[1].trim(); + if lens_cell == "Lens" || !lens_cell.contains(".dag") { + continue; + } + let behavioral = cells[3].trim(); + let v2 = cells[4].trim(); + if behavioral != "COMPLETE" { + continue; + } + if !register_row_has_real_v2_counterpart(v2) { + continue; + } + let basename = lens_cell.trim_matches('`').trim(); + if !basename.ends_with(".dag") { + continue; + } + basenames.insert(basename.to_string()); + } + basenames +} + +fn structural_fields(decl: &Declaration) -> &[(String, FieldValue)] { + let Some(ValueBody::Structural { fields }) = &decl.value_body else { + panic!( + "lens registry entry `{}` must carry a structural value body", + decl.name.as_deref().unwrap_or("") + ); + }; + fields.as_slice() +} + +fn string_field(fields: &[(String, FieldValue)], label: &str, binding: &str) -> String { + fields + .iter() + .find(|(field_label, _)| field_label == label) + .and_then(|(_, value)| match value { + FieldValue::Literal(LiteralBits::String(s)) => Some(s.clone()), + _ => None, + }) + .unwrap_or_else(|| { + panic!("lens registry entry `{binding}` is missing a String `{label}` field") + }) +} + +fn read_lens_registry_name_lens_file_pairs(dag: &Dag) -> Vec<(String, String)> { + let entry_type_id = dag + .declaration_by_name("LensRegistryEntry") + .map(|decl| decl.id) + .expect("regen.dag must declare `LensRegistryEntry`"); + + dag.declarations() + .iter() + .filter(|decl| decl.meta_tag == Some(entry_type_id)) + .map(|decl| { + let binding = decl + .name + .clone() + .unwrap_or_else(|| "".to_string()); + let fields = structural_fields(decl); + ( + string_field(fields, "name", &binding), + string_field(fields, "lens_file", &binding), + ) + }) + .collect() +} + +/// `regen_lens --lens ` keys that must have cementing modules per the +/// capability register escalation rule. +fn registry_names_required_by_register_and_regen() -> BTreeSet { + let basenames = lens_basenames_requiring_v2_cementing_from_register_md(); + let dag = Dag::new(); + assert!( + dag.diagnostics().is_empty(), + "bootstrap should load `src/v3/compiler/regen.dag` cleanly, got {:?}", + dag.diagnostics().iter().collect::>() + ); + let rows = read_lens_registry_name_lens_file_pairs(&dag); + let mut names = BTreeSet::new(); + let mut matched_basenames = BTreeSet::new(); + for (name, lens_file) in rows { + let basename = Path::new(&lens_file) + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or_else(|| { + panic!("registry entry `{name}` has lens_file without basename: {lens_file}") + }); + if basenames.contains(basename) { + names.insert(name); + matched_basenames.insert(basename.to_string()); + } + } + let missing: Vec<_> = basenames.difference(&matched_basenames).cloned().collect(); + assert!( + missing.is_empty(), + "docs/v3-lens-capability-register.md escalates v2 cementing for lens basenames {missing:?}, \ + but no `LensRegistryEntry` in src/v3/compiler/regen.dag names those files — \ + fix the register table or add registry entries." + ); + names +} + +fn integration_rs_text() -> String { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("integration.rs"); + std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())) +} + +fn assert_cementing_stem_wired_in_integration_rs( + integration_rs: &str, + stem: &str, + registry_name: &str, +) { + let expected = format!(r#"#[path = "integration/cementing/{stem}.rs"]"#); + assert!( + integration_rs_cementing_path_attr_binds_mod_stem(integration_rs, stem), + "registry lens `{registry_name}` lists cementing stem `{stem}` but \ + `tests/integration.rs` does not bind `{expected}` to `mod {stem};` in the same item \ + (Band-C dispatch — not a stray `#[path]` plus an unrelated `mod` line). \ + Land both lines in the same PR as the on-disk `cementing/{stem}.rs` module." + ); +} + +fn find_bind_value_port(dag: &v3_compiler::dag::Dag, name: &str) -> v3_compiler::dag::PortId { + dag.nodes() + .iter() + .filter_map(Behavior::as_bind) + .find(|bind| bind.name == name) + .unwrap_or_else(|| panic!("bind `{name}` not found")) + .value +} + +#[test] +fn provenance_origin_of_cements_complete_row_via_compile_to_dag_fixture() { + // Register row: `provenance.dag` — BEHAVIORALLY COMPLETE, v3-native. + // Integration crate cannot reach `Dag`'s `pub(crate)` builder helpers + // (`alloc_port_with_shape`); `compile_to_dag` fixtures still cement the + // shipped `origin_of` **walk + glue** on the live lowering path. The five-way + // `Behavior → Origin` mirror in this file stays in sync with `provenance.dag`; + // richer mapping pins stay in-crate unit tests. + // Exhaustive `NoProducer` / `Missing*` cases stay in `lib.rs::lens_provenance::tests`. + let dag = compile_to_dag("let lit: Int = 7", "cementing_provenance_lit.v3").expect("compiles"); + assert_provenance_origin_matches_lens_authority(&dag, "lit", "cementing_provenance_lit"); + + let dag = + compile_to_dag("let sum: Int = 1 + 2", "cementing_provenance_sum.v3").expect("compiles"); + assert_provenance_origin_matches_lens_authority(&dag, "sum", "cementing_provenance_sum"); +} + +#[test] +fn cementing_escalation_slice_matches_capability_register() { + let expected = registry_names_required_by_register_and_regen(); + let declared: BTreeSet<&str> = CEMENTING_MODULES_FOR_V2_COMPLETE_CLAIMS + .iter() + .map(|(name, _)| *name) + .collect(); + let expected_refs: BTreeSet<&str> = expected.iter().map(String::as_str).collect(); + assert_eq!( + declared, expected_refs, + "`CEMENTING_MODULES_FOR_V2_COMPLETE_CLAIMS` must list exactly the registry `name` keys \ + for rows where docs/v3-lens-capability-register.md marks `COMPLETE` with a real v2 \ + counterpart (not `None (v3-native)` / not `N/A` per `TESTING.md`) — update the slice \ + (and cementing modules + `#[path]` wiring) in the same PR as the register promotion." + ); +} + +#[test] +fn cementing_test_modules_exist_for_escalated_v2_complete_registry_claims() { + let cementing_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("integration") + .join("cementing"); + let integration_rs = integration_rs_text(); + for (registry_name, stem) in CEMENTING_MODULES_FOR_V2_COMPLETE_CLAIMS { + let path = cementing_dir.join(format!("{stem}.rs")); + assert!( + path.is_file(), + "registry lens `{registry_name}` is listed for v2-complete cementing; expected cementing module at {}", + path.display() + ); + assert_cementing_stem_wired_in_integration_rs(&integration_rs, stem, registry_name); + } +} diff --git a/src/v3/compiler/tests/integration/common/mod.rs b/src/v3/compiler/tests/integration/common/mod.rs index a287820c6d7..0d7af70ea60 100644 --- a/src/v3/compiler/tests/integration/common/mod.rs +++ b/src/v3/compiler/tests/integration/common/mod.rs @@ -75,6 +75,254 @@ pub fn find_current_rlib(crate_name: &str) -> PathBuf { .expect("compiled rlib for current crate") } +/// `true` when `bytes[i..]` opens a `b"…"`, `br#"…"#`, `r#"…"#`, or `r"…"` literal in +/// Rust source. The wiring scanner does not model these — fail loud if they appear +/// in `Code` so Band-C ratchets cannot silently mis-scan `tests/integration.rs`. +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, + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum IntegrationRsScan { + Code, + LineComment, + BlockComment(u32), + String, +} + +fn integration_rs_panic_on_raw_string(bytes: &[u8], i: usize) { + if code_opens_raw_or_byte_string_literal(bytes, i) { + panic!( + "integration_rs wiring scan: raw/byte string literal at byte offset {i} \ + (e.g. r\"…\", r#\"…\"#, b\"…\", br#\"…\"#). Extend IntegrationRsScan before scanning this file." + ); + } +} + +/// Byte offsets of `needle` in `integration_rs` that occur in **Code** (outside +/// `//`, `/* */`, and `"…"` strings). See [`integration_rs_active_line_contains`]. +fn integration_rs_code_substring_positions(integration_rs: &str, needle: &str) -> Vec { + let mut hits = Vec::new(); + if needle.is_empty() { + hits.push(0); + return 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_panic_on_raw_string(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[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; + } + }, + } + } + 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) -> bool { + 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 true; + } + } + false +} + +/// True when `needle` appears in `integration_rs` **outside** Rust line comments +/// (`//` … including `///` / `//!`), **nested** block comments (`/* … */`), and +/// normal `"…"` string literals. +/// +/// 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#"…"#`), or +/// char literals — none appear in today’s `tests/integration.rs` module list. If +/// they do, extend `IntegrationRsScan` **or** the scan will **panic** when the +/// raw/byte-string opener probe fires in `Code` (loud failure vs a silent false green). +pub fn integration_rs_active_line_contains(integration_rs: &str, needle: &str) -> bool { + !integration_rs_code_substring_positions(integration_rs, needle).is_empty() +} + /// Harness for spawning rustc against generated-Rust harnesses. /// Holds the per-test-binary scratch root and a counter so parallel /// tests within the same binary get distinct temp dirs. diff --git a/src/v3/compiler/tests/integration/parse_corpus_manifest.txt b/src/v3/compiler/tests/integration/parse_corpus_manifest.txt index db2faa455ea..c3f60355270 100644 --- a/src/v3/compiler/tests/integration/parse_corpus_manifest.txt +++ b/src/v3/compiler/tests/integration/parse_corpus_manifest.txt @@ -11,7 +11,7 @@ dsl/std/types.dag 108 137815 42478679b0276003 src/v3/compiler/operators.dag 5 47160 b75a91c65bf8a242 src/v3/compiler/parse_tables.dag 29 69841 c2db27c9e6efa073 src/v3/compiler/pipeline.dag 26 47032 a13b41aa379fd167 -src/v3/compiler/regen.dag 11 30786 01fdfda0b031563d +src/v3/compiler/regen.dag 11 30786 49f47c59c626d776 src/v3/compiler/runtime_mirrors.dag 17 95346 ec2f9c233bfc9d26 src/v3/compiler/tests/four_fixture_pressure/drop.v3 2 2517 00953bc6b6d8c811 src/v3/compiler/tests/four_fixture_pressure/id.v3 2 2452 ec8b40edc3d3c017 diff --git a/src/v3/compiler/tests/integration/sg0_census_test.rs b/src/v3/compiler/tests/integration/sg0_census_test.rs index cdf7c7fc330..f364748bfab 100644 --- a/src/v3/compiler/tests/integration/sg0_census_test.rs +++ b/src/v3/compiler/tests/integration/sg0_census_test.rs @@ -180,6 +180,7 @@ const EXPECTED_HAND_AUTHORED: &[&str] = &[ "src/v3/compiler/tests/boundary/m2_emit_multi_field_struct_variant_test.rs", "src/v3/compiler/tests/determinism_test.rs", "src/v3/compiler/tests/integration.rs", + "src/v3/compiler/tests/integration/cementing/cementing_lens_registry_dispatch_test.rs", "src/v3/compiler/tests/integration/common/budgeted.rs", "src/v3/compiler/tests/integration/common/cached_compile.rs", "src/v3/compiler/tests/integration/common/determinism_fixtures.rs", diff --git a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs index 696e69ea750..c1e339a9650 100644 --- a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs +++ b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs @@ -53,10 +53,12 @@ // `lens_path()` helpers, or the `include_str!` targets diverge. use std::collections::{BTreeSet, HashMap}; +use std::fs; use std::io::Write; use std::path::PathBuf; use std::process::{Command, Stdio}; +use crate::common::integration_rs_cementing_path_attr_binds_mod_stem; use v3_compiler::compile_to_dag; use v3_compiler::dag::{Dag, Declaration, FieldValue, LiteralBits, ValueBody}; use v3_compiler::emit_rust::emit_rust_module; @@ -455,6 +457,25 @@ fn sg6_lens_registry_names_resolve_to_singleton_entry() { } } +// Band-C cementing: `cementing_lens_registry_dispatch_test.rs` cannot assert its own +// `#[path = ...]` wiring from inside the module — if the `mod` line is deleted, the +// whole subtree is dropped before any in-module test runs. SG-6 owns integration +// harness glue checks; this ratchet lives here so CI stays honest. +#[test] +fn sg6_cementing_lens_registry_dispatch_module_is_wired_in_integration_rs() { + let path = manifest_dir().join("tests").join("integration.rs"); + let text = fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())); + assert!( + integration_rs_cementing_path_attr_binds_mod_stem( + &text, + "cementing_lens_registry_dispatch_test", + ), + "{} must bind `#[path = \"integration/cementing/cementing_lens_registry_dispatch_test.rs\"]` \ + to `mod cementing_lens_registry_dispatch_test;` as one item (Band-C dispatch).", + path.display(), + ); +} + #[test] fn sg6_infer_helpers_generated_module_matches_checked_in_snapshot() { let (_dag, rows) = load_registry();