Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2781da8
verify: Add a budget of the expression nodes a construction builds on…
SwayamInSync Oct 20, 2026
076091d
verify: Implement the node budget and the per-thread node limit.
SwayamInSync Oct 20, 2026
bc8b9f9
verify: Build the node budget.
SwayamInSync Oct 20, 2026
179c9a8
verify: Count every VCR expression node against the node budget.
SwayamInSync Oct 20, 2026
98d8337
verify: Count every logic expression node against the node budget.
SwayamInSync Oct 20, 2026
ae781d3
verify: Declare the size error and the node limit of building a module.
SwayamInSync Oct 20, 2026
192cf75
verify: Refuse obligations over the node limit before building their …
SwayamInSync Oct 20, 2026
ea09fe3
verify: Give materializing spec definitions the module's node limit.
SwayamInSync Oct 20, 2026
78880d7
verify: Count spec definitions against the module's node limit as the…
SwayamInSync Oct 20, 2026
3fa7607
verify: Record on a passive program that a construction passed the no…
SwayamInSync Oct 20, 2026
06a9dd6
verify: Bound the spec facts a function's passivization adds by the n…
SwayamInSync Oct 20, 2026
f4bb7d0
verify: Stop unfolding specs once the node budget is spent.
SwayamInSync Oct 20, 2026
6ac0888
verify: Add the solver.memory-limit reason and the solvers' memory li…
SwayamInSync Oct 20, 2026
a9e8613
verify: Name solver.memory-limit and share the module size check.
SwayamInSync Oct 20, 2026
76ead33
verify: Track whether a Z3 encoder stopped for memory.
SwayamInSync Oct 20, 2026
a918feb
verify: Stop a Z3 check past the memory limit and report it.
SwayamInSync Oct 20, 2026
0c97cff
verify: Keep the memory limit in the cvc5 backend.
SwayamInSync Oct 20, 2026
4f7a0c8
verify: Limit each cvc5 process's memory and report when it runs out.
SwayamInSync Oct 20, 2026
5f1aa13
verify: Default the node and memory limits.
SwayamInSync Oct 20, 2026
d5157d4
verify: Report a function too large to build as unresolved with query…
SwayamInSync Oct 20, 2026
a89df38
verify: Add --memory-limit, a solver option, and accept --max-query-n…
SwayamInSync Oct 21, 2026
178690a
docs: Describe the memory limits in the tooling reference, with check…
SwayamInSync Oct 20, 2026
d1d6725
docs: List --memory-limit and the new meaning of --max-query-nodes in…
SwayamInSync Oct 20, 2026
44e0faa
docs: Explain solver.memory-limit and query.size-limit in chapter 16.
SwayamInSync Oct 20, 2026
4f2eba0
docs: Note the node limit of a function's obligations in the limitati…
SwayamInSync Oct 20, 2026
8f3fd97
docs: Add the memory limits to the 0.2.0 release notes.
SwayamInSync Oct 20, 2026
e1ad346
docs: Describe the node and memory limits in DESIGN.
SwayamInSync Oct 20, 2026
5e803d7
docs: Describe how the node and memory limits are enforced in ARCHITE…
SwayamInSync Oct 20, 2026
35115b8
docs: Let a page expect a function refused for its size to fail lower…
SwayamInSync Oct 20, 2026
35cb2e9
test: Check that obligations over the node limit are refused at their…
SwayamInSync Oct 20, 2026
9716be4
test: Generate a function of many checks for the memory tests.
SwayamInSync Oct 20, 2026
7a70fbe
test: Check that large obligations and deep fuel end within a 3 GB ad…
SwayamInSync Oct 20, 2026
c9a59c2
test: Check that a Z3 query past --memory-limit stops with solver.mem…
SwayamInSync Oct 20, 2026
9800226
test: Check that a cvc5 process past --memory-limit reports solver.me…
SwayamInSync Oct 20, 2026
c101243
test: Expect the node limit's new messages in the backend execution t…
SwayamInSync Oct 20, 2026
69da72a
test: Expect --memory-limit, not --max-query-nodes, to be rejected wi…
SwayamInSync Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 50 additions & 17 deletions clang/lib/Verify/Backend/CVC5Backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#else
#include <cerrno>
#include <csignal>
#include <sys/resource.h>
#include <sys/wait.h>
#endif

Expand Down Expand Up @@ -1505,15 +1506,9 @@ class SMTLibEncoder {
static VerifyResult querySizeLimitResult(const ObligationModule &Module,
uint64_t MaxQueryNodes,
llvm::StringRef BackendName) {
VerifyResult Result;
if (MaxQueryNodes == 0 || obligationModuleNodeCount(Module) <= MaxQueryNodes)
return Result;
Result.Status = VerifyStatus::Unresolved;
Result.Reason = VerifyReason::QuerySizeLimit;
Result.Message = "canonical obligation module exceeds query node budget " +
std::to_string(MaxQueryNodes);
Result.BackendName = BackendName.str();
return Result;
if (auto Limited = moduleSizeLimit(Module, MaxQueryNodes, BackendName))
return std::move(*Limited);
return VerifyResult();
}

static std::optional<std::string>
Expand Down Expand Up @@ -2404,7 +2399,8 @@ CVC5VerifyBackend::CVC5VerifyBackend(const BackendExecutionOptions &Execution)
: TimeoutMs(Execution.SolverTimeoutMs),
CollectionTimeoutMs(Execution.CollectionTimeoutMs),
CertifyTimeoutMs(Execution.CertifyTimeoutMs),
ResourceLimit(Execution.SolverResourceLimit), Jobs(Execution.Jobs),
ResourceLimit(Execution.SolverResourceLimit),
MemoryLimitMB(Execution.MemoryLimitMB), Jobs(Execution.Jobs),
Pool(Execution.Pool), MaxQueryNodes(Execution.MaxQueryNodes),
IntegerEncoding(Execution.IntegerEncoding) {
llvm::StringRef Requested = Execution.CVC5Path.empty()
Expand All @@ -2431,6 +2427,7 @@ struct SolverRun {

static SolverRun runCVC5(const std::string &SolverPath, llvm::StringRef Script,
unsigned TimeoutMs, unsigned ResourceLimit,
uint64_t MemoryLimitMB = 0,
const std::atomic<bool> *Cancelled = nullptr);

/// Whether the cvc5 at \p SolverPath prints a sequence value last element
Expand Down Expand Up @@ -2473,6 +2470,7 @@ static bool printsSequencesReversed(const std::string &SolverPath) {

static SolverRun runCVC5(const std::string &SolverPath, llvm::StringRef Script,
unsigned TimeoutMs, unsigned ResourceLimit,
uint64_t MemoryLimitMB,
const std::atomic<bool> *Cancelled) {
SolverRun Run;
VerifyResult &Result = Run.Failure;
Expand Down Expand Up @@ -2530,9 +2528,27 @@ static SolverRun runCVC5(const std::string &SolverPath, llvm::StringRef Script,
ErrorPath.str()};
std::string InvocationError;
bool ExecutionFailed = false;
// On Windows the limit bounds the process's committed memory. On POSIX
// hosts a limit makes ExecuteNoWait fork rather than spawn, and fork runs
// Z3's fork handlers, which wait for the timers of every check running in
// the process; the limit is set on the spawned process instead.
#if defined(_WIN32)
const unsigned SpawnLimit = static_cast<unsigned>(
std::min<uint64_t>(MemoryLimitMB, std::numeric_limits<unsigned>::max()));
#else
const unsigned SpawnLimit = 0;
#endif
llvm::sys::ProcessInfo Process =
llvm::sys::ExecuteNoWait(SolverPath, Args, std::nullopt, Redirects, 0,
&InvocationError, &ExecutionFailed);
llvm::sys::ExecuteNoWait(SolverPath, Args, std::nullopt, Redirects,
SpawnLimit, &InvocationError, &ExecutionFailed);
#if defined(__linux__)
// What cvc5 allocates: its data segment and private writable mappings.
if (MemoryLimitMB != 0 && Process.Pid != llvm::sys::ProcessInfo::InvalidPid) {
struct rlimit Data;
Data.rlim_cur = Data.rlim_max = static_cast<rlim_t>(MemoryLimitMB) << 20;
(void)::prlimit(Process.Pid, RLIMIT_DATA, &Data, nullptr);
}
#endif
if (ExecutionFailed || Process.Pid == llvm::sys::ProcessInfo::InvalidPid) {
Result.Reason = VerifyReason::SolverInvocationFailure;
Result.Message = InvocationError.empty() ? "cannot start cvc5 process"
Expand Down Expand Up @@ -2615,16 +2631,33 @@ static SolverRun runCVC5(const std::string &SolverPath, llvm::StringRef Script,
return Run;
}
if (ExitCode != 0) {
std::string OutputError;
std::optional<std::string> StandardOutput =
readSolverOutput(OutputPath, OutputError, MaxSolverOutputBytes);
// An allocation past the limit fails, and cvc5 reports it as an error
// ((error "std::bad_alloc")) or aborts on it (std::bad_alloc, or its SAT
// solver's Minisat::OutOfMemoryException).
auto outOfMemory = [](llvm::StringRef Text) {
return Text.contains("bad_alloc") || Text.contains("OutOfMemory");
};
if (outOfMemory(*StandardError) ||
(StandardOutput && outOfMemory(*StandardOutput))) {
Result.Reason = VerifyReason::SolverMemoryLimit;
Result.Message = "cvc5 ran out of memory";
if (MemoryLimitMB != 0)
Result.Message += ": each cvc5 process may use " +
std::to_string(MemoryLimitMB) +
" MB (--memory-limit)";
return Run;
}
Result.Reason = VerifyReason::SolverInvocationFailure;
Result.Message = "cvc5 exited with status " + std::to_string(ExitCode);
if (!InvocationError.empty())
Result.Message += ": " + InvocationError;
if (!StandardError->empty())
Result.Message += ": " + diagnosticText(*StandardError);
// cvc5 reports a script it rejects on standard output.
std::string OutputError;
if (std::optional<std::string> StandardOutput =
readSolverOutput(OutputPath, OutputError, MaxSolverOutputBytes))
if (StandardOutput)
for (llvm::StringRef Rest = *StandardOutput; !Rest.empty();) {
auto [Line, Next] = Rest.split('\n');
if (Line.trim().starts_with("(error")) {
Expand Down Expand Up @@ -2802,8 +2835,8 @@ CVC5VerifyBackend::verifyQuery(const ObligationModule &Module,
}
Budget = static_cast<unsigned>(Remaining);
}
SolverRun Run =
runCVC5(SolverPath, *Script, Budget, ResourceLimit, &Cancelled);
SolverRun Run = runCVC5(SolverPath, *Script, Budget, ResourceLimit,
MemoryLimitMB, &Cancelled);
auto giveUp = [&](llvm::StringRef Outcome) {
RefinementDecision Exhausted = Refinement.exhausted(Outcome);
Result.Reason = Exhausted.Reason;
Expand Down
1 change: 1 addition & 0 deletions clang/lib/Verify/Backend/CVC5Backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class CVC5VerifyBackend : public VerifyBackend {
/// Set by cancel(): the running process is stopped and no other starts.
std::atomic<bool> Cancelled{false};
unsigned ResourceLimit;
uint64_t MemoryLimitMB;
unsigned Jobs;
llvm::StdThreadPool *Pool;
std::optional<std::chrono::steady_clock::time_point> Deadline;
Expand Down
63 changes: 59 additions & 4 deletions clang/lib/Verify/Backend/Obligation.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
//===--- Obligation.cpp ---------------------------------------------------===//
#include "LogicTerms.h"
#include "ObligationLowering.h"
#include "ObligationSimplify.h"
#include "SpecAxioms.h"
#include "llvm/ADT/APInt.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/MathExtras.h"
#include <algorithm>
#include <tuple>

Expand Down Expand Up @@ -2159,7 +2161,8 @@ class ObligationBuilder {
return std::move(Result);
}

llvm::Expected<ObligationModule> buildPassive(const PassiveProgram &P) {
llvm::Expected<ObligationModule> buildPassive(const PassiveProgram &P,
uint64_t MaxNodes) {
ObligationModule M;
M.FunctionName = P.FunctionName;
M.FunctionIdentity = P.FunctionIdentity;
Expand Down Expand Up @@ -2272,6 +2275,37 @@ class ObligationBuilder {
return llvm::createStringError(llvm::inconvertibleErrorCode(), "%s",
ConstructionError.c_str());

// The goals' size is known before any is built: each is its condition
// under a negated copy of every assumption before it (a Not and an Or
// each), each query is its goal negated, and the complete goal joins the
// goals by And, with its query.
uint64_t Prefix = 0, Goals = 0, GoalCount = 0;
auto assumed = [&](const VCExpr *Assumption) {
Prefix =
llvm::SaturatingAdd(Prefix, logicNodeCount(Assumption), uint64_t(2));
};
auto goal = [&](const VCExpr *Condition) {
Goals = llvm::SaturatingAdd(Goals, Prefix, logicNodeCount(Condition));
++GoalCount;
};
for (const auto &Entry : EntryAssumes)
assumed(Entry.get());
for (const LoweredStmt &Stmt : Stmts) {
if (Stmt.Kind == PassiveStmt::Assume)
assumed(Stmt.Cond.get());
else
goal(Stmt.Cond.get());
}
for (const auto &Exit : ExitAsserts)
goal(std::get<1>(Exit).get());
const uint64_t GoalNodes =
GoalCount == 0
? 3
: llvm::SaturatingAdd(llvm::SaturatingMultiply(Goals, uint64_t(4)),
3 * GoalCount - 1);
if (MaxNodes != 0 && GoalNodes > MaxNodes)
return ObligationSizeError::obligations(GoalNodes, MaxNodes);

std::vector<const VCExpr *> Assumptions;
for (const auto &Entry : EntryAssumes)
Assumptions.push_back(Entry.get());
Expand Down Expand Up @@ -2324,10 +2358,13 @@ class ObligationBuilder {

M.CorrectnessGoal = buildCompleteGoal(M.Obligations);
M.CounterexampleQuery = vcNot(cloneVCExpr(M.CorrectnessGoal.get()));
assert(GoalNodes == obligationModuleNodeCount(M) &&
"the goals' size was not predicted exactly");

SpecAxiomContext AxiomContext{P.SpecFunctions, P.SpecFuel, P.HiddenSpecs,
P.RevealedSpecs};
if (llvm::Error Error = materializeLogicFunctions(M, AxiomContext))
if (llvm::Error Error =
materializeLogicFunctions(M, AxiomContext, MaxNodes, GoalNodes))
return std::move(Error);

auto RequiredFeatures = validateObligationModule(M);
Expand Down Expand Up @@ -2588,14 +2625,32 @@ verify::validateObligationModule(const ObligationModule &M) {
return RequiredFeatures;
}

char ObligationSizeError::ID = 0;

llvm::Error ObligationSizeError::obligations(uint64_t Nodes, uint64_t Limit) {
return llvm::make_error<ObligationSizeError>(
"the proof obligations", Nodes, Limit,
"each check repeats every assumption before it, so split the function "
"or raise the limit");
}

void ObligationSizeError::log(llvm::raw_ostream &OS) const {
OS << What << " would take at least " << Nodes
<< " expression nodes, more than the limit of " << Limit
<< " (--max-query-nodes): " << Advice;
}

llvm::Expected<ObligationModule>
verify::buildObligationModule(const PassiveProgram &P) {
verify::buildObligationModule(const PassiveProgram &P, uint64_t MaxNodes) {
ObligationBuilder B(P.ResultVarName,
P.OldHeapName.empty() ? std::string(VHeapName) + "_0"
: P.OldHeapName,
P.CallerIntMode, P.HeapVariables,
/*ForceCallerMode=*/false, &P.SpecFunctions);
auto Module = B.buildPassive(P);
if (const auto &Over = P.TooLarge)
return llvm::make_error<ObligationSizeError>(Over->What, Over->Nodes,
Over->Limit, Over->Advice);
auto Module = B.buildPassive(P, MaxNodes);
if (Module) {
Module->AssumedPosts = P.AssumedPosts;
Module->AssumedUnfoldings = P.AssumedUnfoldings;
Expand Down
3 changes: 2 additions & 1 deletion clang/lib/Verify/Backend/Obligation.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#ifndef LLVM_CLANG_VERIFY_BACKEND_OBLIGATION_H
#define LLVM_CLANG_VERIFY_BACKEND_OBLIGATION_H

#include "../IR/NodeBudget.h"
#include "clang/Basic/SourceLocation.h"
#include "llvm/Support/Error.h"
#include <cstdint>
Expand Down Expand Up @@ -217,7 +218,7 @@ class LogicExpr {
/// archives.
std::vector<std::unique_ptr<LogicExpr>> Patterns;

explicit LogicExpr(Kind K) : K(K) {}
explicit LogicExpr(Kind K) : K(K) { NodeBudget::count(); }
};

// Compatibility name for the Z3/spec adapters while they migrate internally.
Expand Down
31 changes: 30 additions & 1 deletion clang/lib/Verify/Backend/ObligationLowering.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,37 @@
namespace clang {
namespace verify {

/// Something a function's verification builds would have more expression
/// nodes than the limit allows (--max-query-nodes), found before or as it
/// was built: the obligations, each repeating every assumption before it
/// (their size is known before any is built), or a spec's definition
/// unfolded as deep as its fuel.
class ObligationSizeError : public llvm::ErrorInfo<ObligationSizeError> {
public:
static char ID;
ObligationSizeError(std::string What, uint64_t Nodes, uint64_t Limit,
std::string Advice)
: What(std::move(What)), Nodes(Nodes), Limit(Limit),
Advice(std::move(Advice)) {}
/// The obligations of a module of \p Nodes nodes.
static llvm::Error obligations(uint64_t Nodes, uint64_t Limit);
void log(llvm::raw_ostream &OS) const override;
std::error_code convertToErrorCode() const override {
return llvm::inconvertibleErrorCode();
}
std::string What;
/// At least this many: a construction stops once it passes the limit.
uint64_t Nodes;
uint64_t Limit;
std::string Advice;
};

/// With \p MaxNodes, a module that would have more nodes than that (as
/// obligationModuleNodeCount counts them) is an ObligationSizeError, before
/// its goals are built. So is a program one of whose constructions passed
/// its limit (PassiveProgram::TooLarge).
llvm::Expected<ObligationModule>
buildObligationModule(const PassiveProgram &Program);
buildObligationModule(const PassiveProgram &Program, uint64_t MaxNodes = 0);

ObligationKind obligationKind(ProofObligationKind Kind);

Expand Down
38 changes: 36 additions & 2 deletions clang/lib/Verify/Backend/SpecAxioms.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
//===--- SpecAxioms.cpp - Verus-style spec defining axioms ----------------===//
#include "SpecAxioms.h"
#include "../Transform/SpecInline.h"
#include "LogicTerms.h"
#include "ObligationLowering.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/MathExtras.h"

using namespace clang;
using namespace verify;
Expand Down Expand Up @@ -114,7 +116,9 @@ static const VFunction *findSpecFunction(const FunctionMap &Functions,
}

llvm::Error verify::materializeLogicFunctions(ObligationModule &Module,
const SpecAxiomContext &Ctx) {
const SpecAxiomContext &Ctx,
uint64_t MaxNodes,
uint64_t Nodes) {
// The functions the query reaches; then, for a counterexample check only,
// those their unfoldings and postconditions apply, where a failure leaves
// the function out instead of failing the module.
Expand Down Expand Up @@ -153,6 +157,20 @@ llvm::Error verify::materializeLogicFunctions(ObligationModule &Module,
Module.LogicFunctions.try_emplace(Identity, std::move(Declaration));
(void)Inserted;
LogicFunctionDecl &Owned = It->second;
// Its definitions join the module's nodes once all are lowered.
uint64_t Own = 0;
auto count = [&](const LogicExpr *Definition) -> llvm::Error {
Own = llvm::SaturatingAdd(Own, logicNodeCount(Definition));
const uint64_t Total = llvm::SaturatingAdd(Nodes, Own);
if (MaxNodes != 0 && Total > MaxNodes)
return llvm::make_error<ObligationSizeError>(
"the proof obligations with the definitions of the specs they "
"apply",
Total, MaxNodes,
"lower the fuel cppverify::reveal_with_fuel gives, split the "
"function, or raise the limit");
return llvm::Error::success();
};
const std::string DefinitionHeap = Spec->ReadsHeap
? std::string(VSpecHeapName)
: std::string(VHeapName) + "_0";
Expand Down Expand Up @@ -223,13 +241,26 @@ llvm::Error verify::materializeLogicFunctions(ObligationModule &Module,
return llvm::createStringError(
llvm::inconvertibleErrorCode(),
"spec step definition result sort mismatch: %s", Spec->Name.c_str());
if (llvm::Error Error = count(Owned.StepDefinition.get()))
return Error;
collectReferencedLogicFunctions(Owned.StepDefinition.get(), Pending);

const unsigned MaxDepth =
Spec->NeedsDecreasesCheck ? Fuel : std::min(Fuel, 1U);
Owned.DefinitionFuel = MaxDepth;
for (unsigned Depth = 1; Depth <= MaxDepth; ++Depth) {
std::unique_ptr<VExpr> Body = unfoldSpecDefinition(*Spec, Ctx, Depth);
std::unique_ptr<VExpr> Body;
{
NodeBudget Unfolding;
Body = unfoldSpecDefinition(*Spec, Ctx, Depth);
if (Unfolding.exceeded())
return llvm::make_error<ObligationSizeError>(
"the definition of " + Spec->Name + " unfolded " +
std::to_string(Depth) + " levels deep",
Unfolding.built(), Unfolding.limit(),
"lower its fuel (cppverify::reveal_with_fuel), or raise the "
"limit");
}
if (!Body)
continue;
auto Lowered = lowerLogicExpr(Body.get(), "", DefinitionHeap,
Expand All @@ -245,6 +276,8 @@ llvm::Error verify::materializeLogicFunctions(ObligationModule &Module,
return llvm::createStringError(
llvm::inconvertibleErrorCode(),
"spec definition result sort mismatch: %s", Spec->Name.c_str());
if (llvm::Error Error = count(Definition.get()))
return Error;
collectReferencedLogicFunctions(Definition.get(), Pending);
Owned.DefinitionLevels.push_back(std::move(Definition));
}
Expand All @@ -262,6 +295,7 @@ llvm::Error verify::materializeLogicFunctions(ObligationModule &Module,
Owned.Decreases.push_back(std::move(*Lowered));
}
}
Nodes = llvm::SaturatingAdd(Nodes, Own);
return llvm::Error::success();
};
std::set<std::string> Pending;
Expand Down
Loading
Loading