diff --git a/clang/lib/Verify/Backend/Certify.cpp b/clang/lib/Verify/Backend/Certify.cpp index 55df72c841..9562568603 100644 --- a/clang/lib/Verify/Backend/Certify.cpp +++ b/clang/lib/Verify/Backend/Certify.cpp @@ -4150,6 +4150,22 @@ void sourceVariables(const LogicExpr *Expr, std::set &Bound, } } // namespace +std::shared_ptr +verify::counterexampleAssignment(const Obligation &Item, + CandidateModel &Model) { + std::vector> Variables; + std::set Bound; + sourceVariables(Item.Goal.get(), Bound, Variables); + auto Assignment = std::make_shared(); + for (const auto &[Name, Sort] : Variables) { + std::optional Given = Model.constant(Name, Sort); + if (!Given) + return nullptr; + Assignment->Values.emplace(Name, std::move(*Given)); + } + return Assignment; +} + std::vector verify::candidateValues(const ObligationModule &Module, const LogicExpr &Query, CandidateModel &Model) { diff --git a/clang/lib/Verify/Backend/Certify.h b/clang/lib/Verify/Backend/Certify.h index f9df4c387e..9c8a95c665 100644 --- a/clang/lib/Verify/Backend/Certify.h +++ b/clang/lib/Verify/Backend/Certify.h @@ -232,6 +232,18 @@ std::optional evaluateTerm(const ObligationModule &Module, const CertifyLimits &Limits = {}, std::string *Failure = nullptr); +/// The values a certified counterexample gives the free variables of the goal +/// of the obligation it fails, by name: what a refutation of that goal +/// applies it to. +struct CounterexampleAssignment { + std::map Values; +}; + +/// The assignment \p Model gives the free variables of \p Item's goal, or +/// null when it gives one no value. +std::shared_ptr +counterexampleAssignment(const Obligation &Item, CandidateModel &Model); + /// The source variables of \p Query with the values \p Model gives them, as /// a counterexample shows them; no value where the model gives none. std::vector candidateValues(const ObligationModule &Module, diff --git a/clang/lib/Verify/Backend/LeanBackend.cpp b/clang/lib/Verify/Backend/LeanBackend.cpp index 98eda10f5b..e58cd24d6c 100644 --- a/clang/lib/Verify/Backend/LeanBackend.cpp +++ b/clang/lib/Verify/Backend/LeanBackend.cpp @@ -1,5 +1,6 @@ //===--- LeanBackend.cpp --------------------------------------------------===// #include "LeanBackend.h" +#include "Certify.h" #include "ObligationSerialization.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallString.h" @@ -25,6 +26,10 @@ static thread_local std::map LeanTemporaryNames; static thread_local const LogicExpr *LeanTemporaryRoot = nullptr; static thread_local const LogicFunctionDecl *LeanActiveDefinition = nullptr; static thread_local unsigned LeanActiveDefinitionDepth = 0; +// Printing a spec's own definition for Lean to accept by well-founded +// recursion: every condition that reaches an application is a hypothesis +// there (`if _h : c`), so that the decrease can be proved from it. +static thread_local bool LeanDefinitionMode = false; static bool needsLeanNameEncoding(llvm::StringRef Name) { static const std::set Keywords = { @@ -108,6 +113,8 @@ static void printLeanCommentText(llvm::StringRef Text, llvm::raw_ostream &OS) { } } +// A sequence is a list, a set its membership test, a multiset its counts, +// and a map the value at each key it holds (CppVerify/Prelude.lean). static void printLeanSort(const LogicSort &Sort, llvm::raw_ostream &OS) { switch (Sort.Kind) { case LogicSortKind::Bool: @@ -123,17 +130,83 @@ static void printLeanSort(const LogicSort &Sort, llvm::raw_ostream &OS) { case LogicSortKind::Heap: OS << "CppHeap"; return; - case LogicSortKind::Invalid: case LogicSortKind::Seq: + OS << "List Int"; + return; case LogicSortKind::Set: + OS << "(Int → Bool)"; + return; case LogicSortKind::Multiset: + OS << "(Int → Nat)"; + return; case LogicSortKind::Map: + OS << "(Int → Option Int)"; + return; + case LogicSortKind::Invalid: LeanEmissionFailed = true; OS << "False"; return; } } +// The prelude's function for each collection operation. +static const char *leanCollectionName(LogicCollectionOp Operation) { + using Op = LogicCollectionOp; + switch (Operation) { + case Op::SeqEmpty: + return "cppSeqEmpty"; + case Op::SeqUnit: + return "cppSeqUnit"; + case Op::SeqLength: + return "cppSeqLength"; + case Op::SeqIndex: + return "cppSeqAt"; + case Op::SeqPush: + return "cppSeqPush"; + case Op::SeqSubrange: + return "cppSeqSubrange"; + case Op::SeqConcat: + return "cppSeqConcat"; + case Op::SeqContains: + return "cppSeqContains"; + case Op::SetEmpty: + return "cppSetEmpty"; + case Op::SetInsert: + return "cppSetInsert"; + case Op::SetRemove: + return "cppSetRemove"; + case Op::SetContains: + return "cppSetContains"; + case Op::SetUnion: + return "cppSetUnion"; + case Op::SetIntersect: + return "cppSetIntersect"; + case Op::SetDifference: + return "cppSetDifference"; + case Op::SetSubset: + return "cppSetSubset"; + case Op::MultisetEmpty: + return "cppMultisetEmpty"; + case Op::MultisetInsert: + return "cppMultisetInsert"; + case Op::MultisetRemove: + return "cppMultisetRemove"; + case Op::MultisetCount: + return "cppMultisetCount"; + case Op::MapEmpty: + return "cppMapEmpty"; + case Op::MapInsert: + return "cppMapInsert"; + case Op::MapRemove: + return "cppMapRemove"; + case Op::MapContains: + return "cppMapContains"; + case Op::MapGet: + return "cppMapGet"; + } + return nullptr; +} + static void collectLeanVariables(const LogicExpr *E, std::set Bound, std::map &Variables) { @@ -237,6 +310,16 @@ static const LogicFunctionDecl *findLeanFunction(llvm::StringRef Identity) { return It == LeanLogicFunctions->end() ? nullptr : &It->second; } +static bool leanAppliesSpec(const LogicExpr *E) { + if (!E) + return false; + if (E->K == VCExpr::SpecCall) + return true; + return llvm::any_of(E->Children, [](const auto &Child) { + return leanAppliesSpec(Child.get()); + }); +} + static void printLeanNativeSpecCall(const LogicExpr *Call, const LogicFunctionDecl &Function, llvm::raw_ostream &OS) { @@ -309,8 +392,33 @@ static void printVCExprLean(const VCExpr *E, llvm::raw_ostream &OS, printVCExprLean(E->Children[0].get(), OS, LeanArgument); }); break; - // Right-associative: only the last operand may be of the same level. + // Right-associative: only the last operand may be of the same level. In a + // definition, the operands before an application are its condition: + // `a ∧ b` is `if _h : a then b else False`. case VCExpr::And: + if (LeanDefinitionMode) + for (unsigned I = 1; I < E->Children.size(); ++I) + if (leanAppliesSpec(E->Children[I].get())) { + paren(LeanTop, [&] { + OS << "if _h : "; + for (unsigned J = 0; J < I; ++J) { + if (J) + OS << " ∧ "; + printVCExprLean(E->Children[J].get(), OS, + J + 1 == I ? LeanAnd : LeanAnd + 1); + } + OS << " then "; + for (unsigned J = I; J < E->Children.size(); ++J) { + if (J != I) + OS << " ∧ "; + printVCExprLean(E->Children[J].get(), OS, + J + 1 == E->Children.size() ? LeanAnd + : LeanAnd + 1); + } + OS << " else False"; + }); + return; + } paren(LeanAnd, [&] { for (unsigned I = 0; I < E->Children.size(); ++I) { if (I) @@ -320,7 +428,31 @@ static void printVCExprLean(const VCExpr *E, llvm::raw_ostream &OS, } }); break; + // `a ∨ b` is `if _h : a then True else b`, and `¬a ∨ b` is `if _h : a then b + // else True`. case VCExpr::Or: + if (LeanDefinitionMode && E->Children.size() > 1 && + llvm::any_of(llvm::drop_begin(E->Children), [](const auto &Child) { + return leanAppliesSpec(Child.get()); + })) { + paren(LeanTop, [&] { + const LogicExpr *First = E->Children[0].get(); + const bool Negated = First->K == VCExpr::Not; + OS << "if _h : "; + printVCExprLean(Negated ? First->Children[0].get() : First, OS, + LeanTop); + OS << (Negated ? " then " : " then True else "); + for (unsigned J = 1; J < E->Children.size(); ++J) { + if (J != 1) + OS << " ∨ "; + printVCExprLean(E->Children[J].get(), OS, + J + 1 == E->Children.size() ? LeanOr : LeanOr + 1); + } + if (Negated) + OS << " else True"; + }); + break; + } paren(LeanOr, [&] { for (unsigned I = 0; I < E->Children.size(); ++I) { if (I) @@ -332,7 +464,7 @@ static void printVCExprLean(const VCExpr *E, llvm::raw_ostream &OS, break; case VCExpr::Ite: paren(LeanTop, [&] { - OS << "if "; + OS << (LeanDefinitionMode ? "if _h : " : "if "); printVCExprLean(E->Children[0].get(), OS, LeanTop); OS << " then "; printVCExprLean(E->Children[1].get(), OS, LeanTop); @@ -535,10 +667,25 @@ static void printVCExprLean(const VCExpr *E, llvm::raw_ostream &OS, printVCExprLean(E->Children[3].get(), OS, LeanArgument); OS << ")"; break; - case VCExpr::Collection: - LeanEmissionFailed = true; - OS << "False"; + case VCExpr::Collection: { + const char *Name = leanCollectionName(E->CollectionOp); + if (!Name) { + LeanEmissionFailed = true; + OS << "False"; + break; + } + if (E->Children.empty()) { + OS << Name; + break; + } + OS << "(" << Name; + for (const auto &Child : E->Children) { + OS << " "; + printVCExprLean(Child.get(), OS, LeanArgument); + } + OS << ")"; break; + } case VCExpr::HeapFrame: OS << "(∀ ("; printLeanName("__frame_address", OS); @@ -575,6 +722,12 @@ static void printVCExprLean(const VCExpr *E, llvm::raw_ostream &OS, printVCExprLean(E->Children[0].get(), OS, LeanTop); return; } + // In a definition, the range of an existential that applies a spec + // is its condition, as a universal's is its antecedent. + const bool Guarded = E->K == VCExpr::Exists && LeanDefinitionMode && + leanAppliesSpec(E->Children[2].get()); + if (Guarded) + OS << "if _h : "; printVCExprLean(E->Children[0].get(), OS, LeanCompare + 1); OS << " ≤ "; printLeanName(E->Binder, OS); @@ -582,6 +735,12 @@ static void printVCExprLean(const VCExpr *E, llvm::raw_ostream &OS, printLeanName(E->Binder, OS); OS << " < "; printVCExprLean(E->Children[1].get(), OS, LeanCompare + 1); + if (Guarded) { + OS << " then "; + printVCExprLean(E->Children[2].get(), OS, LeanTop); + OS << " else False"; + return; + } OS << (E->K == VCExpr::Forall ? " → " : " ∧ "); printVCExprLean(E->Children[2].get(), OS, E->K == VCExpr::Forall ? LeanArrow : LeanAnd); @@ -1033,25 +1192,165 @@ static void emitLeanGoalDefinition(llvm::StringRef Name, const LogicExpr *Goal, emitLeanReadableGoal(Name, Goal, Variables, HasSpecAxioms, *Intros, OS); } -static void -emitLeanFunctionDeclarations(const ObligationModule &Module, - llvm::raw_ostream &OS, - std::set &EmittedFunctions) { - for (const auto &[Identity, Function] : Module.LogicFunctions) { - if (!EmittedFunctions.insert(Identity).second) +static void collectLeanApplied(const LogicExpr *E, + std::set &Applied) { + if (!E) + return; + if (E->K == VCExpr::SpecCall) + Applied.insert(E->SpecCallee); + for (const auto &Child : E->Children) + collectLeanApplied(Child.get(), Applied); +} + +static std::string leanOpaqueSpec(const LogicFunctionDecl &Function) { + std::string Text; + llvm::raw_string_ostream OS(Text); + OS << "/- spec function: "; + printLeanCommentText(Function.DisplayName, OS); + OS << " -/\n"; + OS << "opaque "; + printLeanSpecName(Function.Identity, OS); + OS << " : "; + for (const LogicFunctionParameter &Parameter : Function.Parameters) { + printLeanSort(Parameter.Sort, OS); + OS << " -> "; + } + printLeanSort(Function.ResultSort, OS); + OS << "\n\n"; + return Text; +} + +// A spec's own definition, for Lean to accept by well-founded recursion: its +// step definition, applying its recursion group by name, every condition +// that reaches an application a hypothesis there, and its termination +// measures, each m as `(m + 1).toNat`: for a caller's m at least 0 and a +// call's below it (it may be -1), the call's is below the caller's, the +// relation the termination check proves, lexicographic over a tuple. None +// for a choice, a function without a definition, or a recursive one without +// measures (an archive's). +static std::string leanSpecDefinition(const ObligationModule &Module, + const LogicFunctionDecl &Function) { + if (Function.Choice || !Function.StepDefinition) + return ""; + std::set Reached, Pending; + collectLeanApplied(Function.StepDefinition.get(), Pending); + while (!Pending.empty()) { + std::string Next = *Pending.begin(); + Pending.erase(Pending.begin()); + if (!Reached.insert(Next).second) continue; - OS << "/- spec function: "; - printLeanCommentText(Function.DisplayName, OS); - OS << " -/\n"; - OS << "opaque "; - printLeanSpecName(Identity, OS); + auto It = Module.LogicFunctions.find(Next); + if (It != Module.LogicFunctions.end() && It->second.StepDefinition) + collectLeanApplied(It->second.StepDefinition.get(), Pending); + } + const bool Recursive = Reached.count(Function.Identity); + if (Recursive && Function.Decreases.empty()) + return ""; + + const bool FailedBefore = LeanEmissionFailed; + LeanEmissionFailed = false; + auto Temporaries = std::move(LeanTemporaryNames); + LeanTemporaryNames.clear(); + LeanDefinitionMode = true; + LeanEmitBudget = 1000000; + std::string Text; + llvm::raw_string_ostream OS(Text); + OS << "/- spec function: "; + printLeanCommentText(Function.DisplayName, OS); + OS << " -/\n"; + OS << "def "; + printLeanSpecName(Function.Identity, OS); + for (const LogicFunctionParameter &Parameter : Function.Parameters) { + OS << " ("; + printLeanName(Parameter.Name, OS); OS << " : "; - for (const LogicFunctionParameter &Parameter : Function.Parameters) { - printLeanSort(Parameter.Sort, OS); - OS << " -> "; + printLeanSort(Parameter.Sort, OS); + OS << ")"; + } + OS << " : "; + printLeanSort(Function.ResultSort, OS); + OS << " :=\n "; + printVCExprLean(Function.StepDefinition.get(), OS, LeanTop); + if (Recursive) { + OS << "\ntermination_by "; + if (Function.Decreases.size() > 1) + OS << "("; + for (unsigned I = 0; I < Function.Decreases.size(); ++I) { + if (I) + OS << ", "; + OS << "(("; + printVCExprLean(Function.Decreases[I].get(), OS, LeanTop); + OS << ") + 1).toNat"; } - printLeanSort(Function.ResultSort, OS); - OS << "\n\n"; + if (Function.Decreases.size() > 1) + OS << ")"; + OS << "\ndecreasing_by all_goals cppverify_decreasing"; + } + OS << "\n\n"; + LeanDefinitionMode = false; + LeanTemporaryNames = std::move(Temporaries); + const bool Failed = LeanEmissionFailed; + LeanEmissionFailed = FailedBefore; + return Failed ? "" : Text; +} + +// A quantifier over a range of integers, as a goal states one, is decided by +// trying each integer of the range, as core Lean decides one over a range of +// naturals, when the range has at most 4096 integers, as many as the +// certifier expands (CertifyLimits::DirectExpansion). Each integer is a level +// of recursion whose memory heartbeats do not count: under the generated +// files' recursion depth a range of 64000 took 58 GB before failing, and one +// of 4000 already spent a step's heartbeats. A larger range has the classical +// instance, which decide cannot evaluate. +static constexpr llvm::StringLiteral LeanBoundedQuantifierPrelude = + R"lean(instance cppIntBallDecidable (lo hi : Int) (p : Int → Prop) [DecidablePred p] : + Decidable (∀ x : Int, lo ≤ x ∧ x < hi → p x) := + if (hi - lo).toNat ≤ 4096 then + decidable_of_iff (∀ k : Nat, k < (hi - lo).toNat → p (lo + k)) (by + constructor + · intro h x hx + have := h (x - lo).toNat (by omega) + rwa [show lo + ((x - lo).toNat : Int) = x by omega] at this + · intro h k hk + exact h _ ⟨by omega, by omega⟩) + else Classical.propDecidable _ + +instance cppIntBexDecidable (lo hi : Int) (p : Int → Prop) [DecidablePred p] : + Decidable (∃ x : Int, lo ≤ x ∧ x < hi ∧ p x) := + if (hi - lo).toNat ≤ 4096 then + decidable_of_iff (∃ k : Nat, k < (hi - lo).toNat ∧ p (lo + k)) (by + constructor + · rintro ⟨k, hk, hp⟩ + exact ⟨lo + k, by omega, by omega, hp⟩ + · rintro ⟨x, h1, h2, hp⟩ + refine ⟨(x - lo).toNat, by omega, ?_⟩ + rwa [show lo + ((x - lo).toNat : Int) = x by omega]) + else Classical.propDecidable _ + +)lean"; + +static void emitLeanFunctionDeclarations( + const ObligationModule &Module, llvm::raw_ostream &OS, + std::set &EmittedFunctions, + std::map *Specs) { + for (const auto &[Identity, Function] : Module.LogicFunctions) { + if (Specs) { + auto [It, Inserted] = Specs->try_emplace(Identity); + LeanSpecDeclaration &Declaration = It->second; + if (Inserted) { + llvm::raw_string_ostream Name(Declaration.Name); + printLeanSpecName(Identity, Name); + Declaration.Opaque = leanOpaqueSpec(Function); + } + if (Declaration.Definition.empty()) { + Declaration.Definition = leanSpecDefinition(Module, Function); + Declaration.Applies.clear(); + collectLeanApplied(Function.StepDefinition.get(), Declaration.Applies); + } + continue; + } + if (EmittedFunctions.insert(Identity).second) + OS << leanOpaqueSpec(Function); } } @@ -1087,12 +1386,439 @@ static void emitLeanFunctionBodies(const ObligationModule &Module, } } +// Collections of mathematical integers, with the total meaning +// gives each operation, and lemmas that state it for simp, grind, and proofs +// by hand. Checking a goal never needs them: they are theorems of the +// definitions. +static constexpr llvm::StringLiteral LeanCollectionPrelude = + R"lean(/- Collections of mathematical integers, total as defines + them: a sequence is a list, a set its membership test, a multiset its + counts, and a map the value at each key it holds. -/ + +def cppSeqEmpty : List Int := [] +def cppSeqUnit (x : Int) : List Int := [x] +def cppSeqLength (s : List Int) : Int := s.length +def cppSeqAt (s : List Int) (i : Int) : Int := + if i < 0 then 0 else s[i.toNat]?.getD 0 +def cppSeqPush (s : List Int) (x : Int) : List Int := s ++ [x] +def cppSeqConcat (s t : List Int) : List Int := s ++ t +def cppSeqSubrange (s : List Int) (lo hi : Int) : List Int := + (s.drop (max 0 (min lo s.length)).toNat).take + (max (max 0 (min lo s.length)) (min hi s.length) - + max 0 (min lo s.length)).toNat +def cppSeqContains (s : List Int) (x : Int) : Prop := x ∈ s +instance (s : List Int) (x : Int) : Decidable (cppSeqContains s x) := + inferInstanceAs (Decidable (x ∈ s)) + +def cppSetEmpty : Int → Bool := fun _ => false +def cppSetInsert (s : Int → Bool) (x : Int) : Int → Bool := + fun y => y == x || s y +def cppSetRemove (s : Int → Bool) (x : Int) : Int → Bool := + fun y => y != x && s y +def cppSetContains (s : Int → Bool) (x : Int) : Prop := s x = true +instance (s : Int → Bool) (x : Int) : Decidable (cppSetContains s x) := + inferInstanceAs (Decidable (s x = true)) +def cppSetUnion (s t : Int → Bool) : Int → Bool := fun y => s y || t y +def cppSetIntersect (s t : Int → Bool) : Int → Bool := fun y => s y && t y +def cppSetDifference (s t : Int → Bool) : Int → Bool := + fun y => s y && !t y +def cppSetSubset (s t : Int → Bool) : Prop := ∀ y, s y = true → t y = true + +def cppMultisetEmpty : Int → Nat := fun _ => 0 +def cppMultisetInsert (m : Int → Nat) (x : Int) : Int → Nat := + fun y => if y = x then m y + 1 else m y +def cppMultisetRemove (m : Int → Nat) (x : Int) : Int → Nat := + fun y => if y = x then m y - 1 else m y +def cppMultisetCount (m : Int → Nat) (x : Int) : Int := m x + +def cppMapEmpty : Int → Option Int := fun _ => none +def cppMapInsert (m : Int → Option Int) (k v : Int) : Int → Option Int := + fun y => if y = k then some v else m y +def cppMapRemove (m : Int → Option Int) (k : Int) : Int → Option Int := + fun y => if y = k then none else m y +def cppMapContains (m : Int → Option Int) (k : Int) : Prop := + (m k).isSome = true +instance (m : Int → Option Int) (k : Int) : Decidable (cppMapContains m k) := + inferInstanceAs (Decidable ((m k).isSome = true)) +def cppMapGet (m : Int → Option Int) (k : Int) : Int := (m k).getD 0 + +/- What each operation means, for simp, grind, and proofs by hand. -/ + +private theorem cppverify_ite_pos {c : Prop} [Decidable c] {α : Sort _} + {a b : α} (h : c) : (if c then a else b) = a := by simp [h] +private theorem cppverify_ite_neg {c : Prop} [Decidable c] {α : Sort _} + {a b : α} (h : ¬c) : (if c then a else b) = b := by simp [h] + +@[simp, grind =] theorem cppSeqLength_empty : cppSeqLength cppSeqEmpty = 0 := rfl +@[simp, grind =] theorem cppSeqLength_unit (x : Int) : + cppSeqLength (cppSeqUnit x) = 1 := rfl +@[simp, grind =] theorem cppSeqLength_push (s : List Int) (x : Int) : + cppSeqLength (cppSeqPush s x) = cppSeqLength s + 1 := by + simp [cppSeqLength, cppSeqPush] +@[simp, grind =] theorem cppSeqLength_concat (s t : List Int) : + cppSeqLength (cppSeqConcat s t) = cppSeqLength s + cppSeqLength t := by + simp [cppSeqLength, cppSeqConcat] +theorem cppSeqLength_nonneg (s : List Int) : 0 ≤ cppSeqLength s := by + simp [cppSeqLength] +grind_pattern cppSeqLength_nonneg => cppSeqLength s +@[simp, grind =] theorem cppSeqLength_subrange (s : List Int) (lo hi : Int) : + cppSeqLength (cppSeqSubrange s lo hi) = + max (max 0 (min lo (cppSeqLength s))) (min hi (cppSeqLength s)) - + max 0 (min lo (cppSeqLength s)) := by + simp only [cppSeqLength, cppSeqSubrange, List.length_take, List.length_drop] + omega + +theorem cppSeqAt_out (s : List Int) (i : Int) + (h : i < 0 ∨ cppSeqLength s ≤ i) : cppSeqAt s i = 0 := by + unfold cppSeqAt cppSeqLength at * + split + · rfl + · rw [List.getElem?_eq_none (by omega)]; rfl +@[simp, grind =] theorem cppSeqAt_empty (i : Int) : cppSeqAt cppSeqEmpty i = 0 := + cppSeqAt_out [] i (by simp [cppSeqLength]; omega) +@[simp, grind =] theorem cppSeqAt_push (s : List Int) (x i : Int) : + cppSeqAt (cppSeqPush s x) i = + if i = cppSeqLength s then x else cppSeqAt s i := by + unfold cppSeqAt cppSeqPush cppSeqLength + by_cases h0 : i < 0 + · simp only [h0, ↓reduceIte, show ¬ i = (s.length : Int) by omega] + · by_cases he : i = s.length + · subst he; simp only [h0, ↓reduceIte]; simp + · simp only [h0, he, ↓reduceIte] + by_cases hl : i.toNat < s.length + · rw [List.getElem?_append_left hl] + · rw [List.getElem?_append_right (by omega), + List.getElem?_eq_none (show s.length ≤ i.toNat by omega), + List.getElem?_eq_none (by simp; omega)] +@[simp, grind =] theorem cppSeqAt_concat (s t : List Int) (i : Int) : + cppSeqAt (cppSeqConcat s t) i = + if i < cppSeqLength s then cppSeqAt s i + else cppSeqAt t (i - cppSeqLength s) := by + unfold cppSeqAt cppSeqLength cppSeqConcat + by_cases h0 : i < 0 + · simp only [h0, ↓reduceIte, show i < (s.length : Int) by omega] + · by_cases hl : i < s.length + · simp only [h0, hl, ↓reduceIte] + rw [List.getElem?_append_left (by omega)] + · simp only [h0, hl, ↓reduceIte, show ¬ (i - s.length < 0) by omega] + rw [List.getElem?_append_right (by omega)] + congr 3; omega +@[simp, grind =] theorem cppSeqAt_unit (x i : Int) : + cppSeqAt (cppSeqUnit x) i = if i = 0 then x else 0 := by + unfold cppSeqAt cppSeqUnit + by_cases h0 : i < 0 + · simp only [h0, ↓reduceIte, show ¬ i = 0 by omega] + · by_cases h : i = 0 + · subst h; simp + · simp only [h0, h, ↓reduceIte] + rw [List.getElem?_eq_none (by simp; omega)]; rfl +@[simp, grind =] theorem cppSeqAt_subrange (s : List Int) (lo hi i : Int) : + cppSeqAt (cppSeqSubrange s lo hi) i = + if 0 ≤ i ∧ i < cppSeqLength (cppSeqSubrange s lo hi) + then cppSeqAt s (max 0 (min lo (cppSeqLength s)) + i) else 0 := by + rw [cppSeqLength_subrange] + unfold cppSeqAt cppSeqSubrange cppSeqLength + by_cases h0 : i < 0 + · simp only [h0, ↓reduceIte, show ¬ (0 ≤ i) by omega, false_and] + · simp only [h0, ↓reduceIte] + split + · rw [List.getElem?_take, cppverify_ite_pos (by omega), List.getElem?_drop, + cppverify_ite_neg (show ¬ (max 0 (min lo (s.length : Int)) + i < 0) by omega)] + congr 3; omega + · rw [List.getElem?_take, cppverify_ite_neg (by omega)]; rfl +@[simp, grind =] theorem cppSeqContains_empty (x : Int) : + cppSeqContains cppSeqEmpty x ↔ False := by + simp [cppSeqContains, cppSeqEmpty] +@[simp, grind =] theorem cppSeqContains_unit (y x : Int) : + cppSeqContains (cppSeqUnit y) x ↔ x = y := by + simp [cppSeqContains, cppSeqUnit] +@[simp, grind =] theorem cppSeqContains_push (s : List Int) (y x : Int) : + cppSeqContains (cppSeqPush s y) x ↔ cppSeqContains s x ∨ x = y := by + simp [cppSeqContains, cppSeqPush] +@[simp, grind =] theorem cppSeqContains_concat (s t : List Int) (x : Int) : + cppSeqContains (cppSeqConcat s t) x ↔ + cppSeqContains s x ∨ cppSeqContains t x := by + simp [cppSeqContains, cppSeqConcat] +@[simp, grind =] theorem cppSeqSubrange_all (s : List Int) : + cppSeqSubrange s 0 (cppSeqLength s) = s := by + unfold cppSeqSubrange cppSeqLength + rw [show (max 0 (min (0 : Int) (s.length : Int))).toNat = 0 by omega, + show (max (max 0 (min (0 : Int) (s.length : Int))) + (min (s.length : Int) (s.length : Int)) - + max 0 (min (0 : Int) (s.length : Int))).toNat = s.length by omega] + simp +@[simp, grind =] theorem cppSeqSubrange_concat_left (s t : List Int) : + cppSeqSubrange (cppSeqConcat s t) 0 (cppSeqLength s) = s := by + unfold cppSeqSubrange cppSeqConcat cppSeqLength + have hl : (s ++ t).length = s.length + t.length := List.length_append + rw [show (max 0 (min (0 : Int) ((s ++ t).length : Int))).toNat = 0 by omega, + show (max (max 0 (min (0 : Int) ((s ++ t).length : Int))) + (min (s.length : Int) ((s ++ t).length : Int)) - + max 0 (min (0 : Int) ((s ++ t).length : Int))).toNat = s.length by + omega] + simp +@[simp, grind =] theorem cppSeqSubrange_concat_right (s t : List Int) : + cppSeqSubrange (cppSeqConcat s t) (cppSeqLength s) + (cppSeqLength s + cppSeqLength t) = t := by + unfold cppSeqSubrange cppSeqConcat cppSeqLength + have hl : (s ++ t).length = s.length + t.length := List.length_append + rw [show (max 0 (min (s.length : Int) ((s ++ t).length : Int))).toNat = + s.length by omega, + show (max (max 0 (min (s.length : Int) ((s ++ t).length : Int))) + (min ((s.length : Int) + (t.length : Int)) ((s ++ t).length : Int)) - + max 0 (min (s.length : Int) ((s ++ t).length : Int))).toNat = + t.length by omega] + simp +@[simp, grind =] theorem cppSeqSubrange_push (s : List Int) (x : Int) : + cppSeqSubrange (cppSeqPush s x) 0 (cppSeqLength s) = s := + cppSeqSubrange_concat_left s [x] +theorem cppSeqContains_iff (s : List Int) (x : Int) : + cppSeqContains s x ↔ + ∃ i, 0 ≤ i ∧ i < cppSeqLength s ∧ cppSeqAt s i = x := by + unfold cppSeqContains cppSeqLength cppSeqAt + rw [List.mem_iff_getElem] + constructor + · rintro ⟨i, hi, he⟩ + refine ⟨i, by omega, by omega, ?_⟩ + rw [cppverify_ite_neg (by omega), Int.toNat_natCast, List.getElem?_eq_getElem hi] + simpa using he + · rintro ⟨i, h0, hl, he⟩ + refine ⟨i.toNat, by omega, ?_⟩ + rw [cppverify_ite_neg (by omega), List.getElem?_eq_getElem (by omega)] at he + simpa using he +theorem cppSeq_ext (s t : List Int) (hl : cppSeqLength s = cppSeqLength t) + (h : ∀ i, 0 ≤ i → i < cppSeqLength s → cppSeqAt s i = cppSeqAt t i) : + s = t := by + unfold cppSeqLength at hl + apply List.ext_getElem (by omega) + intro i h1 h2 + have := h i (by omega) (by unfold cppSeqLength; omega) + unfold cppSeqAt at this + simp only [show ¬ ((i : Int) < 0) by omega, ↓reduceIte, Int.toNat_natCast, + List.getElem?_eq_getElem h1, List.getElem?_eq_getElem h2, + Option.getD_some] at this + exact this + +@[simp, grind =] theorem cppSetContains_empty (x : Int) : + cppSetContains cppSetEmpty x ↔ False := by simp [cppSetContains, cppSetEmpty] +@[simp, grind =] theorem cppSetContains_insert (s : Int → Bool) (x y : Int) : + cppSetContains (cppSetInsert s x) y ↔ y = x ∨ cppSetContains s y := by + simp [cppSetContains, cppSetInsert] +@[simp, grind =] theorem cppSetContains_remove (s : Int → Bool) (x y : Int) : + cppSetContains (cppSetRemove s x) y ↔ y ≠ x ∧ cppSetContains s y := by + simp [cppSetContains, cppSetRemove] +@[simp, grind =] theorem cppSetContains_union (s t : Int → Bool) (y : Int) : + cppSetContains (cppSetUnion s t) y ↔ + cppSetContains s y ∨ cppSetContains t y := by + simp [cppSetContains, cppSetUnion] +@[simp, grind =] theorem cppSetContains_intersect (s t : Int → Bool) (y : Int) : + cppSetContains (cppSetIntersect s t) y ↔ + cppSetContains s y ∧ cppSetContains t y := by + simp [cppSetContains, cppSetIntersect] +@[simp, grind =] theorem cppSetContains_difference (s t : Int → Bool) (y : Int) : + cppSetContains (cppSetDifference s t) y ↔ + cppSetContains s y ∧ ¬ cppSetContains t y := by + simp [cppSetContains, cppSetDifference] +@[simp, grind =] theorem cppSetSubset_iff (s t : Int → Bool) : + cppSetSubset s t ↔ ∀ y, cppSetContains s y → cppSetContains t y := Iff.rfl +theorem cppSet_ext (s t : Int → Bool) + (h : ∀ y, cppSetContains s y ↔ cppSetContains t y) : s = t := by + funext y + have := h y + unfold cppSetContains at this + cases hs : s y <;> cases ht : t y <;> simp_all + +@[simp, grind =] theorem cppMultisetCount_empty (x : Int) : + cppMultisetCount cppMultisetEmpty x = 0 := rfl +@[simp, grind =] theorem cppMultisetCount_insert (m : Int → Nat) (x y : Int) : + cppMultisetCount (cppMultisetInsert m x) y = + if y = x then cppMultisetCount m y + 1 else cppMultisetCount m y := by + unfold cppMultisetCount cppMultisetInsert; split <;> simp +@[simp, grind =] theorem cppMultisetCount_remove (m : Int → Nat) (x y : Int) : + cppMultisetCount (cppMultisetRemove m x) y = + if y = x then max 0 (cppMultisetCount m y - 1) + else cppMultisetCount m y := by + unfold cppMultisetCount cppMultisetRemove; split <;> omega +theorem cppMultisetCount_nonneg (m : Int → Nat) (x : Int) : + 0 ≤ cppMultisetCount m x := by simp [cppMultisetCount] +grind_pattern cppMultisetCount_nonneg => cppMultisetCount m x +theorem cppMultiset_ext (m n : Int → Nat) + (h : ∀ y, cppMultisetCount m y = cppMultisetCount n y) : m = n := by + funext y; have := h y; unfold cppMultisetCount at this; omega + +@[simp, grind =] theorem cppMapContains_empty (k : Int) : + cppMapContains cppMapEmpty k ↔ False := by simp [cppMapContains, cppMapEmpty] +@[simp, grind =] theorem cppMapContains_insert (m : Int → Option Int) (k v y : Int) : + cppMapContains (cppMapInsert m k v) y ↔ y = k ∨ cppMapContains m y := by + unfold cppMapContains cppMapInsert; by_cases h : y = k <;> simp [h] +@[simp, grind =] theorem cppMapContains_remove (m : Int → Option Int) (k y : Int) : + cppMapContains (cppMapRemove m k) y ↔ y ≠ k ∧ cppMapContains m y := by + unfold cppMapContains cppMapRemove; by_cases h : y = k <;> simp [h] +@[simp, grind =] theorem cppMapGet_empty (k : Int) : cppMapGet cppMapEmpty k = 0 := rfl +@[simp, grind =] theorem cppMapGet_insert (m : Int → Option Int) (k v y : Int) : + cppMapGet (cppMapInsert m k v) y = if y = k then v else cppMapGet m y := by + unfold cppMapGet cppMapInsert; split <;> simp +@[simp, grind =] theorem cppMapGet_remove (m : Int → Option Int) (k y : Int) : + cppMapGet (cppMapRemove m k) y = if y = k then 0 else cppMapGet m y := by + unfold cppMapGet cppMapRemove; split <;> simp +theorem cppMapGet_out (m : Int → Option Int) (k : Int) + (h : ¬ cppMapContains m k) : cppMapGet m k = 0 := by + unfold cppMapGet; unfold cppMapContains at h; cases hm : m k <;> simp_all +theorem cppMap_ext (m n : Int → Option Int) + (hc : ∀ y, cppMapContains m y ↔ cppMapContains n y) + (hg : ∀ y, cppMapGet m y = cppMapGet n y) : m = n := by + funext y + have c := hc y; have g := hg y + unfold cppMapGet at g + unfold cppMapContains at c + cases hm : m y <;> cases hn : n y <;> simp_all + +)lean"; + +// The definitions every exported goal is stated with: the heap model, the +// machine comparisons, C++ division, and the collections; a project's goals +// also have the lemmas their readable forms' bridges use. +void verify::emitLeanPrelude(llvm::raw_ostream &OS, bool Project) { + OS << "noncomputable section\n\n"; + OS << "set_option maxRecDepth 100000\n"; + OS << "set_option maxHeartbeats 0\n\n"; + OS << "local instance cppVerifyPropDecidable (p : Prop) : Decidable p :=\n"; + OS << " Classical.propDecidable p\n\n"; + OS << "abbrev CppHeap := Int -> Int\n\n"; + OS << "def heapSelectInt (h : CppHeap) (p : Int) : Int := h p\n"; + OS << "abbrev heapSelectBool (h : CppHeap) (p : Int) : Prop := h p ≠ 0\n"; + OS << "def heapSelectBv (w : Nat) (h : CppHeap) (p : Int) : BitVec w :=\n"; + OS << " BitVec.ofInt w (h p)\n\n"; + OS << "def heapStoreInt (h : CppHeap) (p v : Int) (h' : CppHeap) : " + "Prop :=\n"; + OS << " h' = fun q => if q = p then v else h q\n"; + OS << "def heapStoreBool (h : CppHeap) (p : Int) (v : Prop)\n"; + OS << " (h' : CppHeap) : Prop :=\n"; + OS << " h' = fun q => if q = p then\n"; + OS << " @ite Int v (Classical.propDecidable v) 1 0 else h q\n"; + OS << "def heapStoreBv {w : Nat} (h : CppHeap) (p : Int) (v : BitVec w)\n"; + OS << " (h' : CppHeap) : Prop :=\n"; + OS << " h' = fun q => if q = p then Int.ofNat v.toNat else h q\n\n"; + OS << "opaque validPtr : Int -> Prop\n\n"; + // Reducible, so instance search sees the Bool comparison and decides it. + OS << "abbrev cppBvUlt {w : Nat} (x y : BitVec w) : Prop := x.ult y = " + "true\n"; + OS << "abbrev cppBvUle {w : Nat} (x y : BitVec w) : Prop := x.ule y = " + "true\n"; + OS << "abbrev cppBvSlt {w : Nat} (x y : BitVec w) : Prop := x.slt y = " + "true\n"; + OS << "abbrev cppBvSle {w : Nat} (x y : BitVec w) : Prop := x.sle y = " + "true\n\n"; + OS << "def cppIntDiv (x y : Int) : Int :=\n"; + OS << " if y = 0 then 0 else\n"; + OS << " let q := (if x < 0 then -x else x) / " + "(if y < 0 then -y else y)\n"; + OS << " if (x < 0) ≠ (y < 0) then -q else q\n"; + OS << "def cppIntRem (x y : Int) : Int :=\n"; + OS << " if y = 0 then x else x - cppIntDiv x y * y\n\n"; + OS << LeanBoundedQuantifierPrelude; + OS << LeanCollectionPrelude; + OS << "/- Proves that an application in a spec's definition lowers its " + "measure. -/\n"; + OS << "macro \"cppverify_decreasing\" : tactic => `(tactic| first\n"; + OS << " | (simp_wf; omega)\n"; + OS << " | (simp_wf; simp only [cppSeqLength, cppMultisetCount, cppBvSlt, " + "cppBvSle,\n"; + OS << " cppBvUlt, cppBvUle] at *; omega)\n"; + // C++ division and remainder by a constant, which omega reads once the + // signs are split. + OS << " | (simp_wf; simp only [cppIntDiv, cppIntRem] at *;\n"; + OS << " (repeat' split) <;> simp_all <;> omega)\n"; + OS << " | (simp_wf; simp_all; omega)\n"; + OS << " | decreasing_tactic)\n\n"; + if (Project) { + // What the bridges from readable goals use. + OS << "theorem cppverify_imp_or {a b : Prop} (h : a → b) : ¬a ∨ b :=\n"; + OS << " (Classical.em a).elim (fun ha => Or.inr (h ha)) Or.inl\n"; + OS << "theorem cppverify_not_imp_or {a b : Prop} (h : ¬a → b) : a ∨ b " + ":=\n"; + OS << " (Classical.em a).elim Or.inl (fun ha => Or.inr (h ha))\n\n"; + } +} + +// A piecewise-constant function of the integers as a Lean term: the cell +// of each address, from the run it lies in. +static void printLeanRuns(const HeapValue &Runs, llvm::StringRef Before, + llvm::StringRef After, llvm::raw_ostream &OS) { + OS << "(fun (a : Int) => "; + for (auto It = Runs.Breaks.rbegin(); It != Runs.Breaks.rend(); ++It) + OS << "if a ≥ (" << It->first.toDecimal() << " : Int) then " << Before + << It->second.toDecimal() << After << " else "; + OS << Before << Runs.Default.toDecimal() << After << ")"; +} + +// A model's value as a Lean term of its sort; false when it has none. +static bool printLeanValue(const LogicValue &Value, const LogicSort &Sort, + llvm::raw_ostream &OS) { + switch (Sort.Kind) { + case LogicSortKind::Bool: + if (Value.K != LogicValue::Kind::Bool) + return false; + OS << (Value.Truth ? "True" : "False"); + return true; + case LogicSortKind::MathematicalInteger: + case LogicSortKind::Pointer: + if (Value.K != LogicValue::Kind::Integer) + return false; + OS << "(" << Value.Integer.toDecimal() << " : Int)"; + return true; + case LogicSortKind::BitVector: + if (Value.K != LogicValue::Kind::Integer) + return false; + OS << "(BitVec.ofInt " << Sort.BitWidth << " (" << Value.Integer.toDecimal() + << "))"; + return true; + case LogicSortKind::Heap: + if (Value.K != LogicValue::Kind::Heap || !Value.Heap) + return false; + printLeanRuns(*Value.Heap, "(", " : Int)", OS); + return true; + case LogicSortKind::Seq: + if (Value.K != LogicValue::Kind::Seq || !Value.Elements) + return false; + OS << "(["; + for (size_t I = 0; I != Value.Elements->size(); ++I) + OS << (I ? ", " : "") << "(" << (*Value.Elements)[I].toDecimal() + << " : Int)"; + OS << "] : List Int)"; + return true; + case LogicSortKind::Set: + if (Value.K != LogicValue::Kind::Set || !Value.Heap) + return false; + printLeanRuns(*Value.Heap, "decide ((", " : Int) ≠ 0)", OS); + return true; + case LogicSortKind::Multiset: + if (Value.K != LogicValue::Kind::Multiset || !Value.Heap) + return false; + printLeanRuns(*Value.Heap, "((", " : Int).toNat)", OS); + return true; + case LogicSortKind::Map: + if (Value.K != LogicValue::Kind::Map || !Value.Heap || !Value.Values) + return false; + OS << "(fun (k : Int) => if "; + printLeanRuns(*Value.Heap, "(", " : Int)", OS); + OS << " k ≠ 0 then some ("; + printLeanRuns(*Value.Values, "(", " : Int)", OS); + OS << " k) else none)"; + return true; + case LogicSortKind::Invalid: + return false; + } + return false; +} + VerifyResult verify::exportLeanScratchPad( const ObligationModule &Module, llvm::raw_ostream &OS, bool EmitPreamble, std::set &EmittedFunctions, std::set &EmittedTheorems, unsigned ModuleIndex, std::vector *ProjectGoals, - const std::set *Selection) { + const std::set *Selection, + std::map *Specs) { VerifyResult Result; if (!Module.CorrectnessGoal || !Module.CounterexampleQuery) { Result.Status = VerifyStatus::Unresolved; @@ -1130,60 +1856,22 @@ VerifyResult verify::exportLeanScratchPad( OS << " This file exports canonical obligations but does not certify " "them. -/\n\n"; } - OS << "noncomputable section\n\n"; - OS << "set_option maxRecDepth 100000\n"; - OS << "set_option maxHeartbeats 0\n\n"; - OS << "local instance cppVerifyPropDecidable (p : Prop) : Decidable p :=\n"; - OS << " Classical.propDecidable p\n\n"; - OS << "abbrev CppHeap := Int -> Int\n\n"; - OS << "def heapSelectInt (h : CppHeap) (p : Int) : Int := h p\n"; - OS << "abbrev heapSelectBool (h : CppHeap) (p : Int) : Prop := h p ≠ 0\n"; - OS << "def heapSelectBv (w : Nat) (h : CppHeap) (p : Int) : BitVec w :=\n"; - OS << " BitVec.ofInt w (h p)\n\n"; - OS << "def heapStoreInt (h : CppHeap) (p v : Int) (h' : CppHeap) : " - "Prop :=\n"; - OS << " h' = fun q => if q = p then v else h q\n"; - OS << "def heapStoreBool (h : CppHeap) (p : Int) (v : Prop)\n"; - OS << " (h' : CppHeap) : Prop :=\n"; - OS << " h' = fun q => if q = p then\n"; - OS << " @ite Int v (Classical.propDecidable v) 1 0 else h q\n"; - OS << "def heapStoreBv {w : Nat} (h : CppHeap) (p : Int) (v : BitVec w)\n"; - OS << " (h' : CppHeap) : Prop :=\n"; - OS << " h' = fun q => if q = p then Int.ofNat v.toNat else h q\n\n"; - OS << "opaque validPtr : Int -> Prop\n\n"; - // Reducible, so instance search sees the Bool comparison and decides it. - OS << "abbrev cppBvUlt {w : Nat} (x y : BitVec w) : Prop := x.ult y = " - "true\n"; - OS << "abbrev cppBvUle {w : Nat} (x y : BitVec w) : Prop := x.ule y = " - "true\n"; - OS << "abbrev cppBvSlt {w : Nat} (x y : BitVec w) : Prop := x.slt y = " - "true\n"; - OS << "abbrev cppBvSle {w : Nat} (x y : BitVec w) : Prop := x.sle y = " - "true\n\n"; - OS << "def cppIntDiv (x y : Int) : Int :=\n"; - OS << " if y = 0 then 0 else\n"; - OS << " let q := (if x < 0 then -x else x) / " - "(if y < 0 then -y else y)\n"; - OS << " if (x < 0) ≠ (y < 0) then -q else q\n"; - OS << "def cppIntRem (x y : Int) : Int :=\n"; - OS << " if y = 0 then x else x - cppIntDiv x y * y\n\n"; - if (ProjectGoals) { - // What the bridges from readable goals use. - OS << "theorem cppverify_imp_or {a b : Prop} (h : a → b) : ¬a ∨ b :=\n"; - OS << " (Classical.em a).elim (fun ha => Or.inr (h ha)) Or.inl\n"; - OS << "theorem cppverify_not_imp_or {a b : Prop} (h : ¬a → b) : a ∨ b " - ":=\n"; - OS << " (Classical.em a).elim Or.inl (fun ha => Or.inr (h ha))\n\n"; - } + emitLeanPrelude(OS, ProjectGoals != nullptr); } - emitLeanFunctionDeclarations(Module, OS, EmittedFunctions); + emitLeanFunctionDeclarations(Module, OS, EmittedFunctions, Specs); emitLeanFunctionBodies(Module, OS); OS << "/- function: "; printLeanCommentText(Module.FunctionName, OS); OS << " -/\n"; OS << "/- required logic: " << formatLogicFeatures(Module.RequiredFeatures) - << " -/\n\n"; + << " -/\n"; + if (Module.BMCTransform) + OS << "/- bound: " << Module.BMCTransform->UnrollBound + << " loop iterations, by unrolling: these goals speak only of the\n" + " executions within them, and an unwinding goal says that every " + "loop\n ends within them -/\n"; + OS << "\n"; std::string TheoremName = "cppverify_" + @@ -1231,6 +1919,7 @@ VerifyResult verify::exportLeanScratchPad( Goal.LegacyName = encodeLeanName(ObligationName + "_goal"); Goal.LegacyBodyPrefix = LegacyBodyPrefix; Goal.Exported = Selected; + Goal.Unwinding = Item.Kind == ObligationKind::Unwinding; if (!Selected) { ProjectGoals->push_back(std::move(Goal)); continue; @@ -1273,4 +1962,60 @@ VerifyResult verify::exportLeanScratchPad( Result.Message = ProjectGoals ? "editable Lean project obligation export" : "unchecked Lean obligation export"; return Result; +} + +std::optional verify::exportLeanRefutation( + const ObligationModule &Module, const Obligation &Item, + const CounterexampleAssignment &Assignment, + std::map &Specs, std::string &Why) { + // The goal, its bodies, and its specs as its project states them. + std::string Goals; + llvm::raw_string_ostream GoalsOS(Goals); + std::set Functions, Theorems; + std::vector Stated; + const std::set Selection = {Item.Id}; + VerifyResult Exported = exportLeanScratchPad( + Module, GoalsOS, /*EmitPreamble=*/false, Functions, Theorems, + /*ModuleIndex=*/1, &Stated, &Selection, &Specs); + auto Goal = llvm::find_if( + Stated, [](const LeanProjectGoal &Goal) { return Goal.Exported; }); + if (Exported.Status != VerifyStatus::Exported || Goal == Stated.end()) { + Why = Exported.Message.empty() ? "the goal has no Lean statement" + : Exported.Message; + return std::nullopt; + } + + // The goal applied to the values of its variables, in the order it binds + // them; the definition instances it states then hold by evaluation. + std::string Application; + llvm::raw_string_ostream ApplicationOS(Application); + std::map Variables; + collectLeanVariables(Item.Goal.get(), {}, Variables); + for (const auto &[Name, Sort] : Variables) { + auto Value = Assignment.Values.find(Name); + ApplicationOS << "\n "; + if (Value == Assignment.Values.end() || + !printLeanValue(Value->second, Sort, ApplicationOS)) { + Why = "the counterexample gives " + Name + " no value Lean can state"; + return std::nullopt; + } + } + + LeanRefutation Refutation; + Refutation.Group = Goal->Group; + Refutation.Goal = Goal->Name; + Refutation.StableId = Goal->StableId; + llvm::raw_string_ostream OS(Refutation.Text); + OS << "import CppVerify.Specs\nimport CppVerify.Auto\n\n" + << "noncomputable section\n\nset_option maxRecDepth 100000\n" + << "set_option linter.unusedVariables false\n\n" + << "section Goal\n\nset_option maxHeartbeats 0\n\n" + << "attribute [local instance] cppVerifyPropDecidable\n\n" + << Goals << "end Goal\n\n" + << "/- The goal is false at the counterexample of " + << (Item.StableId.empty() ? Item.Id : Item.StableId) << ". -/\n" + << "theorem " << Goal->Name << "_refuted : ¬ " << Goal->Name + << " := by\n intro h\n have h₁ := h" << Application + << "\n cppverify_refute h₁\n"; + return Refutation; } \ No newline at end of file diff --git a/clang/lib/Verify/Backend/LeanBackend.h b/clang/lib/Verify/Backend/LeanBackend.h index 7145e8596c..c2cc672246 100644 --- a/clang/lib/Verify/Backend/LeanBackend.h +++ b/clang/lib/Verify/Backend/LeanBackend.h @@ -4,8 +4,10 @@ #include "VerifyBackend.h" #include "llvm/Support/raw_ostream.h" +#include #include #include +#include #include namespace clang { @@ -33,26 +35,75 @@ struct LeanProjectGoal { /// False for an obligation of an exported function that a selection left /// to the solver: its goal is known, but has no definition or proof file. bool Exported = true; + /// An unwinding goal of a bounded module: that its loops end within the + /// bound. + bool Unwinding = false; }; /// The group of a module's goals in a project: its function's directory. std::string leanProjectGroup(const ObligationModule &Module); +/// A logical function of a Lean project: its opaque declaration and, when it +/// has a definition Lean may accept, that definition, recursion measured by +/// its termination measures, with the functions it applies. +struct LeanSpecDeclaration { + /// Its Lean constant. + std::string Name; + std::string Opaque; + std::string Definition; + std::set Applies; +}; + +/// The generated modules of a Lean project: the specs its goals apply, by +/// identity, which every goal module imports, and each group's spec +/// definitions and goals. +struct LeanProjectModules { + std::map Specs; + std::map Goals; + /// The groups in the order their functions were exported: the source's. + std::vector Order; +}; + +/// The definitions every exported goal is stated with. +void emitLeanPrelude(llvm::raw_ostream &OS, bool Project); + /// Selection names the internal IDs to export; a project goal is named by /// its function and semantic hash, so it keeps its name whatever else is /// exported, and a scratch-pad goal keeps the positional name it has when /// every obligation is exported. A project records every goal of the module, -/// the ones left out as not exported. -VerifyResult -exportLeanScratchPad(const ObligationModule &Module, llvm::raw_ostream &OS, - bool EmitPreamble, std::set &EmittedFunctions, - std::set &EmittedTheorems, - unsigned ModuleIndex, - std::vector *ProjectGoals, - const std::set *Selection = nullptr); +/// the ones left out as not exported. Specs, when given, receives each spec +/// once, as its project declares it, instead of OS. +VerifyResult exportLeanScratchPad( + const ObligationModule &Module, llvm::raw_ostream &OS, bool EmitPreamble, + std::set &EmittedFunctions, + std::set &EmittedTheorems, unsigned ModuleIndex, + std::vector *ProjectGoals, + const std::set *Selection = nullptr, + std::map *Specs = nullptr); + +/// A certified counterexample shown in Lean: a module of the project's +/// group directory, Refutations//.lean, that states the goal as +/// the project would and proves `_refuted : ¬ ` at the values. +struct LeanRefutation { + std::string Group; + std::string Goal; + std::string StableId; + std::string Text; +}; + +/// The refutation of Item's goal at Assignment, the values a certified +/// counterexample gives its variables. Specs receives the specs it applies, +/// as a project's export does. None, with Why, when a value has no Lean +/// form. +std::optional +exportLeanRefutation(const ObligationModule &Module, const Obligation &Item, + const CounterexampleAssignment &Assignment, + std::map &Specs, + std::string &Why); class LeanVerifyBackend : public VerifyBackend { - llvm::raw_ostream *Out; + llvm::raw_ostream *Out = nullptr; + LeanProjectModules *Modules = nullptr; bool PreambleEmitted = false; std::set EmittedFunctions; std::set EmittedTheorems; @@ -64,12 +115,13 @@ class LeanVerifyBackend : public VerifyBackend { LeanVerifyBackend(llvm::raw_ostream *OS, std::vector *ProjectGoals) : Out(OS), ProjectGoals(ProjectGoals) {} + /// A project's export, into its modules. + LeanVerifyBackend(LeanProjectModules *Modules, + std::vector *ProjectGoals) + : Modules(Modules), ProjectGoals(ProjectGoals) {} llvm::StringRef getName() const override { return "lean"; } BackendCapabilities getCapabilities() const override { - // Collections have no Lean semantics yet. - return {allLogicFeatures() & ~logicFeature(LogicFeature::Sequences) & - ~logicFeature(LogicFeature::Collections), - false}; + return {allLogicFeatures(), false}; } /// Restrict later exports to these internal obligation IDs. void selectObligations(std::optional> Ids) { diff --git a/clang/lib/Verify/Backend/Obligation.cpp b/clang/lib/Verify/Backend/Obligation.cpp index 07524b070f..fa36569e16 100644 --- a/clang/lib/Verify/Backend/Obligation.cpp +++ b/clang/lib/Verify/Backend/Obligation.cpp @@ -2578,6 +2578,26 @@ verify::validateObligationModule(const ObligationModule &M) { ValidationError.c_str()); collectRequiredFeatures(Definition.get(), RequiredFeatures); } + // A measure only chooses an induction or Lean's termination argument, + // so it adds no feature; it must still be an integer over the + // parameters. + for (const auto &Measure : Declaration.Decreases) { + if (!validateLogicExpr(Measure.get(), ValidationError)) + return llvm::createStringError(llvm::inconvertibleErrorCode(), "%s", + ValidationError.c_str()); + if (Measure->Sort.Kind != LogicSortKind::MathematicalInteger && + Measure->Sort.Kind != LogicSortKind::BitVector) + return llvm::createStringError( + llvm::inconvertibleErrorCode(), + "logical function termination measure is not an integer"); + if (!validateLogicCalls(Measure.get(), M.LogicFunctions, + ValidationError) || + !validateVariableScopes(Measure.get(), ValidationError) || + !validateDefinitionVariables(Measure.get(), Parameters, {}, + ValidationError)) + return llvm::createStringError(llvm::inconvertibleErrorCode(), "%s", + ValidationError.c_str()); + } } return RequiredFeatures; } diff --git a/clang/lib/Verify/Backend/Obligation.h b/clang/lib/Verify/Backend/Obligation.h index a3281d8f53..e93984883f 100644 --- a/clang/lib/Verify/Backend/Obligation.h +++ b/clang/lib/Verify/Backend/Obligation.h @@ -230,7 +230,8 @@ class LogicExpr : public llvm::ThreadSafeRefCountedBase { std::vector Patterns; /// For an Or that states one assumption of a goal (`!A || rest`): where A /// comes from (`pre`, `call:f`, `def:x_1`), so a goal can be shown as - /// named hypotheses. Outside the term's meaning, identity, and archives. + /// named hypotheses. Outside the term's meaning and identity; archives + /// keep it with their source metadata. std::string Premise; explicit LogicExpr(Kind K) : K(K) {} @@ -355,7 +356,8 @@ struct LogicFunctionDecl { /// value; a verdict that does rests on them. Not archived. std::vector> Postconditions; /// A recursive function's termination measure over the parameters, by - /// which an induction over its applications descends. Not archived. + /// which an induction over its applications descends and Lean proves its + /// definition terminates. Archived, never hashed. std::vector> Decreases; static constexpr const char *ResultVariable = "cppverify.result"; }; diff --git a/clang/lib/Verify/Backend/ObligationSerialization.cpp b/clang/lib/Verify/Backend/ObligationSerialization.cpp index 547b55818f..6b20cf5979 100644 --- a/clang/lib/Verify/Backend/ObligationSerialization.cpp +++ b/clang/lib/Verify/Backend/ObligationSerialization.cpp @@ -501,17 +501,24 @@ class ArchiveWriter { writeU8(collectionTag(Expr->CollectionOp)); writeString(Expr->SpecCallee); writeSource(Expr->Source); + // Where an assumption comes from names its hypothesis in a readable + // Lean goal, as source points name a diagnostic's place. + if (IncludeSource && Expr->K == LogicExpr::Or) + writeString(Expr->Premise); writeU64(Expr->Children.size()); for (const auto &Child : Expr->Children) writeExpr(Child.get()); } + // A record keeps what a hash leaves out: the display name, and the + // termination measures, which change no meaning (Lean checks termination + // itself, and an induction descends by a well-founded relation whatever + // the measure). void writeFunction(const std::string &MapIdentity, - const LogicFunctionDecl &Function, - bool IncludeDisplayName) { + const LogicFunctionDecl &Function, bool Archived) { writeString(MapIdentity); writeString(Function.Identity); - writeString(IncludeDisplayName ? Function.DisplayName : ""); + writeString(Archived ? Function.DisplayName : ""); writeU64(Function.Parameters.size()); for (const LogicFunctionParameter &Parameter : Function.Parameters) { writeString(Parameter.Name); @@ -523,6 +530,11 @@ class ArchiveWriter { writeU64(Function.DefinitionLevels.size()); for (const auto &Definition : Function.DefinitionLevels) writeExpr(Definition.get()); + if (!Archived) + return; + writeU64(Function.Decreases.size()); + for (const auto &Measure : Function.Decreases) + writeExpr(Measure.get()); } void writeFunctionSignature(const std::string &MapIdentity, @@ -536,10 +548,10 @@ class ArchiveWriter { } void writeFunctions(const std::map &Functions, - bool IncludeDisplayNames) { + bool Archived) { writeU64(Functions.size()); for (const auto &[Identity, Function] : Functions) - writeFunction(Identity, Function, IncludeDisplayNames); + writeFunction(Identity, Function, Archived); } void writeModule(const ObligationModule &Module, @@ -804,6 +816,8 @@ class ArchiveReader { } Expr->SpecCallee = readString(); Expr->Source = readSource(IncludeSource); + if (Version >= 3 && IncludeSource && Expr->K == LogicExpr::Or) + Expr->Premise = readString(); uint64_t Children = readCount(); if (!Failure.empty()) @@ -839,6 +853,12 @@ class ArchiveReader { Function.DefinitionLevels.reserve(Levels); for (uint64_t I = 0; I != Levels && Failure.empty(); ++I) Function.DefinitionLevels.push_back(readExpr()); + if (Version >= 3) { + uint64_t Measures = readCount(); + Function.Decreases.reserve(Measures); + for (uint64_t I = 0; I != Measures && Failure.empty(); ++I) + Function.Decreases.push_back(readExpr()); + } return Function; } diff --git a/clang/lib/Verify/Backend/ObligationSerialization.h b/clang/lib/Verify/Backend/ObligationSerialization.h index 3eb10da883..944dcd85e8 100644 --- a/clang/lib/Verify/Backend/ObligationSerialization.h +++ b/clang/lib/Verify/Backend/ObligationSerialization.h @@ -12,7 +12,10 @@ namespace clang { namespace verify { -inline constexpr uint32_t ObligationSerializationVersion = 2; +/// Version 2 adds the precise obligation kinds; version 3 each logical +/// function's termination measures and, with source metadata, where each +/// assumption of a goal comes from. Readers read every earlier version. +inline constexpr uint32_t ObligationSerializationVersion = 3; inline constexpr uint32_t ObligationSemanticHashVersion = 4; /// Serialize one module as a deterministic, versioned binary record. Records diff --git a/clang/lib/Verify/Backend/ObligationSimplify.cpp b/clang/lib/Verify/Backend/ObligationSimplify.cpp index 3fc0f03d45..66154f06fe 100644 --- a/clang/lib/Verify/Backend/ObligationSimplify.cpp +++ b/clang/lib/Verify/Backend/ObligationSimplify.cpp @@ -363,10 +363,14 @@ void simplifyFunctions(ObligationModule &Module, auto Function = Module.LogicFunctions.find(Pending[I]); if (Function == Module.LogicFunctions.end()) continue; + // A function's declaration depends on what its definition and its + // measures apply. std::set Dependencies; collectCalledFunctions(Function->second.StepDefinition.get(), Dependencies); for (const auto &Definition : Function->second.DefinitionLevels) collectCalledFunctions(Definition.get(), Dependencies); + for (const auto &Measure : Function->second.Decreases) + collectCalledFunctions(Measure.get(), Dependencies); for (const std::string &Dependency : Dependencies) if (Reachable.insert(Dependency).second) Pending.push_back(Dependency); diff --git a/clang/lib/Verify/Backend/VerifyBackend.cpp b/clang/lib/Verify/Backend/VerifyBackend.cpp index 134495fe9a..fac014a9f8 100644 --- a/clang/lib/Verify/Backend/VerifyBackend.cpp +++ b/clang/lib/Verify/Backend/VerifyBackend.cpp @@ -15,17 +15,31 @@ using namespace verify; VerifyResult LeanVerifyBackend::verifyModule(const ObligationModule &Module) { VerifyResult R; - if (!Out) { + if (!Out && !Modules) { R.Status = VerifyStatus::Unresolved; R.Reason = VerifyReason::LeanExportFailure; R.Message = "no lean output stream"; return R; } const size_t Before = ProjectGoals ? ProjectGoals->size() : 0; - VerifyResult Result = exportLeanScratchPad( - Module, *Out, !PreambleEmitted, EmittedFunctions, EmittedTheorems, - ++ModuleIndex, ProjectGoals, Selection ? &*Selection : nullptr); - PreambleEmitted = true; + VerifyResult Result; + if (Modules) { + // A project's goals go to their function's module, and the specs they + // apply are declared once, in the module every goal module imports. + const std::string Group = leanProjectGroup(Module); + if (!Modules->Goals.count(Group)) + Modules->Order.push_back(Group); + llvm::raw_string_ostream Goals(Modules->Goals[Group]); + Result = exportLeanScratchPad( + Module, Goals, /*EmitPreamble=*/false, EmittedFunctions, + EmittedTheorems, ++ModuleIndex, ProjectGoals, + Selection ? &*Selection : nullptr, &Modules->Specs); + } else { + Result = exportLeanScratchPad( + Module, *Out, !PreambleEmitted, EmittedFunctions, EmittedTheorems, + ++ModuleIndex, ProjectGoals, Selection ? &*Selection : nullptr); + PreambleEmitted = true; + } if (ProjectGoals) for (size_t I = Before; I < ProjectGoals->size(); ++I) if ((*ProjectGoals)[I].Exported) @@ -754,11 +768,14 @@ std::unique_ptr verify::createVerifyBackend(BackendKind K, llvm::raw_ostream *LeanOut, unsigned BMCUnroll, const BackendExecutionOptions &Execution, - std::vector *LeanProjectGoals) { + std::vector *LeanProjectGoals, + LeanProjectModules *LeanModules) { switch (K) { case BackendKind::Z3: return std::make_unique(Execution, "z3"); case BackendKind::Lean: + if (LeanModules) + return std::make_unique(LeanModules, LeanProjectGoals); return std::make_unique(LeanOut, LeanProjectGoals); case BackendKind::BMC: return std::make_unique(BMCUnroll, Execution); diff --git a/clang/lib/Verify/Backend/VerifyBackend.h b/clang/lib/Verify/Backend/VerifyBackend.h index 6159936740..f006f4a5dc 100644 --- a/clang/lib/Verify/Backend/VerifyBackend.h +++ b/clang/lib/Verify/Backend/VerifyBackend.h @@ -128,6 +128,8 @@ struct QuantifierProfileEntry { unsigned MaxGeneration = 0; }; +struct CounterexampleAssignment; + struct VerifyResult { VerifyStatus Status = VerifyStatus::Unresolved; VerifyReason Reason = VerifyReason::None; @@ -138,6 +140,9 @@ struct VerifyResult { std::vector Trace; std::string ObligationId; std::optional ObligationType; + /// For a certified counterexample: the value of every free variable of the + /// goal of the obligation it fails, when the model gives each one. + std::shared_ptr Assignment; SourceLocation Location; ObligationSource Source; uint64_t CacheHits = 0; @@ -307,12 +312,14 @@ inline unsigned moduleTimeoutMs(const ObligationModule &Module, } struct LeanProjectGoal; +struct LeanProjectModules; std::unique_ptr createVerifyBackend(BackendKind K, llvm::raw_ostream *LeanOut = nullptr, unsigned BMCUnroll = 10, const BackendExecutionOptions &Execution = {}, - std::vector *LeanProjectGoals = nullptr); + std::vector *LeanProjectGoals = nullptr, + LeanProjectModules *LeanModules = nullptr); VerifyResult lowerObligationModule(const ObligationModule &Module, diff --git a/clang/lib/Verify/Backend/Z3Encode.cpp b/clang/lib/Verify/Backend/Z3Encode.cpp index d1c13b6fc1..93cc05b0f5 100644 --- a/clang/lib/Verify/Backend/Z3Encode.cpp +++ b/clang/lib/Verify/Backend/Z3Encode.cpp @@ -3322,6 +3322,12 @@ VerifyResult Z3Encoder::decideModule(const ObligationModule &Module, EncodingError = std::move(SavedError); return sourceValue(Evaluated, Expr->Sort); }; + // Completing a model adds the values it chooses to it, and the model + // reported keeps the solver's alone: an assignment reads a copy. + auto assignment = [&](const Obligation &Item) { + Z3CandidateModel Copy(*this, z3::model(Mod, Ctx, z3::model::translate())); + return counterexampleAssignment(Item, Copy); + }; if (IsCompleteQuery && !TraceEventCount) { // The certified model is the completed one; the certifier evaluates // what the solver's evaluator leaves open, such as quantifiers. @@ -3346,9 +3352,18 @@ VerifyResult Z3Encoder::decideModule(const ObligationModule &Module, Out.ObligationType = Item.Kind; Out.Location = Item.Loc; Out.Source = Item.Source; + Out.Assignment = assignment(Item); TraceEventCount = Item.TraceEventCount; break; } + } else { + // A refutation of the goal of the obligation this query is the + // counterexample of applies the goal to the model's values. + for (const Obligation &Item : Module.Obligations) + if (Item.CounterexampleQuery.get() == Query) { + Out.Assignment = assignment(Item); + break; + } } for (const auto &[Name, Sort] : ModelVariables) { auto Metadata = Module.DiagnosticVariables.find(Name); diff --git a/clang/lib/Verify/Driver/LeanAutomation.h b/clang/lib/Verify/Driver/LeanAutomation.h index 0db32bac01..8166156719 100644 --- a/clang/lib/Verify/Driver/LeanAutomation.h +++ b/clang/lib/Verify/Driver/LeanAutomation.h @@ -9,23 +9,32 @@ namespace verify { /// Names this module's tactics: a stub or automatic proof that an earlier /// revision wrote is tried again. -inline constexpr llvm::StringLiteral LeanAutomationRevision = "1"; +inline constexpr llvm::StringLiteral LeanAutomationRevision = "3"; /// The steps of cppverify_auto, each the whole proof of a goal it proves. inline constexpr llvm::StringLiteral LeanAutomationSteps[] = { - "cppverify_auto_grind", "cppverify_auto_heap", "cppverify_auto_bv", - "cppverify_auto_decide", "cppverify_auto_simp"}; + "cppverify_auto_grind", "cppverify_auto_heap", "cppverify_auto_bv", + "cppverify_auto_decide", "cppverify_auto_simp", "cppverify_auto_bveq", + "cppverify_auto_ext", "cppverify_auto_unfold", "cppverify_auto_eval", + "cppverify_auto_div"}; /// CppVerify/Auto.lean, written with every export. Its steps were chosen by /// what proves the goals of the test suite (heap stores as the functions /// they define, machine arithmetic as integers, then grind; stating toInt /// ranges and removing Int.bmod first proved nothing more, the latter less), /// each with a heartbeat budget of its own, as Vermilion's discharger ladder. +/// Revision 2 adds the last three, after the others, so that every goal an +/// earlier step proves keeps its proof: machine equalities and sign +/// extensions as integer facts, collection equality element by element, and +/// the definition instances a goal states, by the bodies they name. +/// Revision 3 adds the last two: the spec functions with Lean definitions +/// evaluated by their equations, and C++ division and remainder by their +/// definitions. inline constexpr llvm::StringLiteral LeanAutomationSource = R"lean(/- Generated by CppVerify: the tactics of its automatic proofs, which proof files may use too. Regeneration replaces this file. -/ import Lean -import CppVerify.Generated +import CppVerify.Prelude register_option cppverify.autoBudget : Nat := { defValue := 50000 @@ -82,6 +91,51 @@ elab "cppverify_unfold_readable" : tactic => withMainContext do let unfolded ← deltaExpand target (· == name) replaceMainGoal [← (← getMainGoal).change unfolded] +open Lean Elab Tactic Meta in +/-- Unfolds the definitions of spec bodies that a goal and its hypotheses +name, so that each definition instance a goal states says what the body +does. A body names only lower levels of its definition, so this ends. -/ +elab "cppverify_unfold_bodies" : tactic => withMainContext do + let isBody : Name → Bool := fun + | .str _ s => s.startsWith "cppSpecBody_" + | _ => false + let rec expand (e : Expr) : Nat → MetaM Expr + | 0 => return e + | fuel + 1 => do + let e' ← deltaExpand e isBody + if e' == e then return e else expand e' fuel + let mut goal ← getMainGoal + goal ← goal.change (← expand (← instantiateMVars (← goal.getType)) 64) + for d in ← goal.withContext getLCtx do + if d.isImplementationDetail then continue + let t ← instantiateMVars d.type + let t' ← expand t 64 + if t' != t then + goal ← goal.changeLocalDecl d.fvarId t' + replaceMainGoal [goal] + +open Lean Elab Tactic Meta in +/-- Rewrites with the equations of every spec function that Specs.lean +defines, and C++ division and remainder, so that applications at known +arguments become values. On unknown arguments it unfolds until its budget +is spent. -/ +elab "cppverify_eval_specs" : tactic => withMainContext do + let env ← getEnv + let mut lemmas : Array (TSyntax `Lean.Parser.Tactic.simpLemma) := #[] + let some idx := env.getModuleIdx? `CppVerify.Specs + | throwError "no spec function has a Lean definition" + let some data := env.header.moduleData[idx.toNat]? + | throwError "no spec function has a Lean definition" + for name in data.constNames do + if let .str .anonymous s := name then + if s.startsWith "cppSpec_" then + if let some (.defnInfo _) := env.find? name then + lemmas := lemmas.push + (← `(Lean.Parser.Tactic.simpLemma| $(mkIdent name):ident)) + if lemmas.isEmpty then + throwError "no spec function has a Lean definition" + evalTactic (← `(tactic| simp [$lemmas,*, cppIntDiv, cppIntRem])) + /-- Machine comparisons, constants, and overflow conditions as facts about integers. -/ macro "cppverify_norm" : tactic => `(tactic| (try simp only [cppBvSlt, cppBvSle, @@ -118,6 +172,108 @@ macro "cppverify_auto_decide" : tactic => macro "cppverify_auto_simp" : tactic => `(tactic| (cppverify_unfold_readable; simp_all)) +/-- Machine equalities, as of `x ≠ y` for distinct keys, and sign extensions +as facts about integers. -/ +macro "cppverify_bveq" : tactic => `(tactic| (try simp (disch := omega) only + [ne_eq, ← BitVec.toInt_inj, BitVec.toInt_signExtend_of_le] at *)) +macro "cppverify_auto_bveq" : tactic => `(tactic| ( + cppverify_unfold_readable + intros + cppverify_bveq + cppverify_norm + all_goals grind)) +/-- Two collections are equal when they agree at every element: equal +lengths and reads for sequences, membership for sets, counts for +multisets, and keys and values for maps. -/ +macro "cppverify_auto_ext" : tactic => `(tactic| ( + cppverify_unfold_readable + intros + (first | apply cppSeq_ext | apply cppSet_ext | apply cppMultiset_ext + | apply cppMap_ext) + all_goals (intros; cppverify_norm; all_goals grind))) +macro "cppverify_auto_unfold" : tactic => `(tactic| ( + cppverify_unfold_readable + intros + cppverify_unfold_bodies + cppverify_bveq + cppverify_norm + all_goals grind)) +macro "cppverify_auto_eval" : tactic => `(tactic| ( + cppverify_unfold_readable + intros + cppverify_bveq + cppverify_eval_specs + all_goals grind)) +/-- C++ division and remainder by their definitions, which grind reads by +constant divisors once the signs are split. -/ +macro "cppverify_auto_div" : tactic => `(tactic| ( + cppverify_unfold_readable + intros + cppverify_bveq + cppverify_norm + all_goals grind [cppIntDiv, cppIntRem])) + +/-- Memory reads and machine comparisons by their definitions, so that a +closed instance decides. -/ +macro "cppverify_reads" : tactic => `(tactic| (try simp only [heapSelectInt, + heapSelectBv, cppBvSle, cppBvSlt, cppBvUle, cppBvUlt])) + +/-- An instance of a goal at known values, evaluated: the bodies it names, +the spec definitions of `CppVerify.Specs` by their equations (which proves +the definition instances it states), memory reads and machine comparisons +by their definitions; then decided, or shown by `grind`, since a store +relates two heaps at every address. -/ +macro "cppverify_evaluate" : tactic => `(tactic| ( + cppverify_unfold_bodies + try cppverify_eval_specs + all_goals cppverify_reads + all_goals first + | decide + | grind [heapSelectInt, heapSelectBv, heapStoreInt, heapStoreBool, + heapStoreBv])) + +open Lean Elab Tactic in +/-- Whether a tactic closes every goal within `cppverify.autoBudget` +heartbeats of its own, as each step of cppverify_auto; running out of them +is a no, as any other error is. -/ +def CppVerify.Auto.withinBudget (tactic : TacticM Unit) : TacticM Bool := do + let budget := cppverify.autoBudget.get (← getOptions) + let saved ← saveState + tryCatchRuntimeEx + (do + let heartbeats ← IO.getNumHeartbeats + withTheReader Core.Context + (fun ctx => { ctx with initHeartbeats := heartbeats, + maxHeartbeats := budget * 1000 }) tactic + return (← getUnsolvedGoals).isEmpty) + fun _ => do saved.restore; return false + +open Lean Elab Tactic Meta in +/-- Shows that the instance of a goal that `h` states is false, or fails +saying why not: that Lean decides it true, which contradicts the +counterexample, or that it cannot decide it. Each attempt has the heartbeats +of a step of cppverify_auto. -/ +elab "cppverify_refute " h:ident : tactic => withMainContext do + let saved ← saveState + let attempts : List Syntax.Tactic := [ + ← `(tactic| (revert $h:ident; decide)), + ← `(tactic| (revert $h:ident; cppverify_evaluate))] + for attempt in attempts do + if ← CppVerify.Auto.withinBudget (evalTactic attempt) then return + saved.restore + let some decl := (← getLCtx).findFromUserName? h.getId + | throwError "cppverify: no hypothesis {h.getId}" + -- Whether the instance holds of itself: in a context without the goal, + -- which would prove any instance. + let probe ← mkFreshExprMVarAt {} {} decl.type + setGoals [probe.mvarId!] + let holds ← CppVerify.Auto.withinBudget + (evalTactic (← `(tactic| first | decide | cppverify_evaluate))) + saved.restore + if holds then + throwError "cppverify: Lean decides the goal true at the counterexample" + throwError "cppverify: Lean cannot decide the goal at the counterexample" + open Lean Elab Tactic in /-- Tries each step on the goal, each with `cppverify.autoBudget` heartbeats of its own, and keeps the first that proves it. A step that runs out of @@ -132,7 +288,12 @@ elab "cppverify_auto" : tactic => do ("cppverify_auto_heap", ← `(tactic| cppverify_auto_heap)), ("cppverify_auto_bv", ← `(tactic| cppverify_auto_bv)), ("cppverify_auto_decide", ← `(tactic| cppverify_auto_decide)), - ("cppverify_auto_simp", ← `(tactic| cppverify_auto_simp))] + ("cppverify_auto_simp", ← `(tactic| cppverify_auto_simp)), + ("cppverify_auto_bveq", ← `(tactic| cppverify_auto_bveq)), + ("cppverify_auto_ext", ← `(tactic| cppverify_auto_ext)), + ("cppverify_auto_unfold", ← `(tactic| cppverify_auto_unfold)), + ("cppverify_auto_eval", ← `(tactic| cppverify_auto_eval)), + ("cppverify_auto_div", ← `(tactic| cppverify_auto_div))] for (name, step) in steps do let attempt : TacticM Unit := do let heartbeats ← IO.getNumHeartbeats diff --git a/clang/lib/Verify/Driver/LeanCertifier.h b/clang/lib/Verify/Driver/LeanCertifier.h index dc62393db9..da062f8322 100644 --- a/clang/lib/Verify/Driver/LeanCertifier.h +++ b/clang/lib/Verify/Driver/LeanCertifier.h @@ -16,7 +16,7 @@ open Lean /-- The axioms a constant depends on, through the statements and values of the constants it uses, read from the kernel's constants. -/ -def axiomsOf (env : Environment) (root : Name) : Except String NameSet := Id.run do +def axiomsOf (env : Kernel.Environment) (root : Name) : Except String NameSet := Id.run do let mut seen : NameSet := {} let mut axioms : NameSet := {} let mut work := #[root] @@ -38,9 +38,10 @@ def axiomsOf (env : Environment) (root : Name) : Except String NameSet := Id.run | none => return .error s!"{c} is not in the environment" return .ok axioms -/-- A module's imports and its own constants, read from its compiled files. -/ +/-- A module's imports and its own constants, in the order it declares them, +read from its compiled files. -/ unsafe def moduleConstants (module : Name) : - IO (Array Import × Std.HashMap Name ConstantInfo) := do + IO (Array Import × Array Name × Std.HashMap Name ConstantInfo) := do let file ← findOLean module let mut files := #[file] let server := OLeanLevel.server.adjustFileName file @@ -54,40 +55,99 @@ unsafe def moduleConstants (module : Name) : let mut constants := {} for name in parts[parts.size - 1].1.constNames, info in parts[parts.size - 1].1.constants do constants := constants.insert name info - return (parts[0].1.imports, constants) - -/-- Replays constants with the kernel into the environment of imports, as -leanchecker does: nothing in them escaped the kernel. -/ -unsafe def replayInto (imports : Array Import) - (constants : Std.HashMap Name ConstantInfo) : IO Unit := do - let (_, s) ← importModulesCore imports |>.run - let env ← finalizeImport s imports {} 0 false false (isModule := true) - discard <| env.toKernelEnv.replay constants - -/-- Replays a module's own constants into the environment of its imports. -/ -unsafe def replayModule (module : Name) : IO Unit := do - let (imports, constants) ← moduleConstants module - replayInto imports constants - -/-- Whether the proof of a goal states exactly the goal the generated module -defines and depends on no axiom outside the allowed ones. -/ -def checkStatement (env : Environment) (goal module : Name) (allowed : NameSet) : - Option String := Id.run do - let generated := `CppVerify.Generated - let moduleOf (c : Name) : Option Name := - (env.getModuleIdxFor? c).bind fun i => env.header.moduleNames[i.toNat]? - let proof := goal.appendAfter "_proof" + return (parts[0].1.imports, parts[parts.size - 1].1.constNames, constants) + +/-- A module of the project, read from its compiled files. -/ +structure ProjectModule where + name : Name + imports : Array Import + declared : Array Name + constants : Std.HashMap Name ConstantInfo + +/-- The modules of the project some modules import, each after the modules it +imports, and the toolchain's modules they import. -/ +structure Closure where + modules : Array ProjectModule := #[] + toolchain : Array Import := #[] + seen : NameSet := {} + +unsafe def Closure.visit (toolchain : String) (module : Name) : StateT Closure IO Unit := do + if (← get).seen.contains module then return + modify fun c => { c with seen := c.seen.insert module } + if (← findOLean module).toString.startsWith toolchain then + modify fun c => { c with toolchain := c.toolchain.push { module } } + return + let (imports, declared, constants) ← moduleConstants module + for i in imports do + Closure.visit toolchain i.module + modify fun c => { c with modules := c.modules.push { name := module, imports, declared, constants } } + +/-- The kernel's environment of the project: the toolchain's modules the +project imports, imported once, then every module of the project replayed +with the kernel, each after the modules it imports, as leanchecker does. A +module the kernel rejects, one that imports it, and one that declares a +constant an earlier module or the toolchain declares otherwise are left out, +each with why; the constant each other module declares is recorded with it. -/ +structure Replayed where + env : Kernel.Environment + rejected : Std.HashMap Name String + owner : Std.HashMap Name Name + +unsafe def replayProject (closure : Closure) : IO Replayed := do + let (_, s) ← importModulesCore closure.toolchain |>.run + let imported ← finalizeImport s closure.toolchain {} 0 false false (isModule := true) + let mut env := imported.toKernelEnv + let mut rejected : Std.HashMap Name String := {} + let mut owner : Std.HashMap Name Name := {} + for m in closure.modules do + if let some i := m.imports.find? (rejected.contains ·.module) then + rejected := rejected.insert m.name rejected[i.module]! + continue + let mut fresh : Std.HashMap Name ConstantInfo := {} + let mut clash : Option String := none + for (name, info) in m.constants do + match env.find? name with + | none => fresh := fresh.insert name info + | some known => + -- Lean derives an equation lemma in each module that unfolds a + -- definition, the same constant every time, and the kernel takes a + -- theorem twice when it states the same. + let same := known.type == info.type && known.levelParams == info.levelParams && + (known.isTheorem || known.value? == info.value?) + unless same do + let other := (owner[name]?).map toString |>.getD "the toolchain" + clash := some s!"{m.name} declares {name}, which {other} declares" + break + if let some why := clash then + rejected := rejected.insert m.name why + continue + try env ← env.replay fresh + catch e => + rejected := rejected.insert m.name s!"the kernel rejects {m.name}: {e}" + continue + for (name, _) in fresh do + owner := owner.insert name m.name + return { env, rejected, owner } + +/-- Whether the proof of a goal states exactly the goal its generated module +defines, or a refutation exactly its negation, and depends on no axiom +outside the allowed ones. -/ +def checkStatement (project : Replayed) (goal module generated : Name) + (refutes : Bool) (allowed : NameSet) : Option String := Id.run do + let env := project.env + let proof := goal.appendAfter (if refutes then "_refuted" else "_proof") + let stated := if refutes then mkApp (mkConst ``Not) (mkConst goal) else mkConst goal match env.find? goal with | some (.defnInfo _) => - if moduleOf goal != some generated then + if project.owner[goal]? != some generated then return some s!"{goal} is not the goal {generated} defines" | _ => return some s!"{generated} defines no goal {goal}" match env.find? proof with | some (.thmInfo v) => - if moduleOf proof != some module then + if project.owner[proof]? != some module then return some s!"{proof} is not declared in {module}" - if v.type != .const goal [] then - return some s!"{proof} proves {v.type}, not {goal}" + if v.type != stated then + return some s!"{proof} proves {v.type}, not {stated}" match axiomsOf env proof with | .error e => return some e | .ok axioms => @@ -100,80 +160,56 @@ def checkStatement (env : Environment) (goal module : Name) (allowed : NameSet) | some _ => return some s!"{proof} is not a theorem" | none => return some s!"{module} declares no {proof}" -/-- A goal is proved when every module of the project its proof imports passes -the kernel (each replayed once in a run), and its proof states exactly the -goal and uses only the allowed axioms. Imported data is never freed: a later -import in the process reads it. -/ -unsafe def checkGoal (goal module : Name) (allowed : NameSet) (toolchain : String) - (replayed : IO.Ref NameSet) : IO (Option String) := do - let env ← importModules #[{ module }] {} - let project ← env.header.moduleNames.filterM fun m => - return !(← findOLean m).toString.startsWith toolchain - let why := checkStatement env goal module allowed - for m in project do - if (← replayed.get).contains m then continue - try replayModule m - catch e => return some s!"the kernel rejects {m}: {e}" - replayed.modify (·.insert m) - return why - -/-- Every goal checked in one environment: each proof module imported once, -and every module of the project replayed together into the environment of the -toolchain's modules they import. A check reads only kernel constants, each -in the module that declares it, so proofs imported together cannot change -each other's checks; but one bad module would stop the replay of all, and a -proof file that declares a goal's name stops the import, so then none is -reported (`none`) and each goal is checked by itself. A proof file of -cppverify_auto imports `Lean`, whose import each goal would repeat. -/ -unsafe def checkTogether (entries : Array (Name × Name)) (allowed : NameSet) - (toolchain : String) : IO (Option (Array (Option String))) := do - let modules := entries.foldl (fun ms (_, m) => if ms.contains m then ms else ms.push m) #[] - let some env ← (try some <$> importModules (modules.map ({ module := · })) {} - catch _ => pure none) | return none - let project ← env.header.moduleNames.filterM fun m => - return !(← findOLean m).toString.startsWith toolchain - let mut imports : Array Import := #[] - let mut constants : Std.HashMap Name ConstantInfo := {} - for m in project do - let (own, declared) ← moduleConstants m - for i in own do - unless project.contains i.module || imports.any (·.module == i.module) do - imports := imports.push i - -- Lean derives an equation lemma in each module that unfolds the - -- definition, the same constant every time. - for (name, info) in declared do - if let some known := constants[name]? then - unless known.type == info.type && known.value? == info.value? do - return none - continue - constants := constants.insert name info - try replayInto imports constants catch _ => return none - return some (entries.map fun (goal, module) => checkStatement env goal module allowed) - -/-- Usage: Certify GOALS ALLOWED, where each line of GOALS is a goal and its -proof module, separated by a tab, and ALLOWED lists the axioms a proof may -use, separated by commas. Prints one line per goal: `ok` or `fail`, the goal, -and why it failed. -/ +/-- The declaration of the user's lemmas that uses `sorry`, if one does: an +admitted lemma fails every goal, as when the module was compiled with `sorry` +an error. -/ +def userSorry (closure : Closure) (project : Replayed) : Option String := Id.run do + let some user := closure.modules.find? (·.name == `CppVerify.User) | return none + for name in user.declared do + if let .ok axioms := axiomsOf project.env name then + if axioms.contains ``sorryAx then + return some s!"{name} of CppVerify.User uses sorry" + return none + +/-- What a check reports: the goal, or the theorem that refutes it. -/ +def shown (goal : Name) (refutes : Bool) : Name := + if refutes then goal.appendAfter "_refuted" else goal + +/-- Usage: Certify GOALS ALLOWED, where each line of GOALS is a goal, its +proof module, and the module that defines it, separated by tabs, then +`refutation` for a module that refutes the goal it defines, and ALLOWED lists +the axioms a proof may use, separated by commas. Prints one line per goal: +`ok` or `fail`, the goal (a refutation's theorem for a refutation), and why it +failed. A goal is proved when its proof module and its generated module pass +the kernel, with every module of the project they import, and its proof +states exactly the goal (a refutation, its negation) and uses only the +allowed axioms. The generated modules are replayed first, so that a proof +file that declares a name one declares is the one left out. -/ unsafe def main (args : List String) : IO UInt32 := do let [goals, allowed] := args | throw <| IO.userError "usage: Certify GOALS ALLOWED" let sysroot ← findSysroot initSearchPath sysroot let toolchain := (sysroot / "lib" / "lean").toString let allowed := (allowed.splitOn ",").foldl (fun s a => s.insert a.toName) ({} : NameSet) - let mut entries : Array (Name × Name) := #[] + let mut entries : Array (Name × Name × Name × Bool) := #[] for line in ← IO.FS.lines goals do - let [goal, module] := line.splitOn "\t" | continue - entries := entries.push (goal.toName, module.toName) - let report (goal : Name) : Option String → IO Unit - | none => IO.println s!"ok\t{goal}" - | some why => IO.println s!"fail\t{goal}\t{why.replace "\n" " "}" - if let some results ← checkTogether entries allowed toolchain then - for (goal, _) in entries, why in results do - report goal why - return 0 - let replayed ← IO.mkRef ({} : NameSet) - for (goal, module) in entries do - report goal (← checkGoal goal module allowed toolchain replayed) + match line.splitOn "\t" with + | [goal, module, generated] => + entries := entries.push (goal.toName, module.toName, generated.toName, false) + | [goal, module, generated, "refutation"] => + entries := entries.push (goal.toName, module.toName, generated.toName, true) + | _ => continue + let roots := entries.map (·.2.2.1) ++ entries.map (·.2.1) + let (_, closure) ← (roots.forM (Closure.visit toolchain)).run {} + let project ← replayProject closure + let user := userSorry closure project + for (goal, module, generated, refutes) in entries do + let why := (project.rejected[module]?).orElse fun _ => + (project.rejected[generated]?).orElse fun _ => user.orElse fun _ => + checkStatement project goal module generated refutes allowed + match why with + | none => IO.println s!"ok\t{shown goal refutes}" + | some why => IO.println s!"fail\t{shown goal refutes}\t{why.replace "\n" " "}" return 0 )lean"; diff --git a/clang/lib/Verify/Driver/Verifier.cpp b/clang/lib/Verify/Driver/Verifier.cpp index f317e60815..00ff4c3233 100644 --- a/clang/lib/Verify/Driver/Verifier.cpp +++ b/clang/lib/Verify/Driver/Verifier.cpp @@ -32,6 +32,7 @@ #include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringExtras.h" #include "llvm/Support/Error.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/FormatVariadic.h" @@ -41,6 +42,7 @@ #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Path.h" #include "llvm/Support/Program.h" +#include "llvm/Support/SHA256.h" #include "llvm/Support/ThreadPool.h" #include "llvm/Support/Threading.h" #include "llvm/Support/raw_ostream.h" @@ -1061,6 +1063,8 @@ struct VerifyDiagnostic { /// For an export to a Lean project: of its goals, how many cppverify_auto /// proves. unsigned LeanAutomatic = 0; + /// A certified failure that its refutation shows in Lean too. + bool LeanRefuted = false; /// A proof by a solver, by Lean, or by both, which establishes what it /// proves. bool isProof() const { @@ -1255,6 +1259,28 @@ static llvm::Error writeTextFile(llvm::StringRef Path, return llvm::Error::success(); } +// A generated file is rewritten only when it changes, so that a rerun of the +// same source leaves every file as it was. +static llvm::Error writeTextFileIfChanged(llvm::StringRef Path, + llvm::StringRef Contents) { + if (auto Buffer = llvm::MemoryBuffer::getFile(Path, /*IsText=*/true)) + if ((*Buffer)->getBuffer() == Contents) + return llvm::Error::success(); + return writeTextFile(Path, Contents); +} + +static constexpr llvm::StringLiteral LeanGeneratedNote = + "/- Generated by CppVerify. Regeneration replaces this file, never a " + "proof. -/"; + +// What each module of goals starts with: options and the instance attribute +// of the prelude's classical decidability are local to a file. +static constexpr llvm::StringLiteral LeanGoalModulePreamble = + "noncomputable section\n\n" + "set_option maxRecDepth 100000\n" + "set_option maxHeartbeats 0\n\n" + "attribute [local instance] cppVerifyPropDecidable\n\n"; + static llvm::Error writeTextFileIfMissing(llvm::StringRef Path, llvm::StringRef Contents) { if (llvm::sys::fs::exists(Path)) @@ -1314,18 +1340,28 @@ struct LeanProofRecord { std::string Automation; }; -// A proof starts from the goal's readable form, its names introduced. Stubs -// before cppverify_auto did not import it. -static std::string leanProofHead(llvm::StringRef Goal, bool Automation = true) { - return "import CppVerify.User\n" + - std::string(Automation ? "import CppVerify.Auto\n" : "") + - "\ntheorem " + Goal.str() + "_proof : " + Goal.str() + +// The imports a proof file starts with: its function's goals, the user's +// lemmas, and the automation. Before 0.2's modules a proof imported only +// User (which imported every goal), and before cppverify_auto, not Auto. +enum class LeanProofImports { Modules, Automation, User }; + +// A proof starts from the goal's readable form, its names introduced. +static std::string +leanProofHead(llvm::StringRef Goal, llvm::StringRef Group, + LeanProofImports Imports = LeanProofImports::Modules) { + std::string Head; + if (Imports == LeanProofImports::Modules) + Head += "import CppVerify.Goals." + Group.str() + "\n"; + Head += "import CppVerify.User\n"; + if (Imports != LeanProofImports::User) + Head += "import CppVerify.Auto\n"; + return Head + "\ntheorem " + Goal.str() + "_proof : " + Goal.str() + " := by\n apply " + Goal.str() + ".of_readable\n"; } -static std::string leanProof(llvm::StringRef Goal, llvm::StringRef Intros, - llvm::StringRef Last) { - std::string Proof = leanProofHead(Goal); +static std::string leanProof(llvm::StringRef Goal, llvm::StringRef Group, + llvm::StringRef Intros, llvm::StringRef Last) { + std::string Proof = leanProofHead(Goal, Group); if (!Intros.empty()) Proof += " intro " + Intros.str() + "\n"; return Proof + " " + Last.str() + "\n"; @@ -1335,9 +1371,15 @@ static std::string leanProof(llvm::StringRef Goal, llvm::StringRef Intros, // whether cppverify_auto proves it: its last line is a stub's `sorry` or a // step of cppverify_auto. static std::optional leanGeneratedProof(llvm::StringRef Body, - llvm::StringRef Goal) { - if (!Body.consume_front(leanProofHead(Goal)) && - !Body.consume_front(leanProofHead(Goal, /*Automation=*/false))) + llvm::StringRef Goal, + llvm::StringRef Group) { + if (!llvm::any_of( + std::initializer_list{LeanProofImports::Modules, + LeanProofImports::Automation, + LeanProofImports::User}, + [&](LeanProofImports Imports) { + return Body.consume_front(leanProofHead(Goal, Group, Imports)); + })) return std::nullopt; if (Body.consume_front(" intro ")) { const size_t End = Body.find('\n'); @@ -1354,6 +1396,10 @@ static std::optional leanGeneratedProof(llvm::StringRef Body, } // The revision of cppverify_auto that an automation note was written by. +// Names the printing of spec definitions and the prelude's tactic for their +// measures: a verdict an earlier revision kept is not used. +static constexpr llvm::StringLiteral LeanSpecRevision = "1"; + static std::string leanAutomationTag() { return ("cppverify_auto " + LeanAutomationRevision + " ").str(); } @@ -1407,8 +1453,9 @@ static std::optional readLeanProof(llvm::StringRef Path) { return std::nullopt; const size_t End = Text.find("-/\n"); if (End != llvm::StringRef::npos) - if (std::optional Automatic = - leanGeneratedProof(Text.substr(End + 3), Record.Goal)) { + if (std::optional Automatic = leanGeneratedProof( + Text.substr(End + 3), Record.Goal, + llvm::sys::path::filename(llvm::sys::path::parent_path(Path)))) { Record.Pristine = true; Record.Automatic = *Automatic; } @@ -1428,7 +1475,8 @@ static void replaceAllIn(std::string &Text, llvm::StringRef From, static llvm::Error moveLeanProof(llvm::StringRef From, llvm::StringRef To, const LeanProjectGoal &Goal, llvm::StringRef OldGoal, - llvm::StringRef OldBodies) { + llvm::StringRef OldBodies, + llvm::StringRef OldGroup = "") { auto Buffer = llvm::MemoryBuffer::getFile(From); if (!Buffer) return llvm::createStringError( @@ -1449,6 +1497,9 @@ static llvm::Error moveLeanProof(llvm::StringRef From, llvm::StringRef To, } replaceAllIn(Body, OldGoal, Goal.Name); replaceAllIn(Body, OldBodies, Goal.BodyPrefix); + if (!OldGroup.empty()) + replaceAllIn(Body, ("import CppVerify.Goals." + OldGroup + "\n").str(), + "import CppVerify.Goals." + Goal.Group + "\n"); if (std::error_code EC = llvm::sys::fs::create_directories(llvm::sys::path::parent_path(To))) return llvm::createStringError(EC, "cannot create %s", To.str().c_str()); @@ -1499,131 +1550,25 @@ executeWithLogs(llvm::StringRef Program, return Stdout; } -class Verifier { - ASTContext &Ctx; - VerifyOptions Opts; - llvm::raw_ostream *DumpOS = nullptr; - std::vector Diags; +// A Lean project: its generated modules, the proof file of each goal, the +// trial of cppverify_auto, and certification. Source verification and +// archive replay both write one through it; what it warns of is kept for +// the caller to report. +class LeanProject { + const VerifyOptions &Opts; + std::vector Warnings; const std::string &leanProjectRoot() const { return Opts.LeanProjectPath.empty() ? Opts.LeanFallbackProjectPath : Opts.LeanProjectPath; } - /// The archive record of Module, empty without --obligation-out. - llvm::Expected archiveRecord(const ObligationModule &Module) { - if (!Opts.ObligationOut) - return std::string(); - - std::string Serialized = serializeObligationModule(Module); - auto RoundTrip = deserializeObligationModules(Serialized); - if (!RoundTrip) - return RoundTrip.takeError(); - if (RoundTrip->size() != 1 || - serializeObligationModule(RoundTrip->front()) != Serialized || - obligationSemanticHash(RoundTrip->front()) != - obligationSemanticHash(Module)) - return llvm::createStringError( - llvm::inconvertibleErrorCode(), - "canonical obligation serialization did not round-trip"); - return Serialized; - } - - llvm::Error emitObligationArchive(const ObligationModule &Module) { - llvm::Expected Record = archiveRecord(Module); - if (!Record) - return Record.takeError(); - if (Opts.ObligationOut) - *Opts.ObligationOut << *Record; - return llvm::Error::success(); - } - - void annotateObligationSources(ObligationModule &Module) { - const SourceManager &SourceMgr = Ctx.getSourceManager(); - auto annotateSource = [&](SourceLocation Loc, SourceLocation EndLoc, - ObligationSource &Source) { - if (!Loc.isValid()) - return; - PresumedLoc Location = SourceMgr.getPresumedLoc(Loc); - if (!Location.isValid()) - return; - ObligationSource Found; - Found.File = Location.getFilename(); - Found.Line = Location.getLine(); - Found.Column = Location.getColumn(); - PresumedLoc End = - SourceMgr.getPresumedLoc(EndLoc.isValid() ? EndLoc : Loc); - if (End.isValid() && Found.File == End.getFilename() && - (End.getLine() > Found.Line || - (End.getLine() == Found.Line && End.getColumn() >= Found.Column))) { - Found.EndLine = End.getLine(); - Found.EndColumn = End.getColumn(); - } else { - Found.EndLine = Found.Line; - Found.EndColumn = Found.Column; - } - // A term shared with a module annotated before, which other threads - // may be reading, is read and not written. - if (Source.File != Found.File || Source.Line != Found.Line || - Source.Column != Found.Column || Source.EndLine != Found.EndLine || - Source.EndColumn != Found.EndColumn) - Source = std::move(Found); - }; - // Terms share subterms: each node once. - llvm::DenseSet Annotated; - auto annotateExpr = [&](const LogicExpr *Root) { - if (!Root) - return; - std::vector Pending = {Root}; - while (!Pending.empty()) { - const LogicExpr *Expr = Pending.back(); - Pending.pop_back(); - if (!Annotated.insert(Expr).second) - continue; - annotateSource(Expr->Loc, Expr->EndLoc, Expr->Source); - for (const auto &Child : Expr->Children) - if (Child) - Pending.push_back(Child.get()); - } - }; +public: + explicit LeanProject(const VerifyOptions &Opts) : Opts(Opts) {} - annotateExpr(Module.CorrectnessGoal.get()); - annotateExpr(Module.CounterexampleQuery.get()); - std::map StableIdCounts; - for (Obligation &Item : Module.Obligations) { - annotateSource(Item.Loc, Item.EndLoc, Item.Source); - std::string StableId = - Module.FunctionIdentity + "::" + obligationKindName(Item.Kind) + "@"; - StableId += Item.Source.isValid() - ? std::to_string(Item.Source.Line) + ":" + - std::to_string(Item.Source.Column) - : "synthetic"; - unsigned &Count = StableIdCounts[StableId]; - if (++Count > 1) - StableId += "#" + std::to_string(Count); - Item.StableId = std::move(StableId); - annotateExpr(Item.Goal.get()); - annotateExpr(Item.CounterexampleQuery.get()); - } - for (auto &[InternalName, Variable] : Module.DiagnosticVariables) { - (void)InternalName; - annotateSource(Variable.Loc, Variable.EndLoc, Variable.Source); - } - for (DiagnosticTraceEvent &Event : Module.TraceEvents) { - annotateSource(Event.Loc, Event.EndLoc, Event.Source); - annotateExpr(Event.Guard.get()); - for (DiagnosticTraceValue &Value : Event.Values) - annotateExpr(Value.Value.get()); - } - for (auto &[Identity, Function] : Module.LogicFunctions) { - (void)Identity; - annotateExpr(Function.StepDefinition.get()); - for (auto &Definition : Function.DefinitionLevels) - annotateExpr(Definition.get()); - } - } + std::vector takeWarnings() { return std::move(Warnings); } - llvm::Expected initializeLeanProject() { + llvm::Error initializeLeanProject() { const std::string &Root = leanProjectRoot(); std::error_code EC = llvm::sys::fs::create_directories( leanProjectPath(Root, {"CppVerify", "Proofs"})); @@ -1649,14 +1594,12 @@ class Verifier { LeanToolchain.str().c_str()); if (llvm::Error Error = writeTextFile(ToolchainPath, Toolchain)) return std::move(Error); - Diags.push_back( - {VerifyDiagnostic::Warning, - "the Lean project " + Root + " pinned " + Pin + - ", which an earlier release of cpp-verify wrote; it now " - "pins " + - LeanToolchain.str() + - ", so a proof that no longer builds needs updating for " - "that Lean"}); + Warnings.push_back("the Lean project " + Root + " pinned " + Pin + + ", which an earlier release of cpp-verify wrote; " + "it now pins " + + LeanToolchain.str() + + ", so a proof that no longer builds needs " + "updating for that Lean"); } } else if (llvm::Error Error = writeTextFile(ToolchainPath, Toolchain)) { return std::move(Error); @@ -1672,18 +1615,104 @@ class Verifier { leanProjectPath(Root, {"lakefile.toml"}), Lakefile)) return std::move(Error); + // A project of an earlier release imports CppVerify.Generated here, + // which still imports every goal. constexpr llvm::StringLiteral UserFile = - "import CppVerify.Generated\n\n" + "import CppVerify.Specs\n\n" "/- Add reusable lemmas here. This file is never regenerated. -/\n"; if (llvm::Error Error = writeTextFileIfMissing( leanProjectPath(Root, {"CppVerify", "User.lean"}), UserFile)) - return std::move(Error); - if (llvm::Error Error = - writeTextFile(leanProjectPath(Root, {"CppVerify", "Auto.lean"}), - LeanAutomationSource)) - return std::move(Error); + return Error; + + std::string Prelude = std::string(LeanGeneratedNote) + "\n\n"; + llvm::raw_string_ostream PreludeOS(Prelude); + emitLeanPrelude(PreludeOS, /*Project=*/true); + for (const auto &[File, Contents] : + {std::pair{"Prelude.lean", Prelude}, + {"Auto.lean", LeanAutomationSource}}) + if (llvm::Error Error = writeTextFileIfChanged( + leanProjectPath(Root, {"CppVerify", File}), Contents)) + return Error; + // The root of the library, so that `lake build` builds every proof. + return writeTextFileIfChanged(leanProjectPath(Root, {"CppVerify.lean"}), + "import CppVerify.Check\n"); + } + + // The generated modules of a run: Specs, a goal module for each function + // the run exported, and Generated, which imports them all. Any other goal + // module is removed, so that every module of the project builds: one an + // earlier run wrote may apply a spec Specs no longer declares, as after a + // fallback run whose solver settled the functions that applied it. The + // proofs of a function the run did not export stay as they are. + llvm::Error writeLeanProjectModules(const LeanProjectModules &Modules) { + const std::string &Root = leanProjectRoot(); + const std::string Header = std::string(LeanGeneratedNote) + "\n"; + if (llvm::Error Error = writeTextFileIfChanged( + leanProjectPath(Root, {"CppVerify", "Specs.lean"}), + Header + + "import CppVerify.Prelude\n\nnoncomputable section\n\n" + "attribute [local instance] cppVerifyPropDecidable\n\n" + + leanSpecsText(Modules))) + return Error; + const std::string GoalsDir = leanProjectPath(Root, {"CppVerify", "Goals"}); + if (std::error_code EC = llvm::sys::fs::create_directories(GoalsDir)) + return llvm::createStringError(EC, "cannot create %s", GoalsDir.c_str()); + std::string Generated = Header + "import CppVerify.Prelude\n" + "import CppVerify.Specs\n"; + for (const std::string &Group : Modules.Order) { + if (llvm::Error Error = writeTextFileIfChanged( + leanProjectPath(GoalsDir, {Group + ".lean"}), + Header + "import CppVerify.Specs\n\n" + + LeanGoalModulePreamble.str() + Modules.Goals.at(Group))) + return Error; + Generated += "import CppVerify.Goals." + Group + "\n"; + } + if (llvm::Error Error = writeTextFileIfChanged( + leanProjectPath(Root, {"CppVerify", "Generated.lean"}), Generated)) + return Error; + + std::error_code EC; + for (llvm::sys::fs::directory_iterator It(GoalsDir, EC), End; + It != End && !EC; It.increment(EC)) + if (llvm::sys::path::extension(It->path()) == ".lean" && + !Modules.Goals.count(llvm::sys::path::stem(It->path()).str())) + llvm::sys::fs::remove(It->path()); + return llvm::Error::success(); + } - return leanProjectPath(Root, {"CppVerify", "Generated.lean"}); + // The refutation of each certified failure of the run, + // Refutations//.lean. Any other is removed: its failure is + // gone, or its program is not this one. + llvm::Error + writeLeanRefutations(const std::vector &Refutations) { + const std::string Dir = + leanProjectPath(leanProjectRoot(), {"CppVerify", "Refutations"}); + std::set Written; + for (const LeanRefutation &Refutation : Refutations) { + const std::string Path = + leanProjectPath(Dir, {Refutation.Group, Refutation.Goal + ".lean"}); + if (std::error_code EC = llvm::sys::fs::create_directories( + llvm::sys::path::parent_path(Path))) + return llvm::createStringError(EC, "cannot create %s", Path.c_str()); + if (llvm::Error Error = writeTextFileIfChanged( + Path, std::string(LeanGeneratedNote) + "\n" + Refutation.Text)) + return Error; + Written.insert(Path); + } + std::error_code EC; + std::vector Gone; + for (llvm::sys::fs::recursive_directory_iterator It(Dir, EC), End; + It != End && !EC; It.increment(EC)) + if (llvm::sys::path::extension(It->path()) == ".lean" && + !Written.count(It->path())) + Gone.push_back(It->path()); + for (const std::string &Path : Gone) { + llvm::sys::fs::remove(Path); + llvm::sys::fs::remove(llvm::sys::path::parent_path(Path)); + } + if (Written.empty()) + llvm::sys::fs::remove(Dir); + return llvm::Error::success(); } static std::vector @@ -1802,15 +1831,15 @@ class Verifier { (Moving->Path != ProofPath || !Taken.count(ProofPath))) { const bool WasKept = llvm::StringRef(Moving->Path).starts_with(StaleDir); - if (llvm::Error Error = moveLeanProof(Moving->Path, ProofPath, *Goal, - Moving->Goal, Moving->BodyPrefix)) + if (llvm::Error Error = + moveLeanProof(Moving->Path, ProofPath, *Goal, Moving->Goal, + Moving->BodyPrefix, groupOf(*Moving))) return Error; - Diags.push_back({VerifyDiagnostic::Warning, - "the Lean proof of " + Goal->StableId + - (WasKept ? " came back from " : " moved from ") + - relative(Moving->Path) + " to " + - relative(ProofPath) + - ", the file of the goal with its statement"}); + Warnings.push_back("the Lean proof of " + Goal->StableId + + (WasKept ? " came back from " : " moved from ") + + relative(Moving->Path) + " to " + + relative(ProofPath) + + ", the file of the goal with its statement"); Taken.insert(Moving->Path); Taken.insert(ProofPath); Emptied.insert(llvm::sys::path::parent_path(Moving->Path).str()); @@ -1821,11 +1850,9 @@ class Verifier { Goal->LegacyBodyPrefix)) return Error; if (!Stub) - Diags.push_back({VerifyDiagnostic::Warning, - "the Lean proof " + relative(LegacyPath) + - " that cpp-verify 0.1 wrote for " + - Goal->StableId + " is now " + - relative(ProofPath)}); + Warnings.push_back("the Lean proof " + relative(LegacyPath) + + " that cpp-verify 0.1 wrote for " + + Goal->StableId + " is now " + relative(ProofPath)); Legacy.erase(LegacyPath); Taken.insert(ProofPath); } else if (llvm::sys::fs::exists(ProofPath)) { @@ -1841,9 +1868,10 @@ class Verifier { llvm::sys::path::parent_path(ProofPath))) return llvm::createStringError(EC, "cannot create %s", ProofPath.c_str()); - if (llvm::Error Error = writeTextFile( - ProofPath, leanProofHeader(*Goal) + - leanProof(Goal->Name, Goal->Intros, "sorry"))) + if (llvm::Error Error = + writeTextFile(ProofPath, leanProofHeader(*Goal) + + leanProof(Goal->Name, Goal->Group, + Goal->Intros, "sorry"))) return Error; Taken.insert(ProofPath); } @@ -1870,9 +1898,8 @@ class Verifier { llvm::raw_fd_ostream Note(Target, EC, llvm::sys::fs::OF_Append); if (!EC) Note << "\n" << LeanQuarantineNote << Why << "\n"; - Diags.push_back({VerifyDiagnostic::Warning, - "the Lean proof " + relative(Path) + " is kept in " + - relative(Target) + ": " + Why}); + Warnings.push_back("the Lean proof " + relative(Path) + " is kept in " + + relative(Target) + ": " + Why); return llvm::Error::success(); }; for (const LeanProofRecord &Record : Records) { @@ -1918,12 +1945,11 @@ class Verifier { if (std::error_code EC = llvm::sys::fs::remove_directories(StaleDir)) return llvm::createStringError(EC, "cannot remove %s", StaleDir.c_str()); - Diags.push_back({VerifyDiagnostic::Warning, - "removed " + - (Pruned == 1 ? std::string("the Lean proof") - : "the " + std::to_string(Pruned) + - " Lean proofs") + - " kept in Stale/, as --lean-prune-stale asks"}); + Warnings.push_back( + "removed " + + (Pruned == 1 ? std::string("the Lean proof") + : "the " + std::to_string(Pruned) + " Lean proofs") + + " kept in Stale/, as --lean-prune-stale asks"); } CheckFile += "\n"; @@ -1931,8 +1957,8 @@ class Verifier { CheckFile += "example : " + Name + " := " + Name + "_proof\n"; CheckFile += "#print axioms " + Name + "_proof\n"; } - return writeTextFile(leanProjectPath(Root, {"CppVerify", "Check.lean"}), - CheckFile); + return writeTextFileIfChanged( + leanProjectPath(Root, {"CppVerify", "Check.lean"}), CheckFile); } // Certifies each goal's proof: an error fails every goal; otherwise the @@ -2008,9 +2034,301 @@ class Verifier { return Launcher; } + // The specs of a project as Specs.lean declares them: each recursion group + // by its real definitions where Lean accepts them, callees first, and + // opaque where it does not, where a member has no definition, or where no + // Lean is installed to try them. Lean's verdict on a group's text, with + // what its callees are, is kept in .cppverify/specs, so that the file is + // the same on every run. A group is tried with its undecided callees real + // and keeps its verdict only if they are; else it is tried again with the + // rejected callees opaque. + std::string leanSpecsText(const LeanProjectModules &Modules) { + const auto &Specs = Modules.Specs; + // Strongly connected components of the calls, callees first (Tarjan). + std::vector> Groups; + std::map GroupOf; + { + std::map Index, Low; + std::set OnStack; + std::vector Stack; + unsigned Counter = 0; + std::function visit = + [&](const std::string &V) { + Index[V] = Low[V] = Counter++; + Stack.push_back(V); + OnStack.insert(V); + for (const std::string &W : Specs.at(V).Applies) { + if (!Specs.count(W)) + continue; + if (!Index.count(W)) { + visit(W); + Low[V] = std::min(Low[V], Low[W]); + } else if (OnStack.count(W)) { + Low[V] = std::min(Low[V], Index[W]); + } + } + if (Low[V] != Index[V]) + return; + std::vector Group; + std::string W; + do { + W = Stack.back(); + Stack.pop_back(); + OnStack.erase(W); + Group.push_back(W); + GroupOf[W] = Groups.size(); + } while (W != V); + std::sort(Group.begin(), Group.end()); + Groups.push_back(std::move(Group)); + }; + for (const auto &[Identity, Declaration] : Specs) + if (!Index.count(Identity)) + visit(Identity); + } + std::vector> Callees(Groups.size()); + std::vector Definition(Groups.size()); + // 1 real, 0 opaque, -1 undecided. + std::vector Verdict(Groups.size(), -1); + for (size_t G = 0; G != Groups.size(); ++G) { + for (const std::string &Member : Groups[G]) { + const LeanSpecDeclaration &Declaration = Specs.at(Member); + if (Declaration.Definition.empty()) + Verdict[G] = 0; + Definition[G] += Declaration.Definition; + for (const std::string &Callee : Declaration.Applies) + if (Specs.count(Callee) && GroupOf.at(Callee) != G) + Callees[G].insert(GroupOf.at(Callee)); + } + if (Groups[G].size() > 1) + Definition[G] = "mutual\n" + Definition[G] + "end\n\n"; + } + auto opaque = [&](size_t G) { + std::string Text; + for (const std::string &Member : Groups[G]) + Text += Specs.at(Member).Opaque; + return Text; + }; + // What a group's verdict rests on: the toolchain, this revision of the + // printer and the prelude's tactic, its text, and what its callees are. + auto keyOf = [&](size_t G, const std::set &Assumed) { + llvm::SHA256 Hasher; + Hasher.update(LeanToolchain); + Hasher.update(LeanSpecRevision); + Hasher.update(Definition[G]); + for (size_t C : Callees[G]) + Hasher.update(Groups[C].front() + (Verdict[C] == 1 || Assumed.count(C) + ? "=real" + : "=opaque")); + return llvm::toHex(Hasher.final()); + }; + const std::string &Root = leanProjectRoot(); + const std::string Dir = leanProjectPath(Root, {".cppverify", "specs"}); + std::optional Launcher; + bool LeanUnavailable = false; + for (size_t Round = 0; Round <= Groups.size(); ++Round) { + std::set Trying; + std::map Keys; + for (size_t G = 0; G != Groups.size(); ++G) { + if (Verdict[G] >= 0) + continue; + const bool CalleesDecided = + llvm::all_of(Callees[G], [&](size_t C) { return Verdict[C] >= 0; }); + const std::string Key = keyOf(G, Trying); + if (CalleesDecided) + if (auto Kept = + llvm::MemoryBuffer::getFile(leanProjectPath(Dir, {Key}))) { + Verdict[G] = (*Kept)->getBuffer().trim() == "real" ? 1 : 0; + continue; + } + if (LeanUnavailable) { + Verdict[G] = 0; + continue; + } + Trying.insert(G); + Keys[G] = Key; + } + if (Trying.empty()) + break; + std::map Axioms; + if (!tryLeanSpecs(Specs, Groups, Definition, Verdict, Trying, Launcher, + opaque, Axioms)) { + LeanUnavailable = true; + for (size_t G : Trying) + Verdict[G] = 0; + break; + } + std::map Accepted; + for (size_t G : Trying) + Accepted[G] = llvm::all_of(Groups[G], [&](const std::string &Member) { + auto It = Axioms.find(Specs.at(Member).Name); + if (It == Axioms.end() || It->second == "missing") + return false; + llvm::SmallVector Used; + llvm::StringRef(It->second).split(Used, ',', -1, false); + return llvm::all_of(Used, [](llvm::StringRef Axiom) { + return Axiom == "propext" || Axiom == "Quot.sound" || + Axiom == "Classical.choice"; + }); + }); + // A callee decided before this round was what the trial assumed; one + // tried in it was assumed real. + for (size_t G : Trying) { + if (!llvm::all_of(Callees[G], [&](size_t C) { + return !Trying.count(C) || Accepted[C]; + })) + continue; + Verdict[G] = Accepted[G] ? 1 : 0; + if (!llvm::sys::fs::create_directories(Dir)) + llvm::consumeError( + writeTextFile(leanProjectPath(Dir, {Keys[G]}), + Accepted[G] ? "real\n" : "opaque\n")); + } + } + std::string Text; + for (size_t G = 0; G != Groups.size(); ++G) + Text += Verdict[G] == 1 ? Definition[G] : opaque(G); + return Text; + } + + // Elaborates the spec groups of Trying with the others as decided, in a + // file of their own, and reports the axioms of each member of Trying: a + // definition Lean rejects is added with sorryAx, or not at all. False when + // no pinned Lean can run it. + bool tryLeanSpecs(const std::map &Specs, + const std::vector> &Groups, + const std::vector &Definition, + const std::vector &Verdict, + const std::set &Trying, + std::optional &Launcher, + llvm::function_ref Opaque, + std::map &Axioms) { + const std::string &Root = leanProjectRoot(); + if (!Launcher) { + auto Found = leanLauncher(".cppverify-specs"); + if (!Found) { + llvm::consumeError(Found.takeError()); + return false; + } + if (auto Build = Found->run({"build", "CppVerify.Auto"}, "build", Root); + !Build) { + llvm::consumeError(Build.takeError()); + return false; + } + Launcher = std::move(*Found); + } + const std::string Dir = leanProjectPath(Root, {".cppverify", "specs"}); + if (llvm::sys::fs::create_directories(Dir)) + return false; + const std::string Source = leanProjectPath(Dir, {"trial.lean"}); + const std::string Report = leanProjectPath(Dir, {"trial.report"}); + std::string Quoted = "\""; + for (char C : Report) { + if (C == '\\' || C == '"') + Quoted += '\\'; + Quoted += C; + } + Quoted += "\""; + std::string Text = "import CppVerify.Auto\n\n#eval CppVerify.Auto.report " + + Quoted + + " \"ready\"\n\nnoncomputable section\n\n" + "attribute [local instance] cppVerifyPropDecidable\n\n"; + for (size_t G = 0; G != Groups.size(); ++G) { + if (Verdict[G] == 1 || Trying.count(G)) + Text += Definition[G]; + else + Text += Opaque(G); + if (Trying.count(G)) + for (const std::string &Member : Groups[G]) + Text += "#cppverify_axioms " + Quoted + " " + Specs.at(Member).Name + + "\n\n"; + } + llvm::sys::fs::remove(Report); + if (llvm::Error Error = writeTextFile(Source, Text)) { + llvm::consumeError(std::move(Error)); + return false; + } + // Lean exits with an error status when a definition fails; the report + // says which. + if (auto Run = Launcher->run({"env", "lean", "-DmaxErrors=1000000", Source}, + "specs", Root); + !Run) + llvm::consumeError(Run.takeError()); + auto Buffer = llvm::MemoryBuffer::getFile(Report); + if (!Buffer) + return false; + bool Ready = false; + for (llvm::StringRef Rest = (*Buffer)->getBuffer(); !Rest.empty();) { + auto [Line, Next] = Rest.split('\n'); + Rest = Next; + Line = Line.rtrim("\r"); + if (Line == "ready") { + Ready = true; + continue; + } + llvm::SmallVector Fields; + Line.split(Fields, '\t', 2); + if (Fields.size() == 3 && Fields[0] == "axioms") + Axioms[Fields[1].str()] = Fields[2].str(); + } + return Ready; + } + + // What a proof is built from: the pinned toolchain, its source, and the + // compiled modules of the project in its import closure, read from the + // import lines of their sources. None when a module of the closure has no + // compiled module. + std::optional leanProofKey(llvm::StringRef ProofSource) { + const std::string &Root = leanProjectRoot(); + auto importsOf = [](llvm::StringRef Source) { + std::vector Imports; + if (auto Buffer = llvm::MemoryBuffer::getFile(Source)) + for (llvm::StringRef Rest = (*Buffer)->getBuffer(); !Rest.empty();) { + auto [Line, Next] = Rest.split('\n'); + Rest = Next; + if (Line.consume_front("import ")) + Imports.push_back(Line.trim().str()); + } + return Imports; + }; + llvm::SHA256 Hasher; + Hasher.update(LeanToolchain); + auto Source = llvm::MemoryBuffer::getFile(ProofSource); + if (!Source) + return std::nullopt; + Hasher.update((*Source)->getBuffer()); + std::map Compiled; + std::vector Work = importsOf(ProofSource); + while (!Work.empty()) { + const std::string Module = Work.back(); + Work.pop_back(); + if (Compiled.count(Module)) + continue; + std::string Relative = Module; + std::replace(Relative.begin(), Relative.end(), '.', '/'); + const std::string ModuleSource = + leanProjectPath(Root, {Relative + ".lean"}); + if (!llvm::sys::fs::exists(ModuleSource)) + continue; + auto Object = llvm::MemoryBuffer::getFile(leanProjectPath( + Root, {".lake", "build", "lib", "lean", Relative + ".olean"})); + if (!Object) + return std::nullopt; + Compiled[Module] = llvm::toHex(llvm::SHA256::hash( + llvm::arrayRefFromStringRef((*Object)->getBuffer()))); + for (std::string &Import : importsOf(ModuleSource)) + Work.push_back(std::move(Import)); + } + for (const auto &[Module, Hash] : Compiled) { + Hasher.update(Module); + Hasher.update(Hash); + } + return llvm::toHex(Hasher.final()); + } + llvm::Expected> - certifyLeanProject(const std::vector &ProjectGoals) { - if (ProjectGoals.empty()) + certifyLeanProject(const std::vector &ProjectGoals, + const std::vector &Refutations = {}) { + if (ProjectGoals.empty() && Refutations.empty()) return llvm::createStringError(llvm::inconvertibleErrorCode(), "the project has no proof obligations"); auto Launcher = leanLauncher(".cppverify-certification"); @@ -2028,15 +2346,6 @@ class Verifier { !Build) return Build.takeError(); - const std::string UserObject = leanProjectPath( - Root, {".lake", "build", "lib", "lean", "CppVerify", "User.olean"}); - if (auto User = - runLake({"env", "lean", "-EhasSorry", "-R", Root, "-o", UserObject, - leanProjectPath(Root, {"CppVerify", "User.lean"})}, - "user"); - !User) - return User.takeError(); - const std::string ProofBuildDir = leanProjectPath( Root, {".lake", "build", "lib", "lean", "CppVerify", "Proofs"}); if (std::error_code EC = llvm::sys::fs::create_directories(ProofBuildDir)) @@ -2046,31 +2355,70 @@ class Verifier { std::map UniqueGoals; for (const LeanProjectGoal &Goal : ProjectGoals) UniqueGoals.emplace(Goal.Name, &Goal); - std::vector> Proofs( - UniqueGoals.begin(), UniqueGoals.end()); - for (const auto &[Name, Goal] : Proofs) + // What is built: each goal's proof, and each refutation, which is its + // goal's module too; with the certifier's line for each, and the name + // it reports. + struct Build { + std::string Name, Source, Object, Entry; + }; + std::vector Proofs; + for (const auto &[Name, Goal] : UniqueGoals) { + std::string File; + Proofs.push_back( + {Name, leanProjectPath(Root, leanProofComponents(*Goal, File)), + leanProjectPath(leanProjectPath(ProofBuildDir, {Goal->Group}), + {Name + ".olean"}), + Name + "\tCppVerify.Proofs." + Goal->Group + "." + Name + + "\tCppVerify.Goals." + Goal->Group + "\n"}); + } + for (const LeanRefutation &Refutation : Refutations) { + const std::string Module = + "CppVerify.Refutations." + Refutation.Group + "." + Refutation.Goal; + Proofs.push_back( + {Refutation.Goal + "_refuted", + leanProjectPath(Root, {"CppVerify", "Refutations", Refutation.Group, + Refutation.Goal + ".lean"}), + leanProjectPath(Root, {".lake", "build", "lib", "lean", "CppVerify", + "Refutations", Refutation.Group, + Refutation.Goal + ".olean"}), + Refutation.Goal + "\t" + Module + "\t" + Module + "\trefutation\n"}); + } + for (const Build &Proof : Proofs) if (std::error_code EC = llvm::sys::fs::create_directories( - leanProjectPath(ProofBuildDir, {Goal->Group}))) + llvm::sys::path::parent_path(Proof.Object))) return llvm::createStringError( EC, "cannot create Lean proof build directory"); - // Each proof builds by itself, several at once. + // Each proof builds by itself, several at once, unless nothing it is + // built from changed since it last built: its source and the compiled + // modules of the project it imports (which Lake has just built). Lake + // cannot say this itself: a module whose import failed is not built and + // not named, and its .olean of an earlier build would stay. + const std::string KeyDir = leanProjectPath(Root, {".cppverify", "built"}); + if (std::error_code EC = llvm::sys::fs::create_directories(KeyDir)) + return llvm::createStringError(EC, "cannot create %s", KeyDir.c_str()); std::vector> BuildErrors(Proofs.size()); std::atomic NextProof{0}; auto buildProofs = [&] { for (size_t I; (I = NextProof++) < Proofs.size();) { - const auto &[Name, Goal] = Proofs[I]; - std::string File; - const std::string ProofSource = - leanProjectPath(Root, leanProofComponents(*Goal, File)); - const std::string ProofObject = leanProjectPath( - leanProjectPath(ProofBuildDir, {Goal->Group}), {Name + ".olean"}); + const std::string &Name = Proofs[I].Name; + const std::string &ProofSource = Proofs[I].Source; + const std::string &ProofObject = Proofs[I].Object; + const std::string KeyFile = leanProjectPath(KeyDir, {Name}); + const std::optional Key = leanProofKey(ProofSource); + if (Key && llvm::sys::fs::exists(ProofObject)) + if (auto Kept = llvm::MemoryBuffer::getFile(KeyFile)) + if ((*Kept)->getBuffer() == *Key) + continue; llvm::sys::fs::remove(ProofObject); + llvm::sys::fs::remove(KeyFile); if (auto Proof = runLake({"env", "lean", "-EhasSorry", "-R", Root, "-o", ProofObject, ProofSource}, Name); !Proof) BuildErrors[I] = llvm::StringRef(llvm::toString(Proof.takeError())).rtrim().str(); + else if (Key) + llvm::consumeError(writeTextFile(KeyFile, *Key)); } }; { @@ -2083,12 +2431,11 @@ class Verifier { std::map Failures; std::string Built; for (size_t I = 0; I != Proofs.size(); ++I) { - const auto &[Name, Goal] = Proofs[I]; if (BuildErrors[I]) { - Failures[Name] = *BuildErrors[I]; + Failures[Proofs[I].Name] = *BuildErrors[I]; continue; } - Built += Name + "\tCppVerify.Proofs." + Goal->Group + "." + Name + "\n"; + Built += Proofs[I].Entry; } if (Built.empty()) return Failures; @@ -2121,9 +2468,9 @@ class Verifier { Failures[Fields[1].str()] = Fields.size() == 3 ? Fields[2].str() : "the certifier failed"; } - for (const auto &[Name, Goal] : UniqueGoals) - if (!Failures.count(Name) && !Checks.count(Name)) - Failures[Name] = "the certifier did not check it"; + for (const Build &Proof : Proofs) + if (!Failures.count(Proof.Name) && !Checks.count(Proof.Name)) + Failures[Proof.Name] = "the certifier did not check it"; return Failures; } @@ -2179,7 +2526,8 @@ class Verifier { : ""; for (size_t Next = 0; Next < Goals.size() && !interruptRequested();) { - std::string Text = "import CppVerify.User\nimport CppVerify.Auto\n\n" + std::string Text = "import CppVerify.Generated\nimport CppVerify.User\n" + "import CppVerify.Auto\n\n" "set_option cppverify.autoReport " + ReportName + "\n#eval CppVerify.Auto.report " + ReportName + " \"ready\"\n"; @@ -2318,9 +2666,9 @@ class Verifier { return Trials; } - // Each export's count of the goals cppverify_auto proves, from the proof - // files as they are now. - void countLeanAutomation(const std::vector &ProjectGoals) { + // The goals cppverify_auto proves, from the proof files as they are now. + std::set + automaticLeanGoals(const std::vector &ProjectGoals) const { std::set Automatic; for (const LeanProjectGoal &Goal : ProjectGoals) { std::string File; @@ -2331,12 +2679,7 @@ class Verifier { if (Record->Automatic) Automatic.insert(Goal.Name); } - for (VerifyDiagnostic &Diagnostic : Diags) - if (Diagnostic.K == VerifyDiagnostic::Exported && Diagnostic.Result) - Diagnostic.LeanAutomatic = llvm::count_if( - std::set(Diagnostic.Result->LeanGoals.begin(), - Diagnostic.Result->LeanGoals.end()), - [&](const std::string &Goal) { return Automatic.count(Goal); }); + return Automatic; } // Tries cppverify_auto on each goal whose proof file is still the one @@ -2369,12 +2712,12 @@ class Verifier { return; } auto skipped = [&](const std::string &Why) { - Diags.push_back( - {VerifyDiagnostic::Warning, - "cppverify_auto was not tried on the new Lean goals: " + Why}); + Warnings.push_back( + "cppverify_auto was not tried on the new Lean goals: " + Why); }; - if (auto Build = Launcher->run( - {"build", "CppVerify.User", "CppVerify.Auto"}, "build", Root); + if (auto Build = Launcher->run({"build", "CppVerify.Generated", + "CppVerify.User", "CppVerify.Auto"}, + "build", Root); !Build) return skipped( "the project does not build (" + @@ -2425,20 +2768,145 @@ class Verifier { const std::string Text = Trial.Step.empty() ? leanProofHeader(Goal, Tag + Trial.Why) + - leanProof(Goal.Name, Goal.Intros, "sorry") + leanProof(Goal.Name, Goal.Group, Goal.Intros, "sorry") : leanProofHeader(Goal, Tag + "proves it with " + Trial.Step, /*Automatic=*/true) + - leanProof(Goal.Name, Goal.Intros, Trial.Step); + leanProof(Goal.Name, Goal.Group, Goal.Intros, Trial.Step); if (llvm::Error Error = writeTextFile(pathOf(Goal), Text)) Failures.push_back(llvm::toString(std::move(Error))); } std::set Reported; for (const std::string &Failure : Failures) if (!Failure.empty() && Reported.insert(Failure).second) - Diags.push_back({VerifyDiagnostic::Warning, - "cppverify_auto stopped before every new Lean goal " - "was tried: " + - Failure}); + Warnings.push_back("cppverify_auto stopped before every new Lean goal " + "was tried: " + + Failure); + } +}; + +class Verifier { + ASTContext &Ctx; + VerifyOptions Opts; + llvm::raw_ostream *DumpOS = nullptr; + std::vector Diags; + std::optional Lean; + + void reportLeanWarnings() { + for (std::string &Warning : Lean->takeWarnings()) + Diags.push_back({VerifyDiagnostic::Warning, std::move(Warning)}); + } + + /// The archive record of Module, empty without --obligation-out. + llvm::Expected archiveRecord(const ObligationModule &Module) { + if (!Opts.ObligationOut) + return std::string(); + + std::string Serialized = serializeObligationModule(Module); + auto RoundTrip = deserializeObligationModules(Serialized); + if (!RoundTrip) + return RoundTrip.takeError(); + if (RoundTrip->size() != 1 || + serializeObligationModule(RoundTrip->front()) != Serialized || + obligationSemanticHash(RoundTrip->front()) != + obligationSemanticHash(Module)) + return llvm::createStringError( + llvm::inconvertibleErrorCode(), + "canonical obligation serialization did not round-trip"); + return Serialized; + } + + llvm::Error emitObligationArchive(const ObligationModule &Module) { + llvm::Expected Record = archiveRecord(Module); + if (!Record) + return Record.takeError(); + if (Opts.ObligationOut) + *Opts.ObligationOut << *Record; + return llvm::Error::success(); + } + + void annotateObligationSources(ObligationModule &Module) { + const SourceManager &SourceMgr = Ctx.getSourceManager(); + auto annotateSource = [&](SourceLocation Loc, SourceLocation EndLoc, + ObligationSource &Source) { + if (!Loc.isValid()) + return; + PresumedLoc Location = SourceMgr.getPresumedLoc(Loc); + if (!Location.isValid()) + return; + ObligationSource Found; + Found.File = Location.getFilename(); + Found.Line = Location.getLine(); + Found.Column = Location.getColumn(); + PresumedLoc End = + SourceMgr.getPresumedLoc(EndLoc.isValid() ? EndLoc : Loc); + if (End.isValid() && Found.File == End.getFilename() && + (End.getLine() > Found.Line || + (End.getLine() == Found.Line && End.getColumn() >= Found.Column))) { + Found.EndLine = End.getLine(); + Found.EndColumn = End.getColumn(); + } else { + Found.EndLine = Found.Line; + Found.EndColumn = Found.Column; + } + // A term shared with a module annotated before, which other threads + // may be reading, is read and not written. + if (Source.File != Found.File || Source.Line != Found.Line || + Source.Column != Found.Column || Source.EndLine != Found.EndLine || + Source.EndColumn != Found.EndColumn) + Source = std::move(Found); + }; + // Terms share subterms: each node once. + llvm::DenseSet Annotated; + auto annotateExpr = [&](const LogicExpr *Root) { + if (!Root) + return; + std::vector Pending = {Root}; + while (!Pending.empty()) { + const LogicExpr *Expr = Pending.back(); + Pending.pop_back(); + if (!Annotated.insert(Expr).second) + continue; + annotateSource(Expr->Loc, Expr->EndLoc, Expr->Source); + for (const auto &Child : Expr->Children) + if (Child) + Pending.push_back(Child.get()); + } + }; + + annotateExpr(Module.CorrectnessGoal.get()); + annotateExpr(Module.CounterexampleQuery.get()); + std::map StableIdCounts; + for (Obligation &Item : Module.Obligations) { + annotateSource(Item.Loc, Item.EndLoc, Item.Source); + std::string StableId = + Module.FunctionIdentity + "::" + obligationKindName(Item.Kind) + "@"; + StableId += Item.Source.isValid() + ? std::to_string(Item.Source.Line) + ":" + + std::to_string(Item.Source.Column) + : "synthetic"; + unsigned &Count = StableIdCounts[StableId]; + if (++Count > 1) + StableId += "#" + std::to_string(Count); + Item.StableId = std::move(StableId); + annotateExpr(Item.Goal.get()); + annotateExpr(Item.CounterexampleQuery.get()); + } + for (auto &[InternalName, Variable] : Module.DiagnosticVariables) { + (void)InternalName; + annotateSource(Variable.Loc, Variable.EndLoc, Variable.Source); + } + for (DiagnosticTraceEvent &Event : Module.TraceEvents) { + annotateSource(Event.Loc, Event.EndLoc, Event.Source); + annotateExpr(Event.Guard.get()); + for (DiagnosticTraceValue &Value : Event.Values) + annotateExpr(Value.Value.get()); + } + for (auto &[Identity, Function] : Module.LogicFunctions) { + (void)Identity; + annotateExpr(Function.StepDefinition.get()); + for (auto &Definition : Function.DefinitionLevels) + annotateExpr(Definition.get()); + } } void checkCalleeRecommendsOnFailure(const VFunction &Caller, @@ -2613,13 +3081,33 @@ class Verifier { Converter.getUnverifiedCallers(); for (const auto &[Loc, Message] : Converter.getWarnings()) Diags.push_back({VerifyDiagnostic::Warning, Message, Loc}); + // A construct the verifier does not represent costs its function alone: + // the function is Unresolved, its first error carrying the verdict, and + // the rest of the translation unit is verified. An error outside every + // function stops the run. + bool OutsideFunctions = false; + std::map UnsupportedConstructs; for (const ConversionError &Err : Converter.getErrors()) { VerifyDiagnostic D{VerifyDiagnostic::Error, Err.Message, Err.Range.getBegin()}; D.Span = Err.Range; + if (Err.Function.empty()) + OutsideFunctions = true; + else + D.FunctionName = Err.FunctionName; + if (!Err.Function.empty() && + UnsupportedConstructs.emplace(Err.Function, Err.Message).second) { + VerifyResult R; + R.Status = VerifyStatus::Unresolved; + R.Reason = VerifyReason::UnsupportedConstruct; + R.Message = Err.Message; + D.Result = std::move(R); + } Diags.push_back(std::move(D)); } - if (!Converter.getErrors().empty()) + if (OutsideFunctions) + return false; + if (Functions.empty() && !UnsupportedConstructs.empty()) return false; if (Functions.empty()) { if (Opts.LeanCertify) { @@ -2664,19 +3152,19 @@ class Verifier { std::string LeanOutputPath = Opts.LeanOutPath; const bool HasLeanProject = !Opts.LeanProjectPath.empty() || !Opts.LeanFallbackProjectPath.empty(); + // A project's goals go to modules, one per function. + LeanProjectModules LeanModules; if (HasLeanProject) { - auto GeneratedPath = initializeLeanProject(); - if (!GeneratedPath) { + Lean.emplace(Opts); + llvm::Error Error = Lean->initializeLeanProject(); + reportLeanWarnings(); + if (Error) { Diags.push_back({VerifyDiagnostic::Error, "cannot initialize Lean project (" + - llvm::toString(GeneratedPath.takeError()) + ")"}); + llvm::toString(std::move(Error)) + ")"}); return false; } - LeanOutputPath = std::move(*GeneratedPath); - } - if ((Opts.Backend == BackendKind::Lean || - !Opts.LeanFallbackProjectPath.empty()) && - !LeanOutputPath.empty()) { + } else if (Opts.Backend == BackendKind::Lean && !LeanOutputPath.empty()) { std::error_code EC; LeanFile = std::make_unique(LeanOutputPath, EC, llvm::sys::fs::OF_Text); @@ -2720,11 +3208,12 @@ class Verifier { if (!Parallel) SharedBackend = createVerifyBackend( Opts.Backend, LeanOut, Opts.BMCUnroll, Execution, - Opts.LeanProjectPath.empty() ? nullptr : &LeanProjectGoals); + Opts.LeanProjectPath.empty() ? nullptr : &LeanProjectGoals, + Opts.LeanProjectPath.empty() ? nullptr : &LeanModules); std::unique_ptr LeanFallbackBackend; if (!Opts.LeanFallbackProjectPath.empty()) - LeanFallbackBackend = std::make_unique( - LeanFile.get(), &LeanProjectGoals); + LeanFallbackBackend = + std::make_unique(&LeanModules, &LeanProjectGoals); // Vacuity checks: whether a verified function's end is reachable at all. // A vacuous path is refuted quickly; a satisfiable one is not worth // searching long, since an unsettled check only omits a warning. @@ -2744,7 +3233,7 @@ class Verifier { }; const unsigned DumpLayers = Opts.DumpIRLayers; const bool MultiLayerDump = llvm::popcount(DumpLayers) > 1; - bool AllOk = true; + bool AllOk = UnsupportedConstructs.empty(); bool AnyFailed = false; std::set FailedCallers; // A spec whose termination is not established has no definition, so a @@ -2848,6 +3337,14 @@ class Verifier { BackendKind Kind; }; std::vector Confirmations; + /// Certified failures a fallback project shows in Lean: each one's + /// place in Diags and its refutation, or why it has none. + struct Refutation { + size_t Index; + std::optional Lean; + std::string Why; + }; + std::vector Refutations; /// For a verified proof function: what its verification proves. std::shared_ptr Contract; /// The verdict on the definedness of the function's contract, when it @@ -3085,12 +3582,35 @@ class Verifier { // which is reported only if it fails. if (Fn->Uninterpreted || (Fn->IsBuiltin && !Fn->NeedsDecreasesCheck)) continue; + // What a function says depends on the definitions of the specs it + // reaches, so one that reaches a spec the verifier does not + // represent is not verified. + std::string ReliesOn; + for (const std::string &Spec : Fn->SpecDependencies) + if (auto It = FnMap.find(Spec); + It != FnMap.end() && It->second->UnsupportedConstruct) + ReliesOn += (ReliesOn.empty() ? "" : ", ") + It->second->Name; + if (!ReliesOn.empty()) { + AllOk = false; + VerifyResult R; + R.Status = VerifyStatus::Unresolved; + R.Reason = VerifyReason::UnsupportedConstruct; + R.Message = "relies on " + ReliesOn + + ", which uses a construct the verifier does not support"; + VerifyDiagnostic D{VerifyDiagnostic::Unresolved, + Fn->Name + " [reason=construct.unsupported] (" + + R.Message + ")", + Fn->DeclLoc, Fn->Name}; + D.Result = std::move(R); + Diags.push_back(std::move(D)); + continue; + } if (Fn->IsExternalContract) { if (Fn->IsTrusted) Diags.push_back({VerifyDiagnostic::Trusted, Fn->Name + " (contract assumed, not verified)", Fn->DeclLoc, Fn->Name}); - else + else if (!Fn->UnsupportedConstruct) Diags.push_back( {VerifyDiagnostic::Warning, Fn->Name + " has a contract but no definition, so its callers " @@ -4065,6 +4585,21 @@ class Verifier { AnyFailed = true; if (!Fn->IsProof) FailedCallers.insert(Fn->Identity); + // A fallback project shows a certified counterexample in Lean too. + if (LeanFallbackBackend && R.Reason == VerifyReason::Counterexample) { + FunctionRun::Refutation Pending{Diags.size(), std::nullopt, + "the solver's model gives the " + "goal's variables no values"}; + auto Item = + llvm::find_if(Module.Obligations, [&](const Obligation &Item) { + return (Item.StableId.empty() ? Item.Id : Item.StableId) == + R.ObligationId; + }); + if (R.Assignment && Item != Module.Obligations.end()) + Pending.Lean = exportLeanRefutation( + Module, *Item, *R.Assignment, LeanModules.Specs, Pending.Why); + Run.Refutations.push_back(std::move(Pending)); + } std::string Msg = "verification failed: " + Fn->Name; if (!R.ObligationId.empty()) Msg += " [" + R.ObligationId + "]"; @@ -4111,6 +4646,7 @@ class Verifier { D.Hidden = true; Run.AllOk = false; Run.Confirmations.clear(); + Run.Refutations.clear(); VerifyResult Result; Result.Status = VerifyStatus::Unresolved; Result.Reason = VerifyReason::Interrupted; @@ -4118,6 +4654,10 @@ class Verifier { Fn.Name, std::move(Result)}); }; std::vector Confirmations; + std::vector Refutations; + // What certification found of the refutations, or why it checked none. + std::optional> RefutationChecks; + std::string RefutationsUnchecked; for (size_t I = 0; I != Runs.size(); ++I) { FunctionRun &Run = Runs[I]; if (Run.Interrupted) @@ -4148,6 +4688,10 @@ class Verifier { Pending.Index += Base; Confirmations.push_back(std::move(Pending)); } + for (FunctionRun::Refutation &Pending : Run.Refutations) { + Pending.Index += Base; + Refutations.push_back(std::move(Pending)); + } if (Run.ContractCheck) ContractChecks[Functions[I]->Identity] = Base + *Run.ContractCheck; if (DumpOS) @@ -4160,19 +4704,34 @@ class Verifier { // certified one is a proof like a solver's, and one that fails, or that // certification does not check, establishes nothing. if (HasLeanProject) { - if (LeanFile) { - LeanFile->flush(); - LeanFile.reset(); - } std::vector ExportedGoals; llvm::copy_if(LeanProjectGoals, std::back_inserter(ExportedGoals), [](const LeanProjectGoal &Goal) { return Goal.Exported; }); - llvm::Error Finalized = finalizeLeanProject( - LeanProjectGoals, LeanFallbackBackend ? &LeanRunGroups : nullptr); - if (!Finalized && Opts.LeanAuto) - automateLeanProject(LeanProjectGoals); + const std::set *RunGroups = + LeanFallbackBackend ? &LeanRunGroups : nullptr; + std::vector LeanRefutations; + for (const FunctionRun::Refutation &Pending : Refutations) + if (Pending.Lean) + LeanRefutations.push_back(*Pending.Lean); + llvm::Error Finalized = Lean->writeLeanProjectModules(LeanModules); if (!Finalized) - countLeanAutomation(LeanProjectGoals); + Finalized = Lean->finalizeLeanProject(LeanProjectGoals, RunGroups); + if (!Finalized && LeanFallbackBackend) + Finalized = Lean->writeLeanRefutations(LeanRefutations); + if (!Finalized && Opts.LeanAuto) + Lean->automateLeanProject(LeanProjectGoals); + reportLeanWarnings(); + if (!Finalized) { + // Each export's count of the goals cppverify_auto proves. + const std::set Automatic = + Lean->automaticLeanGoals(LeanProjectGoals); + for (VerifyDiagnostic &Diagnostic : Diags) + if (Diagnostic.K == VerifyDiagnostic::Exported && Diagnostic.Result) + Diagnostic.LeanAutomatic = llvm::count_if( + std::set(Diagnostic.Result->LeanGoals.begin(), + Diagnostic.Result->LeanGoals.end()), + [&](const std::string &Goal) { return Automatic.count(Goal); }); + } if (Finalized) { AllOk = false; AnyFailed = true; @@ -4180,7 +4739,7 @@ class Verifier { "cannot finalize Lean project (" + llvm::toString(std::move(Finalized)) + ")"}); } else if (Opts.LeanCertify && !interruptRequested() && - (!ExportedGoals.empty() || + (!ExportedGoals.empty() || !Refutations.empty() || Opts.LeanFallbackProjectPath.empty())) { // A verdict certification does not make a proof is reported, and // what its vacuity checks found is not. @@ -4205,15 +4764,22 @@ class Verifier { Diagnostic.Result->Message = Reason; } }; - auto Certified = certifyLeanProject(ExportedGoals); + // The goals that failed, by name, once; testing an Expected that + // holds an error again would leave it unchecked. + std::optional> Certified; + std::string NotCertified; + if (auto Checked = + Lean->certifyLeanProject(ExportedGoals, LeanRefutations)) + Certified = std::move(*Checked); + else + NotCertified = llvm::toString(Checked.takeError()); std::map GoalsByName; for (const LeanProjectGoal &Goal : ExportedGoals) GoalsByName.emplace(Goal.Name, &Goal); if (!Certified) { - const std::string Reason = llvm::toString(Certified.takeError()); for (size_t Index = 0; Index != Diags.size(); ++Index) if (Diags[Index].K == VerifyDiagnostic::Exported) - failed(Index, Reason); + failed(Index, NotCertified); } else { for (size_t Index = 0; Index != Diags.size(); ++Index) { VerifyDiagnostic &Diagnostic = Diags[Index]; @@ -4277,6 +4843,11 @@ class Verifier { Diagnostic.Message += " [vacuous]"; } } + + if (Certified) + RefutationChecks = *Certified; + else + RefutationsUnchecked = NotCertified; } } for (const SpecReliance &Check : ProofDependencies) @@ -4397,6 +4968,8 @@ class Verifier { auto Check = ContractChecks.find(Callee); if (Check != ContractChecks.end() && !settled(Check->second)) Names += " (its clauses are not shown to be defined)"; + else if (It != FnMap.end() && It->second->UnsupportedConstruct) + Names += " (it uses a construct the verifier does not support)"; else if (It != FnMap.end() && It->second->IsExternalContract && !It->second->IsTrusted) Names += " (no definition; mark it [[cppverify::trusted]] to " @@ -4431,9 +5004,12 @@ class Verifier { continue; // Definitions and frames of what the proof may reach; // postconditions and unfoldings where its module assumes them. - std::string Undefined, Unframed, Unproven, Unruled; + std::string Unsupported, Undefined, Unframed, Unproven, Unruled; for (const std::string &Identity : Reliance.Specs) { - if (UndefinedSpecs.count(Identity)) + if (auto It = FnMap.find(Identity); + It != FnMap.end() && It->second->UnsupportedConstruct) + list(Unsupported, nameOf(Identity)); + else if (UndefinedSpecs.count(Identity)) list(Undefined, nameOf(Identity)); else if (UnframedSpecs.count(Identity)) list(Unframed, nameOf(Identity)); @@ -4450,7 +5026,12 @@ class Verifier { for (const std::string &Identity : Reliance.AssumedUnfoldings) if (Unproved.count(Identity) && !Reliance.Withheld.count(Identity)) list(Unruled, nameOf(Identity)); - if (!Undefined.empty()) + if (!Unsupported.empty()) + demote(Reliance.Index, VerifyReason::UnsupportedConstruct, + "relies on " + Unsupported + + ", which uses a construct the verifier does not " + "support"); + else if (!Undefined.empty()) demote(Reliance.Index, VerifyReason::SpecTermination, "relies on the definition of " + Undefined + ", whose termination is not established"); @@ -4805,6 +5386,54 @@ class Verifier { if (!Trusted.empty()) Diagnostic.Message += " [trusts=" + Trusted + "]"; } + // A certified failure stands on the certifier's evaluation. Its + // refutation shows it in Lean too; one Lean decides the other way is a + // disagreement between two evaluators of one goal, never a failure. Only + // the failures that stand once the verdicts settle are reported. + for (const FunctionRun::Refutation &Pending : Refutations) { + if ((!RefutationChecks && RefutationsUnchecked.empty()) || + Diags[Pending.Index].K != VerifyDiagnostic::Error) + continue; + const std::string Function = Diags[Pending.Index].FunctionName; + std::string Why = Pending.Why; + if (Pending.Lean && !RefutationChecks) { + Why = RefutationsUnchecked; + } else if (Pending.Lean) { + auto Outcome = RefutationChecks->find(Pending.Lean->Goal + "_refuted"); + if (Outcome == RefutationChecks->end()) { + Diags[Pending.Index].Message += " (confirmed in Lean)"; + Diags[Pending.Index].LeanRefuted = true; + continue; + } + Why = Outcome->second; + if (llvm::StringRef(Why).contains( + "Lean decides the goal true at the counterexample")) { + VerifyDiagnostic &Failure = Diags[Pending.Index]; + const std::string Detail = + "Lean decides the goal of " + Pending.Lean->StableId + + " true at the solver's counterexample, which the certifier " + "found false"; + Failure.K = VerifyDiagnostic::Unresolved; + Failure.Message = Function + " (" + Detail + ")"; + if (Failure.Result) { + Failure.Result->Status = VerifyStatus::Unresolved; + Failure.Result->Reason = VerifyReason::InconsistentBackendResults; + Failure.Result->Message = Detail; + Failure.Message += backendSuffix(*Failure.Result); + } + continue; + } + } + // What the refutation tactic says, else the first line. + llvm::StringRef Said = Why; + if (size_t At = Said.find("cppverify: "); At != llvm::StringRef::npos) + Said = Said.substr(At + std::strlen("cppverify: ")); + Said = Said.take_until([](char C) { return C == '\n'; }); + Diags.push_back({VerifyDiagnostic::Warning, + "the counterexample of " + Function + + " is not confirmed in Lean: " + Said.str()}); + } + // Calls from code that is not verified rely on the precondition without // checking it, so the proof holds only where they establish it. for (const auto &[Index, Identity, Callees] : CallDependencies) { @@ -4954,6 +5583,8 @@ class Verifier { } if (D.K == VerifyDiagnostic::Exported && D.LeanAutomatic) Record["lean_automatic"] = static_cast(D.LeanAutomatic); + if (D.LeanRefuted) + Record["lean_refuted"] = true; if (D.Result) { const VerifyResult &Result = *D.Result; if (!Result.BackendName.empty()) @@ -5180,4 +5811,101 @@ bool verify::verifyTranslationUnit(ASTContext &Ctx, llvm::raw_ostream &OS, bool Ok = V.run(); V.printDiagnostics(OS); return Ok; +} + +LeanProjectReplay +verify::replayLeanProject(const std::vector &Modules, + const VerifyOptions &Opts) { + LeanProjectReplay Replay; + LeanProject Project(Opts); + auto report = [&] { + for (std::string &Warning : Project.takeWarnings()) + Replay.Warnings.push_back(std::move(Warning)); + }; + llvm::Error Error = Project.initializeLeanProject(); + report(); + if (Error) { + Replay.Error = "cannot initialize Lean project (" + + llvm::toString(std::move(Error)) + ")"; + return Replay; + } + + // The goals have the names, files, and proofs a source run gives the + // same obligations, since both name them by their statements. + std::vector Goals; + LeanProjectModules Generated; + LeanVerifyBackend Backend(&Generated, &Goals); + for (const ObligationModule &Module : Modules) { + VerifyResult Result = Backend.verify(Module); + if (Module.BMCTransform) + Result.Bound = Module.BMCTransform->UnrollBound; + Replay.Results.push_back(std::move(Result)); + } + Error = Project.writeLeanProjectModules(Generated); + if (!Error) + Error = Project.finalizeLeanProject(Goals, nullptr); + if (!Error && Opts.LeanAuto) + Project.automateLeanProject(Goals); + report(); + if (Error) { + Replay.Error = "cannot finalize Lean project (" + + llvm::toString(std::move(Error)) + ")"; + return Replay; + } + const std::set Automatic = Project.automaticLeanGoals(Goals); + for (const VerifyResult &Result : Replay.Results) + Replay.Automatic.push_back(llvm::count_if( + std::set(Result.LeanGoals.begin(), Result.LeanGoals.end()), + [&](const std::string &Goal) { return Automatic.count(Goal); })); + if (!Opts.LeanCertify || interruptRequested()) + return Replay; + + // A module is proved when each of its goals is certified. A bounded one + // whose unwinding goals alone are not is safe within its bound: Lean + // proves goals, and never refutes one. + // As for source certification: the Expected is tested once. + std::optional> Certified; + std::string Failed; + if (auto Checked = Project.certifyLeanProject(Goals)) + Certified = std::move(*Checked); + else + Failed = llvm::toString(Checked.takeError()); + std::map GoalsByName; + for (const LeanProjectGoal &Goal : Goals) + GoalsByName.emplace(Goal.Name, &Goal); + for (VerifyResult &Result : Replay.Results) { + if (Result.Status != VerifyStatus::Exported) + continue; + std::string Reason = Failed; + bool OnlyUnwinding = Failed.empty(); + if (Certified) + for (const std::string &Goal : std::set( + Result.LeanGoals.begin(), Result.LeanGoals.end())) + if (auto Failure = Certified->find(Goal); Failure != Certified->end()) { + const LeanProjectGoal *Known = GoalsByName.at(Goal); + OnlyUnwinding = OnlyUnwinding && Known->Unwinding; + if (Result.ObligationId.empty()) + Result.ObligationId = Known->StableId; + Reason += (Reason.empty() ? "" : "; ") + + std::string("the proof of ") + Known->StableId + ": " + + Failure->second; + } + if (Reason.empty()) { + Result.Status = VerifyStatus::Verified; + Result.Reason = VerifyReason::None; + Result.Message.clear(); + } else if (Result.Bound && OnlyUnwinding) { + Result.Status = VerifyStatus::BoundedSafe; + Result.Reason = VerifyReason::IncompleteBound; + Result.Message = "every goal is certified but the unwinding: no " + "execution within " + + std::to_string(*Result.Bound) + + " loop iterations fails (" + Reason + ")"; + } else { + Result.Status = VerifyStatus::Unresolved; + Result.Reason = VerifyReason::LeanExportFailure; + Result.Message = "Lean certification failed: " + Reason; + } + } + return Replay; } \ No newline at end of file diff --git a/clang/lib/Verify/Driver/Verifier.h b/clang/lib/Verify/Driver/Verifier.h index 887f375853..2407637712 100644 --- a/clang/lib/Verify/Driver/Verifier.h +++ b/clang/lib/Verify/Driver/Verifier.h @@ -91,6 +91,25 @@ struct VerifyOptions { bool verifyTranslationUnit(ASTContext &Ctx, llvm::raw_ostream &OS, const VerifyOptions &Opts = VerifyOptions()); + +/// What exporting the modules of an archive to a Lean project gives: a +/// result for each module, in order, with the goals cppverify_auto proves of +/// it, and what the project warns of; or why the project was not written. +struct LeanProjectReplay { + std::vector Results; + std::vector Automatic; + std::vector Warnings; + std::string Error; +}; + +/// Exports Modules to the Lean project of Opts as a source run exports its +/// functions, with their proofs and cppverify_auto, and with LeanCertify +/// certifies them. A module alone is all that is checked: one whose goals +/// all certify is Verified, its obligations proved; a bounded one is +/// BoundedSafe when only unwinding goals do not. +LeanProjectReplay +replayLeanProject(const std::vector &Modules, + const VerifyOptions &Opts); } // namespace verify } // namespace clang diff --git a/clang/lib/Verify/Frontend/ASTConverter.cpp b/clang/lib/Verify/Frontend/ASTConverter.cpp index 6b2d271bd9..de8609aa69 100644 --- a/clang/lib/Verify/Frontend/ASTConverter.cpp +++ b/clang/lib/Verify/Frontend/ASTConverter.cpp @@ -845,9 +845,15 @@ std::string ASTConverter::functionIdentity(const FunctionDecl *FD) { std::string Mangled; llvm::raw_string_ostream OS(Mangled); std::unique_ptr MC(Ctx.createMangleContext()); - if (MC->shouldMangleDeclName(FD)) - MC->mangleName(GlobalDecl(FD), OS); - else + // A constructor or destructor is mangled as its complete-object variant. + if (MC->shouldMangleDeclName(FD)) { + if (const auto *Ctor = dyn_cast(FD)) + MC->mangleName(GlobalDecl(Ctor, Ctor_Complete), OS); + else if (const auto *Dtor = dyn_cast(FD)) + MC->mangleName(GlobalDecl(Dtor, Dtor_Complete), OS); + else + MC->mangleName(GlobalDecl(FD), OS); + } else OS << FD->getQualifiedNameAsString() << '\0' << FD->getType().getCanonicalType().getAsString(); OS.flush(); @@ -1903,6 +1909,28 @@ static void collectFunctionCandidates(const DeclContext *DC, ASTContext &Ctx, } } +/// What remains of a function that uses a construct the verifier does not +/// represent: an executable or proof function becomes the contract its +/// callers assume, not verified; a spec, a declaration without the +/// definition, postconditions, and reads that its body would establish. +static void markUnsupported(VFunction &Fn) { + Fn.UnsupportedConstruct = true; + Fn.Body.clear(); + Fn.ClauseProofs.clear(); + if (!Fn.IsSpec) { + Fn.IsExternalContract = true; + return; + } + Fn.Uninterpreted = true; + Fn.Postconditions.clear(); + Fn.Decreases.clear(); + Fn.Recommends.clear(); + Fn.Reads.clear(); + Fn.Domain.reset(); + Fn.Unfolding.reset(); + Fn.InductiveLoc = SourceLocation(); +} + std::vector> ASTConverter::convertTranslationUnit() { std::vector> Out; llvm::StringSet<> Identities; @@ -1910,19 +1938,22 @@ std::vector> ASTConverter::convertTranslationUnit() { collectFunctionCandidates(Ctx.getTranslationUnitDecl(), Ctx, Definitions); UsedConstexprSpecs.clear(); for (const FunctionDecl *FD : Definitions) { - if (isa(FD)) { - if (functionContract(FD) || hasContractStatements(FD, Ctx)) + if (isa(FD) || FD->isTemplated()) { + // Only one with contracts is reported here: one without matters where + // verified code calls it, which reports the call. + if (!functionContract(FD) && !hasContractStatements(FD, Ctx)) + continue; + llvm::SaveAndRestore Owner(ErrorFunction, functionIdentity(FD)); + llvm::SaveAndRestore OwnerName(ErrorFunctionName, FD->getNameAsString()); + llvm::SaveAndRestore InContract(ErrorInContract, true); + if (isa(FD)) { error(FD->getLocation(), FD->getNameAsString() + ": member function verification is unsupported"); - continue; - } - // Only a template with contracts is reported here: one without matters - // where verified code calls it, which reports the call. - if (FD->isTemplated()) { + continue; + } if (!FD->isInStdNamespace() && - Ctx.getSourceManager().isWrittenInMainFile(FD->getLocation()) && - (functionContract(FD) || hasContractStatements(FD, Ctx))) + Ctx.getSourceManager().isWrittenInMainFile(FD->getLocation())) error(FD->getLocation(), FD->getNameAsString() + ": function template verification is unsupported"); @@ -1930,12 +1961,24 @@ std::vector> ASTConverter::convertTranslationUnit() { } if (FD->isInStdNamespace()) continue; + const size_t ErrorsBefore = Errors.size(); auto Fn = convertFunction(FD); - if (Fn) { + if (Fn && Errors.size() == ErrorsBefore) { Identities.insert(Fn->Identity); Out.push_back(std::move(Fn)); continue; } + // A construct the verifier does not represent costs its function alone. + // What remains is what others may rely on: the contract, when it + // converted, of an executable or proof function; a spec's declaration. + if (Fn && + (Fn->IsSpec || + std::none_of(Errors.begin() + ErrorsBefore, Errors.end(), + [](const ConversionError &E) { return E.InContract; }))) { + markUnsupported(*Fn); + Identities.insert(Fn->Identity); + Out.push_back(std::move(Fn)); + } // A body the verifier does not verify still calls contracted functions, // whose preconditions it then assumes without checking (Frama-C reports // such properties as valid under hypotheses). @@ -1979,7 +2022,10 @@ std::vector> ASTConverter::convertTranslationUnit() { Changed = true; if (Identities.contains(functionIdentity(FD))) continue; + const size_t ErrorsBefore = Errors.size(); if (auto Fn = convertConstexprSpec(FD)) { + if (Errors.size() != ErrorsBefore) + markUnsupported(*Fn); Identities.insert(Fn->Identity); Lifted.push_back({I, std::move(Fn)}); } @@ -2039,9 +2085,10 @@ std::vector> ASTConverter::convertTranslationUnit() { } for (auto &Fn : Unspecified) Out.push_back(std::move(Fn)); - expandInductivePredicates(Out, [&](SourceLocation Loc, std::string Message) { - error(Loc, std::move(Message)); - }); + expandInductivePredicates( + Out, [&](const VFunction &Fn, SourceLocation Loc, std::string Message) { + errorOf(Fn, Loc, std::move(Message)); + }); for (auto &Fn : ChoiceFunctions) Out.push_back(std::move(Fn)); ChoiceFunctions.clear(); @@ -2132,14 +2179,14 @@ std::vector> ASTConverter::convertTranslationUnit() { llvm::any_of(Reach[Fn->Identity], [&](const std::string &Id) { return ChoiceIdentities.count(Id) && Reach[Id].count(Fn->Identity); }); - error(Fn->DeclLoc, - Fn->Name + - (InChoice - ? ": an inductive predicate occurs in the bodies of its " - "group only positively, never inside cppverify::choose" - : ": an inductive predicate applies itself only in its " - "body or through inductive predicates, not through " - "another spec")); + errorOf(*Fn, Fn->DeclLoc, + Fn->Name + + (InChoice + ? ": an inductive predicate occurs in the bodies of its " + "group only positively, never inside cppverify::choose" + : ": an inductive predicate applies itself only in its " + "body or through inductive predicates, not through " + "another spec")); for (const std::string &Other : Reach[Fn->Identity]) if (Reach[Other].count(Fn->Identity)) InductiveCycles.insert(Other); @@ -2200,9 +2247,9 @@ std::vector> ASTConverter::convertTranslationUnit() { return kindOf(*ByIdentity[Id]) == kindOf(*Fn); }); if (!SameKind) { - error(Fn->DeclLoc, - Fn->Name + ": mutual recursion is supported only among " - "functions of one kind: spec, proof, or executable"); + errorOf(*Fn, Fn->DeclLoc, + Fn->Name + ": mutual recursion is supported only among " + "functions of one kind: spec, proof, or executable"); continue; } const bool AllDiverge = @@ -2222,16 +2269,16 @@ std::vector> ASTConverter::convertTranslationUnit() { !ByIdentity[Id]->DivergenceDeclared; }); if (!SameMeasure) { - error(Fn->DeclLoc, - Fn->Name + ": mutually recursive " + - (Fn->IsSpec ? "spec" - : Fn->IsProof ? "proof" - : "executable") + - " functions require decreases clauses, all of the " - "same length" + - (Fn->IsSpec || Fn->IsProof - ? "" - : ", or all cppverify::decreases(*)")); + errorOf(*Fn, Fn->DeclLoc, + Fn->Name + ": mutually recursive " + + (Fn->IsSpec ? "spec" + : Fn->IsProof ? "proof" + : "executable") + + " functions require decreases clauses, all of the " + "same length" + + (Fn->IsSpec || Fn->IsProof + ? "" + : ", or all cppverify::decreases(*)")); continue; } Fn->RecursionGroup = std::move(Group); @@ -2242,12 +2289,13 @@ std::vector> ASTConverter::convertTranslationUnit() { if (Recursive && Fn->Decreases.empty() && Fn->DivergenceDeclared) continue; if (Recursive && Fn->Decreases.empty()) { - error(Fn->DeclLoc, - Fn->Name + - (Fn->IsSpec || Fn->IsProof - ? ": recursive spec and proof functions require decreases" - : ": recursive executable functions require decreases, or " - "cppverify::decreases(*) to allow divergence")); + errorOf( + *Fn, Fn->DeclLoc, + Fn->Name + + (Fn->IsSpec || Fn->IsProof + ? ": recursive spec and proof functions require decreases" + : ": recursive executable functions require decreases, or " + "cppverify::decreases(*) to allow divergence")); continue; } Fn->NeedsDecreasesCheck = Recursive; @@ -2307,6 +2355,15 @@ std::vector> ASTConverter::convertTranslationUnit() { Fn->SpecFuel.erase(Identity); } } + // A function an error after conversion is about (a recursion without a + // measure, a malformed inductive predicate) costs its function alone, as + // an error in its body does. + std::set Faulty; + for (const ConversionError &Err : Errors) + Faulty.insert(Err.Function); + for (auto &Fn : Out) + if (Faulty.count(Fn->Identity) && !Fn->UnsupportedConstruct) + markUnsupported(*Fn); return Out; } @@ -2554,6 +2611,10 @@ bool ASTConverter::exceedsNestingLimit(const FunctionDecl *FD, std::unique_ptr ASTConverter::convertFunction(const FunctionDecl *FD) { llvm::SaveAndRestore At(CurrentRange, SourceRange(FD->getLocation())); + llvm::SaveAndRestore Owner(ErrorFunction, functionIdentity(FD)); + llvm::SaveAndRestore OwnerName(ErrorFunctionName, FD->getNameAsString()); + llvm::SaveAndRestore InContract(ErrorInContract, true); + const size_t ErrorsAtStart = Errors.size(); const FunctionContractInfo *FCI = functionContract(FD); if (!FCI) { // Assertions and ghost code are obligations even without a contract @@ -2948,6 +3009,7 @@ ASTConverter::convertFunction(const FunctionDecl *FD) { Fn->Body.push_back(std::make_unique( std::move(Condition), Check->getExprLoc())); } + ErrorInContract = false; for (auto &S : convertStmt(Body)) Fn->Body.push_back(std::move(S)); TrackInitialization = false; @@ -2983,11 +3045,13 @@ ASTConverter::convertFunction(const FunctionDecl *FD) { CurrentFn = nullptr; return nullptr; } + // A spec whose body the verifier does not represent remains a + // declaration (markUnsupported), which its users apply. if (Fn->IsSpec && !specBodyCanBeAxiomatized(Fn->Body)) { error(Fn->Name + ": spec function body is unsupported by axiomatic " "lowering"); CurrentFn = nullptr; - return nullptr; + return Fn; } if (!Fn->IsSpec && Fn->Body.empty() && Fn->Postconditions.empty() && !functionContract(FD)) { @@ -3010,7 +3074,8 @@ ASTConverter::convertFunction(const FunctionDecl *FD) { InPost = SavedPost; Fn->ClauseProofs.push_back(std::move(Converted)); } - if (Fn->IsSpec && Fn->Body.empty() && Fn->Decreases.empty()) { + if (Fn->IsSpec && Fn->Body.empty() && Fn->Decreases.empty() && + Errors.size() == ErrorsAtStart) { CurrentFn = nullptr; return nullptr; } @@ -3157,6 +3222,10 @@ bool ASTConverter::checkLoopTermination(const Stmt *Loop, bool HasMeasure) { std::unique_ptr ASTConverter::convertConstexprSpec(const FunctionDecl *FD) { + llvm::SaveAndRestore Owner(ErrorFunction, functionIdentity(FD)); + llvm::SaveAndRestore OwnerName(ErrorFunctionName, FD->getNameAsString()); + llvm::SaveAndRestore InContract(ErrorInContract, true); + const size_t ErrorsAtStart = Errors.size(); ParameterNames.clear(); if (exceedsNestingLimit(FD, functionContract(FD)) || hasIndirectMemoryAccess(FD)) @@ -3203,10 +3272,11 @@ ASTConverter::convertConstexprSpec(const FunctionDecl *FD) { if (const Stmt *Body = FD->getBody()) { beginInitializationTracking(FD); + ErrorInContract = false; Fn->Body = convertStmt(Body); TrackInitialization = false; } - if (Fn->Body.empty()) { + if (Fn->Body.empty() && Errors.size() == ErrorsAtStart) { CurrentFn = nullptr; return nullptr; } diff --git a/clang/lib/Verify/Frontend/ASTConverter.h b/clang/lib/Verify/Frontend/ASTConverter.h index abf66a2105..152d0d48c4 100644 --- a/clang/lib/Verify/Frontend/ASTConverter.h +++ b/clang/lib/Verify/Frontend/ASTConverter.h @@ -16,6 +16,13 @@ namespace verify { struct ConversionError { SourceRange Range; std::string Message; + /// The function it is written in (its identity and name), whose verdict it + /// decides; empty for an error of the translation unit. + std::string Function; + std::string FunctionName; + /// In the function's signature or contract, rather than its body: then + /// its callers cannot use the contract either. + bool InContract = false; }; class ASTConverter { @@ -37,9 +44,20 @@ class ASTConverter { /// The innermost declaration, statement, or expression being converted, /// where an error is reported unless it names a place of its own. SourceRange CurrentRange; + /// The function whose conversion an error belongs to, and whether the + /// converter is in its signature and contract or in its body. + std::string ErrorFunction; + std::string ErrorFunctionName; + bool ErrorInContract = false; void error(std::string Message) { error(CurrentRange, std::move(Message)); } void error(SourceRange Range, std::string Message) { - Errors.push_back({Range, std::move(Message)}); + Errors.push_back({Range, std::move(Message), ErrorFunction, + ErrorFunctionName, ErrorInContract}); + } + /// An error about \p Fn found once every function is converted. + void errorOf(const VFunction &Fn, SourceLocation Loc, std::string Message) { + Errors.push_back( + {SourceRange(Loc), std::move(Message), Fn.Identity, Fn.Name, false}); } std::vector> Warnings; /// Maps unqualified field name -> "param." for type_invariant lowering. diff --git a/clang/lib/Verify/IR/VStmt.cpp b/clang/lib/Verify/IR/VStmt.cpp index 2d7a0a134d..687197e9ff 100644 --- a/clang/lib/Verify/IR/VStmt.cpp +++ b/clang/lib/Verify/IR/VStmt.cpp @@ -188,6 +188,7 @@ VFunction verify::cloneVFunction(const VFunction &Fn) { {cloneVExpr(R.Base.get()), cloneVExpr(R.Count.get()), R.ElementSize}); Out.Domain = cloneVExpr(Fn.Domain.get()); Out.Uninterpreted = Fn.Uninterpreted; + Out.UnsupportedConstruct = Fn.UnsupportedConstruct; Out.IsChoice = Fn.IsChoice; Out.UsesDynamicStorage = Fn.UsesDynamicStorage; Out.FreshOwnedReturn = Fn.FreshOwnedReturn; diff --git a/clang/lib/Verify/IR/VStmt.h b/clang/lib/Verify/IR/VStmt.h index b5c84ce10d..6db19ef883 100644 --- a/clang/lib/Verify/IR/VStmt.h +++ b/clang/lib/Verify/IR/VStmt.h @@ -374,6 +374,10 @@ struct VFunction { std::unique_ptr Domain; /// A spec without a definition: the value of another outside its domain. bool Uninterpreted = false; + /// Its body or contract uses a construct the verifier does not represent, + /// so the function is not verified: an executable or proof function keeps + /// its contract for its callers, a spec only its declaration. + bool UnsupportedConstruct = false; /// A choose(...) lifted to a function of what its body mentions: its post /// is the defining axiom (a witness when one exists), assumed, not proved. bool IsChoice = false; diff --git a/clang/lib/Verify/Transform/Inductive.cpp b/clang/lib/Verify/Transform/Inductive.cpp index 4e686f60e9..8c9ca9b15f 100644 --- a/clang/lib/Verify/Transform/Inductive.cpp +++ b/clang/lib/Verify/Transform/Inductive.cpp @@ -1055,7 +1055,8 @@ class GroupExpander { void verify::expandInductivePredicates( std::vector> &Functions, - llvm::function_ref Report) { + llvm::function_ref + Report) { std::map Inductive; for (auto &Fn : Functions) if (Fn->InductiveLoc.isValid()) @@ -1067,7 +1068,7 @@ void verify::expandInductivePredicates( std::map Unfoldings; for (auto &[Identity, Fn] : Inductive) { auto reject = [&, &Identity = Identity, Fn = Fn](const std::string &Why) { - Report(Fn->InductiveLoc, Fn->Name + ": " + Why); + Report(*Fn, Fn->InductiveLoc, Fn->Name + ": " + Why); Rejected.insert(Identity); }; if (!Fn->IsSpec || Fn->ReturnType.Kind != VTypeKind::Bool) { @@ -1142,7 +1143,7 @@ void verify::expandInductivePredicates( nonPositiveOccurrence(Unfoldings.at(Member).get(), Group); !Where.empty()) { Report( - Fn.InductiveLoc, + Fn, Fn.InductiveLoc, Fn.Name + ": " + (Group.size() == 1 ? Fn.Name : std::string("a predicate it is " diff --git a/clang/lib/Verify/Transform/Inductive.h b/clang/lib/Verify/Transform/Inductive.h index a128132d4e..c68e0d22bf 100644 --- a/clang/lib/Verify/Transform/Inductive.h +++ b/clang/lib/Verify/Transform/Inductive.h @@ -21,10 +21,11 @@ namespace verify { /// Defines every predicate marked inductive in \p Functions as the least /// fixpoint of its body, and appends the specs and proof functions that /// establish its rules. A malformed predicate is reported through \p Report, -/// at its inductive marker, and left as it is. +/// with the predicate and at its inductive marker, and left as it is. void expandInductivePredicates( std::vector> &Functions, - llvm::function_ref Report); + llvm::function_ref + Report); } // namespace verify } // namespace clang diff --git a/clang/lib/Verify/Transform/LoopUnroll.cpp b/clang/lib/Verify/Transform/LoopUnroll.cpp index e6fe44f6a2..1f6bd7d3a3 100644 --- a/clang/lib/Verify/Transform/LoopUnroll.cpp +++ b/clang/lib/Verify/Transform/LoopUnroll.cpp @@ -231,6 +231,7 @@ VFunction LoopUnroller::unroll(const VFunction &Fn, unsigned K) { {cloneVExpr(R.Base.get()), cloneVExpr(R.Count.get()), R.ElementSize}); Out.Domain = cloneVExpr(Fn.Domain.get()); Out.Uninterpreted = Fn.Uninterpreted; + Out.UnsupportedConstruct = Fn.UnsupportedConstruct; Out.IsChoice = Fn.IsChoice; Out.IsExternalContract = Fn.IsExternalContract; Out.IsTrusted = Fn.IsTrusted; diff --git a/clang/test/Verify/located_errors.cpp b/clang/test/Verify/located_errors.cpp index 9f8a43b940..0db71d256a 100644 --- a/clang/test/Verify/located_errors.cpp +++ b/clang/test/Verify/located_errors.cpp @@ -4,7 +4,7 @@ // // An error about source the verifier cannot represent points at that source, // with its line and a caret, as a compiler error does, and its JSON record -// carries the range. +// carries the range and the verdict of the function it is in. namespace cv = cppverify; @@ -16,7 +16,7 @@ int pick(int x) // CHECK: {{.*}}located_errors.cpp:[[@LINE-2]]:3: error: pick: unsupported statement: SwitchStmt // CHECK-NEXT: {{^ +}}[[@LINE-3]] | switch (x) { case 1: return 1; default: return 0; } // CHECK-NEXT: {{^ +}}| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// JSON: {"message":"pick: unsupported statement: SwitchStmt","schema":"cppverify.diagnostic/1","severity":"error","source":{"column":3,"end_column":53,"end_line":[[#@LINE-5]],"file":"{{.*}}located_errors.cpp","line":[[#@LINE-5]]},"status":"error"} +// JSON: {"function":"pick","message":"pick: unsupported statement: SwitchStmt","reason":"construct.unsupported","schema":"cppverify.diagnostic/1","severity":"error","source":{"column":3,"end_column":53,"end_line":[[#@LINE-5]],"file":"{{.*}}located_errors.cpp","line":[[#@LINE-5]]},"status":"unresolved"} struct Counter { int get(int x) { cv::check(x >= 0 || x < 0); return x; } @@ -50,3 +50,4 @@ int down(int n) // CHECK: {{.*}}located_errors.cpp:[[@LINE-8]]:5: error: down: recursive executable functions require decreases, or cppverify::decreases(*) to allow divergence // CHECK-NEXT: {{^ +}}[[@LINE-9]] | int down(int n) // CHECK-NEXT: {{^ +}}| ^~~~ +// JSON: {"function":"down","message":"down: recursive executable functions require decreases, or cppverify::decreases(*) to allow divergence","reason":"construct.unsupported",{{.*}}"status":"unresolved"} diff --git a/clang/test/Verify/microsoft_identity.cpp b/clang/test/Verify/microsoft_identity.cpp new file mode 100644 index 0000000000..feb8c18b5b --- /dev/null +++ b/clang/test/Verify/microsoft_identity.cpp @@ -0,0 +1,29 @@ +// RUN: %cpp-verify --jobs=1 %s -- --target=x86_64-pc-windows-msvc 2>&1 \ +// RUN: | FileCheck %s +// +// Only the functions the verifier converts or reports are named. A template +// or member function without contracts is neither, so it is never mangled; +// under the Microsoft ABI, whose mangler cannot mangle a dependent type, the +// templates of the standard headers made every run fail. + +template int length(const char (&s)[N]) { return N; } + +template struct Box { + T get() const { return v; } + T v; +}; + +struct Plain { + int get() const { return 0; } +}; + +int abs_value(int x) + cppverify::pre(x > -2147483647 - 1) + cppverify::post(cppverify::result >= 0) +{ + return x < 0 ? -x : x; +} + +// CHECK-NOT: error +// CHECK: Verified: abs_value [backend=z3] +// CHECK-NOT: error diff --git a/clang/test/Verify/suite/Inputs/backend_fidelity_proofs/cppverify_heap_spec_read_2c60ae_postcondition_af8523f4.lean b/clang/test/Verify/suite/Inputs/backend_fidelity_proofs/cppverify_heap_spec_read_2c60ae_postcondition_af8523f4.lean new file mode 100644 index 0000000000..9ee9a7538e --- /dev/null +++ b/clang/test/Verify/suite/Inputs/backend_fidelity_proofs/cppverify_heap_spec_read_2c60ae_postcondition_af8523f4.lean @@ -0,0 +1,12 @@ +import CppVerify.Goals.cppverify_heap_spec_read_2c60ae +import CppVerify.User +import CppVerify.Auto + +theorem cppverify_heap_spec_read_2c60ae_postcondition_af8523f4_proof : cppverify_heap_spec_read_2c60ae_postcondition_af8523f4 := by + apply cppverify_heap_spec_read_2c60ae_postcondition_af8523f4.of_readable + intro __heap_0 __heap_1 __heap_alloc_0 __heap_live_0 pointer_0 cppverify_spec_axiom_1_1 cppverify_spec_axiom_1_2 cppverify_spec_axiom_1_3 cppverify_spec_axiom_2_1 cppverify_spec_axiom_2_2 cppverify_spec_axiom_2_3 h_pre_1 h_pre_2 h_entry_1 h_null_1 h_null_2 h_object_1 h_store_1 + rw [cppverify_spec_axiom_1_2, cppverify_spec_axiom_2_2] + cppverify_unfold_bodies + cppverify_heap + simp + grind diff --git a/clang/test/Verify/suite/Inputs/lean_certifier_first.cpp b/clang/test/Verify/suite/Inputs/lean_certifier_first.cpp new file mode 100644 index 0000000000..29404fc469 --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_certifier_first.cpp @@ -0,0 +1,11 @@ +int early() + cppverify::post(cppverify::result == 2) +{ + return 1; +} + +int fine() + cppverify::post(cppverify::result == 1) +{ + return 1; +} diff --git a/clang/test/Verify/suite/Inputs/lean_certifier_proofs/cppverify_fine_52006a_missing_return_54ae7c80.lean b/clang/test/Verify/suite/Inputs/lean_certifier_proofs/cppverify_fine_52006a_missing_return_54ae7c80.lean index 24d917ca90..53b4aee615 100644 --- a/clang/test/Verify/suite/Inputs/lean_certifier_proofs/cppverify_fine_52006a_missing_return_54ae7c80.lean +++ b/clang/test/Verify/suite/Inputs/lean_certifier_proofs/cppverify_fine_52006a_missing_return_54ae7c80.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_fine_52006a import CppVerify.User theorem cppverify_fine_52006a_missing_return_54ae7c80_proof : cppverify_fine_52006a_missing_return_54ae7c80 := by diff --git a/clang/test/Verify/suite/Inputs/lean_certifier_proofs/cppverify_fine_52006a_postcondition_cd6bdfcd.lean b/clang/test/Verify/suite/Inputs/lean_certifier_proofs/cppverify_fine_52006a_postcondition_cd6bdfcd.lean index 2d440f8a0e..415249b275 100644 --- a/clang/test/Verify/suite/Inputs/lean_certifier_proofs/cppverify_fine_52006a_postcondition_cd6bdfcd.lean +++ b/clang/test/Verify/suite/Inputs/lean_certifier_proofs/cppverify_fine_52006a_postcondition_cd6bdfcd.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_fine_52006a import CppVerify.User theorem cppverify_fine_52006a_postcondition_cd6bdfcd_proof : cppverify_fine_52006a_postcondition_cd6bdfcd := by diff --git a/clang/test/Verify/suite/Inputs/lean_certifier_proofs/cppverify_one_99bcee_missing_return_54ae7c80.lean b/clang/test/Verify/suite/Inputs/lean_certifier_proofs/cppverify_one_99bcee_missing_return_54ae7c80.lean index f7d3d1a1a7..c6061d122c 100644 --- a/clang/test/Verify/suite/Inputs/lean_certifier_proofs/cppverify_one_99bcee_missing_return_54ae7c80.lean +++ b/clang/test/Verify/suite/Inputs/lean_certifier_proofs/cppverify_one_99bcee_missing_return_54ae7c80.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_one_99bcee import CppVerify.User theorem cppverify_one_99bcee_missing_return_54ae7c80_proof : cppverify_one_99bcee_missing_return_54ae7c80 := by diff --git a/clang/test/Verify/suite/Inputs/lean_certifier_proofs/first/cppverify_early_103f65_postcondition_646d2761.lean b/clang/test/Verify/suite/Inputs/lean_certifier_proofs/first/cppverify_early_103f65_postcondition_646d2761.lean new file mode 100644 index 0000000000..cc38fc4238 --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_certifier_proofs/first/cppverify_early_103f65_postcondition_646d2761.lean @@ -0,0 +1,5 @@ +/- A goal of the same name of its own, true, in the proof of the goal that + comes first, which imports nothing of the project. -/ +def cppverify_early_103f65_postcondition_646d2761 : Prop := True + +theorem cppverify_early_103f65_postcondition_646d2761_proof : cppverify_early_103f65_postcondition_646d2761 := trivial diff --git a/clang/test/Verify/suite/Inputs/lean_certifier_proofs/hidden/cppverify_one_99bcee_postcondition_646d2761.lean b/clang/test/Verify/suite/Inputs/lean_certifier_proofs/hidden/cppverify_one_99bcee_postcondition_646d2761.lean index 823511abad..3ed1c6e593 100644 --- a/clang/test/Verify/suite/Inputs/lean_certifier_proofs/hidden/cppverify_one_99bcee_postcondition_646d2761.lean +++ b/clang/test/Verify/suite/Inputs/lean_certifier_proofs/hidden/cppverify_one_99bcee_postcondition_646d2761.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_one_99bcee import CppVerify.User /- An axiom behind a lemma. -/ diff --git a/clang/test/Verify/suite/Inputs/lean_certifier_proofs/imported/Lemmas.lean b/clang/test/Verify/suite/Inputs/lean_certifier_proofs/imported/Lemmas.lean index 0897c5c20f..71e1a1f4ad 100644 --- a/clang/test/Verify/suite/Inputs/lean_certifier_proofs/imported/Lemmas.lean +++ b/clang/test/Verify/suite/Inputs/lean_certifier_proofs/imported/Lemmas.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_one_99bcee import CppVerify.User import Lean diff --git a/clang/test/Verify/suite/Inputs/lean_certifier_proofs/rewritten/cppverify_one_99bcee_postcondition_646d2761.lean b/clang/test/Verify/suite/Inputs/lean_certifier_proofs/rewritten/cppverify_one_99bcee_postcondition_646d2761.lean index f5ce026671..ed060924ed 100644 --- a/clang/test/Verify/suite/Inputs/lean_certifier_proofs/rewritten/cppverify_one_99bcee_postcondition_646d2761.lean +++ b/clang/test/Verify/suite/Inputs/lean_certifier_proofs/rewritten/cppverify_one_99bcee_postcondition_646d2761.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_one_99bcee import CppVerify.User theorem cppverify_one_99bcee_postcondition_646d2761_proof : True := trivial diff --git a/clang/test/Verify/suite/Inputs/lean_certifier_proofs/unchecked/cppverify_one_99bcee_postcondition_646d2761.lean b/clang/test/Verify/suite/Inputs/lean_certifier_proofs/unchecked/cppverify_one_99bcee_postcondition_646d2761.lean index 296d7e4797..0c477a9fa8 100644 --- a/clang/test/Verify/suite/Inputs/lean_certifier_proofs/unchecked/cppverify_one_99bcee_postcondition_646d2761.lean +++ b/clang/test/Verify/suite/Inputs/lean_certifier_proofs/unchecked/cppverify_one_99bcee_postcondition_646d2761.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_one_99bcee import CppVerify.User import Lean diff --git a/clang/test/Verify/suite/Inputs/lean_collections.cpp b/clang/test/Verify/suite/Inputs/lean_collections.cpp new file mode 100644 index 0000000000..333d61d940 --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_collections.cpp @@ -0,0 +1,60 @@ +// Sequences, sets, multisets, and maps in a Lean project, for +// lean_collections.test. +using cppverify::map; +using cppverify::multiset; +using cppverify::seq; +using cppverify::set; + +cppverify::spec int total(seq s) + cppverify::decreases(s.len()) +{ + return s.len() <= 0 ? 0 : total(s.subrange(0, s.len() - 1)) + s[s.len() - 1]; +} + +cppverify::proof void sequences(seq s, int x) +{ + cppverify::check(s.push(x).len() == s.len() + 1); + cppverify::check(s.push(x)[s.len()] == x); + cppverify::check((s + cppverify::seq_of(x)).contains(x)); + cppverify::check(s.subrange(0, s.len()) == s); +} + +cppverify::proof void sets(set a, int x, int y) + cppverify::pre(x != y) +{ + cppverify::check(a.insert(x).remove(y).contains(x)); + cppverify::check(a.subset_of(a.unite(cppverify::set_empty().insert(y)))); +} + +cppverify::proof void multisets(multiset m, int x) +{ + cppverify::check(m.insert(x).count(x) == m.count(x) + 1); + cppverify::check(m.insert(x).remove(x) == m); +} + +cppverify::proof void maps(map m, int k, int v) +{ + cppverify::check(m.insert(k, v)[k] == v); + cppverify::check(!m.remove(k).contains(k)); +} + +cppverify::proof void total_push(seq s, int x) + cppverify::post(total(s.push(x)) == total(s) + x) +{ +} + +cppverify::proof void wrong(set a, int x) +{ + cppverify::check(a.remove(x).contains(x) == a.contains(x)); +} + +cppverify::spec int members(set a, int n) + cppverify::decreases(n) +{ + return n <= 0 ? 0 : members(a, n - 1) + (a.contains(n) ? 1 : 0); +} + +cppverify::proof void no_members(set a) + cppverify::post(members(a, 0) == 0) +{ +} diff --git a/clang/test/Verify/suite/Inputs/lean_collections_total_push.lean b/clang/test/Verify/suite/Inputs/lean_collections_total_push.lean new file mode 100644 index 0000000000..ee5b58fee6 --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_collections_total_push.lean @@ -0,0 +1,13 @@ +import CppVerify.Goals.cppverify_total_push_cdfd2d +import CppVerify.User +import CppVerify.Auto + +theorem cppverify_total_push_cdfd2d_postcondition_3a9fac6f_proof : + cppverify_total_push_cdfd2d_postcondition_3a9fac6f := by + apply cppverify_total_push_cdfd2d_postcondition_3a9fac6f.of_readable + intro _ _ s x pushed _ _ _ + rw [pushed] + unfold cppSpecBody_cdfd2da1_fn_5f5a35746f74616c4e396370707665726966793373657145_1 + have := cppSeqLength_nonneg s + simp [BitVec.toInt_signExtend_of_le] + omega diff --git a/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_division_by_zero_df06c970.lean b/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_division_by_zero_df06c970.lean index daf2ade742..6002daa66b 100644 --- a/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_division_by_zero_df06c970.lean +++ b/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_division_by_zero_df06c970.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_reading_8d7035 import CppVerify.User theorem cppverify_reading_8d7035_division_by_zero_df06c970_proof : cppverify_reading_8d7035_division_by_zero_df06c970 := by diff --git a/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_missing_return_54ae7c80.lean b/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_missing_return_54ae7c80.lean index 7bb7dd4b6b..8a10f2476c 100644 --- a/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_missing_return_54ae7c80.lean +++ b/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_missing_return_54ae7c80.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_reading_8d7035 import CppVerify.User theorem cppverify_reading_8d7035_missing_return_54ae7c80_proof : cppverify_reading_8d7035_missing_return_54ae7c80 := by diff --git a/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_postcondition_54ae7c80.lean b/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_postcondition_54ae7c80.lean index 97d154cc3c..b99f29c9e1 100644 --- a/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_postcondition_54ae7c80.lean +++ b/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_postcondition_54ae7c80.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_reading_8d7035 import CppVerify.User theorem cppverify_reading_8d7035_postcondition_54ae7c80_proof : cppverify_reading_8d7035_postcondition_54ae7c80 := by diff --git a/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_precondition_155ffe95.lean b/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_precondition_155ffe95.lean index d8692c5599..df12dae878 100644 --- a/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_precondition_155ffe95.lean +++ b/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_precondition_155ffe95.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_reading_8d7035 import CppVerify.User theorem cppverify_reading_8d7035_precondition_155ffe95_proof : cppverify_reading_8d7035_precondition_155ffe95 := by diff --git a/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_precondition_814bc5b2.lean b/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_precondition_814bc5b2.lean index 5779536cb6..589c770d90 100644 --- a/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_precondition_814bc5b2.lean +++ b/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_reading_8d7035_precondition_814bc5b2.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_reading_8d7035 import CppVerify.User theorem cppverify_reading_8d7035_precondition_814bc5b2_proof : cppverify_reading_8d7035_precondition_814bc5b2 := by diff --git a/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_sensor_contract_eb6f5c_division_by_zero_d81a153d.lean b/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_sensor_contract_eb6f5c_division_by_zero_d81a153d.lean index 8e3effb2e7..8d4ca35013 100644 --- a/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_sensor_contract_eb6f5c_division_by_zero_d81a153d.lean +++ b/clang/test/Verify/suite/Inputs/lean_contract_certify_proofs/cppverify_sensor_contract_eb6f5c_division_by_zero_d81a153d.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_sensor_contract_eb6f5c import CppVerify.User theorem cppverify_sensor_contract_eb6f5c_division_by_zero_d81a153d_proof : cppverify_sensor_contract_eb6f5c_division_by_zero_d81a153d := by diff --git a/clang/test/Verify/suite/Inputs/lean_dependencies_impossible.lean b/clang/test/Verify/suite/Inputs/lean_dependencies_impossible.lean index 391927a090..c2cb31fbe6 100644 --- a/clang/test/Verify/suite/Inputs/lean_dependencies_impossible.lean +++ b/clang/test/Verify/suite/Inputs/lean_dependencies_impossible.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_impossible_9d6c7c import CppVerify.User theorem cppverify_impossible_9d6c7c_postcondition_1cd35301_proof : cppverify_impossible_9d6c7c_postcondition_1cd35301 := by diff --git a/clang/test/Verify/suite/Inputs/lean_dependencies_looping.lean b/clang/test/Verify/suite/Inputs/lean_dependencies_looping.lean index b07ce3c20e..4a74afae54 100644 --- a/clang/test/Verify/suite/Inputs/lean_dependencies_looping.lean +++ b/clang/test/Verify/suite/Inputs/lean_dependencies_looping.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_looping_3f4bd4 import CppVerify.User theorem cppverify_looping_3f4bd4_postcondition_d06fccd0_proof : cppverify_looping_3f4bd4_postcondition_d06fccd0 := by diff --git a/clang/test/Verify/suite/Inputs/lean_dependencies_mixed.lean b/clang/test/Verify/suite/Inputs/lean_dependencies_mixed.lean index 9df085a719..b6bbaa9d98 100644 --- a/clang/test/Verify/suite/Inputs/lean_dependencies_mixed.lean +++ b/clang/test/Verify/suite/Inputs/lean_dependencies_mixed.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_product_4751c3 import CppVerify.User theorem cppverify_product_4751c3_postcondition_c4d4674b_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_dependencies_positive.lean b/clang/test/Verify/suite/Inputs/lean_dependencies_positive.lean index 4a3c213156..5fd5c25e71 100644 --- a/clang/test/Verify/suite/Inputs/lean_dependencies_positive.lean +++ b/clang/test/Verify/suite/Inputs/lean_dependencies_positive.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_positive_fb6abf import CppVerify.User theorem cppverify_positive_fb6abf_postcondition_78bdd643_proof : cppverify_positive_fb6abf_postcondition_78bdd643 := by diff --git a/clang/test/Verify/suite/Inputs/lean_dependencies_same.lean b/clang/test/Verify/suite/Inputs/lean_dependencies_same.lean index 5a8320c770..a663e3e6a1 100644 --- a/clang/test/Verify/suite/Inputs/lean_dependencies_same.lean +++ b/clang/test/Verify/suite/Inputs/lean_dependencies_same.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_same_9601c2 import CppVerify.User theorem cppverify_same_9601c2_postcondition_ce92acf8_proof : cppverify_same_9601c2_postcondition_ce92acf8 := by diff --git a/clang/test/Verify/suite/Inputs/lean_dependencies_seven.lean b/clang/test/Verify/suite/Inputs/lean_dependencies_seven.lean index ae71525385..741439d952 100644 --- a/clang/test/Verify/suite/Inputs/lean_dependencies_seven.lean +++ b/clang/test/Verify/suite/Inputs/lean_dependencies_seven.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_seven_634ea3 import CppVerify.User theorem cppverify_seven_634ea3_postcondition_f0d135e6_proof : cppverify_seven_634ea3_postcondition_f0d135e6 := by diff --git a/clang/test/Verify/suite/Inputs/lean_dependencies_termination.lean b/clang/test/Verify/suite/Inputs/lean_dependencies_termination.lean index 86ffa0028e..ef58ba38cf 100644 --- a/clang/test/Verify/suite/Inputs/lean_dependencies_termination.lean +++ b/clang/test/Verify/suite/Inputs/lean_dependencies_termination.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_scaled_decreases_52d363 import CppVerify.User theorem cppverify_scaled_decreases_52d363_termination_3164b050_proof : cppverify_scaled_decreases_52d363_termination_3164b050 := by diff --git a/clang/test/Verify/suite/Inputs/lean_dependencies_trusting.lean b/clang/test/Verify/suite/Inputs/lean_dependencies_trusting.lean index 3fd338653d..d075683fb3 100644 --- a/clang/test/Verify/suite/Inputs/lean_dependencies_trusting.lean +++ b/clang/test/Verify/suite/Inputs/lean_dependencies_trusting.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_trusting_b6301a import CppVerify.User theorem cppverify_trusting_b6301a_postcondition_bafafdfb_proof : cppverify_trusting_b6301a_postcondition_bafafdfb := by diff --git a/clang/test/Verify/suite/Inputs/lean_division.cpp b/clang/test/Verify/suite/Inputs/lean_division.cpp new file mode 100644 index 0000000000..99b3672250 --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_division.cpp @@ -0,0 +1,13 @@ +// C++ division and remainder in contracts, for lean_division.test. + +cppverify::proof void half_bound(int a) + cppverify::pre(a >= 4) + cppverify::post(a / 2 >= 2 && a % 2 >= 0) +{ +} + +cppverify::proof void negative_division(int a) + cppverify::pre(a < 0 && a > -100) + cppverify::post(a / 2 <= 0 && a % 3 <= 0) +{ +} diff --git a/clang/test/Verify/suite/Inputs/lean_fallback_goal_1.lean b/clang/test/Verify/suite/Inputs/lean_fallback_goal_1.lean index 8ba810ff34..1db8715a34 100644 --- a/clang/test/Verify/suite/Inputs/lean_fallback_goal_1.lean +++ b/clang/test/Verify/suite/Inputs/lean_fallback_goal_1.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_fibo_step_624f01 import CppVerify.User theorem cppverify_fibo_step_624f01_missing_return_ba8abb90_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_fallback_goal_2.lean b/clang/test/Verify/suite/Inputs/lean_fallback_goal_2.lean index df65b45915..86bfafa577 100644 --- a/clang/test/Verify/suite/Inputs/lean_fallback_goal_2.lean +++ b/clang/test/Verify/suite/Inputs/lean_fallback_goal_2.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_fibo_step_624f01 import CppVerify.User theorem cppverify_fibo_step_624f01_postcondition_559cb60a_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_fallback_goal_3.lean b/clang/test/Verify/suite/Inputs/lean_fallback_goal_3.lean index 55b9772bad..6ec932841d 100644 --- a/clang/test/Verify/suite/Inputs/lean_fallback_goal_3.lean +++ b/clang/test/Verify/suite/Inputs/lean_fallback_goal_3.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_fibo_six_ec829a import CppVerify.User theorem cppverify_fibo_six_ec829a_missing_return_ba8abb90_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_fallback_goal_4.lean b/clang/test/Verify/suite/Inputs/lean_fallback_goal_4.lean index 8ebf102ec4..df78e0c7ec 100644 --- a/clang/test/Verify/suite/Inputs/lean_fallback_goal_4.lean +++ b/clang/test/Verify/suite/Inputs/lean_fallback_goal_4.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_fibo_six_ec829a import CppVerify.User theorem cppverify_fibo_six_ec829a_postcondition_30cd1b97_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_fallback_partial_goal_1.lean b/clang/test/Verify/suite/Inputs/lean_fallback_partial_goal_1.lean index aa658e5ebc..9e7048ae1a 100644 --- a/clang/test/Verify/suite/Inputs/lean_fallback_partial_goal_1.lean +++ b/clang/test/Verify/suite/Inputs/lean_fallback_partial_goal_1.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_bump_ce87d1 import CppVerify.User theorem cppverify_bump_ce87d1_overflow_f5ee4d20_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_fallback_partial_goal_2.lean b/clang/test/Verify/suite/Inputs/lean_fallback_partial_goal_2.lean index 8d12720c5a..0218241f17 100644 --- a/clang/test/Verify/suite/Inputs/lean_fallback_partial_goal_2.lean +++ b/clang/test/Verify/suite/Inputs/lean_fallback_partial_goal_2.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_bump_ce87d1 import CppVerify.User theorem cppverify_bump_ce87d1_assertion_06404a7a_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_bump_ce87d1_missing_return_ba8abb90.lean b/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_bump_ce87d1_missing_return_ba8abb90.lean index 145a149822..acb538820c 100644 --- a/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_bump_ce87d1_missing_return_ba8abb90.lean +++ b/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_bump_ce87d1_missing_return_ba8abb90.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_bump_ce87d1 import CppVerify.User theorem cppverify_bump_ce87d1_missing_return_ba8abb90_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_one_99bcee_missing_return_54ae7c80.lean b/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_one_99bcee_missing_return_54ae7c80.lean index f9024c9662..9bece241a3 100644 --- a/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_one_99bcee_missing_return_54ae7c80.lean +++ b/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_one_99bcee_missing_return_54ae7c80.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_one_99bcee import CppVerify.User theorem cppverify_one_99bcee_missing_return_54ae7c80_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_one_99bcee_postcondition_cd6bdfcd.lean b/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_one_99bcee_postcondition_cd6bdfcd.lean index eee644659e..58448ce153 100644 --- a/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_one_99bcee_postcondition_cd6bdfcd.lean +++ b/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_one_99bcee_postcondition_cd6bdfcd.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_one_99bcee import CppVerify.User theorem cppverify_one_99bcee_postcondition_cd6bdfcd_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_one_99bcee_postcondition_cd6bdfcd_2.lean b/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_one_99bcee_postcondition_cd6bdfcd_2.lean index 7f4b99e6d4..1f724407fa 100644 --- a/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_one_99bcee_postcondition_cd6bdfcd_2.lean +++ b/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_one_99bcee_postcondition_cd6bdfcd_2.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_one_99bcee import CppVerify.User theorem cppverify_one_99bcee_postcondition_cd6bdfcd_2_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_one_99bcee_postcondition_ebbe6a6a.lean b/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_one_99bcee_postcondition_ebbe6a6a.lean index 5b4dde2b95..b09560a383 100644 --- a/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_one_99bcee_postcondition_ebbe6a6a.lean +++ b/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_one_99bcee_postcondition_ebbe6a6a.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_one_99bcee import CppVerify.User theorem cppverify_one_99bcee_postcondition_ebbe6a6a_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_six_6a2cf3_missing_return_54ae7c80.lean b/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_six_6a2cf3_missing_return_54ae7c80.lean index f3ca2db1f1..8fdce88ad3 100644 --- a/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_six_6a2cf3_missing_return_54ae7c80.lean +++ b/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_six_6a2cf3_missing_return_54ae7c80.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_six_6a2cf3 import CppVerify.User theorem cppverify_six_6a2cf3_missing_return_54ae7c80_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_six_6a2cf3_postcondition_198bc076.lean b/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_six_6a2cf3_postcondition_198bc076.lean index 3dae61ac78..0d1c2b271c 100644 --- a/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_six_6a2cf3_postcondition_198bc076.lean +++ b/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_six_6a2cf3_postcondition_198bc076.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_six_6a2cf3 import CppVerify.User theorem cppverify_six_6a2cf3_postcondition_198bc076_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_tri_decreases_f55d4c_termination_3c6bb93f.lean b/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_tri_decreases_f55d4c_termination_3c6bb93f.lean index 9f5bed9a7f..05952c73c6 100644 --- a/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_tri_decreases_f55d4c_termination_3c6bb93f.lean +++ b/clang/test/Verify/suite/Inputs/lean_identity_proofs/cppverify_tri_decreases_f55d4c_termination_3c6bb93f.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_tri_decreases_f55d4c import CppVerify.User theorem cppverify_tri_decreases_f55d4c_termination_3c6bb93f_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_modules.cpp b/clang/test/Verify/suite/Inputs/lean_modules.cpp new file mode 100644 index 0000000000..953d77f17a --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_modules.cpp @@ -0,0 +1,36 @@ +// Five functions for lean_modules.test: inc and dbl, and two that apply the +// recursive spec tri, whose postconditions Inputs/lean_modules_*.lean also +// prove by hand. + +cppverify::spec int tri(int n) + cppverify::decreases(n) +{ + return n <= 0 ? 0 : tri(n - 1) + n; +} + +int inc(int x) + cppverify::pre(0 <= x && x < 1000) + cppverify::post(cppverify::result == x + 1) +{ + return x + 1; +} + +int dbl(int x) + cppverify::pre(0 <= x && x < 1000) + cppverify::post(cppverify::result == 2 * x) +{ + return x + x; +} + +int tri_zero() + cppverify::post(cppverify::result == tri(0)) +{ + return 0; +} + +int plus_tri_zero(int x) + cppverify::pre(0 <= x && x < 1000) + cppverify::post(cppverify::result == x + tri(0)) +{ + return x; +} diff --git a/clang/test/Verify/suite/Inputs/lean_modules_plus_tri_zero.lean b/clang/test/Verify/suite/Inputs/lean_modules_plus_tri_zero.lean new file mode 100644 index 0000000000..2af3414bc0 --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_modules_plus_tri_zero.lean @@ -0,0 +1,11 @@ +import CppVerify.Goals.cppverify_plus_tri_zero_10bb8a +import CppVerify.User +import CppVerify.Auto + +theorem cppverify_plus_tri_zero_10bb8a_postcondition_8b45419c_proof : + cppverify_plus_tri_zero_10bb8a_postcondition_8b45419c := by + apply cppverify_plus_tri_zero_10bb8a_postcondition_8b45419c.of_readable + intro __heap_alloc_0 __heap_live_0 __result_1 x_0 tri_0 _ _ h_pre_1 h_null_1 h_null_2 h_result_1 + rw [tri_0] at * + simp only [cppSpecBody_10bb8a86_fn_5f5a3374726969_1] at * + cppverify_auto diff --git a/clang/test/Verify/suite/Inputs/lean_modules_tri_zero.lean b/clang/test/Verify/suite/Inputs/lean_modules_tri_zero.lean new file mode 100644 index 0000000000..03445c1a79 --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_modules_tri_zero.lean @@ -0,0 +1,11 @@ +import CppVerify.Goals.cppverify_tri_zero_e086e9 +import CppVerify.User +import CppVerify.Auto + +theorem cppverify_tri_zero_e086e9_postcondition_44e49d34_proof : + cppverify_tri_zero_e086e9_postcondition_44e49d34 := by + apply cppverify_tri_zero_e086e9_postcondition_44e49d34.of_readable + intro __heap_alloc_0 __heap_live_0 __result_1 tri_0 _ _ h_null_1 h_null_2 h_result_1 + rw [tri_0] at * + simp only [cppSpecBody_e086e9ab_fn_5f5a3374726969_1] at * + cppverify_auto diff --git a/clang/test/Verify/suite/Inputs/lean_project_text.py b/clang/test/Verify/suite/Inputs/lean_project_text.py new file mode 100644 index 0000000000..a090df2bd5 --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_project_text.py @@ -0,0 +1,11 @@ +"""Prints the generated text of a Lean project: its prelude, its specs, and +each goal module in the order its Generated module imports them, the +functions' source order.""" +import pathlib, sys + +root = pathlib.Path(sys.argv[1]) / "CppVerify" +parts = [root / "Prelude.lean", root / "Specs.lean"] +for line in (root / "Generated.lean").read_text().splitlines(): + if line.startswith("import CppVerify.Goals."): + parts.append(root / "Goals" / (line[len("import CppVerify.Goals."):] + ".lean")) +sys.stdout.write("".join(part.read_text() for part in parts)) diff --git a/clang/test/Verify/suite/Inputs/lean_readable_proofs/cppverify_twice_edf933_missing_return_54ae7c80.lean b/clang/test/Verify/suite/Inputs/lean_readable_proofs/cppverify_twice_edf933_missing_return_54ae7c80.lean index d450f13548..18f468999d 100644 --- a/clang/test/Verify/suite/Inputs/lean_readable_proofs/cppverify_twice_edf933_missing_return_54ae7c80.lean +++ b/clang/test/Verify/suite/Inputs/lean_readable_proofs/cppverify_twice_edf933_missing_return_54ae7c80.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_twice_edf933 import CppVerify.User theorem cppverify_twice_edf933_missing_return_54ae7c80_proof : cppverify_twice_edf933_missing_return_54ae7c80 := by diff --git a/clang/test/Verify/suite/Inputs/lean_readable_proofs/cppverify_twice_edf933_postcondition_28de726e.lean b/clang/test/Verify/suite/Inputs/lean_readable_proofs/cppverify_twice_edf933_postcondition_28de726e.lean index e1080ebc94..11440c7bf0 100644 --- a/clang/test/Verify/suite/Inputs/lean_readable_proofs/cppverify_twice_edf933_postcondition_28de726e.lean +++ b/clang/test/Verify/suite/Inputs/lean_readable_proofs/cppverify_twice_edf933_postcondition_28de726e.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_twice_edf933 import CppVerify.User theorem cppverify_twice_edf933_postcondition_28de726e_proof : cppverify_twice_edf933_postcondition_28de726e := by diff --git a/clang/test/Verify/suite/Inputs/lean_readable_proofs/cppverify_twice_edf933_precondition_51b3c1f0.lean b/clang/test/Verify/suite/Inputs/lean_readable_proofs/cppverify_twice_edf933_precondition_51b3c1f0.lean index 5c6b0e43e4..30d1e5cc82 100644 --- a/clang/test/Verify/suite/Inputs/lean_readable_proofs/cppverify_twice_edf933_precondition_51b3c1f0.lean +++ b/clang/test/Verify/suite/Inputs/lean_readable_proofs/cppverify_twice_edf933_precondition_51b3c1f0.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_twice_edf933 import CppVerify.User theorem cppverify_twice_edf933_precondition_51b3c1f0_proof : cppverify_twice_edf933_precondition_51b3c1f0 := by diff --git a/clang/test/Verify/suite/Inputs/lean_readable_proofs/cppverify_twice_edf933_precondition_f0a3d715.lean b/clang/test/Verify/suite/Inputs/lean_readable_proofs/cppverify_twice_edf933_precondition_f0a3d715.lean index 2607fff7eb..9b2059d45a 100644 --- a/clang/test/Verify/suite/Inputs/lean_readable_proofs/cppverify_twice_edf933_precondition_f0a3d715.lean +++ b/clang/test/Verify/suite/Inputs/lean_readable_proofs/cppverify_twice_edf933_precondition_f0a3d715.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_twice_edf933 import CppVerify.User theorem cppverify_twice_edf933_precondition_f0a3d715_proof : cppverify_twice_edf933_precondition_f0a3d715 := by diff --git a/clang/test/Verify/suite/Inputs/lean_refutation.cpp b/clang/test/Verify/suite/Inputs/lean_refutation.cpp new file mode 100644 index 0000000000..bc715e344f --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_refutation.cpp @@ -0,0 +1,39 @@ +// Certified failures shown in Lean, for lean_refutation.test. + +int add_one(int x) + cppverify::pre(x < 100) + cppverify::post(cppverify::result == x + 1) +{ + return x + 2; +} + +int below_three() + cppverify::post(cppverify::forall(i, 0, 4, i < 3)) +{ + return 0; +} + +void zero_first(int *p) + cppverify::pre(cppverify::valid(p, 2)) + cppverify::modifies(*p) + cppverify::post(p[0] == 1) +{ + p[0] = 0; +} + +cppverify::spec int tri(int n) + cppverify::decreases(n) +{ + return n <= 0 ? 0 : tri(n - 1) + n; +} + +cppverify::proof void tri_three() + cppverify::post(tri(3) == 7) +{ +} + +// A choice is opaque in Lean, so its counterexample is not decided there. +cppverify::proof void pick_one() + cppverify::post(cppverify::choose(k, 0, 2, k == 1) == 0) +{ +} diff --git a/clang/test/Verify/suite/Inputs/lean_refutation_budget.cpp b/clang/test/Verify/suite/Inputs/lean_refutation_budget.cpp new file mode 100644 index 0000000000..6b38456856 --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_refutation_budget.cpp @@ -0,0 +1,31 @@ +// Refutations that are bounded, and refutations that are not reported, for +// lean_refutation.test. + +// Lean decides a quantifier over a thousand integers by trying each one. +cppverify::proof void below_a_thousand() + cppverify::post(cppverify::forall(i, 0, 1000, i < 999)) +{ +} + +// It would decide this one by trying each of a billion integers, which the +// prelude does only for ranges of at most 4096: each integer is a level of +// recursion, whose memory heartbeats do not bound. The counterexample is not +// decided. +cppverify::proof void below_a_billion() + cppverify::post(cppverify::forall(i, 0, 1000000000, i < 999999999)) +{ +} + +// A counterexample checked with a postcondition that is not established is +// not a failure (spec.post), so nothing is reported of its refutation. +cppverify::spec bool ge(int n) + cppverify::inductive + cppverify::post(!cppverify::result || n > -1000) +{ + return n == 20 || ge(n + 1); +} + +cppverify::proof void far() + cppverify::post(ge(-5000)) +{ +} diff --git a/clang/test/Verify/suite/Inputs/lean_refutation_tactic.lean b/clang/test/Verify/suite/Inputs/lean_refutation_tactic.lean new file mode 100644 index 0000000000..02f2844211 --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_refutation_tactic.lean @@ -0,0 +1,16 @@ +import CppVerify.Auto + +-- An instance that holds: the refutation says Lean decides it true. +example : ¬ (∀ x : Int, x = 1 → x > 0) := by + intro h + have h₁ := h 1 + cppverify_refute h₁ + +opaque cppverify_unknown : Int + +-- An instance about a constant Lean has no value for: the refutation says +-- Lean cannot decide it. +example : ¬ (∀ x : Int, x = 0 → x = cppverify_unknown) := by + intro h + have h₁ := h 0 + cppverify_refute h₁ diff --git a/clang/test/Verify/suite/Inputs/lean_replay.cpp b/clang/test/Verify/suite/Inputs/lean_replay.cpp new file mode 100644 index 0000000000..f2f232d87b --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_replay.cpp @@ -0,0 +1,33 @@ +// A program whose archive replays into a Lean project, for lean_replay.test. + +cppverify::spec int tri(int n) + cppverify::decreases(n) +{ + return n <= 0 ? 0 : tri(n - 1) + n; +} + +cppverify::proof void tri_nonneg(int n) + cppverify::post(tri(n) >= 0) +{ +} + +// A measure that applies a spec: its module keeps the spec it applies. +cppverify::spec int size(int n) { return n; } + +cppverify::spec int down(int n) + cppverify::decreases(size(n)) +{ + return n <= 0 ? 0 : down(n - 1); +} + +cppverify::proof void down_zero() + cppverify::post(down(0) == 0) +{ +} + +int inc(int x) + cppverify::pre(x < 100) + cppverify::post(cppverify::result == x + 1) +{ + return x + 1; +} diff --git a/clang/test/Verify/suite/Inputs/lean_replay_loops.cpp b/clang/test/Verify/suite/Inputs/lean_replay_loops.cpp new file mode 100644 index 0000000000..80232d4525 --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_replay_loops.cpp @@ -0,0 +1,22 @@ +// Loops for a bounded archive, for lean_replay.test: one ends within the +// bound, one need not. + +int sum_small(int n) + cppverify::pre(0 <= n && n <= 2) + cppverify::post(cppverify::result >= 0) +{ + int s = 0; + for (int i = 0; i < n; i = i + 1) + s = s + i; + return s; +} + +int count_up(int n) + cppverify::pre(0 <= n && n <= 1000) + cppverify::post(cppverify::result == n) +{ + int c = 0; + while (c < n) + c = c + 1; + return c; +} diff --git a/clang/test/Verify/suite/Inputs/lean_semantics_call.lean b/clang/test/Verify/suite/Inputs/lean_semantics_call.lean index 8dc0f46fa3..5df905de2c 100644 --- a/clang/test/Verify/suite/Inputs/lean_semantics_call.lean +++ b/clang/test/Verify/suite/Inputs/lean_semantics_call.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_small_74ff94 import CppVerify.User theorem cppverify_small_74ff94_precondition_4e324fa2_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_semantics_mix.lean b/clang/test/Verify/suite/Inputs/lean_semantics_mix.lean index 4c46c601c2..623902f5cf 100644 --- a/clang/test/Verify/suite/Inputs/lean_semantics_mix.lean +++ b/clang/test/Verify/suite/Inputs/lean_semantics_mix.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_mix_b839ab import CppVerify.User theorem cppverify_mix_b839ab_assertion_255467f3_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_semantics_numerals.lean b/clang/test/Verify/suite/Inputs/lean_semantics_numerals.lean index 5142c33706..96e97fd298 100644 --- a/clang/test/Verify/suite/Inputs/lean_semantics_numerals.lean +++ b/clang/test/Verify/suite/Inputs/lean_semantics_numerals.lean @@ -1,3 +1,4 @@ +import CppVerify.Goals.cppverify_difference_465d5b import CppVerify.User theorem cppverify_difference_465d5b_assertion_d7e0e821_proof : diff --git a/clang/test/Verify/suite/Inputs/lean_spec_definitions.cpp b/clang/test/Verify/suite/Inputs/lean_spec_definitions.cpp new file mode 100644 index 0000000000..bf9fbec303 --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_spec_definitions.cpp @@ -0,0 +1,110 @@ +// Spec functions as Lean definitions, for lean_spec_definitions.test. + +cppverify::spec int tri(int n) + cppverify::decreases(n) +{ + return n <= 0 ? 0 : tri(n - 1) + n; +} + +cppverify::spec bool odd(int n); + +cppverify::spec bool even(int n) + cppverify::decreases(n, 1) +{ + return n <= 0 || odd(n - 1); +} + +cppverify::spec bool odd(int n) + cppverify::decreases(n, 0) +{ + return n > 0 && even(n - 1); +} + +cppverify::spec int ack(int m, int n) + cppverify::decreases(m, n) +{ + return m <= 0 ? n + 1 : (n <= 0 ? ack(m - 1, 1) : ack(m - 1, ack(m, n - 1))); +} + +cppverify::spec int half(int n) + cppverify::decreases(n) + cppverify::post(cppverify::result >= 0 && (n <= 1 || cppverify::result < n)) +{ + return n <= 1 ? 0 : half(n - 2) + 1; +} + +// Its measure falls by C++ division. +cppverify::spec int log2(int n) + cppverify::decreases(n) +{ + return n <= 1 ? 0 : log2(n / 2) + 1; +} + +// Its termination rests on the postcondition of half, which Lean does not +// know: it stays opaque. +cppverify::spec int steps(int n) + cppverify::decreases(n) +{ + return n <= 1 ? 0 : steps(half(n)) + 1; +} + +cppverify::proof void tri_nonneg(int n) + cppverify::post(tri(n) >= 0) +{ +} + +cppverify::proof void tri_four() + cppverify::post(tri(4) == 10) +{ +} + +cppverify::proof void even_four() + cppverify::post(even(4) && !odd(4)) +{ +} + +cppverify::proof void small_ack() + cppverify::post(ack(1, 1) == 3) +{ +} + +cppverify::proof void log2_eight() + cppverify::post(log2(8) == 3) +{ +} + +cppverify::proof void steps_one() + cppverify::post(steps(1) == 0) +{ +} + +// Its call may have the measure -1 where the caller's is 0. +cppverify::spec int down(int n) + cppverify::decreases(n) +{ + return n < 0 ? 0 : down(n - 1) + 1; +} + +// It calls steps, which stays opaque: Lean accepts it once steps is. +cppverify::spec int count_steps(int n) + cppverify::decreases(n) +{ + return n <= 0 ? 0 : count_steps(n - 1) + steps(n); +} + +cppverify::proof void down_zero() + cppverify::post(down(0) == 1 && count_steps(0) == 0) +{ +} + +// A call under an existential sees its range. +cppverify::spec bool reaches(int n) + cppverify::decreases(n) +{ + return n <= 0 || cppverify::exists(k, 0, n, reaches(k)); +} + +cppverify::proof void reaches_zero() + cppverify::post(reaches(0)) +{ +} diff --git a/clang/test/Verify/suite/Inputs/lean_spec_definitions_tri_nonneg.lean b/clang/test/Verify/suite/Inputs/lean_spec_definitions_tri_nonneg.lean new file mode 100644 index 0000000000..3e20c0085f --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_spec_definitions_tri_nonneg.lean @@ -0,0 +1,15 @@ +import CppVerify.Goals.cppverify_tri_nonneg_42a030 +import CppVerify.User +import CppVerify.Auto + +/-- tri is never negative, by induction over its own recursion. -/ +theorem tri_nonneg (m : Int) : 0 ≤ cppSpec_fn_5f5a3374726969 m := by + induction m using cppSpec_fn_5f5a3374726969.induct with + | case1 x h => rw [cppSpec_fn_5f5a3374726969]; simp [h] + | case2 x h ih => rw [cppSpec_fn_5f5a3374726969]; simp [h]; omega + +theorem cppverify_tri_nonneg_42a030_postcondition_053b1980_proof : + cppverify_tri_nonneg_42a030_postcondition_053b1980 := by + apply cppverify_tri_nonneg_42a030_postcondition_053b1980.of_readable + intro _ _ n _ _ _ + exact tri_nonneg _ diff --git a/clang/test/Verify/suite/Inputs/mutate_obligation_archive.py b/clang/test/Verify/suite/Inputs/mutate_obligation_archive.py index a057a36fe5..64dfac1893 100644 --- a/clang/test/Verify/suite/Inputs/mutate_obligation_archive.py +++ b/clang/test/Verify/suite/Inputs/mutate_obligation_archive.py @@ -18,6 +18,11 @@ def __init__(self, data): self.obligation_ids = [] self.obligation_kind_offsets = [] self.version_offsets = [] + self.version = 0 + # What schema 3 adds: measures and premises, which a record of an + # earlier schema has not. + self.measure_spans = [] + self.measures = [] self.modules = [] def u8(self): @@ -99,6 +104,11 @@ def expression(self, track=True): self.offset += 1 self.string() source = self.source() + if self.version >= 3 and self.flags & 1 and kind == 26: + # Schema 3 adds where an assumption comes from. + start = self.offset + self.string() + self.measure_spans.append((start, self.offset)) children_count_offset = self.offset children = [self.expression(track=track) for _ in range(self.u64())] return { @@ -124,12 +134,18 @@ def function(self): self.offset += 4 expressions = [self.expression()] expressions.extend(self.expression() for _ in range(self.u64())) + if self.version >= 3: + # Schema 3 adds the termination measures. + start = self.offset + for _ in range(self.u64()): + self.measures.append(self.expression()) + self.measure_spans.append((start, self.offset)) return expressions, identity def module(self): self.offset += 8 self.version_offsets.append(self.offset) - self.offset += 4 + self.version = self.u32() self.flags = self.u32() function_name = self.string() self.string() @@ -351,6 +367,8 @@ def invalidate_quantifier_binder_sort(self, node): "invalid-utf8", "schema-v1", "schema-v1-kinds", + "schema-v2", + "measure-free-variable", "truncated", ], ) @@ -364,14 +382,41 @@ def invalidate_quantifier_binder_sort(self, node): if args.mode == "invalid-kind": archive.data[archive.expression_kind_offsets[0]] = 255 -elif args.mode in ("schema-v1", "schema-v1-kinds"): +elif args.mode in ("schema-v1", "schema-v1-kinds", "schema-v2"): for offset in archive.version_offsets: - struct.pack_into(" 3: archive.data[offset] = 1 + # Records before schema 3 have no measures or premises; a premise + # inside a measure goes with it. + spans = sorted(archive.measure_spans) + outer = [ + span + for span in spans + if not any(o != span and o[0] <= span[0] and span[1] <= o[1] for o in spans) + ] + for start, end in reversed(outer): + del archive.data[start:end] +elif args.mode == "measure-free-variable": + def first_variable(node): + if node is None: + return None + if node["kind"] == 5 and node["name"][1] > 0: + return node + return next( + (found for child in node["children"] if (found := first_variable(child))), + None, + ) + variable = next( + (found for measure in archive.measures if (found := first_variable(measure))), + None, + ) + if variable is None: + raise ValueError("archive has no measure that reads a variable") + archive.data[variable["name"][0]] = ord("z") elif args.mode == "feature-mismatch": struct.pack_into("= 1) +{ + switch (x) { + case 0: + return 1; + default: + return 2; + } +} + +int uses_pick(int x) + cppverify::post(cppverify::result >= 1) +{ + return pick(x); +} + +// A contract the verifier does not represent leaves no contract to rely on. +int lookup(int i) + cppverify::pre(0 <= i && i < 4 && table[i] > 0) + cppverify::post(cppverify::result >= 0) +{ + return 1; +} + +int uses_lookup() + cppverify::post(cppverify::result >= 0) +{ + return lookup(1); +} + +// A spec the verifier does not represent: what reaches it is not verified. +cppverify::spec int choice(int x) { + switch (x) { + case 0: + return 1; + default: + return 2; + } +} + +int uses_choice(int x) + cppverify::post(cppverify::result == x || choice(x) > 0) +{ + return x; +} + +cppverify::proof void choice_lemma(int x) + cppverify::post(choice(x) == choice(x)) +{ +} + +// A lemma whose body the verifier does not represent. +cppverify::proof void zero_lemma(int x) + cppverify::post(x * 0 == 0) +{ + switch (x) { + default: + break; + } +} + +int uses_zero_lemma(int x) + cppverify::post(cppverify::result == 0) +{ + cppverify::ghost { zero_lemma(x); } + return 0; +} + +// An uncontracted constexpr helper, lifted for its caller. +constexpr int step(int x) { + switch (x) { + case 0: + return 1; + default: + return 2; + } +} + +int uses_step(int x) + cppverify::post(cppverify::result >= 1) +{ + return step(x); +} + +// The functions the verifier represents are verified as before. +int inc(int x) + cppverify::pre(x < 100) + cppverify::post(cppverify::result == x + 1) +{ + return x + 1; +} + +cppverify::spec int twice(int x) { return x + x; } + +int uses_twice(int x) + cppverify::pre(0 <= x && x < 100) + cppverify::post(cppverify::result == twice(x)) +{ + return x + x; +} diff --git a/clang/test/Verify/suite/Inputs/run_backend_fidelity.py b/clang/test/Verify/suite/Inputs/run_backend_fidelity.py index 18d97cddca..cbbbdf3871 100644 --- a/clang/test/Verify/suite/Inputs/run_backend_fidelity.py +++ b/clang/test/Verify/suite/Inputs/run_backend_fidelity.py @@ -4,6 +4,7 @@ import json import pathlib import re +import shutil import subprocess import sys @@ -476,8 +477,14 @@ def check_lean_identity(cpp_verify, source, canonical, work): ], {0}, ) - generated = project / "CppVerify" / "Generated.lean" - text = generated.read_text() + # The project's generated text: its prelude, specs, and each goal module. + root = project / "CppVerify" + modules = [root / "Prelude.lean", root / "Specs.lean"] + [ + root / "Goals" / (line[len("import CppVerify.Goals."):] + ".lean") + for line in (root / "Generated.lean").read_text().splitlines() + if line.startswith("import CppVerify.Goals.") + ] + text = "".join(module.read_text() for module in modules) if "sorry" in text: fail("generated Lean semantics contains an admitted proof") @@ -522,6 +529,64 @@ def check_lean_identity(cpp_verify, source, canonical, work): run(["lake", "--dir={}".format(project), "build", "CppVerify.Generated"], {0}) +def check_lean_verdicts(cpp_verify, source, proofs, work): + # Lean decides every case: a project certifies each valid one, with the + # proof by hand of the one goal cppverify_auto does not prove, and the + # one only a hidden definition proves, which Lean sees; a fallback + # project confirms the counterexample of each invalid one in Lean. + project = work / "lean-verdicts" + command = [ + str(cpp_verify), + "--backend=lean", + "--lean-project={}".format(project), + "--diagnostics-format=json", + str(source), + "--", + ] + run(command, {0, 1}) + for proof in proofs.glob("*.lean"): + group = re.match(r"(cppverify_.+?_[0-9a-f]{6})_", proof.name).group(1) + shutil.copy(proof, project / "CppVerify" / "Proofs" / group / proof.name) + # Every verdict on a function: its obligations, and the check of its + # contract when it has one. + certified = {} + for record in parse_json_lines( + run(command[:4] + ["--lean-certify"] + command[4:], {0, 1}) + ): + certified.setdefault(record.get("function"), []).append(record) + for name in sorted(VALID_FUNCTIONS | set(UNRESOLVED_REASONS)): + for record in certified.get(name) or [{}]: + if record.get("status") != "certified": + fail( + "Lean returned {} ({}) instead of certified for {}".format( + record.get("status"), record.get("message"), name + ) + ) + refuted = function_records( + parse_json_lines( + run( + [ + str(cpp_verify), + "--lean-fallback={}".format(work / "lean-refutations"), + "--lean-certify", + "--diagnostics-format=json", + str(source), + "--", + ], + {1}, + ) + ) + ) + for name in sorted(INVALID_FUNCTIONS): + record = refuted[name] + if record.get("status") != "failed" or record.get("lean_refuted") is not True: + fail( + "Lean did not confirm the counterexample of {} ({}, {})".format( + name, record.get("status"), record.get("message") + ) + ) + + def warning_messages(records): return [ record.get("message", "") @@ -611,10 +676,12 @@ def main(): parser.add_argument("--cpp-verify", required=True, type=pathlib.Path) parser.add_argument("--source", required=True, type=pathlib.Path) parser.add_argument("--recommends", required=True, type=pathlib.Path) + parser.add_argument("--lean-proofs", required=True, type=pathlib.Path) parser.add_argument("--work-dir", required=True, type=pathlib.Path) arguments = parser.parse_args() - for path in (arguments.cpp_verify, arguments.source, arguments.recommends): + for path in (arguments.cpp_verify, arguments.source, arguments.recommends, + arguments.lean_proofs): if not path.exists(): fail("required path does not exist: {}".format(path)) arguments.work_dir.mkdir(parents=True, exist_ok=False) @@ -636,6 +703,12 @@ def main(): ) print("PASS: Lean theorem, hash, replay, and kernel-export identity") + check_lean_verdicts( + arguments.cpp_verify, arguments.source, arguments.lean_proofs, + arguments.work_dir + ) + print("PASS: decisive Lean verdicts: valid cases certified, counterexamples refuted") + check_recommends_exclusion( arguments.cpp_verify, arguments.recommends, arguments.work_dir ) diff --git a/clang/test/Verify/suite/backend_fidelity_gate.test b/clang/test/Verify/suite/backend_fidelity_gate.test index 4af1f028b2..93dc370831 100644 --- a/clang/test/Verify/suite/backend_fidelity_gate.test +++ b/clang/test/Verify/suite/backend_fidelity_gate.test @@ -5,6 +5,7 @@ # RUN: --cpp-verify=%cpp-verify \ # RUN: --source=%S/Inputs/backend_fidelity_matrix.cpp \ # RUN: --recommends=%S/Inputs/backend_fidelity_recommends.cpp \ +# RUN: --lean-proofs=%S/Inputs/backend_fidelity_proofs \ # RUN: --work-dir=%t | FileCheck %s # # CHECK: PASS: canonical feature and operator matrix @@ -12,4 +13,5 @@ # CHECK-NEXT: PASS: source diagnostic identity and parallel determinism # CHECK-NEXT: PASS: canonical and incremental-BMC archive replay # CHECK-NEXT: PASS: Lean theorem, hash, replay, and kernel-export identity +# CHECK-NEXT: PASS: decisive Lean verdicts: valid cases certified, counterexamples refuted # CHECK-NEXT: PASS: recommends exclusion from BMC and Lean replay artifacts diff --git a/clang/test/Verify/suite/lean_automation.test b/clang/test/Verify/suite/lean_automation.test index 0862d09576..cb906ffbd1 100644 --- a/clang/test/Verify/suite/lean_automation.test +++ b/clang/test/Verify/suite/lean_automation.test @@ -28,8 +28,8 @@ # # A stub that no revision of cppverify_auto tried, as before automation, and # one an earlier revision tried, are tried. -# RUN: sed -i -e '/^ automation:/d' -e '/^import CppVerify.Auto$/d' -e 's/^ cppverify_auto_grind$/ sorry/' %t/p/CppVerify/Proofs/cppverify_next_e1e917/cppverify_next_e1e917_missing_return_54ae7c80.lean -# RUN: sed -i 's/cppverify_auto 1 did not prove it/cppverify_auto 0 did not prove it/' %t/p/CppVerify/Proofs/cppverify_square_934e38/cppverify_square_934e38_overflow_f86973f7.lean +# RUN: sed -i -e '/^ automation:/d' -e '/^import CppVerify.Goals\./d' -e '/^import CppVerify.Auto$/d' -e 's/^ cppverify_auto_grind$/ sorry/' %t/p/CppVerify/Proofs/cppverify_next_e1e917/cppverify_next_e1e917_missing_return_54ae7c80.lean +# RUN: sed -i 's/cppverify_auto 3 did not prove it/cppverify_auto 2 did not prove it/' %t/p/CppVerify/Proofs/cppverify_square_934e38/cppverify_square_934e38_overflow_f86973f7.lean # RUN: %cpp-verify --backend=lean --lean-project=%t/p %S/Inputs/lean_automation.cpp -- 2>&1 | FileCheck %s --check-prefix=EDITED # RUN: FileCheck %s --input-file=%t/p/CppVerify/Proofs/cppverify_next_e1e917/cppverify_next_e1e917_missing_return_54ae7c80.lean --check-prefix=TRIED # RUN: FileCheck %s --input-file=%t/p/CppVerify/Proofs/cppverify_square_934e38/cppverify_square_934e38_overflow_f86973f7.lean --check-prefix=STUB @@ -54,9 +54,10 @@ # EXPORT-NEXT: Exported: lean obligation: next (cppverify_auto proved 3 of 3 goals) # EXPORT-NEXT: Exported: lean obligation: square (cppverify_auto proved 1 of 3 goals) -# PROVED: automation: cppverify_auto 1 proves it with cppverify_auto_grind +# PROVED: automation: cppverify_auto 3 proves it with cppverify_auto_grind # PROVED-NEXT: cppverify_auto proves this goal. cpp-verify rewrites this file only while it # PROVED-NEXT: is unedited; a proof you write here is yours, kept as any other. -/ +# PROVED-NEXT: import CppVerify.Goals.cppverify_next_e1e917 # PROVED-NEXT: import CppVerify.User # PROVED-NEXT: import CppVerify.Auto # PROVED-EMPTY: @@ -66,11 +67,11 @@ # PROVED-NEXT: cppverify_auto_grind # PROVED-EMPTY: -# STUB: automation: cppverify_auto 1 did not prove it{{$}} +# STUB: automation: cppverify_auto 3 did not prove it{{$}} # STUB-NEXT: Complete this proof. # STUB: {{^}} sorry{{$}} -# SLOW: automation: cppverify_auto 1 did not prove it within 3 s +# SLOW: automation: cppverify_auto 3 did not prove it within 3 s # SLOW: {{^}} sorry{{$}} # JSON: "function":"next","lean_automatic":3,{{.*}}"status":"exported" @@ -81,10 +82,10 @@ # EDITED: Exported: lean obligation: next (cppverify_auto proved 2 of 3 goals) -# TRIED: automation: cppverify_auto 1 proves it with cppverify_auto_grind +# TRIED: automation: cppverify_auto 3 proves it with cppverify_auto_grind # TRIED: {{^}} cppverify_auto_grind{{$}} -# HONEST: automation: cppverify_auto 1 proves it with cppverify_auto_heap +# HONEST: automation: cppverify_auto 3 proves it with cppverify_auto_heap # HONEST: {{^}} cppverify_auto_heap{{$}} # PLAIN: Exported: lean obligation: a_slow{{$}} diff --git a/clang/test/Verify/suite/lean_certifier.test b/clang/test/Verify/suite/lean_certifier.test index b738cacb2d..7edbabeb7f 100644 --- a/clang/test/Verify/suite/lean_certifier.test +++ b/clang/test/Verify/suite/lean_certifier.test @@ -38,6 +38,14 @@ # RUN: cp %S/Inputs/lean_certifier_proofs/shadowed/cppverify_one_* %t/shadowed/CppVerify/Proofs/cppverify_one_99bcee/ # RUN: not %cpp-verify --backend=lean --lean-project=%t/shadowed --lean-certify %S/Inputs/lean_certifier.cpp -- 2>&1 | FileCheck %s --check-prefix=SHADOWED # +# The same in the proof of the goal that comes first, which imports nothing +# of the project: the generated module is replayed first all the same, so the +# proof file is the module left out, and the next function is certified. +# RUN: %cpp-verify --backend=lean --no-lean-auto --lean-project=%t/first %S/Inputs/lean_certifier_first.cpp -- > /dev/null +# RUN: cp %S/Inputs/lean_certifier_proofs/cppverify_fine_* %t/first/CppVerify/Proofs/cppverify_fine_52006a/ +# RUN: cp %S/Inputs/lean_certifier_proofs/first/* %t/first/CppVerify/Proofs/cppverify_early_103f65/ +# RUN: not %cpp-verify --backend=lean --no-lean-auto --lean-project=%t/first --lean-certify %S/Inputs/lean_certifier_first.cpp -- 2>&1 | FileCheck %s --check-prefix=FIRST +# # An axiom behind a lemma. # RUN: cp -r %t/base %t/hidden # RUN: cp %S/Inputs/lean_certifier_proofs/hidden/cppverify_one_* %t/hidden/CppVerify/Proofs/cppverify_one_99bcee/ @@ -59,7 +67,10 @@ # IMPORTED-NEXT: Unresolved: lean obligation: one (Lean certification failed: the proof of fn_5f5a336f6e6576::postcondition@8:37: the kernel rejects CppVerify.Lemmas: while replaying declaration 'cheat': (kernel) declaration type mismatch, 'cheat' has type True but it is expected to have type cppverify_one_99bcee_postcondition_646d2761) # SHADOWED: Certified: fine [backend=Lean] -# SHADOWED-NEXT: Unresolved: lean obligation: one (Lean certification failed: the proof of fn_5f5a336f6e6576::postcondition@8:37: cppverify_one_99bcee_postcondition_646d2761 is not the goal CppVerify.Generated defines) +# SHADOWED-NEXT: Unresolved: lean obligation: one (Lean certification failed: the proof of fn_5f5a336f6e6576::postcondition@8:37: CppVerify.Proofs.cppverify_one_99bcee.cppverify_one_99bcee_postcondition_646d2761 declares cppverify_one_99bcee_postcondition_646d2761, which CppVerify.Goals.cppverify_one_99bcee declares) + +# FIRST: Unresolved: lean obligation: early (Lean certification failed: {{.*}}; the proof of fn_5f5a356561726c7976::postcondition@2:37: CppVerify.Proofs.cppverify_early_103f65.cppverify_early_103f65_postcondition_646d2761 declares cppverify_early_103f65_postcondition_646d2761, which CppVerify.Goals.cppverify_early_103f65 declares) +# FIRST-NEXT: Certified: fine [backend=Lean] # HIDDEN: Certified: fine [backend=Lean] # HIDDEN-NEXT: Unresolved: lean obligation: one (Lean certification failed: the proof of fn_5f5a336f6e6576::postcondition@8:37: cppverify_one_99bcee_postcondition_646d2761_proof depends on the axiom escape, which is not one of Lean's foundations) diff --git a/clang/test/Verify/suite/lean_certifier_memory.test b/clang/test/Verify/suite/lean_certifier_memory.test new file mode 100644 index 0000000000..c892c86670 --- /dev/null +++ b/clang/test/Verify/suite/lean_certifier_memory.test @@ -0,0 +1,20 @@ +# REQUIRES: lean, system-linux +# +# The checker imports the toolchain's modules once and replays the project's +# into one kernel environment, so its memory does not grow with the goals or +# with the imports of their proofs. A proof that imports Lean through a module +# the kernel rejects made it import Lean's environment again for every goal +# and every module it replayed, never freeing one: 13 GB here, now under 4. +# +# RUN: rm -rf %t && mkdir -p %t +# RUN: %cpp-verify --backend=lean --lean-project=%t/p %S/Inputs/lean_certifier.cpp -- > /dev/null +# RUN: cp %S/Inputs/lean_certifier_proofs/cppverify_fine_* %t/p/CppVerify/Proofs/cppverify_fine_52006a/ +# RUN: cp %S/Inputs/lean_certifier_proofs/cppverify_one_* %t/p/CppVerify/Proofs/cppverify_one_99bcee/ +# RUN: cp %S/Inputs/lean_certifier_proofs/imported/Lemmas.lean %t/p/CppVerify/ +# RUN: cp %S/Inputs/lean_certifier_proofs/imported/cppverify_one_* %t/p/CppVerify/Proofs/cppverify_one_99bcee/ +# RUN: cd %t/p && lake build CppVerify.Lemmas +# RUN: not %python %S/Inputs/peak_memory.py 6144 %cpp-verify --backend=lean --lean-project=%t/p --lean-certify %S/Inputs/lean_certifier.cpp -- 2>&1 | FileCheck %s + +# CHECK: Certified: fine [backend=Lean] +# CHECK-NEXT: Unresolved: lean obligation: one (Lean certification failed: the proof of fn_5f5a336f6e6576::postcondition@8:37: the kernel rejects CppVerify.Lemmas: while replaying declaration 'cheat' +# CHECK-NEXT: peak memory below 6144 MB diff --git a/clang/test/Verify/suite/lean_certify.cpp b/clang/test/Verify/suite/lean_certify.cpp index 2cd5205c70..831f3d40c4 100644 --- a/clang/test/Verify/suite/lean_certify.cpp +++ b/clang/test/Verify/suite/lean_certify.cpp @@ -28,7 +28,6 @@ void trivial() // ADMITTED-SAME: declaration uses `sorry` // CERTIFIED: Certified: trivial [backend=Lean] // USER-ADMITTED: Unresolved: lean obligation: trivial (Lean certification failed: -// USER-ADMITTED-SAME: User.lean -// USER-ADMITTED-SAME: declaration uses `sorry` +// USER-ADMITTED-SAME: unfinished of CppVerify.User uses sorry // AXIOM: Unresolved: lean obligation: trivial (Lean certification failed: the proof of {{.*}}::missing-return@synthetic: cppverify_trivial_3e4f5a_missing_return_ba8abb90_proof depends on the axiom escape, which is not one of Lean's foundations; the proof of {{.*}}::postcondition@{{[0-9]+}}:{{[0-9]+}}: cppverify_trivial_3e4f5a_postcondition_ba8abb90_proof depends on the axiom escape, which is not one of Lean's foundations) // STALE: Unresolved: lean obligation: trivial (Lean certification failed: diff --git a/clang/test/Verify/suite/lean_collections.test b/clang/test/Verify/suite/lean_collections.test new file mode 100644 index 0000000000..2a2daf888c --- /dev/null +++ b/clang/test/Verify/suite/lean_collections.test @@ -0,0 +1,76 @@ +# REQUIRES: lean +# +# The spec collections in Lean: a sequence is a List Int, a set its +# membership test, a multiset its counts, and a map the value at each key it +# holds, with each operation of defined in the prelude, total +# as it is there, and lemmas for each. +# +# RUN: rm -rf %t && mkdir -p %t +# RUN: %cpp-verify --backend=lean --lean-project=%t/p %S/Inputs/lean_collections.cpp -- 2>&1 | FileCheck %s --check-prefix=EXPORT +# RUN: FileCheck %s --input-file=%t/p/CppVerify/Specs.lean --check-prefix=SPECS +# RUN: FileCheck %s --input-file=%t/p/CppVerify/Prelude.lean --check-prefix=PRELUDE +# RUN: cat %t/p/CppVerify/Goals/cppverify_sets_d46b53.lean %t/p/CppVerify/Goals/cppverify_multisets_68c3c1.lean %t/p/CppVerify/Goals/cppverify_maps_289bf4.lean %t/p/CppVerify/Goals/cppverify_sequences_b14fed.lean | FileCheck %s --check-prefix=GOALS +# +# Each step that revision 2 of cppverify_auto adds proves a goal here first: +# machine equalities as integers (x != y), a collection equality element by +# element, and a definition instance by the body it names. +# RUN: grep -h 'automation:' %t/p/CppVerify/Proofs/cppverify_sets_d46b53/cppverify_sets_d46b53_assertion_a932edd2.lean %t/p/CppVerify/Proofs/cppverify_multisets_68c3c1/cppverify_multisets_68c3c1_assertion_962b234c.lean %t/p/CppVerify/Proofs/cppverify_total_push_cdfd2d/cppverify_total_push_cdfd2d_postcondition_3a9fac6f.lean | FileCheck %s --check-prefix=STEPS +# +# cppverify_auto proves the goals over each kind, those that unfold a spec +# too. A proof by hand may use the lemmas, as this one of total_push does in +# place of the automatic one; the false claim stays unproved. +# RUN: cp %S/Inputs/lean_collections_total_push.lean %t/p/CppVerify/Proofs/cppverify_total_push_cdfd2d/cppverify_total_push_cdfd2d_postcondition_3a9fac6f.lean +# RUN: not %cpp-verify --backend=lean --lean-project=%t/p --lean-certify %S/Inputs/lean_collections.cpp -- 2>&1 | FileCheck %s --check-prefix=CERTIFY +# +# The scratch-pad states the same goals. +# RUN: %cpp-verify --backend=lean --lean-out=%t/scratch.lean %S/Inputs/lean_collections.cpp -- 2>&1 | FileCheck %s --check-prefix=SCRATCH +# RUN: FileCheck %s --input-file=%t/scratch.lean --check-prefix=SCRATCH-TEXT + +# EXPORT: Exported: decreases: total (cppverify_auto proved 1 of 1 goals) +# EXPORT-NEXT: Exported: lean obligation: sequences (cppverify_auto proved 5 of 5 goals) +# EXPORT-NEXT: Exported: lean obligation: sets (cppverify_auto proved 3 of 3 goals) +# EXPORT-NEXT: Exported: lean obligation: multisets (cppverify_auto proved 3 of 3 goals) +# EXPORT-NEXT: Exported: lean obligation: maps (cppverify_auto proved 3 of 3 goals) +# EXPORT-NEXT: Exported: lean obligation: total_push (cppverify_auto proved 2 of 2 goals) +# EXPORT-NEXT: Exported: lean obligation: wrong (cppverify_auto proved 1 of 2 goals) +# EXPORT-NEXT: Exported: decreases: members (cppverify_auto proved 1 of 1 goals) +# EXPORT-NEXT: Exported: lean obligation: no_members (cppverify_auto proved 2 of 2 goals) + +# STEPS: automation: cppverify_auto 3 proves it with cppverify_auto_bveq +# STEPS-NEXT: automation: cppverify_auto 3 proves it with cppverify_auto_ext +# STEPS-NEXT: automation: cppverify_auto 3 proves it with cppverify_auto_unfold + +# SPECS: /- spec function: total -/ +# SPECS-NEXT: {{^}}def cppSpec_fn_5f5a35746f74616c4e396370707665726966793373657145 (s : List Int) : Int :={{$}} +# SPECS: /- spec function: members -/ +# SPECS-NEXT: {{^}}def cppSpec_fn_5f5a376d656d626572734e39637070766572696679337365744569 (a : (Int → Bool)) (n : Int) : Int :={{$}} + +# PRELUDE: {{^}}def cppSeqAt (s : List Int) (i : Int) : Int :={{$}} +# PRELUDE-NEXT: {{^}} if i < 0 then 0 else s[i.toNat]?.getD 0{{$}} +# PRELUDE: {{^}}def cppSetContains (s : Int → Bool) (x : Int) : Prop := s x = true{{$}} +# PRELUDE: {{^}}def cppMultisetRemove (m : Int → Nat) (x : Int) : Int → Nat :={{$}} +# PRELUDE: {{^}}def cppMapGet (m : Int → Option Int) (k : Int) : Int := (m k).getD 0{{$}} +# PRELUDE: {{^}}theorem cppSeq_ext (s t : List Int) + +# GOALS: (a_0 : (Int → Bool)) +# GOALS: (cppSetSubset a_0 (cppSetUnion a_0 (cppSetInsert cppSetEmpty ((BitVec.signExtend 64 y_0)).toInt))) +# GOALS: (m_0 : (Int → Nat)) +# GOALS: (cppMultisetRemove (cppMultisetInsert m_0 ((BitVec.signExtend 64 x_0)).toInt) ((BitVec.signExtend 64 x_0)).toInt) = m_0 +# GOALS: (m_0 : (Int → Option Int)) +# GOALS: (cppMapGet (cppMapInsert m_0 +# GOALS: (s_0 : List Int) +# GOALS: (cppSeqContains (cppSeqConcat s_0 (cppSeqUnit ((BitVec.signExtend 64 x_0)).toInt)) ((BitVec.signExtend 64 x_0)).toInt) + +# CERTIFY: Certified: decreases: total [backend=Lean] +# CERTIFY-NEXT: Certified: sequences [backend=Lean] +# CERTIFY-NEXT: Certified: sets [backend=Lean] +# CERTIFY-NEXT: Certified: multisets [backend=Lean] +# CERTIFY-NEXT: Certified: maps [backend=Lean] +# CERTIFY-NEXT: Certified: total_push [backend=Lean] +# CERTIFY-NEXT: Unresolved: lean obligation: wrong (Lean certification failed: the proof of {{.*}}::assertion@48:3: {{.*}}declaration uses `sorry` +# CERTIFY-NEXT: Certified: decreases: members [backend=Lean] +# CERTIFY-NEXT: Certified: no_members [backend=Lean] + +# SCRATCH: Exported: lean obligation: sets +# SCRATCH-TEXT: {{^}}def cppSetInsert (s : Int → Bool) (x : Int) : Int → Bool :={{$}} +# SCRATCH-TEXT: (cppSetContains (cppSetRemove (cppSetInsert a_0 diff --git a/clang/test/Verify/suite/lean_division.test b/clang/test/Verify/suite/lean_division.test new file mode 100644 index 0000000000..cccf396085 --- /dev/null +++ b/clang/test/Verify/suite/lean_division.test @@ -0,0 +1,20 @@ +# REQUIRES: lean +# +# cppverify_auto reads C++ division and remainder by their definitions in +# its last step, so a contract that divides by a constant is proved in a +# project whose specs Lean does not define. +# +# RUN: rm -rf %t && mkdir -p %t +# RUN: %cpp-verify --backend=lean --lean-project=%t/p %S/Inputs/lean_division.cpp -- 2>&1 | FileCheck %s --check-prefix=EXPORT +# RUN: FileCheck %s --input-file=%t/p/CppVerify/Proofs/cppverify_half_bound_186b9f/cppverify_half_bound_186b9f_postcondition_40c34bea.lean --check-prefix=DIV +# RUN: FileCheck %s --input-file=%t/p/CppVerify/Proofs/cppverify_negative_division_a7749a/cppverify_negative_division_a7749a_postcondition_212234d0.lean --check-prefix=DIV +# RUN: %cpp-verify --backend=lean --lean-project=%t/p --lean-certify %S/Inputs/lean_division.cpp -- 2>&1 | FileCheck %s --check-prefix=CERTIFY + +# EXPORT: Exported: lean obligation: half_bound (cppverify_auto proved 3 of 3 goals) +# EXPORT-NEXT: Exported: lean obligation: negative_division (cppverify_auto proved 3 of 3 goals) + +# DIV: automation: cppverify_auto 3 proves it with cppverify_auto_div +# DIV: {{^}} cppverify_auto_div{{$}} + +# CERTIFY: Certified: half_bound [backend=Lean] +# CERTIFY-NEXT: Certified: negative_division [backend=Lean] diff --git a/clang/test/Verify/suite/lean_fallback.test b/clang/test/Verify/suite/lean_fallback.test index 1aed3ce551..1939ff8428 100644 --- a/clang/test/Verify/suite/lean_fallback.test +++ b/clang/test/Verify/suite/lean_fallback.test @@ -1,20 +1,22 @@ // REQUIRES: lean // +// The fallback runs without cppverify_auto, so that the goals wait for the +// proofs this test writes. // RUN: rm -rf %t.project -// RUN: not %cpp-verify --solver-rlimit=400 --lean-fallback-scope=all --lean-fallback=%t.project %S/spec_fuel_recursive_pass.cpp -- 2>&1 | FileCheck %s +// RUN: not %cpp-verify --solver-rlimit=400 --no-lean-auto --lean-fallback-scope=all --lean-fallback=%t.project %S/spec_fuel_recursive_pass.cpp -- 2>&1 | FileCheck %s // RUN: test -f %t.project/CppVerify/Generated.lean // RUN: test -f %t.project/CppVerify/Check.lean // RUN: grep -q '^import CppVerify.Proofs.cppverify_' %t.project/CppVerify/Check.lean -// RUN: FileCheck %s --input-file=%t.project/CppVerify/Generated.lean --check-prefix=GENERATED -// RUN: not %cpp-verify --solver-rlimit=400 --lean-fallback-scope=all --lean-fallback=%t.project --lean-certify %S/spec_fuel_recursive_pass.cpp -- 2>&1 | FileCheck %s --check-prefix=CERTIFY +// RUN: %python %S/Inputs/lean_project_text.py %t.project | FileCheck %s --check-prefix=GENERATED +// RUN: not %cpp-verify --solver-rlimit=400 --no-lean-auto --lean-fallback-scope=all --lean-fallback=%t.project --lean-certify %S/spec_fuel_recursive_pass.cpp -- 2>&1 | FileCheck %s --check-prefix=CERTIFY // RUN: cp %S/Inputs/lean_fallback_goal_1.lean %t.project/CppVerify/Proofs/cppverify_fibo_step_624f01/cppverify_fibo_step_624f01_missing_return_ba8abb90.lean // RUN: cp %S/Inputs/lean_fallback_goal_2.lean %t.project/CppVerify/Proofs/cppverify_fibo_step_624f01/cppverify_fibo_step_624f01_postcondition_559cb60a.lean // RUN: cp %S/Inputs/lean_fallback_goal_3.lean %t.project/CppVerify/Proofs/cppverify_fibo_six_ec829a/cppverify_fibo_six_ec829a_missing_return_ba8abb90.lean // RUN: cp %S/Inputs/lean_fallback_goal_4.lean %t.project/CppVerify/Proofs/cppverify_fibo_six_ec829a/cppverify_fibo_six_ec829a_postcondition_30cd1b97.lean -// RUN: %cpp-verify --solver-rlimit=400 --lean-fallback-scope=all --lean-fallback=%t.project --lean-certify %S/spec_fuel_recursive_pass.cpp -- 2>&1 | FileCheck %s --check-prefix=CERTIFIED -// RUN: %cpp-verify --solver-rlimit=400 --lean-fallback-scope=all --lean-fallback=%t.project --lean-certify --diagnostics-format=json %S/spec_fuel_recursive_pass.cpp -- 2>&1 | FileCheck %s --check-prefix=CERTIFIED-JSON +// RUN: %cpp-verify --solver-rlimit=400 --no-lean-auto --lean-fallback-scope=all --lean-fallback=%t.project --lean-certify %S/spec_fuel_recursive_pass.cpp -- 2>&1 | FileCheck %s --check-prefix=CERTIFIED +// RUN: %cpp-verify --solver-rlimit=400 --no-lean-auto --lean-fallback-scope=all --lean-fallback=%t.project --lean-certify --diagnostics-format=json %S/spec_fuel_recursive_pass.cpp -- 2>&1 | FileCheck %s --check-prefix=CERTIFIED-JSON // RUN: rm -rf %t.json-project -// RUN: not %cpp-verify --solver-rlimit=400 --lean-fallback-scope=all --lean-fallback=%t.json-project --lean-certify --diagnostics-format=json %S/spec_fuel_recursive_pass.cpp -- 2>&1 | FileCheck %s --check-prefix=FAILED-JSON +// RUN: not %cpp-verify --solver-rlimit=400 --no-lean-auto --lean-fallback-scope=all --lean-fallback=%t.json-project --lean-certify --diagnostics-format=json %S/spec_fuel_recursive_pass.cpp -- 2>&1 | FileCheck %s --check-prefix=FAILED-JSON // A small deterministic resource limit leaves both functions to Lean, and the // complete export exports every obligation, including the one Z3 proved. @@ -37,6 +39,6 @@ // FAILED-JSON-SAME: "reason":"lean.export-failed" // FAILED-JSON-SAME: "status":"unresolved" -// GENERATED: Generated by CppVerify for an editable Lean project. +// GENERATED: Generated by CppVerify. Regeneration replaces this file, never a proof. // GENERATED: function: fibo_step // GENERATED: function: fibo_six diff --git a/clang/test/Verify/suite/lean_identity.test b/clang/test/Verify/suite/lean_identity.test index d66b654f83..47ca665edc 100644 --- a/clang/test/Verify/suite/lean_identity.test +++ b/clang/test/Verify/suite/lean_identity.test @@ -26,7 +26,7 @@ # statement: every proof stays, and so do the spec bodies the proofs unfold. # RUN: %cpp-verify --backend=lean --lean-project=%t/p %t/-src/inserted.cpp -- 2>&1 | FileCheck %s --check-prefix=INSERTED --implicit-check-not=warning # RUN: grep -q 'cppSpecBody_6a2cf351_fn_5f5a3374726969_4,' %t/p/CppVerify/Proofs/cppverify_six_6a2cf3/cppverify_six_6a2cf3_postcondition_198bc076.lean -# RUN: grep -q '^def cppSpecBody_6a2cf351_fn_5f5a3374726969_4$' %t/p/CppVerify/Generated.lean +# RUN: grep -q '^def cppSpecBody_6a2cf351_fn_5f5a3374726969_4$' %t/p/CppVerify/Goals/cppverify_six_6a2cf3.lean # RUN: test -f %t/p/CppVerify/Proofs/cppverify_zero_5ece85/cppverify_zero_5ece85_postcondition_36923ed0.lean # # Renamed functions take their proofs along, each its own, with the names @@ -117,6 +117,7 @@ # HEADER-NEXT: bodies: cppSpecBody_6a2cf351_ # HEADER-NEXT: source: {{.*}}/ -src/base.cpp:8:37 # HEADER: -/ +# HEADER-NEXT: import CppVerify.Goals.cppverify_six_6a2cf3 # HEADER-NEXT: import CppVerify.User # HEADER-EMPTY: # HEADER-NEXT: theorem cppverify_six_6a2cf3_postcondition_198bc076_proof : diff --git a/clang/test/Verify/suite/lean_kernel.test b/clang/test/Verify/suite/lean_kernel.test index 15440d41e0..df0e028ac2 100644 --- a/clang/test/Verify/suite/lean_kernel.test +++ b/clang/test/Verify/suite/lean_kernel.test @@ -7,12 +7,13 @@ // RUN: lake --dir=%t.quant build CppVerify.Generated // RUN: %cpp-verify --backend=lean --lean-project=%t.recursive %S/spec_fuel_recursive_pass.cpp -- // RUN: lake --dir=%t.recursive build CppVerify.Generated -// RUN: %python -c "import os, sys; sys.exit(os.path.getsize(r'%t.recursive/CppVerify/Generated.lean') >= 100000)" +// RUN: %python %S/Inputs/lean_project_text.py %t.recursive > %t.recursive.lean +// RUN: %python -c "import os, sys; sys.exit(os.path.getsize(r'%t.recursive.lean') >= 100000)" // RUN: %cpp-verify --backend=lean --lean-project=%t.names %S/Inputs/lean_name_encoding.cpp -- -// RUN: grep -Eq 'cppEncoded_[0-9a-f]+' %t.names/CppVerify/Generated.lean +// RUN: %python %S/Inputs/lean_project_text.py %t.names | grep -Eq 'cppEncoded_[0-9a-f]+' // RUN: lake --dir=%t.names build CppVerify.Generated // RUN: lake --dir=%t.names build CppVerify.Check // Heap-reading specs take the heap state as a CppHeap argument. // RUN: %cpp-verify --check-ub --backend=lean --lean-project=%t.heapspec %S/heap_reading_spec.cpp -- -// RUN: grep -q '(__spec_heap : CppHeap)' %t.heapspec/CppVerify/Generated.lean +// RUN: %python %S/Inputs/lean_project_text.py %t.heapspec | grep -q '(__spec_heap : CppHeap)' // RUN: lake --dir=%t.heapspec build CppVerify.Generated diff --git a/clang/test/Verify/suite/lean_modules.test b/clang/test/Verify/suite/lean_modules.test new file mode 100644 index 0000000000..dc49ce3bbb --- /dev/null +++ b/clang/test/Verify/suite/lean_modules.test @@ -0,0 +1,123 @@ +# REQUIRES: lean +# +# A Lean project has a module of goals for each function, +# CppVerify/Goals/.lean, which imports CppVerify.Specs, where each spec +# the goals apply is declared once. A proof imports its function's goals, +# User, and Auto, so certification builds again only the proofs of a function +# whose goals changed. +# +# RUN: rm -rf %t && mkdir -p %t && cp %S/Inputs/lean_modules.cpp %t/m.cpp +# RUN: %cpp-verify --backend=lean --lean-project=%t/p %t/m.cpp -- 2>&1 | FileCheck %s --check-prefix=EXPORT +# RUN: FileCheck %s --input-file=%t/p/CppVerify/Generated.lean --check-prefix=GENERATED +# RUN: FileCheck %s --input-file=%t/p/CppVerify/Specs.lean --check-prefix=SPECS '--implicit-check-not=spec function:' +# RUN: FileCheck %s --input-file=%t/p/CppVerify/Goals/cppverify_tri_zero_e086e9.lean --check-prefix=GOALS +# RUN: FileCheck %s --input-file=%t/p/CppVerify/Goals/cppverify_plus_tri_zero_10bb8a.lean --check-prefix=GOALS-PLUS +# RUN: FileCheck %s --input-file=%t/p/CppVerify/Proofs/cppverify_inc_c0810e/cppverify_inc_c0810e_postcondition_e4ef4574.lean --check-prefix=IMPORTS +# RUN: FileCheck %s --input-file=%t/p/CppVerify/User.lean --check-prefix=USER +# RUN: FileCheck %s --input-file=%t/p/CppVerify.lean --check-prefix=ROOT +# +# The two goal modules that apply tri are certified together, with proofs +# by hand of their postconditions in place of the automatic ones. +# RUN: cp %S/Inputs/lean_modules_tri_zero.lean %t/p/CppVerify/Proofs/cppverify_tri_zero_e086e9/cppverify_tri_zero_e086e9_postcondition_44e49d34.lean +# RUN: cp %S/Inputs/lean_modules_plus_tri_zero.lean %t/p/CppVerify/Proofs/cppverify_plus_tri_zero_10bb8a/cppverify_plus_tri_zero_10bb8a_postcondition_8b45419c.lean +# RUN: %cpp-verify --backend=lean --lean-project=%t/p --lean-certify %t/m.cpp -- 2>&1 | FileCheck %s --check-prefix=CERTIFY +# +# A run of the same source writes no file. The two proofs by hand are not +# cppverify_auto's. +# RUN: touch %t/same +# RUN: %cpp-verify --backend=lean --lean-project=%t/p %t/m.cpp -- 2>&1 | FileCheck %s --check-prefix=EXPORT-AGAIN +# RUN: find %t/p -newer %t/same -type f -not -path '*/.lake/*' | count 0 +# +# A change to dbl builds dbl's goal module and proofs again, and the module +# that imports every goal module; nothing else. +# RUN: touch %t/before +# RUN: sed 's/cppverify::result == 2 \* x/cppverify::result == x + x/' %S/Inputs/lean_modules.cpp > %t/m.cpp +# RUN: %cpp-verify --backend=lean --lean-project=%t/p --lean-certify %t/m.cpp -- 2>&1 | FileCheck %s --check-prefix=CERTIFY +# RUN: cd %t/p/.lake/build/lib/lean && find CppVerify -name '*.olean' -newer %t/before | env LC_ALL=C sort | FileCheck %s --check-prefix=REBUILT --implicit-check-not=olean +# +# `lake build` builds every proof. Run in the project, elan selects its +# toolchain. +# RUN: cd %t/p && lake build 2>&1 | FileCheck %s --check-prefix=LAKE +# +# A function that is gone takes its goal module with it. +# RUN: sed -e 's/cppverify::result == 2 \* x/cppverify::result == x + x/' -e '/^int inc(/,/^}/d' %S/Inputs/lean_modules.cpp > %t/m.cpp +# RUN: %cpp-verify --backend=lean --lean-project=%t/p %t/m.cpp -- 2>&1 | FileCheck %s --check-prefix=WITHOUT-INC +# RUN: not test -e %t/p/CppVerify/Goals/cppverify_inc_c0810e.lean +# +# A project of an earlier release, whose User imports Generated, still +# certifies: each proof then depends on every goal module. +# RUN: printf 'import CppVerify.Generated\n' > %t/p/CppVerify/User.lean +# RUN: %cpp-verify --backend=lean --lean-project=%t/p --lean-certify %t/m.cpp -- 2>&1 | FileCheck %s --check-prefix=OLD-USER +# +# A fallback run writes the goal modules of the functions it exports and no +# other, since Specs then declares only the specs those apply. Here the +# solver verifies every function: no goal module stays, and every proof does. +# RUN: %cpp-verify --lean-fallback=%t/p %t/m.cpp -- 2>&1 | FileCheck %s --check-prefix=FALLBACK +# RUN: not test -e %t/p/CppVerify/Goals/cppverify_tri_zero_e086e9.lean +# RUN: test -f %t/p/CppVerify/Proofs/cppverify_tri_zero_e086e9/cppverify_tri_zero_e086e9_postcondition_44e49d34.lean +# RUN: cd %t/p && lake build 2>&1 | FileCheck %s --check-prefix=LAKE + +# EXPORT: Exported: decreases: tri (cppverify_auto proved 1 of 1 goals) +# EXPORT-NEXT: Exported: lean obligation: inc (cppverify_auto proved 3 of 3 goals) +# EXPORT-NEXT: Exported: lean obligation: dbl (cppverify_auto proved 3 of 3 goals) +# EXPORT-NEXT: Exported: lean obligation: tri_zero (cppverify_auto proved 2 of 2 goals) +# EXPORT-NEXT: Exported: lean obligation: plus_tri_zero (cppverify_auto proved 2 of 2 goals) + +# EXPORT-AGAIN: Exported: lean obligation: tri_zero (cppverify_auto proved 1 of 2 goals) +# EXPORT-AGAIN-NEXT: Exported: lean obligation: plus_tri_zero (cppverify_auto proved 1 of 2 goals) + +# GENERATED: {{^}}import CppVerify.Prelude{{$}} +# GENERATED-NEXT: {{^}}import CppVerify.Specs{{$}} +# GENERATED-NEXT: {{^}}import CppVerify.Goals.cppverify_tri_decreases_f55d4c{{$}} +# GENERATED-NEXT: {{^}}import CppVerify.Goals.cppverify_inc_c0810e{{$}} +# GENERATED-NEXT: {{^}}import CppVerify.Goals.cppverify_dbl_47613e{{$}} +# GENERATED-NEXT: {{^}}import CppVerify.Goals.cppverify_tri_zero_e086e9{{$}} +# GENERATED-NEXT: {{^}}import CppVerify.Goals.cppverify_plus_tri_zero_10bb8a{{$}} +# GENERATED-EMPTY: + +# SPECS: {{^}}import CppVerify.Prelude{{$}} +# SPECS: /- spec function: tri -/ +# SPECS-NEXT: {{^}}def cppSpec_fn_5f5a3374726969 (n : Int) : Int :={{$}} + +# Each goal module defines the spec bodies it uses under its own prefix. +# GOALS: {{^}}import CppVerify.Specs{{$}} +# GOALS-NOT: {{^}}import +# GOALS: {{^}}attribute [local instance] cppVerifyPropDecidable{{$}} +# GOALS: {{^}}def cppSpecBody_e086e9ab_fn_5f5a3374726969_1 +# GOALS-PLUS: {{^}}import CppVerify.Specs{{$}} +# GOALS-PLUS: {{^}}def cppSpecBody_10bb8a86_fn_5f5a3374726969_1 + +# IMPORTS: {{^}}import CppVerify.Goals.cppverify_inc_c0810e{{$}} +# IMPORTS-NEXT: {{^}}import CppVerify.User{{$}} +# IMPORTS-NEXT: {{^}}import CppVerify.Auto{{$}} +# IMPORTS-NOT: import + +# USER: {{^}}import CppVerify.Specs{{$}} +# ROOT: {{^}}import CppVerify.Check{{$}} + +# CERTIFY: Certified: decreases: tri [backend=Lean] +# CERTIFY-NEXT: Certified: inc [backend=Lean] +# CERTIFY-NEXT: Certified: dbl [backend=Lean] +# CERTIFY-NEXT: Certified: tri_zero [backend=Lean] +# CERTIFY-NEXT: Certified: plus_tri_zero [backend=Lean] + +# REBUILT: CppVerify/Generated.olean +# REBUILT-NEXT: CppVerify/Goals/cppverify_dbl_47613e.olean +# REBUILT-NEXT: CppVerify/Proofs/cppverify_dbl_47613e/cppverify_dbl_47613e_missing_return_54ae7c80.olean +# REBUILT-NEXT: CppVerify/Proofs/cppverify_dbl_47613e/cppverify_dbl_47613e_overflow_fce34d13.olean +# REBUILT-NEXT: CppVerify/Proofs/cppverify_dbl_47613e/cppverify_dbl_47613e_postcondition_ce469baa.olean + +# LAKE: Build completed successfully + +# WITHOUT-INC: Exported: decreases: tri +# WITHOUT-INC-NEXT: Exported: lean obligation: dbl +# WITHOUT-INC-NEXT: Exported: lean obligation: tri_zero + +# OLD-USER: Certified: decreases: tri [backend=Lean] +# OLD-USER-NEXT: Certified: dbl [backend=Lean] +# OLD-USER-NEXT: Certified: tri_zero [backend=Lean] +# OLD-USER-NEXT: Certified: plus_tri_zero [backend=Lean] + +# FALLBACK: Verified: dbl [backend=z3] +# FALLBACK-NEXT: Verified: tri_zero [backend=z3] +# FALLBACK-NEXT: Verified: plus_tri_zero [backend=z3] diff --git a/clang/test/Verify/suite/lean_project.cpp b/clang/test/Verify/suite/lean_project.cpp index 51f4a7c9c6..5561998437 100644 --- a/clang/test/Verify/suite/lean_project.cpp +++ b/clang/test/Verify/suite/lean_project.cpp @@ -7,7 +7,7 @@ // RUN: test -f %t.project/CppVerify/Check.lean // RUN: test -f %t.project/CppVerify/Proofs/cppverify_trivial_3e4f5a/cppverify_trivial_3e4f5a_missing_return_ba8abb90.lean // RUN: grep -Fxq 'leanprover/lean4:v4.35.0' %t.project/lean-toolchain -// RUN: FileCheck %s --input-file=%t.project/CppVerify/Generated.lean --check-prefix=GENERATED +// RUN: %python %S/Inputs/lean_project_text.py %t.project | FileCheck %s --check-prefix=GENERATED // RUN: grep -Fq 'import CppVerify.Proofs.cppverify_trivial_3e4f5a.cppverify_trivial_3e4f5a_missing_return_ba8abb90' %t.project/CppVerify/Check.lean // RUN: echo '/- USER_KEEP -/' >> %t.project/CppVerify/User.lean // RUN: echo '/- PROOF_KEEP -/' >> %t.project/CppVerify/Proofs/cppverify_trivial_3e4f5a/cppverify_trivial_3e4f5a_missing_return_ba8abb90.lean @@ -30,7 +30,7 @@ void trivial() {} // EXPORT: Exported: lean obligation: trivial -// GENERATED: Generated by CppVerify for an editable Lean project. +// GENERATED: Generated by CppVerify. Regeneration replaces this file, never a proof. // GENERATED-NOT: unchecked Lean scratch-pad // GENERATED: function: trivial // GENERATED: source: {{.*}}lean_project.cpp:{{[0-9]+:[0-9]+}} diff --git a/clang/test/Verify/suite/lean_readable.cpp b/clang/test/Verify/suite/lean_readable.cpp index 8227146516..16bd3f0f2b 100644 --- a/clang/test/Verify/suite/lean_readable.cpp +++ b/clang/test/Verify/suite/lean_readable.cpp @@ -7,7 +7,7 @@ // // RUN: rm -rf %t && mkdir -p %t // RUN: %cpp-verify --backend=lean --no-lean-auto --lean-project=%t/p %s -- 2>&1 | FileCheck %s --check-prefix=EXPORT -// RUN: FileCheck %s --input-file=%t/p/CppVerify/Generated.lean +// RUN: %python %S/Inputs/lean_project_text.py %t/p | FileCheck %s // RUN: FileCheck %s --input-file=%t/p/CppVerify/Proofs/cppverify_twice_edf933/cppverify_twice_edf933_postcondition_28de726e.lean --check-prefix=STUB // RUN: FileCheck %s --input-file=%t/p/CppVerify/Proofs/cppverify_poly_c94108/cppverify_poly_c94108_postcondition_fde367c1.lean --check-prefix=LETS // Every stub's names match its readable form: the project builds, with @@ -21,7 +21,7 @@ // // A readable form that says less than its goal does not build: here the // definition of b is replaced by the conclusion it should lead to. -// RUN: sed -i 's/(h_b_1 : b_1 = __local_call_2_1)/(h_b_1 : b_1 = x_0 + (BitVec.ofInt 32 (2)))/' %t/p/CppVerify/Generated.lean +// RUN: sed -i 's/(h_b_1 : b_1 = __local_call_2_1)/(h_b_1 : b_1 = x_0 + (BitVec.ofInt 32 (2)))/' %t/p/CppVerify/Goals/cppverify_twice_edf933.lean // RUN: cd %t/p && not lake build CppVerify.Generated 2>&1 | FileCheck %s --check-prefix=BRIDGE int next(int x) @@ -222,4 +222,4 @@ int poly(int x) // TRUSTED: Exported: lean obligation: twice // CERTIFIED: Certified: twice [backend=Lean] [trusts=next] -// BRIDGE: Generated.lean:{{[0-9]+}}:{{[0-9]+}}: Application type mismatch: The argument +// BRIDGE: cppverify_twice_edf933.lean:{{[0-9]+}}:{{[0-9]+}}: Application type mismatch: The argument diff --git a/clang/test/Verify/suite/lean_refutation.test b/clang/test/Verify/suite/lean_refutation.test new file mode 100644 index 0000000000..9d98556ddd --- /dev/null +++ b/clang/test/Verify/suite/lean_refutation.test @@ -0,0 +1,89 @@ +# REQUIRES: lean +# +# A fallback project shows each certified counterexample in Lean too: its +# refutation states the failing goal as the project would and proves it false +# at the model's values, and certification checks it like a proof. A +# counterexample Lean cannot decide stays a failure, with a warning; Lean +# deciding the goal true would be a disagreement, never a failure. +# +# RUN: rm -rf %t && mkdir -p %t && cp %S/Inputs/lean_refutation.cpp %t/r.cpp +# RUN: not %cpp-verify --lean-fallback=%t/p %t/r.cpp -- 2>&1 | FileCheck %s --check-prefix=EXPORT +# RUN: FileCheck %s --input-file=%t/p/CppVerify/Refutations/cppverify_add_one_fc20c3/cppverify_add_one_fc20c3_postcondition_1b36ffe1.lean --check-prefix=REFUTATION +# RUN: not %cpp-verify --lean-fallback=%t/p --lean-certify %t/r.cpp -- 2>&1 | FileCheck %s --check-prefix=CERTIFY +# RUN: not %cpp-verify --lean-fallback=%t/p --lean-certify --diagnostics-format=json %t/r.cpp -- 2>&1 | FileCheck %s --check-prefix=JSON +# +# The checker takes a refutation only when its theorem states the negation +# of the goal its module defines. +# RUN: mkdir -p %t/p/CppVerify/Refutations/cppverify_fake_000000 %t/p/.lake/build/lib/lean/CppVerify/Refutations/cppverify_fake_000000 +# RUN: printf 'def cppverify_fake_000000_goal : Prop := True\ntheorem cppverify_fake_000000_goal_refuted : True := trivial\n' > %t/p/CppVerify/Refutations/cppverify_fake_000000/cppverify_fake_000000_goal.lean +# RUN: cd %t/p && lake env lean -R %t/p -o .lake/build/lib/lean/CppVerify/Refutations/cppverify_fake_000000/cppverify_fake_000000_goal.olean CppVerify/Refutations/cppverify_fake_000000/cppverify_fake_000000_goal.lean +# RUN: printf 'cppverify_fake_000000_goal\tCppVerify.Refutations.cppverify_fake_000000.cppverify_fake_000000_goal\tCppVerify.Refutations.cppverify_fake_000000.cppverify_fake_000000_goal\trefutation\n' > %t/goals +# RUN: cd %t/p && lake env lean --run .cppverify/Certify.lean %t/goals propext,Quot.sound,Classical.choice | FileCheck %s --check-prefix=CHECKER +# +# A failure that is gone takes its refutation with it, as does a module of +# no failure. +# RUN: sed -i 's/return x + 2;/return x + 1;/' %t/r.cpp +# RUN: not %cpp-verify --lean-fallback=%t/p %t/r.cpp -- 2>&1 | FileCheck %s --check-prefix=FIXED +# RUN: not test -e %t/p/CppVerify/Refutations/cppverify_add_one_fc20c3 +# RUN: not test -e %t/p/CppVerify/Refutations/cppverify_fake_000000 +# RUN: test -f %t/p/CppVerify/Refutations/cppverify_tri_three_55daf7/cppverify_tri_three_55daf7_postcondition_ffa2cd20.lean +# +# The tactic says why it cannot refute: Lean decides the instance true, or +# cannot decide it. +# +# Each attempt of the tactic has the heartbeats of a step of cppverify_auto, +# so a refutation Lean could only decide by trying a billion integers ends; +# and a counterexample that is not a failure once the verdicts settle reports +# nothing of its refutation. +# RUN: cd %t/p && not lake env lean %S/Inputs/lean_refutation_tactic.lean 2>&1 | FileCheck %s --check-prefix=TACTIC +# RUN: not %cpp-verify --lean-fallback=%t/b --lean-certify %S/Inputs/lean_refutation_budget.cpp -- 2>&1 | FileCheck %s --check-prefix=BUDGET + +# EXPORT: error: verification failed: add_one +# EXPORT-NOT: confirmed in Lean + +# The goal's definitions are elaborated without a heartbeat limit; the +# theorem keeps Lean's, which also bounds the kernel's check of its proof. +# REFUTATION: {{^}}import CppVerify.Specs{{$}} +# REFUTATION-NOT: maxHeartbeats +# REFUTATION: {{^}}section Goal{{$}} +# REFUTATION-EMPTY: +# REFUTATION-NEXT: {{^}}set_option maxHeartbeats 0{{$}} +# REFUTATION-NOT: maxHeartbeats +# REFUTATION: {{^}}def cppverify_add_one_fc20c3_postcondition_1b36ffe1 : Prop :={{$}} +# REFUTATION-NOT: maxHeartbeats +# REFUTATION: {{^}}end Goal{{$}} +# REFUTATION-NOT: maxHeartbeats +# REFUTATION: {{^}}theorem cppverify_add_one_fc20c3_postcondition_1b36ffe1_refuted : ¬ cppverify_add_one_fc20c3_postcondition_1b36ffe1 := by{{$}} +# REFUTATION-NEXT: {{^}} intro h{{$}} +# REFUTATION-NEXT: {{^}} have h₁ := h{{$}} +# REFUTATION-NEXT: {{^}} (fun (a : Int) => (0 : Int)){{$}} +# REFUTATION-NEXT: {{^}} (fun (a : Int) => (0 : Int)){{$}} +# REFUTATION-NEXT: {{^}} (BitVec.ofInt 32 ({{-?[0-9]+}})){{$}} +# REFUTATION-NEXT: {{^}} (BitVec.ofInt 32 ({{-?[0-9]+}})){{$}} +# REFUTATION-NEXT: {{^}} cppverify_refute h₁{{$}} + +# A contract, a bounded quantifier, a store to memory, and a spec Lean +# defines; the choice is opaque in Lean. +# CERTIFY: error: verification failed: add_one {{.*}}[reason=counterexample] (confirmed in Lean) +# CERTIFY-NEXT: error: verification failed: below_three {{.*}}[reason=counterexample] (confirmed in Lean) +# CERTIFY-NEXT: error: verification failed: zero_first {{.*}}[reason=counterexample] (confirmed in Lean) +# CERTIFY-NEXT: Verified: spec decreases: tri +# CERTIFY-NEXT: error: verification failed: tri_three {{.*}}[reason=counterexample] (confirmed in Lean) +# CERTIFY-NEXT: error: verification failed: pick_one {{.*}}[reason=counterexample]{{$}} +# CERTIFY-NEXT: warning: the counterexample of pick_one is not confirmed in Lean: Lean cannot decide the goal at the counterexample + +# JSON: {"backend":"z3","function":"add_one","lean_refuted":true, +# JSON: {"message":"the counterexample of pick_one is not confirmed in Lean: Lean cannot decide the goal at the counterexample","schema":"cppverify.diagnostic/1","severity":"warning","status":"warning"} + +# CHECKER: fail cppverify_fake_000000_goal_refuted cppverify_fake_000000_goal_refuted proves True, not Not cppverify_fake_000000_goal + +# FIXED: Verified: add_one [backend=z3] + +# TACTIC: lean_refutation_tactic.lean:7:2: error: cppverify: Lean decides the goal true at the counterexample +# TACTIC: lean_refutation_tactic.lean:16:2: error: cppverify: Lean cannot decide the goal at the counterexample + +# BUDGET: error: verification failed: below_a_thousand {{.*}}[reason=counterexample] (confirmed in Lean) +# BUDGET: error: verification failed: below_a_billion {{.*}}[reason=counterexample]{{$}} +# BUDGET: Unresolved: verification unresolved: far {{.*}}[reason=spec.post] (its counterexample is checked with the postcondition of ge, which is not established){{$}} +# BUDGET: warning: the counterexample of below_a_billion is not confirmed in Lean: Lean cannot decide the goal at the counterexample +# BUDGET-NOT: the counterexample of far diff --git a/clang/test/Verify/suite/lean_refutation_memory.test b/clang/test/Verify/suite/lean_refutation_memory.test new file mode 100644 index 0000000000..f29653a6d5 --- /dev/null +++ b/clang/test/Verify/suite/lean_refutation_memory.test @@ -0,0 +1,13 @@ +# REQUIRES: lean, system-linux +# +# Lean decides a quantifier over a range of integers by trying each one, and +# each integer is a level of recursion whose memory heartbeats do not bound: +# refuting below_a_billion took 58 GB before Lean gave up. The prelude tries +# each integer only for a range of at most 4096, so Lean gives up at once. +# +# RUN: rm -rf %t && mkdir -p %t +# RUN: not %python %S/Inputs/peak_memory.py 4096 %cpp-verify --lean-fallback=%t/b --lean-certify %S/Inputs/lean_refutation_budget.cpp -- 2>&1 | FileCheck %s + +# CHECK: error: verification failed: below_a_thousand {{.*}}[reason=counterexample] (confirmed in Lean) +# CHECK: warning: the counterexample of below_a_billion is not confirmed in Lean: Lean cannot decide the goal at the counterexample +# CHECK: peak memory below 4096 MB diff --git a/clang/test/Verify/suite/lean_replay.test b/clang/test/Verify/suite/lean_replay.test new file mode 100644 index 0000000000..6b3b9484c1 --- /dev/null +++ b/clang/test/Verify/suite/lean_replay.test @@ -0,0 +1,99 @@ +# REQUIRES: lean +# +# An archive replays into a Lean project as its source exports: the same +# goals, specs, and proof files, so a proof written for the source certifies +# the archive's goal. Replay checks each module alone, as with a solver. +# +# RUN: rm -rf %t && mkdir -p %t +# RUN: %cpp-verify --lower-only --obligation-out=%t/a.cpv %S/Inputs/lean_replay.cpp -- +# RUN: %cpp-verify --backend=lean --lean-project=%t/src %S/Inputs/lean_replay.cpp -- 2>&1 | FileCheck %s --check-prefix=SOURCE +# RUN: %cpp-verify --obligation-in=%t/a.cpv --lean-project=%t/rep 2>&1 | FileCheck %s --check-prefix=REPLAY +# RUN: diff -r %t/src/CppVerify/Goals %t/rep/CppVerify/Goals +# RUN: diff %t/src/CppVerify/Specs.lean %t/rep/CppVerify/Specs.lean +# RUN: diff -r %t/src/CppVerify/Proofs %t/rep/CppVerify/Proofs +# RUN: FileCheck %s --input-file=%t/rep/CppVerify/Specs.lean --check-prefix=SPECS +# RUN: FileCheck %s --input-file=%t/rep/CppVerify/Goals/cppverify_inc_c0810e.lean --check-prefix=READABLE +# +# RUN: cp %S/Inputs/lean_spec_definitions_tri_nonneg.lean %t/rep/CppVerify/Proofs/cppverify_tri_nonneg_42a030/cppverify_tri_nonneg_42a030_postcondition_053b1980.lean +# RUN: %cpp-verify --obligation-in=%t/a.cpv --lean-project=%t/rep --lean-certify 2>&1 | FileCheck %s --check-prefix=CERTIFY +# RUN: %cpp-verify --obligation-in=%t/a.cpv --lean-project=%t/rep --diagnostics-format=json | FileCheck %s --check-prefix=JSON +# +# A record of schema 2 has no measures, and no names for a goal's +# assumptions: tri is opaque, and a readable goal names no hypothesis. +# RUN: %python %S/Inputs/mutate_obligation_archive.py schema-v2 %t/a.cpv %t/v2.cpv +# RUN: %cpp-verify --obligation-in=%t/v2.cpv --lean-project=%t/v2 2>&1 | FileCheck %s --check-prefix=REPLAY +# RUN: FileCheck %s --input-file=%t/v2/CppVerify/Specs.lean --check-prefix=SPECS-V2 +# RUN: FileCheck %s --input-file=%t/v2/CppVerify/Goals/cppverify_inc_c0810e.lean --check-prefix=READABLE-V2 +# +# A bounded module states its bound with its goals. Every goal of sum_small +# certifies, its unwinding included; count_up's unwinding does not, since +# its loop may run longer, and the rest shows it safe within the bound. +# RUN: not %cpp-verify --backend=bmc --unroll=3 --obligation-out=%t/b.cpv %S/Inputs/lean_replay_loops.cpp -- +# RUN: %cpp-verify --obligation-in=%t/b.cpv --backend=lean --lean-out=%t/b.lean 2>&1 | FileCheck %s --check-prefix=BOUNDED-EXPORT +# RUN: FileCheck %s --input-file=%t/b.lean --check-prefix=BOUND +# RUN: not %cpp-verify --obligation-in=%t/b.cpv --lean-project=%t/b --lean-certify 2>&1 | FileCheck %s --check-prefix=BOUNDED +# +# A replay moves and keeps proofs as a source run does: the proof of +# tri_nonneg, which no goal of the bounded archive has, is kept in Stale/. +# RUN: %cpp-verify --obligation-in=%t/b.cpv --lean-project=%t/rep 2>&1 | FileCheck %s --check-prefix=STALE +# RUN: test -f %t/rep/Stale/Proofs/cppverify_tri_nonneg_42a030/cppverify_tri_nonneg_42a030_postcondition_053b1980.lean +# Its goal back, it takes the proof back, which a JSON run says as JSON. +# RUN: %cpp-verify --obligation-in=%t/a.cpv --lean-project=%t/rep --diagnostics-format=json | FileCheck %s --check-prefix=BACK +# +# RUN: not %cpp-verify --obligation-in=%t/a.cpv --lean-certify 2>&1 | FileCheck %s --check-prefix=NO-PROJECT +# RUN: not %cpp-verify --obligation-in=%t/a.cpv --lean-fallback=%t/f 2>&1 | FileCheck %s --check-prefix=NO-FALLBACK +# RUN: not %cpp-verify --obligation-in=%t/a.cpv --lean-project=%t/x --lean-out=%t/x.lean 2>&1 | FileCheck %s --check-prefix=BOTH +# RUN: not %cpp-verify --obligation-in=%t/a.cpv --backend=z3 --lean-project=%t/x 2>&1 | FileCheck %s --check-prefix=Z3 + +# SOURCE: Exported: decreases: tri (cppverify_auto proved 1 of 1 goals) +# SOURCE-NEXT: Exported: lean obligation: tri_nonneg (cppverify_auto proved 1 of 2 goals) +# SOURCE: Exported: decreases: down (cppverify_auto proved 1 of 1 goals) +# SOURCE-NEXT: Exported: lean obligation: down_zero (cppverify_auto proved 2 of 2 goals) +# SOURCE-NEXT: Exported: lean obligation: inc (cppverify_auto proved 3 of 3 goals) + +# REPLAY: Exported: lean obligation: tri.decreases (cppverify_auto proved 1 of 1 goals) [backend=lean] [semantic-hash=sha256:{{[0-9a-f]+}}] +# REPLAY-NEXT: Exported: lean obligation: tri_nonneg (cppverify_auto proved 1 of 2 goals) [backend=lean] +# REPLAY-NEXT: Exported: lean obligation: down.decreases (cppverify_auto proved 1 of 1 goals) [backend=lean] +# REPLAY-NEXT: Exported: lean obligation: down_zero (cppverify_auto proved 2 of 2 goals) [backend=lean] +# REPLAY-NEXT: Exported: lean obligation: inc (cppverify_auto proved 3 of 3 goals) [backend=lean] + +# The archive keeps tri's measure, and, for down's, the spec it applies. +# SPECS: {{^}}def cppSpec_fn_5f5a3374726969 (n : Int) : Int :={{$}} +# SPECS: {{^}}termination_by ((n) + 1).toNat{{$}} +# SPECS: {{^}}def cppSpec_fn_5f5a3473697a6569 (n : Int) : Int :={{$}} + +# READABLE: (h_pre_1 : (cppBvSlt x_0 (BitVec.ofInt 32 (100)))) → + +# CERTIFY: Proved obligations: tri.decreases (dependencies not checked in replay) [backend=lean] +# CERTIFY-NEXT: Proved obligations: tri_nonneg (dependencies not checked in replay) [backend=lean] +# CERTIFY-NEXT: Proved obligations: down.decreases (dependencies not checked in replay) [backend=lean] +# CERTIFY-NEXT: Proved obligations: down_zero (dependencies not checked in replay) [backend=lean] +# CERTIFY-NEXT: Proved obligations: inc (dependencies not checked in replay) [backend=lean] + +# JSON: {"backend":"lean","function":"tri.decreases","lean_automatic":1,{{.*}}"status":"exported"} + +# SPECS-V2: {{^}}opaque cppSpec_fn_5f5a3374726969 : Int -> Int{{$}} +# READABLE-V2-NOT: h_pre + +# BOUNDED-EXPORT: Exported: lean obligation: sum_small [backend=lean, bound=2] +# BOUNDED-EXPORT-NEXT: Exported: lean obligation: count_up [backend=lean, bound=3] + +# BOUND: /- function: sum_small -/ +# BOUND: /- bound: 2 loop iterations, by unrolling: these goals speak only of the +# BOUND: kind: unwinding +# BOUND: /- function: count_up -/ +# BOUND: /- bound: 3 loop iterations, by unrolling: these goals speak only of the + +# BOUNDED: Proved obligations: sum_small (dependencies not checked in replay) [backend=lean, bound=2] +# BOUNDED-NEXT: lean_replay_loops.cpp:19:3: BoundedSafe: count_up (every goal is certified but the unwinding: no execution within 3 loop iterations fails (the proof of fn_5f5a38636f756e745f757069::unwinding@19:3: {{.*}}) [backend=lean, bound=3] [reason=bmc.incomplete-bound] + +# STALE: warning: the Lean proof CppVerify/Proofs/cppverify_tri_nonneg_42a030/cppverify_tri_nonneg_42a030_postcondition_053b1980.lean is kept in Stale/Proofs/cppverify_tri_nonneg_42a030/cppverify_tri_nonneg_42a030_postcondition_053b1980.lean: its function is gone, and no goal has its statement +# STALE-NEXT: Exported: lean obligation: sum_small (cppverify_auto proved 7 of 7 goals) [backend=lean, bound=2] + +# BACK: {"message":"the Lean proof of fn_5f5a31307472695f6e6f6e6e656769::postcondition@{{[0-9]+}}:{{[0-9]+}} came back from Stale/Proofs/cppverify_tri_nonneg_42a030/cppverify_tri_nonneg_42a030_postcondition_053b1980.lean to CppVerify/Proofs/cppverify_tri_nonneg_42a030/cppverify_tri_nonneg_42a030_postcondition_053b1980.lean, the file of the goal with its statement","schema":"cppverify.diagnostic/1","severity":"warning","status":"warning"} +# BACK-NEXT: {"backend":"lean","function":"tri.decreases",{{.*}}"status":"exported"} + +# NO-PROJECT: error: --lean-certify, --lean-prune-stale, and --no-lean-auto require --lean-project +# NO-FALLBACK: error: --lean-fallback applies to source verification; archive replay writes a Lean project with --lean-project +# BOTH: error: --lean-project cannot be combined with --lean-out +# Z3: error: --lean-project requires --backend=lean diff --git a/clang/test/Verify/suite/lean_semantics.cpp b/clang/test/Verify/suite/lean_semantics.cpp index dcafd92436..5ec309a04d 100644 --- a/clang/test/Verify/suite/lean_semantics.cpp +++ b/clang/test/Verify/suite/lean_semantics.cpp @@ -9,8 +9,8 @@ // // RUN: rm -rf %t.export %t.mix %t.numerals %t.call // RUN: %cpp-verify --backend=lean --lean-project=%t.export %s -- 2>&1 | FileCheck %s --check-prefix=EXPORT -// RUN: FileCheck %s --input-file=%t.export/CppVerify/Generated.lean --check-prefix=LEAN -// RUN: not grep -E 'BitVec\.(udiv|sdiv) ' %t.export/CppVerify/Generated.lean +// RUN: %python %S/Inputs/lean_project_text.py %t.export | FileCheck %s --check-prefix=LEAN +// RUN: %python %S/Inputs/lean_project_text.py %t.export | not grep -E 'BitVec\.(udiv|sdiv) ' // RUN: lake --dir=%t.export build CppVerify.Generated // // The prelude's values where C++, SMT-LIB, and Lean's own operators differ. diff --git a/clang/test/Verify/suite/lean_spec_definitions.test b/clang/test/Verify/suite/lean_spec_definitions.test new file mode 100644 index 0000000000..cb1c1582ed --- /dev/null +++ b/clang/test/Verify/suite/lean_spec_definitions.test @@ -0,0 +1,84 @@ +# REQUIRES: lean +# +# A spec function of a Lean project is a definition when Lean proves that it +# terminates by its decreases measures, and opaque otherwise. Its goals state +# the same definition instances as before; a proof may also unfold the +# definition, or prove by induction over it. +# +# RUN: rm -rf %t && mkdir -p %t +# RUN: %cpp-verify --backend=lean --lean-project=%t/p %S/Inputs/lean_spec_definitions.cpp -- 2>&1 | FileCheck %s --check-prefix=EXPORT +# RUN: FileCheck %s --input-file=%t/p/CppVerify/Specs.lean --check-prefix=SPECS +# RUN: FileCheck %s --input-file=%t/p/CppVerify/Specs.lean --check-prefix=REACHES +# +# Lean's verdict on each spec is kept: a run of the same source tries +# nothing and writes no file. +# RUN: touch %t/same +# RUN: %cpp-verify --backend=lean --lean-project=%t/p %S/Inputs/lean_spec_definitions.cpp -- 2>&1 | FileCheck %s --check-prefix=EXPORT +# RUN: find %t/p -newer %t/same -type f -not -path '*/.lake/*' | count 0 +# +# A proof by induction over tri, which an opaque tri did not allow. +# RUN: cp %S/Inputs/lean_spec_definitions_tri_nonneg.lean %t/p/CppVerify/Proofs/cppverify_tri_nonneg_42a030/cppverify_tri_nonneg_42a030_postcondition_053b1980.lean +# RUN: not %cpp-verify --backend=lean --lean-project=%t/p --lean-certify %S/Inputs/lean_spec_definitions.cpp -- 2>&1 | FileCheck %s --check-prefix=CERTIFY +# +# Without Lean nothing is tried, and every spec is opaque. +# RUN: mkdir -p %t/empty +# RUN: env PATH=%t/empty %cpp-verify --backend=lean --lean-project=%t/q %S/Inputs/lean_spec_definitions.cpp -- 2>&1 | FileCheck %s --check-prefix=WITHOUT +# RUN: FileCheck %s --input-file=%t/q/CppVerify/Specs.lean --check-prefix=OPAQUE --implicit-check-not=def + +# EXPORT: Exported: decreases: tri +# EXPORT: Exported: lean obligation: tri_nonneg (cppverify_auto proved 1 of 2 goals) +# EXPORT-NEXT: Exported: lean obligation: tri_four (cppverify_auto proved 2 of 2 goals) +# EXPORT-NEXT: Exported: lean obligation: even_four (cppverify_auto proved 2 of 2 goals) +# EXPORT-NEXT: Exported: lean obligation: small_ack (cppverify_auto proved 2 of 2 goals) +# EXPORT-NEXT: Exported: lean obligation: log2_eight (cppverify_auto proved 2 of 2 goals) +# EXPORT: Exported: lean obligation: down_zero (cppverify_auto proved 2 of 2 goals) +# EXPORT: Exported: lean obligation: reaches_zero (cppverify_auto proved 2 of 2 goals) + +# Callees come first. steps, whose termination rests on the postcondition +# of half, stays opaque, and count_steps, which calls it, is defined against +# it. A lexicographic measure is a tuple; a call under `||` or `&&` sees its +# condition; a mutual recursion is one block; down's call has the measure -1 +# where its caller's is 0; log2's measure falls by C++ division. +# SPECS: {{^}}def cppSpec_fn_5f5a3468616c6669 (n : Int) : Int :={{$}} +# SPECS: {{^}}opaque cppSpec_fn_5f5a35737465707369 : Int -> Int{{$}} +# SPECS: {{^}}def cppSpec_fn_5f5a3131636f756e745f737465707369 (n : Int) : Int :={{$}} +# SPECS: {{^}}def cppSpec_fn_5f5a3361636b6969 (m : Int) (n : Int) : Int :={{$}} +# SPECS: {{^}}termination_by (((m) + 1).toNat, ((n) + 1).toNat){{$}} +# SPECS: {{^}}mutual{{$}} +# SPECS: {{^}}def cppSpec_fn_5f5a336f646469 (n : Int) : Prop :={{$}} +# SPECS-NEXT: {{^}} if _h : n > (0 : Int) then (cppSpec_fn_5f5a346576656e69 (n - (1 : Int))) else False{{$}} +# SPECS: {{^}}def cppSpec_fn_5f5a346576656e69 (n : Int) : Prop :={{$}} +# SPECS-NEXT: {{^}} if _h : n ≤ (0 : Int) then True else (cppSpec_fn_5f5a336f646469 (n - (1 : Int))){{$}} +# SPECS: {{^}}end{{$}} +# SPECS: {{^}}def cppSpec_fn_5f5a3374726969 (n : Int) : Int :={{$}} +# SPECS-NEXT: {{^}} if _h : n ≤ (0 : Int) then (0 : Int) else (cppSpec_fn_5f5a3374726969 (n - (1 : Int))) + n{{$}} +# SPECS-NEXT: {{^}}termination_by ((n) + 1).toNat{{$}} +# SPECS-NEXT: {{^}}decreasing_by all_goals cppverify_decreasing{{$}} +# SPECS: {{^}}def cppSpec_fn_5f5a34646f776e69 (n : Int) : Int :={{$}} +# SPECS: {{^}}def cppSpec_fn_5f5a346c6f673269 (n : Int) : Int :={{$}} +# SPECS-NEXT: {{^}} if _h : n ≤ (1 : Int) then (0 : Int) else (cppSpec_fn_5f5a346c6f673269 (cppIntDiv n (2 : Int))) + (1 : Int){{$}} + +# CERTIFY: Certified: decreases: tri [backend=Lean] +# CERTIFY-NEXT: Certified: decreases: even [backend=Lean] +# CERTIFY-NEXT: Certified: decreases: odd [backend=Lean] +# CERTIFY-NEXT: Certified: decreases: ack [backend=Lean] +# CERTIFY: Certified: decreases: log2 [backend=Lean] +# CERTIFY: Certified: tri_nonneg [backend=Lean] +# CERTIFY-NEXT: Certified: tri_four [backend=Lean] +# CERTIFY-NEXT: Certified: even_four [backend=Lean] +# CERTIFY-NEXT: Certified: small_ack [backend=Lean] +# CERTIFY-NEXT: Certified: log2_eight [backend=Lean] +# A verdict still rests on the termination cpp-verify establishes: that of +# half is not, so neither are those that apply count_steps. +# CERTIFY: Certified: decreases: down [backend=Lean] +# CERTIFY-NEXT: Unresolved: decreases: count_steps [backend=Lean] [reason=spec.termination] +# CERTIFY-NEXT: Unresolved: down_zero [backend=Lean] [reason=spec.termination] + +# A call under an existential sees its range. +# REACHES: {{^}}def cppSpec_fn_5f5a377265616368657369 (n : Int) : Prop :={{$}} +# REACHES-NEXT: {{^}} if _h : n ≤ (0 : Int) then True else (∃ ([[K:[_a-z0-9]+]] : Int), if _h : (0 : Int) ≤ [[K]] ∧ [[K]] < n then (cppSpec_fn_5f5a377265616368657369 [[K]]) else False){{$}} + +# WITHOUT: Exported: lean obligation: tri_four{{$}} + +# OPAQUE: {{^}}opaque cppSpec_fn_5f5a3361636b6969 : Int -> Int -> Int{{$}} +# OPAQUE: {{^}}opaque cppSpec_fn_5f5a3374726969 : Int -> Int{{$}} diff --git a/clang/test/Verify/suite/lean_toolchain.test b/clang/test/Verify/suite/lean_toolchain.test index 306038ccee..8973e1f6a2 100644 --- a/clang/test/Verify/suite/lean_toolchain.test +++ b/clang/test/Verify/suite/lean_toolchain.test @@ -3,7 +3,7 @@ # outside elan must report the pinned Lean. # # UNSUPPORTED: system-windows -# RUN: rm -rf %t %t.project && mkdir -p %t/elan %t/plain +# RUN: rm -rf %t %t.project %t.replay && mkdir -p %t/elan %t/plain # Without automation at export, the goal is new to cppverify_auto whether or # not Lean is installed, so the certification run below tries it first. # RUN: %cpp-verify --backend=lean --no-lean-auto --lean-project=%t.project %S/Inputs/lean_toolchain.cpp -- @@ -14,15 +14,22 @@ # RUN: not env PATH=%t/elan %cpp-verify --backend=lean --lean-project=%t.project --lean-certify %S/Inputs/lean_toolchain.cpp -- 2>&1 | FileCheck %s --check-prefix=ELAN # RUN: FileCheck %s --input-file=%t/elan.log --check-prefix=ELAN-LOG # +# Replaying an archive certifies with the same steps; the failure is a +# verdict, which an assertions build once turned into an abort (an Expected +# tested again after its error was taken). +# RUN: %cpp-verify --obligation-out=%t/a.cpv %S/Inputs/lean_toolchain.cpp -- > /dev/null +# RUN: not env PATH=%t/elan %cpp-verify --obligation-in=%t/a.cpv --lean-project=%t.replay --lean-certify 2>&1 | FileCheck %s --check-prefix=REPLAY +# # RUN: printf '#!/bin/sh\necho "Lake version 5.0.0 (Lean version 4.1.0)"\n' > %t/plain/lake # RUN: chmod +x %t/plain/lake # RUN: not env PATH=%t/plain %cpp-verify --backend=lean --lean-project=%t.project --lean-certify %S/Inputs/lean_toolchain.cpp -- 2>&1 | FileCheck %s --check-prefix=PLAIN -DDIR=%t/plain # ELAN: Unresolved: lean obligation: trivial (Lean certification failed: command failed with exit code 1 +# REPLAY: Unresolved: trivial (Lean certification failed: command failed with exit code 1 # The trial of cppverify_auto, then certification, each with the pin. # ELAN-LOG: {{^}}+leanprover/lean4:v4.35.0 --version{{$}} -# ELAN-LOG-NEXT: {{^}}+leanprover/lean4:v4.35.0 --dir={{.*}}.project build CppVerify.User CppVerify.Auto{{$}} +# ELAN-LOG-NEXT: {{^}}+leanprover/lean4:v4.35.0 --dir={{.*}}.project build CppVerify.Generated CppVerify.User CppVerify.Auto{{$}} # ELAN-LOG-NEXT: {{^}}+leanprover/lean4:v4.35.0 --version{{$}} # ELAN-LOG-NEXT: {{^}}+leanprover/lean4:v4.35.0 --dir={{.*}}.project build CppVerify.Generated CppVerify.User CppVerify.Auto{{$}} diff --git a/clang/test/Verify/suite/obligation_archive.test b/clang/test/Verify/suite/obligation_archive.test index 9939b4d523..61e78e97d0 100644 --- a/clang/test/Verify/suite/obligation_archive.test +++ b/clang/test/Verify/suite/obligation_archive.test @@ -15,12 +15,18 @@ # RUN: %python %S/Inputs/mutate_obligation_archive.py truncated %t.one %t.truncated # RUN: not %cpp-verify --obligation-in=%t.truncated 2>&1 | FileCheck %s --check-prefix=TRUNCATED # RUN: cp %t.one %t.version -# RUN: printf '\003\000\000\000' | dd of=%t.version bs=1 seek=8 conv=notrunc 2>/dev/null +# RUN: printf '\004\000\000\000' | dd of=%t.version bs=1 seek=8 conv=notrunc 2>/dev/null # RUN: not %cpp-verify --obligation-in=%t.version 2>&1 | FileCheck %s --check-prefix=VERSION # RUN: %python %S/Inputs/mutate_obligation_archive.py schema-v1 %t.one %t.schema-v1 # RUN: not %cpp-verify --obligation-in=%t.schema-v1 2>&1 | FileCheck %s --check-prefix=SCHEMA-V1 # RUN: %python %S/Inputs/mutate_obligation_archive.py schema-v1-kinds %t.one %t.schema-v1-kinds # RUN: %cpp-verify --obligation-in=%t.schema-v1-kinds --backend=z3 | FileCheck %s --check-prefix=SCHEMA-V1-KINDS +# RUN: %python %S/Inputs/mutate_obligation_archive.py schema-v2 %t.one %t.schema-v2 +# RUN: %cpp-verify --obligation-in=%t.schema-v2 --backend=z3 | FileCheck %s --check-prefix=SCHEMA-V2 +# A measure is read like a definition, over the function's parameters. +# RUN: %cpp-verify --lower-only --obligation-out=%t.measures %S/Inputs/lean_replay.cpp -- +# RUN: %python %S/Inputs/mutate_obligation_archive.py measure-free-variable %t.measures %t.measure-variable +# RUN: not %cpp-verify --obligation-in=%t.measure-variable 2>&1 | FileCheck %s --check-prefix=MEASURE # RUN: %python %S/Inputs/mutate_obligation_archive.py invalid-kind %t.one %t.invalid-kind # RUN: not %cpp-verify --obligation-in=%t.invalid-kind 2>&1 | FileCheck %s --check-prefix=KIND # RUN: %python %S/Inputs/mutate_obligation_archive.py feature-mismatch %t.one %t.features @@ -93,7 +99,7 @@ # REPLAY: Proved obligations: canonical_obligations (dependencies not checked in replay) [backend=z3] [semantic-hash=sha256:{{[0-9a-f]+}}] # LOWER: vc canonical_obligations -# LOWER: schema cppverify.obligation/2 +# LOWER: schema cppverify.obligation/3 # LOWER: (let # LOWER: Lowered: canonical_obligations [backend=z3] [semantic-hash=sha256:{{[0-9a-f]+}}] # LEAN: Exported: lean obligation: canonical_obligations [backend=lean] @@ -104,6 +110,8 @@ # VERSION: error: invalid obligation archive: unsupported obligation archive version # SCHEMA-V1: error: invalid obligation archive: invalid obligation kind in obligation archive # SCHEMA-V1-KINDS: Proved obligations: canonical_obligations (dependencies not checked in replay) [backend=z3] +# SCHEMA-V2: Proved obligations: canonical_obligations (dependencies not checked in replay) [backend=z3] +# MEASURE: error: invalid obligation archive: logical function definition has a free variable: z # KIND: error: invalid obligation archive: invalid expression kind in obligation archive # FEATURES: error: invalid obligation archive: obligation archive feature declaration does not match contents # DUPLICATE-ID: error: invalid obligation archive: obligation module has an empty or duplicate obligation identity diff --git a/clang/test/Verify/suite/obligation_ir.cpp b/clang/test/Verify/suite/obligation_ir.cpp index 1545435386..f8f2fddeab 100644 --- a/clang/test/Verify/suite/obligation_ir.cpp +++ b/clang/test/Verify/suite/obligation_ir.cpp @@ -26,7 +26,7 @@ int canonical_obligations(int x) } // OBLIGATION-LABEL: vc canonical_obligations -// OBLIGATION-NEXT: schema cppverify.obligation/2 +// OBLIGATION-NEXT: schema cppverify.obligation/3 // OBLIGATION-NEXT: simplification nodes {{[1-9][0-9]*}} -> {{[1-9][0-9]*}}, rewrites {{[1-9][0-9]*}}, functions-removed 0 // OBLIGATION-NEXT: semantic-hash sha256:f6d09cd4e2ac5c541fa8e0e443f4c2981e5763ce9b4d94d69f9856eaa1e95f2e // OBLIGATION-NEXT: identity [[IDENTITY:fn_[0-9a-f]+]] diff --git a/clang/test/Verify/suite/per_function_errors.test b/clang/test/Verify/suite/per_function_errors.test new file mode 100644 index 0000000000..eb04759f06 --- /dev/null +++ b/clang/test/Verify/suite/per_function_errors.test @@ -0,0 +1,51 @@ +# A construct the verifier does not represent costs its function alone. The +# function is reported where the construct is, and its JSON record carries its +# verdict, Unresolved with construct.unsupported; every other function is +# verified. A caller relies on the function's contract, which nothing +# establishes (callee.contract); a call of a function whose contract did not +# convert is unsupported where it is; a function that reaches a spec the +# verifier does not represent is not verified. +# +# RUN: not %cpp-verify %S/Inputs/per_function_errors.cpp -- 2>&1 \ +# RUN: | FileCheck %s --check-prefix=TEXT +# RUN: not %cpp-verify --diagnostics-format=json %S/Inputs/per_function_errors.cpp -- 2>/dev/null \ +# RUN: | FileCheck %s --check-prefix=JSON +# +# The same with BMC, and in a Lean export, which exports what it can. +# RUN: not %cpp-verify --backend=bmc --unroll=1 %S/Inputs/per_function_errors.cpp -- 2>&1 \ +# RUN: | FileCheck %s --check-prefix=BMC +# RUN: not %cpp-verify --backend=lean --lean-out=%t.lean %S/Inputs/per_function_errors.cpp -- 2>&1 \ +# RUN: | FileCheck %s --check-prefix=LEAN + +TEXT: per_function_errors.cpp:11:3: error: pick: unsupported statement: SwitchStmt +TEXT: per_function_errors.cpp:27:37: error: lookup: the global array 'table' is unsupported +TEXT: per_function_errors.cpp:41:3: error: choice: unsupported statement: SwitchStmt +TEXT: per_function_errors.cpp:64:3: error: zero_lemma: unsupported statement: SwitchStmt +TEXT: per_function_errors.cpp:79:3: error: step: unsupported statement: SwitchStmt +TEXT-NOT: {{(Verified|Unresolved): (spec [a-z]+: )?(pick|lookup|choice|zero_lemma|step) }} +TEXT: Unresolved: uses_pick [backend=z3] [reason=callee.contract] (relies on the contract of pick (it uses a construct the verifier does not support), which is not established) +TEXT-NEXT: per_function_errors.cpp:36:3: Unresolved: uses_lookup [backend=z3] [reason=construct.unsupported] (the verifier cannot model a construct here: a call of lookup, whose contract is not available) +TEXT-NEXT: per_function_errors.cpp:49:5: Unresolved: uses_choice [reason=construct.unsupported] (relies on choice, which uses a construct the verifier does not support) +TEXT-NEXT: per_function_errors.cpp:55:23: Unresolved: choice_lemma [reason=construct.unsupported] (relies on choice, which uses a construct the verifier does not support) +TEXT-NEXT: Unresolved: uses_zero_lemma [backend=z3] [reason=callee.contract] (relies on the contract of zero_lemma (it uses a construct the verifier does not support), which is not established) +TEXT-NEXT: per_function_errors.cpp:87:5: Unresolved: uses_step [reason=construct.unsupported] (relies on step, which uses a construct the verifier does not support) +TEXT-NEXT: Verified: inc [backend=z3] +TEXT-NEXT: Verified: spec axiom: twice +TEXT-NEXT: Verified: uses_twice [backend=z3] +TEXT-NOT: {{.}} + +JSON: {"function":"pick","message":"pick: unsupported statement: SwitchStmt","reason":"construct.unsupported",{{.*}}"line":11},"status":"unresolved"} +JSON: {"function":"lookup","message":"lookup: the global array 'table' is unsupported","reason":"construct.unsupported",{{.*}}"status":"unresolved"} +JSON-NEXT: {"function":"lookup","message":"lookup: unsupported expression in pre",{{.*}}"status":"error"} +JSON: {"backend":"z3","function":"uses_pick",{{.*}}"reason":"callee.contract",{{.*}}"status":"unresolved"} +JSON: {"backend":"z3","function":"inc",{{.*}}"status":"verified"} + +BMC: Unresolved: uses_pick [backend=bmc, bound=0] [reason=callee.contract] +BMC: Verified: inc [backend=bmc, bound=0] +BMC: Verified: uses_twice [backend=bmc, bound=0] + +LEAN: per_function_errors.cpp:11:3: error: pick: unsupported statement: SwitchStmt +LEAN: Exported: lean obligation: uses_pick +LEAN: per_function_errors.cpp:49:5: Unresolved: uses_choice [reason=construct.unsupported] +LEAN: Exported: lean obligation: inc +LEAN: Exported: lean obligation: uses_twice diff --git a/clang/tools/cpp-verify/Main.cpp b/clang/tools/cpp-verify/Main.cpp index 7c27e2f10d..4b99460ea2 100644 --- a/clang/tools/cpp-verify/Main.cpp +++ b/clang/tools/cpp-verify/Main.cpp @@ -325,6 +325,20 @@ lowerForBackend(const verify::ObligationModule &Module, return verify::lowerObligationModule(Module, Z3Out, Execution); } +// The options of Lean output and Lean projects, which source verification +// and archive replay share. +static void setLeanOptions(verify::VerifyOptions &VOpts) { + VOpts.LeanOutPath = LeanOut.getValue(); + VOpts.LeanProjectPath = LeanProject.getValue(); + VOpts.LeanFallbackProjectPath = LeanFallback.getValue(); + VOpts.LeanScope = LeanFallbackScope.getValue(); + VOpts.LeanCertify = LeanCertify.getValue(); + VOpts.LeanPruneStale = LeanPruneStale.getValue(); + VOpts.LeanAuto = !NoLeanAuto.getValue(); + VOpts.LeanAutoTimeoutMs = LeanAutoTimeout.getValue(); + VOpts.LeanJobs = Jobs.getNumOccurrences() ? Jobs.getValue() : 0; +} + class VerifyConsumer : public ASTConsumer { public: void HandleTranslationUnit(ASTContext &Ctx) override { @@ -346,15 +360,7 @@ class VerifyConsumer : public ASTConsumer { VOpts.Backend = verify::BackendKind::Race; else VOpts.Backend = verify::BackendKind::Z3; - VOpts.LeanOutPath = LeanOut.getValue(); - VOpts.LeanProjectPath = LeanProject.getValue(); - VOpts.LeanFallbackProjectPath = LeanFallback.getValue(); - VOpts.LeanScope = LeanFallbackScope.getValue(); - VOpts.LeanCertify = LeanCertify.getValue(); - VOpts.LeanPruneStale = LeanPruneStale.getValue(); - VOpts.LeanAuto = !NoLeanAuto.getValue(); - VOpts.LeanAutoTimeoutMs = LeanAutoTimeout.getValue(); - VOpts.LeanJobs = Jobs.getNumOccurrences() ? Jobs.getValue() : 0; + setLeanOptions(VOpts); VOpts.BMCUnroll = BMCUnroll.getValue(); VOpts.SolverTimeoutMs = SolverTimeout.getValue(); VOpts.CollectionTimeoutMs = collectionTimeoutMs(); @@ -465,7 +471,8 @@ replaySourceJSON(const verify::ObligationSource &Source) { static void printReplayJSON(const verify::ObligationModule &Module, const verify::VerifyResult &Result, - llvm::StringRef SemanticHash) { + llvm::StringRef SemanticHash, + unsigned LeanAutomatic) { llvm::json::Object Record; Record["schema"] = "cppverify.diagnostic/1"; Record["status"] = replayStatusCode(Result.Status); @@ -486,6 +493,8 @@ static void printReplayJSON(const verify::ObligationModule &Module, Record["reason"] = verify::verifyReasonCode(Result.Reason); if (Result.Bound) Record["bound"] = static_cast(*Result.Bound); + if (Result.Status == verify::VerifyStatus::Exported && LeanAutomatic) + Record["lean_automatic"] = static_cast(LeanAutomatic); if (!Result.ExploredBounds.empty()) { llvm::json::Array Bounds; for (unsigned Bound : Result.ExploredBounds) @@ -632,16 +641,31 @@ static int replayObligationArchive() { "exclusive\n"; return 1; } - if (!LeanProject.empty() || !LeanFallback.empty() || LeanCertify || - LeanPruneStale || NoLeanAuto || LeanAutoTimeout.getNumOccurrences()) { - llvm::errs() << "error: archive replay supports Lean scratch-pad export " - "with --lean-out, not Lean project generation or " - "certification\n"; + if (!LeanFallback.empty()) { + llvm::errs() << "error: --lean-fallback applies to source verification; " + "archive replay writes a Lean project with " + "--lean-project\n"; return 1; } - if (BackendOpt == "lean" && (jobs() != 1 || !ProofCache.empty())) { - llvm::errs() - << "error: --jobs and --proof-cache are not supported by Lean replay\n"; + if (LeanProject.empty() && (LeanCertify || LeanPruneStale || NoLeanAuto)) { + llvm::errs() << "error: --lean-certify, --lean-prune-stale, and " + "--no-lean-auto require --lean-project\n"; + return 1; + } + if (!LeanProject.empty() && BackendOpt != "lean") { + llvm::errs() << "error: --lean-project requires --backend=lean\n"; + return 1; + } + if (!LeanProject.empty() && (!LeanOut.empty() || LowerOnly)) { + llvm::errs() << "error: --lean-project cannot be combined with " + << (LowerOnly ? "--lower-only" : "--lean-out") << "\n"; + return 1; + } + // A project's Lean processes run at once; the scratch-pad is one file. + if (BackendOpt == "lean" && + ((LeanProject.empty() && jobs() != 1) || !ProofCache.empty())) { + llvm::errs() << "error: --proof-cache is not supported by Lean replay, " + "nor --jobs by its scratch-pad\n"; return 1; } if (BackendOpt == "cvc5" && !ProofCache.empty()) { @@ -691,11 +715,6 @@ static int replayObligationArchive() { "before obligation lowering\n"; return 1; } - if (BackendOpt == "lean" && Module.BMCTransform) { - llvm::errs() << "error: Lean scratch export cannot replay a " - "BMC-transformed obligation archive\n"; - return 1; - } if ((BackendOpt == "cvc5" || BackendOpt == "portfolio" || BackendOpt == "race") && Module.BMCTransform) { @@ -718,10 +737,10 @@ static int replayObligationArchive() { std::unique_ptr LeanFile; llvm::raw_ostream *LeanStream = nullptr; - if (BackendOpt == "lean") { + if (BackendOpt == "lean" && LeanProject.empty()) { if (LeanOut.empty()) { - llvm::errs() - << "error: --backend=lean archive replay requires --lean-out\n"; + llvm::errs() << "error: --backend=lean archive replay requires " + "--lean-out or --lean-project\n"; return 1; } std::error_code EC; @@ -750,17 +769,47 @@ static int replayObligationArchive() { const unsigned DumpLayers = DumpIR.getNumOccurrences() > 0 ? verify::parseDumpIRLayers(DumpIR.getValue()) : 0; + // A Lean project takes every module before any is certified. + std::optional Project; + if (!LeanProject.empty() && !verify::interruptRequested()) { + verify::VerifyOptions VOpts; + VOpts.Backend = verify::BackendKind::Lean; + setLeanOptions(VOpts); + Project = verify::replayLeanProject(Modules, VOpts); + for (const std::string &Warning : Project->Warnings) { + if (DiagnosticsFormat != verify::DiagnosticFormat::Json) { + llvm::outs() << "warning: " << Warning << "\n"; + continue; + } + llvm::json::Object Record; + Record["schema"] = "cppverify.diagnostic/1"; + Record["status"] = "warning"; + Record["severity"] = "warning"; + Record["message"] = jsonText(Warning); + llvm::outs() << llvm::formatv("{0}\n", + llvm::json::Value(std::move(Record))); + } + if (!Project->Error.empty()) { + llvm::errs() << "error: " << Project->Error << "\n"; + return 1; + } + } bool AllOk = true; - for (const verify::ObligationModule &Module : Modules) { + for (size_t Index = 0; Index != Modules.size(); ++Index) { + const verify::ObligationModule &Module = Modules[Index]; if (DumpLayers & verify::LayerVC) verify::dumpVC(Module, llvm::outs()); verify::VerifyResult Result; - if (LowerOnly) { + const unsigned LeanAutomatic = Project ? Project->Automatic[Index] : 0; + if (Project) { + Result = Project->Results[Index]; + } else if (LowerOnly) { Result = lowerForBackend( Module, Kind, DumpLayers & verify::LayerZ3 ? &llvm::outs() : nullptr); } else if (!verify::interruptRequested()) { - if (Module.BMCTransform) { + // Lean exports a bounded module, its bound stated with its goals. + if (Module.BMCTransform && Kind != verify::BackendKind::Lean) { std::unique_ptr BMCBackend = verify::createVerifyBackend(verify::BackendKind::BMC, nullptr, Module.BMCTransform->UnrollBound, @@ -768,6 +817,8 @@ static int replayObligationArchive() { Result = BMCBackend->verify(Module); } else { Result = Backend->verify(Module); + if (Module.BMCTransform) + Result.Bound = Module.BMCTransform->UnrollBound; } } // After Ctrl-C no result is final, whatever the queries returned. @@ -779,7 +830,7 @@ static int replayObligationArchive() { const std::string Hash = verify::obligationSemanticHash(Module); if (DiagnosticsFormat == verify::DiagnosticFormat::Json) { - printReplayJSON(Module, Result, Hash); + printReplayJSON(Module, Result, Hash, LeanAutomatic); if (Result.Status != verify::VerifyStatus::Lowered && Result.Status != verify::VerifyStatus::Verified && Result.Status != verify::VerifyStatus::Exported) @@ -833,8 +884,14 @@ static int replayObligationArchive() { << ReplayProofNote << ")" << Suffix << "\n"; break; case verify::VerifyStatus::Exported: - llvm::outs() << "Exported: lean obligation: " << Module.FunctionName - << Suffix << "\n"; + llvm::outs() << "Exported: lean obligation: " << Module.FunctionName; + if (LeanAutomatic) + llvm::outs() << " (cppverify_auto proved " << LeanAutomatic << " of " + << std::set(Result.LeanGoals.begin(), + Result.LeanGoals.end()) + .size() + << " goals)"; + llvm::outs() << Suffix << "\n"; break; case verify::VerifyStatus::Failed: AllOk = false; diff --git a/clang/unittests/Verify/CertifyTest.cpp b/clang/unittests/Verify/CertifyTest.cpp index 5a076f1329..9769016bcf 100644 --- a/clang/unittests/Verify/CertifyTest.cpp +++ b/clang/unittests/Verify/CertifyTest.cpp @@ -102,6 +102,7 @@ Expr clone(const LogicExpr &E) { Copy->Binder = E.Binder; Copy->OverflowOp = E.OverflowOp; Copy->SpecCallee = E.SpecCallee; + Copy->CollectionOp = E.CollectionOp; for (const auto &Child : E.Children) Copy->Children.push_back(clone(*Child)); return Copy; @@ -1936,6 +1937,213 @@ TEST(CertifyTest, AgreesWithLeanEvaluation) { "rem" + Operands); } + // Collections: what reads a random chain of operations gives, and when two + // sequences are equal. Lean decides each with the prelude's definitions. + using CollectionOp = LogicCollectionOp; + auto collection = [](CollectionOp Op, LogicSort Sort, auto... Operands) { + Expr E = node(LogicExpr::Collection, Sort, std::move(Operands)...); + E->CollectionOp = Op; + return E; + }; + const LogicSort Seq = LogicSort::collection(LogicSortKind::Seq); + const LogicSort Set = LogicSort::collection(LogicSortKind::Set); + const LogicSort Multiset = LogicSort::collection(LogicSortKind::Multiset); + const LogicSort Map = LogicSort::collection(LogicSortKind::Map); + auto small = [&] { + return mathLiteral(static_cast(Random() % 8) - 2); + }; + std::function sequence = [&](unsigned Depth) -> Expr { + switch (Depth == 0 ? Random() % 2 : Random() % 5) { + case 0: + return collection(CollectionOp::SeqEmpty, Seq); + case 1: + return collection(CollectionOp::SeqUnit, Seq, small()); + case 2: + return collection(CollectionOp::SeqPush, Seq, sequence(Depth - 1), + small()); + case 3: + return collection(CollectionOp::SeqConcat, Seq, sequence(Depth - 1), + sequence(Depth - 1)); + default: + return collection(CollectionOp::SeqSubrange, Seq, sequence(Depth - 1), + small(), small()); + } + }; + std::function set = [&](unsigned Depth) -> Expr { + const CollectionOp Combined[] = {CollectionOp::SetUnion, + CollectionOp::SetIntersect, + CollectionOp::SetDifference}; + switch (Depth == 0 ? 0 : Random() % 4) { + case 0: + return collection(CollectionOp::SetEmpty, Set); + case 1: + return collection(CollectionOp::SetInsert, Set, set(Depth - 1), small()); + case 2: + return collection(CollectionOp::SetRemove, Set, set(Depth - 1), small()); + default: + return collection(Combined[Random() % 3], Set, set(Depth - 1), + set(Depth - 1)); + } + }; + std::function multiset = [&](unsigned Depth) -> Expr { + if (Depth == 0) + return collection(CollectionOp::MultisetEmpty, Multiset); + return collection(Random() % 3 == 0 ? CollectionOp::MultisetRemove + : CollectionOp::MultisetInsert, + Multiset, multiset(Depth - 1), small()); + }; + std::function map = [&](unsigned Depth) -> Expr { + if (Depth == 0) + return collection(CollectionOp::MapEmpty, Map); + if (Random() % 3 == 0) + return collection(CollectionOp::MapRemove, Map, map(Depth - 1), small()); + return collection(CollectionOp::MapInsert, Map, map(Depth - 1), small(), + small()); + }; + for (unsigned I = 0; I != 60; ++I) { + const std::string Name = "collection " + std::to_string(I); + switch (I % 6) { + case 0: + evaluated(collection(CollectionOp::SeqLength, math(), sequence(3)), Name); + break; + case 1: + evaluated( + collection(CollectionOp::SeqIndex, math(), sequence(3), small()), + Name); + break; + case 2: + evaluated(collection(CollectionOp::SeqContains, LogicSort::boolSort(), + sequence(3), small()), + Name); + break; + case 3: + evaluated(collection(CollectionOp::SetContains, LogicSort::boolSort(), + set(4), small()), + Name); + break; + case 4: + evaluated( + collection(CollectionOp::MultisetCount, math(), multiset(5), small()), + Name); + break; + default: + evaluated(collection(I % 12 == 5 ? CollectionOp::MapGet + : CollectionOp::MapContains, + I % 12 == 5 ? math() : LogicSort::boolSort(), map(4), + small()), + Name); + break; + } + } + for (unsigned I = 0; I != 20; ++I) + evaluated(boolean(LogicExpr::Eq, sequence(2), sequence(2)), + "sequence equality " + std::to_string(I)); + // Each operation at operands where it matters: reads in and out of range, + // every clamp of a subrange, an element inserted and removed, each + // combination of membership, a count removed that is there and one that + // is not, and a key written over and removed. + auto number = [](int64_t Value) { return mathLiteral(Value); }; + auto list = [&](std::initializer_list Elements) { + Expr S = collection(CollectionOp::SeqEmpty, Seq); + for (int64_t Element : Elements) + S = collection(CollectionOp::SeqPush, Seq, std::move(S), number(Element)); + return S; + }; + auto read = [&](CollectionOp Op, LogicSort Sort, Expr Of, int64_t At, + const std::string &Name) { + evaluated(collection(Op, Sort, std::move(Of), number(At)), Name); + }; + for (int64_t I = -1; I <= 3; ++I) + read(CollectionOp::SeqIndex, math(), list({1, 2, 3}), I, + "index " + std::to_string(I)); + for (int64_t X = 0; X <= 3; ++X) + read(CollectionOp::SeqContains, LogicSort::boolSort(), list({1, 2, 3}), X, + "sequence contains " + std::to_string(X)); + evaluated(collection(CollectionOp::SeqLength, math(), + collection(CollectionOp::SeqUnit, Seq, number(5))), + "unit length"); + for (int64_t I = 0; I <= 2; ++I) + read(CollectionOp::SeqIndex, math(), + collection(CollectionOp::SeqConcat, Seq, list({1}), list({2})), I, + "concat index " + std::to_string(I)); + for (int64_t Lo = -1; Lo <= 4; ++Lo) + for (int64_t Hi = -1; Hi <= 4; ++Hi) { + auto subrange = [&] { + return collection(CollectionOp::SeqSubrange, Seq, list({1, 2, 3}), + number(Lo), number(Hi)); + }; + const std::string Name = + "subrange " + std::to_string(Lo) + " " + std::to_string(Hi); + evaluated(collection(CollectionOp::SeqLength, math(), subrange()), + Name + " length"); + for (int64_t I = 0; I <= 2; ++I) + read(CollectionOp::SeqIndex, math(), subrange(), I, + Name + " index " + std::to_string(I)); + } + auto members = [&](std::initializer_list Elements) { + Expr S = collection(CollectionOp::SetEmpty, Set); + for (int64_t Element : Elements) + S = collection(CollectionOp::SetInsert, Set, std::move(S), + number(Element)); + return S; + }; + for (int64_t X = 1; X <= 2; ++X) { + read(CollectionOp::SetContains, LogicSort::boolSort(), members({1}), X, + "set insert " + std::to_string(X)); + read(CollectionOp::SetContains, LogicSort::boolSort(), + collection(CollectionOp::SetRemove, Set, members({1, 2}), number(1)), + X, "set remove " + std::to_string(X)); + } + for (CollectionOp Op : {CollectionOp::SetUnion, CollectionOp::SetIntersect, + CollectionOp::SetDifference}) + for (int64_t X = 1; X <= 4; ++X) + read(CollectionOp::SetContains, LogicSort::boolSort(), + collection(Op, Set, members({1, 2}), members({2, 3})), X, + std::string(logicCollectionOpName(Op)) + " " + std::to_string(X)); + auto counts = [&](std::initializer_list Elements) { + Expr M = collection(CollectionOp::MultisetEmpty, Multiset); + for (int64_t Element : Elements) + M = collection(CollectionOp::MultisetInsert, Multiset, std::move(M), + number(Element)); + return M; + }; + for (int64_t X = 3; X <= 4; ++X) { + read(CollectionOp::MultisetCount, math(), counts({3, 3}), X, + "count " + std::to_string(X)); + for (std::initializer_list Before : + {std::initializer_list{}, std::initializer_list{3}, + std::initializer_list{3, 3}}) + read(CollectionOp::MultisetCount, math(), + collection(CollectionOp::MultisetRemove, Multiset, counts(Before), + number(3)), + X, + "count after remove of " + std::to_string(Before.size()) + " at " + + std::to_string(X)); + } + auto entries = [&] { + return collection(CollectionOp::MapInsert, Map, + collection(CollectionOp::MapInsert, Map, + collection(CollectionOp::MapEmpty, Map), + number(1), number(10)), + number(2), number(20)); + }; + for (int64_t K = 1; K <= 3; ++K) { + const std::string At = " " + std::to_string(K); + read(CollectionOp::MapContains, LogicSort::boolSort(), entries(), K, + "map contains" + At); + read(CollectionOp::MapGet, math(), entries(), K, "map get" + At); + read(CollectionOp::MapGet, math(), + collection(CollectionOp::MapInsert, Map, entries(), number(1), + number(11)), + K, "map overwrite" + At); + read(CollectionOp::MapContains, LogicSort::boolSort(), + collection(CollectionOp::MapRemove, Map, entries(), number(1)), K, + "map remove contains" + At); + read(CollectionOp::MapGet, math(), + collection(CollectionOp::MapRemove, Map, entries(), number(1)), K, + "map remove get" + At); + } + ObligationModule &Module = Facts.Module; Module.FunctionName = "certify_lean"; Module.FunctionIdentity = "certify_lean"; diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e2ed48978b..21fc4fe1df 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -609,7 +609,8 @@ canonicalization step before hashing or backend dispatch. The pass folds only sort-preserving Boolean constants, double negation, constant conditionals, and reflexive equality/inequality. It simplifies owned finite-fuel definitions and removes logical declarations unreachable transitively from every ordered -obligation. It then rebuilds each exact counterexample negation, the complete +obligation, a declaration reaching what its definition and its termination +measures apply. It then rebuilds each exact counterexample negation, the complete ordered goal/query, and required feature set before revalidation. Arithmetic, quantifiers, pointer/heap terms, and assumptions are not rewritten. Layer 3 reports before/after node counts, rewrite count, and removed declarations for @@ -678,7 +679,7 @@ and obligation kinds. ### Canonical archives and semantic identity `--obligation-out=FILE` writes one or more deterministic -`cppverify.obligation/2` records after source attribution. The binary format has +`cppverify.obligation/3` records after source attribution. The binary format has the `CPVOBL\r\n` magic, an explicit schema version, little-endian fixed-width integers, length-delimited strings/collections, and stable hand-assigned wire tags rather than C++ enum ordinals. The reader bounds strings, collections, @@ -691,10 +692,20 @@ which tag 6, the former sequence update, is reserved); the collection sorts `seq`, `set`, `multiset`, and `map` have sort tags 6 to 9. Trigger patterns and the `Choice` marker of a logical function are not archived, so a replayed module instantiates quantifiers by the solver's own patterns and -treats a choice as an ordinary uninterpreted function. - -Schema v2 adds the precise obligation kinds and still reads schema v1, whose -records name only assertions, postconditions, and unwinding checks. Records +treats a choice as an ordinary uninterpreted function. A logical function's +`Decreases` follow its definition levels, and with source metadata the +`Premise` of each `Or` follows its source point; neither is hashed. A +measure is validated like a definition (an integer over the parameters, +every call resolved): it changes no meaning, since Lean proves a +definition's termination itself and an induction descends by a +well-founded relation whatever the measure, and canonicalization keeps the +functions a measure applies, so a module never names one it does not +declare. Before, a spec only a measure applied was removed, and the +measure could not be printed. + +Schema v2 adds the precise obligation kinds, and schema v3 the measures and +premises; readers still read schema v2 and schema v1, whose records name +only assertions, postconditions, and unwinding checks. Records limit strings to 64 MiB, collections and expression nodes/edges to 100,000 per record, expression nesting to 4096, canonical integer numerals to 4096 decimal characters, and integer sorts to 4096 bits. @@ -706,8 +717,9 @@ that are not the conjunction of their ordered obligations. Every module and individual obligation has a SHA-256 semantic hash. Semantic hashes omit source paths, display-only function names, internal positional -or public source-anchored obligation IDs, and every obligation kind except -`unwinding` (the only kind that changes result semantics), so moving identical +or public source-anchored obligation IDs, measures, premises, and every +obligation kind except `unwinding` (the only kind that changes result +semantics), so moving identical C++ source or inserting an unrelated earlier obligation does not invalidate an individual goal's proof identity. Per-obligation hashes include only the transitive @@ -728,6 +740,7 @@ cpp-verify --obligation-in=goals.cpv --backend=z3 cpp-verify --obligation-in=goals.cpv --backend=portfolio cpp-verify --obligation-in=goals.cpv --lower-only --dump-ir=3,4 cpp-verify --obligation-in=goals.cpv --backend=lean --lean-out=goals.lean +cpp-verify --obligation-in=goals.cpv --lean-project=proof --lean-certify ``` Replay treats decoded records as untrusted input: validation happens before @@ -743,13 +756,33 @@ f (dependencies not checked in replay)` (JSON status `proved-obligations`), never `Verified`. A counterexample is still a failure: postconditions are not archived either, so replay certifies it by the definitions alone. +With `--lean-project`, `replayLeanProject` (`Verifier.h`) exports every +module through the project adapter and drives the same `LeanProject` a +source run does: the modules and the spec trial, regeneration with moves +and quarantine, automation, and with `--lean-certify` certification. The +goals have the names, files, and proofs the same obligations have from +source (both name a goal by its statement), and `Specs.lean` the same +definitions, since the measures are archived; a readable goal names the +same hypotheses, since the premises are. A module whose goals all certify +is `Verified` there, printed `Proved obligations ... [backend=lean]`, never +`Certified`, which claims the dependency fixpoint. + BMC-produced modules retain the unroll bound as semantic transform provenance. Replay aggregates those modules with BMC unwinding semantics even when Z3 is the selected solver, so an exhausted frontier remains `BoundedSafe(N)` rather than becoming an ordinary program failure or an unbounded proof. Applying BMC to an untransformed archive is rejected because loop unrolling must precede -obligation construction. Lean scratch export of a bounded archive is also -rejected until bounded theorem provenance is represented in generated Lean. +obligation construction. Lean exports a bounded module, to the scratch-pad +or a project, with its bound stated over its goals (`/- bound: N loop +iterations, by unrolling: ... -/`), and `LeanProjectGoal::Unwinding` marks +its unwinding goals: every goal certified is `Verified` (`Proved +obligations`, `bound=N`), every goal but some unwinding goal certified is +`BoundedSafe` (`bmc.incomplete-bound`), else `Unresolved`. Lean proves and +never refutes, so its `BoundedSafe` says that the unwinding goals are not +certified, where BMC's says that they fail; both claim what the safety +goals state, no failure in an execution within the bound, and reporting +the unproved unwinding unresolved would leave Lean no bounded verdict for +a module whose unwinding fails, the case BMC is for. Failure-triggered `cppverify::recommends` diagnostics are excluded from archives, keeping archive contents independent of solver outcomes. @@ -1546,6 +1579,10 @@ proof of a printed goal is a proof of its obligation. | `MathematicalInteger`, `Pointer` | `Int` | | `BitVector(N)` | `BitVec N` | | `Heap` | `CppHeap` (`Int -> Int`) | +| `Seq` | `List Int` | +| `Set` | `Int → Bool`, its membership test | +| `Multiset` | `Int → Nat`, its counts | +| `Map` | `Int → Option Int`, the value at each key it holds | - **Precedence.** Each operator is printed at its Lean notation level (`Init/Notation.lean`, `Init/Core.lean`), and an operand that binds looser @@ -1590,6 +1627,24 @@ proof of a printed goal is a proof of its obligation. only the `if`s of generated definitions whose condition has no decision procedure; Lean's instance search already tries a specific instance before it. +- **Collections.** Each operation is a prelude function + (`LeanCollectionPrelude`), `cppSeqAt`, `cppSetInsert`, `cppMapGet`, ..., with + the total meaning of ``: a read outside a sequence or a map's + domain is 0, a subrange clamps `lo` into `[0, len]` and `hi` into + `[lo', len]`, a multiset removal of a count of 0 leaves 0 (`Nat` + subtraction). A set is its membership test rather than a finite set, since + a set may be infinite (Vermilion's mirror of vstd uses Mathlib's `Finset`, + for finite sets only). The three membership predicates are `def`s with + `Decidable` instances, not `abbrev`s: as `abbrev`s, the Boolean lemmas of + `cppverify_norm` rewrote `cppSetContains s x` to `s x = true`, and the + lemmas stated on `cppSetContains` no longer matched. As Vermilion states + its vstd operations, the prelude states each operation's meaning in + `@[simp, grind =]` lemmas (`cppSeqAt_push`, `cppSetContains_remove`, + `cppMultisetCount_remove`, ...), with `grind_pattern`s for the facts that + lengths and counts are not negative (grind's own pattern for `0 ≤ count m + x` is the inequality, which no goal contains), and extensionality lemmas + (`cppSeq_ext`, `cppSet_ext`, `cppMultiset_ext`, `cppMap_ext`); all of them + are theorems of the definitions, so no goal rests on them. `CertifyTest.AgreesWithLeanEvaluation` checks the agreement in the pinned Lean. The value the certifier computes for each ground term is a theorem Lean @@ -1600,7 +1655,15 @@ bits signed and unsigned, every mathematical operator inside every other, and every connective inside every other at all eight assignments of its operands, so that a lost parenthesis changes a value; and every operator at the extreme operands of widths 1, 8, and 64 (a zero divisor, `INT_MIN / -1`, a shift past -the width, each overflow predicate). `lean_semantics.cpp` checks the printed +the width, each overflow predicate); and for collections, 80 random chains of +operations read by length, index, membership, count, and key, 20 equalities +of sequences, and each operation where it matters (every clamp of a +subrange of `[1, 2, 3]`, membership of an element inserted and removed, each +combination of two sets, a multiset removal of a count of 0, 1, and 2, a key +written over and removed). Printing set removal as insertion fails it. +`lean_collections.test` exports a function over each kind, certifies their +automatic proofs and a proof by hand of `total(s.push(x)) == total(s) + x`, +and keeps a false claim unproved. `lean_semantics.cpp` checks the printed forms, builds an export, certifies a goal whose proof needs the quantifier closed and `decide` on a comparison, refuses proofs that read `(a ^ b) + 1` as `a ^ (b + 1)` or `2 - 3` in `Nat`, and its probe file @@ -1610,7 +1673,10 @@ SMT-LIB, and Lean's own operators differ. ### Project goals and their proofs A project names nothing by position, so that a proof keeps every name it -uses when the source around it changes. +uses when the source around it changes. `LeanProject` (`Verifier.cpp`) owns +a project: its modules, its proof files, automation, and certification, +reporting what it warns of to its caller; a source run and an archive's +replay both drive it. - **Names.** A module's goals form a group, `cppverify__
` (`leanProjectGroup`), `h6` from the MD5 of the function identity, which @@ -1627,6 +1693,59 @@ uses when the source around it changes. `Proofs/Goal_.lean`: reordering two postconditions gave each proof the other's goal, and inserting a function renamed the bodies every later proof used. +- **Modules.** The adapter writes a project's goals by group + (`LeanProjectModules`): the goals, readable forms, bridges, and spec + bodies of each function go to `CppVerify/Goals/.lean`, which + imports `CppVerify/Specs.lean`, where `emitLeanFunctionDeclarations` + declares each spec the run's modules apply once (`opaque + cppSpec_`). Two goal modules that declared one spec would clash + when imported together, and Lean 4.35's import keeps one of two differing + declarations without a word. The prelude is `CppVerify/Prelude.lean`, and + `CppVerify/Generated.lean` imports every goal module, so a `User.lean` of + 0.1, which imports it, still sees every goal. A proof imports its goal + module, `User` (which a new project starts with `import CppVerify.Specs`), + and `Auto` (which imports `Prelude`); `CppVerify.lean`, the library's + root, imports `Check`, so `lake build` builds every proof (before, the + library had no root and `lake build` failed). Every file is written only + when its text changes (`writeTextFileIfChanged`), and + `writeLeanProjectModules` removes every goal module the run did not + write: a fallback run rewrites `Specs` with the specs of the modules it + exports, and a module it kept from an earlier run failed (`Unknown + identifier cppSpec_...`). +- **Spec definitions** (`leanSpecDefinition`, `leanSpecsText`, + `tryLeanSpecs`). The adapter gives each logical function of a project its + opaque declaration and, unless it is a choice, has no step definition, or + recurses without `Decreases` (an archive's), a candidate definition: the + step definition printed with the recursion's applications by name and no + temporaries (a hoisted application would lose its guard), in definition + mode (`LeanDefinitionMode`), where each `ite` is `if _h : c`, a + conjunction or disjunction whose later operand applies a spec is a + dependent `if` (`a ∧ b` -> `if _h : a then b else False`, `a ∨ b` -> `if + _h : a then True else b`, `¬a ∨ b` -> `if _h : a then b else True`), and + a bounded existential over one guards its body by its range, each a + classical equivalence; then `termination_by ((m1 + 1).toNat, ...)` and + `decreasing_by all_goals cppverify_decreasing` (a prelude macro: `simp_wf; + omega`, then with the collection and comparison wrappers unfolded, then + with `cppIntDiv` and `cppIntRem` unfolded and their conditions split, so + that a measure falling by a constant divisor is linear (`log2(n / 2)`), + then `simp_all; omega`, then `decreasing_tactic`). `(m + 1).toNat` decreases + exactly where the termination check's relation does (`upper >= 0 && + lower < upper`, lower possibly -1); Vermilion instead guards the whole + body by `0 ≤ measure`, which changes the value where the measure is + negative and the body does not recurse. The driver groups the functions + by the strongly connected components of their applications (Tarjan, + callees first), each group one `mutual` block, and decides each from + `.cppverify/specs/`, the key the SHA-256 of the toolchain, + `LeanSpecRevision`, the group's text, and what each callee group is. The + rest it tries in one Lean file importing `Auto`, the others as decided, + undecided callees as definitions, `#cppverify_axioms` after each member: + a definition Lean rejects is added with `sorryAx`, or not at all. A + group's verdict is kept only when each callee tried in the same round was + accepted; else it is tried again with the rejected callee opaque, until + nothing changes. No Lean: undecided groups are opaque and nothing is kept. + `Specs.lean` has the groups in that order, and its definitions use the + classical decidability instance locally. Statements do not change: a goal + states the same definition instances, which a definition makes theorems. - **Records.** The exporter returns a `LeanProjectGoal` for every obligation of each module it exports, those a fallback selection leaves to the solver marked `Exported = false`, with the 0.1 names of the goal and @@ -1653,11 +1772,57 @@ uses when the source around it changes. termination module it gives the solver (`LeanRunGroups`); a record whose group is among them but has no goal this run, or whose goal the run knows but does not export, is left where it is and never moved. +- **Refutations.** Where the adapter attributes a certified counterexample + to an obligation, it keeps the model's value of every free variable of + that obligation's goal (`counterexampleAssignment`, read through the + certifier's `CandidateModel::constant`, so the values are the ones it + evaluated; `VerifyResult::Assignment`). A fallback run turns each such + failure into `exportLeanRefutation`'s module, which states the goal and + the bodies it unfolds as the project would (its specs added to `Specs`, + so the trial may define them), then `theorem _refuted : ¬ `: + the goal applied to the values in the order it binds its variables + (machine integers as `BitVec.ofInt`, a heap or collection by its runs of + equal cells as nested `if`s), and closed by `cppverify_refute`, an + `Auto.lean` tactic that tries `decide`, then evaluation by the bodies, + the spec equations (which prove the definition instances the goal states), + memory reads, and machine comparisons, closed by `decide` or, for a store + relation, which quantifies over every address, `grind`. When both fail it + tries to show the instance true, in a context without the goal (which + would prove any instance of itself): Lean deciding it true fails with + `Lean decides the goal true at the counterexample`, else with `Lean cannot + decide the goal`. Each attempt, and that probe, has `cppverify.autoBudget` + heartbeats of its own (`withinBudget`, as a step of `cppverify_auto`), and + the module lifts the heartbeat limit for the goal's definitions only, so + the theorem keeps Lean's limit, which bounds the kernel's check of it too: + with no limit, the refutation of an inductive predicate applied at -5000 + ran for over an hour. The prelude decides a quantifier over a + range of at most 4096 integers, as many as the certifier expands + (`CertifyLimits::DirectExpansion`), by trying each one + (`cppIntBallDecidable`, `cppIntBexDecidable`, through core's `Nat` ones), + and a larger range has the classical instance, which `decide` cannot + evaluate. Each integer is a level of recursion whose memory heartbeats do + not count, and the generated files allow a depth of 100000: before the + bound, refuting `forall(i, 0, 1000000000, i < 999999999)` took one Lean + process 58 GB (16 GB for a range of 16000, which also spent the step's + heartbeats; 1.9 GB for one of 1000), and CI's 16 GB runners were shut down + (`lean_refutation_memory.test`, Linux, keeps it below 4 GB). `writeLeanRefutations` + writes `Refutations//.lean` and removes every other, and + certification builds them beside the proofs and gives the checker each as + a `refutation` entry, whose theorem must state exactly `Not ` of the + goal its module defines, with Lean's foundations alone. Once the verdicts + settle (a failure checked with a spec postcondition that is not + established is `spec.post`), the driver marks each failure that stands and + was confirmed (`LeanRefuted`, JSON `lean_refuted`), warns of one Lean + cannot decide, and makes one Lean decides true `Unresolved` + (`InconsistentBackendResults`): two evaluators of one goal disagree, so + the failure is not a fact. On the gate's matrix every certified failure is + refuted; a goal applying an opaque function (a choice, `validPtr`) is not. - **Readable goals.** `makeGoal` (`Obligation.cpp`) marks each `Or` it builds, `!A || rest`, with where `A` comes from (`LogicExpr::Premise`, - like `Patterns` outside the term's meaning, its hash, and archives, and - kept by the clones and the in-place simplifier). The passivizer records + like `Patterns` outside the term's meaning and its hash, kept by the + clones, the in-place simplifier, and archives of schema 3, so a replayed + goal names the same hypotheses). The passivizer records the origin of every assumption it emits (`PassiveStmt::Origin`, from a current origin each statement kind sets and its parts refine: `def`, `call:f`, `inv`, `cond`, `exit`, `keep`, `exact`, `check`, @@ -1675,7 +1840,7 @@ uses when the source around it changes. `G.of_readable : G.readable → G` is a term, one prelude lemma per hypothesis (`cppverify_imp_or : (a → b) → ¬a ∨ b`, or `cppverify_not_imp_or : (¬a → b) → a ∨ b`), so the kernel checks with - `Generated.lean` that the readable form implies the goal: a printer bug in + the goal module that the readable form implies the goal: a printer bug in it fails the build, never certifies. `LeanProjectGoal::Intros` gives the stub its `intro` list. On the 375 suite exports, the 351 that built before still build, with every bridge. @@ -1696,11 +1861,49 @@ uses when the source around it changes. Z3 proves, they prove 90.2%. Stating the range of every `BitVec.toInt` term first proved nothing more (`grind` knows the ranges), and removing `Int.bmod` with an `omega` discharger proved 32 fewer and took longer, so - neither is a step. Heartbeats alone did not bound the time, since a kernel - check is not counted. The trial takes every exported goal whose proof file + neither is a step. Revision 2 (`LeanAutomationRevision`) appends three + steps after the others, so that every goal an earlier step proves keeps + its proof: `_bveq` states equalities of machine values and sign extensions + as integer facts too (`← BitVec.toInt_inj`, `toInt_signExtend_of_le` with + `omega` for its side condition); `_ext` applies a collection's + extensionality lemma; `_unfold` delta-expands the generated spec bodies + (`cppverify_unfold_bodies`, every constant named `cppSpecBody_…`, to a + fixed point, since a body names only lower levels of its definition) in + the goal and its hypotheses, so each definition instance says what the + body does, then as `_bveq`. Tagging the bodies for `grind` instead would + have changed what every earlier step does. `_bveq` is not subsumed by + `_unfold`: on the 37 goals of `roundtrip_uleb128` (`llvm_uleb128.cpp`) + unfolding the lifted helpers' bodies exhausts the heartbeats, and `_bveq` + proves them. Over the same exports (375 sources, 9259 goals), of the 5582 + that Z3 proves revision 1 proves 5044 (90.4%) and revision 2 5210 + (93.3%), none fewer; of the 112 over collections, 86 and 104. A goal + neither proves now takes 2.8 s on average instead of 1.4 s. Revision 3 + appends `_eval`, which rewrites with the equations of every spec that + `CppVerify.Specs` defines (`cppverify_eval_specs`, from that module's + constant names), and with `cppIntDiv` and `cppIntRem`, and closes goals + at known arguments; on unknown ones it unfolds until its budget is spent. + It then appends `_div`, `grind` given the definitions of `cppIntDiv` and + `cppIntRem`, which decides a mathematical division or remainder by a + constant once their conditions are split; `_eval` fails without a spec + definition, and a contract such as `a / 2 >= 2` needs no spec. + Over the same exports, of the 5582 goals Z3 proves revision 2 proves + 5210 (93.3%) and revision 3 5301 (95.0%), none fewer, and of all 9259 + goals 8078 and 8194; in the five sources with the most new proofs, 48 of + their 61 are `_div`'s (pointer differences divide by the element size, + and binary search halves an interval) and 13 `_eval`'s (the factorial + constants of `mvp_factorial.cpp`). A goal neither proves takes 4.4 s on + average instead of 3.1 s. The trial makes 213 of the suite's 293 spec + functions definitions; of the 80 opaque, 69 have no definition to try + (20 choices, 47 heights and witnesses of inductive predicates, 2 + `unspecified` functions), and 11 do not terminate, or terminate only by + their own postcondition or a choice's axiom. Its first ladder missed + three that terminate by C++ division (`log2(n / 2)`), since `omega` does + not see through `cppIntDiv`. + Heartbeats alone did not bound the time, since a kernel check is not + counted. The trial takes every exported goal whose proof file is pristine and whose header (`automation:`) names no attempt by the - current `LeanAutomationRevision`, builds `User` and `Auto` with the pinned - toolchain, and runs `lean` itself (with the `LEAN_PATH` of `lake env`, so a + current `LeanAutomationRevision`, builds `Generated`, `User`, and `Auto` + with the pinned toolchain, and runs `lean` itself (with the `LEAN_PATH` of `lake env`, so a stopped process leaves no child) on a file of one theorem per goal, in `--jobs` chunks. Lean reports to a file, line by line: the step that proved each goal, then the axioms of its theorem or that the kernel @@ -1712,28 +1915,46 @@ uses when the source around it changes. the attempt. An export's `LeanAutomatic` counts its goals with automatic proofs. Without the toolchain nothing is tried. - **Certification** (`certifyLeanProject`) builds `Generated`, `User`, and - `Auto`, then each proof file with `lean -EhasSorry`, `--jobs` at once, - recording the goals whose proofs do not build, and runs the checker of + `Auto` with Lake, then each proof file with `lean -EhasSorry`, `--jobs` at + once, unless its key is unchanged (`leanProofKey`: SHA-256 of the + toolchain, the proof's text, and the bytes of the `.olean` of every project + module in its import closure, read from the `import` lines of their + sources; written to `.cppverify/built/` after a build, and removed + with the `.olean` before one). Lake cannot tell this itself: when a + dependency fails it names only the dependency, and a dependent's `.olean` + of an earlier build stays, so a proof the user broke would certify from + its old `.olean`. It records the goals whose proofs do not build and runs + the checker of `LeanCertifier.h`, written into the project as `.cppverify/Certify.lean`, with `lake env lean --run` over the goals that built. The checker elaborates none of the user's source, so their macros and commands cannot - change what it checks. It first checks every goal in one environment: all - proof modules imported at once, and the constants of every module of the - project replayed together through the kernel into the environment of the - toolchain modules they import (a constant two modules declare, as an - equation lemma derived in each, must be the same). A check reads only - kernel constants, so proofs imported together cannot change each other's - checks; an automatic proof imports `Lean`, whose import each goal would - repeat (33 goals took 161 s, now 8 s). If that import or replay fails, as a - proof file that declares a goal's name or a module the kernel rejects - makes it, each goal is checked on its own: it imports the proof module - (never freeing imported data, which a later import in the process reads), - and every module of the import closure whose `.olean` lies outside the - toolchain is replayed (as `leanchecker`'s `replayFromImports`), each once - in a run. Either way it checks that `G_proof` is a theorem of type exactly - `.const G []` with `G` a definition of `CppVerify.Generated`, collects its - axioms by walking the kernel constants it uses, and prints `ok` or `fail` - with the reason per goal. Each Lean result + change what it checks. It reads, from their compiled files, the goal + modules the goal list names, every proof module, and every module of the + project they import (its `.olean` lies outside the toolchain), imports the + toolchain modules among their imports once, and replays the project's + modules through the kernel into that one environment, each after the + modules it imports and the goal modules first, as `leanchecker` does. A + module the kernel rejects, a module that imports one, and a module that + declares a constant an earlier module or the toolchain declares otherwise + are left out, each with why; a constant two modules declare alike (an + equation lemma Lean derives in each, a theorem stated the same, which the + kernel's replay takes twice) is the same. So a proof file that declares a + goal's name is the one left out (`... declares G, which + CppVerify.Goals. declares`), and a bad module fails only the + goals whose proofs import it. Every check reads the kernel's constants and + the module that declared each, so proofs cannot change each other's + checks: a goal passes when its proof module and its goal module were + replayed, `G_proof` is a theorem of type exactly `.const G []` with `G` a + definition of its goal module, and the axioms its kernel constants reach + are allowed; every goal fails when a declaration of `CppVerify.User` + depends on `sorryAx` (`userSorry`; before, `User` was compiled with `sorry` + an error); it prints `ok` or `fail` with the reason per goal. One import + matters because an automatic proof imports `Lean`: a check per goal took + 161 s for 33 goals (now 8 s), and the first 0.2 checker, which fell back to + it when one module failed, imported each proof module and the imports of + each module it replayed again, never freeing imported data, which took + 13 GB for one proof through a rejected module (now 3.6 GB, + `lean_certifier_memory.test`) and stopped CI runners. Each Lean result records its goals (`VerifyResult::LeanGoals`), and a function is `Certified` only when all of them pass; a failure names the obligation. Before 0.2 certification parsed what `Check.lean` printed, which imports @@ -1743,7 +1964,9 @@ uses when the source around it changes. `lean_certifier.test` checks each of those attacks, a proof file that defines a goal of the same name of its own (the type check alone would take it), a proof that does not build, and an axiom behind a lemma, with a function whose -proofs are sound certified beside each. `lean_readable.cpp` checks the readable form of each kind of assumption, +proofs are sound certified beside each; `lean_certifier_memory.test` bounds +the checker's memory where a proof imports `Lean` through a module the kernel +rejects. `lean_readable.cpp` checks the readable form of each kind of assumption, the bridge, the stubs (every one builds), a proof by hypothesis names that certifies, and a readable form that says less than its goal, which fails at its bridge. `lean_automation.test` checks what an export writes (an automatic @@ -1755,7 +1978,12 @@ unfinished lemma does not count, and `--no-lean-auto`. `lean_identity.test` walk reordering, renames of two functions at once, a changed statement, a removed function and its return, fallback runs, pruning, and a 0.1 project with proofs, stubs, and a proof of a goal that is gone) and certifies it after -the renames and the migration. +the renames and the migration. `lean_modules.test` checks the layout (a +spec declared once for two goal modules certified together, a proof's +imports, a new `User`), that a rerun writes no file, that a change to one +function compiles only its goal module, `Generated`, and its proofs again, +`lake build`, that a deleted function takes its module, a 0.1 `User.lean`, +and a fallback run that keeps no goal module. ## Counterexample Extraction @@ -1795,7 +2023,22 @@ caret included, as a compiler error is, and its JSON record carries the range. ## Modular Verification Protocol -1. Build VFunction for each function in source order. +1. Build VFunction for each function in source order. An error of the + converter belongs to the function it is raised in (`ConversionError:: + Function`), and to its signature or contract (`InContract`) or its body; + the errors that passes after conversion find about a function (a recursion + without a measure, a malformed inductive predicate) belong to it too + (`errorOf`). Such a function keeps what others may rely on + (`markUnsupported`, `VFunction::UnsupportedConstruct`): an executable or + proof function whose contract converted becomes an external contract, its + body not verified, and one whose contract did not is left out, so a call + of it is an `unsupported` obligation; a spec becomes a declaration without + definition, postconditions, reads, or domain. The driver prints every + error, gives the first of each function its verdict (`Unresolved`, + `construct.unsupported`), reports a function whose `SpecDependencies` + reach an unsupported spec the same way without verifying it, and verifies + the rest; the fixpoint demotes callers of the external contracts + (`callee.contract`). Only an error of no function stops the run. 2. For each VFunction: a. Build Layer 1 IR. b. Passivize to Layer 2 (SSA + havoc/assume/assert + heap versioning). @@ -1885,7 +2128,7 @@ The driver selects a backend via `VerifyOptions` (`Verifier.h` / `cpp-verify --b | **Strict portfolio** | `PortfolioVerifyBackend` | Runs ordered Z3 and cvc5 queries. Matching UNSAT proves; matching certified counterexamples fail and retain the Z3 model; disagreement or an unresolved side is unresolved. | | **Race** | `RaceVerifyBackend` | Runs the Z3 and cvc5 backends at once; the first proof or certified counterexample stands and cancels the other (`cancel`/`resume`: a backend-wide `Race` of Z3 encoders, each forwarding an interrupt to the pass it runs, and a flag the cvc5 process loop polls). Otherwise the obligations either proved are joined, `Verified` (`z3+cvc5`) when they cover the module. | | **BMC** | `LoopUnroll` on VCR, then shared obligation/Z3 path | Source verification grows bounds from zero through `--unroll=N`, stopping on a counterexample, complete unwinding, unresolved query, or the maximum frontier. Safety with failed unwinding is `BoundedSafe(N)`; only proved unwinding is `Verified`. A spec's checks have no loops: the driver solves them with a Z3 backend (`SpecBackend`), as on the default path, and keeps the bound on their archived modules for replay. | -| **Lean** | `exportLeanScratchPad` / project certification | Standalone mode emits unchecked theorem stubs. Project mode emits direct source goals, total functional heaps, typed bit-vector/integer operations, and compact finite-fuel spec bodies into generated files while preserving user proofs. Export is `Exported`; only the pinned admission-free kernel/axiom check is `Certified`. Collections are `logic.unsupported` (planned for a future release). | +| **Lean** | `exportLeanScratchPad` / project certification | Standalone mode emits unchecked theorem stubs. Project mode emits direct source goals, total functional heaps, typed bit-vector/integer operations, and compact finite-fuel spec bodies into generated files while preserving user proofs. Export is `Exported`; only the pinned admission-free kernel/axiom check is `Certified`. Collections are core Lean values (`List Int`, `Int → Bool`, `Int → Nat`, `Int → Option Int`) with prelude operations and lemmas. | A Lean project pins `LeanToolchain` (`Driver/CppVerifyVersion.h`) in its `lean-toolchain` file. A pin in `EarlierLeanToolchains`, which an earlier @@ -1908,8 +2151,9 @@ prove individually, and by default only those are exported, under their complete-export goal names (`--lean-fallback-scope=all` exports every obligation). Once they kernel-check, the result is `MixedProof` (`Proved (z3+lean)`, JSON `mixed-proof`) with `ProofEvidence` recording the -split; `Certified` still means Lean checked every obligation. SAT -counterexamples are never fallback successes. +split; `Certified` still means Lean checked every obligation. A certified +counterexample is never a fallback success; its refutation shows it in +Lean. A verdict left to Lean (a Lean-backend export, or a fallback export under `--lean-certify`) rests on what its module uses, as a solver's does. At export @@ -2336,7 +2580,7 @@ an unchecked support table: | Quantifiers and finite-fuel specs | Both quantifier kinds, recursive spec calls, and conservative `unknown` rather than false proof | | Frontend-derived semantics | Type invariants and complete-loop Z3/BMC parity | | Portable artifacts | Source-stable obligation IDs, canonical/BMC archive replay, and deterministic parallel results | -| Lean | Source/archive obligation identity, one proof file per project goal recording its statement hash, generated-file kernel build, and bounds instrumentation | +| Lean | Source/archive obligation identity, one proof file per project goal recording its statement hash, generated-file kernel build, bounds instrumentation, and decisive verdicts: every valid case certified (one with a proof by hand, `Inputs/backend_fidelity_proofs`), every counterexample refuted | | Advisory checks | `cppverify::recommends` warnings stay source-only and never enter BMC or Lean replay artifacts | An approved valid quantified or inductive query may remain diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 2e47c14254..92d375894c 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1497,7 +1497,16 @@ int safe_fib(int n) cppverify::pre(...) cppverify::post(cppverify::result == fib the verifier does not model, so its counterexample says nothing about the program; the message names the construct (for example "the operands of this pointer difference may address the objects of different parameters - or globals, which may be one caller array"). + or globals, which may be one caller array"). A function whose body or + contract uses a construct the verifier does not represent at all has this + verdict too, with the error at the construct, and costs that function + alone, as Prusti reports an unsupported feature and Kani a construct it + does not translate: every other function is verified. What others may + rely on stays: a caller verifies against the function's contract, which + nothing establishes (`callee.contract`); a call of a function whose + contract is not represented is unsupported where it is; a function that + reaches a spec the verifier does not represent, which keeps only its + declaration, is not verified (`construct.unsupported`). - Qualifiers: `[partial]` (proved for terminating executions only, after `cppverify::decreases(*)`), `[trusts=f]` (relies on the contract of `f`, marked `[[cppverify::trusted]]`), `[vacuous]` (no execution reaches the claim). @@ -1595,9 +1604,14 @@ cppverify::proof void sum_concat(seq s, seq t) values: `[1, 2]`, `{1, 3..5}` (a run), `{2: 3}` (counts), `{1 -> 7}`, and `{..}` (every integer). - Backends: Z3 and cvc5 decide all four (cvc5 settles fewer goals that - need an infinite set or ghost collections in loops). Lean does not - support them yet (`logic.unsupported`); that is planned for a future - release. + need an infinite set or ghost collections in loops). Lean states them with + core types: a sequence is a `List Int`, a set its membership test + `Int → Bool` (a set may be infinite, so not a finite set), a multiset its + counts `Int → Nat`, and a map `Int → Option Int`. Each operation is a + function of the generated prelude with the total meaning above, and + lemmas state what each means for `simp` and `grind`, as Vermilion's + mirror of vstd states each operation for its ladder; Lean's evaluation of + every operation agrees with the certifier's. ## Clang Modification Details @@ -1770,15 +1784,18 @@ Before publication or dispatch, a shared fail-closed canonicalizer applies a deliberately narrow set of semantics-preserving rewrites: Boolean constants, double negation, constant conditionals, and reflexive equality/inequality. It simplifies logical definitions, removes declarations unreachable transitively -from the ordered goals, rebuilds exact per-obligation and complete +from the ordered goals (through definitions and termination measures, since +a declaration depends on what both apply), rebuilds exact per-obligation and complete goal/counterexample pairs, recomputes features, and revalidates. The same step runs after archive decoding, so adapters never depend on producer-specific dead declarations or trivial Boolean structure. It does not rewrite arithmetic, quantifiers, pointer/heap expressions, or assumptions. `--obligation-out=FILE` publishes deterministic, concatenable -`cppverify.obligation/2` binary records with stable wire tags and portable -source attribution; schema v1 records remain readable. Publication includes an +`cppverify.obligation/3` binary records with stable wire tags and portable +source attribution; schema v3 adds each logical function's termination +measures and, with source attribution, where each assumption of a goal comes +from, neither hashed, and records of schemas v1 and v2 remain readable. Publication includes an exact serialize/deserialize/validate/reserialize check. SHA-256 module and per-obligation semantic hashes exclude source paths, display-only names, both positional and source-anchored obligation IDs, and every obligation kind except @@ -1788,14 +1805,21 @@ wire version. Canonicalization introduced semantic-hash format v2, excluding diagnostic identities advanced it to v3, and excluding diagnostic kinds advances it to v4. `--obligation-in=FILE` validates and replays the records through Z3, cvc5, -strict Z3+cvc5 portfolio, lower-only/Layer 3-4 dumps, or Lean scratch export -without reparsing C++. Each module is replayed alone, without the contracts, +strict Z3+cvc5 portfolio, lower-only/Layer 3-4 dumps, Lean scratch export, or +a Lean project (`--lean-project`, with certification) without reparsing C++. +A project gets the goals, specs, and proof files a source run gives the same +obligations, which are named by their statements, so a proof certifies from +either. Each module is replayed alone, without the contracts, spec facts, partiality, and vacuity checks its source verdict rests on, so a module whose obligations hold reports `Proved obligations` (JSON `proved-obligations`), never `Verified`. BMC-produced records carry their unroll bound in semantics and replay through the BMC result aggregator; untransformed records cannot acquire BMC semantics -after this boundary. Lean scratch replay of bounded records is rejected. +after this boundary. In Lean the goals of a bounded module state its bound, +and its result aggregates the same way: every goal certified proves its +obligations, and every goal but some unwinding goal certified is +`BoundedSafe`, which in Lean says that the unwinding is not certified (Lean +never refutes a goal). Failure-only `cppverify::recommends` diagnostics do not alter archive contents. Imported records are untrusted input: bounded parsing precedes structural, scope, sort, signature, feature, canonical-payload, exact-negation, and @@ -1913,7 +1937,23 @@ Lean's evaluation agrees with the certifier on random ground terms and on every operator at its extreme operands. Standalone output is an unchecked scratch-pad. Editable project mode pins Lean 4.35.0 (`LeanToolchain`), overwrites only generated semantics and the active check module, and preserves reusable user lemmas and one proof file -per goal. A goal is named by its function and its statement, never by a +per goal. The generated semantics are a module per function, +`CppVerify/Goals/.lean`, over one `CppVerify/Specs.lean` that +declares each spec the goals apply once, as Vermilion emits a unit per +function over one `Specs` file; each goal module defines the spec bodies its +goals unfold under the function's prefix. A proof imports its function's +goal module, the user's lemmas (`User.lean`, which a new project starts by +importing `Specs`), and `Auto`, so a change to one function builds again +only what imports its module. A file is written only when its text changes, +and a run writes the goal modules of the functions it exports and removes +every other, so that every module of the project builds. A spec function is a +Lean definition, by its step definition with every condition that reaches a +recursive application a hypothesis there and its `decreases` measures as +`termination_by`, when the pinned Lean accepts it at export, and opaque +otherwise; Lean's verdict is kept in the project. The definition has the +spec's value at every argument (it is never guarded by the measure as a +whole), so a proof may unfold it or use its induction principle, and goals +do not change. A goal is named by its function and its statement, never by a position: `cppverify__
__`, where `h6` hashes the function's identity (overloads) and `h8` begins the SHA-256 hash of the obligation's statement, its goal with the logical declarations it uses and @@ -1942,7 +1982,11 @@ tactic of the generated `CppVerify/Auto.lean`, with the pinned toolchain when it is installed, as Vermilion, Why3, and Frama-C's WP run their automation before a goal reaches the user: a ladder of steps (`grind`; memory stores as the functions they define and machine arithmetic as integers, then `grind`; -`decide`; `simp_all`), each with a heartbeat budget of its own and a +`decide`; `simp_all`; machine equalities and sign extensions as integers; +collection equality element by element; the definition instances a goal +states, by the bodies they name; the spec definitions by their equations; +mathematical division and remainder by their definitions), +each with a heartbeat budget of its own and a wall-clock limit per goal (`--lean-auto-timeout`), since heartbeats do not bound a kernel check. A step counts only when it closes the goal with a proof whose axioms are Lean's foundations, so the user's unfinished lemma never @@ -1952,16 +1996,22 @@ any other; any other gets a stub that records the attempt and the revision of `cppverify_auto` that made it. A file is tried again only when no attempt by the current revision is recorded, and only while nobody has edited it. Initial generation is `Exported`. Certification -holds against a proof author in bad faith. It compiles the user module and each -proof file on its own with `sorry` as an error, then runs a checker +holds against a proof author in bad faith. It builds the generated modules, +`User`, and `Auto` with Lake and compiles each proof file on its own with +`sorry` as an error, unless nothing it is built from changed since it last +built: its text and the compiled project modules it imports, by hash, since +Lake does not say which modules of a failed build are current. It then runs a checker (`LeanCertifier.h`) on the compiled modules, which elaborates none of the user's source, so no macro, notation, or command of theirs changes what it -checks: the kernel replays every module of the project a proof imports (only -the pinned toolchain's own modules are trusted), as `leanchecker` does, so no -declaration escaped the kernel; the proof's type is exactly the constant of its -goal, which `CppVerify.Generated` defines; and the axioms it depends on, +checks: the kernel replays every module of the project a proof imports into +one environment, after the toolchain's modules, imported once (only those are +trusted), as `leanchecker` does, so no declaration escaped the kernel, and a +module that declares a name an earlier one declares otherwise is rejected, +the goal modules being replayed first; the proof's type is exactly the +constant of its goal, which its function's goal module defines; and the axioms it depends on, collected from the constants it uses, are among `propext`, `Quot.sound`, and -`Classical.choice`. A function is `Certified` when every one of its goals +`Classical.choice`. A declaration of `User.lean` that uses `sorry` fails +every goal. A function is `Certified` when every one of its goals passes, and a failing goal leaves only its own function `Unresolved`, with the obligation and the reason. Certification runs the pinned toolchain whatever elan's default is, and a project pinned by an earlier release moves to @@ -1987,6 +2037,19 @@ A result proved in Lean, alone or with the solver, rests on the callee contracts and spec facts its module uses, as a solver's does: certification runs before those are settled, whatever else failed, and a Lean result gets the same qualifiers and vacuity checks as a verified one. +A certified counterexample is shown false in Lean too: the fallback project +gets its refutation, the failing goal stated as the project states it and a +theorem `¬ goal` proved at the values the model gives its variables +(`cppverify_refute`: decided, or evaluated by the spec definitions, a store +by `grind`, a quantifier over a range of at most 4096 integers by trying +each one, each +attempt within the heartbeats of a step of `cppverify_auto`), which +`--lean-certify` checks as it checks a proof. Of the failures that stand once +the verdicts settle, a refutation that checks marks +the failure `(confirmed in Lean)`; one Lean cannot decide leaves it a failure +with a warning; one Lean decides true is a disagreement between two +evaluators of one goal and makes the function `Unresolved` +(`backend.inconsistent-results`), never `Failed`. ## MVP Acceptance Programs diff --git a/website/scripts/check-doc-examples.py b/website/scripts/check-doc-examples.py index 2d7fa3122d..ae116befc1 100644 --- a/website/scripts/check-doc-examples.py +++ b/website/scripts/check-doc-examples.py @@ -209,8 +209,10 @@ def check(cpp_verify, job): f"\nactual output:\n{output.strip()[:2500]}"), False return None # A Lean export compiles the example itself; it has no lowering of its own. + # Lowering takes no Lean output. if "--backend=lean" not in arguments: - code, output = run(cpp_verify, source, ["--lower-only", *arguments], 300) + lowering = [a for a in arguments if not a.startswith("--lean-")] + code, output = run(cpp_verify, source, ["--lower-only", *lowering], 300) if code is None or code != 0 or "error:" in output: return f"{where}: the example does not compile:\n{output.strip()[:1500]}", False code, output = run(cpp_verify, source, diff --git a/website/source/book/part-ii/ch13-spec-and-proof-functions.rst b/website/source/book/part-ii/ch13-spec-and-proof-functions.rst index 0ae14c8742..c8462ceabf 100644 --- a/website/source/book/part-ii/ch13-spec-and-proof-functions.rst +++ b/website/source/book/part-ii/ch13-spec-and-proof-functions.rst @@ -1123,7 +1123,9 @@ there changes nothing, ``subrange`` clamps its bounds, and a key outside a map's domain maps to 0. Sequences are finite; sets, multisets, and maps range over all integers and may be infinite. Counterexamples show their values, such as ``s = [4, 3]`` or ``m = {1 -> 7, 4.. -> 2}``. Z3 and cvc5 decide all -four; Lean support for them is planned for a future release. +four, and a Lean project states them with core Lean types (a sequence is a +``List Int``, a set its membership test) and lemmas for each operation +(:doc:`ch17-backends-modular-calls`). Inductive predicates -------------------- diff --git a/website/source/book/part-ii/ch16-when-verification-fails.rst b/website/source/book/part-ii/ch16-when-verification-fails.rst index 40b383dfaa..e349049fa1 100644 --- a/website/source/book/part-ii/ch16-when-verification-fails.rst +++ b/website/source/book/part-ii/ch16-when-verification-fails.rst @@ -146,8 +146,10 @@ Every result that is not a proof carries one stable reason code (text - A loop has no termination measure. Add ``cppverify::decreases``, or ``cppverify::decreases(*)``. * - ``construct.unsupported`` - - The failed obligation stands for a construct the verifier does not - model; the message names it. + - The function uses a construct the verifier does not represent (an + error points at it), relies on a spec that does, or the failed + obligation stands for a construct it does not model; the message + names it. * - ``solver.timeout`` - A query, or the function's whole budget, ran out of time. Raise ``--timeout`` or ``--function-timeout``, or help the proof. @@ -214,7 +216,12 @@ Other results that are not counterexamples: - ``construct.unsupported``: the failed obligation stands for a construct the verifier does not model, so it says nothing about the program. The message names the construct, such as a pointer difference between two parameters' - objects or a call that keeps a pointer to local storage. + objects or a call that keeps a pointer to local storage. A function whose + body or contract the verifier does not represent at all gets this reason + too, with an error at the construct, and so does a function that relies on + a spec it does not represent (``relies on choice, which uses a construct + the verifier does not support``); the other functions of the file are + verified (see :doc:`../../language/tooling`). - ``[trusts=f]`` on a verified result: the proof relies on the contract of ``f``, marked ``[[cppverify::trusted]]`` (see :doc:`ch17-backends-modular-calls`). diff --git a/website/source/book/part-ii/ch17-backends-modular-calls.rst b/website/source/book/part-ii/ch17-backends-modular-calls.rst index 032f7b26aa..04fb031093 100644 --- a/website/source/book/part-ii/ch17-backends-modular-calls.rst +++ b/website/source/book/part-ii/ch17-backends-modular-calls.rst @@ -156,21 +156,22 @@ For an explicit interactive proof: # complete proof/CppVerify/Proofs/*/*.lean; put shared lemmas in User.lean cpp-verify --backend=lean --lean-project=proof --lean-certify file.cpp -``Generated.lean``, ``Auto.lean``, and ``Check.lean`` are machine-owned; -``Check.lean`` imports every proof, so ``lake build CppVerify.Check`` builds -them all at once. ``User.lean`` is never overwritten, and each goal has a proof +The generated modules (``Prelude.lean``, ``Specs.lean``, ``Goals/``, +``Generated.lean``, ``Auto.lean``, and ``Check.lean``) are machine-owned; +``Check.lean`` imports every proof, so ``lake build``, run in the project, +builds them all (see `One module of goals per function`_ below). +``User.lean`` is never overwritten, and each goal has a proof file of its own: the proof ``cppverify_auto`` finds for it, or a stub (see `Proofs cppverify_auto finds`_ below). cpp-verify rewrites that file only while nobody has edited it; an edited one it never rewrites, but moves with its goal (see `Proofs that follow their goals`_ below). The project pins ``leanprover/lean4:v4.35.0``, which certification runs whatever elan's default toolchain is; a project 0.1 generated, pinned to ``v4.32.2``, moves to -it with a warning. Certification compiles the user -module and each proof file with admissions promoted to errors, then checks the -compiled modules as described in `What certification checks`_ below. +it with a warning. Certification builds the project +and compiles each proof file with admissions promoted to errors, then checks +the compiled modules as described in `What certification checks`_ below. -Each goal in ``Generated.lean`` is a Lean proposition that means what the -obligation means, so a proof of it is a proof of the obligation: +Each goal is a Lean proposition that means what the obligation means, so a proof of it is a proof of the obligation: - every operator keeps its grouping under Lean's own precedence: ``(a ^ b) + 1`` is ``(a_0 ^^^ b_0) + 1``, since Lean reads @@ -260,11 +261,8 @@ prints: Certified: nonnegative [backend=Lean] Certified: small [backend=Lean] -The Lean backend does not yet translate the spec collections of -```` (``seq``, ``set``, ``multiset``, ``map``): a function whose -obligations use one is ``Unresolved`` with reason ``logic.unsupported`` on -it. Lean support for them is planned for a future release; until then such -functions are verified with Z3 (the default) or cvc5. +The spec collections of ```` reach Lean too (see +`Collections in Lean`_ below). To keep Z3 as the fast path and export only unresolved functions: @@ -281,8 +279,9 @@ proof. When every exported goal kernel-checks the function is ``Proved (z3+lean)``, or ``Proved (portfolio+lean)``, and JSON reports ``"status":"mixed-proof"`` with the per-obligation split. When Lean checked every obligation, because ``--lean-fallback-scope=all`` exported them all or -the solver proved none on its own, the function is ``Certified``. SAT counterexamples are failures, not -fallback candidates. A fallback run examines in Lean only what its solver +the solver proved none on its own, the function is ``Certified``. A certified +counterexample stays a failure, and the project shows it false in Lean too +(`Counterexamples in Lean`_). A fallback run examines in Lean only what its solver leaves unresolved, so the proofs of the obligations and functions the solver settles stay where they are until Lean needs them again. @@ -321,10 +320,73 @@ one does (``[trusts=f]``, ``[partial]``). It also gets the same vacuity checks, run with Z3: a goal that holds only because no execution reaches it is ``Certified: f [backend=Lean] [vacuous]``, with a warning. +Counterexamples in Lean +~~~~~~~~~~~~~~~~~~~~~~~ + +A counterexample the solver finds is a failure once the certifier has +evaluated the program's goal at it, with every spec at its true definition. A +fallback project shows each such failure false in Lean as well: its +refutation, ``CppVerify/Refutations//.lean``, states the +failing goal as the project would and proves it false at the values the +model gives its variables, machine integers as bit-vectors, memory as a +function of its runs of equal cells. ``--lean-certify`` checks it as it +checks a proof, with the kernel and Lean's foundations alone, and says so: + +.. cppverify-example: flags --lean-fallback={dir}/proof --lean-certify + +.. code-block:: cpp + + int add_one(int x) + cppverify::pre(x == 4) + cppverify::post(cppverify::result == x + 1) + { + return x + 2; + } + +.. code-block:: text + + error: verification failed: add_one [fn_5f5a376164645f6f6e6569::postcondition@3:37] (counterexample: result [ssa=__result_1] [type=i32] = 6, x [ssa=x_0] [type=i32] = 4; trace: return.return@5:3 value=6) [backend=z3] [reason=counterexample] (confirmed in Lean) + +The refutation applies the goal to the model's values and decides what is +left false, the definition instances the goal states with it, since they hold +of the spec functions Lean defines: + +.. code-block:: lean4 + + theorem cppverify_add_one_fc20c3_postcondition_8876761e_refuted : ¬ cppverify_add_one_fc20c3_postcondition_8876761e := by + intro h + have h₁ := h + (fun (a : Int) => (0 : Int)) + (fun (a : Int) => (0 : Int)) + (BitVec.ofInt 32 (6)) + (BitVec.ofInt 32 (4)) + cppverify_refute h₁ + +``cppverify_refute`` decides the instance, or evaluates it first, the spec +functions that are Lean definitions (`Spec functions in Lean`_) by their +equations, a store to memory with ``grind``, since it relates two heaps at +every address, and a quantifier over a range of at most 4096 integers by +trying each one (a larger range is not decided: each integer is a level of +recursion, and a billion of them took Lean 58 GB). +A goal that applies a spec Lean leaves opaque, such as a ``cppverify::choose``, +cannot be decided there: the failure stands, and a warning says so (``the +counterexample of pick_one is not confirmed in Lean: Lean cannot decide the +goal at the counterexample``). Should Lean decide the goal true at the +values, the solver's counterexample and Lean would disagree about one goal; +the function is then ``Unresolved`` with reason +``backend.inconsistent-results``, as when two solvers disagree, never +``Failed``. Each attempt of ``cppverify_refute`` has the heartbeats of a step +of ``cppverify_auto``, so a goal Lean could decide only by trying a billion +integers is not decided rather than tried for hours. Only failures that stand +once every verdict is settled report their refutations: a counterexample +checked with a spec's postcondition that is not established is no failure +(``spec.post``), and nothing is said of its refutation. A refutation is the +project's, rewritten by every run and removed with its failure. + Goals as hypotheses ~~~~~~~~~~~~~~~~~~~ -Next to each goal, ``Generated.lean`` states it as hypotheses and a +Next to each goal, its module states it as hypotheses and a conclusion. Each hypothesis is an assumption the goal makes, named by where it comes from, and a bridge from that form to the goal is proved in the same file. For this file: @@ -435,6 +497,197 @@ prints: A proof of the goal itself, as the one for ``small`` above, checks as well. +Spec functions in Lean +~~~~~~~~~~~~~~~~~~~~~~ + +A spec function that a goal applies is a Lean definition when Lean proves +that it terminates, and opaque otherwise, when a goal knows it only through +the definition instances it states. For this file: + +.. cppverify-example: flags --backend=lean --lean-project={dir}/proof + +.. code-block:: cpp + + cppverify::spec int tri(int n) + cppverify::decreases(n) + { + return n <= 0 ? 0 : tri(n - 1) + n; + } + + cppverify::proof void tri_nonneg(int n) + cppverify::post(tri(n) >= 0) + { + } + + cppverify::proof void tri_four() + cppverify::post(tri(4) == 10) + { + } + +``cpp-verify --backend=lean --lean-project=proof file.cpp`` prints: + +.. code-block:: text + + Exported: decreases: tri (cppverify_auto proved 1 of 1 goals) + Exported: lean obligation: tri_nonneg (cppverify_auto proved 1 of 2 goals) + Exported: lean obligation: tri_four (cppverify_auto proved 2 of 2 goals) + +and ``Specs.lean`` defines ``tri`` by its body: + +.. code-block:: lean4 + + def cppSpec_fn_5f5a3374726969 (n : Int) : Int := + if _h : n ≤ (0 : Int) then (0 : Int) else (cppSpec_fn_5f5a3374726969 (n - (1 : Int))) + n + termination_by ((n) + 1).toNat + decreasing_by all_goals cppverify_decreasing + +The definition is the spec's body, with each condition that reaches a +recursive application a hypothesis there, ``if _h : c`` (a ``&&``, ``||``, +or bounded ``cppverify::exists`` whose later part applies the recursion +becomes such an ``if``), so that Lean can prove the decrease from it. Its +measure is its ``cppverify::decreases``, each ``m`` as ``(m + 1).toNat`` and +lexicographic over several: exactly the relation cpp-verify's termination +check proves, where a call's measure may be -1 below a caller's 0. Specs +that apply each other are one ``mutual`` block. The body is never guarded by +the measure as a whole, so the definition has the spec's value at every +argument. + +Lean proves each decrease with ``cppverify_decreasing``, a prelude tactic: +``omega``, after unfolding the comparison and collection wrappers, or the +division and remainder of a measure that falls by a constant divisor +(``log2(n / 2)``), then ``simp_all`` and Lean's own ``decreasing_tactic``. + +The export tries the definitions with the pinned Lean. One that Lean rejects +stays opaque, as when the decrease rests on another spec's postcondition, +and one that applies it is tried again against that opaque declaration. +Lean's verdict on each is kept in ``.cppverify/specs``, so a later export +tries nothing; without Lean every spec is opaque. + +The goals are the same either way, since they state the same definition +instances. With the definition, ``cppverify_auto`` evaluates ``tri(4)`` +(``cppverify_auto_eval``, below), and a proof may unfold ``tri`` or argue by +induction over its recursion, with the principle Lean generates for it: + +.. code-block:: lean4 + + theorem tri_nonneg (m : Int) : 0 ≤ cppSpec_fn_5f5a3374726969 m := by + induction m using cppSpec_fn_5f5a3374726969.induct with + | case1 x h => rw [cppSpec_fn_5f5a3374726969]; simp [h] + | case2 x h ih => rw [cppSpec_fn_5f5a3374726969]; simp [h]; omega + + theorem cppverify_tri_nonneg_42a030_postcondition_053b1980_proof : + cppverify_tri_nonneg_42a030_postcondition_053b1980 := by + apply cppverify_tri_nonneg_42a030_postcondition_053b1980.of_readable + intro _ _ n _ _ _ + exact tri_nonneg _ + +With that proof, ``--lean-certify`` prints ``Certified`` for ``tri``, +``tri_nonneg``, and ``tri_four``. A verdict still rests on the termination +cpp-verify itself establishes: a spec whose own check is unresolved leaves +the goals that rely on it ``spec.termination``, whatever Lean defines. + +Collections in Lean +~~~~~~~~~~~~~~~~~~~ + +The spec collections of ```` reach Lean as values of core Lean +types, and each operation is a function of the generated prelude, total as +```` defines it: + +.. list-table:: + :header-rows: 1 + + * - C++ + - Lean + - Operations + * - ``seq`` + - ``List Int`` + - ``cppSeqEmpty``, ``cppSeqUnit``, ``cppSeqLength``, ``cppSeqAt`` (0 + outside the sequence), ``cppSeqPush``, ``cppSeqConcat``, + ``cppSeqSubrange`` (bounds clamped), ``cppSeqContains`` + * - ``set`` + - ``Int → Bool``, its membership test + - ``cppSetEmpty``, ``cppSetInsert``, ``cppSetRemove``, + ``cppSetContains``, ``cppSetUnion``, ``cppSetIntersect``, + ``cppSetDifference``, ``cppSetSubset`` + * - ``multiset`` + - ``Int → Nat``, its counts + - ``cppMultisetEmpty``, ``cppMultisetInsert``, ``cppMultisetRemove``, + ``cppMultisetCount`` + * - ``map`` + - ``Int → Option Int``, the value at each key it holds + - ``cppMapEmpty``, ``cppMapInsert``, ``cppMapRemove``, + ``cppMapContains``, ``cppMapGet`` (0 outside the domain) + +A set is its membership test, not a finite set, since a set of +```` may hold infinitely many integers; ``decide`` still +evaluates a membership of a set built from constants, as it evaluates a read +of a list. ``update`` and ``reverse`` are specs over these operations, as for +the solvers. + +The prelude also states what each operation means, in lemmas that ``simp`` +and ``grind`` use: ``cppSeqAt_push`` (``s.push(x)[i]`` is ``x`` at +``s.len()`` and ``s[i]`` elsewhere), ``cppSeqAt_concat``, +``cppSeqAt_subrange``, ``cppSeqLength_push``, ``cppSeqContains_push``, +``cppSeqSubrange_push`` (the subrange that drops a pushed element is the +sequence), ``cppSetContains_insert``, ``cppMultisetCount_remove``, +``cppMapGet_insert``, and the others for each operation. Two collections are +equal when they agree everywhere: ``cppSeq_ext`` (equal lengths and reads), +``cppSet_ext``, ``cppMultiset_ext``, and ``cppMap_ext``. For this file: + +.. cppverify-example: flags --backend=lean --lean-project={dir}/proof + +.. code-block:: cpp + + using cppverify::seq; + + cppverify::spec int total(seq s) + cppverify::decreases(s.len()) + { + return s.len() <= 0 ? 0 : total(s.subrange(0, s.len() - 1)) + s[s.len() - 1]; + } + + cppverify::proof void last(seq s, int x) + { + cppverify::check(s.push(x)[s.len()] == x); + cppverify::check(s.push(x).subrange(0, s.len()) == s); + } + + cppverify::proof void total_push(seq s, int x) + cppverify::post(total(s.push(x)) == total(s) + x) + { + } + +``cpp-verify --backend=lean --lean-project=proof file.cpp`` prints: + +.. code-block:: text + + Exported: decreases: total (cppverify_auto proved 1 of 1 goals) + Exported: lean obligation: last (cppverify_auto proved 3 of 3 goals) + Exported: lean obligation: total_push (cppverify_auto proved 2 of 2 goals) + +The postcondition of ``total_push`` needs the definition of ``total`` at +``s.push(x)``, which its goal states as the hypothesis +``cppverify_spec_axiom_1_1``; ``cppverify_auto`` unfolds it +(``cppverify_auto_unfold``, below), and the lemmas about ``push`` and +``subrange`` finish it. A proof by hand uses the same lemmas: + +.. code-block:: lean4 + + theorem cppverify_total_push_cdfd2d_postcondition_3a9fac6f_proof : + cppverify_total_push_cdfd2d_postcondition_3a9fac6f := by + apply cppverify_total_push_cdfd2d_postcondition_3a9fac6f.of_readable + intro _ _ s x pushed _ _ _ + rw [pushed] + unfold cppSpecBody_cdfd2da1_fn_5f5a35746f74616c4e396370707665726966793373657145_1 + have := cppSeqLength_nonneg s + simp [BitVec.toInt_signExtend_of_le] + omega + +Either way, ``--lean-certify`` prints ``Certified`` for ``total``, ``last``, +and ``total_push``. ``total`` is a definition (`Spec functions in Lean`_), so +a goal that needs induction over it, as ``total(s + t) == total(s) + +total(t)`` does, takes a proof by induction over ``total``'s recursion. + Proofs cppverify_auto finds ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -471,9 +724,10 @@ A goal it proves gets that proof, with a header that says so: .. code-block:: lean4 - automation: cppverify_auto 1 proves it with cppverify_auto_grind + automation: cppverify_auto 3 proves it with cppverify_auto_grind cppverify_auto proves this goal. cpp-verify rewrites this file only while it is unedited; a proof you write here is yours, kept as any other. -/ + import CppVerify.Goals.cppverify_next_e1e917 import CppVerify.User import CppVerify.Auto @@ -483,7 +737,7 @@ A goal it proves gets that proof, with a header that says so: cppverify_auto_grind Any other goal gets a stub, whose header says it was tried -(``automation: cppverify_auto 1 did not prove it``). Here those are the two +(``automation: cppverify_auto 3 did not prove it``). Here those are the two goals of ``square`` that need ``x * x`` bounded: nonlinear arithmetic, which core Lean does not decide. Certification checks the automatic proofs like any other, so ``--lean-certify`` prints ``Certified: next [backend=Lean]``, and @@ -499,7 +753,21 @@ first that proves the goal: - ``cppverify_auto_bv``: machine comparisons, constants, and overflow conditions are stated as facts about integers, then ``grind``; - ``cppverify_auto_decide``: ``decide``; -- ``cppverify_auto_simp``: ``simp_all``. +- ``cppverify_auto_simp``: ``simp_all``; +- ``cppverify_auto_bveq``: equalities of machine values, such as the + precondition ``x != y`` of two keys, and sign extensions are stated as + facts about integers too, then as ``cppverify_auto_bv``; +- ``cppverify_auto_ext``: an equality of two collections, element by + element (`Collections in Lean`_), then as ``cppverify_auto_bv``; +- ``cppverify_auto_unfold``: each definition instance the goal states, by + the body it names, then as ``cppverify_auto_bveq``; +- ``cppverify_auto_eval``: the spec functions that are Lean definitions + (`Spec functions in Lean`_), by their equations, and the division and + remainder of contracts and specs by their definitions, so that an + application at known arguments becomes its value; +- ``cppverify_auto_div``: the division and remainder of contracts and specs + by their definitions, then ``grind``, which decides them by a constant + divisor once their signs are split (``a / 2 >= 2`` for ``a >= 4``). A step counts only when it proves the goal with Lean's foundations alone, so an unfinished lemma of yours (proved by ``sorry``) never proves a goal, and a @@ -507,13 +775,15 @@ goal counts as proved only when the kernel accepted its proof. A goal it spends more than 20 seconds on, its kernel check included, gets a stub (``did not prove it within 20 s``; ``--lean-auto-timeout`` sets the limit). The steps and their order were chosen by trying candidates on every goal the -test suite exports: of the goals Z3 proves, ``cppverify_auto`` proves 90%; +test suite exports: of the goals Z3 proves, ``cppverify_auto`` proves 93%; most of the rest need nonlinear arithmetic, induction over a spec, or a quantified frame. A goal is tried once: a later export tries a stub again only if a later -release of cpp-verify brings a new revision of ``cppverify_auto`` (the ``1`` -in the header), and never once you edit it. You can help it: a lemma in +release of cpp-verify brings a new revision of ``cppverify_auto`` (the ``3`` +in the header), and never once you edit it. Revisions 2 and 3 added the last +five steps, after the others, so that a goal an earlier step proves keeps its +proof; a stub that an earlier revision wrote is tried again. You can help it: a lemma in ``User.lean`` tagged ``@[grind]`` or ``@[simp]`` is used by its steps, since the proof files import ``User.lean``; delete a stub to have it tried again. Your own proofs may use ``cppverify_auto``, any of its steps, and the parts @@ -572,6 +842,103 @@ that cpp-verify 0.1 wrote for fn_5f5a3373697876::postcondition@8:37 is now CppVerify/Proofs/cppverify_six_6a2cf3/cppverify_six_6a2cf3_postcondition_198bc076.lean``. A 0.1 proof of a goal the source no longer has is kept in ``Stale/``. +One module of goals per function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each function's goals have a Lean module of their own, so that a change to +one function builds again only what depends on it. For this file: + +.. cppverify-example: flags --backend=lean --lean-project={dir}/proof + +.. code-block:: cpp + + cppverify::spec int tri(int n) + cppverify::decreases(n) + { + return n <= 0 ? 0 : tri(n - 1) + n; + } + + int inc(int x) + cppverify::pre(0 <= x && x < 1000) + cppverify::post(cppverify::result == x + 1) + { + return x + 1; + } + + int tri_zero() + cppverify::post(cppverify::result == tri(0)) + { + return 0; + } + +``cpp-verify --backend=lean --lean-project=proof file.cpp`` prints: + +.. code-block:: text + + Exported: decreases: tri (cppverify_auto proved 1 of 1 goals) + Exported: lean obligation: inc (cppverify_auto proved 3 of 3 goals) + Exported: lean obligation: tri_zero (cppverify_auto proved 2 of 2 goals) + +and writes: + +.. code-block:: text + + proof/ + lakefile.toml + lean-toolchain + CppVerify.lean the library's root, which imports Check + CppVerify/ + Prelude.lean the heap model, the comparisons, the bridge lemmas + Specs.lean each spec the goals apply, declared once + Goals/ + cppverify_tri_decreases_f55d4c.lean + cppverify_inc_c0810e.lean + cppverify_tri_zero_e086e9.lean + Generated.lean imports every goal module + Auto.lean cppverify_auto + User.lean your lemmas, never rewritten + Proofs/ + cppverify_inc_c0810e/ + cppverify_inc_c0810e_postcondition_e4ef4574.lean + ... + Check.lean imports every proof + +``Specs.lean`` declares each spec function the goals apply once, ``tri`` +here, by its definition (`Spec functions in Lean`_). Each goal module imports +it and defines the bodies its goals unfold under its own prefix, as in +``cppSpecBody_e086e9ab_fn_5f5a3374726969_1``, so any goal modules can be +imported together. A proof imports its function's goals, your lemmas, and +the automation: + +.. code-block:: lean4 + + import CppVerify.Goals.cppverify_inc_c0810e + import CppVerify.User + import CppVerify.Auto + +A new ``User.lean`` imports ``CppVerify.Specs``, so a lemma there may speak +of the specs and of the prelude; a lemma about one function's goals belongs +in a module of yours that imports that function's goals. A ``User.lean`` of +an earlier release imports ``CppVerify.Generated``, which still imports every +goal module: its proofs keep working, but each then depends on every goal. + +A generated file is written only when its text changes, so exporting an +unchanged source writes nothing, and ``lake build`` or your editor builds +again only what a change reaches. Certification builds the generated +modules, ``User.lean``, and ``Auto.lean`` with Lake, then compiles each proof +file with admissions promoted to errors, unless nothing it is built from +changed since it last passed: its text and the compiled modules of the +project it imports. After a change to the postcondition of ``inc``, it +compiles again the goal module of ``inc``, ``Generated.lean``, and the three +proofs of ``inc``, and none of the others. + +A run writes the goal module of each function it exports, and removes any +other: a function you delete takes its module with it, and so does a +function that a ``--lean-fallback`` run leaves to its solver, since +``Specs.lean`` then declares only what the exported functions apply. The +proofs of such a function stay where they are, and build again once its +goals are exported again. + What certification checks ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -582,11 +949,17 @@ user's source. For each goal: - the kernel replays every declaration of every module of the project the proof imports, as ``leanchecker`` does, so nothing escaped the kernel; only - the pinned toolchain's own modules are trusted; + the pinned toolchain's own modules are trusted, and they are imported once + for all the goals, however many of their proofs import ``Lean``; +- no module may declare a name that an earlier one declares otherwise, and + the goal modules come first, so a proof file that defines a goal of its + own under the goal's name is rejected; - the proof ``G_proof`` must be a theorem whose type is exactly the goal - ``G`` that ``CppVerify.Generated`` defines; + ``G`` that its function's goal module defines; - the axioms it depends on, collected from the constants it uses, must be - among ``propext``, ``Quot.sound``, and ``Classical.choice``. + among ``propext``, ``Quot.sound``, and ``Classical.choice``; +- no declaration of ``User.lean`` may use ``sorry``: an admitted lemma there + fails every goal. Each function is certified on its own goals. For this file, where ``one`` claims a false postcondition: @@ -629,8 +1002,11 @@ for the rest, gives: A theorem of the goal added by a command with ``debug.skipKernelTC``, which skips the kernel, is rejected at its replay (``the kernel rejects CppVerify.Lemmas: while replaying declaration 'cheat': (kernel) declaration -type mismatch``), and a user axiom behind a lemma by its name (``depends on -the axiom escape, which is not one of Lean's foundations``). +type mismatch``), a proof file that defines a goal of its own under the +goal's name at its replay too (``...cppverify_one_99bcee_postcondition_646d2761 declares +cppverify_one_99bcee_postcondition_646d2761, which +CppVerify.Goals.cppverify_one_99bcee declares``), and a user axiom behind a lemma by its name (``depends on the +axiom escape, which is not one of Lean's foundations``). Portable obligation archives ---------------------------- @@ -644,8 +1020,9 @@ The canonical backend boundary can be persisted and replayed: cpp-verify --obligation-in=goals.cpv --backend=portfolio cpp-verify --obligation-in=goals.cpv --lower-only --dump-ir=3,4 cpp-verify --obligation-in=goals.cpv --backend=lean --lean-out=goals.lean + cpp-verify --obligation-in=goals.cpv --lean-project=proof --lean-certify -Each ``cppverify.obligation/2`` record uses stable wire tags, defensive bounds, +Each ``cppverify.obligation/3`` record uses stable wire tags, defensive bounds, portable source points, and path-independent SHA-256 semantic hashes. Replay revalidates every sort, expression, logical declaration, call signature, feature declaration, and identity before backend dispatch. Multiple records may @@ -667,27 +1044,119 @@ and its replay side by side. Source-built and replayed records pass through the same conservative canonicalizer before hashing and dispatch. It folds only trivial Boolean structure and reflexive equality/inequality, removes transitively unreachable -logical declarations, rebuilds exact ordered and complete queries plus feature +logical declarations (a declaration reaches what its definition and its +termination measures apply), rebuilds exact ordered and complete queries plus feature metadata, and revalidates. It does not rewrite arithmetic, quantifiers, pointers, heaps, or assumptions. Canonicalization introduced semantic-hash format v2; format v3 removed diagnostic obligation identities from the preimage, and format v4 removes every obligation kind except ``unwinding``. -Schema v2 adds precise obligation kinds; schema v1 records remain readable. +Schema v2 adds precise obligation kinds, and schema v3 each spec function's +termination measures and where each assumption of a goal comes from (what a +Lean project needs to define a spec and to name a goal's hypotheses); none +of these is hashed, and records of schemas v1 and v2 remain readable. BMC source verification archives only the terminal module for each function. Those records retain their exact explored unroll bound. Replay is fixed-bound and uses BMC unwinding aggregation, so an incomplete frontier remains ``BoundedSafe(N)`` rather than rerunning the incremental source strategy. BMC cannot be applied to an untransformed record because loop unrolling is a VCR -transformation that occurs before canonical obligation construction. Lean -scratch replay of bounded records is rejected until the generated theorem -format carries the same transform provenance. +transformation that occurs before canonical obligation construction. Failure-only ``cppverify::recommends`` checks are not archived, so bytes do not depend on whether a prior solver run succeeded or failed. The dependency-scoped proof cache is shared by direct verification and replay, so identical canonical goals reuse proofs without trusting source paths or diagnostic identities. +Archives in a Lean project +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``--lean-project`` with ``--obligation-in`` writes the archive's modules to a +Lean project as a source run writes its functions: each goal has the name, +module, and proof file the same obligation has from source, since both name +it by its statement, and the specs are the same Lean definitions. A proof +written for the source certifies the archive's goal. For the specs of +`Spec functions in Lean`_, + +.. code-block:: bash + + cpp-verify --lower-only --obligation-out=goals.cpv file.cpp + cpp-verify --obligation-in=goals.cpv --lean-project=replay + +prints + +.. code-block:: text + + Exported: lean obligation: tri.decreases (cppverify_auto proved 1 of 1 goals) [backend=lean] [semantic-hash=sha256:...] + Exported: lean obligation: tri_nonneg (cppverify_auto proved 1 of 2 goals) [backend=lean] [semantic-hash=sha256:...] + +and ``replay`` has the goals, ``Specs.lean``, and proof files of +``cpp-verify --backend=lean --lean-project=proof file.cpp``. With the proof of +``tri_nonneg`` by induction in its file, ``--lean-certify`` prints + +.. code-block:: text + + Proved obligations: tri.decreases (dependencies not checked in replay) [backend=lean] [semantic-hash=sha256:...] + Proved obligations: tri_nonneg (dependencies not checked in replay) [backend=lean] [semantic-hash=sha256:...] + +As with a solver, replay checks each module alone, so a module whose goals +all certify has its obligations proved, never ``Certified``, which would +also say that what the verdict rests on is established. Automation, moving +and keeping proofs in ``Stale/``, ``--lean-prune-stale``, and ``--jobs`` work +as for a source project; ``--lean-fallback`` is for source verification +only. + +A bounded record of BMC exports too, to a project or to the scratch-pad. +The goals of each bounded module say its bound: + +.. code-block:: lean4 + + /- function: count_up -/ + /- required logic: mathematical-integers, bit-vectors, pointers, heap-arrays -/ + /- bound: 3 loop iterations, by unrolling: these goals speak only of the + executions within them, and an unwinding goal says that every loop + ends within them -/ + +Its result is read as BMC reads one: every goal certified, its unwinding +goals included, proves the obligations, and every goal but some unwinding +goal certified is ``BoundedSafe``: no execution within the bound fails. Lean +proves goals and never refutes one, so a Lean ``BoundedSafe`` says that the +unwinding goals are not certified, not that they fail. For two loops +archived by ``cpp-verify --backend=bmc --unroll=3``, + +.. cppverify-example: flags --backend=bmc --unroll=3 + +.. code-block:: cpp + + int sum_small(int n) + cppverify::pre(0 <= n && n <= 2) + cppverify::post(cppverify::result >= 0) + { + int s = 0; + for (int i = 0; i < n; i = i + 1) + s = s + i; + return s; + } + + int count_up(int n) + cppverify::pre(0 <= n && n <= 1000) + cppverify::post(cppverify::result == n) + { + int c = 0; + while (c < n) + c = c + 1; + return c; + } + +``--lean-certify`` prints + +.. code-block:: text + + Proved obligations: sum_small (dependencies not checked in replay) [backend=lean, bound=2] [semantic-hash=sha256:...] + loops.cpp:16:3: BoundedSafe: count_up (every goal is certified but the unwinding: no execution within 3 loop iterations fails (the proof of fn_5f5a38636f756e745f757069::unwinding@16:3: ...)) [backend=lean, bound=3] [reason=bmc.incomplete-bound] [semantic-hash=sha256:...] + +``count_up`` loops up to 1000 times, so its unwinding goal at the bound 3 is +false, as BMC finds (``BoundedSafe: count_up [backend=bmc, bound=3]``). + Modular function calls ---------------------- diff --git a/website/source/language/limitations.rst b/website/source/language/limitations.rst index 1dcaa09ded..3b58f00f81 100644 --- a/website/source/language/limitations.rst +++ b/website/source/language/limitations.rst @@ -29,11 +29,15 @@ This page separates four different situations: return ``unknown``. Outside the supported subset the verifier *fails closed*: it never reports a -proof for what it does not model. Depending on the construct, the run stops -with an error that names it, and no function of the file gets a verdict, or -the function is ``Unresolved`` with reason ``construct.unsupported``. A -counterexample is never a way of failing closed: ``Failed`` always means a -checked counterexample. +proof for what it does not model. A construct it does not represent is an +error at the construct, and its function is ``Unresolved`` with reason +``construct.unsupported`` while every other function of the file is verified +(callers that rely on its contract are ``callee.contract``, and functions +that reach a spec it cannot represent are ``construct.unsupported`` too); one +it represents but does not model, such as a pointer difference between two +parameters' objects, makes the obligation that stands for it unsupported, and +the function is ``Unresolved`` with the same reason. A counterexample is never +a way of failing closed: ``Failed`` always means a checked counterexample. Meaning of a result ------------------- @@ -426,8 +430,7 @@ Current proof-language limitations include: sharing ``cppverify::decreases`` clauses of one length); otherwise they are ``proof.cycle``; - spec collections hold mathematical integers only (no nested collections or - records), and Lean does not support them yet (planned for a future - release); + records); - user-written loop invariants, with no candidate-invariant/Houdini pass; - no automatic termination-measure or lemma discovery. Inductions over a goal are tried automatically (by the measure of a recursive spec it applies, with @@ -580,9 +583,10 @@ goal per ordered obligation. Generated files are separated from preserved user lemmas/proofs. The pinned admission-free kernel path reports ``Certified`` only after every active proof checks and rejects user axiom/opaque shortcuts. Memory checks are lowered on this path as well as on Z3/cvc5/portfolio/BMC. -Spec collections are not yet supported on Lean (``logic.unsupported``); Lean -support for them is planned for a future release, and Z3 and cvc5 decide them -today. +Spec collections are Lean values of core types (a sequence a ``List Int``, a +set its membership test, a multiset its counts, a map the value at each key), +with a lemma for each operation; a goal over them that needs induction, as +over a recursive spec of a sequence, takes a proof by hand. Automation, proof ergonomics, semantic simplification, dependency-aware caching, and broader future C++ theories remain incomplete. diff --git a/website/source/language/tooling.rst b/website/source/language/tooling.rst index a7082dc386..d5a127198f 100644 --- a/website/source/language/tooling.rst +++ b/website/source/language/tooling.rst @@ -72,8 +72,10 @@ printed ``error: invalid argument '-std=c++17' not allowed with 'C'``. A construct that cpp-verify does not represent yet, in a function it verifies, is an error at that construct, shown with its line and a caret as a -compiler error is. Like a compiler error it stops the file: no function of -the file gets a verdict (:doc:`limitations` lists what is not represented). +compiler error is. It costs that function alone: the function is +``Unresolved`` with reason ``construct.unsupported``, every other function of +the file is verified, and ``cpp-verify`` exits with status 1 +(:doc:`limitations` lists what is not represented). .. cppverify-example: rejects @@ -85,17 +87,51 @@ the file gets a verdict (:doc:`limitations` lists what is not represented). switch (x) { case 1: return 1; default: return 0; } } + int bump(int x) + cv::pre(x < 100) + cv::post(cv::result == x + 1) + { + return x + 1; + } + + int picked(int x) + cv::post(cv::result >= 0) + { + int r = pick(x); + return r; + } + .. code-block:: text pick.cpp:4:3: error: pick: unsupported statement: SwitchStmt 4 | switch (x) { case 1: return 1; default: return 0; } | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Verified: bump [backend=z3] + Unresolved: picked [backend=z3] [reason=callee.contract] (relies on the contract of pick (it uses a construct the verifier does not support), which is not established) + +What others may rely on stays: + +- a caller of an executable or proof function whose body is not represented + is verified against its contract, which nothing establishes, so it is + ``Unresolved`` with reason ``callee.contract``, as ``picked`` above; +- a function whose contract is not represented leaves nothing to rely on: a + call of it is ``construct.unsupported`` where it is (``a call of lookup, + whose contract is not available``); +- a spec whose body or contract is not represented has no definition, and a + function that reaches it, through its body, its contract, or a callee's + contract, is not verified: ``Unresolved`` with reason + ``construct.unsupported`` (``relies on choice, which uses a construct the + verifier does not support``). An uncontracted ``constexpr`` helper that + verified code calls is such a spec. An error about the function as a whole, such as a recursion without a -measure, points at its name. With ``--diagnostics-format=json`` the record -carries the range, here ``"source":{"column":3,"end_column":53,"end_line":4, -"file":"pick.cpp","line":4}``. Before 0.2 such an error named only the -function: ``error: pick: unsupported statement: SwitchStmt``. +measure, points at its name. With ``--diagnostics-format=json`` the record of +a function's first error carries its verdict and the range, here +``"function":"pick"``, ``"status":"unresolved"``, +``"reason":"construct.unsupported"``, and ``"source":{"column":3, +"end_column":53,"end_line":4,"file":"pick.cpp","line":4}``. Before 0.2 such an +error stopped the file, so that no function of it got a verdict, and named +only the function: ``error: pick: unsupported statement: SwitchStmt``. Stopping a run with Ctrl-C ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -286,7 +322,13 @@ Backends output uses one shared preamble and identity-qualified theorem names. * - ``--lean-project=DIR`` - With ``--backend=lean``, which it selects, generate a pinned editable project. Generated - semantics live in ``CppVerify/Generated.lean``; reusable lemmas and + semantics live in a module per function, ``CppVerify/Goals/.lean``, + with each spec they apply declared once in ``CppVerify/Specs.lean``: by + its definition when the pinned Lean proves that it terminates, the + verdict kept in ``.cppverify/specs``, and opaque otherwise; a + proof imports its function's module, so certification compiles again + only the proofs of a function whose goals changed, and a generated + file is written only when its text changes. Reusable lemmas and per-obligation proof files are preserved across regeneration. A goal is named by its function and the hash of its statement (``cppverify_one_99bcee_postcondition_cd6bdfcd``), and its proof file, @@ -341,6 +383,12 @@ Backends function whose exported goals all kernel-check reports ``Proved (z3+lean)`` (JSON ``"status":"mixed-proof"`` with an ``evidence`` record), or ``Certified`` when Lean checked every goal. + A certified counterexample stays a failure, and the project gets its + refutation, ``CppVerify/Refutations//.lean``; with + ``--lean-certify`` a refutation that checks adds ``(confirmed in + Lean)`` to the failure (JSON ``"lean_refuted": true``), one Lean + cannot decide a warning, and one Lean decides the other way makes the + function ``Unresolved`` (``backend.inconsistent-results``). * - ``--lean-fallback-scope=unproved|all`` - ``unproved`` (default) exports only the obligations the solver did not prove individually; each keeps the goal name it has in a complete @@ -361,7 +409,7 @@ Backends - Milliseconds of wall-clock time ``cppverify_auto`` may spend on one new goal, its kernel check included (default 20000; ``0`` disables). A goal that takes longer gets a stub, as in ``automation: - cppverify_auto 1 did not prove it within 20 s``. Requires + cppverify_auto 3 did not prove it within 20 s``. Requires ``--lean-project`` or ``--lean-fallback``. * - ``--unroll=N`` - Maximum BMC loop bound, which selects ``--backend=bmc``. Source verification @@ -461,11 +509,13 @@ Backends concatenated in one archive. * - ``--obligation-in=FILE`` - Validate and replay an archive without reparsing C++. Supports Z3, cvc5, - strict portfolio, ``--lower-only``/Layer 3-4 dumps, and Lean scratch - export. Archives + strict portfolio, ``--lower-only``/Layer 3-4 dumps, Lean scratch + export, and Lean projects (``--lean-project``, with certification). + Archives produced after BMC unrolling retain their bound and replay with BMC - unwinding semantics; applying BMC to an untransformed archive is - rejected because unrolling must run before obligation lowering. + unwinding semantics, in Lean too; applying BMC to an untransformed + archive is rejected because unrolling must run before obligation + lowering. Each module is replayed on its own, without the callee and trusted contracts, spec facts, partiality, and vacuity its source verdict rests on: a module whose obligations all hold reports ``Proved @@ -777,13 +827,26 @@ against the definitions the archive carries. Before 0.2, replay printed run did not verify the function, for example when a callee's verification failed. -``--obligation-out`` writes ``cppverify.obligation/2`` records only after exact +An archive replays into a Lean project with ``--lean-project``, optionally +with ``--lean-certify``, ``--lean-prune-stale``, ``--no-lean-auto``, +``--lean-auto-timeout``, and ``--jobs``: the project gets the goals, specs, +and proof files a source run of the same program gives, and a module whose +goals all certify is ``Proved obligations`` ``[backend=lean]``. A bounded +record of BMC goes to Lean too, its bound stated with its goals; its +module is ``BoundedSafe`` when every goal but some unwinding goal +certifies (:doc:`../book/part-ii/ch17-backends-modular-calls`). +``--lean-fallback`` applies to source verification only. + +``--obligation-out`` writes ``cppverify.obligation/3`` records only after exact serialize/deserialize/validate/reserialize checks. Stable wire tags make the format independent of C++ enum ordinals. The reader rejects malformed magic, unsupported versions, truncation, invalid tags, inconsistent feature declarations, duplicate identities, and oversized/deep expressions. -Schema v2 adds the precise obligation kinds; schema v1 records, which name -only assertions, postconditions, and unwinding checks, are still read. +Schema v2 adds the precise obligation kinds; schema v3 each spec function's +termination measures, read like its definition, and, with source metadata, +where each assumption of a goal comes from, which names a hypothesis of a +readable Lean goal. Records of schema v2, and of schema v1, which name only +assertions, postconditions, and unwinding checks, are still read. Records cap integer widths at 4096 bits, expression depth at 4096, and collections plus expression nodes/edges at 100,000 per record. It also rejects embedded NULs, non-canonical numerals, inactive payload fields, ill-scoped @@ -793,7 +856,8 @@ backend dispatch. Module and per-obligation SHA-256 hashes omit source paths and display-only names, source ranges, internal positional and public source-anchored IDs, -obligation kinds other than ``unwinding``, and traces, so moving unchanged +obligation kinds other than ``unwinding``, traces, measures, and where +assumptions come from, so moving unchanged source, inserting an unrelated earlier obligation, or changing display metadata preserves an individual goal's semantic identity. Archives still diff --git a/website/source/release-notes.rst b/website/source/release-notes.rst index 42ebaaa422..bb79ffd0b1 100644 --- a/website/source/release-notes.rst +++ b/website/source/release-notes.rst @@ -106,8 +106,8 @@ a false postcondition, and so did a theorem of the goal added by a command with ``debug.skipKernelTC`` and checked by no kernel, in the proof file or in a module it imports. Certification now checks the compiled modules with a program that elaborates none of the user's source: the kernel replays every -module of the project a proof imports, the proof's type must be exactly its -goal, and its axioms are collected from the constants it uses. A failing +module of the project a proof imports, into one environment with the +toolchain's modules imported once, the proof's type must be exactly its goal, and its axioms are collected from the constants it uses. A failing proof also no longer fails every function of the project: each function is certified on its own goals (:doc:`book/part-ii/ch17-backends-modular-calls`). @@ -308,6 +308,16 @@ SwitchStmt``. It now gives the file, line, and column, and shows the line with a caret under the construct, as a compiler error does; its JSON record carries the range (:doc:`language/tooling`). +**An unsupported construct costs its function alone.** One construct the +verifier does not represent, such as a ``switch``, stopped the file: no +function of it was verified. Now that function is ``Unresolved`` with reason +``construct.unsupported``, its first error's JSON record carrying the verdict, +and every other function is verified. A caller that relies on its contract is +``callee.contract``; a function whose contract is not represented leaves a +call of it ``construct.unsupported``; a function that reaches a spec the +verifier does not represent is ``construct.unsupported``. The exit status is 1 +as before (:doc:`language/tooling`). + **Z3 errors.** A Z3 call that failed went unnoticed, and a later step could report something unrelated. The query now ends ``Unresolved`` with reason ``encoding.failed`` and Z3's message (:doc:`language/tooling`). @@ -385,6 +395,78 @@ proofs side by side and checks them in one Lean environment: the 33 goals of one test took 204 s and now take 12 s (:doc:`book/part-ii/ch17-backends-modular-calls`). +**A Lean module per function.** A Lean project kept every goal of every +function in ``CppVerify/Generated.lean``, which every proof imported, so a +change to one function built every proof again, and certification compiled +every proof on every run. Each function's goals now have a module of their +own, ``CppVerify/Goals/.lean``, the specs they apply are declared +once, in ``CppVerify/Specs.lean``, and a proof imports its function's goals, +``User``, and ``Auto``. A generated file is written only when its text +changes, and certification compiles a proof again only when it or a module +it imports changed: after a change to one of five functions, it compiles +that function's three proofs. ``lake build``, which failed for want of the +library's root module, builds every proof. A ``User.lean`` that 0.1 wrote, +which imports ``CppVerify.Generated``, keeps working +(:doc:`book/part-ii/ch17-backends-modular-calls`). + +**Collections in Lean.** A function whose obligations use a ``seq``, ``set``, +``multiset``, or ``map`` was ``Unresolved`` with reason ``logic.unsupported`` +under ``--backend=lean`` and ``--lean-fallback``. They are now Lean values of +core types: a sequence is a ``List Int``, a set its membership test +(``Int → Bool``, since a set may be infinite), a multiset its counts, and a +map the value at each key it holds. Each operation is a function of the +generated prelude with the total meaning of ````, and lemmas +state what each means for ``simp``, ``grind``, and proofs by hand. +``cppverify_auto`` revision 2 adds three steps after the others: machine +equalities as integer facts, collection equality element by element, and the +definition instances a goal states, by the bodies they name. Of the 112 goals +over collections in the test suite that Z3 proves, it proves 104, and +revision 1 86; most of the rest need induction over a recursive spec. Of +every goal the suite exports that Z3 proves, it proves 93.3%, where revision 1 +proves 90.4%, and none fewer. Stubs that revision 1 wrote are tried again +(:doc:`book/part-ii/ch17-backends-modular-calls`). + +**Spec functions as Lean definitions.** Every spec function of a Lean project +was opaque, known to a goal only through the definition instances it states, +so a goal that needed a spec at another argument, or induction over it, could +not be proved. A spec is now a Lean definition when the pinned Lean proves +that it terminates by its ``cppverify::decreases`` measures, with exactly the +spec's value at every argument, and stays opaque otherwise. A proof may unfold +it or argue by induction over its recursion, and ``cppverify_auto`` revision 3 +evaluates its applications at known arguments. Revision 3 also reads the +division and remainder of contracts and specs by their definitions, which +proves goals such as ``a / 2 >= 2`` for ``a >= 4``. Lean's verdict on each +spec is kept in the project, so a later export tries nothing +(:doc:`book/part-ii/ch17-backends-modular-calls`). + +**Lean projects from archives.** An obligation archive could reach Lean only +as the unchecked scratch-pad, and a bounded archive of BMC not at all. +``--obligation-in`` with ``--lean-project`` now writes a Lean project as a +source run of the same program does, with the same goals, specs, and proof +files, so a proof written for the source certifies the archive's goal; +``--lean-certify`` reports each module whose goals all certify as ``Proved +obligations`` ``[backend=lean]``, since replay checks a module alone. A +bounded module states its bound with its goals, in a project and in the +scratch-pad, and its result is read as BMC reads one: it is ``BoundedSafe`` +when every goal but some unwinding goal certifies. Archives record each spec +function's termination measures and the source of each assumption of a goal, +which a project needs to define the spec and to name the goal's hypotheses; +a module keeps the specs its specs' measures apply +(:doc:`book/part-ii/ch17-backends-modular-calls`). + +**Counterexamples confirmed in Lean.** A counterexample the solver found was +checked by cpp-verify's own evaluator only. A fallback project now holds the +refutation of each certified failure, the failing goal stated as the project +states it and proved false at the model's values; ``--lean-certify`` checks it +with the kernel, and a failure it shows reads ``(confirmed in Lean)``. A +counterexample Lean cannot decide, as one that applies a ``cppverify::choose``, +stays a failure with a warning, and one Lean decides the other way would be +``Unresolved``, a disagreement, never a failure. Lean decides a quantifier +over a range of at most 4096 integers by trying each one, within the +heartbeats of a step of ``cppverify_auto``. The release gate's Lean row now +decides every case of its matrix: each valid one certified, each invalid one +refuted (:doc:`book/part-ii/ch17-backends-modular-calls`). + **Lean 4.35.0.** Lean projects pin ``leanprover/lean4:v4.35.0``. A project 0.1 generated, pinned to ``v4.32.2``, moves to the new pin the next time cpp-verify writes it, with a warning; a proof that no longer builds needs updating for that @@ -399,8 +481,11 @@ refuses a ``lake`` outside elan that reports another Lean Data formats ~~~~~~~~~~~~ -Archives stay ``cppverify.obligation/2`` and JSON diagnostics -``cppverify.diagnostic/1``. JSON records may carry the status +Archives are ``cppverify.obligation/3``, which adds measures and the +sources of assumptions and reads ``/2`` and ``/1``; JSON diagnostics stay +``cppverify.diagnostic/1``. A module whose specs' measures apply other specs +keeps them, so its module hash changes; per-obligation hashes, and the proof +cache keyed by them, do not. JSON records may carry the status ``proved-obligations`` (archive replay) and the reason ``interrupted`` (Ctrl-C). Lean projects pin ``leanprover/lean4:v4.35.0``, and each proof file records the hash of its goal's statement (``cppverify-obligation-statement``