Skip to content

Return MatchIndeterminate (not MatchFailed) for mixed-determinacy matches in Eval and Implies modes#4152

Merged
automergerpr-permission-manager[bot] merged 17 commits into
masterfrom
match-indeterminate-bug
Jun 16, 2026
Merged

Return MatchIndeterminate (not MatchFailed) for mixed-determinacy matches in Eval and Implies modes#4152
automergerpr-permission-manager[bot] merged 17 commits into
masterfrom
match-indeterminate-bug

Conversation

@ehildenb

@ehildenb ehildenb commented Jun 8, 2026

Copy link
Copy Markdown
Member

The booster matcher's job is to return the truth — terms either definitely cannot match (MatchFailed) or cannot be decided without further simplification (MatchIndeterminate) — and leave it to the caller to decide what to do with an indeterminate verdict. In Eval and Implies modes the matcher previously short-circuited to a decisive MatchFailed in several cases where the two terms could in fact simplify or instantiate to something equivalent, so the deferral never reached the caller. The unifying principle applied here: a decisive MatchFailed is sound only when the mismatch is between rigid shapes or sorts that cannot change under evaluation or instantiation; every other mismatch must defer.

This matters most for function-equation evaluation: handleFunctionEquation routes FailedMatch _ -> continue but IndeterminateMatch{} -> abort. A spurious decisive MatchFailed caused the evaluator to silently skip a higher-priority equation and commit to a lower-priority catch-all, violating the priority contract function equations rely on. For Implies, the indeterminate verdict lets the existing simplify-LHS / simplify-RHS retry ladder in Pattern.Implies attempt the discharge and report a residual non-match as indeterminate rather than a decisive valid:false. Simplification behaviour is unchanged — handleSimplificationEquation routes both verdicts to continue.

Behavioural changes (each previously a decisive MatchFailed, now addIndeterminate):

  • bindVariable: a pattern variable rebinding to two terms that are not both constructor-like (e.g. a domain value and a function application), in all modes — matching what Rewrite already did.
  • match1: a FunctionApplication pattern against a concrete structured subject (DomainValue/Injection/KMap/KList/KSet) in Eval — falls through to the shared catch-all.
  • match1: an Injection pattern against a builtin collection (KMap/KList/KSet) in Eval, making both directions of this pair indeterminate (the reverse direction already was).
  • matchVar: a pattern variable against a subject whose static sort is not a subsort of the variable's, when the subject is a FunctionApplication or Var and the two sorts share a subsort (the subject can still narrow). Sort-disjoint pairs and rigid subjects keep the decisive DifferentSorts failure.
  • matchInj: an injection-vs-injection pair with differing sources where a child on the wider-sorted side can narrow (a subject variable child, or a pattern function child). Rigid children at incompatible sorts keep the decisive failure.

Refactor (no behaviour change):

  • match1 is inverted to defer-by-default: the table now ends in one generic addIndeterminate fall-through — the always-sound outcome — and every row above it justifies something stronger (\and decomposition, variable binding, same-category descent, or decisive failure). The decisive cross-category rule is a single guarded row over a new isRigidCategory predicate, making the rigidity principle executable rather than commentary. The table shrinks from ~108 rows to 18. bindVariable and matchInj no longer inspect their now-dead MatchType argument.

Tests:

  • Matcher-level cases pinning the post-fix contract in MatchEval, MatchImplies, and MatchRewrite, plus a paired function/simplification soundness-regression test in ApplyEquations (test_soundnessGap) showing a high-priority indeterminate match correctly aborts function-equation evaluation rather than falling through to a lower-priority rule.
  • A new exhaustive dispatch-class grid test (MatchDispatch) pinning the result class (S/F/I) of all 3 × 9 × 9 = 243 (mode, pattern-constructor, subject-constructor) combinations, so any future match1 change surfaces as a reviewable grid-cell diff.

Validation:

  • Full booster unit-test suite passes (979 tests). The behavioural tests were authored to fail against the pre-fix matcher and turn green only with the Match.hs changes applied; the MatchDispatch grid was first pinned on master and shows exactly the eight Eval cells (Injection-vs-collection and FunctionApplication-vs-rigid) that this branch flips from F to I.
  • Tested downstream on KMIR, KEVM, and Kontrol and saw that it does not cause any proofs to fail or performance regressions.

Comment thread booster/library/Booster/Pattern/Match.hs Outdated
ehildenb added a commit that referenced this pull request Jun 12, 2026
…ws into a catch-all

All non-special-case rows for a FunctionApplication pattern resolve to
addIndeterminate in every mode, so replace the per-subject-constructor rows
(DomainValue/Injection/KMap/KList/KSet in all modes, ConsApplication and
FunctionApplication outside Eval, Var outside Rewrite) with a single trailing
catch-all. The mode-specific rows (AndTerm handling, Eval symbol-application
descent, Rewrite subject-variable match) stay explicit above it. No behaviour
change; addresses review feedback on #4152.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ehildenb added a commit that referenced this pull request Jun 15, 2026
…rid test for matchTerms (#4155)

One canonical pattern/subject term per Term constructor, crossed over
all three match modes: 243 (mode, pattern, subject) cells, each
asserting only the MatchResult class (success / decisive failure /
indeterminate) against an explicit per-mode 9x9 expectation grid. The
grids are the artifact: any change to the match1 dispatch table in
Booster.Pattern.Match surfaces as a reviewable grid-cell diff, and the
test documents the matcher's entire dispatch policy at a glance.
Companion fine-grained tests (MatchEval and friends) continue to pin
exact substitutions, failure reasons, and remainders; this module pins
only the class.

This is to prepare for a major overhaul here:
#4152

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ehildenb and others added 13 commits June 15, 2026 13:55
…le-rebind with mixed-determinacy subject

When the matcher (in Implies mode) sees a pattern variable bound first to a
domain value and then to a function application (or vice versa), it currently
returns a decisive MatchFailed VariableConflict, even though the function
application could simplify into the domain value (or anything equivalent).
The pyk team's recover-mode sweep depends on a sound MatchIndeterminate
verdict here so the implies handler's existing simplify-LHS / simplify-RHS
retry ladder can attempt the discharge, and a residual non-match can be
reported with indeterminate:true rather than as a decisive valid:false.

Pin both orderings of the rebind. Tests are expected to fail until
Match.bindVariable in Implies mode mirrors Rewrite mode's addIndeterminate.

(cherry picked from commit 1700d2d0f5dd89836c86a92f057275ac15756a64)
… mode through addIndeterminate on mixed-determinacy rebind

In Match.bindVariable, when a variable is bound a second time and the two
terms are not both constructor-like, the matcher previously failed with a
decisive VariableConflict in both Eval and Implies modes. That is sound for
Eval (the equation evaluator skips to the next priority on MatchFailed) but
unsound for Implies: the comment two lines up — "the term in the binding
could be equivalent (not necessarily syntactically equal)" — captures
exactly why such a decision should be deferred. Routing this case through
addIndeterminate lets the existing simplify-LHS / simplify-RHS retry ladder
in Pattern.Implies attempt to discharge the equivalence and, on residual
failure, mark the implication indeterminate so recover-mode pyk can
escalate to a kore-enabled retry.

Eval keeps the failWith path pending a separate soundness review of the
function-equation evaluator, which currently treats MatchFailed as "skip and
try next priority" while it treats MatchIndeterminate as "abort". The
comment on the carve-out documents this distinction.

Also flip the pre-existing "Matching the same variable in a constructor"
test in MatchImplies from MatchFailed to the new MatchIndeterminate
expectation, mirroring MatchRewrite's analogous case.

(cherry picked from commit 262d49858688eced23a309c7be83489c0a258a09)
…sts for Eval matcher soundness gap

In Eval mode the matcher today returns a decisive MatchFailed when:
  - a pattern variable rebinds to two terms that aren't both
    constructor-like (Match.hs:825), and
  - a function-application pattern is matched against a concrete
    structured subject (Injection/KMap/KList/KSet, match1 lines 289,
    291, 293, 295).

The "truth" verdict for both shapes is MatchIndeterminate: the function
side could simplify into something equivalent to the constructor-like
side, and the matcher cannot decide that without further work. Because
handleFunctionEquation (ApplyEquations.hs) routes FailedMatch _ to
continue but IndeterminateMatch{} to abort, the current decisive verdict
silently skips a higher-priority equation and commits to a lower-priority
catch-all, violating the priority contract function equations rely on.

This adds six matcher-level tests (MatchEval.hs) and a paired
function/simplification soundness-regression test (ApplyEquations.hs)
that pin the post-fix contract. All seven new tests are expected to fail
until Match.bindVariable and the five Eval-specific match1 lines mirror
the catch-all behaviour. The simplification companion already passes
today and serves to pin that simplifications are unaffected by the
proposed fix.

(cherry picked from commit a61e40fb842f002b1d315b5785b4a61610b332b0)
…through addIndeterminate on mixed-determinacy match

The matcher's job is to return the truth — terms either definitely
cannot match (MatchFailed) or cannot be decided without simplification
(MatchIndeterminate) — and to let the caller decide what to do. Eval
mode previously short-circuited in two places where the terms involved
could in fact simplify to be equivalent:

  - Match.bindVariable rebind where the two terms are not both
    constructor-like (e.g. a domain value and a function application):
    previously failWith $ VariableConflict; now addIndeterminate, like
    Rewrite and Implies.

  - Five match1 lines that pair a FunctionApplication pattern with a
    concrete structured subject (DomainValue, Injection, KMap, KList,
    KSet): previously failWith $ DifferentSymbols in Eval mode; the
    Eval-specific lines are now dropped so the same catch-all that
    handles Rewrite/Implies (addIndeterminate) handles Eval too.

The downstream contract — handleFunctionEquation routes
@IndeterminateMatch{} -> abort@ while @FailedMatch _ -> continue@ — now
correctly stops function-equation iteration at an indeterminate match
instead of silently skipping a higher-priority equation and committing
to a lower-priority catch-all. Function-equation priorities are
"semantically binding" (per the comment on handleFunctionEquation), so
preserving priority order under the matcher's deferred verdict is the
sound thing to do.

handleSimplificationEquation routes both verdicts to @continue@, so
simplification behaviour is unchanged. The simplification companion in
test_soundnessGap pins this.

Also flip the two pre-existing MatchEval tests whose expectations
become stale ("function and something else" and "Matching two
constructor argument to be the same (failing)") from MatchFailed to
the new MatchIndeterminate verdict.

(cherry picked from commit b004b6f5270a57ff620877bbdd783a369c924a24)
The mixed-determinacy rebind comment had grown to a verbose paragraph.
Restore master's concise wording, keeping only the one new fact: all
modes now defer (a decisive MatchFailed would skip higher-priority
equations) rather than re-listing every caller's discharge mechanism.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eword test docs to post-fix framing

The test-first commits documented the soundness gap as present ("currently
failing", "under the proposed fix"), and the fix commits never updated the
wording, leaving group names claiming tests fail while they pass. Reword to
describe the pinned contract, mentioning the pre-fix behaviour as history.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ws into a catch-all

All non-special-case rows for a FunctionApplication pattern resolve to
addIndeterminate in every mode, so replace the per-subject-constructor rows
(DomainValue/Injection/KMap/KList/KSet in all modes, ConsApplication and
FunctionApplication outside Eval, Var outside Rewrite) with a single trailing
catch-all. The mode-specific rows (AndTerm handling, Eval symbol-application
descent, Rewrite subject-variable match) stay explicit above it. No behaviour
change; addresses review feedback on #4152.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s-builtin-collection match indeterminate both ways in Eval

match1 was asymmetric for Injection paired with a builtin collection (KMap/KList/KSet) under Eval: the
collection-pattern/Injection-subject direction returned addIndeterminate while the commuted
Injection-pattern/collection-subject direction returned a decisive failWith DifferentSymbols. A spurious
MatchFailed silently skips a higher-priority function equation, so the sound verdict is indeterminate in
both directions. Add the Eval indeterminate carve-out to the Injection rows; Rewrite/Implies stay decisive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…: defer matchVar sort mismatch when the subject can narrow

A pattern variable matched against a subject whose static sort is not a
subsort of the variable's sort decisively failed with DifferentSorts. That is
only sound when the subject's sort is exact: a function application may
evaluate to a term of a narrower sort (matchInj already defers on exactly this
ground), and a subject variable may be instantiated with one. Defer as
indeterminate when the subject is a FunctionApplication or Var and the two
sorts share a subsort; sort-disjoint pairs and rigid subjects keep the
decisive failure. New helper sortsOverlap intersects the reflexive-transitive
subsort closures from the SortTable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hen a child on the wider-sorted side can narrow

The inj-vs-inj fall-through decisively failed with DifferentSorts whenever the
sources differed and the special cases (subject function child, pattern
variable child) did not apply. That is only sound for rigid children: a
subject variable child of the wider sort may be instantiated in the pattern's
source sort, and a pattern function child of the wider sort may evaluate into
the subject's. Restructure the post-subsort-check dispatch as a single case
over both children, deferring those two shapes and keeping the decisive
failure for rigid children at incompatible sorts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tch-alls, drop dead MatchType params

Pure refactor, no behaviour change. The match1 table shrinks from ~108 rows to
46 by: hoisting the per-block \and-subject rows into two generic rows after
the \and-pattern block; closing every pattern block with a catch-all (the
bulk indeterminate or decisive-fail outcome) so only special rows are
explicit; collapsing the Var-pattern block into a single matchVar dispatch;
merging the Rewrite/Implies DomainValue-vs-Var rows; and deleting the Eval
ConsApplication/FunctionApplication cross-kind descents through
matchSymbolAplications, which always resolved to addIndeterminate because
constructor and function symbol names never coincide. A header comment states
the block order and the rigidity principle governing decisive vs indeterminate
outcomes. bindVariable and matchInj no longer inspect their MatchType argument
(all modes defer identically since the mixed-determinacy fix), so the dead
parameters are removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e generic addIndeterminate fall-through

Pure refactor, no behaviour change. Instead of per-pattern blocks each closed
by their own catch-all, the table now ends in one generic rule resolving every
unhandled pair to addIndeterminate — the always-sound outcome — and every row
above it must justify something stronger: \and decomposition, variable
binding via matchVar, same-category descent, or decisive failure. The decisive
cross-category rule is a single guarded row over a new isRigidCategory
predicate (domain values, injections, collections, constructor applications),
making the rigidity principle executable rather than commentary. Explicit
addIndeterminate rows remain only where they must shadow a more generic
stronger row: the Eval \and quirks and the Eval injection-vs-collection
carve-out (now two isCollection-guarded rows covering both directions). The
subjectVariableMatch marker now uniformly covers all non-Eval
rigid-or-function patterns against a variable subject (outcome unchanged:
addIndeterminate). 46 rows become 18.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ss cases to MatchDispatch

The functionApplicationAgainstConcreteCategories group (4 cases), the
injectionAgainstBuiltinCollections group (6 cases), and the 'function and
something else' one-liner asserted only the dispatch class of a sort-aligned
top-level pair — exactly what the MatchDispatch Eval grid now pins cell by
cell. Remove them to avoid double maintenance. The tests that assert more
than the class stay: variableRebindMixedDeterminacy (nested rebind),
injectionChildNarrowing and the matchVar sort tests (sort-driven, invisible
to the grid's sort-aligned canonical terms), and all exact-substitution /
exact-failure-reason cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ehildenb ehildenb force-pushed the match-indeterminate-bug branch from d790b71 to 1046d72 Compare June 15, 2026 13:55
…by-default match1

Eight Eval cells flip from decisive-fail (F) to indeterminate (I), matching
the branch's match1 changes: an Injection pattern against a collection subject,
and a FunctionApplication pattern against any rigid subject, now defer rather
than fail. Rewrite and Implies grids are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ehildenb ehildenb marked this pull request as ready for review June 15, 2026 20:54
@ehildenb ehildenb requested a review from jberthold June 15, 2026 20:55
Comment on lines +199 to +207
-- The matcher defers by default: the final generic rule resolves every pair
-- not handled earlier to addIndeterminate, which is always sound (the caller
-- decides what to do with an indeterminate verdict). Every row above it must
-- justify a stronger outcome: decomposition (\and, same-category descent),
-- variable binding, or decisive failure. Decisive failure is only sound for
-- two terms whose top-level categories are rigid (cannot change under
-- evaluation or instantiation, see isRigidCategory) and distinct. The few
-- explicit addIndeterminate rows below exist only to shadow a more generic
-- stronger row that would otherwise capture their cells.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This code has gone through several reorganisations and was organised very verbosely in pattern shape - subject shape order to make it easy to find the cases that need attention.
I recommend keeping it organised in the previous way, and adding footnotes to explain rationales for all cases where no helper function wraps the logic.
Will try to review the changes but it is tricky to keep the overview of all cases in the proposed form.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'd like to keep the inverted, defer-by-default form. The exhaustive "see every case at a glance" overview that the old pattern-shape × subject-shape layout provided now lives in the MatchDispatch test, which pins the S/F/I outcome of all 3×9×9 cells — any match1 change shows up there as a grid-cell diff, which is a more reliable map than 100+ hand-maintained rows.

More importantly, the eventual goal is for match1 to have no distinctions or branching on the first argument (Eval/Implies/Rewrite) at all, other than pure performance optimizations: the matcher should just report the truth (definite non-match vs. cannot-decide) and leave it to the caller to decide what an indeterminate verdict means in its mode. The defer-by-default structure is a step toward that — the mode-specific rows that remain are exactly the ones still to be eliminated, and keeping them as explicit shadows of the generic rule makes them easy to spot and remove. The verbose per-mode layout would entrench the per-mode distinctions we're trying to delete.

Comment on lines +243 to +257
{- | Whether the term's top-level category is rigid: it cannot change under
evaluation or instantiation. Domain values, injections, internalised
collections, and constructor applications are rigid: two such terms of
different categories can never become equal, so a decisive mismatch is
sound. Function applications, variables, and \and terms are not rigid.
-}
isRigidCategory :: Term -> Bool
isRigidCategory = \case
DomainValue{} -> True
Injection{} -> True
KMap{} -> True
KList{} -> True
KSet{} -> True
ConsApplication{} -> True
_ -> False

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(minor) Maybe this could be defined in Booster.Pattern.Util?
Reminded me of the indexing: all non-rigid terms get an index of Anything

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Left it in Match.hs for now — it's only used by the matcher, and Util doesn't yet have a peer predicate to group it with. You're right it's conceptually the indexing rule (Index.hs:45, variables/functions → Anything); if a second caller appears I'll lift it to Util (or unify with the index computation) then.

Comment on lines +324 to +347
case (trm1, trm2) of
(_, FunctionApplication{})
| s1IsSubsort ->
-- the subject child may evaluate into source1
addIndeterminate trm1 trm2
(_, Var{})
| s1IsSubsort ->
-- the subject child may be instantiated in source1
addIndeterminate trm1 trm2
(Var v, _)
| s2IsSubsort ->
-- pattern variable with a supersort of the subject:
-- bind with a suitable injection
bindVariable v (Injection source2 source1 trm2)
(FunctionApplication{}, _)
| s2IsSubsort ->
-- the pattern child may evaluate into source2
addIndeterminate trm1 trm2
_ ->
-- both children rigid at incompatible sorts, safe to fail
failWith $
DifferentSorts
(Injection source1 target1 trm1)
(Injection source2 target2 trm2)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Subtlety: And terms are never expected as trm2 (or trm1) in this code, therefore safe to use the _ -> catch-all case?
This is currently true but only because of the arrangement (ordering) of the matches in the caller (match1). Maybe better to use isRigidCategory here. Which will also make it so that the default is indeterminate, in the spirit of the new comment above match1.

@ehildenb ehildenb Jun 16, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done: 1910818 — the final failure is now guarded by isRigidCategory trm1, isRigidCategory trm2, with a non-rigid child deferring to addIndeterminate. Soundness no longer depends on match1 never routing an \and (or subject variable) here. No tests changed: the precise Var/FunctionApplication deferrals are still handled by the explicit cases above the catch-all.

@ehildenb ehildenb Jun 16, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I tried this and reverted it. Gating the catch-all on isRigidCategory (deferring when either child is non-rigid) regressed rpc-integration test-3934-smt: a KEVM execution that branched at depth 33 instead aborted at depth 31, because an injection child that cannot actually match now defers and aborts the rewrite.

The decisive failure should be sound, or at least preserves existing behavior: the narrowable directions (a function/variable child that can narrow into the other side) are already deferred by the explicit cases above the catch-all, so the catch-all only sees children that are rigid, or non-rigid at a sort that can't bridge — normalising the narrower injection wraps the child in an inj{narrow -> wide}(...), which the rigid wider-sorted pattern child can never equal regardless of how it resolves. And an \and child never reaches matchInj (match1 decomposes a subject \and first). I kept the decisive failure, documented that invariant in the catch-all comment (e7d4b2b), and added a regression test pinning it (464b0f8) — verified it fails under the isRigidCategory variant and passes with the decisive failure kept.

Comment on lines +788 to +791
-- (not necessarily syntactically equal) to term, so we
-- defer rather than fail in every mode: a decisive
-- MatchFailed here would skip higher-priority equations.
addIndeterminate oldTerm term

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Certainly safer to addIndeterminate here, but I don't understand the comment about "higher-priority equations". Was this a problem in the Eval mode? (maybe just rephrase the comment or provide an issue link?)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done: 1910818 — rephrased and linked #231, and fixed the priority wording: priorities count up from 1, so the hazard is evaluation falling through to a lower-priority equation (a higher priority number) that should not have been reached. To answer your question directly: yes, this is the Eval/function-evaluation concern — handleFunctionEquation routes FailedMatch → continue (try next priority) but IndeterminateMatch → abort, so a spurious decisive failure here would skip the abort and reach a lower-priority equation.

@ehildenb ehildenb Jun 16, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The issue I was having was running the Pyk prover in proxy mode, where it basically would run the haskell backend in booster-only mode, then fallback on its own to calling Kore directly. We were getting some cases where the booster was reporting "nothing else can apply, for sure" (definite match failed), where actually it should be indeterminate. We were trying to use the definite failure as a reason to skip invoking Kore, but then some proofs that were passing before are now failing (get stuck). So we need to always default to indeterminate, and only go to definite succes/failure on cases where it actually is definite. That was for the implies endpoint.

For the equation application, if we get a definite failure, then we are free t otry lower priority (higher priority number) rules. Those rules should not be tried on indeterminate (for equations). So this actually would be a soundness bug in some cases. In reality, I doubt this case was exercised in practice.

Comment on lines +441 to +453
-- The subject's static sort is only an upper bound when
-- the subject can still change shape: a function
-- application may evaluate to a term of a narrower sort,
-- and a variable may be instantiated with one. If the two
-- sorts share a subsort, a match may yet be possible, so
-- defer instead of failing decisively.
FunctionApplication{}
| sortsOverlap subsorts termSort variableSort ->
addIndeterminate (Var var) term2
Var{}
| sortsOverlap subsorts termSort variableSort ->
addIndeterminate (Var var) term2
_ -> failWith $ DifferentSorts (Var var) term2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Again maybe good to use isRigidCategory for the failWith case (and variableSort <= termSort instead of the overlap), although no And terms are expected here..

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done: 3f74ec6, but only partially, on purpose. I adopted the spirit (documented why the catch-all is sound) but kept sortsOverlap rather than switching to isRigidCategory + variableSort <= termSort.

The reason: here the _ branch is reached only for a rigid subject (exact sort) or a function/variable subject whose sort is disjoint from the variable's — sortsOverlap already deferred the overlapping case just above. A disjoint function/variable genuinely cannot match (its result/instantiation is a subsort of a sort sharing nothing with the variable's), so failing decisively is sound and precise. Gating on isRigidCategory would defer those disjoint cases too, trading real precision for robustness against an \and that match1 never routes here (it decomposes a subject \and before dispatching a variable pattern). I added a comment stating that invariant instead.

… defer non-rigid matchInj catch-all

- bindVariable: reword the indeterminate-rebind comment to spell out the
  function-evaluation soundness concern (handleFunctionEquation skip-vs-abort)
  and link issue #231, per review.
- matchInj: gate the final decisive failure on isRigidCategory of both children,
  deferring otherwise, so the catch-all no longer relies on match1's ordering to
  exclude non-rigid (\and / subject-variable) children.
- matchVar: document why the decisive-failure catch-all is sound (subject is
  rigid or sort-disjoint; \and never reaches matchVar), keeping the precise
  sortsOverlap deferral rather than widening it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ehildenb ehildenb force-pushed the match-indeterminate-bug branch from 3f74ec6 to 1910818 Compare June 16, 2026 17:02
ehildenb and others added 2 commits June 16, 2026 19:51
…able priority wording

The previous commit broadened matchInj's final catch-all to addIndeterminate on
any non-rigid child. That regressed rpc-integration test-3934-smt: a KEVM
execution that branched at depth 33 instead aborted at depth 31, because an
injection child that cannot actually match now defers. Revert to the decisive
DifferentSorts failure (the narrowable directions are already deferred by the
explicit cases above the catch-all), documenting why it is sound.

Also fix the bindVariable comment's priority wording: priorities count up from
1, so a spurious decisive failure lets evaluation fall through to a lower-
priority (higher-numbered) equation, not a "higher-priority" one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rrower-sort decisive failure

Pins that an injection-vs-injection match with a non-rigid child on the
narrower-sorted side fails decisively rather than deferring. Broadening
matchInj's catch-all to addIndeterminate on any non-rigid child (a reviewer
suggestion) regressed rpc-integration test-3934-smt, turning a KEVM execution's
branch at depth 33 into an abort at depth 31. Verified the test fails under the
broadened variant and passes with the decisive failure kept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@automergerpr-permission-manager automergerpr-permission-manager Bot merged commit 6f17f7e into master Jun 16, 2026
6 checks passed
@automergerpr-permission-manager automergerpr-permission-manager Bot deleted the match-indeterminate-bug branch June 16, 2026 20:33
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.

2 participants