Repository navigation
verify: Confirm certified counterexamples in Lean, and make the gate's Lean row decisive - #68
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.
…ded integer quantifiers in the prelude.
…d or not reported.
… at most 4096 integers.
… verdict and not an abort.
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 #67 (LS6, on #66, #65, #62, and #63), whose commits show here until they merge; merge after #67.
A counterexample the solver finds is a failure once cpp-verify's certifier has evaluated the goal at it, and nothing else checks it: no Lean user can see in their project that the goal is false, and the release gate's Lean row checks names, hashes, and replay, but no verdict, so a translation that made every goal true, or every goal false, would pass it. LM1 left exactly this for later.
Now a fallback project holds the refutation of each certified failure, which
--lean-certifychecks with the kernel. On the gate's matrix (Inputs/backend_fidelity_matrix.cpp),mainprints the 12 failures as Z3 found them; this branch prints each one(confirmed in Lean), and the gate's new row requires Lean to certify every valid case and refute every invalid one.Design
Decided for 0.2 (LM1, semantic agreement): replay certified counterexamples in Lean; a decisive Lean row in the fidelity gate.
CandidateModel::constant, so they are the values it evaluated (counterexampleAssignment,VerifyResult::Assignment). It reads a copy of the model: Z3's evaluation with completion adds what it chooses to the model it evaluates, and the first version, reading the reported model, printed a value for every variable of a failing goal, also those the diagnostics leave unknown because the goal does not depend on them (MachineIntegerEncodingTest.IrrelevantVariablesStayUnknowncaught it under the bit-vector encoding).exportLeanRefutation): a module stating the failing goal and the bodies it unfolds as the project would (its specs joinSpecs, so the LS3 trial may define them), thentheorem <goal>_refuted : ¬ <goal>: the goal applied to the values in the order it binds them (BitVec.ofInt; a heap or collection by its runs of equal cells as nestedifs), closed bycppverify_refute, whose evaluation also proves the definition instances the goal states as premises (they hold of the spec functions Lean defines).cppverify_refute(Auto.lean):decide; then evaluation by the bodies, the spec equations, memory reads, and machine comparisons, closed bydecideor bygrindwith the heap definitions, since a store relation quantifies over every address. When both fail it tries to show the instance true, in a context without the goal (which would prove any instance of itself: the first version of this probe did, and turnedpick_oneinto a false disagreement), and says why:Lean decides the goal true at the counterexample, orLean cannot decide the goal.cppverify_refute, and the probe, gets the heartbeats of a step ofcppverify_auto(cppverify.autoBudget) and catches the runtime exceptions it raises (withinBudget,tryCatchRuntimeEx), as the steps ofcppverify_autodo: running out of heartbeats or of recursion depth is a "no", and the next attempt starts with a budget of its own. The module lifts the heartbeat limit only for the goal's definitions (section Goal); the theorem keeps Lean's, which also bounds the kernel's check of the proof the attempts build (withset_option maxHeartbeats 1000,decide +kernelon a slowNatrecursion ends in(kernel) deterministic timeout). The first version had no limit at all, and the refutation of an inductive predicate at -5000 ran for over two hours.spec.post), and nothing is said of its refutation.Inthave noDecidableinstance in core; the prelude decides one by trying each integer of its range (cppIntBallDecidable,cppIntBexDecidable, through core'sNatinstances).forall(i, 0, 4, i < 3)needed it.CppVerify/Refutations/<group>/<goal>.lean, rewritten by every run, removed with its failure. Certification builds them with the proofs, and the checker takes them asrefutationentries: the theorem must state exactlyNot <goal>of the goal its own module defines, with Lean's foundations alone.(confirmed in Lean)(JSONlean_refuted). One Lean cannot decide (a goal applying acppverify::choose, which is opaque in Lean) leaves the failure as it was, with a warning. One Lean decides true would be two evaluators of one goal disagreeing: the function isUnresolved,backend.inconsistent-results, as when two solvers disagree, neverFailed.hidden_spec_true(which Z3 leavesspec.hidden, and which Lean sees the definition of), with one proof by hand,Inputs/backend_fidelity_proofs, for the postcondition ofheap_spec_read, the one goalcppverify_autodoes not prove; a fallback project confirms the counterexample of all 12 invalid ones.Measured
Every Verify source (468 files, the exhaustive decoder left out), each run with
--lean-fallback=DIR --lean-certifyand one job: 622 certified failures in 226 files. Each refutation module builds, and none is a disagreement.dynamic_storage_fail.cpp18,local_array_lowering.cpp13,reference_local_object_lowering.cpp6,modular_ownership_fail.cpp5, and others). Their goals chain stores into the allocation memories, each an equation between functions over every address (h' = fun q => ...), which Lean settles only by extensionality;decidecannot, andgrinddoes not within a step's heartbeats.cppverify::choose, opaque in Lean (choose_axioms.cpp5,choose_identity.cpp4), and the rest apply an inductive predicate, quantify over a large range or all integers, or read collections.below_a_billionof the test's input.contract of f) and checks of spec functions (termination, postconditions, their inductions, reads).The whole run takes 4466 s of single-job time.
Memory
The first push of this PR shut down CI's 16 GB Linux runners 22 to 28 minutes into
check-clang(exit 143), as #69's did after it.lean_refutation.testpeaked at 56 GB: compiling the refutation ofbelow_a_billion,forall(i, 0, 1000000000, i < 999999999), one Lean process reached 58.6 GB before failing withmaximum recursion depth has been reached. The prelude'scppIntBallDecidablereduces a range of integers to core'sNat.decidableBallLT, one level of recursion per integer, and the generated files allow a depth of 100000. Heartbeats do not count that memory: under a step's 50000 heartbeats, a range of 1000 is decided in 1.9 GB, one of 4000 runs out of heartbeats at 2.7 GB, one of 16000 at 16.1 GB, and one of 64000 reaches the depth limit at 58.7 GB. Lean's-Mlimit does not bound it either: it is checked only at the interpreter's checkpoints, and the process aborted with an uncaughtmemory_exceptionat 58.7 GB.The prelude now tries each integer of a range of at most 4096, as many as the certifier expands (
CertifyLimits::DirectExpansion), and gives a larger range the classical instance, whichdecidecannot evaluate.lean_refutation.testnow peaks at 4.0 GB (the other Lean tests: 2.8 to 3.8 GB), andbelow_a_billionstays undecided at once. No refutation is lost: none over more than about 4000 integers fit a step's heartbeats before. Four incertified_quantifier_ranges.cppused to stop Lean at the recursion limit and stay undecided; withdecidefailing at once,cppverify_evaluatereachesgrind, which proves them, and the file takes 34 s instead of 354 s.lean_refutation_memory.test(Linux) runs the budget input underpeak_memory.py: its largest process must stay below 4096 MB, andbelow_a_thousand, a range of 1000, must still be confirmed. Mutants: without the bound it fails at 58 GB, and with a bound of 100 the thousand is no longer confirmed (both killed).Assertions build
The second push's Linux assertions job failed
lean_toolchain.test: with a lake that fails, certification failed and the driver aborted withExpected<T> must be checked before access or destruction. The driver took the error ofcertifyLeanProject's result, then, choosing what the refutations rest on, tested the result again, and anExpectedholding an error that is tested again is unchecked once more. Archive replay (#67) made the same second test of its result, which no test reached. Both now move the result into anstd::optionalonce.lean_toolchain.testalso replays an archive into a project with that lake and checks the failed certification is a verdict; on an assertions build with replay's old code it aborts.Tests
suite/lean_refutation.test, onInputs/lean_refutation.cpp:add_one), a bounded quantifier (below_three), a store to memory (zero_first), and a spec Lean defines (tri_three): each(confirmed in Lean);pick_one, whose goal applies a choice: a failure, with the warning;lean_refutedand the warning record;proves True, not Not ...);cppverify_refuteon an instance that holds says Lean decides it true, and on one over every integer that Lean cannot decide it (Inputs/lean_refutation_tactic.lean);section Goalonly;Inputs/lean_refutation_budget.cpp:below_a_billion(forall(i, 0, 1000000000, i < 999999999)), which Lean would decide by trying a billion integers, ends undecided with the warning;far, whose counterexample rests on a postcondition ofgethat is not established, isUnresolved(spec.post), and nothing is said of its refutation.suite/backend_fidelity_gate.test: the new row,PASS: decisive Lean verdicts: valid cases certified, counterexamples refuted.Mutants, each built and run on
lean_refutation.testandbackend_fidelity_gate.test(the last three onlean_refutation.test): on the final code 19 of 21 are killed; a 22nd mutated a step that is now gone. A first run of the three budget mutants reported all three surviving:llvm-lit --timeoutfails at once without Python'spsutil, and the script read the missing failure as a survivor; they ran again under coreutils'timeout.counterexampleAssignmentkeeps none)lean_refutationlean_refutationlean_refutationlean_refutationlean_refutedin JSONdecidein place ofgrind)lean_refutationmkFreshExprMVarin the refutation's context)lean_refutationmaxHeartbeats := 0inwithinBudget)lean_refutation(timeout after 900 s)lean_refutationlean_refutationgrind) evaluated them too. The two attempts are now one,cppverify_evaluate, closed bydecideorgrind, and the mutant is killed.counterexampleAssignmentcall as the obligation path, which is killed.Inputs/lean_refutation_tactic.lean); the verdict it leads to (Unresolved,backend.inconsistent-results) is the one two disagreeing solvers already get.lean_refutation_memory.test(Linux): the budget input's largest process stays below 4096 MB, and a quantifier over a thousand integers is still refuted.Docs
--lean-fallback's refutations.Checked
Inputs/),docs_coverage.test, anddocs_examples.testpass (564 s, 16 workers), on the branch with the budgets.docs_examples.testpass (30 tests, 481 s, 16 workers), and the suite-wide run under Measured is with that binary.