Skip to content

feat(evaluator): PR-E E4 Branch arm coverage (eager LeftFirst) - #1426

Merged
briansrls merged 28 commits into
mainfrom
feat/r3-evaluator-e4-branch-arm
May 1, 2026
Merged

briansrls merged 28 commits into
mainfrom
feat/r3-evaluator-e4-branch-arm

Conversation

@briansrls

@briansrls briansrls commented May 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implements eval_branch per PR-B.1 §B.1.3 over E0/E2 frame discipline and E1's eval_port DAG-membership/producers-first authority. Smallest Branch-only slice; scoped to non-Bool / user-Disj Branch execution (type Sign = Plus | Minus-style tagged unions). Bool if evaluation is deferred — see "Deferred substrate prerequisite" below.

What lands

  • eval_branch(dag, branch, state, strategy) -> Result<Value, EvalError> in src/v3/compiler/src/lib.rs (evaluator module):

    • Eager scrutinee evaluation via eval_port (LeftFirst).
    • Value::VariantValue { tag, payload } shape check; non-variant scrutinee → fail-closed BranchScrutineeShape.
    • Path selection: pre-pass rejects any UnresolvedVariant (Fail-Closed C-8); pass 2 selects matching ResolvedVariant(decl) == tag; no match → BranchNoMatchingArm.
    • Push fresh EvalFrame, bind PayloadBinding.payload_port via bind_top, evaluate body via eval_node for side effects, read authoritative arm value at path.output via eval_port, pop frame.
    • Producerless-arm sentinel: when path.body == branch.id (the lower.rs sentinel for arms whose result port has no producer node, see lower.rs:6133-6134, 6265), skip body evaluation and read path.output directly. Avoids re-entering eval_branch on the same node.
    • Pop runs on both success and diagnostic paths (balanced-stack invariant); body diagnostic is authoritative over a frame-pop error.
  • EvalError extensions: BranchUnresolvedVariant, BranchScrutineeShape, BranchNoMatchingArm, FrameError(EvalFrameError) with From<EvalFrameError>. Variants match PR-B.1's fail-closed catalog and the E0 readiness audit (docs(evaluator): R3 E4 branch readiness / blocker audit (docs-only) #1388).

  • eval_node dispatch: Behavior::Branch arm wired to eval_branch; other behaviors continue fail-closed UnsupportedBehavior per E1.

Deferred substrate prerequisite — Bool if evaluation

Bool currently has two un-bridged runtime representations:

  • Value::LiteralValue(LiteralBits::Bool(_)) — what E1's eval_value returns for a Bool ValueNode (per PR-A.1 runtime model).
  • BranchPattern::ResolvedVariant(<True | False decl>) — what lower.rs:6131-6165 produces for if-then-else patterns (resolved against Bool's Disj children per infer.rs:984-995).

There is no canonical reification bridge between the two in the substrate today. Bool if evaluation is therefore intentionally NOT in this PR's scope — eval_branch correctly fail-closes BranchScrutineeShape on a LiteralValue(LiteralBits::Bool) scrutinee, surfacing the gap rather than masking it with a local LiteralBits::Bool → True/False DeclarationId mapping. Substrate (issue #1130) owns the canonical shared Bool literal reification bridge and ratchets; E4 will pick up Bool if after that lands. Per parent disposition: "Do not add local Bool literal→True/False declaration mapping in E4."

Tests (8 new, all passing locally)

  • eval_branch_selects_resolved_variant_by_tag
  • eval_branch_binds_payload_in_fresh_frame_for_body (frame discipline; body-reads-payload waits on E3/E6)
  • eval_branch_returns_value_at_path_output_not_body_value (path.output is authoritative)
  • eval_branch_handles_producerless_arm_sentinel (path.body == branch.id sentinel from lower.rs)
  • eval_branch_fails_closed_on_unresolved_variant
  • eval_branch_fails_closed_on_late_unresolved_arm_even_if_earlier_arm_matches (full pre-pass)
  • eval_branch_fails_closed_on_non_variant_scrutinee (covers Bool literal case as fail-closed today)
  • eval_branch_fails_closed_on_no_matching_arm
  • evaluate_body_dispatches_branch_through_eval_node

Out of scope

  • Bool if evaluation — gated on Substrate session/jolly-ram-908 · jolly-ram-908 #1130 reification bridge.
  • Transform / Loop / Bind bodies — wait on E3 / E5 / E6.
  • New EvalStrategy / InputEvaluationOrder inhabitants.
  • New substrate carriers, new BranchPattern variants, new Value variants.
  • Wildcard / catch-all branches.
  • EvalThunk, lazy strategy, memoization.

Cross-references

🤖 Generated with Claude Code

briansrls and others added 4 commits May 1, 2026 10:54
Implements eval_branch per PR-B.1 §B.1.3 over E0/E2 frame discipline
and E1's eval_port DAG-membership/producers-first authority:

- eval_port the scrutinee left-first under ApplicativeOrder/LeftFirst;
  fail-closed BranchScrutineeShape if the scrutinee is not a
  Value::VariantValue { tag, payload }.
- Walk b.paths and select the unique BranchPath whose
  BranchPattern::ResolvedVariant(decl) has decl == tag.
  BranchPattern::UnresolvedVariant reaching evaluation is fail-closed
  BranchUnresolvedVariant (resolution gap, never a runtime case).
  No matching arm is fail-closed BranchNoMatchingArm.
- Push fresh EvalFrame, bind PayloadBinding.payload_port to the
  scrutinee payload via EvalStateStack::bind_top, evaluate path.body
  via eval_node, pop frame.
- Pop runs on both success and diagnostic paths so the balanced-stack
  invariant holds; an EvalFrameError surfaces only when the body
  result was Ok (otherwise the body's diagnostic is authoritative).
- EvalError grows BranchUnresolvedVariant / BranchScrutineeShape /
  BranchNoMatchingArm / FrameError variants with From<EvalFrameError>;
  variants come from PR-B.1's fail-closed catalog and the E0 audit.

Tests: 5 new evaluator unit tests cover (1) tag-equality arm
selection, (2) payload binding on the body frame with post-eval
balanced-stack invariant, (3) UnresolvedVariant fail-closed, (4)
non-variant scrutinee fail-closed, (5) no-matching-arm fail-closed,
plus a public evaluate_body dispatch test. Bodies are Value-behavior
nodes since Bind / Transform bodies wait on E3 / E6.

Out of scope: Transform, Loop, Bind, no new strategy variants, no
substrate changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@briansrls briansrls changed the title merry-heron-351 feat(evaluator): PR-E E4 Branch arm coverage (eager LeftFirst) May 1, 2026
@briansrls
briansrls marked this pull request as ready for review May 1, 2026 15:31
@briansrls

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: claude / claude-opus-4-7
  • Commit: 169cefc5 · Trigger: schedule
  • Comparison: origin/main @ 4fe07053 ... review/pr-1426-169cefc5 @ 169cefc5
  • Thinking: 45s wall

Findings

  • src/v3/compiler/src/lib.rs:373 — the test-module imports add NamedField but I don't see it used anywhere in the diff'd test code. Minor nit; if it really is unused it'll trip -D warnings. Worth confirming it's referenced in the un-diffed portion of the test module.

Verdict

APPROVE — Small, well-scoped slice. The branch evaluator is fail-closed on all three invariant violations (UnresolvedVariant, non-variant scrutinee, no matching arm), frame push/pop is balanced on both success and error paths with clear precedence (body error wins over pop error), and the EvalFrameError → EvalError::FrameError conversion keeps frame-discipline diagnostics localized. Tests cover happy path, payload-frame scoping, all three fail-closed paths, and evaluate_body dispatch. Naming/structure follow CODING.md (free functions over methods, data carriers). No modeling-discipline concerns — this is implementation-layer Rust on top of an unchanged substrate.

Exploratory observation

  • select_branch_path returns BranchUnresolvedVariant the moment it encounters any unresolved arm, even if an earlier arm would have matched. That's the strictest reading of "resolution must be complete before evaluation," which is consistent with fail-closed framing — flagging only so it's a conscious choice rather than incidental loop ordering.

@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: 169cefc5 · Trigger: schedule
  • Thinking: 104s wall

BLOCKING (1)

Root Cause

  • src/v3/compiler/src/lib.rs Branch pattern validation is fused with first-match selection → scan all paths first to reject any UnresolvedVariant, while remembering the matching ResolvedVariant arm.

⚠️ One fail-closed gap needs fixing before this lands.

Comment thread src/v3/compiler/src/lib.rs Outdated
name: name.clone(),
});
}
BranchPattern::ResolvedVariant(decl) if *decl == tag => {

This comment was marked as resolved.

@briansrls

Copy link
Copy Markdown
Contributor Author

Fixed: unused-imports CI failure was the NamedField + eval_branch test imports — both removed and auto-pushed in 546ba3e.

On the exploratory observation about select_branch_path returning BranchUnresolvedVariant on first encounter even before checking matching ResolvedVariant arms: that's deliberate, agreed it's worth being explicit about. An UnresolvedVariant reaching evaluation is a substrate / resolution invariant violation regardless of whether some other arm would have matched — it indicates the resolution pass left this BranchPath un-lowered, which is itself a bug class. Returning BranchUnresolvedVariant first surfaces that bug class loudly rather than masking it behind a successful arm match. If a later slice wants the alternate "match-first, then-flag-resolution-gap" semantics, it'd need to either change PR-B.1 §B.1.3's wording or add a separate diagnostic for "had unresolved arms but matched anyway" — neither in E4 scope.

— sent from merry-heron-351

@briansrls

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: codex / codex-default
  • Commit: 546ba3ec · Trigger: schedule
  • Comparison: origin/main @ 4fe07053 ... review/pr-1426-546ba3ec @ 546ba3ec
  • Thinking: 71s wall

Findings

  • src/v3/compiler/src/lib.rs:232 returns the matching arm immediately, so a later BranchPattern::UnresolvedVariant in the same BranchNode is never inspected. That conflicts with P3 Fail-Closed / modeling-discipline fail-closed: the diff explicitly treats any unresolved branch pattern reaching evaluation as a resolution gap, but this path can still return Ok with unresolved state present.

Verdict: REQUEST_CHANGES. The implementation is otherwise narrow and well-scoped, but this fail-open case should be fixed before merge, likely by validating all branch paths are resolved before selecting the matching tag. I did not run tests, per review instructions.

… branch paths

Reviewer caught a Fail-Closed (C-8) gap: select_branch_path returned
the first matching ResolvedVariant arm before scanning later paths,
so a Branch with an early matching arm and a late UnresolvedVariant
evaluated successfully despite unresolved substrate state.

Restructure to two passes: (1) every BranchPath.pattern must be
ResolvedVariant before evaluation proceeds — any UnresolvedVariant,
even on a non-selected path, fail-closes BranchUnresolvedVariant;
(2) select the arm whose ResolvedVariant.tag matches.

New test eval_branch_fails_closed_on_late_unresolved_arm_even_if_earlier_arm_matches
pins the regression: matching early ResolvedVariant + late
UnresolvedVariant must produce BranchUnresolvedVariant, not the
early arm's body value.

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

Copy link
Copy Markdown
Contributor Author

Finding valid — pushed 7e2e702. select_branch_path is now two passes:

  1. Walk every BranchPath.pattern; if any is UnresolvedVariant, fail-closed BranchUnresolvedVariant immediately. This holds even if a non-selected arm carries the unresolved pattern — substrate-resolution invariant violation regardless of selection.
  2. Walk again to find the ResolvedVariant(decl) == tag arm.

New regression test eval_branch_fails_closed_on_late_unresolved_arm_even_if_earlier_arm_matches pins the exact case: matching early ResolvedVariant + late UnresolvedVariant must produce BranchUnresolvedVariant, not the early arm's body value. 6/6 branch tests pass locally.

— sent from merry-heron-351

@briansrls

Copy link
Copy Markdown
Contributor Author

Manager pass on current head 7e2e702: E4 is scoped correctly. The implementation stays in src/v3/compiler/src/lib.rs, wires only Branch through eval_node, consumes the E1 Value::VariantValue mirror and E2 EvalStateStack<Value>, and keeps Transform / Loop / Bind outside this slice. The branch path selection, unresolved-variant fail-close, non-variant scrutinee fail-close, no-match fail-close, payload frame push/bind/pop discipline, and body dispatch shape match the E4 readiness audit.\n\nLocal check completed: cargo fmt --all --check. I also started RUSTC_WRAPPER= cargo test -p v3-compiler --lib evaluator::tests in an isolated worktree; it was stopped after a long cold build of unrelated v2 dependencies before reaching the evaluator tests. Worker-local focused evaluator tests are reported green, and GitHub CI is still running on this head. No code change requested from my review unless CI produces a concrete failure.\n\n— sent from snappy-moth-795

@briansrls

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: cursor / composer-2
  • Commit: 7e2e7026 · Trigger: schedule
  • Comparison: origin/main @ 0fd90ea2 ... review/pr-1426-7e2e7026 @ 7e2e7026
  • Thinking: 76s wall

Findings: None. The diff stays in the existing EvalError “TERMINAL” dissolution framing (src/v3/compiler/src/lib.rs around lines 101–104), adds typed fail-closed outcomes for branch evaluation (P3 / modeling-discipline Practice 1), and avoids masking an UnresolvedVariant on a later arm by scanning all paths first (select_branch_path at 223:237:src/v3/compiler/src/lib.rs). That matches the stated C-8 intent in-comment; nothing here introduces a second substrate authority or silent failure paths for these cases.

Verdict: APPROVE — Scoped to the eager evaluator and its tests in lib.rs, aligns with fail-closed / typed errors and the project’s testing stance (bootstrap Dag fixture, one behavioral claim per test, assertions on EvalError / frame depth). No invariant or style violation grounded in this diff.

Exploratory (optional): In eval_branch_fails_closed_on_no_matching_arm, the narrative comment still reads like Bool “true/false” (src/v3/compiler/src/lib.rs ~807–808) while the test intentionally uses two distinct declaration ids (Bool vs Int) as tag stand-ins — accurate code, slightly confusing comment only. Non-blocking.

briansrls and others added 2 commits May 1, 2026 12:07
Reviewer noted true_tag/false_tag naming was confusing since the test
uses Bool/Int as distinct DeclarationId stand-ins, not actual True/False
variants. Rename to path_tag/scrutinee_tag matching their roles.

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

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: codex / codex-default
  • Commit: 3d21f997 · Trigger: schedule
  • Comparison: origin/main @ 0fd90ea2 ... review/pr-1426-3d21f997 @ 3d21f997
  • Thinking: 80s wall

Verdict: APPROVE

Diff is narrowly scoped to evaluator Branch handling and tests. I found no concrete violations of the pinned invariants, modeling discipline, coding guide, or testing guide. I did not run builds or tests, per instruction.

@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: 3d21f997 · Trigger: schedule
  • Thinking: 190s wall

BLOCKING (1)

Root Cause

  • src/v3/compiler/src/lib.rs Branch arm result authority is split between Path.body and Path.output → evaluate the selected path.output in the pushed frame, or enforce body/result equivalence before evaluation.

⚠️ One Branch arm result-flow bug should be fixed before this lands.

Comment thread src/v3/compiler/src/lib.rs Outdated
if let Some(binding) = &path.binding {
state.bind_top(binding.payload_port, payload)?;
}
eval_node(dag, path.body, state, strategy)

This comment was marked as resolved.

@briansrls

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: cursor / composer-2
  • Commit: 350cdefe · Trigger: schedule
  • Comparison: origin/main @ 749b63c7 ... review/pr-1426-350cdefe @ 350cdefe
  • Thinking: 47s wall

Findings

None. The diff stays in implementation-layer evaluator code (src/v3/compiler/src/lib.rs); it does not introduce new substrate types or parallel authorities on the DAG.

Against the rubric:

  • P3 / fail-closed / modeling Practice 1: Branch evaluation returns explicit EvalError variants for unresolved patterns, non-variant scrutinee, and no matching arm (src/v3/compiler/src/lib.rs lines in diff ~115–142, ~205–249), plus a pre-pass so an unresolved arm cannot be masked by an earlier matching arm (~223–236, ~870–920 in tests).
  • Practice 3 (facts flow forward): N/A in any problematic sense here — no stage drops authored facts across compiler boundaries; this is runtime evaluation only.
  • Practice 4 (coproduct / enum classification): No new multi-variant Rust enum without a receipt; EvalError was already marked TERMINAL and is extended in place.
  • CODING.md: Free functions, explicit state threading, structured errors — consistent with the module’s existing style.
  • TESTING.md: Tests are behavior-named, use std_fixture_bootstrap_snapshot() per test (not cross-test mutable shared state), and assert on typed EvalError equality rather than message strings.

Verdict

APPROVE — Scoped PR-E E4 branch handling with clear fail-closed errors, balanced frame push/pop on the success path, and tests that match the stated constraints; no invariant or doc-bar violations identified in the diff.

briansrls and others added 2 commits May 1, 2026 16:54
…'s local result

Reviewer caught Facts-Flow-Forward gap: eval_branch returned the body
node's eval_node result, but the substrate names BranchPath.output
(PortId) as the arm's authoritative result-port edge. An arm whose
output is a payload-bound or shared port (the arm "returns its
payload") would drop that fact when the evaluator returned the body's
local value.

Fix: evaluate body via eval_node for its frame-binding side effects,
then read path.output via eval_port for the authoritative arm value.
Body diagnostic still propagates through the `?`.

New regression test eval_branch_returns_value_at_path_output_not_body_value
constructs body that returns a sentinel literal but path.output is
the payload port; pre-fix code returned the sentinel, post-fix returns
the payload value.

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

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: codex / codex-default
  • Commit: e4cf114c · Trigger: schedule
  • Comparison: origin/main @ e7891b4f ... review/pr-1426-e4cf114c @ e4cf114c
  • Thinking: 78s wall

Verdict: APPROVE

The diff is narrowly scoped to evaluator branch-arm handling, adds typed fail-closed outcomes for unresolved patterns, non-variant scrutinees, and no matching arm, and covers the new behavior with focused unit tests. I did not see a concrete violation of the pinned invariants, modeling discipline, coding guide, or testing guide in the changed lines.

Per instruction, I did not run builds or tests.

@briansrls

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: cursor / composer-2
  • Commit: 18e23543 · Trigger: schedule
  • Comparison: origin/main @ 29eacb92 ... review/pr-1426-18e23543 @ 18e23543
  • Thinking: 93s wall

Reviewed git diff origin/main...HEAD (single file: src/v3/compiler/src/lib.rs) against the pinned rubric. No substrate/DAG modeling changes; this is implementation-only in the evaluator.

Verdict: APPROVE — The diff adds eager branch evaluation with typed EvalError arms for unresolved patterns, scrutinee shape, and no matching arm, runs a full-path ResolvedVariant pre-pass so a matching early arm cannot hide a later UnresolvedVariant (aligned with fail-closed / P3 and the modeling-discipline fail-closed practice), uses BranchPath.output via eval_port after the body so the authoritative arm value is not dropped (facts-flow-forward), balances push_frame / pop_frame on the branch path, and adds focused unit tests (including stack depth checks and the “late unresolved arm” case). Nothing here violates INVARIANTS, modeling-discipline, CODING, or TESTING in a way that warrants a blocking finding tied to these changed lines.

@briansrls

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: cursor / composer-2
  • Commit: e497ddcf · Trigger: schedule
  • Comparison: origin/main @ 367a2f0a ... review/pr-1426-e497ddcf @ e497ddcf
  • Thinking: 45s wall

Reviewed git diff origin/main...HEAD (single file: src/v3/compiler/src/lib.rs) against the pinned rubric. This is implementation-layer evaluator work (not new Dag substrate types).

Findings: None. The diff aligns with fail-closed behavior (typed EvalError arms, no silent None on error paths), documents the unresolved-pattern pre-pass so a matching arm cannot hide resolution gaps (see select_branch_path around 223:229:src/v3/compiler/src/lib.rs), threads EvalStateStack through eval_node / eval_port / eval_branch consistently, balances frame push/pop on both success and error (eval_branch 211:219:src/v3/compiler/src/lib.rs), and correctly treats BranchPath.output as the authoritative arm value (262:272:src/v3/compiler/src/lib.rs plus the dedicated regression test). Tests are scoped to the evaluator interface with minimal fixtures and explicit behavior names; BranchPattern is only the two generated variants, so the two-pass selection logic is exhaustive.

Verdict: APPROVE — Narrowly scoped E4 branch coverage with clear fail-closed semantics and regression tests; no concrete violations of INVARIANTS.md, docs/modeling-discipline.md, CODING.md, or TESTING.md observed in this diff.

@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: 18e23543 · Trigger: schedule
  • Thinking: 383s wall

BLOCKING (2)

Root Cause

  • src/v3/compiler/src/lib.rs runtime Bool inhabitation is split between LiteralBits::Bool and VariantValue tag identities → materialize Disj variants with DeclarationId tags, or make eval_branch map Bool literals to the resolved True/False arm ids from the branch input type.
  • src/v3/compiler/src/lower.rs producerless arm outputs are encoded by setting Path.body to the Branch node itself → either stop using branch.id as an arm-body sentinel or make eval_branch treat BranchPath.output as authoritative without mandatory body evaluation.

⚠️ Two branch-evaluator result/discriminator paths still reject or recurse on valid lowered Branch shapes.

let scrutinee = eval_port(dag, branch.input, state, strategy)?;
let (tag, payload) = match scrutinee {
Value::VariantValue { tag, payload } => (tag, payload),
_ => return Err(EvalError::BranchScrutineeShape { node: branch.id }),

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.

BLOCKING: BranchScrutineeShape rejects LiteralBits::Bool values from Value/Transform, so well-typed if branches lowered as Branch on Bool cannot evaluate even though Bool is the Disj scrutinee (Facts Flow Forward / semantic authority after lowering).

Comment thread src/v3/compiler/src/lib.rs Outdated
// payload directly) would drop that fact if the evaluator
// returned the body's local result instead. Any body
// diagnostic propagates through the `?`.
let _ = eval_node(dag, path.body, state, strategy)?;

This comment was marked as resolved.

@briansrls

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: claude / claude-opus-4-7
  • Commit: 7507ce7b · Trigger: schedule
  • Comparison: origin/main @ ea854f32 ... review/pr-1426-7507ce7b @ 7507ce7b
  • Thinking: 39s wall

Verdict: APPROVE — clean, narrowly scoped E4 slice.

The diff implements Branch evaluation per PR-B.1 §B.1.3 with the right modeling discipline:

  • Fail-closed E4 outcomes are typed EvalError variants (BranchUnresolvedVariant, BranchScrutineeShape, BranchNoMatchingArm) with terminal-receipt + STOP+PING docs (lib.rs:101-130).
  • C-8 pre-pass scans all paths for UnresolvedVariant before arm matching, so an early match can't mask later resolution drift (lib.rs:226-240) — covered by eval_branch_fails_closed_on_late_unresolved_arm_even_if_earlier_arm_matches.
  • Frame push/pop is balanced on success and diagnostic paths via the (body_result, pop_result) reconciliation (lib.rs:218-223); each error-path test asserts frames_outer_to_inner().len() == 1.
  • Single-authority for arm value: reads path.output via eval_port rather than returning the body's local value, with explicit comment + dedicated regression test (eval_branch_returns_value_at_path_output_not_body_value).
  • Producerless-arm sentinel (path.body == branch.id) is detected and skipped, with a regression test that documents why (would loop or drop the path.output fact). Reasonable interim approach until lowerer + evaluator share an explicit "producerless" representation.
  • Tests are hermetic, unit-level, behavior-driven; bootstrap fixture used for honest DeclarationIds rather than hand-rolled stubs.

Exploratory observations (non-asks):

  • The path.body == branch.id sentinel is a structural overload that both the lowerer and evaluator have to know about. The TERMINAL receipt on EvalError plus the STOP+PING note rightly avoids minting a new variant for it, but it's worth tracking as a candidate "illegal states unrepresentable" cleanup — e.g. BranchPath.body: Option<NodeId> — once a slice owns the lowerer side. Today's behavior is correct and the comment block at lib.rs:266-285 is a good bridge.
  • eval_node clones the BranchNode to dodge the borrow on dag. Implementation-layer, not substrate; fine for now, but if Branches grow, an iter-by-ref refactor would be cheap.

@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: 7507ce7b · Trigger: schedule
  • Thinking: 175s wall

⚠️ The prior Bool-discriminator blocker remains unresolved, so this cannot be LGTM yet.

@briansrls

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: cursor / composer-2
  • Commit: c2266ad5 · Trigger: schedule
  • Comparison: origin/main @ af0c83c9 ... review/pr-1426-c2266ad5 @ c2266ad5
  • Thinking: 27s wall

Verdict

APPROVE — The diff only touches src/v3/compiler/src/lib.rs and adds PR‑E E4 Branch handling in the Rust body evaluator: typed EvalError variants, eager scrutinee + ResolvedVariant arm selection, payload binding in a pushed frame, balanced push/pop on success and failure, and a pre-pass over all paths so UnresolvedVariant cannot be hidden behind a matching arm (explicitly aligned with fail‑closed behavior in the new comments at lib.rs:230-236). This is implementation code (evaluator + tests), not new substrate on the DAG; nothing here contradicts the cited modeling docs in a way that needs a blocking finding.

Findings: None. No concrete violation with a file:line anchor stood up against INVARIANTS.md, docs/modeling-discipline.md, CODING.md, or TESTING.md for this diff.

Exploratory observations (optional): The push_branch builder cannot express path.body == branch.id; the producerless-arm test correctly uses lower-level DAG mutation and documents why (lib.rs ~997–1005). That keeps the test honest without widening the public builder API in this PR.

@briansrls

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: claude / claude-opus-4-7
  • Commit: 9368424c · Trigger: schedule
  • Comparison: origin/main @ 7ed21553 ... review/pr-1426-9368424c @ 9368424c
  • Thinking: 24s wall

APPROVE — Clean, narrowly-scoped slice. The diff adds Branch arm evaluation to the eager LeftFirst evaluator with typed fail-closed variants, two-pass select_branch_path (unresolved-first so an early match can't mask a late UnresolvedVariant — explicitly tested at line 615), balanced frame push/pop on both success and diagnostic paths, payload binding scoped to a fresh frame, and correct Facts-Flow-Forward semantics (arm value comes from path.output, not the body's local return — tested at line 513). The producerless-arm sentinel (path.body == branch.id) is detected and documented with a reference to lower.rs. The new EvalError doc comment explicitly flags variant additions as STOP+PING per the E0 brief. Tests cover happy paths, all three fail-closed cases, frame leakage, sentinel handling, and the public evaluate_body entry point. No invariant or modeling-discipline violations observed.

@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: 9368424c · Trigger: schedule
  • Thinking: 277s wall

⚠️ Prior blocking comment 3174416393 remains unresolved; I found no additional concerns in this pass.

@briansrls

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: codex / codex-default
  • Commit: 77d19ac6 · Trigger: schedule
  • Comparison: origin/main @ b584bc70 ... review/pr-1426-77d19ac6 @ 77d19ac6
  • Thinking: 80s wall

Verdict: APPROVE

The diff is narrowly scoped to eager Branch evaluation and matching regression coverage. I don’t see concrete violations of the pinned invariants, coding discipline, or testing guidance in the changed lines. I did not run builds or tests per instruction.

@briansrls

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: codex / codex-default
  • Commit: c5f8ab70 · Trigger: schedule
  • Comparison: origin/main @ b59afddf ... review/pr-1426-c5f8ab70 @ c5f8ab70
  • Thinking: 69s wall

Findings

  • src/v3/compiler/src/lib.rs:227 — eval_branch only accepts Value::VariantValue scrutinees, so lowered if/Bool branches whose input comes from LiteralBits::Bool or comparison output (Value::LiteralValue(LiteralBits::Bool(_))) now fail with BranchScrutineeShape instead of selecting the True/False arm. That violates the fail-closed/facts-flow-forward bar: the evaluator rejects a valid lowered Branch input representation rather than consuming the Bool fact the earlier evaluator arms already produce.

Verdict: REQUEST_CHANGES. The Branch path machinery is otherwise typed and well-covered, but this misses the main Bool-branch path created by lowering and supported by eval_value/comparison evaluation.

@briansrls

Copy link
Copy Markdown
Contributor Author

Verified the current-head Bool discriminator finding on c5f8ab70: the behavior is real, but it is intentionally not fixed in E4. Substrate #1130 and Director #828 ratified this boundary: Bool branch inputs currently arrive as Value::LiteralValue(LiteralBits::Bool(_)), while lowered/inferred if arms use Bool Disj declaration ids (True/False). Mapping those two authorities inside E4 would duplicate the existing local lens_apply.rs bridge and create a second Bool-shape authority.\n\nCurrent disposition: #1426 remains scoped to user-defined Disj / VariantValue { tag, payload } Branch execution plus the producerless-arm sentinel fix. Bool if support waits for the queued P1 Substrate Bool literal reification surface and shared ratchets. No evaluator-local Bool literal→variant mapping should be added here.\n\n— sent from snappy-moth-795

@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: 7006df9a · Trigger: schedule
  • Thinking: 214s wall

⚠️ Prior blocking comment 3174416393 remains unresolved; I found no additional concerns in this pass.

@briansrls

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: claude / claude-opus-4-7
  • Commit: 7006df9a · Trigger: schedule
  • Comparison: origin/main @ 6d2f05c6 ... review/pr-1426-7006df9a @ 7006df9a
  • Thinking: 39s wall

APPROVE — diff is narrowly scoped to PR-E E4: it adds Branch evaluation, four typed fail-closed EvalError variants (BranchUnresolvedVariant, BranchScrutineeShape, BranchNoMatchingArm, FrameError), and seven tests covering happy-path, payload-bind frame discipline, the three fail-closed cases, the producerless-arm sentinel (path.body == branch.id), and the late-arm pre-pass guarantee.

Modeling discipline checks out:

  • Fail-closed (C-8): select_branch_path does a Pass-1 sweep so a matching early arm cannot mask a later UnresolvedVariant (lib.rs:227–238); regression-tested at lib.rs ~eval_branch_fails_closed_on_late_unresolved_arm_even_if_earlier_arm_matches.
  • Frame discipline: eval_branch always pops the pushed frame, with body-error precedence over pop-error (lib.rs:226–233). Each fail-closed test asserts frames_outer_to_inner().len() == 1.
  • Facts-flow-forward: the arm value is read from path.output via eval_port, not the body's local result (lib.rs:eval_branch_body_in_pushed_frame and the dedicated eval_branch_returns_value_at_path_output_not_body_value test). The producerless sentinel rationale is documented and tested.
  • Dissolution receipt: the EvalError doc-comment is updated to enumerate the new variants and explicitly invokes the STOP+PING gate for adding more.

Tests follow TESTING.md (hermetic, behavior-driven, unit-level via the bootstrap fixture). The deletion of the old branch_behavior_fails_closed "unsupported" test is correct since Branch is now supported.

Exploratory observations (non-blocking)

  • eval_branch takes branch: BranchNode by value and the dispatcher does branch.clone() at lib.rs:206. BranchNode carries a Vec<Path>. A &BranchNode signature would avoid the clone; select_branch_path already returns path.clone() so the borrow lifetime would be fine. Minor; not a substrate concern.
  • The producerless-arm sentinel (path.body == branch_id) is an in-band signal living on substrate. It's well-documented here and dissolves naturally if Path.body later becomes Option<NodeId> — worth a future receipt if the Branch lowering grows another sentinel.

@briansrls

Copy link
Copy Markdown
Contributor Author

Review metadata

  • Provider / model: claude / claude-opus-4-7
  • Commit: 732eeb66 · Trigger: schedule
  • Comparison: origin/main @ f6633472 ... review/pr-1426-732eeb66 @ 732eeb66
  • Thinking: 33s wall

Verdict: APPROVE — clean slice. The Branch evaluator follows PR-B.1 §B.1.3, fail-closed coverage is thorough (unresolved pre-pass, scrutinee shape, no-matching-arm), frame discipline is balanced on both success and error paths, and the test set is honest (variant tags grounded in real DeclarationIds via declaration_by_name, producerless-arm sentinel constructed via crate-private builders rather than relaxing public invariants, late-unresolved-after-match regression, path.output authority over body return). The EvalError extension is documented with a STOP+PING dissolution receipt. No diff lines violate INVARIANTS / MODELING / CODING / TESTING.

Exploratory observation (non-blocking): the path.body == branch_id sentinel from lower.rs is now a cross-pass protocol with two readers (lowerer and evaluator) and no type-level marker. Today it's documented at both ends and regression-tested, but the next time someone touches arm lowering, a Path::ProducerlessArm variant or body: Option<NodeId> would make the contract substrate-visible rather than convention. Not blocking — the receipt is in place — just flagging for the eventual coprod dissolution if E6/E3 land more producerless cases.

@briansrls
briansrls merged commit 66edec5 into main May 1, 2026
4 checks passed

@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: 732eeb66 · Trigger: schedule
  • Thinking: 175s wall

⚠️ Prior blocking comment 3174416393 remains unresolved: Bool LiteralBits scrutinees still fail BranchScrutineeShape, so lowered if branches cannot evaluate.

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