Repository navigation
verify: Define spec functions in Lean when Lean proves they terminate - #66
Merged
Merged
Conversation
…the toolchain once.
…eft out, also when it comes first.
…ugh a rejected module.
… module that redeclares a name.
… in the architecture.
…the prelude on its own.
…they apply to Specs.
…hen a User lemma uses sorry.
… again only when what it imports changed.
…moved proof's imports.
…eps in the design.
…y_auto in the architecture.
…ions and candidate definitions.
…potheses and its measures as termination_by.
…eep its verdict on each.
…ision by its definition in revision 3 of cppverify_auto.
… by induction over one.
… wait for their proofs.
…ppverify_auto in the architecture.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #65 (LS2, on #62 and #63), whose commits show here until they merge; merge after #65.
Every spec function of a Lean project is
opaque cppSpec_f : .... A goal knowsfonly through the definition instances it states, the finite fuel levels the solver got, so a goal that needsfat another argument, or induction overf, cannot be proved in Lean. Onmain, forSpecs.leanhasopaque cppSpec_fn_5f5a3374726969 : Int -> Int, and neither postcondition can be proved:tri(4)is given one level (tri 4 = tri 3 + 4), and nothing is known oftriat an unknownn. Z3 proves both: the first by its unfolding oftri(4), the second by its automatic induction.Now
triis a definition,cppverify_autoevaluatestri(4), andtri_nonnegtakes a proof by induction withcppSpec_fn_5f5a3374726969.induct.Design
Decided for 0.2: a real definition when Lean proves termination, opaque otherwise.
References. Vermilion (read as data) emits every spec fn as a
defwithtermination_byand adecreasing_byladder, but wraps a recursive body inif 0 ≤ measure then body else default. Where the measure is negative and the body does not recurse, that changes the value; our research flagged it. Lean 4.35 (tested): a recursive call must be reached through a dependentifor a binder whose hypothesis proves the decrease. Undera ∧ f xora ∨ f x,ais not a hypothesis. Lean generatesf.induct.The definition is the canonical step definition (
leanSpecDefinition), printed with applications of the recursion by name and nolettemporaries, since a hoisted application would lose its guard. Every condition that reaches an application is a hypothesis there:ifbecomesif _h : c;a ∧ bbecomesif _h : a then b else False,a ∨ bbecomesif _h : a then True else b, and¬a ∨ bbecomesif _h : a then b else True, whenbapplies a spec;Each rewrite is a classical equivalence, so the definition has the spec's value at every argument; it is never guarded by the measure as a whole.
The measure is
termination_by ((m1 + 1).toNat, ...)from the spec'sdecreases. Withupper >= 0 && lower < upper,(lower + 1).toNat < (upper + 1).toNateven forlower = -1(down(n) = n < 0 ? 0 : down(n - 1) + 1), and equal components stay equal. That is the relation the termination check proves, lexicographic.Proving the decrease:
decreasing_by all_goals cppverify_decreasing, a prelude macro that triessimp_wf; omega, then with the collection and comparison wrappers unfolded, then withcppIntDivandcppIntRemunfolded and their conditions split (a measure that falls by a constant divisor,log2(n / 2), whichomegadoes not see throughcppIntDiv), thensimp_all; omega, thendecreasing_tactic.Groups: strongly connected components of the applications (Tarjan, callees first), each one
mutualblock, all definitions or all opaque.The trial (
leanSpecsText,tryLeanSpecs) runs at export: one Lean file importingAuto, the groups in order,#cppverify_axiomsafter each member. A definition Lean rejects is added withsorryAx(tested), or not at all. A group's verdict is kept only when each callee tried in the same round was accepted; otherwise it is tried again with the rejected callee opaque, until nothing changes.The cache: verdicts live in
.cppverify/specs/<sha256>, keyed by the toolchain,LeanSpecRevision, the group's text, and what each callee is, soSpecs.leanis the same on every run. Without Lean, undecided groups are opaque and nothing is kept. The scratch-pad stays opaque, as does archive replay, whose modules carry no measures.Goals and names do not change. A goal states the same definition instances, which a definition makes theorems.
Verdicts still rest on the termination cpp-verify establishes. In the test,
half's own check (combined with an unproved postcondition) is unresolved while Lean accepts its definition, and everything relying on it staysspec.termination. Letting Lean's acceptance establish that fact is left open.cppverify_autorevision 3 appends_eval: it rewrites with the equations of every specCppVerify.Specsdefines, and withcppIntDivandcppIntRem, which closes a goal at known arguments. Then_div:grind [cppIntDiv, cppIntRem], for a contract that divides by a constant (a / 2 >= 2fora >= 4), which needs no spec definition. A spec's own termination goal states the decrease its definition'sdecreasing_byproves, solog2's is proved the same way. Both come after every other step, so a goal an earlier step proves keeps its proof.Measured
Every source of the suite exported with this branch (375 sources, 9259 goals), each goal tried by revision 2 (a replica of its steps) and by revision 3:
None is proved by revision 2 and not by revision 3. The replica of revision 2 gives exactly LS2's figures on LS2's exports (5210), so the definitions change no goal an earlier step proved. In the five sources with the most new proofs, 48 of their 61 are
_div's (pointer differences divide by the element size; binary search halves an interval) and 13_eval's (the factorial constants ofmvp_factorial.cpp).The trial makes 213 of the suite's 293 spec functions definitions. Of the 80 opaque: 69 have no definition to try (20
choose, 47 heights and witnesses of inductive predicates, 2unspecified); 11 do not terminate (diverge,loop,loopy,swing,loops,sum_diverges,diverges_at_one) or terminate only by their own postcondition (g,m91,nested) or a choice's axiom (pick). The census found three that terminate by C++ division (span_sum,log2,log2_below), which the first ladder missed sinceomegadoes not see throughcppIntDiv; the division rung ofcppverify_decreasingand_divcome from that.Tests
suite/lean_spec_definitions.test, on:tri;even/odd(mutual, calls under||and&&);ack(lexicographic);half;steps, whose termination rests onhalf's postcondition: Lean rejects it, and it stays opaque;count_steps, which callssteps: rejected in the first round throughsteps' rejected definition, accepted against the opaque one;down, whose call has measure -1 where its caller's is 0;log2, whose measure falls by C++ division, andlog2_eight(log2(8) == 3), proved by_eval;reaches, with a call under a bounded existential.It checks:
tri_four,even_four,small_ack,log2_eightproved bycppverify_auto, andtri_nonnegby a proof by induction (Inputs/lean_spec_definitions_tri_nonneg.lean), allCertified;count_stepsstillspec.terminationthroughhalf;suite/lean_division.test: two contracts that divide by constants, proved by_divandCertified.Updated:
lean_modules.testandlean_collections.testexpect definitions inSpecs.lean;lean_automation.testandlean_collections.testexpect revision 3.lean_fallback.testnow runs with--no-lean-auto: the new step certifiesfibo_sixtoo, and the test is about fallback projects and their proofs, so its expectations aremain's again.Docs
Specs.leandescription, the collections section (induction overtotal), andcppverify_auto's steps and revision.--lean-project's specs.Checked
lean_spec_definitions.testandlean_division.testfail onmain(no definitions; no_divstep).docs_examples.test(34 examples, 0 failed): all pass.VerifyTests: 62 passed, 1 skipped (the parity test, which needs archives).git clang-formatclean.