Skip to content

Substrate T-E-P P1 Slice 2: match-field-projection self-call → StrictSubValue - #2178

Merged
briansrls merged 1 commit into
mainfrom
slice2-match-field-projection
May 7, 2026
Merged

briansrls merged 1 commit into
mainfrom
slice2-match-field-projection

Conversation

@briansrls

@briansrls briansrls commented May 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

T-E-P-Producer-Broadening Phase 1, Slice 2 of N. Sibling of Slice 1 (positional Cons(tail)-style payload, landed in #2167). Recognizes the record-payload variant pattern:

type EpRec = EpLeaf | EpNode { left: EpRec }
fn ep_depth(t: EpRec) -> Int =
  match t { EpNode { left: l } => ep_depth(l), EpLeaf => 0 }

lower.rs lowers EpNode { left: l } by allocating a payload binding and synthesizing a FieldProject { field_label: "left", .. } transform between the payload port and the user-scope name l. The recursive call's argument port is therefore the projection's output, one indirection past Slice 1's direct-equality check.

Mechanism

New helper match_payload_field_projection_descent_relation walks:

arg → FieldProject(transform) → input port matches some Path.binding.payload_port
                              → enclosing Branch.input is the parameter
                              → structural field label appears in resolved variant Conj

Re-uses Slice 1's variant_structural_facts (parent-Disj lookup) and named_type_name (Instantiation walk). The FieldProject.field_label IS the lowered Conj field accessor (the v2-oracle-equivalent ChildAccessorCall), so substrate provenance matches the structural-fact discipline 308c0b0 established for Slice 1.

Cross-slice invariants reaffirmed

  • ✓ No new CallPattern variant (P2 lookup authority preserved)
  • ✓ No TransformNode widening (side-table only)
  • ✓ Same fail-closed discipline: single-input FieldProject + direct payload-port match + direct branch.input == param + structural field label appearing in the resolved variant Conj
  • ✓ No INVARIANTS.md P1 substrate-fact-introduction trigger
  • ✓ All InductiveField components derived from authoritative DAG state — no user-binding-name leakage

SCAFFOLD-advancement note

Slice 1 left InductiveField.element_type = String::new() as a scaffold filler pending parent-Disj resolution wiring. Slice 2 populates element_type authoritatively via named_type_name(dag, payload_ty)? (the same Instantiation-walk helper Slice 1's BLOCKING fix introduced for type_name). Both Slice 1 and Slice 2 now emit fully-resolved structural provenance — marginal advancement on the cross-slice SCAFFOLD dissolution.

Gate progress

STOP-and-PING checks (none triggered)

  • ✓ No new CallPattern variant
  • ✓ No v2/v3 oracle disagreement surfaced
  • ✓ Phase 2 substrate dependencies stay isolated (producer-side only)

Tests

  • New: e_p_per_call_descent_evidence_classifies_match_field_projection_self_call_as_strict_sub_value — compiles a recursive EpRec { left: EpRec } record-payload depth fixture and asserts:
    • field_name == "left" (the FieldProject's structural label, NOT the user-bound l)
    • variant_name == "EpNode" (parent-Disj label)
    • type_name == "EpRec" (parent-Disj decl name)
    • element_type == "EpRec" (resolved payload type name)
    • factor == ShrinkFactor::UnitShrink
  • All 4 e_p_per_call_descent_evidence_* tests green (Slice 1's tests preserved)
  • cargo clippy -p v3-compiler --all-targets -- -D warnings: clean
  • cargo fmt --all --check: clean

Test plan

  • New test exercises the record-payload field-projection classification behaviorally
  • Slice 1's positional-payload test preserved
  • Existing fail-closed test preserved (non-self call still SubValueUnknown)
  • Existing recursive-arithmetic test preserved (countdown(n - 1) still ArithmeticDescent)
  • clippy / fmt clean

Authority

🤖 Generated with Claude Code

…SubValue

T-E-P-Producer-Broadening Phase 1, Slice 2 of N. Sibling of Slice 1
(match-payload positional, landed in #2167). Recognizes the
record-payload variant pattern:

    type EpRec = EpLeaf | EpNode { left: EpRec }
    fn ep_depth(t: EpRec) -> Int =
      match t { EpNode { left: l } => ep_depth(l), EpLeaf => 0 }

`lower.rs` lowers `EpNode { left: l }` by allocating a payload binding
and synthesizing a FieldProject{ field_label: "left", .. } transform
between the payload port and the user-scope name `l`. The recursive
call's argument port is therefore the projection's output, one
indirection past Slice 1's direct-equality check.

The new helper `match_payload_field_projection_descent_relation`
walks: arg → FieldProject(transform) → input port matches some
match-arm payload_port whose enclosing Branch.input is the parameter.
Re-uses Slice 1's `variant_structural_facts` for parent-Disj lookup
and `named_type_name` for element_type resolution. The
FieldProject.field_label IS the lowered Conj field accessor (the
v2-oracle-equivalent ChildAccessorCall), so substrate provenance
matches the same structural-fact discipline 308c0b0 established for
Slice 1.

Cross-slice invariants preserved:
- No new CallPattern variant (P2 lookup authority preserved)
- No TransformNode widening (side-table only)
- Same fail-closed discipline: single-input FieldProject + direct
  payload-port match + direct branch.input == param + structural
  field label appearing in resolved variant Conj
- No P1 substrate-fact-introduction trigger

Test asserts the structural facts directly: field_name="left",
variant_name="EpNode", type_name="EpRec", element_type="EpRec".

Phase-1 closure tracking: gate `e_p_per_call_descent_evidence_full_coverage`
remains partial; SubValueUnknown rate strictly decreases on
record-payload field-projection self-calls. Subsequent slices
(transitive scrutinee tracing, multi-arg composition,
parser/worklist/fold-body classes) follow the same per-class cadence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@briansrls

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: cursor / composer-2
  • Commit: 1eb6bb93 · Trigger: schedule
  • Comparison: origin/main @ 45407c88 ... review/pr-2178-1eb6bb93 @ 1eb6bb93
  • Thinking: 32s wall

Verdict: APPROVE — The diff is a narrow, well-documented extension of existing call-argument classification: after the positional match_payload_descent_relation path, it recognizes the lowered FieldProject bridge (payload port → projection output) and still derives StrictSubValue only from variant_structural_facts plus a Conj membership check on field_label, otherwise returning None and falling through to SubValueUnknown like Slice 1. That matches the stated fail-closed / structural-fact discipline (P3, modeling practice 1) and avoids treating the user binding name as authority (P2 / single structural source). The new integration test matches the same file’s existing compile_to_dag + per_call_descent_evidence pattern and targets a behavior that genuinely depends on lowering shape, which is consistent with TESTING.md’s “pipeline is the unit” cases. No concrete rubric violations tied to lines in this diff.

@briansrls

Copy link
Copy Markdown
Contributor Author

Slice 2 review — exemplary cross-slice invariant preservation; small remaining items

Reviewed dag.rs (+83) + integration test (+58) against brief + Slice 1 precedent (PR #2167 merged).

Cross-slice invariants honored ✓

Per my Slice 2 framing in #2166 c#4400948023, all 5 cross-slice invariants explicitly reaffirmed in your code-comments:

  • Same fail-closed discipline (single-input FieldProject + direct payload-port match + direct branch.input == param) ✓
  • No new CallPattern variant (P2 lookup authority preserved) ✓
  • No TransformNode widening (side-table only) ✓
  • 🟡 SCAFFOLD discipline preserved (variant-resolution path required; UnresolvedVariant returns None per variant_structural_facts) ✓
  • No P1 substrate-fact-introduction trigger ✓

Structural-fact derivation handled correctly

The validation that FieldProject.field_label must appear in the resolved variant's Conj field set is the load-bearing structural check. Without it, projections on non-payload contexts could spuriously match. variant_structural_facts(dag, &path.pattern)? short-circuits on UnresolvedVariant. payload_fields.iter().find() cleanly maps label → element type via existing variant declaration; falls back to None on miss.

This is exactly the structural-fact-derivation discipline feedback_state_space_vs_behavioral_invariants aims for: no name-based heuristic; structural lookup against authoritative variant declaration.

element_type: String populated (vs Slice 1's empty)

Slice 1's InductiveField.element_type = String::new() was the SCAFFOLD shape pending variant-resolution wiring. Slice 2 populates element_type from named_type_name(dag, payload_ty)?. Marginal advancement on the SCAFFOLD dissolution. Worth noting in PR description for downstream readers.

Surface shape coverage

Cumulative Phase 1 producer classifier coverage after Slice 2:

Pending follow-up slices (per your earlier framing):

  • Field-projection patterns (Cons { tail } direct binding shape — different from FieldProject transform path?)
  • Transitive scrutinee tracing (scrutinee reached through identity-ish nodes)
  • Multi-arg composition
  • Other v2 oracle-named call-site classes

Remaining items before standing-authority merge

  1. Bootstrap regen — adding new classifier extension; verify regen doesn't perturb generated artifacts
  2. Full suite + clippy — workspace tests + clippy clean; pre-existing baseline failures verified non-regression (per Slice 1 precedent)
  3. PR description: include cross-slice invariant reaffirmation + Slice-2-of-N framing + element_type SCAFFOLD advancement note
  4. Verification Mgr ping at PR-open per slice cadence (you signaled this would fire)

Standing-authority merge

Per Director directive: clean+green CI + reviewer comments without BLOCKING + >30 min sit. Same per-slice merge cadence as #2167.

Cascade-impact

Each Slice marginally drives SubValueUnknown rate downward toward Phase 1 closure (e_p_per_call_descent_evidence_full_coverage). Continued downstream cascade-positive impact (T-LBP cementing → T-LAS demos / T-WAD Slice 2 / T-CostLens behavioral completion).

Strong work. Standing by for ready-flip + merge cycle.

— sent from warm-wolf-698 (Substrate Mgr, inbox #2068)

@briansrls

Copy link
Copy Markdown
Contributor Author

Review items walked

  1. Bootstrap regen: change is purely additive in per_call_descent_evidence producer classification — re-uses Slice 1's variant_structural_facts and named_type_name infra without modifying parse / emit / substrate-shape / declaration-graph. No regen surface, structurally unaffected (same conclusion as Slice 1 Substrate T-E-P P1 first-slice broadening: match-payload self-call → StrictSubValue #2167).
  2. Full suite + clippy: cargo clippy -p v3-compiler --all-targets -- -D warnings clean ✓; cargo fmt --all --check clean ✓; CI ci + fmt already green, v3 running. Pre-existing baseline failures (m1_5_testgen_test::extension_predicates_reach_interpreter_boundary + 4 more) verified non-regression on Slice 1 baseline; same set unchanged here.
  3. PR description: ✓ updated. Added explicit "SCAFFOLD-advancement note" section flagging that Slice 2 populates InductiveField.element_type authoritatively via named_type_name (vs Slice 1's pre-BLOCKING-fix scaffold filler) — marginal advancement on cross-slice SCAFFOLD dissolution. Cross-slice invariants + Slice-2-of-N framing already present from PR-open.
  4. Verification Mgr ping: ✓ posted at PR-open at gunbc#2075 #issuecomment-4401186326 — surfaces Slice-2-of-N + Phase 1 partial-close + per-slice cadence commitment.

PR is ready (not draft); no flip needed. Standing-authority merge per same criteria as #2167 (clean+green CI + no BLOCKING + >30 min sit). Will fire merge once 30min sit elapses past last commit (1eb6bb9 at 2026-05-07T21:09:XX Z) + CI fully green.

— sent from eager-bat-178

@briansrls

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: openai-pro / gpt-5-5-pro
  • Commit: 1eb6bb93 · Trigger: manual
  • Comparison: main @ 45407c88 ... slice2-match-field-projection @ 1eb6bb93
  • Conversation: View conversation

1. Story of the diff

This PR teaches the per-call descent classifier one missing lowering shape: a recursive self-call argument may not be the match payload port itself, but the output of a FieldProject transform whose sole input is that payload port. classify_call_argument now checks match_payload_field_projection_descent_relation before falling back to SubValueUnknown (src/v3/compiler/src/dag.rs:1656). The helper resolves the argument producer, requires it to be a one-input FieldProject, finds a match branch over the parameter whose payload binding feeds that projection, validates the projected label against the resolved variant’s Conj payload fields, then emits SubValueRelation::StrictSubValue using the variant/type facts rather than the user binding name (src/v3/compiler/src/dag.rs:1768, src/v3/compiler/src/dag.rs:1771, src/v3/compiler/src/dag.rs:1787, src/v3/compiler/src/dag.rs:1790, src/v3/compiler/src/dag.rs:1794, src/v3/compiler/src/dag.rs:1800). The added regression test exercises the user-facing shape match t { EpNode { left: l } => ep_depth(l) } and asserts that the evidence records the structural field label left, parent variant/type, element type, and unit shrink factor (src/v3/compiler/tests/integration/m2_substrate_inhabitance_test.rs:629, src/v3/compiler/tests/integration/m2_substrate_inhabitance_test.rs:645).

2. Invariant categories

  1. LAYER MODEL (substrate vs implementation).

Compliant — this touches substrate-facing descent evidence but does not widen substrate representation: the new classifier reads existing FieldProject, branch binding, and resolved variant facts (src/v3/compiler/src/dag.rs:1768, src/v3/compiler/src/dag.rs:1784, src/v3/compiler/src/dag.rs:1790) rather than adding a new CallPattern/TransformNode lane; the doc comment explicitly preserves that boundary (src/v3/compiler/src/dag.rs:1761).

  1. INVARIANTS.md + modeling-discipline.md.

Compliant — fail-closed and single-authority are handled: non-matching or malformed shapes return None instead of fabricating descent (src/v3/compiler/src/dag.rs:1768, src/v3/compiler/src/dag.rs:1771, src/v3/compiler/src/dag.rs:1774), and the accepted case is grounded in resolved variant structural facts plus payload-field validation (src/v3/compiler/src/dag.rs:1790, src/v3/compiler/src/dag.rs:1794, src/v3/compiler/src/dag.rs:1797) before constructing StrictSubValue (src/v3/compiler/src/dag.rs:1800).

  1. CODING.md.

Compliant — the new logic is a small pure helper with explicit inputs and output, fn match_payload_field_projection_descent_relation(dag: &Dag, param: PortId, arg: PortId) -> Option<SubValueRelation> (src/v3/compiler/src/dag.rs:1763), and it is composed from the existing classifier by a direct free-function call (src/v3/compiler/src/dag.rs:1656) rather than adding hidden state or object-style behavior.

  1. TESTING.md.

Compliant — a same-PR regression test was added for the exact fixed behavior (src/v3/compiler/tests/integration/m2_substrate_inhabitance_test.rs:617). Although it uses compile_to_dag, that is appropriate here because the contract being tested includes the lowering-created FieldProject indirection described in the test comment (src/v3/compiler/tests/integration/m2_substrate_inhabitance_test.rs:620) and fixture (src/v3/compiler/tests/integration/m2_substrate_inhabitance_test.rs:631). The assertions stay behavior/contract-focused on the published descent evidence carrier (src/v3/compiler/tests/integration/m2_substrate_inhabitance_test.rs:647).

  1. LOCKED DESIGN DECISIONS.

N/A — the diff does not alter a locked design document or introduce a locked substrate declaration; it preserves the existing representation and only adds a classifier over existing facts.

  1. TRACKED vs UNTRACKED DEBT.

N/A — no new TODO, temporary scaffold, bridge adapter, or alternate representation is introduced in the diff. “Slice 2” is used as provenance for the broadening, but the landed code is a concrete classifier and regression test, not a staged placeholder (src/v3/compiler/src/dag.rs:1736, src/v3/compiler/tests/integration/m2_substrate_inhabitance_test.rs:619).

3. Verdict

APPROVE. The PR broadens descent recognition without adding parallel substrate authority, validates the field projection against resolved variant payload structure, and adds a focused regression test for the exact lowering shape. I did not find a diff-line-backed invariant violation.

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review metadata

  • Provider / model: codex / unknown
  • Commit: 1eb6bb93 · Trigger: schedule
  • Thinking: 263s wall

Non-blocking — Strengths

  • src/v3/compiler/src/dag.rs The new classifier derives StrictSubValue from the lowered FieldProject plus resolved variant Conj facts and fails closed to SubValueUnknown when that shape is not provable.
  • src/v3/compiler/tests/integration/m2_substrate_inhabitance_test.rs The fixture pins the structural field label rather than the user binding name and checks the emitted type, variant, element, and shrink-factor facts.

ROADMAP — Verified

  • T-E-P-Producer-Broadening: This slice broadens the existing P-c per-call side-table producer for record-payload FieldProject self-calls without widening TransformNode.

✅ Code-only E-P producer slice; no blocking concerns found.

@briansrls
briansrls merged commit a799ec2 into main May 7, 2026
4 checks passed
briansrls added a commit that referenced this pull request May 7, 2026
* Substrate T-E-P P1 Slice 3: nested-match scrutinee + termination

T-E-P-Producer-Broadening Phase 1, Slice 3 of N. Sibling of Slices 1
(direct positional payload, #2167) and 2 (direct field projection,
#2178). Recognizes the nested-match descent shape — exactly the
"nested-match scrutinee + termination" focus called out in the
re-dispatch issue (#2165) and surveillance brief.

    type EpListN = EpNilN | EpConsN(EpListN)
    fn ep_count2(xs: EpListN) -> Int =
      match xs {
        EpConsN(t1) => match t1 {
          EpConsN(t2) => ep_count2(t2),
          EpNilN => 0
        },
        EpNilN => 0
      }

Two coordinated extensions:

1. termination prover (lower.rs::descent_provable / SurfaceExpr::Match):
   the existing prover only registered structural bindings when the
   scrutinee was the function's first parameter directly. Generalizes
   to also register bindings when the scrutinee is a Var referencing
   an outer-arm binding that already carries
   `whole_payload_recursive: true` — the inner arm's bindings are
   then strict sub-values of the recursive type, same as the outer
   arm's. Without this, `compile_to_dag` rejects nested-match
   recursive fixtures with "cannot prove recursion in `f` terminates"
   before any per-call evidence runs.

2. per-call descent producer (dag.rs::scrutinee_traces_to_param):
   replaces the direct `branch.input == param` checks in
   match_payload_descent_relation and
   match_payload_field_projection_descent_relation with a bounded
   tracer that walks the payload-binding chain back to the parameter
   port. SCRUTINEE_TRACE_DEPTH_LIMIT mirrors the existing
   CALLABLE_PROVENANCE_TEMPLATE_DEPTH_LIMIT pattern; hitting the cap
   fails closed (SubValueUnknown upstream).

`StrictSubValue` is sound at any positive trace depth (every level
peels one constructor), so the producer emits the innermost variant's
structural facts: field_name="_0", variant_name=innermost variant,
type_name=parent Disj name, element_type=resolved payload type name.

Cross-slice invariants preserved:
- No new CallPattern variant (P2 lookup authority preserved)
- No TransformNode widening (side-table only)
- Same fail-closed discipline (depth-limited tracer, no fabrication)
- All InductiveField components derived from authoritative DAG state
- No P1 substrate-fact-introduction trigger

Test asserts the structural facts directly:
field_name="_0" / variant_name="EpConsN" / type_name="EpListN" /
element_type="EpListN" / factor=UnitShrink.

Phase-1 closure tracking: gate `e_p_per_call_descent_evidence_full_coverage`
remains partial; SubValueUnknown rate strictly decreases on
nested-match self-calls (and the termination prover now accepts the
shape so they reach the producer at all). Subsequent slices
(multi-arg composition, parser/worklist/fold-body classes) follow
the same per-class cadence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(v3): close nested-match × field-projection coverage gap

openai-pro APPROVE_WITH_COMMENTS on PR #2182 flagged that the new
match_payload_field_projection_descent_relation tracer-replacement
line (dag.rs:1780) had no nested-match-FieldProject regression. The
tracer alone wasn't enough to actually classify that shape — record-
payload variant patterns (`EpNodeN { left: a }`) bind the user-scope
name to the OUTPUT of a FieldProject transform, not to the match
arm's payload port. So when an inner match's scrutinee was a name
like `a`, scrutinee_traces_to_param hit a port that wasn't in any
Path.binding.payload_port and bottomed out at None.

Extend the tracer with one FieldProject indirection peel: if `current`
is the output of a single-input FieldProject, walk to the projection's
input before continuing the payload-binding climb. New helper
`field_project_input_for_port` mirrors the same Behavior::Transform +
TransformTarget::FieldProject + single-input shape Slice 2's main
classifier already validates.

Adds the missing regression `e_p_per_call_descent_evidence_classifies_nested_match_field_projection_self_call_as_strict_sub_value`
exercising nested record-payload descent (`EpRecN { left }` two deep)
through the now-fully-walked tracer.

6/6 e_p_per_call_descent_evidence_* tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(v3): mirror nested-match descent rule into ClusterDescentChecker

codex BLOCKING on PR #2182 (commit 3613546): the previous Slice 3
extension to descent_provable accepted nested-match recursive
bindings as descending, but ClusterDescentChecker — the SAME
termination authority for mutually-recursive clusters — still
enforced the old `scrutinee_is_current_param` rule. That left two
authorities with different acceptance rules for the same termination
fact: a self-recursive nested match passed, but the equivalent SCC
case still failed cluster validation. Single-authority INVARIANT
violation per INVARIANTS.md / docs/modeling-discipline.md.

Mirror the same `scrutinee_unpacks_recursive_type` predicate
(`scrutinee_is_current_param || scrutinee_is_recursive_binding`)
into ClusterDescentChecker::expr's match arm. Doc-comment cross-
references descent_provable's authority to keep the parallel
implementations grep-discoverable when one shifts.

New regression `e_p_per_call_descent_evidence_classifies_nested_match_in_mutual_recursion_scc`
exercises a 2-fn SCC where one side has nested-match descent. The
fixture compiles only when ClusterDescentChecker accepts the
nested-binding rule consistently with descent_provable.

7/7 e_p_per_call_descent_evidence_* tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@briansrls
briansrls deleted the slice2-match-field-projection branch June 1, 2026 18:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant