Skip to content

feat(core): freshen type variables per call-site in CallElim#1438

Open
fabiomadge wants to merge 3 commits into
strata-org:reviewed-kbd-will-merge-to-mainfrom
fabiomadge:laurel-core-freshen
Open

feat(core): freshen type variables per call-site in CallElim#1438
fabiomadge wants to merge 3 commits into
strata-org:reviewed-kbd-will-merge-to-mainfrom
fabiomadge:laurel-core-freshen

Conversation

@fabiomadge

@fabiomadge fabiomadge commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What

When CallElim inlines a polymorphic Core procedure's contract, the inlined pre/postconditions carry the callee's source type variables (x : a). Copied verbatim, the same a is shared across call sites, so two calls at different concrete types (e.g. idp(7) and idp(true) in one body) force a to unify with both int and bool — a whole-program type-check abort that also masks unrelated obligations in sibling procedures.

Fix: rename the callee's declared type variables to globally-fresh names per call site (freshenTypeArgsSubst in CoreTransform), applied consistently to the temp input/output types, the old-referenced inout types, and the pre/postcondition expressions.

Why it's safe on existing code

For a monomorphic callee (typeArgs = []), freshenTypeArgsSubst returns Subst.empty, which is:

  • a proven identity for LMonoTy.subst (LMonoTy.subst_emptyS), and
  • a definitional short-circuit in LExpr.applySubst (Subst.hasEmptyScopes, with Subst.hasEmptyScopes_empty proven).

So the transform is an exact no-op on all existing non-polymorphic code — it cannot regress current verification.

Scope / stacking

This is a self-contained Strata/Transform fix, independent of the Laurel polymorphism front-end (#1394) that produces such callees. PolyProcFreshenTest exercises it directly on Core programs, one case per freshening path: single-instantiation soundness, multi-instantiation, the abort-masking regression, and old-typed inout freshening. PolymorphicProcedureTest's expected VC labels shift by one (_0_2_0_3) because fresh-name allocation advances the shared CoreGenM counter — updated here alongside the code that causes it.

#1394 (Laurel polymorphism) currently still contains this change; once this lands into reviewed-kbd-will-merge-to-main, rebasing #1394 drops these files from its diff automatically.

Test

Full lake test green (only the pre-existing ion-java JAR fails, unrelated).

When CallElim inlines a polymorphic Core procedure's contract, the inlined
pre/postconditions carry the callee's SOURCE type variables (`x : T`). Copied
verbatim, the same `T` is shared across call sites, so two calls at different
concrete types (`idp(5)`, `idp(true)`) force `T` to unify with both `int` and
`bool` — a whole-program type-check abort that also masks unrelated obligations
in sibling procedures.

Fix: rename the callee's declared type variables to globally-fresh names per call
site (`freshenTypeArgsSubst` in CoreTransform), applied consistently to the temp
input/output types, the `old` types, and the pre/postcondition expressions.

For a monomorphic callee (`typeArgs = []`) `freshenTypeArgsSubst` returns
`Subst.empty`, which is a proven identity for `LMonoTy.subst` (`subst_emptyS`)
and short-circuits `LExpr.applySubst` (`hasEmptyScopes`), so the transform is an
exact no-op on all existing non-polymorphic code — it cannot regress current
verification.

This is a self-contained Core/Transform fix, independent of the Laurel
polymorphism front-end that produces such callees; PolyProcFreshenTest exercises
it directly on Core programs (single-instantiation soundness, multi-instantiation,
and the abort-masking regression). PolymorphicProcedureTest's expected VC labels
shift by one (`_0_2` → `_0_3`) because the fresh-name allocation advances the
shared CoreGenM counter — updated here alongside the code that causes it.
@github-actions github-actions Bot added the Core label Jul 14, 2026
Adds a 4th PolyProcFreshenTest case exercising the oldVars branch of
callElimCmd (freshening the type of an inout param referenced via old() in a
postcondition) — the one freshening site the other three cases did not hit.
bump<a>(inout g:a, out z:a) with free ensures (z == old g); the caller sets
g := 5 before the call so old(g) is load-bearing, and the inlined assume
r == 5 verifies only if the old-typed temp was freshened correctly.

Comment-only test addition to the standalone Core PR; no source change.
@fabiomadge
fabiomadge marked this pull request as ready for review July 14, 2026 12:05
@fabiomadge
fabiomadge requested a review from a team as a code owner July 14, 2026 12:05
The comment said assert_1 'fails', but the pinned #guard_msgs output shows Result:
unknown (model bb=true): the inlined CallElim contract is an over-approximation, so
sat is demoted to unknown (never a wrong pass). Reword to match — soundness preserved,
per-obligation identity still pinned. Comment-only.
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.

1 participant