Skip to content

fix(eval): isolate Env.error at procedure boundary (issue #1185)#1190

Open
PROgram52bc wants to merge 8 commits into
mainfrom
htd/fix-eval
Open

fix(eval): isolate Env.error at procedure boundary (issue #1185)#1190
PROgram52bc wants to merge 8 commits into
mainfrom
htd/fix-eval

Conversation

@PROgram52bc
Copy link
Copy Markdown
Contributor

@PROgram52bc PROgram52bc commented May 19, 2026

Summary

Fixes issue #1185Program.eval propagated Env.error across procedures, silently dropping later procedures' obligations and reporting "All 0 goals passed" even with a literal assert false in scope.

Two surgical fixes restore local invariants:

  • Procedure.fixupError (Strata/Languages/Core/ProcedureEval.lean:23-31) now unconditionally pops scope/PC frames AND clears Env.error, so each procedure's evaluation starts from a clean error state.
  • Env.merge (Strata/Languages/Core/Env.lean:358-373) now unions deferred across the error wall when one ITE branch errors, preserving the clean branch's already-accumulated obligations.

Scope is deliberately minimal: visibility/UX (stderr warnings, per-procedure error stat, peErrors field) was considered and explicitly cut to a follow-up. No new types, helpers, fields, or signature changes.

Test plan

  • lake build StrataTest passes (verified locally — 608/608 jobs).
  • StrataTest.Languages.Core.Tests.ProgramEvalErrorIsolation (new) — issue's exact 17-line repro reports assert_0: ❌ fail; reorder regression still passes.
  • StrataTest.Languages.Core.Tests.EnvMergeErrorPreservesDeferred (new) — 5 cases covering errored/clean combinations of Env.merge, including the both-errored asymmetry contract.
  • Examples/IsolatedProcEvalError.core.st (new CLI golden) — Examples/run_examples.sh reports Test passed.
  • No existing test regressed.

Notes for reviewers

  • Existing-proof impact: none. Confirmed via grep across *WF.lean, VerifierProofs.lean, and StatementSemanticsProps.lean (4091 lines combined): no proof references fixupError, mergeResults, Procedure.eval, Program.eval, Env.merge, performMerge, or Env.error. The merge references in VerifierProofs.lean are about SMT.Result.merge (solver-result lattice join), unrelated.
  • Public signatures unchanged.
  • Soundness argument: ProofObligation.assumptions is PathConditions Expression (a value-typed list of assumptions baked in at obligation creation), so an obligation carried across an error boundary remains independently provable.
  • Trade-off accepted: the underlying PE error (e.g., "function foo already exists") is now silently swallowed. The user sees the assertion failure correctly, but the function-redefinition error itself is no longer surfaced. Visibility work is a small additive follow-up if this becomes a concern in practice.

Commits

  • ecf402211 test: failing repro for cross-procedure leak
  • d55ac1c33 fix: clear Env.error and pop frames in fixupError unconditionally
  • caec30bd4 test: failing unit tests for Env.merge deferred preservation
  • eeb0dfa3d fix: union deferred obligations across Env.merge error wall
  • 2e260c4b8 docs: fix stale StatementEval line reference in fixupError comment
  • d22edcf9f test: CLI golden regression
  • 490d5cbb8 test/docs: document Env.merge both-errored asymmetry

Closes #1185

David Deng added 7 commits May 19, 2026 15:45
The first test (issue1185Repro) currently fails: `assert false` in proc_c
is silently dropped because proc_b's funcDecl collision contaminates
`Env.error` and short-circuits subsequent statement evaluation. The reorder
regression (issue1185Reordered) already passes pre-fix; we lock it in to
protect against regression.
Closes a soundness leak where one procedure's partial-evaluation error
(e.g., function redefinition) would contaminate Env.error, causing
StatementEval.evalAuxGo (line 618-619) to short-circuit every statement
in subsequent procedures. The contaminated env produced zero deferred
obligations, so a literal `assert false` in a later procedure was
silently passed.

fixupError now pops scope+PC frames and clears Env.error on every path,
so each procedure's evaluation starts from a clean error state. Clean
deferred obligations from any path are preserved as-is — each obligation
carries its own assumption list and is independently provable.

Issue: #1185
Cases 1, 2, and 4 currently fail. `Env.merge` returns the errored side
verbatim (case 1: returns E1 with empty deferred, dropping E2's; case 2:
mirror; case 4: returns E1 with one deferred, dropping E2's). Case 3
(clean+clean, four obligations total) passes via the existing
`performMerge` path.

Issue: #1185
When one ITE branch errors, Env.merge previously returned that side
verbatim — silently discarding the clean branch's already-accumulated
deferred obligations. This is the within-procedure analogue of the
cross-procedure leak fixed by the previous commit: an obligation
generated by sound symbolic execution should not be erased because a
sibling path crashed.

Now Env.merge keeps the errored side's structural fields (state, scope,
error, pathConditions) but unions `deferred` from both sides. Path
conditions on the non-errored side are dropped on the error path; this
is fine because the merged env carries an error and future statements
short-circuit before consulting PCs.

Issue: #1185
The Issue #1185 commits (d55ac1c fixupError, eeb0dfa Env.merge) both
referenced `StatementEval.lean:618-619` for the error-state short-circuit.
That range was inherited from the issue report and is now off — the
short-circuit lives at lines 632-633 (the `evalAuxGo` `if good.isEmpty`
return), not 618-619 (which sit inside `addFactoryFunc` match arms).

Env.merge's reference was corrected when its commit was amended; this
commit corrects fixupError's matching reference. No code change.

Issue: #1185
End-to-end test on the exact 17-line program from the issue. Pre-fix,
the CLI reported "All 0 goals passed". Post-fix, it correctly reports
proc_c's `assert false` as failing. This catches any regression that
bypasses the in-tree Lean #guard_msgs tests (e.g., wiring changes in
the CLI surface).

Issue: #1185
The Env.merge fix from eeb0dfa kept E1's error value when both sides
error (an arbitrary choice — the merged env carries an error flag either
way and the caller short-circuits regardless), but the comment didn't
say so. The Case 4 test happened to merge two envs with the same error
string, hiding the asymmetry. Final whole-PR review flagged this as a
minor maintainability issue.

Add one line to Env.merge's comment explaining the both-errored choice,
and add Case 5 to EnvMergeErrorPreservesDeferred.lean that merges two
envs with DISTINCT error values to lock in the contract.

Issue: #1185
Remove "Issue #1185" pointers from the cross-procedure error-isolation
fix (fixupError, Env.merge) and its tests. Rename test-program defs
issue1185Repro / issue1185Reordered to collisionThenAssertFalse /
assertFalseThenCollision to describe what they exercise rather than
where they came from. Tighten the surrounding comments to keep only
the load-bearing "why" — error must not leak across the procedure
boundary, deferred must be unioned across the error wall — and drop
brittle file:line cross-references.

No behavior change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Program.eval propagates Env.error across procedures, silently dropping obligations from later procedures

1 participant