From 128a6cb2551089334b9c853c422ff2c7de137b4e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 22:43:52 +0530 Subject: [PATCH 001/191] verify: Replay a Lean project into one kernel environment, importing the toolchain once. --- clang/lib/Verify/Driver/LeanCertifier.h | 161 +++++++++++++----------- 1 file changed, 86 insertions(+), 75 deletions(-) diff --git a/clang/lib/Verify/Driver/LeanCertifier.h b/clang/lib/Verify/Driver/LeanCertifier.h index dc62393db9..2a1cf1f5b0 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] @@ -56,35 +56,92 @@ unsafe def moduleConstants (module : Name) : 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 +/-- A module of the project, read from its compiled files. -/ +structure ProjectModule where + name : Name + imports : Array Import + constants : Std.HashMap Name ConstantInfo -/-- Replays a module's own constants into the environment of its imports. -/ -unsafe def replayModule (module : Name) : IO Unit := do +/-- 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, constants) ← moduleConstants module - replayInto imports constants + for i in imports do + Closure.visit toolchain i.module + modify fun c => { c with modules := c.modules.push { name := module, imports, 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 the generated module defines and depends on no axiom outside the allowed ones. -/ -def checkStatement (env : Environment) (goal module : Name) (allowed : NameSet) : +def checkStatement (project : Replayed) (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 env := project.env let proof := goal.appendAfter "_proof" 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}" @@ -100,60 +157,14 @@ 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. -/ +and why it failed. A goal is proved when its proof module and the generated +module pass the kernel, with every module of the project they import, and +its proof states exactly the goal and uses only the allowed axioms. The +generated module is replayed first, so that a proof file that declares a +name it 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 @@ -164,16 +175,16 @@ unsafe def main (args : List String) : IO UInt32 := do 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 + let roots := #[`CppVerify.Generated] ++ entries.map (·.2) + let (_, closure) ← (roots.forM (Closure.visit toolchain)).run {} + let project ← replayProject closure + for (goal, module) in entries do + let why := (project.rejected[module]?).orElse fun _ => + (project.rejected[`CppVerify.Generated]?).orElse fun _ => + checkStatement project goal module allowed + match why with | 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) return 0 )lean"; From 62cac8d2ffbddfcf2373563d8533ad6174633152 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 22:43:52 +0530 Subject: [PATCH 002/191] test: Add a program whose first goal sorts before the other function's goals. --- .../test/Verify/suite/Inputs/lean_certifier_first.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/lean_certifier_first.cpp 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; +} From 9d85f6e1c4d8959955ea7081e60b70e24f7d7f85 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 22:43:52 +0530 Subject: [PATCH 003/191] test: Add a proof of the first goal that defines a goal of its own under its name. --- .../first/cppverify_early_103f65_postcondition_646d2761.lean | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/lean_certifier_proofs/first/cppverify_early_103f65_postcondition_646d2761.lean 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 From fe6fc92ee3e6c24d340be9c29633cc29d05dd1ca Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 22:43:53 +0530 Subject: [PATCH 004/191] test: Expect a proof file that redeclares its goal to be the module left out, also when it comes first. --- clang/test/Verify/suite/lean_certifier.test | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/clang/test/Verify/suite/lean_certifier.test b/clang/test/Verify/suite/lean_certifier.test index b738cacb2d..353324ad67 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.Generated 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.Generated 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) From f716cd7a9aa91317b7039f09765c415d9814c2d5 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 22:43:53 +0530 Subject: [PATCH 005/191] test: Add a wrapper that bounds the peak memory of the processes a command runs. --- clang/test/Verify/suite/Inputs/peak_memory.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/peak_memory.py diff --git a/clang/test/Verify/suite/Inputs/peak_memory.py b/clang/test/Verify/suite/Inputs/peak_memory.py new file mode 100644 index 0000000000..9584b83d06 --- /dev/null +++ b/clang/test/Verify/suite/Inputs/peak_memory.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +"""Runs a command, then prints whether the largest process it ran stayed +below a resident memory bound, in MB, and exits with the command's status. + +usage: peak_memory.py MB COMMAND... + +Linux reports ru_maxrss in kilobytes, for every descendant waited for.""" +import resource +import subprocess +import sys + +limit = int(sys.argv[1]) +done = subprocess.run(sys.argv[2:]) +peak = resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss // 1024 +if peak < limit: + print("peak memory below {} MB".format(limit)) +else: + print("peak memory {} MB, not below {} MB".format(peak, limit)) +sys.exit(done.returncode) From a9098e0e874dd3d33de6c7fe920ec2d61cfe75c2 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 22:43:54 +0530 Subject: [PATCH 006/191] test: Bound the Lean checker's memory where a proof imports Lean through a rejected module. --- .../Verify/suite/lean_certifier_memory.test | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 clang/test/Verify/suite/lean_certifier_memory.test 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 From f814f9fa0cc266c53462145cab1f37a9b18b1a2e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 22:43:54 +0530 Subject: [PATCH 007/191] docs: Say that certification imports the toolchain once and rejects a module that redeclares a name. --- .../book/part-ii/ch17-backends-modular-calls.rst | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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..c403e1a17a 100644 --- a/website/source/book/part-ii/ch17-backends-modular-calls.rst +++ b/website/source/book/part-ii/ch17-backends-modular-calls.rst @@ -582,7 +582,11 @@ 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 + ``CppVerify.Generated`` comes 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; - the axioms it depends on, collected from the constants it uses, must be @@ -629,8 +633,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.Generated +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 ---------------------------- From ed50d74e7b47d05139b483242ff9ef08ea8742c8 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 22:43:54 +0530 Subject: [PATCH 008/191] docs: Say in the release notes that the checker replays into one environment. --- website/source/release-notes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/release-notes.rst b/website/source/release-notes.rst index 42ebaaa422..a9443338d7 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`). From e918963b812bc33eed5a039c75a4f15d314a48e3 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 22:43:55 +0530 Subject: [PATCH 009/191] docs: Describe the checker's single kernel environment in the design. --- docs/DESIGN.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 2e47c14254..bb4518e207 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1956,10 +1956,12 @@ 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 (`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 generated module being replayed first; the proof's type is exactly the +constant of its goal, which `CppVerify.Generated` 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 passes, and a failing goal leaves only its own function `Unresolved`, with the From c55de5b2af87925a85824c3bcd328954ed90729d Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 22:43:55 +0530 Subject: [PATCH 010/191] docs: Describe the checker's single kernel environment and its memory in the architecture. --- docs/ARCHITECTURE.md | 45 ++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e2ed48978b..3dda2cbd89 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1717,23 +1717,30 @@ uses when the source around it changes. `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 generated + module, 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 generated module 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.Generated 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 + the generated module were replayed, `G_proof` is a theorem of type exactly + `.const G []` with `G` a definition of `CppVerify.Generated`, and the + axioms its kernel constants reach are allowed; 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 +1750,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 From 51a9a6a503ffa0eeac5c1115ae59ad7833331d40 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:02 +0530 Subject: [PATCH 011/191] verify: Declare a Lean project's generated modules and the prelude's emitter. --- clang/lib/Verify/Backend/LeanBackend.h | 28 +++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/clang/lib/Verify/Backend/LeanBackend.h b/clang/lib/Verify/Backend/LeanBackend.h index 7145e8596c..92adb95a55 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 { @@ -38,21 +40,37 @@ struct LeanProjectGoal { /// The group of a module's goals in a project: its function's directory. std::string leanProjectGroup(const ObligationModule &Module); +/// The generated modules of a Lean project: the declarations of the specs +/// its goals apply, which every goal module imports, and each group's spec +/// definitions and goals. +struct LeanProjectModules { + std::string 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. +/// the ones left out as not exported. Declarations, when given, receives +/// the declarations of the specs, each once. 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); + const std::set *Selection = nullptr, + llvm::raw_ostream *Declarations = nullptr); 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,6 +82,10 @@ 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. From 34ef6d00c0649df59e9562095afc5597058824ab Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:02 +0530 Subject: [PATCH 012/191] verify: Emit a project's spec declarations apart from its goals, and the prelude on its own. --- clang/lib/Verify/Backend/LeanBackend.cpp | 102 ++++++++++++----------- 1 file changed, 55 insertions(+), 47 deletions(-) diff --git a/clang/lib/Verify/Backend/LeanBackend.cpp b/clang/lib/Verify/Backend/LeanBackend.cpp index 98eda10f5b..d1b7d0b6e6 100644 --- a/clang/lib/Verify/Backend/LeanBackend.cpp +++ b/clang/lib/Verify/Backend/LeanBackend.cpp @@ -1087,12 +1087,63 @@ static void emitLeanFunctionBodies(const ObligationModule &Module, } } +// The definitions every exported goal is stated with: the heap model, the +// machine comparisons, and C++ division; 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"; + 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"; + } +} + 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, llvm::raw_ostream *Declarations) { VerifyResult Result; if (!Module.CorrectnessGoal || !Module.CounterexampleQuery) { Result.Status = VerifyStatus::Unresolved; @@ -1130,54 +1181,11 @@ 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, Declarations ? *Declarations : OS, + EmittedFunctions); emitLeanFunctionBodies(Module, OS); OS << "/- function: "; printLeanCommentText(Module.FunctionName, OS); From cdcaec52d11e528e430beed76129c7f750dfd5e9 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:03 +0530 Subject: [PATCH 013/191] verify: Let the Lean backend write the modules of a project. --- clang/lib/Verify/Backend/VerifyBackend.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/clang/lib/Verify/Backend/VerifyBackend.h b/clang/lib/Verify/Backend/VerifyBackend.h index 6159936740..6e3a71e715 100644 --- a/clang/lib/Verify/Backend/VerifyBackend.h +++ b/clang/lib/Verify/Backend/VerifyBackend.h @@ -307,12 +307,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, From 505cf155582c2f9cdb46ab84c4b819888f4f1ed9 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:03 +0530 Subject: [PATCH 014/191] verify: Write each function's Lean goals to its module and the specs they apply to Specs. --- clang/lib/Verify/Backend/VerifyBackend.cpp | 30 +++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/clang/lib/Verify/Backend/VerifyBackend.cpp b/clang/lib/Verify/Backend/VerifyBackend.cpp index 134495fe9a..1c69d41bbc 100644 --- a/clang/lib/Verify/Backend/VerifyBackend.cpp +++ b/clang/lib/Verify/Backend/VerifyBackend.cpp @@ -15,17 +15,32 @@ 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]); + llvm::raw_string_ostream Specs(Modules->Specs); + Result = exportLeanScratchPad(Module, Goals, /*EmitPreamble=*/false, + EmittedFunctions, EmittedTheorems, + ++ModuleIndex, ProjectGoals, + Selection ? &*Selection : nullptr, &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 +769,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); From 86e4a9d83f6bc225e2c9da770f1ca9b9ef5bd095 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:03 +0530 Subject: [PATCH 015/191] verify: Import only the prelude in the module of cppverify_auto. --- clang/lib/Verify/Driver/LeanAutomation.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clang/lib/Verify/Driver/LeanAutomation.h b/clang/lib/Verify/Driver/LeanAutomation.h index 0db32bac01..ee97ecc42a 100644 --- a/clang/lib/Verify/Driver/LeanAutomation.h +++ b/clang/lib/Verify/Driver/LeanAutomation.h @@ -25,7 +25,7 @@ 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 From c23e58c36a1f188fc669926726d70f5ee57de591 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:04 +0530 Subject: [PATCH 016/191] verify: Check each goal against its own module, and fail every goal when a User lemma uses sorry. --- clang/lib/Verify/Driver/LeanCertifier.h | 61 +++++++++++++++---------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/clang/lib/Verify/Driver/LeanCertifier.h b/clang/lib/Verify/Driver/LeanCertifier.h index 2a1cf1f5b0..4286533013 100644 --- a/clang/lib/Verify/Driver/LeanCertifier.h +++ b/clang/lib/Verify/Driver/LeanCertifier.h @@ -38,9 +38,10 @@ def axiomsOf (env : Kernel.Environment) (root : Name) : Except String NameSet := | 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,12 +55,13 @@ 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) + 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 @@ -75,10 +77,10 @@ unsafe def Closure.visit (toolchain : String) (module : Name) : StateT Closure I if (← findOLean module).toString.startsWith toolchain then modify fun c => { c with toolchain := c.toolchain.push { module } } return - let (imports, constants) ← moduleConstants module + 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, constants } } + 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 @@ -127,11 +129,10 @@ unsafe def replayProject (closure : Closure) : IO Replayed := do owner := owner.insert name m.name return { env, rejected, owner } -/-- Whether the proof of a goal states exactly the goal the generated module +/-- Whether the proof of a goal states exactly the goal its generated module defines and depends on no axiom outside the allowed ones. -/ -def checkStatement (project : Replayed) (goal module : Name) (allowed : NameSet) : - Option String := Id.run do - let generated := `CppVerify.Generated +def checkStatement (project : Replayed) (goal module generated : Name) + (allowed : NameSet) : Option String := Id.run do let env := project.env let proof := goal.appendAfter "_proof" match env.find? goal with @@ -157,31 +158,43 @@ def checkStatement (project : Replayed) (goal module : Name) (allowed : NameSet) | some _ => return some s!"{proof} is not a theorem" | none => return some s!"{module} declares no {proof}" -/-- 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. A goal is proved when its proof module and the generated -module pass the kernel, with every module of the project they import, and -its proof states exactly the goal and uses only the allowed axioms. The -generated module is replayed first, so that a proof file that declares a -name it declares is the one left out. -/ +/-- 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 + +/-- Usage: Certify GOALS ALLOWED, where each line of GOALS is a goal, its +proof module, and the module that defines it, separated by tabs, 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. 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 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) := #[] for line in ← IO.FS.lines goals do - let [goal, module] := line.splitOn "\t" | continue - entries := entries.push (goal.toName, module.toName) - let roots := #[`CppVerify.Generated] ++ entries.map (·.2) + let [goal, module, generated] := line.splitOn "\t" | continue + entries := entries.push (goal.toName, module.toName, generated.toName) + let roots := entries.map (·.2.2) ++ entries.map (·.2.1) let (_, closure) ← (roots.forM (Closure.visit toolchain)).run {} let project ← replayProject closure - for (goal, module) in entries do + let user := userSorry closure project + for (goal, module, generated) in entries do let why := (project.rejected[module]?).orElse fun _ => - (project.rejected[`CppVerify.Generated]?).orElse fun _ => - checkStatement project goal module allowed + (project.rejected[generated]?).orElse fun _ => user.orElse fun _ => + checkStatement project goal module generated allowed match why with | none => IO.println s!"ok\t{goal}" | some why => IO.println s!"fail\t{goal}\t{why.replace "\n" " "}" From 2ef7e14bd9b794deda04c182d280bc52747e5fa9 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:04 +0530 Subject: [PATCH 017/191] verify: Give each function's Lean goals a module, and compile a proof again only when what it imports changed. --- clang/lib/Verify/Driver/Verifier.cpp | 281 +++++++++++++++++++++------ 1 file changed, 218 insertions(+), 63 deletions(-) diff --git a/clang/lib/Verify/Driver/Verifier.cpp b/clang/lib/Verify/Driver/Verifier.cpp index f317e60815..8800d39092 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" @@ -1255,6 +1257,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 +1338,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 +1369,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'); @@ -1407,8 +1447,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 +1469,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 +1491,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()); @@ -1623,7 +1668,7 @@ class Verifier { } } - llvm::Expected initializeLeanProject() { + llvm::Error initializeLeanProject() { const std::string &Root = leanProjectRoot(); std::error_code EC = llvm::sys::fs::create_directories( leanProjectPath(Root, {"CppVerify", "Proofs"})); @@ -1672,18 +1717,67 @@ 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"); + } - return leanProjectPath(Root, {"CppVerify", "Generated.lean"}); + // 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" + + Modules.Specs)) + 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(); } static std::vector @@ -1802,8 +1896,9 @@ 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 + @@ -1841,9 +1936,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); } @@ -1931,8 +2027,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,6 +2104,58 @@ class Verifier { return Launcher; } + // 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()) @@ -2028,15 +2176,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)) @@ -2053,7 +2192,14 @@ class Verifier { leanProjectPath(ProofBuildDir, {Goal->Group}))) 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 = [&] { @@ -2064,13 +2210,22 @@ class Verifier { leanProjectPath(Root, leanProofComponents(*Goal, File)); const std::string ProofObject = leanProjectPath( leanProjectPath(ProofBuildDir, {Goal->Group}), {Name + ".olean"}); + 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)); } }; { @@ -2088,7 +2243,8 @@ class Verifier { Failures[Name] = *BuildErrors[I]; continue; } - Built += Name + "\tCppVerify.Proofs." + Goal->Group + "." + Name + "\n"; + Built += Name + "\tCppVerify.Proofs." + Goal->Group + "." + Name + + "\tCppVerify.Goals." + Goal->Group + "\n"; } if (Built.empty()) return Failures; @@ -2179,7 +2335,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"; @@ -2373,8 +2530,9 @@ class Verifier { {VerifyDiagnostic::Warning, "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,10 +2583,10 @@ 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))); } @@ -2664,19 +2822,16 @@ 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) { + if (llvm::Error Error = initializeLeanProject()) { 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 +2875,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. @@ -4160,15 +4316,14 @@ 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); + const std::set *RunGroups = + LeanFallbackBackend ? &LeanRunGroups : nullptr; + llvm::Error Finalized = writeLeanProjectModules(LeanModules); + if (!Finalized) + Finalized = finalizeLeanProject(LeanProjectGoals, RunGroups); if (!Finalized && Opts.LeanAuto) automateLeanProject(LeanProjectGoals); if (!Finalized) From da1845ac80628be4824f9bc6a8db765d98f3f965 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:05 +0530 Subject: [PATCH 018/191] test: Add a helper that prints a Lean project's generated text in import order. --- clang/test/Verify/suite/Inputs/lean_project_text.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/lean_project_text.py 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)) From 548094c26c56f038f8e851c03f52a1851e333cd4 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:05 +0530 Subject: [PATCH 019/191] test: Read a Lean project's goals from its modules in the fidelity gate. --- clang/test/Verify/suite/Inputs/run_backend_fidelity.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/clang/test/Verify/suite/Inputs/run_backend_fidelity.py b/clang/test/Verify/suite/Inputs/run_backend_fidelity.py index 18d97cddca..d8734e15cc 100644 --- a/clang/test/Verify/suite/Inputs/run_backend_fidelity.py +++ b/clang/test/Verify/suite/Inputs/run_backend_fidelity.py @@ -476,8 +476,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") From 2c9de793f21997699dfcd61394e3a85b54104334 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:05 +0530 Subject: [PATCH 020/191] test: Add the five functions of the Lean modules test. --- .../test/Verify/suite/Inputs/lean_modules.cpp | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/lean_modules.cpp 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..eb8b11b768 --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_modules.cpp @@ -0,0 +1,36 @@ +// Five functions for lean_modules.test: inc and dbl, which cppverify_auto +// proves, and two that apply the recursive spec tri, whose postconditions +// Inputs/lean_modules_*.lean prove. + +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; +} From 28af37d0fd176cd96bd9e2656010edb459e3873e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:06 +0530 Subject: [PATCH 021/191] test: Add the proof of tri_zero's postcondition for the Lean modules test. --- .../Verify/suite/Inputs/lean_modules_tri_zero.lean | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/lean_modules_tri_zero.lean 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 From 901c3ca0e94edafadb4f06dd70a4d336589e2ba9 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:06 +0530 Subject: [PATCH 022/191] test: Add the proof of plus_tri_zero's postcondition for the Lean modules test. --- .../suite/Inputs/lean_modules_plus_tri_zero.lean | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/lean_modules_plus_tri_zero.lean 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 From adf19a3b5f4b7f57e432c45502c8610dc5ef1fde Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:06 +0530 Subject: [PATCH 023/191] test: Check the module of each function's Lean goals and incremental certification. --- clang/test/Verify/suite/lean_modules.test | 118 ++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 clang/test/Verify/suite/lean_modules.test diff --git a/clang/test/Verify/suite/lean_modules.test b/clang/test/Verify/suite/lean_modules.test new file mode 100644 index 0000000000..50ef7e179f --- /dev/null +++ b/clang/test/Verify/suite/lean_modules.test @@ -0,0 +1,118 @@ +# 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=opaque +# 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. +# 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. +# RUN: touch %t/same +# RUN: %cpp-verify --backend=lean --lean-project=%t/p %t/m.cpp -- 2>&1 | FileCheck %s --check-prefix=EXPORT +# 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 1 of 2 goals) +# EXPORT-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: {{^}}opaque cppSpec_fn_5f5a3374726969 : 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] From 0350c69fade7a7703c36f24bbe03f6e979cafc9a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:07 +0530 Subject: [PATCH 024/191] test: Expect the goal module among the imports of an automatic proof. --- clang/test/Verify/suite/lean_automation.test | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/clang/test/Verify/suite/lean_automation.test b/clang/test/Verify/suite/lean_automation.test index 0862d09576..5c5ace654f 100644 --- a/clang/test/Verify/suite/lean_automation.test +++ b/clang/test/Verify/suite/lean_automation.test @@ -28,7 +28,7 @@ # # 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 -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 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: %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 @@ -57,6 +57,7 @@ # PROVED: automation: cppverify_auto 1 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: From 56a9fb07d2180a6807a2361a8d51bc0ffc94a13e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:07 +0530 Subject: [PATCH 025/191] test: Expect the goal module in the message for a shadowed goal. --- clang/test/Verify/suite/lean_certifier.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clang/test/Verify/suite/lean_certifier.test b/clang/test/Verify/suite/lean_certifier.test index 353324ad67..7edbabeb7f 100644 --- a/clang/test/Verify/suite/lean_certifier.test +++ b/clang/test/Verify/suite/lean_certifier.test @@ -67,9 +67,9 @@ # 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.Proofs.cppverify_one_99bcee.cppverify_one_99bcee_postcondition_646d2761 declares cppverify_one_99bcee_postcondition_646d2761, which CppVerify.Generated declares) +# 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.Generated 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] From 9f9fbb290836ab9535d99c296c6ea8c7ffee7a73 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:07 +0530 Subject: [PATCH 026/191] test: Expect an admitted User lemma to fail every goal through the checker. --- clang/test/Verify/suite/lean_certify.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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: From e261a4d7eed9464496536ca8a52bbff2f599804a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:08 +0530 Subject: [PATCH 027/191] test: Read the fallback project's generated text from its modules. --- clang/test/Verify/suite/lean_fallback.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clang/test/Verify/suite/lean_fallback.test b/clang/test/Verify/suite/lean_fallback.test index 1aed3ce551..72fe98493b 100644 --- a/clang/test/Verify/suite/lean_fallback.test +++ b/clang/test/Verify/suite/lean_fallback.test @@ -5,7 +5,7 @@ // 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: %python %S/Inputs/lean_project_text.py %t.project | FileCheck %s --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: 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 @@ -37,6 +37,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 From 4cbeefd8613993f09361318f296422ead5f7ea20 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:08 +0530 Subject: [PATCH 028/191] test: Find spec bodies in their goal module and the goal module in a moved proof's imports. --- clang/test/Verify/suite/lean_identity.test | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 : From accb2c9468c06946796c7749c7f5339139ede508 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:09 +0530 Subject: [PATCH 029/191] test: Read the Lean projects' generated text from their modules. --- clang/test/Verify/suite/lean_kernel.test | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 From 66be53e3b1e2adb7339e9506c555eaf6ec35a22d Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:09 +0530 Subject: [PATCH 030/191] test: Read the Lean project's generated text from its modules. --- clang/test/Verify/suite/lean_project.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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]+}} From 97f37766eed6a90d57ab9835c04c1b766a70b230 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:09 +0530 Subject: [PATCH 031/191] test: Read and edit the readable goals in their function's module. --- clang/test/Verify/suite/lean_readable.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 From 6ae6209ce5783d5281ae93d9048380ce697ff449 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:10 +0530 Subject: [PATCH 032/191] test: Read the exported Lean semantics from the project's modules. --- clang/test/Verify/suite/lean_semantics.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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. From 7e119a19fbae3658ee70c9db4c4d7dcfeb00840e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:10 +0530 Subject: [PATCH 033/191] test: Expect the trial of cppverify_auto to build the goal modules too. --- clang/test/Verify/suite/lean_toolchain.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clang/test/Verify/suite/lean_toolchain.test b/clang/test/Verify/suite/lean_toolchain.test index 306038ccee..03b229fd70 100644 --- a/clang/test/Verify/suite/lean_toolchain.test +++ b/clang/test/Verify/suite/lean_toolchain.test @@ -22,7 +22,7 @@ # 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{{$}} From 71ce0e1dddfd5afab70a5cdb7f1f270fccac7ba5 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:10 +0530 Subject: [PATCH 034/191] docs: Explain the module of each function's Lean goals and incremental certification in chapter 17. --- .../part-ii/ch17-backends-modular-calls.rst | 132 ++++++++++++++++-- 1 file changed, 117 insertions(+), 15 deletions(-) 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 c403e1a17a..ce0f0911a4 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 @@ -324,7 +325,7 @@ is ``Certified: f [backend=Lean] [vacuous]``, with a warning. 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: @@ -474,6 +475,7 @@ A goal it proves gets that proof, with a header that says so: automation: cppverify_auto 1 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 @@ -572,6 +574,104 @@ 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 1 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, as a function of which nothing is known +(``opaque cppSpec_fn_5f5a3374726969 : Int -> Int``). 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 ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -585,12 +685,14 @@ user's source. For each goal: 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 - ``CppVerify.Generated`` comes first, so a proof file that defines a goal - of its own under the goal's name is rejected; + 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: @@ -635,8 +737,8 @@ skips the kernel, is rejected at its replay (``the kernel rejects CppVerify.Lemmas: while replaying declaration 'cheat': (kernel) declaration 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.Generated -declares``), and a user axiom behind a lemma by its name (``depends on the +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 From e406b754bdee559b68b004f99990d31a23df2d06 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:11 +0530 Subject: [PATCH 035/191] docs: Describe the modules of a Lean project in the tooling reference. --- website/source/language/tooling.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/source/language/tooling.rst b/website/source/language/tooling.rst index a7082dc386..d2b8429c7b 100644 --- a/website/source/language/tooling.rst +++ b/website/source/language/tooling.rst @@ -286,7 +286,11 @@ 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``; 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, From 0c43c5c4115afdba35f54292c5fcabe50ca2028c Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:11 +0530 Subject: [PATCH 036/191] docs: Note the Lean module per function in the release notes. --- website/source/release-notes.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/website/source/release-notes.rst b/website/source/release-notes.rst index a9443338d7..60b91f1188 100644 --- a/website/source/release-notes.rst +++ b/website/source/release-notes.rst @@ -385,6 +385,20 @@ 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`). + **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 From f2558ecf8d6dfef4f433ecc997541501d11a3c2f Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:11 +0530 Subject: [PATCH 037/191] docs: Describe a Lean project's modules in the design. --- docs/DESIGN.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index bb4518e207..219ba08aad 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1913,7 +1913,16 @@ 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 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 @@ -1952,18 +1961,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 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 generated module being replayed first; the proof's type is exactly the -constant of its goal, which `CppVerify.Generated` defines; and the axioms it depends on, +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 From a9899605f7ca6af32e310ded86bce27c884dd17b Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:12 +0530 Subject: [PATCH 038/191] docs: Describe a Lean project's modules and proof keys in the architecture. --- docs/ARCHITECTURE.md | 95 ++++++++++++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 30 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3dda2cbd89..98795dfc1c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1627,6 +1627,25 @@ 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_...`). - **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 @@ -1675,7 +1694,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. @@ -1699,8 +1718,8 @@ uses when the source around it changes. 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 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,35 +1731,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 reads, from their compiled files, the generated - module, 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 generated module 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.Generated 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 - the generated module were replayed, `G_proof` is a theorem of type exactly - `.const G []` with `G` a definition of `CppVerify.Generated`, and the - axioms its kernel constants reach are allowed; 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 + 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 @@ -1764,7 +1794,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 From f178db1f30a334b614a39b78a7b98714ee0124e6 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:12 +0530 Subject: [PATCH 039/191] test: Import its function's goal module in the proof cppverify_fine_52006a_missing_return_54ae7c80.lean. --- .../cppverify_fine_52006a_missing_return_54ae7c80.lean | 1 + 1 file changed, 1 insertion(+) 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 From b138c4bb9bf5bdd9ac21af8d4eb9a2a64346aa64 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:12 +0530 Subject: [PATCH 040/191] test: Import its function's goal module in the proof cppverify_fine_52006a_postcondition_cd6bdfcd.lean. --- .../cppverify_fine_52006a_postcondition_cd6bdfcd.lean | 1 + 1 file changed, 1 insertion(+) 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 From 1bf5460d8e51968f7f477702c62f2c300565613c Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:13 +0530 Subject: [PATCH 041/191] test: Import its function's goal module in the proof cppverify_one_99bcee_missing_return_54ae7c80.lean. --- .../cppverify_one_99bcee_missing_return_54ae7c80.lean | 1 + 1 file changed, 1 insertion(+) 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 From 77bdebb76f9dac56ba5dd98f113879dcf877c713 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:13 +0530 Subject: [PATCH 042/191] test: Import its function's goal module in the proof cppverify_one_99bcee_postcondition_646d2761.lean. --- .../hidden/cppverify_one_99bcee_postcondition_646d2761.lean | 1 + 1 file changed, 1 insertion(+) 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. -/ From e0d4296270879b789dfd9b234d118680d370295c Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:14 +0530 Subject: [PATCH 043/191] test: Import its function's goal module in the proof Lemmas.lean. --- .../suite/Inputs/lean_certifier_proofs/imported/Lemmas.lean | 1 + 1 file changed, 1 insertion(+) 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 From 98d82b0059cdc9d6a60a8ba050a5f2c671a2f9bd Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:14 +0530 Subject: [PATCH 044/191] test: Import its function's goal module in the proof cppverify_one_99bcee_postcondition_646d2761.lean. --- .../rewritten/cppverify_one_99bcee_postcondition_646d2761.lean | 1 + 1 file changed, 1 insertion(+) 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 From 71886e0fdaacc94fd4cea45c22422eb92671eaee Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:14 +0530 Subject: [PATCH 045/191] test: Import its function's goal module in the proof cppverify_one_99bcee_postcondition_646d2761.lean. --- .../unchecked/cppverify_one_99bcee_postcondition_646d2761.lean | 1 + 1 file changed, 1 insertion(+) 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 From aaf0ec7ffc36b15bfa7274b301a55cd0270c1733 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:15 +0530 Subject: [PATCH 046/191] test: Import its function's goal module in the proof lean_dependencies_impossible.lean. --- clang/test/Verify/suite/Inputs/lean_dependencies_impossible.lean | 1 + 1 file changed, 1 insertion(+) 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 From 910acbeb6d209b683ddd6babc68949401a1836b4 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:15 +0530 Subject: [PATCH 047/191] test: Import its function's goal module in the proof lean_dependencies_looping.lean. --- clang/test/Verify/suite/Inputs/lean_dependencies_looping.lean | 1 + 1 file changed, 1 insertion(+) 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 From 69d11773b5cbd1e6e38d15da70f328b86a431dcb Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:15 +0530 Subject: [PATCH 048/191] test: Import its function's goal module in the proof lean_dependencies_mixed.lean. --- clang/test/Verify/suite/Inputs/lean_dependencies_mixed.lean | 1 + 1 file changed, 1 insertion(+) 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 : From 6ce143542319211b101a66baba5f9d177aa3a8f0 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:16 +0530 Subject: [PATCH 049/191] test: Import its function's goal module in the proof lean_dependencies_positive.lean. --- clang/test/Verify/suite/Inputs/lean_dependencies_positive.lean | 1 + 1 file changed, 1 insertion(+) 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 From d69832dc383c904ea28a032f29bd195d5fc78c28 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:16 +0530 Subject: [PATCH 050/191] test: Import its function's goal module in the proof lean_dependencies_same.lean. --- clang/test/Verify/suite/Inputs/lean_dependencies_same.lean | 1 + 1 file changed, 1 insertion(+) 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 From 1d6af47cdb2b2847432b8a67ef458c5eb67cfe17 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:16 +0530 Subject: [PATCH 051/191] test: Import its function's goal module in the proof lean_dependencies_seven.lean. --- clang/test/Verify/suite/Inputs/lean_dependencies_seven.lean | 1 + 1 file changed, 1 insertion(+) 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 From fad7045e6499a98707b90dbd55e7f4c8bbbf4a41 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:17 +0530 Subject: [PATCH 052/191] test: Import its function's goal module in the proof lean_dependencies_termination.lean. --- .../test/Verify/suite/Inputs/lean_dependencies_termination.lean | 1 + 1 file changed, 1 insertion(+) 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 From 49a0e1f059213da58601b933baa2e78648255327 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:17 +0530 Subject: [PATCH 053/191] test: Import its function's goal module in the proof lean_dependencies_trusting.lean. --- clang/test/Verify/suite/Inputs/lean_dependencies_trusting.lean | 1 + 1 file changed, 1 insertion(+) 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 From 23e010b75f6abc42577c2e5c59aab5e3e0f9bfb9 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:18 +0530 Subject: [PATCH 054/191] test: Import its function's goal module in the proof lean_fallback_goal_1.lean. --- clang/test/Verify/suite/Inputs/lean_fallback_goal_1.lean | 1 + 1 file changed, 1 insertion(+) 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 : From 64c657077b34b58509a2e38feda2ad851351c9df Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:18 +0530 Subject: [PATCH 055/191] test: Import its function's goal module in the proof lean_fallback_goal_2.lean. --- clang/test/Verify/suite/Inputs/lean_fallback_goal_2.lean | 1 + 1 file changed, 1 insertion(+) 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 : From cc18009b8d544da835e6890da39c6a6c5b01376e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:18 +0530 Subject: [PATCH 056/191] test: Import its function's goal module in the proof lean_fallback_goal_3.lean. --- clang/test/Verify/suite/Inputs/lean_fallback_goal_3.lean | 1 + 1 file changed, 1 insertion(+) 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 : From ce46518fa66a885005f9b9e9f855b6fe95e170d8 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:19 +0530 Subject: [PATCH 057/191] test: Import its function's goal module in the proof lean_fallback_goal_4.lean. --- clang/test/Verify/suite/Inputs/lean_fallback_goal_4.lean | 1 + 1 file changed, 1 insertion(+) 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 : From 8d1523beec00f36f20e6e3678d7b4ab0efc0d551 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:19 +0530 Subject: [PATCH 058/191] test: Import its function's goal module in the proof lean_fallback_partial_goal_1.lean. --- clang/test/Verify/suite/Inputs/lean_fallback_partial_goal_1.lean | 1 + 1 file changed, 1 insertion(+) 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 : From df334198eebbf9d6216ff899ca1fec9f20dd0265 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:19 +0530 Subject: [PATCH 059/191] test: Import its function's goal module in the proof lean_fallback_partial_goal_2.lean. --- clang/test/Verify/suite/Inputs/lean_fallback_partial_goal_2.lean | 1 + 1 file changed, 1 insertion(+) 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 : From 2f1a00609dab0203a4bc914fbdca25e6e711b23a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:20 +0530 Subject: [PATCH 060/191] test: Import its function's goal module in the proof cppverify_bump_ce87d1_missing_return_ba8abb90.lean. --- .../cppverify_bump_ce87d1_missing_return_ba8abb90.lean | 1 + 1 file changed, 1 insertion(+) 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 : From 5daf12111e51a806ae56113f9f29740cb36287e1 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:20 +0530 Subject: [PATCH 061/191] test: Import its function's goal module in the proof cppverify_one_99bcee_missing_return_54ae7c80.lean. --- .../cppverify_one_99bcee_missing_return_54ae7c80.lean | 1 + 1 file changed, 1 insertion(+) 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 : From 16a8c939940dc985caa367840f256995e998b1e0 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:20 +0530 Subject: [PATCH 062/191] test: Import its function's goal module in the proof cppverify_one_99bcee_postcondition_cd6bdfcd.lean. --- .../cppverify_one_99bcee_postcondition_cd6bdfcd.lean | 1 + 1 file changed, 1 insertion(+) 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 : From 6be02b0978495205d898a97a1a542558b20a7dbb Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:21 +0530 Subject: [PATCH 063/191] test: Import its function's goal module in the proof cppverify_one_99bcee_postcondition_cd6bdfcd_2.lean. --- .../cppverify_one_99bcee_postcondition_cd6bdfcd_2.lean | 1 + 1 file changed, 1 insertion(+) 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 : From ca6c465ecaf104d43a0db048bdb5cd8dff2fe100 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:21 +0530 Subject: [PATCH 064/191] test: Import its function's goal module in the proof cppverify_one_99bcee_postcondition_ebbe6a6a.lean. --- .../cppverify_one_99bcee_postcondition_ebbe6a6a.lean | 1 + 1 file changed, 1 insertion(+) 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 : From 7b861cb2f2f376f3b5f9d94962b716b15fa9ed14 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:22 +0530 Subject: [PATCH 065/191] test: Import its function's goal module in the proof cppverify_six_6a2cf3_missing_return_54ae7c80.lean. --- .../cppverify_six_6a2cf3_missing_return_54ae7c80.lean | 1 + 1 file changed, 1 insertion(+) 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 : From 5eedfdf9c605a7dc273d83a7086208d0c9827bd4 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:22 +0530 Subject: [PATCH 066/191] test: Import its function's goal module in the proof cppverify_six_6a2cf3_postcondition_198bc076.lean. --- .../cppverify_six_6a2cf3_postcondition_198bc076.lean | 1 + 1 file changed, 1 insertion(+) 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 : From c318e4c86c6eed8af4c610ca86bcdb199ed8c853 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:22 +0530 Subject: [PATCH 067/191] test: Import its function's goal module in the proof cppverify_tri_decreases_f55d4c_termination_3c6bb93f.lean. --- .../cppverify_tri_decreases_f55d4c_termination_3c6bb93f.lean | 1 + 1 file changed, 1 insertion(+) 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 : From 201ea08d254d10ee216cf72e595d218c564f4b70 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:23 +0530 Subject: [PATCH 068/191] test: Import its function's goal module in the proof cppverify_twice_edf933_missing_return_54ae7c80.lean. --- .../cppverify_twice_edf933_missing_return_54ae7c80.lean | 1 + 1 file changed, 1 insertion(+) 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 From 9bacf87068a15d7b66ddd16d6e53889774d3aca9 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:23 +0530 Subject: [PATCH 069/191] test: Import its function's goal module in the proof cppverify_twice_edf933_postcondition_28de726e.lean. --- .../cppverify_twice_edf933_postcondition_28de726e.lean | 1 + 1 file changed, 1 insertion(+) 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 From 451f38fe9868fee7b346ddc8aadd7f363f1ede99 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:23 +0530 Subject: [PATCH 070/191] test: Import its function's goal module in the proof cppverify_twice_edf933_precondition_51b3c1f0.lean. --- .../cppverify_twice_edf933_precondition_51b3c1f0.lean | 1 + 1 file changed, 1 insertion(+) 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 From 05003b5300d550eeabf7e6da1caf390f76f860d8 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:24 +0530 Subject: [PATCH 071/191] test: Import its function's goal module in the proof cppverify_twice_edf933_precondition_f0a3d715.lean. --- .../cppverify_twice_edf933_precondition_f0a3d715.lean | 1 + 1 file changed, 1 insertion(+) 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 From 93b75ad137b25646627b3c56c5f0ddad0d8a30cc Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:24 +0530 Subject: [PATCH 072/191] test: Import its function's goal module in the proof lean_semantics_call.lean. --- clang/test/Verify/suite/Inputs/lean_semantics_call.lean | 1 + 1 file changed, 1 insertion(+) 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 : From d49e8f9625aa4f6ff51bfc7a031d6b5020a9a254 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:24 +0530 Subject: [PATCH 073/191] test: Import its function's goal module in the proof lean_semantics_mix.lean. --- clang/test/Verify/suite/Inputs/lean_semantics_mix.lean | 1 + 1 file changed, 1 insertion(+) 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 : From 1136d88661c58f48d674b8d02dcf63aa38d1c951 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 06:50:25 +0530 Subject: [PATCH 074/191] test: Import its function's goal module in the proof lean_semantics_numerals.lean. --- clang/test/Verify/suite/Inputs/lean_semantics_numerals.lean | 1 + 1 file changed, 1 insertion(+) 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 : From 9359c25e7403528271106f9fbac530623a9d5662 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 18:35:40 +0530 Subject: [PATCH 075/191] test: Import its function's goal module in the proof cppverify_reading_8d7035_division_by_zero_df06c970.lean. --- .../cppverify_reading_8d7035_division_by_zero_df06c970.lean | 1 + 1 file changed, 1 insertion(+) 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 From cf3fc2c9752e4bfb6da719321559845e066ad7e9 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 18:35:40 +0530 Subject: [PATCH 076/191] test: Import its function's goal module in the proof cppverify_reading_8d7035_missing_return_54ae7c80.lean. --- .../cppverify_reading_8d7035_missing_return_54ae7c80.lean | 1 + 1 file changed, 1 insertion(+) 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 From 1e8a0a14f1aae7eec49e1246e1ba6315c45c9ae7 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 18:35:41 +0530 Subject: [PATCH 077/191] test: Import its function's goal module in the proof cppverify_reading_8d7035_postcondition_54ae7c80.lean. --- .../cppverify_reading_8d7035_postcondition_54ae7c80.lean | 1 + 1 file changed, 1 insertion(+) 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 From 4eb310f89959d312267b05181ca167fcb08ee50c Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 18:35:41 +0530 Subject: [PATCH 078/191] test: Import its function's goal module in the proof cppverify_reading_8d7035_precondition_155ffe95.lean. --- .../cppverify_reading_8d7035_precondition_155ffe95.lean | 1 + 1 file changed, 1 insertion(+) 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 From 9b346cc5723c659a9c0c70fe74ce943e9e3fc24d Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 18:35:41 +0530 Subject: [PATCH 079/191] test: Import its function's goal module in the proof cppverify_reading_8d7035_precondition_814bc5b2.lean. --- .../cppverify_reading_8d7035_precondition_814bc5b2.lean | 1 + 1 file changed, 1 insertion(+) 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 From 04706a910e770bf4eaa1aa0cfda25d69e07979e8 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 18:35:41 +0530 Subject: [PATCH 080/191] test: Import its function's goal module in the proof cppverify_sensor_contract_eb6f5c_division_by_zero_d81a153d.lean. --- ...pverify_sensor_contract_eb6f5c_division_by_zero_d81a153d.lean | 1 + 1 file changed, 1 insertion(+) 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 From 5683c258f82d262497a925b2076621bf0e4b40f8 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:36 +0530 Subject: [PATCH 081/191] verify: Let the Lean backend export goals over spec collections. --- clang/lib/Verify/Backend/LeanBackend.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/clang/lib/Verify/Backend/LeanBackend.h b/clang/lib/Verify/Backend/LeanBackend.h index 92adb95a55..5e2961edbe 100644 --- a/clang/lib/Verify/Backend/LeanBackend.h +++ b/clang/lib/Verify/Backend/LeanBackend.h @@ -88,10 +88,7 @@ class LeanVerifyBackend : public VerifyBackend { : 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) { From d5c8a51b184a6d3941466b66401303846c263946 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:36 +0530 Subject: [PATCH 082/191] verify: State the spec collections in Lean with core types, prelude operations, and their lemmas. --- clang/lib/Verify/Backend/LeanBackend.cpp | 385 ++++++++++++++++++++++- 1 file changed, 379 insertions(+), 6 deletions(-) diff --git a/clang/lib/Verify/Backend/LeanBackend.cpp b/clang/lib/Verify/Backend/LeanBackend.cpp index d1b7d0b6e6..dd8ea9a31b 100644 --- a/clang/lib/Verify/Backend/LeanBackend.cpp +++ b/clang/lib/Verify/Backend/LeanBackend.cpp @@ -108,6 +108,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 +125,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) { @@ -535,10 +603,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); @@ -1087,9 +1170,298 @@ 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, and C++ division; a project's goals also have the -// lemmas their readable forms' bridges use. +// 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"; @@ -1128,6 +1500,7 @@ void verify::emitLeanPrelude(llvm::raw_ostream &OS, bool Project) { 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 << LeanCollectionPrelude; if (Project) { // What the bridges from readable goals use. OS << "theorem cppverify_imp_or {a b : Prop} (h : a → b) : ¬a ∨ b :=\n"; From 91bccccab8a411835a19b7a8a8585b859144ecf4 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:36 +0530 Subject: [PATCH 083/191] verify: Add machine equalities, collection extensionality, and spec unfolding to cppverify_auto as revision 2. --- clang/lib/Verify/Driver/LeanAutomation.h | 66 ++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/clang/lib/Verify/Driver/LeanAutomation.h b/clang/lib/Verify/Driver/LeanAutomation.h index ee97ecc42a..2eca874d9e 100644 --- a/clang/lib/Verify/Driver/LeanAutomation.h +++ b/clang/lib/Verify/Driver/LeanAutomation.h @@ -9,18 +9,23 @@ 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 = "2"; /// 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.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. 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. -/ @@ -82,6 +87,29 @@ 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] + /-- Machine comparisons, constants, and overflow conditions as facts about integers. -/ macro "cppverify_norm" : tactic => `(tactic| (try simp only [cppBvSlt, cppBvSle, @@ -118,6 +146,33 @@ 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)) + 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 +187,10 @@ 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))] for (name, step) in steps do let attempt : TacticM Unit := do let heartbeats ← IO.getNumHeartbeats From dc6e8212081e0a9e0532073722fc56886467034b Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:37 +0530 Subject: [PATCH 084/191] test: Check that Lean evaluates every collection operation as the certifier does. --- clang/unittests/Verify/CertifyTest.cpp | 208 +++++++++++++++++++++++++ 1 file changed, 208 insertions(+) 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"; From e6287c9dff95c491a99dab73e3e8a1adfba3c907 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:37 +0530 Subject: [PATCH 085/191] test: Add the functions over each spec collection of the Lean collections test. --- .../Verify/suite/Inputs/lean_collections.cpp | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/lean_collections.cpp 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) +{ +} From 6dac71e35e0563497281851a7bd98a995826804c Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:37 +0530 Subject: [PATCH 086/191] test: Add a proof by hand over a sequence for the Lean collections test. --- .../suite/Inputs/lean_collections_total_push.lean | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/lean_collections_total_push.lean 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 From ce1c27c9cf919fe4550795e012bee63202de9bd6 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:38 +0530 Subject: [PATCH 087/191] test: Check the export, automation, and certification of goals over spec collections. --- clang/test/Verify/suite/lean_collections.test | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 clang/test/Verify/suite/lean_collections.test diff --git a/clang/test/Verify/suite/lean_collections.test b/clang/test/Verify/suite/lean_collections.test new file mode 100644 index 0000000000..2398cea2b0 --- /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 2 proves it with cppverify_auto_bveq +# STEPS-NEXT: automation: cppverify_auto 2 proves it with cppverify_auto_ext +# STEPS-NEXT: automation: cppverify_auto 2 proves it with cppverify_auto_unfold + +# SPECS: /- spec function: total -/ +# SPECS-NEXT: {{^}}opaque cppSpec_fn_5f5a35746f74616c4e396370707665726966793373657145 : List Int -> Int{{$}} +# SPECS: /- spec function: members -/ +# SPECS-NEXT: {{^}}opaque cppSpec_fn_5f5a376d656d626572734e39637070766572696679337365744569 : (Int → Bool) -> 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 From 5a1bcfca5eff35ae2991d31804d1e03ff0b9fd0f Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:38 +0530 Subject: [PATCH 088/191] test: Expect revision 2 of cppverify_auto in the proof headers. --- clang/test/Verify/suite/lean_automation.test | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/clang/test/Verify/suite/lean_automation.test b/clang/test/Verify/suite/lean_automation.test index 5c5ace654f..74c4a8a628 100644 --- a/clang/test/Verify/suite/lean_automation.test +++ b/clang/test/Verify/suite/lean_automation.test @@ -29,7 +29,7 @@ # 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.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 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 's/cppverify_auto 2 did not prove it/cppverify_auto 1 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,7 +54,7 @@ # 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 2 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 @@ -67,11 +67,11 @@ # PROVED-NEXT: cppverify_auto_grind # PROVED-EMPTY: -# STUB: automation: cppverify_auto 1 did not prove it{{$}} +# STUB: automation: cppverify_auto 2 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 2 did not prove it within 3 s # SLOW: {{^}} sorry{{$}} # JSON: "function":"next","lean_automatic":3,{{.*}}"status":"exported" @@ -82,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 2 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 2 proves it with cppverify_auto_heap # HONEST: {{^}} cppverify_auto_heap{{$}} # PLAIN: Exported: lean obligation: a_slow{{$}} From fb9d7ee315f5eb10cc694f6d485d9cb6bd027dd9 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:38 +0530 Subject: [PATCH 089/191] test: Expect cppverify_auto to prove the goals over tri, and the proofs by hand to be the user's. --- clang/test/Verify/suite/lean_modules.test | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/clang/test/Verify/suite/lean_modules.test b/clang/test/Verify/suite/lean_modules.test index 50ef7e179f..0861dc5eef 100644 --- a/clang/test/Verify/suite/lean_modules.test +++ b/clang/test/Verify/suite/lean_modules.test @@ -16,14 +16,16 @@ # 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. +# 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. +# 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 +# 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 @@ -58,8 +60,11 @@ # 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 1 of 2 goals) -# EXPORT-NEXT: Exported: lean obligation: plus_tri_zero (cppverify_auto proved 1 of 2 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{{$}} From 106cd5ea5169e220b77f78f16f8392f69c1b808b Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:39 +0530 Subject: [PATCH 090/191] test: Say that the proofs over tri are also written by hand. --- clang/test/Verify/suite/Inputs/lean_modules.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/clang/test/Verify/suite/Inputs/lean_modules.cpp b/clang/test/Verify/suite/Inputs/lean_modules.cpp index eb8b11b768..953d77f17a 100644 --- a/clang/test/Verify/suite/Inputs/lean_modules.cpp +++ b/clang/test/Verify/suite/Inputs/lean_modules.cpp @@ -1,6 +1,6 @@ -// Five functions for lean_modules.test: inc and dbl, which cppverify_auto -// proves, and two that apply the recursive spec tri, whose postconditions -// Inputs/lean_modules_*.lean prove. +// 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) From 511d17d40fa8d4205af1c57ab20427a539c3a7fd Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:39 +0530 Subject: [PATCH 091/191] test: Expect cppverify_auto to certify fibo_step, and fibo_six to need its proof. --- clang/test/Verify/suite/lean_fallback.test | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/clang/test/Verify/suite/lean_fallback.test b/clang/test/Verify/suite/lean_fallback.test index 72fe98493b..68674c76a0 100644 --- a/clang/test/Verify/suite/lean_fallback.test +++ b/clang/test/Verify/suite/lean_fallback.test @@ -23,9 +23,12 @@ // CHECK-DAG: Unresolved: fibo_step [backend=z3] [reason=solver.resource-limit] // CHECK-DAG: Exported: lean fallback: fibo_six [z3: 1 of 2 proved; lean: 2 exported] -// CERTIFY: Unresolved: lean fallback: fibo_step [z3: 1 of 2 proved; lean: 2 exported] (Lean certification failed: +// cppverify_auto proves the goals of fibo_step, the postcondition by the +// definition instances it states; that of fibo_six needs a proof. +// CERTIFY: Certified: fibo_step [backend=Lean] +// CERTIFY-NEXT: Unresolved: lean fallback: fibo_six [z3: 1 of 2 proved; lean: 2 exported] (Lean certification failed: // CERTIFY-SAME: declaration uses `sorry` -// CERTIFY-NOT: Unresolved: fibo_step (proof obligation +// CERTIFY-NOT: Unresolved: fibo_six (proof obligation // CERTIFIED: Certified: fibo_step [backend=Lean] // CERTIFIED: Certified: fibo_six [backend=Lean] @@ -33,7 +36,7 @@ // CERTIFIED-JSON: "backend":"lean" // CERTIFIED-JSON-SAME: "function":"fibo_step" // CERTIFIED-JSON-SAME: "status":"certified" -// FAILED-JSON: {"backend":"lean","evidence":{"lean":[{{.*}}],"solver":"z3","solver_proved":1,"total":2},"function":"fibo_step" +// FAILED-JSON: {"backend":"lean","evidence":{"lean":[{{.*}}],"solver":"z3","solver_proved":1,"total":2},"function":"fibo_six" // FAILED-JSON-SAME: "reason":"lean.export-failed" // FAILED-JSON-SAME: "status":"unresolved" From 1bbf9549c029340ac34923fba69cc37c0f5a5431 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:40 +0530 Subject: [PATCH 092/191] docs: Explain collections in Lean and revision 2 of cppverify_auto in chapter 17. --- .../part-ii/ch17-backends-modular-calls.rst | 132 ++++++++++++++++-- 1 file changed, 120 insertions(+), 12 deletions(-) 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 ce0f0911a4..e2563f24d6 100644 --- a/website/source/book/part-ii/ch17-backends-modular-calls.rst +++ b/website/source/book/part-ii/ch17-backends-modular-calls.rst @@ -261,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: @@ -436,6 +433,108 @@ prints: A proof of the goal itself, as the one for ``small`` above, checks as well. +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``. A goal that needs induction over a recursive spec, as +``total(s + t) == total(s) + total(t)`` does, cannot be proved in Lean yet: +Lean knows ``total`` only through the definition instances its goals state. + Proofs cppverify_auto finds ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -472,7 +571,7 @@ 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 2 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 @@ -485,7 +584,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 2 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 @@ -501,7 +600,14 @@ 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``. 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 @@ -509,13 +615,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 ``2`` +in the header), and never once you edit it. Revision 2 added the last three +steps, after the others, so that a goal an earlier step proves keeps its +proof; a stub that revision 1 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 @@ -609,7 +717,7 @@ one function builds again only what depends on it. For this file: 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 1 of 2 goals) + Exported: lean obligation: tri_zero (cppverify_auto proved 2 of 2 goals) and writes: From 98dfdd96520be958ddc1622615490687015cc486 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:40 +0530 Subject: [PATCH 093/191] docs: Say in chapter 13 that a Lean project states the spec collections. --- website/source/book/part-ii/ch13-spec-and-proof-functions.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 -------------------- From 20b4f9678cdd4ce4d02e4385e26a47d987ace9b4 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:40 +0530 Subject: [PATCH 094/191] docs: Drop the limitation that Lean lacks the spec collections. --- website/source/language/limitations.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/source/language/limitations.rst b/website/source/language/limitations.rst index 1dcaa09ded..383e32a315 100644 --- a/website/source/language/limitations.rst +++ b/website/source/language/limitations.rst @@ -426,8 +426,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 +579,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. From 64bc9483cfd323239a5a58c5db85f37bf8cb6e13 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:41 +0530 Subject: [PATCH 095/191] docs: Show revision 2 of cppverify_auto in the tooling reference. --- website/source/language/tooling.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/language/tooling.rst b/website/source/language/tooling.rst index d2b8429c7b..03a0daa2c3 100644 --- a/website/source/language/tooling.rst +++ b/website/source/language/tooling.rst @@ -365,7 +365,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 2 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 From 5cc1e0256fb971bb87cf4b88e3e3da64fe4e9fe4 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:41 +0530 Subject: [PATCH 096/191] docs: Note collections in Lean and revision 2 of cppverify_auto in the release notes. --- website/source/release-notes.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/website/source/release-notes.rst b/website/source/release-notes.rst index 60b91f1188..323c7da048 100644 --- a/website/source/release-notes.rst +++ b/website/source/release-notes.rst @@ -399,6 +399,23 @@ 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`). + **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 From 0d71ea434ffbfcb2ddeb58db0788a46fdbcd6aa1 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:41 +0530 Subject: [PATCH 097/191] docs: Describe the spec collections in Lean and the new automation steps in the design. --- docs/DESIGN.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 219ba08aad..9705ea040e 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1595,9 +1595,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 @@ -1951,7 +1956,9 @@ 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), 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 From a734035b2764efcdca9b37a0085314463096ec2c Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 11:42:42 +0530 Subject: [PATCH 098/191] docs: Describe the Lean collection prelude and revision 2 of cppverify_auto in the architecture. --- docs/ARCHITECTURE.md | 55 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 98795dfc1c..f0ee2fb13e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1546,6 +1546,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 +1594,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 +1622,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 @@ -1715,8 +1745,25 @@ 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. + 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 `Generated`, `User`, and `Auto` with the pinned toolchain, and runs `lean` itself (with the `LEAN_PATH` of `lake env`, so a @@ -1929,7 +1976,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 From 79094d8a0b5690ffa8741a620125fd5767cd4db0 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:03 +0530 Subject: [PATCH 099/191] verify: Declare a project's spec functions with their opaque declarations and candidate definitions. --- clang/lib/Verify/Backend/LeanBackend.h | 36 ++++++++++++++++---------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/clang/lib/Verify/Backend/LeanBackend.h b/clang/lib/Verify/Backend/LeanBackend.h index 5e2961edbe..176d58923d 100644 --- a/clang/lib/Verify/Backend/LeanBackend.h +++ b/clang/lib/Verify/Backend/LeanBackend.h @@ -40,11 +40,22 @@ struct LeanProjectGoal { /// The group of a module's goals in a project: its function's directory. std::string leanProjectGroup(const ObligationModule &Module); -/// The generated modules of a Lean project: the declarations of the specs -/// its goals apply, which every goal module imports, and each group's spec +/// 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::string Specs; + std::map Specs; std::map Goals; /// The groups in the order their functions were exported: the source's. std::vector Order; @@ -57,16 +68,15 @@ void emitLeanPrelude(llvm::raw_ostream &OS, bool Project); /// 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. Declarations, when given, receives -/// the declarations of the specs, each once. -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, - llvm::raw_ostream *Declarations = 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); class LeanVerifyBackend : public VerifyBackend { llvm::raw_ostream *Out = nullptr; From f8701ea85b3f4f2555c8f130ab22b32dc52810b6 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:03 +0530 Subject: [PATCH 100/191] verify: Print a spec's step definition for Lean, its conditions as hypotheses and its measures as termination_by. --- clang/lib/Verify/Backend/LeanBackend.cpp | 235 +++++++++++++++++++++-- 1 file changed, 214 insertions(+), 21 deletions(-) diff --git a/clang/lib/Verify/Backend/LeanBackend.cpp b/clang/lib/Verify/Backend/LeanBackend.cpp index dd8ea9a31b..25c590fa63 100644 --- a/clang/lib/Verify/Backend/LeanBackend.cpp +++ b/clang/lib/Verify/Backend/LeanBackend.cpp @@ -25,6 +25,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 = { @@ -305,6 +309,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) { @@ -377,8 +391,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) @@ -388,7 +427,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) @@ -400,7 +463,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); @@ -658,6 +721,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); @@ -665,6 +734,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); @@ -1116,25 +1191,130 @@ 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"; + } + 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; +} + +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; } - printLeanSort(Function.ResultSort, OS); - OS << "\n\n"; + if (EmittedFunctions.insert(Identity).second) + OS << leanOpaqueSpec(Function); } } @@ -1501,6 +1681,19 @@ void verify::emitLeanPrelude(llvm::raw_ostream &OS, bool Project) { OS << "def cppIntRem (x y : Int) : Int :=\n"; OS << " if y = 0 then x else x - cppIntDiv x y * y\n\n"; 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"; @@ -1516,7 +1709,8 @@ VerifyResult verify::exportLeanScratchPad( std::set &EmittedFunctions, std::set &EmittedTheorems, unsigned ModuleIndex, std::vector *ProjectGoals, - const std::set *Selection, llvm::raw_ostream *Declarations) { + const std::set *Selection, + std::map *Specs) { VerifyResult Result; if (!Module.CorrectnessGoal || !Module.CounterexampleQuery) { Result.Status = VerifyStatus::Unresolved; @@ -1557,8 +1751,7 @@ VerifyResult verify::exportLeanScratchPad( emitLeanPrelude(OS, ProjectGoals != nullptr); } - emitLeanFunctionDeclarations(Module, Declarations ? *Declarations : OS, - EmittedFunctions); + emitLeanFunctionDeclarations(Module, OS, EmittedFunctions, Specs); emitLeanFunctionBodies(Module, OS); OS << "/- function: "; printLeanCommentText(Module.FunctionName, OS); From 0f293cccc450c7b5f72f5ab1d02f7f31aa4d2f51 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:04 +0530 Subject: [PATCH 101/191] verify: Collect a project's spec functions by identity. --- clang/lib/Verify/Backend/VerifyBackend.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/clang/lib/Verify/Backend/VerifyBackend.cpp b/clang/lib/Verify/Backend/VerifyBackend.cpp index 1c69d41bbc..fac014a9f8 100644 --- a/clang/lib/Verify/Backend/VerifyBackend.cpp +++ b/clang/lib/Verify/Backend/VerifyBackend.cpp @@ -30,11 +30,10 @@ VerifyResult LeanVerifyBackend::verifyModule(const ObligationModule &Module) { if (!Modules->Goals.count(Group)) Modules->Order.push_back(Group); llvm::raw_string_ostream Goals(Modules->Goals[Group]); - llvm::raw_string_ostream Specs(Modules->Specs); - Result = exportLeanScratchPad(Module, Goals, /*EmitPreamble=*/false, - EmittedFunctions, EmittedTheorems, - ++ModuleIndex, ProjectGoals, - Selection ? &*Selection : nullptr, &Specs); + Result = exportLeanScratchPad( + Module, Goals, /*EmitPreamble=*/false, EmittedFunctions, + EmittedTheorems, ++ModuleIndex, ProjectGoals, + Selection ? &*Selection : nullptr, &Modules->Specs); } else { Result = exportLeanScratchPad( Module, *Out, !PreambleEmitted, EmittedFunctions, EmittedTheorems, From 842d4b16b11041119a1dc97423fb362258d8acfe Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:04 +0530 Subject: [PATCH 102/191] verify: Define the spec functions that Lean proves terminating, and keep its verdict on each. --- clang/lib/Verify/Driver/Verifier.cpp | 249 ++++++++++++++++++++++++++- 1 file changed, 247 insertions(+), 2 deletions(-) diff --git a/clang/lib/Verify/Driver/Verifier.cpp b/clang/lib/Verify/Driver/Verifier.cpp index 8800d39092..841c52b63f 100644 --- a/clang/lib/Verify/Driver/Verifier.cpp +++ b/clang/lib/Verify/Driver/Verifier.cpp @@ -1394,6 +1394,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(); } @@ -1751,8 +1755,10 @@ class Verifier { 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" + - Modules.Specs)) + 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)) @@ -2104,6 +2110,245 @@ 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 From cc0fc2e6c073600f642cbdafebb48a0ed17957dc Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:04 +0530 Subject: [PATCH 103/191] verify: Evaluate spec definitions by their equations and read C++ division by its definition in revision 3 of cppverify_auto. --- clang/lib/Verify/Driver/LeanAutomation.h | 52 +++++++++++++++++++++--- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/clang/lib/Verify/Driver/LeanAutomation.h b/clang/lib/Verify/Driver/LeanAutomation.h index 2eca874d9e..fa807cf23c 100644 --- a/clang/lib/Verify/Driver/LeanAutomation.h +++ b/clang/lib/Verify/Driver/LeanAutomation.h @@ -9,13 +9,14 @@ 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 = "2"; +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_bveq", - "cppverify_auto_ext", "cppverify_auto_unfold"}; + "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 @@ -26,6 +27,9 @@ inline constexpr llvm::StringLiteral LeanAutomationSteps[] = { /// 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. -/ @@ -110,6 +114,28 @@ elab "cppverify_unfold_bodies" : tactic => withMainContext do 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, @@ -172,6 +198,20 @@ macro "cppverify_auto_unfold" : tactic => `(tactic| ( 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])) open Lean Elab Tactic in /-- Tries each step on the goal, each with `cppverify.autoBudget` heartbeats @@ -190,7 +230,9 @@ elab "cppverify_auto" : tactic => do ("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_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 From 53c7ab55239b51255c23ce7bb57e892806833131 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:05 +0530 Subject: [PATCH 104/191] test: Add the spec functions of the Lean spec definitions test. --- .../suite/Inputs/lean_spec_definitions.cpp | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/lean_spec_definitions.cpp 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)) +{ +} From bc2d146dc30a9d2e683a2fddedcb5137e0708c9f Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:05 +0530 Subject: [PATCH 105/191] test: Add a proof by induction over tri for the Lean spec definitions test. --- .../Inputs/lean_spec_definitions_tri_nonneg.lean | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/lean_spec_definitions_tri_nonneg.lean 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 _ From 0a8d6e416077a51020342f3a222a754f9207e1fa Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:05 +0530 Subject: [PATCH 106/191] test: Check which spec functions become Lean definitions, and a proof by induction over one. --- .../Verify/suite/lean_spec_definitions.test | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 clang/test/Verify/suite/lean_spec_definitions.test 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{{$}} From d6900928efbd286654124e2232f6392d53f43a98 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:06 +0530 Subject: [PATCH 107/191] test: Add contracts that divide by constants for the Lean division test. --- clang/test/Verify/suite/Inputs/lean_division.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/lean_division.cpp 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) +{ +} From 30de5c8aa8e93c4c43efcbe408b4c98bcd201ef1 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:06 +0530 Subject: [PATCH 108/191] test: Check that cppverify_auto proves contracts that divide by constants. --- clang/test/Verify/suite/lean_division.test | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 clang/test/Verify/suite/lean_division.test 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] From 9f7861186e892adab387b0e01b1bf1aa4a1a020e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:06 +0530 Subject: [PATCH 109/191] test: Expect tri as a definition in the project's Specs module. --- clang/test/Verify/suite/lean_modules.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clang/test/Verify/suite/lean_modules.test b/clang/test/Verify/suite/lean_modules.test index 0861dc5eef..dc49ce3bbb 100644 --- a/clang/test/Verify/suite/lean_modules.test +++ b/clang/test/Verify/suite/lean_modules.test @@ -9,7 +9,7 @@ # 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=opaque +# 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 @@ -77,7 +77,7 @@ # SPECS: {{^}}import CppVerify.Prelude{{$}} # SPECS: /- spec function: tri -/ -# SPECS-NEXT: {{^}}opaque cppSpec_fn_5f5a3374726969 : Int -> Int{{$}} +# 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{{$}} From 5adb8f96b7fa47ef648925d75ee055a2d1702248 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:07 +0530 Subject: [PATCH 110/191] test: Expect total and members as definitions, and revision 3 of cppverify_auto. --- clang/test/Verify/suite/lean_collections.test | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/clang/test/Verify/suite/lean_collections.test b/clang/test/Verify/suite/lean_collections.test index 2398cea2b0..2a2daf888c 100644 --- a/clang/test/Verify/suite/lean_collections.test +++ b/clang/test/Verify/suite/lean_collections.test @@ -36,14 +36,14 @@ # 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 2 proves it with cppverify_auto_bveq -# STEPS-NEXT: automation: cppverify_auto 2 proves it with cppverify_auto_ext -# STEPS-NEXT: automation: cppverify_auto 2 proves it with cppverify_auto_unfold +# 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: {{^}}opaque cppSpec_fn_5f5a35746f74616c4e396370707665726966793373657145 : List Int -> Int{{$}} +# SPECS-NEXT: {{^}}def cppSpec_fn_5f5a35746f74616c4e396370707665726966793373657145 (s : List Int) : Int :={{$}} # SPECS: /- spec function: members -/ -# SPECS-NEXT: {{^}}opaque cppSpec_fn_5f5a376d656d626572734e39637070766572696679337365744569 : (Int → Bool) -> Int -> Int{{$}} +# 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{{$}} From dc2f5d8f34e01ffb7c5dc3a6e2b5608d214e6e00 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:07 +0530 Subject: [PATCH 111/191] test: Expect revision 3 of cppverify_auto in the proof headers. --- clang/test/Verify/suite/lean_automation.test | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/clang/test/Verify/suite/lean_automation.test b/clang/test/Verify/suite/lean_automation.test index 74c4a8a628..cb906ffbd1 100644 --- a/clang/test/Verify/suite/lean_automation.test +++ b/clang/test/Verify/suite/lean_automation.test @@ -29,7 +29,7 @@ # 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.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 2 did not prove it/cppverify_auto 1 did not prove it/' %t/p/CppVerify/Proofs/cppverify_square_934e38/cppverify_square_934e38_overflow_f86973f7.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,7 +54,7 @@ # 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 2 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 @@ -67,11 +67,11 @@ # PROVED-NEXT: cppverify_auto_grind # PROVED-EMPTY: -# STUB: automation: cppverify_auto 2 did not prove it{{$}} +# STUB: automation: cppverify_auto 3 did not prove it{{$}} # STUB-NEXT: Complete this proof. # STUB: {{^}} sorry{{$}} -# SLOW: automation: cppverify_auto 2 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" @@ -82,10 +82,10 @@ # EDITED: Exported: lean obligation: next (cppverify_auto proved 2 of 3 goals) -# TRIED: automation: cppverify_auto 2 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 2 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{{$}} From b284bcc360c1198abeec649474a90e7f263ffb00 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:08 +0530 Subject: [PATCH 112/191] test: Run the fallback test without cppverify_auto, so that its goals wait for their proofs. --- clang/test/Verify/suite/lean_fallback.test | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/clang/test/Verify/suite/lean_fallback.test b/clang/test/Verify/suite/lean_fallback.test index 68674c76a0..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: %python %S/Inputs/lean_project_text.py %t.project | FileCheck %s --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: 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. @@ -23,12 +25,9 @@ // CHECK-DAG: Unresolved: fibo_step [backend=z3] [reason=solver.resource-limit] // CHECK-DAG: Exported: lean fallback: fibo_six [z3: 1 of 2 proved; lean: 2 exported] -// cppverify_auto proves the goals of fibo_step, the postcondition by the -// definition instances it states; that of fibo_six needs a proof. -// CERTIFY: Certified: fibo_step [backend=Lean] -// CERTIFY-NEXT: Unresolved: lean fallback: fibo_six [z3: 1 of 2 proved; lean: 2 exported] (Lean certification failed: +// CERTIFY: Unresolved: lean fallback: fibo_step [z3: 1 of 2 proved; lean: 2 exported] (Lean certification failed: // CERTIFY-SAME: declaration uses `sorry` -// CERTIFY-NOT: Unresolved: fibo_six (proof obligation +// CERTIFY-NOT: Unresolved: fibo_step (proof obligation // CERTIFIED: Certified: fibo_step [backend=Lean] // CERTIFIED: Certified: fibo_six [backend=Lean] @@ -36,7 +35,7 @@ // CERTIFIED-JSON: "backend":"lean" // CERTIFIED-JSON-SAME: "function":"fibo_step" // CERTIFIED-JSON-SAME: "status":"certified" -// FAILED-JSON: {"backend":"lean","evidence":{"lean":[{{.*}}],"solver":"z3","solver_proved":1,"total":2},"function":"fibo_six" +// FAILED-JSON: {"backend":"lean","evidence":{"lean":[{{.*}}],"solver":"z3","solver_proved":1,"total":2},"function":"fibo_step" // FAILED-JSON-SAME: "reason":"lean.export-failed" // FAILED-JSON-SAME: "status":"unresolved" From 2cdcfea7768017f4435ac018bbc3c63eac98a034 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:08 +0530 Subject: [PATCH 113/191] docs: Explain spec functions as Lean definitions in chapter 17. --- .../part-ii/ch17-backends-modular-calls.rst | 119 ++++++++++++++++-- 1 file changed, 107 insertions(+), 12 deletions(-) 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 e2563f24d6..7ed662d840 100644 --- a/website/source/book/part-ii/ch17-backends-modular-calls.rst +++ b/website/source/book/part-ii/ch17-backends-modular-calls.rst @@ -433,6 +433,95 @@ 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 ~~~~~~~~~~~~~~~~~~~ @@ -531,9 +620,9 @@ The postcondition of ``total_push`` needs the definition of ``total`` at omega Either way, ``--lean-certify`` prints ``Certified`` for ``total``, ``last``, -and ``total_push``. A goal that needs induction over a recursive spec, as -``total(s + t) == total(s) + total(t)`` does, cannot be proved in Lean yet: -Lean knows ``total`` only through the definition instances its goals state. +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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -571,7 +660,7 @@ A goal it proves gets that proof, with a header that says so: .. code-block:: lean4 - automation: cppverify_auto 2 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 @@ -584,7 +673,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 2 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 @@ -607,7 +696,14 @@ first that proves the goal: - ``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``. + 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 @@ -620,10 +716,10 @@ 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 ``2`` -in the header), and never once you edit it. Revision 2 added the last three -steps, after the others, so that a goal an earlier step proves keeps its -proof; a stub that revision 1 wrote is tried again. 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 @@ -744,8 +840,7 @@ and writes: Check.lean imports every proof ``Specs.lean`` declares each spec function the goals apply once, ``tri`` -here, as a function of which nothing is known -(``opaque cppSpec_fn_5f5a3374726969 : Int -> Int``). Each goal module imports +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 From 02d6e5b260cad9fb9fbf65b53ff5e7a227a80246 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:08 +0530 Subject: [PATCH 114/191] docs: Say how a Lean project declares its spec functions. --- website/source/language/tooling.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/website/source/language/tooling.rst b/website/source/language/tooling.rst index 03a0daa2c3..9b46373167 100644 --- a/website/source/language/tooling.rst +++ b/website/source/language/tooling.rst @@ -287,7 +287,9 @@ Backends * - ``--lean-project=DIR`` - With ``--backend=lean``, which it selects, generate a pinned editable project. Generated semantics live in a module per function, ``CppVerify/Goals/.lean``, - with each spec they apply declared once in ``CppVerify/Specs.lean``; a + 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 @@ -365,7 +367,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 2 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 From 32486bcdcc38fe6e2096664cc10548dc65bb8e09 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:09 +0530 Subject: [PATCH 115/191] docs: Note spec functions as Lean definitions in the release notes. --- website/source/release-notes.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/website/source/release-notes.rst b/website/source/release-notes.rst index 323c7da048..7f0115d48a 100644 --- a/website/source/release-notes.rst +++ b/website/source/release-notes.rst @@ -416,6 +416,19 @@ 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 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 From 8c7906809f7b4786bf3e57eb9bc38c6a4a5ee92e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 16:19:09 +0530 Subject: [PATCH 116/191] docs: Describe spec functions as Lean definitions in the design. --- docs/DESIGN.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 9705ea040e..3f51623778 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1927,7 +1927,14 @@ 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 goal is named by its function and its statement, never by a +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 @@ -1958,7 +1965,9 @@ 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`; machine equalities and sign extensions as integers; collection equality element by element; the definition instances a goal -states, by the bodies they name), each with a heartbeat budget of its own and a +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 From 50740fec7995c7f5490f5a39f05268104307c689 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:13:35 +0530 Subject: [PATCH 117/191] docs: Describe the spec definitions, their trial, and revision 3 of cppverify_auto in the architecture. --- docs/ARCHITECTURE.md | 57 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f0ee2fb13e..e2603cfe67 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1676,6 +1676,40 @@ uses when the source around it changes. 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 @@ -1761,7 +1795,28 @@ uses when the source around it changes. 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. + 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 From 747ba388f935e049a86b9641036afb3fef317416 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:34 +0530 Subject: [PATCH 118/191] verify: Say that archives keep a logical function's measures and where a goal's assumptions come from. --- clang/lib/Verify/Backend/Obligation.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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"; }; From d4592ba9d1f988e1bf4bd99ba22afe85079b166d Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:35 +0530 Subject: [PATCH 119/191] verify: Validate a logical function's termination measures like its definition. --- clang/lib/Verify/Backend/Obligation.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) 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; } From 2ce8c54a830334894954591ddc8d44a30a1fbb18 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:35 +0530 Subject: [PATCH 120/191] verify: Keep the functions a termination measure applies when canonicalizing a module. --- clang/lib/Verify/Backend/ObligationSimplify.cpp | 4 ++++ 1 file changed, 4 insertions(+) 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); From 6a7d0a6edcd28c947eb4e93933f775c678e00a02 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:35 +0530 Subject: [PATCH 121/191] verify: Make obligation archives schema 3. --- clang/lib/Verify/Backend/ObligationSerialization.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 From d67c76fa5f016e8a3e1705be9851d49a876444d5 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:36 +0530 Subject: [PATCH 122/191] verify: Archive termination measures and where each assumption of a goal comes from. --- .../Backend/ObligationSerialization.cpp | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) 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; } From 4196eefcd58aae6db9af8c68aa851451efae1a7d Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:36 +0530 Subject: [PATCH 123/191] verify: Mark the unwinding goals of a Lean project. --- clang/lib/Verify/Backend/LeanBackend.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/clang/lib/Verify/Backend/LeanBackend.h b/clang/lib/Verify/Backend/LeanBackend.h index 176d58923d..94f82a6edd 100644 --- a/clang/lib/Verify/Backend/LeanBackend.h +++ b/clang/lib/Verify/Backend/LeanBackend.h @@ -35,6 +35,9 @@ 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. From 760896fef8724d79654ac7de90c608ebf13257d8 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:37 +0530 Subject: [PATCH 124/191] verify: State the bound of a bounded module with its Lean goals. --- clang/lib/Verify/Backend/LeanBackend.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/clang/lib/Verify/Backend/LeanBackend.cpp b/clang/lib/Verify/Backend/LeanBackend.cpp index 25c590fa63..944369ecfd 100644 --- a/clang/lib/Verify/Backend/LeanBackend.cpp +++ b/clang/lib/Verify/Backend/LeanBackend.cpp @@ -1757,7 +1757,13 @@ VerifyResult verify::exportLeanScratchPad( 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_" + @@ -1805,6 +1811,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; From ee37f710fb8280332cabdd008d75bc60e40f5136 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:37 +0530 Subject: [PATCH 125/191] verify: Declare the replay of archived modules into a Lean project. --- clang/lib/Verify/Driver/Verifier.h | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 From 818ce9a924fdfaab897f22daa6333bb3ea808edf Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:37 +0530 Subject: [PATCH 126/191] verify: Write Lean projects through one LeanProject, from source or from an archive. --- clang/lib/Verify/Driver/Verifier.cpp | 447 +++++++++++++++++---------- 1 file changed, 280 insertions(+), 167 deletions(-) diff --git a/clang/lib/Verify/Driver/Verifier.cpp b/clang/lib/Verify/Driver/Verifier.cpp index 841c52b63f..afecdfcb86 100644 --- a/clang/lib/Verify/Driver/Verifier.cpp +++ b/clang/lib/Verify/Driver/Verifier.cpp @@ -1548,129 +1548,23 @@ 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::Error initializeLeanProject() { const std::string &Root = leanProjectRoot(); @@ -1698,14 +1592,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); @@ -1906,12 +1798,11 @@ class Verifier { 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()); @@ -1922,11 +1813,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)) { @@ -1972,9 +1861,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) { @@ -2020,12 +1908,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"; @@ -2720,9 +2607,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; @@ -2733,12 +2620,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 @@ -2771,9 +2653,8 @@ 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.Generated", "CppVerify.User", "CppVerify.Auto"}, @@ -2838,10 +2719,135 @@ class Verifier { 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, @@ -3070,7 +3076,10 @@ class Verifier { // A project's goals go to modules, one per function. LeanProjectModules LeanModules; if (HasLeanProject) { - if (llvm::Error Error = initializeLeanProject()) { + Lean.emplace(Opts); + llvm::Error Error = Lean->initializeLeanProject(); + reportLeanWarnings(); + if (Error) { Diags.push_back({VerifyDiagnostic::Error, "cannot initialize Lean project (" + llvm::toString(std::move(Error)) + ")"}); @@ -4566,13 +4575,23 @@ class Verifier { [](const LeanProjectGoal &Goal) { return Goal.Exported; }); const std::set *RunGroups = LeanFallbackBackend ? &LeanRunGroups : nullptr; - llvm::Error Finalized = writeLeanProjectModules(LeanModules); + llvm::Error Finalized = Lean->writeLeanProjectModules(LeanModules); if (!Finalized) - Finalized = finalizeLeanProject(LeanProjectGoals, RunGroups); + Finalized = Lean->finalizeLeanProject(LeanProjectGoals, RunGroups); if (!Finalized && Opts.LeanAuto) - automateLeanProject(LeanProjectGoals); - if (!Finalized) - countLeanAutomation(LeanProjectGoals); + 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; @@ -4605,7 +4624,7 @@ class Verifier { Diagnostic.Result->Message = Reason; } }; - auto Certified = certifyLeanProject(ExportedGoals); + auto Certified = Lean->certifyLeanProject(ExportedGoals); std::map GoalsByName; for (const LeanProjectGoal &Goal : ExportedGoals) GoalsByName.emplace(Goal.Name, &Goal); @@ -5580,4 +5599,98 @@ 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. + auto Certified = Project.certifyLeanProject(Goals); + std::string Failed; + if (!Certified) + Failed = llvm::toString(Certified.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 From 4c0bc6ae119c3530030ada097716c412ad9c28e7 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:38 +0530 Subject: [PATCH 127/191] verify: Replay archives into Lean projects, and bounded archives into Lean. --- clang/tools/cpp-verify/Main.cpp | 121 +++++++++++++++++++++++--------- 1 file changed, 89 insertions(+), 32 deletions(-) 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; From 93f2c2c70365bec0535ddd106e78763a78684c6e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:38 +0530 Subject: [PATCH 128/191] test: Read schema 3 archives, and write schema 2 ones, in the archive mutator. --- .../suite/Inputs/mutate_obligation_archive.py | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) 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(" Date: Fri, 23 Oct 2026 17:54:38 +0530 Subject: [PATCH 129/191] test: Expect schema 3, read schema 2, and reject a measure with a free variable. --- clang/test/Verify/suite/obligation_archive.test | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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 From f799055c7cabfb7203afd65645fbd3ff6ec8e07e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:39 +0530 Subject: [PATCH 130/191] test: Expect schema 3 in the obligation IR dump. --- clang/test/Verify/suite/obligation_ir.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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]+]] From b6e2857164db92f3245e74fdf215102b1469780e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:39 +0530 Subject: [PATCH 131/191] test: Add the program of the Lean replay test. --- .../test/Verify/suite/Inputs/lean_replay.cpp | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/lean_replay.cpp 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; +} From b5b3b0634e98cf28e7160c5463ec109609c5679a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:39 +0530 Subject: [PATCH 132/191] test: Add the loops of the Lean replay test. --- .../Verify/suite/Inputs/lean_replay_loops.cpp | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/lean_replay_loops.cpp 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; +} From 7df524a2299e722bae77a0ee7c52812ed13dfc7a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:40 +0530 Subject: [PATCH 133/191] test: Check that an archive replays into a Lean project as its source exports. --- clang/test/Verify/suite/lean_replay.test | 99 ++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 clang/test/Verify/suite/lean_replay.test 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 From b255c4373017889f61910567e4748b2daa0ee6ea Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:40 +0530 Subject: [PATCH 134/191] docs: Explain archives in Lean projects in chapter 17. --- .../part-ii/ch17-backends-modular-calls.rst | 105 +++++++++++++++++- 1 file changed, 99 insertions(+), 6 deletions(-) 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 7ed662d840..84198725c5 100644 --- a/website/source/book/part-ii/ch17-backends-modular-calls.rst +++ b/website/source/book/part-ii/ch17-backends-modular-calls.rst @@ -956,8 +956,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 @@ -979,27 +980,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 ---------------------- From 8ce4ce2bd4e85579623b07e40016016c100e99fb Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:41 +0530 Subject: [PATCH 135/191] docs: Say how an archive replays into a Lean project. --- website/source/language/tooling.rst | 32 +++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/website/source/language/tooling.rst b/website/source/language/tooling.rst index 9b46373167..bc8a7b150f 100644 --- a/website/source/language/tooling.rst +++ b/website/source/language/tooling.rst @@ -467,11 +467,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 @@ -783,13 +785,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 @@ -799,7 +814,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 From cdeb7df3865e515c4f7e9f33f68299b27916adea Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:41 +0530 Subject: [PATCH 136/191] docs: Note Lean projects from archives and schema 3 in the release notes. --- website/source/release-notes.rst | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/website/source/release-notes.rst b/website/source/release-notes.rst index 7f0115d48a..b116cdb75a 100644 --- a/website/source/release-notes.rst +++ b/website/source/release-notes.rst @@ -429,6 +429,21 @@ 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`). + **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 @@ -443,8 +458,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`` From 76270a9743d738a3c5b36f71161bb411b8a57d70 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:41 +0530 Subject: [PATCH 137/191] docs: Describe Lean replay and schema 3 in the design. --- docs/DESIGN.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 3f51623778..712f4e18a6 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1775,15 +1775,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 @@ -1793,14 +1796,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 From cb86df1de4e847629bca8540b8f1dce1cf474d50 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 17:54:42 +0530 Subject: [PATCH 138/191] docs: Describe LeanProject, Lean replay, and schema 3 in the architecture. --- docs/ARCHITECTURE.md | 58 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e2603cfe67..b245212b65 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. @@ -1640,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 From 87dc023efb628b2da1ae4cbabf9b94cddab4e73f Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 19:25:19 +0530 Subject: [PATCH 139/191] docs: Say that archives keep where each assumption of a goal comes from in the architecture. --- docs/ARCHITECTURE.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b245212b65..44d83331bf 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1775,8 +1775,9 @@ replay both drive it. - **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`, From 5f55b5a492bfdccd7c149a301eb2fdaf834a6432 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:45 +0530 Subject: [PATCH 140/191] verify: Let a certified counterexample carry the values of its goal's variables. --- clang/lib/Verify/Backend/VerifyBackend.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/clang/lib/Verify/Backend/VerifyBackend.h b/clang/lib/Verify/Backend/VerifyBackend.h index 6e3a71e715..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; From 742e6c7a2a8d21fbd24f9a5eb0aff35ec17d3c7e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:45 +0530 Subject: [PATCH 141/191] verify: Declare the assignment of a certified counterexample. --- clang/lib/Verify/Backend/Certify.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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, From 288e9a4df53fc49fbce416456e409689c6a7eda1 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:46 +0530 Subject: [PATCH 142/191] verify: Read the assignment of a certified counterexample from its candidate model. --- clang/lib/Verify/Backend/Certify.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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) { From aeb21829f22619d55380f94bacde8f15fc562a6b Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:46 +0530 Subject: [PATCH 143/191] verify: Keep the assignment of each counterexample the Z3 adapter certifies. --- clang/lib/Verify/Backend/Z3Encode.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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); From ce3dc2b408a13cf25880f9d2543ca313925904d0 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:47 +0530 Subject: [PATCH 144/191] verify: Declare the Lean refutation of a counterexample. --- clang/lib/Verify/Backend/LeanBackend.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/clang/lib/Verify/Backend/LeanBackend.h b/clang/lib/Verify/Backend/LeanBackend.h index 94f82a6edd..c2cc672246 100644 --- a/clang/lib/Verify/Backend/LeanBackend.h +++ b/clang/lib/Verify/Backend/LeanBackend.h @@ -81,6 +81,26 @@ VerifyResult exportLeanScratchPad( 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 = nullptr; LeanProjectModules *Modules = nullptr; From 34415e8e42602999dd8ef9e4be553aaa4e12a21c Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:47 +0530 Subject: [PATCH 145/191] verify: Export a counterexample as a Lean refutation, and decide bounded integer quantifiers in the prelude. --- clang/lib/Verify/Backend/LeanBackend.cpp | 155 +++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/clang/lib/Verify/Backend/LeanBackend.cpp b/clang/lib/Verify/Backend/LeanBackend.cpp index 944369ecfd..5ea0ce0786 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" @@ -1293,6 +1294,32 @@ static std::string leanSpecDefinition(const ObligationModule &Module, 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. +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) := + 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⟩) + +instance cppIntBexDecidable (lo hi : Int) (p : Int → Prop) [DecidablePred p] : + Decidable (∃ x : Int, lo ≤ x ∧ x < hi ∧ p x) := + 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]) + +)lean"; + static void emitLeanFunctionDeclarations( const ObligationModule &Module, llvm::raw_ostream &OS, std::set &EmittedFunctions, @@ -1680,6 +1707,7 @@ void verify::emitLeanPrelude(llvm::raw_ostream &OS, bool Project) { 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"; @@ -1704,6 +1732,77 @@ void verify::emitLeanPrelude(llvm::raw_ostream &OS, bool Project) { } } +// 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, @@ -1854,4 +1953,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 From 0ce103143c3ac93d36767dc87371029d3c909f11 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:47 +0530 Subject: [PATCH 146/191] verify: Add the cppverify_refute tactic. --- clang/lib/Verify/Driver/LeanAutomation.h | 61 ++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/clang/lib/Verify/Driver/LeanAutomation.h b/clang/lib/Verify/Driver/LeanAutomation.h index fa807cf23c..8166156719 100644 --- a/clang/lib/Verify/Driver/LeanAutomation.h +++ b/clang/lib/Verify/Driver/LeanAutomation.h @@ -213,6 +213,67 @@ macro "cppverify_auto_div" : tactic => `(tactic| ( 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 From 0a057fff7de7d039e06bef4ac8c97d98c7649a1e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:48 +0530 Subject: [PATCH 147/191] verify: Check refutation entries against the negation of their goals. --- clang/lib/Verify/Driver/LeanCertifier.h | 52 +++++++++++++++---------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/clang/lib/Verify/Driver/LeanCertifier.h b/clang/lib/Verify/Driver/LeanCertifier.h index 4286533013..da062f8322 100644 --- a/clang/lib/Verify/Driver/LeanCertifier.h +++ b/clang/lib/Verify/Driver/LeanCertifier.h @@ -130,11 +130,13 @@ unsafe def replayProject (closure : Closure) : IO Replayed := do return { env, rejected, owner } /-- Whether the proof of a goal states exactly the goal its generated module -defines and depends on no axiom outside the allowed ones. -/ +defines, or a refutation exactly its negation, and depends on no axiom +outside the allowed ones. -/ def checkStatement (project : Replayed) (goal module generated : Name) - (allowed : NameSet) : Option String := Id.run do + (refutes : Bool) (allowed : NameSet) : Option String := Id.run do let env := project.env - let proof := goal.appendAfter "_proof" + 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 project.owner[goal]? != some generated then @@ -144,8 +146,8 @@ def checkStatement (project : Replayed) (goal module generated : Name) | some (.thmInfo v) => 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 => @@ -169,35 +171,45 @@ def userSorry (closure : Closure) (project : Replayed) : Option String := Id.run 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, 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. 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 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. -/ +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 × Name) := #[] + let mut entries : Array (Name × Name × Name × Bool) := #[] for line in ← IO.FS.lines goals do - let [goal, module, generated] := line.splitOn "\t" | continue - entries := entries.push (goal.toName, module.toName, generated.toName) - let roots := entries.map (·.2.2) ++ entries.map (·.2.1) + 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) in entries do + 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 allowed + checkStatement project goal module generated refutes allowed match why with - | none => IO.println s!"ok\t{goal}" - | some why => IO.println s!"fail\t{goal}\t{why.replace "\n" " "}" + | 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"; From e31d6454a65bba4e618f481639bda3d12b7c3644 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:48 +0530 Subject: [PATCH 148/191] verify: Confirm the counterexamples of a fallback project in Lean. --- clang/lib/Verify/Driver/Verifier.cpp | 197 ++++++++++++++++++++++++--- 1 file changed, 176 insertions(+), 21 deletions(-) diff --git a/clang/lib/Verify/Driver/Verifier.cpp b/clang/lib/Verify/Driver/Verifier.cpp index afecdfcb86..6854f53d33 100644 --- a/clang/lib/Verify/Driver/Verifier.cpp +++ b/clang/lib/Verify/Driver/Verifier.cpp @@ -1063,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 { @@ -1678,6 +1680,41 @@ class LeanProject { return llvm::Error::success(); } + // 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 leanProofComponents(const LeanProjectGoal &Goal, std::string &File) { File = Goal.Name + ".lean"; @@ -2289,8 +2326,9 @@ class LeanProject { } 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"); @@ -2317,11 +2355,37 @@ class LeanProject { 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, unless nothing it is @@ -2336,12 +2400,9 @@ class LeanProject { 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)) @@ -2370,13 +2431,11 @@ class LeanProject { 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 + - "\tCppVerify.Goals." + Goal->Group + "\n"; + Built += Proofs[I].Entry; } if (Built.empty()) return Failures; @@ -2409,9 +2468,9 @@ class LeanProject { 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; } @@ -3258,6 +3317,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 @@ -4475,6 +4542,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 + "]"; @@ -4521,6 +4603,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; @@ -4528,6 +4611,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) @@ -4558,6 +4645,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) @@ -4575,9 +4666,15 @@ class Verifier { [](const LeanProjectGoal &Goal) { return Goal.Exported; }); 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) Finalized = Lean->finalizeLeanProject(LeanProjectGoals, RunGroups); + if (!Finalized && LeanFallbackBackend) + Finalized = Lean->writeLeanRefutations(LeanRefutations); if (!Finalized && Opts.LeanAuto) Lean->automateLeanProject(LeanProjectGoals); reportLeanWarnings(); @@ -4599,7 +4696,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. @@ -4624,12 +4721,15 @@ class Verifier { Diagnostic.Result->Message = Reason; } }; - auto Certified = Lean->certifyLeanProject(ExportedGoals); + auto Certified = + Lean->certifyLeanProject(ExportedGoals, LeanRefutations); std::map GoalsByName; for (const LeanProjectGoal &Goal : ExportedGoals) GoalsByName.emplace(Goal.Name, &Goal); + std::string NotCertified; if (!Certified) { const std::string Reason = llvm::toString(Certified.takeError()); + NotCertified = Reason; for (size_t Index = 0; Index != Diags.size(); ++Index) if (Diags[Index].K == VerifyDiagnostic::Exported) failed(Index, Reason); @@ -4696,6 +4796,11 @@ class Verifier { Diagnostic.Message += " [vacuous]"; } } + + if (Certified) + RefutationChecks = *Certified; + else + RefutationsUnchecked = NotCertified; } } for (const SpecReliance &Check : ProofDependencies) @@ -5224,6 +5329,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) { @@ -5373,6 +5526,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()) From 2706a6eb95b92f8cc472a1784ac1f95036ad314d Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:49 +0530 Subject: [PATCH 149/191] test: Add the program of the Lean refutation test. --- .../Verify/suite/Inputs/lean_refutation.cpp | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/lean_refutation.cpp 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) +{ +} From 481b3c8f390fed19a0bbb42b4f038e0c647654b3 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 02:02:48 +0530 Subject: [PATCH 150/191] test: Add the refutations of the Lean refutation test that are bounded or not reported. --- .../suite/Inputs/lean_refutation_budget.cpp | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/lean_refutation_budget.cpp 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..1d75a803eb --- /dev/null +++ b/clang/test/Verify/suite/Inputs/lean_refutation_budget.cpp @@ -0,0 +1,24 @@ +// Refutations that are bounded, and refutations that are not reported, for +// lean_refutation.test. + +// Lean would decide this one by trying each of a billion integers: each +// attempt of the refutation stops at the heartbeats of a step of +// cppverify_auto, and the counterexample is not decided there. +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)) +{ +} From 8143117a7b40f46496670e0c3a6be7f874805a64 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:49 +0530 Subject: [PATCH 151/191] test: Add the instances the refutation tactic must not refute. --- .../suite/Inputs/lean_refutation_tactic.lean | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/lean_refutation_tactic.lean 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₁ From feac3c452728b522c07bcf96011a22f4c118e443 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:49 +0530 Subject: [PATCH 152/191] test: Check that counterexamples are confirmed in Lean, or left with a reason. --- clang/test/Verify/suite/lean_refutation.test | 88 ++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 clang/test/Verify/suite/lean_refutation.test diff --git a/clang/test/Verify/suite/lean_refutation.test b/clang/test/Verify/suite/lean_refutation.test new file mode 100644 index 0000000000..55ddb8a3a4 --- /dev/null +++ b/clang/test/Verify/suite/lean_refutation.test @@ -0,0 +1,88 @@ +# 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_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 From e6cc4dadfc4b6cb2d3201dd7f34f4b617241b354 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:50 +0530 Subject: [PATCH 153/191] test: Add the proof of the one fidelity matrix goal cppverify_auto leaves open. --- ...heap_spec_read_2c60ae_postcondition_af8523f4.lean | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/backend_fidelity_proofs/cppverify_heap_spec_read_2c60ae_postcondition_af8523f4.lean 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 From 97c7129f3e8a10d9bc062dd962f510b0c0bb7418 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:50 +0530 Subject: [PATCH 154/191] test: Require decisive Lean verdicts in the backend fidelity gate. --- .../suite/Inputs/run_backend_fidelity.py | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/clang/test/Verify/suite/Inputs/run_backend_fidelity.py b/clang/test/Verify/suite/Inputs/run_backend_fidelity.py index d8734e15cc..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 @@ -528,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", "") @@ -617,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) @@ -642,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 ) From bda0be4e62e020243eed678dc1e0e59e801bccb1 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:51 +0530 Subject: [PATCH 155/191] test: Check the decisive Lean row of the backend fidelity gate. --- clang/test/Verify/suite/backend_fidelity_gate.test | 2 ++ 1 file changed, 2 insertions(+) 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 From 4e72ac8a906ae05c1c2cbe508e4e42e7ec5db3c4 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:51 +0530 Subject: [PATCH 156/191] docs: Lower a documentation example without its Lean output options. --- website/scripts/check-doc-examples.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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, From 7ae367156d72f4efc4f98e020617aef84d8a4397 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:51 +0530 Subject: [PATCH 157/191] docs: Explain counterexamples in Lean in chapter 17. --- .../part-ii/ch17-backends-modular-calls.rst | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) 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 84198725c5..b007f3583c 100644 --- a/website/source/book/part-ii/ch17-backends-modular-calls.rst +++ b/website/source/book/part-ii/ch17-backends-modular-calls.rst @@ -279,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. @@ -319,6 +320,67 @@ 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 integers by trying each one. +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 ~~~~~~~~~~~~~~~~~~~ From 823ae1f0b3293adbc991bcf1f137638fb82f9ad7 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:52 +0530 Subject: [PATCH 158/191] docs: Say that a fallback project refutes certified counterexamples. --- website/source/language/tooling.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/source/language/tooling.rst b/website/source/language/tooling.rst index bc8a7b150f..e26a16a7d9 100644 --- a/website/source/language/tooling.rst +++ b/website/source/language/tooling.rst @@ -347,6 +347,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 From 88cd63aded47f0fa455d45f594f42a903fa6a4a5 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:52 +0530 Subject: [PATCH 159/191] docs: Note counterexamples confirmed in Lean in the release notes. --- website/source/release-notes.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/website/source/release-notes.rst b/website/source/release-notes.rst index b116cdb75a..c615fa3d1e 100644 --- a/website/source/release-notes.rst +++ b/website/source/release-notes.rst @@ -444,6 +444,19 @@ 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 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 From a957a73f4ec820766abc705b436fb8ded70dd9e0 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:52 +0530 Subject: [PATCH 160/191] docs: Describe Lean refutations in the design. --- docs/DESIGN.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 712f4e18a6..cc5524ed61 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -2028,6 +2028,18 @@ 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 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 From 90189a8f19f232accd6ac59da12aeecfc43abb38 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 23 Oct 2026 21:49:53 +0530 Subject: [PATCH 161/191] docs: Describe Lean refutations and the gate's Lean row in the architecture. --- docs/ARCHITECTURE.md | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 44d83331bf..5002ca77de 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1772,6 +1772,43 @@ replay both drive it. 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 integers by trying each one (`cppIntBallDecidable`, + `cppIntBexDecidable`, through core's `Nat` ones). `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`, @@ -2091,8 +2128,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 @@ -2519,7 +2557,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 From b41eba8cfa6f8ed603088bd46b54925edd7ec5d7 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:28 +0530 Subject: [PATCH 162/191] verify: Mark a function that uses a construct the verifier does not represent. --- clang/lib/Verify/IR/VStmt.h | 4 ++++ 1 file changed, 4 insertions(+) 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; From 8966deb569bde36f01050766e4a2a58ec9333c90 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:28 +0530 Subject: [PATCH 163/191] verify: Copy the unsupported-construct mark with a function. --- clang/lib/Verify/IR/VStmt.cpp | 1 + 1 file changed, 1 insertion(+) 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; From d2c2979642bb0a29743c7fee98c2fb00402bf9ed Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:28 +0530 Subject: [PATCH 164/191] verify: Keep the unsupported-construct mark through loop unrolling. --- clang/lib/Verify/Transform/LoopUnroll.cpp | 1 + 1 file changed, 1 insertion(+) 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; From 7a445598e5f36ea85b4bd04e1b5a2fcfc7182559 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:29 +0530 Subject: [PATCH 165/191] verify: Report a malformed inductive predicate with the predicate. --- clang/lib/Verify/Transform/Inductive.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 From 219e43588ba20ab97f08a7580a9b4497f16e139d Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:29 +0530 Subject: [PATCH 166/191] verify: Name the predicate of each inductive predicate error. --- clang/lib/Verify/Transform/Inductive.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 " From 036a3762894e1171d8b67192b92790a96def71f0 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:29 +0530 Subject: [PATCH 167/191] verify: Record the function and part of it each conversion error belongs to. --- clang/lib/Verify/Frontend/ASTConverter.h | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) 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. From a027243f186b666203a0735d5a5a925f47192ee6 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:30 +0530 Subject: [PATCH 168/191] verify: Keep what others may rely on of a function the verifier cannot represent. --- clang/lib/Verify/Frontend/ASTConverter.cpp | 152 +++++++++++++++------ 1 file changed, 108 insertions(+), 44 deletions(-) diff --git a/clang/lib/Verify/Frontend/ASTConverter.cpp b/clang/lib/Verify/Frontend/ASTConverter.cpp index 6b2d271bd9..e04ac734fa 100644 --- a/clang/lib/Verify/Frontend/ASTConverter.cpp +++ b/clang/lib/Verify/Frontend/ASTConverter.cpp @@ -1903,6 +1903,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,16 +1932,19 @@ 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)) - 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()) { + if (isa(FD) || FD->isTemplated()) { + llvm::SaveAndRestore Owner(ErrorFunction, functionIdentity(FD)); + llvm::SaveAndRestore OwnerName(ErrorFunctionName, FD->getNameAsString()); + llvm::SaveAndRestore InContract(ErrorInContract, true); + if (isa(FD)) { + if (functionContract(FD) || hasContractStatements(FD, Ctx)) + 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->isInStdNamespace() && Ctx.getSourceManager().isWrittenInMainFile(FD->getLocation()) && (functionContract(FD) || hasContractStatements(FD, Ctx))) @@ -1930,12 +1955,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 +2016,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 +2079,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 +2173,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 +2241,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 +2263,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 +2283,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 +2349,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 +2605,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 +3003,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 +3039,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 +3068,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 +3216,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 +3266,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; } From 5de19af5037441c0c13bd7b1728cc7f9453da631 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:30 +0530 Subject: [PATCH 169/191] verify: Verify every function but those using a construct the verifier does not represent. --- clang/lib/Verify/Driver/Verifier.cpp | 65 +++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/clang/lib/Verify/Driver/Verifier.cpp b/clang/lib/Verify/Driver/Verifier.cpp index 6854f53d33..d9f0ad5f33 100644 --- a/clang/lib/Verify/Driver/Verifier.cpp +++ b/clang/lib/Verify/Driver/Verifier.cpp @@ -3081,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) { @@ -3213,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 @@ -3562,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 " @@ -4921,6 +4964,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 " @@ -4955,9 +5000,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)); @@ -4974,7 +5022,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"); From f96eed0228fc4d10c9ced22d46b2733d6ec63cb7 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:30 +0530 Subject: [PATCH 170/191] test: Check the verdict that an unsupported construct's record carries. --- clang/test/Verify/located_errors.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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"} From 80eb0e4efa61a6b8ab7e7898f8edf489cc7ec02c Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:31 +0530 Subject: [PATCH 171/191] test: Add the program of the per-function errors test. --- .../suite/Inputs/per_function_errors.cpp | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 clang/test/Verify/suite/Inputs/per_function_errors.cpp diff --git a/clang/test/Verify/suite/Inputs/per_function_errors.cpp b/clang/test/Verify/suite/Inputs/per_function_errors.cpp new file mode 100644 index 0000000000..97a1f30df2 --- /dev/null +++ b/clang/test/Verify/suite/Inputs/per_function_errors.cpp @@ -0,0 +1,108 @@ +// Functions the verifier cannot represent, beside ones it can, for +// per_function_errors.test. + +int table[4] = {1, 2, 3, 4}; + +// A statement the verifier does not represent: pick is not verified, and its +// caller relies on its contract. +int pick(int x) + cppverify::post(cppverify::result >= 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; +} From 10dba84ad4ee556d5a69368ef77d993f31015b43 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:31 +0530 Subject: [PATCH 172/191] test: Check that an unsupported construct costs its function alone. --- .../Verify/suite/per_function_errors.test | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 clang/test/Verify/suite/per_function_errors.test 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 From a24894dbb035dee5f4d7192c46ec1d94dae4ded8 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:32 +0530 Subject: [PATCH 173/191] docs: Say that an unsupported construct costs its function alone. --- website/source/language/tooling.rst | 48 +++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/website/source/language/tooling.rst b/website/source/language/tooling.rst index e26a16a7d9..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 ~~~~~~~~~~~~~~~~~~~~~~~~~~ From 94c24f4873c9809e325df1c23bb96c46746dd44a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:32 +0530 Subject: [PATCH 174/191] docs: Describe failing closed per function in the limitations. --- website/source/language/limitations.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/website/source/language/limitations.rst b/website/source/language/limitations.rst index 383e32a315..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 ------------------- From 6898ea4fb859e820b835e3eb72110ffb08cf5d2a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:32 +0530 Subject: [PATCH 175/191] docs: Extend construct.unsupported to whole functions in chapter 16. --- .../book/part-ii/ch16-when-verification-fails.rst | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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`). From e176567787a303d52e22794cba06c36a1717af95 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:33 +0530 Subject: [PATCH 176/191] docs: Note that an unsupported construct costs its function alone in the release notes. --- website/source/release-notes.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/website/source/release-notes.rst b/website/source/release-notes.rst index c615fa3d1e..c79f4204f3 100644 --- a/website/source/release-notes.rst +++ b/website/source/release-notes.rst @@ -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`). From 74951722cf9de65bb1815fa6f39d932b98f26f08 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:33 +0530 Subject: [PATCH 177/191] docs: Describe per-function unsupported constructs in the design. --- docs/DESIGN.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index cc5524ed61..3ebdc07ef9 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). From faa6c01fdf72250a48eb46c0896a0017b322a56a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:33 +0530 Subject: [PATCH 178/191] docs: Describe per-function conversion errors in the architecture. --- docs/ARCHITECTURE.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5002ca77de..1389282dcf 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2015,7 +2015,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). From 17b3c6892812bd31aa013ec69dc24aa8b3a87956 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 08:26:59 +0530 Subject: [PATCH 179/191] verify: Decide a quantifier over a range of integers in Lean only for at most 4096 integers. --- clang/lib/Verify/Backend/LeanBackend.cpp | 39 +++++++++++++++--------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/clang/lib/Verify/Backend/LeanBackend.cpp b/clang/lib/Verify/Backend/LeanBackend.cpp index 5ea0ce0786..e58cd24d6c 100644 --- a/clang/lib/Verify/Backend/LeanBackend.cpp +++ b/clang/lib/Verify/Backend/LeanBackend.cpp @@ -1296,27 +1296,36 @@ static std::string leanSpecDefinition(const ObligationModule &Module, // 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. +// 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) := - 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⟩) + 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) := - 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]) + 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"; From 0776e1d156ab91ff079eba0cf6f25d8d04277517 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 08:27:00 +0530 Subject: [PATCH 180/191] test: Add a refutation over a thousand integers to the budget input. --- .../Verify/suite/Inputs/lean_refutation_budget.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/clang/test/Verify/suite/Inputs/lean_refutation_budget.cpp b/clang/test/Verify/suite/Inputs/lean_refutation_budget.cpp index 1d75a803eb..6b38456856 100644 --- a/clang/test/Verify/suite/Inputs/lean_refutation_budget.cpp +++ b/clang/test/Verify/suite/Inputs/lean_refutation_budget.cpp @@ -1,9 +1,16 @@ // Refutations that are bounded, and refutations that are not reported, for // lean_refutation.test. -// Lean would decide this one by trying each of a billion integers: each -// attempt of the refutation stops at the heartbeats of a step of -// cppverify_auto, and the counterexample is not decided there. +// 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)) { From 9f63ff77dd13e988d394a5d71d765b0293be6229 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 08:27:00 +0530 Subject: [PATCH 181/191] test: Check that a quantifier over a thousand integers is refuted in Lean. --- clang/test/Verify/suite/lean_refutation.test | 1 + 1 file changed, 1 insertion(+) diff --git a/clang/test/Verify/suite/lean_refutation.test b/clang/test/Verify/suite/lean_refutation.test index 55ddb8a3a4..9d98556ddd 100644 --- a/clang/test/Verify/suite/lean_refutation.test +++ b/clang/test/Verify/suite/lean_refutation.test @@ -82,6 +82,7 @@ # 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 From d5d21db3a10ddfa00a2888bbe4c0c7d5f766acbc Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 08:27:01 +0530 Subject: [PATCH 182/191] test: Check that refuting a quantifier over a billion integers stays below 4 GB. --- clang/test/Verify/suite/lean_refutation_memory.test | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 clang/test/Verify/suite/lean_refutation_memory.test 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 From 824cbf92498e576130988a9d99c10cb6ff043ff1 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 08:27:02 +0530 Subject: [PATCH 183/191] docs: Describe the range bound of the Lean prelude's quantifiers in the architecture. --- docs/ARCHITECTURE.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5002ca77de..894d010973 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1796,8 +1796,16 @@ replay both drive it. 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 integers by trying each one (`cppIntBallDecidable`, - `cppIntBexDecidable`, through core's `Nat` ones). `writeLeanRefutations` + 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 From b8fec4f77f679501eaeccebcc6352ce77c23238f Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 08:27:02 +0530 Subject: [PATCH 184/191] docs: Bound the Lean prelude's quantifiers to 4096 integers in the design. --- docs/DESIGN.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index cc5524ed61..d2aeb8b810 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -2032,7 +2032,8 @@ 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 integers by trying each one, each +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 From 30fae896d274c53c3d84619cc38bc6f533b683b6 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 08:27:03 +0530 Subject: [PATCH 185/191] docs: Bound the Lean prelude's quantifiers to 4096 integers in chapter 17. --- website/source/book/part-ii/ch17-backends-modular-calls.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 b007f3583c..04fb031093 100644 --- a/website/source/book/part-ii/ch17-backends-modular-calls.rst +++ b/website/source/book/part-ii/ch17-backends-modular-calls.rst @@ -365,7 +365,9 @@ of the spec functions Lean defines: ``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 integers by trying each one. +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 From c26a4edd5f75d61de2f7a5e1bfb429d071d1576c Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 08:27:03 +0530 Subject: [PATCH 186/191] docs: Bound the Lean prelude's quantifiers to 4096 integers in the release notes. --- website/source/release-notes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/release-notes.rst b/website/source/release-notes.rst index c615fa3d1e..2b6a54f7a3 100644 --- a/website/source/release-notes.rst +++ b/website/source/release-notes.rst @@ -452,8 +452,8 @@ 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 integers by trying each one, within the heartbeats of a step -of ``cppverify_auto``. The release gate's Lean row now +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`). From 51cde57b4a375ddf50382a0bb03a14ccbc7fa5d5 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 09:40:29 +0530 Subject: [PATCH 187/191] verify: Test the result of Lean certification once, so a failure is a verdict and not an abort. --- clang/lib/Verify/Driver/Verifier.cpp | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/clang/lib/Verify/Driver/Verifier.cpp b/clang/lib/Verify/Driver/Verifier.cpp index 6854f53d33..2833bf10a0 100644 --- a/clang/lib/Verify/Driver/Verifier.cpp +++ b/clang/lib/Verify/Driver/Verifier.cpp @@ -4721,18 +4721,22 @@ class Verifier { Diagnostic.Result->Message = Reason; } }; - auto Certified = - Lean->certifyLeanProject(ExportedGoals, LeanRefutations); + // 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); - std::string NotCertified; if (!Certified) { - const std::string Reason = llvm::toString(Certified.takeError()); - NotCertified = Reason; 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]; @@ -5806,10 +5810,13 @@ verify::replayLeanProject(const std::vector &Modules, // 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. - auto Certified = Project.certifyLeanProject(Goals); + // As for source certification: the Expected is tested once. + std::optional> Certified; std::string Failed; - if (!Certified) - Failed = llvm::toString(Certified.takeError()); + 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); From a3fa00e39a9e74db6efe7d4c903226724d8d4c2b Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 09:40:30 +0530 Subject: [PATCH 188/191] test: Check that replaying an archive reports a failed Lean certification. --- clang/test/Verify/suite/lean_toolchain.test | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/clang/test/Verify/suite/lean_toolchain.test b/clang/test/Verify/suite/lean_toolchain.test index 03b229fd70..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,11 +14,18 @@ # 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{{$}} From 76aa3b920d1204f8b5839f6743f65c80c9d5a001 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 11:16:54 +0530 Subject: [PATCH 189/191] verify: Mangle a constructor or destructor as its complete-object variant. --- clang/lib/Verify/Frontend/ASTConverter.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/clang/lib/Verify/Frontend/ASTConverter.cpp b/clang/lib/Verify/Frontend/ASTConverter.cpp index e04ac734fa..68221e7583 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(); From 6e1b3f89df41ee3695d2920794de7ba41924d0a6 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 13:02:00 +0530 Subject: [PATCH 190/191] verify: Name a template or member function only when it has contracts to report, so no header's is mangled. --- clang/lib/Verify/Frontend/ASTConverter.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/clang/lib/Verify/Frontend/ASTConverter.cpp b/clang/lib/Verify/Frontend/ASTConverter.cpp index 68221e7583..de8609aa69 100644 --- a/clang/lib/Verify/Frontend/ASTConverter.cpp +++ b/clang/lib/Verify/Frontend/ASTConverter.cpp @@ -1939,21 +1939,21 @@ std::vector> ASTConverter::convertTranslationUnit() { UsedConstexprSpecs.clear(); for (const FunctionDecl *FD : Definitions) { 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)) { - if (functionContract(FD) || hasContractStatements(FD, Ctx)) - error(FD->getLocation(), - FD->getNameAsString() + - ": member function verification is unsupported"); + 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->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"); From 7412ac91a3f5a400648aa93faccec40dad43f385 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 13:02:00 +0530 Subject: [PATCH 191/191] test: Check that a run under the Microsoft ABI mangles no template. --- clang/test/Verify/microsoft_identity.cpp | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 clang/test/Verify/microsoft_identity.cpp 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