Skip to content

SMT encoder: opt-in define-fun-rec for recursive functions - #1478

Open
kondylidou wants to merge 13 commits into
strata-org:mainfrom
kondylidou:feat/define-fun-rec-optin
Open

kondylidou wants to merge 13 commits into
strata-org:mainfrom
kondylidou:feat/define-fun-rec-optin

Conversation

@kondylidou

@kondylidou kondylidou commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Recursive functions are encoded as uninterpreted functions with per-constructor
axioms. Good for proving, but the solver cannot evaluate them during model
search, so a false obligation whose counterexample must be computed through
the function returns unknown.

This adds two options, both default off:

  • VerifyOptions.recursiveFnsAsDefineFunRec: emit the body as define-fun-rec
    and skip the axioms. With cvc5's fmf-fun the solver then finds concrete
    constructor terms.
  • VerifyOptions.obligationsToVerify: discharge only the listed obligations
    (no solver call, no result for the others). Lets a client re-query just the
    obligations a first pass left unknown, so the weaker proving mode never
    touches the rest. Mirrors proceduresToVerify, at discharge level.

Follow-up to #1432, which made define-fun-rec the default and was withdrawn.
Here the axiom encoding stays the default. The July comparison ran without
fmf-fun, which is why both encodings looked the same then.

Changes

  • Term.IF.isRec, SMT.Context.recFnsAsDefineFunRec.
  • Both encoders register the function name before encoding its body, so the
    self-call resolves to it instead of declaring an uninterpreted twin.
  • AbstractSolver.defineFunRec, for IncrementalSolver and SolverM.
  • Self-recursive only; mutual recursion under the option is an error.
  • Obligation filter in the discharge loop of Core.verify.

Test (DefineFunRecTests.lean): binary nat, obligation
toInt(a) = 73 ∧ toInt(a + b) = 200 ⊢ toInt(b) = 0, false with b = 127.
11 obligations: 9 library proof obligations + the false one (assert, ensures).

encoding proof obligations false obligation
default 9 pass unknown, candidate a=2 b=3
define-fun-rec 9 pass timeout
define-fun-rec + fmf-fun 7 pass, 2 unknown fail, a = 73, b = 127
same, obligationsToVerify = [b_is_zero] skipped fail, a = 73, b = 127

Row 2: the encoding alone does not regress proving. Row 3: fmf-fun weakens
proving, hence row 4 is the intended client pattern (Strata-Boole side, cvc5
only).

Default behaviour unchanged; full StrataTest passes.

🤖 Generated with Claude Code

@kondylidou

Copy link
Copy Markdown
Contributor Author

@shigoel

kondylidou and others added 3 commits September 23, 2026 09:58
Companion to recursiveFnsAsDefineFunRec.  The intended client pattern is a
primary pass with the default encoding and then a second pass, with
define-fun-rec + fmf-fun, over just the obligations that came back
unknown -- the weaker proving mode must not touch the others.  Without a
filter the second pass re-verifies the whole program.

When set, obligations whose label is not listed are skipped: no solver
call, no result.  Mirrors proceduresToVerify (which filters at the
pipeline level; this filters at discharge).

Test: DefineFunRecTests gains a fourth eval, obligationsToVerify :=
[b_is_zero] under define-fun-rec + fmf-fun, pinning exactly that one
obligation as fail with a = 73, b = 127.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
are skipped (no solver call, no result). Lets a client re-query a subset
of obligations, e.g. those a first pass left `unknown`, under different
options. -/
obligationsToVerify : Option (List String) := none

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I understand why this is used for the define-fun-rec example, but is the intended use case that a user gets the unknown proof obligations, then calls strata again passing in those obligations by name? This does not seem like an ideal workflow.

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.

The caller isn't a person, it's a dialect front-end: Boole's verify takes the
labels that came back unknown from the results it already has and re-runs only
those under a model-finding configuration. No label is typed by hand.

The objection still lands, the field is a mechanism with the policy left to
every caller, and a caller that gets it wrong is unsound. If you'd rather Strata
owned the loop, I'm happy to move it upstream; one constraint to know is that our
retry also substitutes a different nat encoding, so it re-verifies a different
program, not the same one under different options.

kondylidou added a commit to strata-org/Strata-Boole that referenced this pull request Sep 24, 2026
The axiom form of the nat library is what proves things, but it cannot yield a
certified model: cvc5 has nothing to evaluate, so a false obligation comes back
`unknown` with a junk candidate rather than a counterexample.  The computable
form - `pos.toInt` and `pos.fromInt` as `define-fun-rec` - is what cvc5's
`fmf-fun` can search, and it finds the constructor term.

So `Boole.verify` keeps both: the axiom form proves, and on `unknown` it
re-runs just those obligations against the computable form with `fmf-fun`.
`fmf-fun` weakens proving, which is why it never touches the primary pass and
why only a certified failure may replace an `unknown`.

Needs strata-org/Strata#1478 for `recursiveFnsAsDefineFunRec` and
`obligationsToVerify`; the Strata pin has to move with it.

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

Copy link
Copy Markdown
Contributor Author

@shigoel @atomb can I get another approval here please? :D :D

Comment thread StrataTest/Languages/Core/Tests/DefineFunRecTests.lean Outdated
Comment thread StrataTest/Languages/Core/Tests/DefineFunRecTests.lean Outdated
kondylidou and others added 4 commits September 25, 2026 10:18
A solver timeout is a property of the budget and the machine, not of the
encoding, so pinning one made the suite fail for reasons unrelated to the
feature. The measurement is kept as a comment instead.

Model values are no longer pinned either. `a` and `b` are forced by the
preconditions, but the model also assigns the query's unconstrained variables
and nothing obliges a solver to choose the same values twice. The verdicts are
what the feature promises, so those are what the tests pin.

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

# Conflicts:
#	Strata/Languages/Core/Options.lean
Folds in the branch updates made on GitHub. Content-identical to the
local tip: the merged tree matches HEAD exactly, since those commits
only merged older main, which this branch already contains.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shigoel
shigoel enabled auto-merge September 25, 2026 21:03
@shigoel
shigoel disabled auto-merge September 25, 2026 21:03

This branch has not been deployed

No deployments
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.

3 participants