From b41eba8cfa6f8ed603088bd46b54925edd7ec5d7 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 06:52:28 +0530 Subject: [PATCH 01/94] 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 02/94] 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 03/94] 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 04/94] 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 05/94] 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 06/94] 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 07/94] 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 08/94] 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 09/94] 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 10/94] 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 11/94] 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 12/94] 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 13/94] 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 14/94] 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 15/94] 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 16/94] 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 17/94] 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 e6bf536ced124d7a6f31127d639fd9e0776bbbd4 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 07:59:45 +0530 Subject: [PATCH 18/94] verify: Keep only scalar reference parameters as footprints of one cell. --- clang/lib/Verify/IR/VStmt.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/clang/lib/Verify/IR/VStmt.h b/clang/lib/Verify/IR/VStmt.h index 6db19ef883..816f9f6910 100644 --- a/clang/lib/Verify/IR/VStmt.h +++ b/clang/lib/Verify/IR/VStmt.h @@ -419,7 +419,10 @@ struct VFunction { std::set RevealedSpecs; std::vector> Params; std::map SourceVariables; - std::set ReferenceParams; + /// Reference parameters to a scalar, whose `modifies(r)` is the one cell + /// they bind (perhaps a field of a larger object). A reference to a record + /// is a pointer to it, and `modifies(r)` its object. + std::set ScalarReferenceParams; /// Pointer and reference parameters to const objects: the function /// writes nothing through them. std::set ConstAddressParams; From 69c1fcc4cfe5b2b2b312281ca19fac417f2d9e93 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 07:59:45 +0530 Subject: [PATCH 19/94] verify: Clone a function's scalar reference parameters. --- clang/lib/Verify/IR/VStmt.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clang/lib/Verify/IR/VStmt.cpp b/clang/lib/Verify/IR/VStmt.cpp index 687197e9ff..88f5970470 100644 --- a/clang/lib/Verify/IR/VStmt.cpp +++ b/clang/lib/Verify/IR/VStmt.cpp @@ -197,7 +197,7 @@ VFunction verify::cloneVFunction(const VFunction &Fn) { Out.RevealedSpecs = Fn.RevealedSpecs; Out.Params = Fn.Params; Out.SourceVariables = Fn.SourceVariables; - Out.ReferenceParams = Fn.ReferenceParams; + Out.ScalarReferenceParams = Fn.ScalarReferenceParams; Out.ConstAddressParams = Fn.ConstAddressParams; Out.BoundedPointers = Fn.BoundedPointers; Out.ReturnFields = Fn.ReturnFields; From b504ed449ba7638ba400510997215ccea6200709 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 07:59:46 +0530 Subject: [PATCH 20/94] verify: Keep a function's scalar reference parameters through loop unrolling. --- clang/lib/Verify/Transform/LoopUnroll.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clang/lib/Verify/Transform/LoopUnroll.cpp b/clang/lib/Verify/Transform/LoopUnroll.cpp index 1f6bd7d3a3..80c2dc5254 100644 --- a/clang/lib/Verify/Transform/LoopUnroll.cpp +++ b/clang/lib/Verify/Transform/LoopUnroll.cpp @@ -255,7 +255,7 @@ VFunction LoopUnroller::unroll(const VFunction &Fn, unsigned K) { Out.FreshOwnedReturn = Fn.FreshOwnedReturn; Out.Params = Fn.Params; Out.SourceVariables = Fn.SourceVariables; - Out.ReferenceParams = Fn.ReferenceParams; + Out.ScalarReferenceParams = Fn.ScalarReferenceParams; Out.ConstAddressParams = Fn.ConstAddressParams; Out.BoundedPointers = Fn.BoundedPointers; Out.ReturnFields = Fn.ReturnFields; From 49ecde6d45f0ca89ebc9d4a6b7a773caa8dec9d5 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 07:59:46 +0530 Subject: [PATCH 21/94] verify: Frame a record reference's footprint as its object, and name the address passed to a callee. --- clang/lib/Verify/Transform/Passivize.cpp | 30 +++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/clang/lib/Verify/Transform/Passivize.cpp b/clang/lib/Verify/Transform/Passivize.cpp index b416922e30..e41db3316c 100644 --- a/clang/lib/Verify/Transform/Passivize.cpp +++ b/clang/lib/Verify/Transform/Passivize.cpp @@ -1680,8 +1680,9 @@ samePointerDifferenceOrigin(const VExpr *L, const VExpr *R, /// Whether a footprint is the whole object its pointer addresses, rather /// than one cell or a range of cells. -static bool isRegionFootprint(const VFootprint &F, - const std::set &ReferenceParams) { +static bool +isRegionFootprint(const VFootprint &F, + const std::set &ScalarReferenceParams) { if (F.Count) return false; const VExpr *E = F.Target.get(); @@ -1691,7 +1692,7 @@ static bool isRegionFootprint(const VFootprint &F, while (Ptr && Ptr->K == VExpr::Cast) Ptr = static_cast(Ptr)->Inner.get(); if (Ptr && Ptr->K == VExpr::Var && - ReferenceParams.count(static_cast(Ptr)->Name)) + ScalarReferenceParams.count(static_cast(Ptr)->Name)) return false; return !Ptr || Ptr->K == VExpr::Var; } @@ -1700,7 +1701,7 @@ static bool hasUnboundedExtentWrite(const VFunction &Fn, const std::string &Base) { for (const VFootprint &Modify : Fn.Modifies) { if (!Modify.Target || Modify.Target->K != VExpr::Load || - !isRegionFootprint(Modify, Fn.ReferenceParams)) + !isRegionFootprint(Modify, Fn.ScalarReferenceParams)) continue; const VExpr *Ptr = pointerBase( static_cast(Modify.Target.get())->Ptr.get()); @@ -3548,7 +3549,7 @@ class PassivizerImpl { std::vector Out; for (const VFootprint &F : W.Modifies) { const auto *Load = static_cast(F.Target.get()); - if (isRegionFootprint(F, Fn.ReferenceParams)) { + if (isRegionFootprint(F, Fn.ScalarReferenceParams)) { auto R = rootRegion(addressRoot(Load->Ptr.get()), Renames, Load->Loc); if (!R) return std::nullopt; @@ -3648,7 +3649,7 @@ class PassivizerImpl { Out.push_back(rangeBytes(Ptr.get(), Count.get(), M.ElementSize, Loc)); continue; } - if (isRegionFootprint(M, Fn.ReferenceParams)) { + if (isRegionFootprint(M, Fn.ScalarReferenceParams)) { auto R = rootRegion(addressRoot(Load->Ptr.get()), Renames, Loc); if (!R) return false; @@ -3715,7 +3716,7 @@ class PassivizerImpl { TargetPtr = static_cast(TargetPtr)->Inner.get(); const bool OneCell = !M.Count && TargetPtr == CalleeRoot && - (!isRegionFootprint(M, Callee->ReferenceParams) || + (!isRegionFootprint(M, Callee->ScalarReferenceParams) || (Callee->ObjectModel && (Target->Ty.isInt() || Target->Ty.Kind == VTypeKind::Bool || Target->Ty.Kind == VTypeKind::Ptr) && @@ -4904,7 +4905,7 @@ class PassivizerImpl { const VFootprint &Declared = Callee.Modifies[I]; if (Count) return rangeBytes(Actual->Ptr.get(), Count, Declared.ElementSize, Loc); - if (!isRegionFootprint(Declared, Callee.ReferenceParams)) + if (!isRegionFootprint(Declared, Callee.ScalarReferenceParams)) return cellRegion(Actual->Ptr.get(), Actual->Ty, Loc); if (!Callee.ObjectModel) return std::nullopt; @@ -4949,7 +4950,8 @@ class PassivizerImpl { for (size_t I = 0; I < ActualModifies.size(); ++I) if (!ActualModifies[I] || ActualModifies[I]->K != VExpr::Load || ActualCounts[I] || - (isRegionFootprint(Callee.Modifies[I], Callee.ReferenceParams) && + (isRegionFootprint(Callee.Modifies[I], + Callee.ScalarReferenceParams) && !hasPointerProvenance( static_cast(ActualModifies[I].get()) ->Ptr.get()))) @@ -5055,7 +5057,7 @@ class PassivizerImpl { ActiveScans)) { emitPassive(P, PassiveStmt::Assert, makeBoolLiteral(false, C.Loc), nullptr, C.Loc, ProofObligationKind::Unsupported, - "a pointer to local or dynamic storage is passed to " + + "the address of local or dynamic storage is passed to " + C.Callee + ", which may keep, offset, or free it, or has no " "verified body"); @@ -5344,8 +5346,8 @@ class PassivizerImpl { ? static_cast(M.get()) : nullptr; if (ActualLoad) { - const bool ActualIsRegion = - isRegionFootprint(Callee->Modifies[I], Callee->ReferenceParams); + const bool ActualIsRegion = isRegionFootprint( + Callee->Modifies[I], Callee->ScalarReferenceParams); if (auto Provenance = pointerProvenance(ActualLoad->Ptr.get())) for (const std::string &Identity : OwnedAllocationIdentities) Allowed = makeOr(std::move(Allowed), @@ -5375,7 +5377,7 @@ class PassivizerImpl { OuterBytes = rangeBytes(CallerLoad->Ptr.get(), CallerCounts[J].get(), Fn.Modifies[J].ElementSize, C.Loc); const bool OuterIsRegion = - isRegionFootprint(Fn.Modifies[J], Fn.ReferenceParams); + isRegionFootprint(Fn.Modifies[J], Fn.ScalarReferenceParams); Allowed = makeOr( std::move(Allowed), footprintWithin(CallerLoad->Ptr.get(), OuterIsRegion, OuterBytes, @@ -5624,7 +5626,7 @@ class PassivizerImpl { St.Loc); } auto EffectivePtr = resolveReferenceAddress(Ptr.get()); - const bool Region = isRegionFootprint(M, Fn.ReferenceParams); + const bool Region = isRegionFootprint(M, Fn.ScalarReferenceParams); Allowed = makeOr( std::move(Allowed), footprintWithin(DeclaredPtr.get(), Region, Bytes, From 7825fcb13b5af802dc217ff94a11d8aaf1929d82 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 07:59:47 +0530 Subject: [PATCH 22/94] verify: Declare the places and pointers reached through references to records. --- clang/lib/Verify/Frontend/ASTConverter.h | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/clang/lib/Verify/Frontend/ASTConverter.h b/clang/lib/Verify/Frontend/ASTConverter.h index 152d0d48c4..e1eb288972 100644 --- a/clang/lib/Verify/Frontend/ASTConverter.h +++ b/clang/lib/Verify/Frontend/ASTConverter.h @@ -150,7 +150,8 @@ class ASTConverter { const Expr *Cond); bool isTriggerMarked(const Expr *E) const; void markTrigger(std::unique_ptr &Result, const Expr *E); - /// A modifies footprint: an lvalue, or the cells p[lo : n]. + /// A modifies footprint: an lvalue, the cells p[lo : n], or the bytes of a + /// record or array inside an object. std::optional convertFootprint(const Expr *E); /// A loop's declared footprints, each naming memory. std::vector convertLoopFootprints(const LoopContractInfo &LCI); @@ -188,6 +189,16 @@ class ASTConverter { /// object. Returns the root only when it was promoted to one automatic /// byte-addressed object. const VarDecl *promotedRootLocal(const Expr *E) const; + /// The reference parameter or local whose record \p E selects a field or + /// element of (or is), if any. + const VarDecl *recordReferenceRoot(const Expr *E) const; + /// Whether \p E is a place in an object reached through a pointer or a + /// reference, rather than a value of the function's own. + bool inPointeeObject(const Expr *E) const; + /// Whether a pointer may be formed into the place \p E (its address, or a + /// decayed member array): a place in an object that outlives the function, + /// reached through a pointer or a reference to a record. + bool subobjectPointerRoot(const Expr *E) const; /// The pointer whose pointee a chain of field and fixed-array element /// selections starts at (p in *p, p->m, p[k], (*p).a.b[i]); null when the /// chain does not start at a pointee. From 959018e1a0cb31b993c86fc101a904fe910bb64a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 07:59:48 +0530 Subject: [PATCH 23/94] verify: Verify records through references, as pointers never null or reseated, and members named whole in modifies as their bytes. --- clang/lib/Verify/Frontend/ASTConverter.cpp | 97 ++++++++++++++++++---- 1 file changed, 79 insertions(+), 18 deletions(-) diff --git a/clang/lib/Verify/Frontend/ASTConverter.cpp b/clang/lib/Verify/Frontend/ASTConverter.cpp index e04ac734fa..795f625d8f 100644 --- a/clang/lib/Verify/Frontend/ASTConverter.cpp +++ b/clang/lib/Verify/Frontend/ASTConverter.cpp @@ -609,6 +609,16 @@ static bool isSupportedScalarLValueReference(QualType Ty) { Referent->isEnumeralType(); } +/// A reference to a record is a pointer that is never null and never +/// reseated, so it is supported where a pointer to the record is (not to a +/// volatile one): its fields and elements are places in the object it names. +static bool isSupportedRecordLValueReference(QualType Ty, ASTContext &Ctx) { + return !Ty.isNull() && Ty->isLValueReferenceType() && + Ty.getNonReferenceType()->isRecordType() && + isSupportedVerificationType( + Ctx.getPointerType(Ty.getNonReferenceType())); +} + static std::optional findUnsupportedType(const FunctionDecl *FD, bool AllowScalarReferences) { // By-value records keep their flattened SSA representation, so only records @@ -624,7 +634,9 @@ findUnsupportedType(const FunctionDecl *FD, bool AllowScalarReferences) { for (const ParmVarDecl *Param : FD->parameters()) { QualType Ty = Param->getType(); if (Ty->isReferenceType()) { - if (!AllowScalarReferences || !isSupportedScalarLValueReference(Ty)) + if (!AllowScalarReferences || + !(isSupportedScalarLValueReference(Ty) || + isSupportedRecordLValueReference(Ty, FD->getASTContext()))) return Ty.getAsString(); } else if (UnsupportedValueType(Ty)) { return Param->getType().getAsString(); @@ -653,7 +665,8 @@ findUnsupportedType(const FunctionDecl *FD, bool AllowScalarReferences) { return false; QualType Ty = D->getType(); if (Ty->isReferenceType() && AllowScalarReferences && - isSupportedScalarLValueReference(Ty) && + (isSupportedScalarLValueReference(Ty) || + isSupportedRecordLValueReference(Ty, D->getASTContext())) && (isa(D) || D->hasLocalStorage())) return true; if (!isSupportedVerificationType(Ty)) @@ -1003,11 +1016,12 @@ static bool exprReferencesSpecCall(const VExpr *E, const std::string &Name) { } static const RecordDecl *getRecordFromType(QualType T) { - // See through references: a `C&`/`const C&` parameter's field access lowers - // to the same "param.field" variable as a by-value `C`, so type_invariant - // injection applies. Pointers are intentionally excluded: `p->field` lowers - // to a heap Load, which the "param.field" substitution does not model. - T = T.getNonReferenceType().getUnqualifiedType(); + // A record by value, whose fields are values of its own ("param.field"). + // A reference to a record is an address, as a pointer is: its fields are + // places in the object it names. + if (T->isReferenceType()) + return nullptr; + T = T.getUnqualifiedType(); // A cppverify collection is one mathematical value, not a record of fields. if (VType::collectionKind(T)) return nullptr; @@ -1170,10 +1184,10 @@ ASTConverter::convertPointerDifferenceOperand(const Expr *E, const Expr *Peeled = E->IgnoreParens(); if (const auto *Decay = dyn_cast(Peeled); Decay && Decay->getCastKind() == CK_ArrayToPointerDecay && - pointeeRootPointer(Decay->getSubExpr())) + subobjectPointerRoot(Decay->getSubExpr())) return convertExpr(Peeled); if (const auto *U = dyn_cast(Peeled); - U && U->getOpcode() == UO_AddrOf && pointeeRootPointer(U->getSubExpr())) + U && U->getOpcode() == UO_AddrOf && subobjectPointerRoot(U->getSubExpr())) return convertExpr(Peeled); E = E->IgnoreParenImpCasts(); if (const auto *DRE = dyn_cast(E)) @@ -2716,8 +2730,8 @@ ASTConverter::convertFunction(const FunctionDecl *FD) { SmallVector AddressParams; for (const ParmVarDecl *P : FD->parameters()) { - if (P->getType()->isReferenceType()) - Fn->ReferenceParams.insert(valueName(P)); + if (isSupportedScalarLValueReference(P->getType())) + Fn->ScalarReferenceParams.insert(valueName(P)); if ((P->getType()->isPointerType() || P->getType()->isReferenceType()) && !isMutableAddressParam(P)) Fn->ConstAddressParams.insert(valueName(P)); @@ -3141,6 +3155,24 @@ std::optional ASTConverter::convertFootprint(const Expr *E) { if (!Out.Count) Out.Target.reset(); } + } else if (const Expr *Inner = E->IgnoreParens(); + (isa(Inner) || isa(Inner)) && + (Inner->getType()->isRecordType() || + Inner->getType()->isConstantArrayType()) && + inPointeeObject(Inner)) { + // A record or array inside an object (p->inner, r.buf, p[k]) is its + // bytes, a range of one element of its size; *p and a reference to a + // record name their whole object. + if (auto Place = objectPlace(Inner)) { + Out.Target = std::make_unique( + Place->takeAddress(), + VType::fromQualType(Inner->getType(), IntMode, Ctx), + Inner->getExprLoc(), "", Place->takeAccessCondition()); + Out.Count = std::make_unique( + 1, VType::fromQualType(Ctx.IntTy, IntMode, Ctx), Inner->getExprLoc()); + Out.ElementSize = static_cast( + Ctx.getTypeSizeInChars(Inner->getType()).getQuantity()); + } } else { Out.Target = convertExpr(E); } @@ -3382,7 +3414,7 @@ ASTConverter::subscriptPlace(const ArraySubscriptExpr *AS) { return std::nullopt; // An element of a fixed array inside a represented object: a promoted // local, or the object a pointer addresses (r->buf[i], p[k].grid[i][j]). - if (promotedRootLocal(AS) || pointeeRootPointer(AS->getBase())) + if (promotedRootLocal(AS) || inPointeeObject(AS->getBase())) return objectPlace(AS); return pointerElementPlace(AS); } @@ -3498,6 +3530,28 @@ const VarDecl *ASTConverter::promotedRootLocal(const Expr *E) const { return VD && AddressableLocals.count(VD) ? VD : nullptr; } +const VarDecl *ASTConverter::recordReferenceRoot(const Expr *E) const { + const VarDecl *VD = selectionRootVar(E); + return VD && (isa(VD) || VD->hasLocalStorage()) && + VD->getType()->isLValueReferenceType() && + VD->getType().getNonReferenceType()->isRecordType() + ? VD + : nullptr; +} + +bool ASTConverter::inPointeeObject(const Expr *E) const { + return pointeeRootPointer(E) || recordReferenceRoot(E); +} + +bool ASTConverter::subobjectPointerRoot(const Expr *E) const { + if (pointeeRootPointer(E)) + return true; + // A local reference bound to local or dynamic storage names an object + // whose lifetime a pointer formed from it would not carry. + const VarDecl *VD = recordReferenceRoot(E); + return VD && !LocalReferenceProvenanceVariables.count(VD); +} + std::string ASTConverter::arrayDecayError(const Expr *Array) const { const auto *M = dyn_cast_or_null(peelObjectExpr(Array)); const std::string Member = @@ -3662,6 +3716,9 @@ std::optional ASTConverter::objectPlace(const Expr *E) { if (const auto *DRE = dyn_cast(E)) { const auto *VD = dyn_cast(DRE->getDecl()); + // The object a reference to a record names, as *p for a pointer. + if (VD && recordReferenceRoot(E)) + return lvaluePlace(E); if (!VD || !AddressableLocals.count(VD)) return std::nullopt; return automaticLocalPlace(VD, E->getExprLoc()); @@ -3947,6 +4004,9 @@ std::optional ASTConverter::lvaluePlace(const Expr *E) { // rooted in it is an exact byte offset from its single automatic object. if (promotedRootLocal(E)) return objectPlace(E); + // A subobject of the record a reference names: c.f, c.buf[i]. + if (recordReferenceRoot(E) && !isa(E)) + return objectPlace(E); if (const auto *DRE = dyn_cast(E)) { const auto *VD = dyn_cast(DRE->getDecl()); @@ -4713,7 +4773,7 @@ std::unique_ptr ASTConverter::convertExprImpl(const Expr *E) { return convertExpr(MTE->getSubExpr()); if (const auto *Decay = dyn_cast(E); Decay && Decay->getCastKind() == CK_ArrayToPointerDecay) { - if (pointeeRootPointer(Decay->getSubExpr())) + if (subobjectPointerRoot(Decay->getSubExpr())) return subobjectPointer(E, Decay->getSubExpr(), /*AddressOf=*/false); error(CurrentFn->Name + ": " + arrayDecayError(Decay->getSubExpr())); return nullptr; @@ -4893,7 +4953,7 @@ std::unique_ptr ASTConverter::convertExprImpl(const Expr *E) { } if (const auto *U = dyn_cast(E)) { if (U->getOpcode() == UO_AddrOf) { - if (pointeeRootPointer(U->getSubExpr())) + if (subobjectPointerRoot(U->getSubExpr())) return subobjectPointer(E, U->getSubExpr(), /*AddressOf=*/true); error(CurrentFn->Name + ": raw address-of is unsupported until lexical lifetime and escape " @@ -5192,7 +5252,7 @@ std::unique_ptr ASTConverter::convertExprImpl(const Expr *E) { return std::make_unique(std::move(Address), Ty, E->getExprLoc()); } - if (!M->isArrow() && (promotedRootLocal(M) || pointeeRootPointer(M))) { + if (!M->isArrow() && (promotedRootLocal(M) || inPointeeObject(M))) { VType Ty = VType::fromQualType(E->getType(), IntMode, Ctx); if (Ty.isAggregate() || Ty.Kind == VTypeKind::Unsupported) { error(CurrentFn->Name + @@ -5200,7 +5260,8 @@ std::unique_ptr ASTConverter::convertExprImpl(const Expr *E) { ? ": aggregate value use of an automatic object " "subobject is unsupported" : ": a record or array read as a whole through a " - "pointer is unsupported; read its fields")); + "pointer or reference is unsupported; read its " + "fields")); return nullptr; } auto Place = objectPlace(M); @@ -5815,7 +5876,7 @@ void ASTConverter::appendAssignment(const Expr *LHS, return; } // A field inside the object a pointer addresses: p->inner.x, p[k].f. - if (!ME->isArrow() && pointeeRootPointer(ME)) { + if (!ME->isArrow() && inPointeeObject(ME)) { appendPointeeStore(LHS, std::move(Value), Loc, Out); return; } @@ -5869,7 +5930,7 @@ void ASTConverter::appendAssignment(const Expr *LHS, ": proof functions cannot modify executable memory"); return; } - if (pointeeRootPointer(AS->getBase())) { + if (inPointeeObject(AS->getBase())) { appendPointeeStore(LHS, std::move(Value), Loc, Out); return; } From 6b05b739408591b54ce426a148f65f1af08cd757 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 07:59:48 +0530 Subject: [PATCH 24/94] test: Check records through references and members named whole in modifies. --- clang/test/Verify/suite/record_references.cpp | 420 ++++++++++++++++++ 1 file changed, 420 insertions(+) create mode 100644 clang/test/Verify/suite/record_references.cpp diff --git a/clang/test/Verify/suite/record_references.cpp b/clang/test/Verify/suite/record_references.cpp new file mode 100644 index 0000000000..87e778efcd --- /dev/null +++ b/clang/test/Verify/suite/record_references.cpp @@ -0,0 +1,420 @@ +// RUN: not %cpp-verify --jobs=1 %s -- 2>&1 | FileCheck %s +// RUN: %cpp-verify --lower-only --dump-ir=1 %s -- -DLOWER 2>&1 \ +// RUN: | FileCheck %s --check-prefix=VCR +// RUN: not %cpp-verify --jobs=1 --backend=bmc --unroll=1 %s -- 2>&1 \ +// RUN: | FileCheck %s --check-prefix=BMC +// +// A reference to a record is a pointer to it that is never null and never +// reseated: its fields and elements are places in the object it names, as +// p->f is for a pointer, and modifies(c) is that object, as modifies(*p) is. + +struct Counter { + int n; + int limit; +}; + +struct Pair { + Counter left; + Counter right; +}; + +struct Ring { + int buf[4]; + int head; +}; + +void reset(Counter &c) + cppverify::modifies(c) + cppverify::post(c.n == 0 && c.limit == 10) +{ + c.n = 0; + c.limit = 10; +} +// CHECK-DAG: Verified: reset [backend=z3] +// BMC-DAG: Verified: reset [backend=bmc, bound=0] +// VCR-LABEL: fn reset +// VCR: modifies +// VCR-NEXT: load +// VCR-NEXT: c +// VCR-NEXT: body + +// A record inside an object, through a reference or a pointer, is its bytes. +void left_only(Pair &p) + cppverify::modifies(p.left) + cppverify::post(p.left.n == 0 && p.right.n == cppverify::old(p.right.n)) +{ + reset(p.left); +} +// CHECK-DAG: Verified: left_only [backend=z3] +// VCR-LABEL: fn left_only +// VCR: modifies +// VCR-NEXT: load +// VCR-NEXT: + +// VCR-NEXT: p +// VCR-NEXT: 0 +// VCR-NEXT: range 8 +// VCR-NEXT: 1 +// VCR-NEXT: body + +void right_only(Pair *p) + cppverify::pre(p != nullptr) + cppverify::modifies(p->right) + cppverify::post(p->right.limit == 10 && p->left.n == cppverify::old(p->left.n)) +{ + reset(p->right); +} +// CHECK-DAG: Verified: right_only [backend=z3] + +#ifndef LOWER +void bump(Counter &c) + cppverify::pre(c.n < c.limit) + cppverify::modifies(c.n) + cppverify::post(c.n == cppverify::old(c.n) + 1) + cppverify::post(c.limit == cppverify::old(c.limit)) +{ + c.n = c.n + 1; +} +// CHECK-DAG: Verified: bump [backend=z3] +// BMC-DAG: Verified: bump [backend=bmc, bound=0] + +int read(const Counter &c) + cppverify::post(cppverify::result == c.n) +{ + return c.n; +} +// CHECK-DAG: Verified: read [backend=z3] + +// A reference passed on, from a reference, a pointer, and a field. +void twice(Counter &c) + cppverify::pre(c.n + 1 < c.limit) + cppverify::modifies(c.n) + cppverify::post(c.n == cppverify::old(c.n) + 2) +{ + bump(c); + bump(c); +} +// CHECK-DAG: Verified: twice [backend=z3] + +void via_pointer(Counter *p) + cppverify::pre(p != nullptr && p->n < p->limit) + cppverify::modifies(p->n) + cppverify::post(p->n == cppverify::old(p->n) + 1) +{ + bump(*p); +} +// CHECK-DAG: Verified: via_pointer [backend=z3] + +void via_field(Pair &p) + cppverify::pre(p.left.n < p.left.limit) + cppverify::modifies(p.left.n) + cppverify::post(p.left.n == cppverify::old(p.left.n) + 1) + cppverify::post(p.right.n == cppverify::old(p.right.n)) +{ + bump(p.left); +} +// CHECK-DAG: Verified: via_field [backend=z3] + +// Two mutable references name distinct objects unless aliases says so. +void frame(Counter &a, Counter &b) + cppverify::modifies(a) + cppverify::post(a.n == 0 && b.n == cppverify::old(b.n)) +{ + reset(a); +} +// CHECK-DAG: Verified: frame [backend=z3] + +void self_frame(Counter &c) + cppverify::modifies(c) +{ + frame(c, c); +} +// CHECK-DAG: error: verification failed: self_frame [{{.*}}::aliasing@[[@LINE-2]]:3] (counterexample: c [ssa=c_0] [type=pointer] = + +void may_alias(Counter &a, Counter &b) + cppverify::aliases(a, b) + cppverify::modifies(a.n) + cppverify::post(b.n == 5) +{ + a.n = 5; +} +// CHECK-DAG: error: verification failed: may_alias [{{.*}}::postcondition@[[@LINE-4]]:23] + +int sum(const Counter &a, const Counter &b) + cppverify::pre(-1000 < a.n && a.n < 1000 && -1000 < b.n && b.n < 1000) + cppverify::post(cppverify::result == a.n + b.n) +{ + return a.n + b.n; +} + +int sum_same(Counter &c) + cppverify::pre(-1000 < c.n && c.n < 1000) + cppverify::post(cppverify::result == 2 * c.n) +{ + return sum(c, c); +} +// CHECK-DAG: Verified: sum_same [backend=z3] + +// Each store lies in a footprint, and a callee's footprints in the caller's. +void bad_frame(Counter &c) + cppverify::modifies(c.n) +{ + c.limit = 3; +} +// CHECK-DAG: error: verification failed: bad_frame [{{.*}}::frame@[[@LINE-2]]:11] + +void narrow_caller(Counter &c) + cppverify::modifies(c.n) +{ + reset(c); +} +// CHECK-DAG: error: verification failed: narrow_caller [{{.*}}::frame@[[@LINE-2]]:3] + +void narrow_member(Pair &p) + cppverify::modifies(p.left.n) +{ + reset(p.left); +} +// CHECK-DAG: error: verification failed: narrow_member [{{.*}}::frame@[[@LINE-2]]:3] + +// A callee's whole record is every byte of it, which only a footprint naming +// the record, or an object around it, contains; its fields one by one do not. +void fields_one_by_one(Pair &p) + cppverify::modifies(p.left.n, p.left.limit) +{ + reset(p.left); +} +// CHECK-DAG: error: verification failed: fields_one_by_one [{{.*}}::frame@[[@LINE-2]]:3] + +// Array members: indices bounded by their array, and pointers into them +// bounded by it. +void put(Ring &r, int v) + cppverify::pre(0 <= r.head && r.head < 4) + cppverify::modifies(r.buf[r.head]) + cppverify::post(r.buf[r.head] == v && r.head == cppverify::old(r.head)) +{ + r.buf[r.head] = v; +} +// CHECK-DAG: Verified: put [backend=z3] + +void put_at(Ring &r, int i, int v) + cppverify::pre(0 <= i && i <= 4) + cppverify::modifies(r.buf[0 : 4]) +{ + r.buf[i] = v; +} +// CHECK-DAG: error: verification failed: put_at [{{.*}}::bounds@[[@LINE-2]]:12] + +void set_first(int *q, int v) + cppverify::pre(cppverify::valid(q, 4)) + cppverify::modifies(q[0]) + cppverify::post(q[0] == v) +{ + q[0] = v; +} + +void set_through_decay(Ring &r) + cppverify::modifies(r.buf[0]) + cppverify::post(r.buf[0] == 7 && r.head == cppverify::old(r.head)) +{ + set_first(r.buf, 7); +} +// CHECK-DAG: Verified: set_through_decay [backend=z3] + +int head_through_pointer(const Ring &r) + cppverify::post(cppverify::result == r.head) +{ + const int *h = &r.head; + return *h; +} +// CHECK-DAG: Verified: head_through_pointer [backend=z3] + +int read_object(const Counter *p) + cppverify::pre(p != nullptr) + cppverify::post(cppverify::result == p->n) +{ + return p->n; +} + +int through_address(const Counter &c) + cppverify::post(cppverify::result == c.n) +{ + return read_object(&c); +} +// CHECK-DAG: Verified: through_address [backend=z3] + +int past_head(const Ring &r) +{ + const int *h = &r.head; + cppverify::check(h[1] == 0); + return 0; +} +// CHECK-DAG: error: verification failed: past_head [{{.*}}::bounds@[[@LINE-3]]:3] + +// Local references to records bind a place as a parameter does. +void local_deref(Counter *p) + cppverify::pre(p != nullptr) + cppverify::modifies(*p) + cppverify::post(p->n == 0) +{ + Counter &r = *p; + r.n = 0; +} +// CHECK-DAG: Verified: local_deref [backend=z3] + +void local_member(Pair *p) + cppverify::pre(p != nullptr && p->left.n < p->left.limit) + cppverify::modifies(p->left.n) + cppverify::post(p->left.n == cppverify::old(p->left.n) + 1) +{ + Counter &r = p->left; + Counter &s = r; + bump(s); +} +// CHECK-DAG: Verified: local_member [backend=z3] + +void local_wrong_member(Pair *p) + cppverify::pre(p != nullptr) + cppverify::modifies(p->left) +{ + Counter &r = p->right; + r.n = 0; +} +// CHECK-DAG: error: verification failed: local_wrong_member [{{.*}}::frame@[[@LINE-2]]:7] + +void local_elements(Counter *a, int n) + cppverify::pre(cppverify::valid(a, n) && n >= 0) + cppverify::modifies(a[0 : n]) + cppverify::post(cppverify::forall(k, 0, n, a[k].n == 0)) +{ + for (int i = 0; i < n; i = i + 1) + cppverify::invariant(0 <= i && i <= n) + cppverify::invariant(cppverify::forall(k, 0, i, a[k].n == 0)) + cppverify::decreases(n - i) + { + Counter &r = a[i]; + r.n = 0; + } +} +// CHECK-DAG: Verified: local_elements [backend=z3] + +void local_past(Counter *a, int i) + cppverify::pre(cppverify::valid(a, 4) && 0 <= i && i <= 4) + cppverify::modifies(a[0 : 4]) +{ + Counter &r = a[i]; + r.n = 3; +} +// CHECK-DAG: error: verification failed: local_past [{{.*}}::dereference@[[@LINE-3]]:3] (counterexample: a [ssa=a_0] [type=pointer] = {{[0-9]+}}, i [ssa=i_0] [type=i32] = 4) + +// A local record bound to a reference is one object in memory. +int local_object() + cppverify::post(cppverify::result == 1) +{ + Counter c; + c.n = 0; + Counter &r = c; + r.n = 1; + return c.n; +} +// CHECK-DAG: Verified: local_object [backend=z3] + +int local_object_uninitialized() + cppverify::post(cppverify::result == 0) +{ + Counter c; + c.n = 0; + Counter &r = c; + return r.limit; +} +// CHECK-DAG: error: verification failed: local_object_uninitialized [{{.*}}::initialization@[[@LINE-2]]:3] + +// A type invariant is a fact about records by value; one a reference names is +// an object whose fields the function reads from memory. +struct Bounded { + int v; + cppverify::type_invariant(v >= 0); +}; + +int by_value(Bounded b) + cppverify::post(cppverify::result >= 0) +{ + return b.v; +} +// CHECK-DAG: Verified: by_value [backend=z3] + +int by_reference(const Bounded &b) + cppverify::post(cppverify::result >= 0) +{ + return b.v; +} +// CHECK-DAG: error: verification failed: by_reference [{{.*}}::postcondition@[[@LINE-4]]:37] + +// Not yet: a local passed to a callee by reference, whose non-escape is not +// established for records (B3a-4), a pointer formed from a local through a +// reference, a whole record read through one, a proof function's reference, +// and rvalue and volatile references. +int local_to_callee() + cppverify::post(cppverify::result == 1) +{ + Counter c; + c.n = 0; + c.limit = 5; + bump(c); + return c.n; +} +// CHECK-DAG: [[@LINE-3]]:3: Unresolved: local_to_callee [backend=z3] [reason=construct.unsupported] (the verifier cannot model a construct here: the address of local or dynamic storage is passed to bump, which may keep, offset, or free it, or has no verified body) + +int local_address() + cppverify::post(cppverify::result == 0) +{ + Ring local; + Ring &r = local; + int *q = &r.head; + return 0; +} +// CHECK-DAG: error: local_address: raw address-of is unsupported until lexical lifetime and escape effects are modeled + +int local_decay() + cppverify::post(cppverify::result == 0) +{ + Ring local; + local.buf[0] = 1; + Ring &r = local; + int *q = r.buf; + return 0; +} +// CHECK-DAG: error: local_decay: array-to-pointer decay of automatic storage is unsupported until lexical lifetime and escape effects are modeled + +int by_copy(Counter c) + cppverify::post(cppverify::result == c.n) +{ + return c.n; +} + +int whole_read(const Pair &p) + cppverify::post(cppverify::result == p.left.n) +{ + return by_copy(p.left); +} +// CHECK-DAG: error: whole_read: a record or array read as a whole through a pointer or reference is unsupported; read its fields + +cppverify::proof void proof_reference(Counter &c) + cppverify::post(true) +{ +} +// CHECK-DAG: error: proof_reference: unsupported C++ type in verification: Counter & + +int rvalue_reference(Counter &&c) + cppverify::post(cppverify::result == c.n) +{ + return c.n; +} +// CHECK-DAG: error: rvalue_reference: unsupported C++ type in verification: Counter && + +int volatile_reference(volatile Counter &c) + cppverify::post(cppverify::result == c.n) +{ + return c.n; +} +// CHECK-DAG: error: volatile_reference: unsupported C++ type in verification: volatile Counter & +#endif From 1f3fedf077e6d51911b49a34b4efc8f55e0c869a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 07:59:49 +0530 Subject: [PATCH 25/94] test: Stop expecting a reference to a record to be rejected. --- clang/test/Verify/suite/references_unsupported.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/clang/test/Verify/suite/references_unsupported.cpp b/clang/test/Verify/suite/references_unsupported.cpp index c72a17a1ef..412371b3e5 100644 --- a/clang/test/Verify/suite/references_unsupported.cpp +++ b/clang/test/Verify/suite/references_unsupported.cpp @@ -14,11 +14,6 @@ int read_value(const int &value) return value; } -struct Pair { - int first; - int second; -}; - struct WithReference { int &value; }; @@ -48,12 +43,6 @@ int &reference_return(int &value) cppverify::pre(true) { return value; } -int record_reference(Pair &value) - cppverify::post(cppverify::result == value.first) -{ - return value.first; -} - int rvalue_reference(int &&value) cppverify::post(cppverify::result == value) { @@ -140,7 +129,6 @@ void conditional_reference_actual(bool choose, int *value) // VERIFY-DAG: error: temporary_actual: reference arguments require a supported reference, addressable local, or direct pointer dereference // VERIFY-DAG: error: reference_return: unsupported C++ type in verification: int & -// VERIFY-DAG: error: record_reference: unsupported C++ type in verification: Pair & // VERIFY-DAG: error: rvalue_reference: unsupported C++ type in verification: int && // VERIFY-DAG: error: volatile_reference: unsupported C++ type in verification: volatile int & // VERIFY-DAG: error: pointer_reference: unsupported C++ type in verification: int *& From d906f9940ec075cc7c31406e2b4a91d2a4239c3e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 07:59:49 +0530 Subject: [PATCH 26/94] docs: Describe records through references, with a checked example. --- website/source/language/structs.rst | 107 ++++++++++++++++++++++++++-- 1 file changed, 103 insertions(+), 4 deletions(-) diff --git a/website/source/language/structs.rst b/website/source/language/structs.rst index 4b33605a57..6f82ceea6c 100644 --- a/website/source/language/structs.rst +++ b/website/source/language/structs.rst @@ -175,6 +175,103 @@ would be lost there: returning it, or storing it in memory, is an error. 10 | return &r->buf[i]; | ^~~~~~ +Records through references +-------------------------- + +A reference to a record (``Counter &``, ``const Counter &``) is a pointer to +it that is never null and never reseated, so everything above holds of it: +``c.n``, ``c.buf[i]``, and ``c.inner.x`` are cells of the object ``c`` names, +and ``&c.n`` and ``c.buf`` are pointers into it. The verifier requires the +object to be live and initialized at entry, as for a scalar reference, and +two references of which one is mutable name distinct objects unless +``cppverify::aliases`` allows them to name one. ``cppverify::modifies(c)`` is the +whole object, as ``cppverify::modifies(*p)`` is for a pointer, and a record +or array member named whole, ``cppverify::modifies(p.left)`` or +``cppverify::modifies(r.buf)``, is the bytes of that member. A reference +parameter may be bound to a reference, to ``*p``, to a field such as +``p->left``, or to an element ``a[i]``, and so may a local reference: + +.. cppverify-example: fails reset_count + +.. code-block:: cpp + + struct Counter { + int n; + int limit; + }; + + struct Pair { + Counter left; + Counter right; + }; + + void bump(Counter &c) + cv::pre(c.n < c.limit) + cv::modifies(c.n) + cv::post(c.n == cv::old(c.n) + 1) + { + c.n = c.n + 1; + } + + void reset(Counter &c) + cv::modifies(c) + cv::post(c.n == 0 && c.limit == 10) + { + c.n = 0; + c.limit = 10; + } + + void reset_left(Pair &p) + cv::modifies(p.left) + cv::post(p.left.n == 0 && p.right.n == cv::old(p.right.n)) + { + reset(p.left); + } + + void bump_left(Pair *p) + cv::pre(p != nullptr && p->left.n < p->left.limit) + cv::modifies(p->left.n) + cv::post(p->left.n == cv::old(p->left.n) + 1) + { + Counter &c = p->left; + bump(c); + } + + void reset_count(Pair &p) + cv::modifies(p.left.n) + { + reset(p.left); + } + +.. code-block:: text + + Verified: bump [backend=z3] + Verified: reset [backend=z3] + Verified: reset_left [backend=z3] + Verified: bump_left [backend=z3] + pair.cpp:46:3: error: verification failed: reset_count [...::frame@46:3] (counterexample: p [ssa=p_0] [type=pointer] = 1; trace: call.reset@46:3 arg0=1) [backend=z3] [reason=counterexample] + +``reset_left`` may write ``p.left``, which holds the whole object ``reset`` +may write, so ``p.right`` keeps its value. ``reset_count`` may write only +``p.left.n``, and ``reset`` writes ``p.left.limit`` too: the call fails its +``frame`` check. A caller that may write both fields one by one, +``cppverify::modifies(p.left.n, p.left.limit)``, still fails it: the callee's +footprint is every byte of the record, which only a footprint naming the +record, or an object around it, contains. + +Not yet supported: + +- a local record passed to a reference or pointer parameter (``bump(c)`` with + ``Counter c;``): the verifier does not yet show that the callee keeps no + pointer to it, and the call is ``Unresolved`` with reason + ``construct.unsupported``. A local reference may bind a local record, and + reads and writes through it are checked against that object's lifetime and + initialization, but forming a pointer through such a reference (``&r.n``, + ``r.buf``) is an error; +- reading a whole record through a reference, as through a pointer; +- reference returns, rvalue and ``volatile`` references, and references in + spec and proof functions. + ``cppverify::type_invariant`` ----------------------------- @@ -198,7 +295,8 @@ Semantics: - The invariant is injected as a **precondition** at the first use of an invariant field for by-value parameters. Because it is a precondition, callers must establish it when they pass such a value. -- It is **not** injected for raw pointer-to-struct parameters (their fields are heap loads). +- It is **not** injected for pointer-to-struct or reference-to-struct + parameters: their fields are heap loads of an object in memory. - **Returns are checked**: when a function returns a struct value of an invariant-bearing type, the verifier asserts the invariant holds for the returned value. Building and returning a struct whose fields violate the invariant is a verification error. @@ -232,8 +330,9 @@ returned by value must be trivial, standard-layout, and flat, with scalar fields. A local object whose address is needed (it holds a fixed array, or a reference binds to one of its members) is promoted to an automatic object and may also have nested record, pointer, and fixed-array members. A record -reached through a pointer may have the same members, and its scalar fields and -elements are reached one at a time (see `Records through pointers`_), or -through a pointer into it (see `Pointers into a record`_). Reference +reached through a pointer or a reference may have the same members, and its +scalar fields and elements are reached one at a time (see `Records through +pointers`_ and `Records through references`_), or through a pointer into it +(see `Pointers into a record`_). Reference members, unions, bases, virtual dispatch, and non-trivial construction/destruction are rejected rather than approximated. From a8b47d558bffb6573b247b92f01f3b879e3a807f Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 07:59:50 +0530 Subject: [PATCH 27/94] docs: Extend records and frames to references to records in chapter 14. --- .../part-ii/ch14-pointers-frames-modifies.rst | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/website/source/book/part-ii/ch14-pointers-frames-modifies.rst b/website/source/book/part-ii/ch14-pointers-frames-modifies.rst index c5cd6bbc02..e0c0cd1e5f 100644 --- a/website/source/book/part-ii/ch14-pointers-frames-modifies.rst +++ b/website/source/book/part-ii/ch14-pointers-frames-modifies.rst @@ -77,8 +77,10 @@ liveness, initialization, and a non-escaping lifetime identity. A local binding snapshots its address, so changing a source pointer later does not rebind the reference. -Conditional bindings, temporaries, reference returns, address-taking, rvalue -references, and non-scalar referents remain rejected. +Conditional bindings, temporaries, reference returns, the address of a +scalar reference, and rvalue references remain rejected. A reference to a +record names an object in memory, as a pointer to it does (see `Records +through pointers`_ below). Addressable declarations inside loops and ``cppverify::old`` of automatic locals or local bindings are also fail-closed; outer automatic locals and loop-local reference aliases are supported. @@ -101,7 +103,10 @@ There are three frame granularities: - a **range**, ``cppverify::modifies(p[lo : n])``, names the ``n`` elements from ``p[lo]``, half-open ``[lo, lo + n)`` (Clang's array-section syntax); - a **region**, ``cppverify::modifies(*p)``, names the object ``p`` addresses: its - ``valid(p, n)`` extent, or one object. + ``valid(p, n)`` extent, or one object. ``cppverify::modifies(c)`` for a + reference ``c`` to a record names the object ``c`` names, and a record or + array member named whole (``cppverify::modifies(s->inner)``, + ``cppverify::modifies(s->items)``) names the bytes of that member. Inside the function, every store must lie in a footprint read in the entry state. At a modular call, the caller's heap changes only inside the callee's @@ -328,6 +333,12 @@ pointer stepped through the array stays a whole number of elements from its start, as a generated invariant it proves, so the walk never lands between elements. :doc:`../../language/structs` lists the forms that are supported. +All of this holds through a reference to a record too, which is a pointer +that is never null and never reseated: ``void push(Stack &s, int v)`` reads +and writes ``s.items[s.size]`` and ``s.size`` as the version above does +through ``s->``, ``cppverify::modifies(s)`` is the whole stack, and +``&s.size`` and ``s.items`` are pointers into it. + Declaring a checked extent -------------------------- From a430f47dd92a3cc4e259501b7a4a50b407b5981a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 07:59:51 +0530 Subject: [PATCH 28/94] docs: List references to records as supported in the limitations. --- website/source/language/limitations.rst | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/website/source/language/limitations.rst b/website/source/language/limitations.rst index 3b58f00f81..70d3943bfa 100644 --- a/website/source/language/limitations.rst +++ b/website/source/language/limitations.rst @@ -124,10 +124,14 @@ C++ feature boundaries boolean, integral, and enum referents, and bindings to a field or fixed array element of a promoted local object, or to a field or element of a parameter's buffer or of a record a pointer addresses - (``int &alias = p->second;``, ``p[i]``, ``p->buf[i]``). - - Conditional bindings, reference returns, non-scalar referents, rvalue - references, temporaries/lifetime extension, reference members, and - forwarding. + (``int &alias = p->second;``, ``p[i]``, ``p->buf[i]``). References to + records (``Counter &``, ``const Counter &``) as parameters and locals: + a pointer never null or reseated, whose fields and elements are cells + of the object it names (:doc:`structs`). + - Conditional bindings, reference returns, rvalue references, + temporaries/lifetime extension, reference members, and forwarding; a + local record passed to a reference parameter (``construct.unsupported``) + and a pointer formed through a local reference to a local record. * - Arrays - Pointer-parameter indexing, quantified abstract buffers, fixed local arrays (including arrays of records and multidimensional arrays) @@ -287,9 +291,10 @@ the lexical closing brace or on each early return, in reverse construction order; heap-reading return values are materialized before teardown. Scalar references may bind supported fields and fixed-array elements of these -promoted objects. Conditional bindings, temporaries, reference returns, general -raw address-taking/array decay, rvalue references, and non-scalar referents -remain fail-closed. Addressable declarations inside loops and ``cppverify::old`` of local +promoted objects, and a local reference to a record may bind a promoted +record. Conditional bindings, temporaries, reference returns, general raw +address-taking/array decay (through such a reference too), rvalue references, +and passing a promoted record to a callee by reference remain fail-closed. Addressable declarations inside loops and ``cppverify::old`` of local objects/bindings are also rejected; outer automatic locals and loop-local aliases are supported. Pointer leaves may retain abstract pointer values, but storing a provenance-bearing local dynamic pointer fails closed until pointer provenance is From af65377c2ea4ea3daa8222f35670847377b65985 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 07:59:51 +0530 Subject: [PATCH 29/94] docs: Mention references to records on the front page. --- website/source/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/index.rst b/website/source/index.rst index 7865a9bb98..783fb1bea7 100644 --- a/website/source/index.rst +++ b/website/source/index.rst @@ -189,8 +189,8 @@ Reference values lower to heap loads, writes lower to stores, and ``cppverify::o the entry heap. The bounded scalar slice supports direct forwarding, direct ``*p`` bindings, fields and elements of local objects and of parameter buffers, ordinary initialized scalar local actuals, and chained local reference aliases. -Conditional bindings, temporaries, reference returns, and non-scalar referents -remain fail-closed. See +A reference to a record is a pointer to it that is never null or reseated. +Conditional bindings, temporaries, and reference returns remain fail-closed. See :doc:`language/pointers`. Backends and modular calls From 34b3eceb9effcc96bf3d95a2316e4c27e72c9040 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 07:59:52 +0530 Subject: [PATCH 30/94] docs: Note records through references and members named whole in the release notes. --- website/source/release-notes.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/website/source/release-notes.rst b/website/source/release-notes.rst index c79f4204f3..42d5c4bbb1 100644 --- a/website/source/release-notes.rst +++ b/website/source/release-notes.rst @@ -212,6 +212,14 @@ although the callee writes only cells the caller may write, in two cases: The callee's footprint is now compared with the caller's frame cell by cell (:doc:`language/pointers`). +**A record member named whole in** ``cppverify::modifies``. +``cppverify::modifies(p->left)``, for a member that is a record or an array, +was taken for one cell holding the whole member. A call ``reset(&p->left)`` +of a function that may write its object failed its ``frame`` check, and a +call of the function itself could not be verified (``obligation lowering +failed: ... (invalid sort in obligation IR)``). The footprint is now the +bytes of the member (:doc:`language/structs`). + **Lean goals that could not be proved.** In a Lean goal a quantifier took everything after it into its body, which made a goal with a quantified fact before its conclusion say more than its obligation: the goal of a call that @@ -262,6 +270,17 @@ accesses, steps, pointer differences, and call arguments through it are checked against that subobject, so ``q[8]`` past ``r->buf`` is an error, though it lies in ``*r`` (:doc:`language/structs`). +**Records through references.** A reference to a record (``Counter &c``, +``const Counter &c``), as a parameter or a local, was an error +(``unsupported C++ type in verification: Counter &``), so the free function +form of a method on an object, ``bump(Counter &c)``, could not be verified. +A reference to a record is now a pointer to it that is never null and never +reseated: ``c.n`` and ``c.buf[i]`` are cells of the object, ``&c.n`` and +``c.buf`` point into it, ``cppverify::modifies(c)`` is the whole object, and +a reference can be bound to ``*p``, ``p->left``, ``a[i]``, or another +reference. Passing a local record to a reference parameter is still +``construct.unsupported`` (:doc:`language/structs`). + Running cpp-verify ~~~~~~~~~~~~~~~~~~ From 02e7fa7a250c41ef05252f261dab5c1e35ccda1a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 07:59:52 +0530 Subject: [PATCH 31/94] docs: Describe records through references in the design. --- docs/DESIGN.md | 71 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 3ebdc07ef9..ef70dc2988 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1109,7 +1109,8 @@ identity. Addressable local declarations inside loops, `cppverify::old(local)`, `cppverify::old(local_ref)`, conditional bindings, temporaries, reference returns, -address-taking, rvalue references, and non-scalar referents remain fail-closed. +address-taking, and rvalue references remain fail-closed. A reference to a +record is the address of an object (3c). An outer automatic local and a local reference declaration may be used inside a loop. Recursive executable bodies may store through pointer and reference parameters and use automatic objects; recursion through a function that @@ -1159,6 +1160,56 @@ void put(Ring *r, int v) Returning such a pointer or storing it in memory would lose the bounds and is rejected. +### 3c. Records through references + +```cpp +struct Counter { int n; int limit; }; +struct Pair { Counter left; Counter right; }; + +void reset(Counter &c) + cppverify::modifies(c) + cppverify::post(c.n == 0 && c.limit == 10) +{ + c.n = 0; + c.limit = 10; +} + +void reset_left(Pair &p) + cppverify::modifies(p.left) + cppverify::post(p.left.n == 0 && p.right.n == cppverify::old(p.right.n)) +{ + reset(p.left); +} +``` + +- A reference to a record (`T &`, `const T &`, a parameter or a local) is a + pointer to it that is never null and never reseated, so everything 3b + says holds of it: `c.f`, `c.buf[i]`, and `c.inner.x` are cells of the + object `c` names, with the same two checks, and `&c.f` and `c.buf` are + pointers into it with subobject bounds. A record by value stays its + fields; only a reference or a pointer names an object. +- A reference parameter gets the generated preconditions of a scalar + reference: its object is non-null, live, and initialized, and it takes + part in the default non-aliasing of address parameters over its whole + object (`cppverify::aliases` opts out). A record's type invariant is not + injected for it, as for a pointer: its fields are read from memory. +- `cppverify::modifies(c)` is the object, as `cppverify::modifies(*p)` is, + and as Dafny's `modifies c` is every field of the object `c` references; + `cppverify::modifies(c.n)` is one cell, as Dafny's ``modifies c`n``. A + scalar reference's `modifies(r)` stays one cell, since it may bind a field + of a larger object. +- A reference argument or a local reference binds a place: another + reference, `*p`, a field (`p->left`), an element (`a[i]`), checked where it + is bound. A local reference may also bind a promoted local record; reads + and writes through it carry that object's lifetime and initialization + checks, but a pointer formed through it would not, and is rejected (as + `&local` is). +- Not yet: a local record passed to a reference or pointer parameter. The + non-escape scan that admits a local scalar actual does not cover records, + so the call is `Unresolved` (`construct.unsupported`). Also rejected: a + whole record read through a reference, reference returns, rvalue and + `volatile` references, and references in spec and proof functions. + ### 4. `cppverify::modifies(...)` frame condition ```cpp @@ -1191,14 +1242,20 @@ void incr_first(int* a, int* b) `p->buf[lo : n]` for an array member; - a region `*p`: the object `p` addresses, which is its `valid(p, n)` extent when it has one and otherwise one object. A region of a single - scalar object is one cell. + scalar object is one cell. `c` for a reference to a record is the object + it names; + - the bytes of a record or array inside an object, named whole + (`p->inner`, `c.buf`, `p[k]` of a record extent): a range of one element + of its size. - Inside the callee every store must lie in a footprint, read in the entry state, so reassigning a pointer cannot widen the frame; a footprint of a callee must lie within the caller's own frame, cell by cell: a region of one scalar object is one cell, ranges and extents compare by their cells, and a footprint reached through another pointer than a caller region's (a copy of a pointer into the object) lies in that region when its cells - lie in the region's object, as a store does. + lie in the region's object, as a store does. A region or range lies in + one footprint of the caller: the heap holds a cell at every address, so + footprints of a record's fields one by one do not contain the record. - At a call, the caller's heap changes only inside the callee's footprints instantiated with the arguments: a cell or range is exactly those cells, a region is the argument's extent (or one object). Every other cell keeps its @@ -1209,8 +1266,7 @@ void incr_first(int* a, int* b) forgets the whole heap. - Preconditions and `cppverify::old(parameter)` use entry actual arguments, and so does a parameter named in a postcondition. -- General reference binding and member-function effects are not yet in the - verified subset. +- Member-function effects are not yet in the verified subset. ### Worked example @@ -1267,7 +1323,8 @@ int dist_sq(Coordinate p, Coordinate q) ``` - `cppverify::type_invariant(expr)`: holds at function boundaries for by-value - records of the type; pointer-to-record parameters get no injection. + records of the type; pointer-to-record and reference-to-record parameters + get no injection. - `expr` may reference any field of the enclosing type by name. - Must be contextually convertible to bool. @@ -2087,5 +2144,5 @@ fixed arrays, scalar subobject references, the fields and fixed-array elements of a record a pointer addresses, modular slices, and general same-array pointer difference under `valid(p, n)` are also supported. Provenance across ownership-taking or recursive interfaces, pointer -reinterpretation/cross-object arithmetic, aggregate/rvalue references, +reinterpretation/cross-object arithmetic, rvalue references, by-value pointer-bearing records, and general arrays remains unsupported. From 63b2b98481bfcaa986b1ec1b0e3a0b96e6ed5a4f Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 07:59:53 +0530 Subject: [PATCH 32/94] docs: Describe records through references in the architecture. --- docs/ARCHITECTURE.md | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1389282dcf..3a0b6377b6 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -176,6 +176,31 @@ whose own access UB instrumentation checks against the pointer's extent or object as before. `p->buf[i]` therefore has both checks, and the array's is what rejects `p->buf[8]`, the address of the next field. +**Records through references:** a reference to a record, a parameter or a +local, is a pointer-typed VCR variable holding the object's address +(`VType::Ptr` of the record's size), never flattened: `getRecordFromType` +stops at references, so only a record by value becomes `param.field` +values, and so do source variables, type-invariant injection, and call +arguments. `recordReferenceRoot` finds the reference a selection chain +starts at (`c.f`, `c.buf[i]`), and `inPointeeObject` (it, or +`pointeeRootPointer`) admits the place wherever a place through a pointer is +admitted: loads, stores, subscripts, reference bindings, footprints. +`objectPlace` of the bare reference is the place at its address, as `*p`. +Pointers into the object (`&c.f`, `c.buf`) are formed only where +`subobjectPointerRoot` holds: through a pointer, or a reference whose object +outlives the function (a parameter, or a local not bound to represented +storage, which `LocalReferenceProvenanceVariables` records), since a +subobject pointer does not carry a promoted object's lifetime. +`VFunction::ScalarReferenceParams` holds only scalar references, whose +`modifies(r)` is the one cell they bind; a record reference's is a region, +its parameter's entry object (`entryObjectRegion`), as for `*p`. +`convertFootprint` lowers a record or array named whole inside an object +(`p->inner`, `c.buf`, `p[k]`) to a range of one element of its size, so +every frame path (own stores, call havoc, containment, loop frames) treats it +as its bytes; before, it was a cell holding the record, which held no +callee's footprint inside the member and whose havoc at a call had no logic +sort. + **Pointers into objects:** `ASTConverter::subobjectPointer` lowers a decayed member array of a pointee and the address of a pointee's element, field, or nested record. In a statement (`SubobjectSink`) it asserts the place's access @@ -300,8 +325,10 @@ VFunction = source spelling remain distinct through modular calls and SMT symbols. - `cppverify::aliases` empty means the implicit non-aliasing precondition applies to every pair of distinct pointer/reference parameters of which at least one is mutable. - A `VFootprint` is a target lvalue with an optional element count and - element size: a cell (`p[i]`, `p->f`, a reference), a range `p[lo : n]` - (target `p[lo]`, count `n`), or a region `*p` (the object `p` addresses). + element size: a cell (`p[i]`, `p->f`, a scalar reference), a range + `p[lo : n]` (target `p[lo]`, count `n`; a record or array named whole + inside an object, `p->inner`, is a range of one element of its size), or a + region `*p` (the object `p` addresses, or a reference to a record names). - Behaviors are desugared in the frontend: each scoped `cppverify::pre` becomes `!assumes || pre`, each scoped `cppverify::post` `!old(assumes) || post`, and `cppverify::complete_behaviors`/`cppverify::disjoint_behaviors` become assertions at body entry. @@ -2381,9 +2408,11 @@ disjoint from incoming address parameters, complete declared extents, stored pointer cells, and every live represented object. Their lifetimes end at the lexical closing brace and on each supported early return in reverse construction order, after materializing any heap-reading return value. Scalar references may -bind fields and fixed-array elements with access-local nested bounds. General -escape, raw array decay/address-taking, aggregate references, and addressable -declarations inside loops remain fail-closed; so does the decay of an array +bind fields and fixed-array elements with access-local nested bounds, and a +local reference to a record may bind a promoted record, its provenance +companion carried like a scalar reference's. General escape, raw array +decay/address-taking (through such a reference too), passing a promoted record +to a callee, and addressable declarations inside loops remain fail-closed; so does the decay of an array member through a pointer (`ASTConverter::arrayDecayError` names which). Automatic metadata transitions are private and a modular caller may frame them away. From 61f543b538655851d1711036bf4fd7b9520cf46e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 09:25:15 +0530 Subject: [PATCH 33/94] verify: Record the pointer and reference parameters that address a record. --- 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 816f9f6910..3164896888 100644 --- a/clang/lib/Verify/IR/VStmt.h +++ b/clang/lib/Verify/IR/VStmt.h @@ -423,6 +423,10 @@ struct VFunction { /// they bind (perhaps a field of a larger object). A reference to a record /// is a pointer to it, and `modifies(r)` its object. std::set ScalarReferenceParams; + /// Pointer and reference parameters to a record, with the identity of the + /// record's entry in Layouts: an object whose leaves lie at offsets inside + /// it. + std::map RecordParams; /// Pointer and reference parameters to const objects: the function /// writes nothing through them. std::set ConstAddressParams; From c132d065e5a234cfdb79aeeb0efcfab1f9121d1a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 09:25:16 +0530 Subject: [PATCH 34/94] verify: Clone a function's record parameters. --- 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 88f5970470..90348d6bc9 100644 --- a/clang/lib/Verify/IR/VStmt.cpp +++ b/clang/lib/Verify/IR/VStmt.cpp @@ -198,6 +198,7 @@ VFunction verify::cloneVFunction(const VFunction &Fn) { Out.Params = Fn.Params; Out.SourceVariables = Fn.SourceVariables; Out.ScalarReferenceParams = Fn.ScalarReferenceParams; + Out.RecordParams = Fn.RecordParams; Out.ConstAddressParams = Fn.ConstAddressParams; Out.BoundedPointers = Fn.BoundedPointers; Out.ReturnFields = Fn.ReturnFields; From 98aced80939783ed99e5904835d59d12d60b6d64 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 09:25:16 +0530 Subject: [PATCH 35/94] verify: Keep a function's record parameters 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 80c2dc5254..c22a67864d 100644 --- a/clang/lib/Verify/Transform/LoopUnroll.cpp +++ b/clang/lib/Verify/Transform/LoopUnroll.cpp @@ -256,6 +256,7 @@ VFunction LoopUnroller::unroll(const VFunction &Fn, unsigned K) { Out.Params = Fn.Params; Out.SourceVariables = Fn.SourceVariables; Out.ScalarReferenceParams = Fn.ScalarReferenceParams; + Out.RecordParams = Fn.RecordParams; Out.ConstAddressParams = Fn.ConstAddressParams; Out.BoundedPointers = Fn.BoundedPointers; Out.ReturnFields = Fn.ReturnFields; From 9ff5f65e2986f3e1b5867d11499e9c741fe40f77 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 09:25:17 +0530 Subject: [PATCH 36/94] verify: Pass the address of a local to a pointer parameter, and record the parameters that address a record. --- clang/lib/Verify/Frontend/ASTConverter.cpp | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/clang/lib/Verify/Frontend/ASTConverter.cpp b/clang/lib/Verify/Frontend/ASTConverter.cpp index 795f625d8f..ba762b436a 100644 --- a/clang/lib/Verify/Frontend/ASTConverter.cpp +++ b/clang/lib/Verify/Frontend/ASTConverter.cpp @@ -3088,6 +3088,19 @@ ASTConverter::convertFunction(const FunctionDecl *FD) { return nullptr; } collectLayouts(FD, *Fn); + for (const ParmVarDecl *P : FD->parameters()) { + QualType Ty = P->getType(); + if (!Ty->isPointerType() && !Ty->isReferenceType()) + continue; + const std::string Identity = canonicalTypeIdentity( + Ty->getPointeeType().getCanonicalType().getUnqualifiedType(), Ctx); + if (Ty->getPointeeType()->isRecordType() && + llvm::any_of(Fn->Layouts, [&](const VObjectLayout &Layout) { + return Layout.Kind == VObjectKind::Record && + Layout.TypeIdentity == Identity; + })) + Fn->RecordParams[valueName(P)] = Identity; + } CurrentFn = nullptr; return Fn; } @@ -5495,6 +5508,25 @@ void ASTConverter::convertExecCallArg( "local, or direct pointer dereference"); return; } + // The address of a local object, or of a field or element of one, binds a + // pointer parameter as a reference argument binds a reference: the call + // checks that the callee keeps no pointer into it. + if (Formal && Formal->getType()->isPointerType()) + if (const auto *U = dyn_cast(E->IgnoreParenImpCasts()); + U && U->getOpcode() == UO_AddrOf && + !U->getSubExpr()->getType()->isArrayType() && + promotedRootLocal(U->getSubExpr())) { + std::unique_ptr AccessCondition; + Out = convertLValueAddress(U->getSubExpr(), &AccessCondition); + if (AccessCondition) + Prelude.push_back(std::make_unique( + std::move(AccessCondition), E->getExprLoc(), + ProofObligationKind::Bounds)); + if (Out) + appendReferenceBindingCheck(U->getSubExpr(), Out.get(), E->getExprLoc(), + Prelude); + return; + } const VarDecl *Source = directDynamicPointer(E); if (Source || isDynamicAddress(E)) { if (!Source || !Formal || !Formal->getType()->isPointerType() || From ced3b16bef173e585cde55024ea6949aa9f28e04 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 09:25:17 +0530 Subject: [PATCH 37/94] verify: Lend a local record to a callee that keeps no address of it, checking the whole record and its fields at the call. --- clang/lib/Verify/Transform/Passivize.cpp | 272 +++++++++++++++++++++-- 1 file changed, 259 insertions(+), 13 deletions(-) diff --git a/clang/lib/Verify/Transform/Passivize.cpp b/clang/lib/Verify/Transform/Passivize.cpp index e41db3316c..e5851c138b 100644 --- a/clang/lib/Verify/Transform/Passivize.cpp +++ b/clang/lib/Verify/Transform/Passivize.cpp @@ -1487,6 +1487,159 @@ static bool scalarDynamicCalleeSafe( return Safe; } +/// Whether a pointer-typed expression may hold an address derived from one +/// of the aliases. +static bool pointerFromAliases(const VExpr *E, + const std::set &Aliases) { + return E && E->Ty.Kind == VTypeKind::Ptr && referencesAnyAlias(E, Aliases); +} + +/// The pointer variables of a body that may hold an address derived from +/// the aliases: those assigned one, to a fixpoint. +static void +collectRecordAliases(const std::vector> &Body, + std::set &Aliases, bool &Changed) { + for (const auto &S : Body) { + switch (S->K) { + case VStmt::Assign: { + const auto &A = static_cast(*S); + if (pointerFromAliases(A.Value.get(), Aliases) && + Aliases.insert(A.Target).second) + Changed = true; + break; + } + case VStmt::If: { + const auto &I = static_cast(*S); + collectRecordAliases(I.Then, Aliases, Changed); + collectRecordAliases(I.Else, Aliases, Changed); + break; + } + case VStmt::While: + collectRecordAliases(static_cast(*S).Body, Aliases, + Changed); + break; + case VStmt::Seq: + collectRecordAliases(static_cast(*S).Stmts, Aliases, + Changed); + break; + case VStmt::GhostBlock: + collectRecordAliases(static_cast(*S).Body, + Aliases, Changed); + break; + default: + break; + } + } +} + +static bool +keepsNoAddress(const VFunction &Fn, const std::string &Param, bool AllowExtent, + const FunctionMap &FnMap, + std::set> &ActiveScans); + +/// Whether a statement keeps no address derived from the aliases beyond the +/// call: none is stored in memory, returned, freed, or passed to a callee +/// that keeps it. An address converted to an integer is no escape, since no +/// integer becomes a pointer again (the frontend rejects those casts). +static bool +recordStmtSafe(const VStmt &S, const std::set &Aliases, + const FunctionMap &FnMap, + std::set> &ActiveScans) { + auto Each = [&](const std::vector> &Body) { + return llvm::all_of(Body, [&](const std::unique_ptr &Nested) { + return recordStmtSafe(*Nested, Aliases, FnMap, ActiveScans); + }); + }; + switch (S.K) { + case VStmt::Store: + return !pointerFromAliases(static_cast(S).Value.get(), + Aliases); + case VStmt::Allocate: + case VStmt::Free: + // A function that allocates or frees is UsesDynamicStorage, which + // keepsNoAddress rejects before it looks at a statement. + return false; + case VStmt::If: { + const auto &I = static_cast(S); + return Each(I.Then) && Each(I.Else); + } + case VStmt::While: + return Each(static_cast(S).Body); + case VStmt::Call: { + const auto &C = static_cast(S); + auto CalleeIt = FnMap.find(C.CalleeIdentity); + if (CalleeIt == FnMap.end()) + return false; + const VFunction &Callee = *CalleeIt->second; + // The function passing an address proved that the callee's object or + // extent lies in its own, so the callee need only keep none, returning + // one included. + for (unsigned I = 0; I < C.Args.size(); ++I) + if (pointerFromAliases(C.Args[I].get(), Aliases) && + (I >= Callee.Params.size() || + Callee.Params[I].second.Kind != VTypeKind::Ptr || + !keepsNoAddress(Callee, Callee.Params[I].first, + /*AllowExtent=*/true, FnMap, ActiveScans))) + return false; + return true; + } + case VStmt::Return: + return !pointerFromAliases(static_cast(S).Value.get(), + Aliases); + case VStmt::Seq: + return Each(static_cast(S).Stmts); + case VStmt::GhostBlock: + return Each(static_cast(S).Body); + case VStmt::Assign: + case VStmt::Assert: + case VStmt::Assume: + case VStmt::ContractAssert: + case VStmt::EndLifetime: + case VStmt::Havoc: + case VStmt::RevealWithFuel: + case VStmt::HideSpec: + case VStmt::RevealSpec: + case VStmt::Break: + case VStmt::Continue: + return true; + } + return false; +} + +/// Whether Fn keeps no pointer into the object its parameter Param addresses +/// beyond the call. Fn is verified in the object model, so every access +/// through Param, at whatever field or element, is proved to lie in its +/// object (or its extent, which only a caller that proved the extent inside +/// its own object may give, AllowExtent); what remains is that no address +/// derived from it escapes. +static bool +keepsNoAddress(const VFunction &Fn, const std::string &Param, bool AllowExtent, + const FunctionMap &FnMap, + std::set> &ActiveScans) { + if (Fn.IsSpec || Fn.IsProof || Fn.IsExternalContract || + Fn.UsesDynamicStorage || !Fn.ObjectModel || + (!AllowExtent && + llvm::any_of(Fn.ValidExtents, [&](const VValidExtent &Extent) { + return Extent.Base == Param; + }))) + return false; + // An escape is a finite path to a statement that keeps an address, so a + // scan that reaches itself again, through recursion, may assume none. + const auto Key = std::make_pair(Fn.Identity, Param); + if (!ActiveScans.insert(Key).second) + return true; + std::set Aliases = {Param}; + for (bool Changed = true; Changed;) { + Changed = false; + collectRecordAliases(Fn.Body, Aliases, Changed); + } + bool Safe = true; + for (const auto &S : Fn.Body) + Safe = Safe && recordStmtSafe(*S, Aliases, FnMap, ActiveScans); + ActiveScans.erase(Key); + return Safe; +} + static bool pointerResultComesFrom(const VExpr *E, const std::set &Params) { if (!E) @@ -4932,6 +5085,72 @@ class PassivizerImpl { Loc)}; } + /// A record in local or dynamic storage passed to a parameter that + /// addresses one: the generated validity clause names its first byte, and + /// the callee reads every leaf as valid and initialized storage. Each byte + /// up to its last belongs to the live allocation of its first when the + /// last does (an allocation's bytes are contiguous), and each leaf must be + /// initialized, which C++ does not require of an out-parameter. + void emitRecordActualChecks( + PassiveProgram &P, const VFunction &Callee, + const std::set &DynamicParams, + const std::map> &ParamMap, + const CloneCtx &Ctx, SourceLocation Loc) { + if (!Fn.ObjectModel) + return; + for (const auto &[Name, Identity] : Callee.RecordParams) { + auto Actual = ParamMap.find(Name); + if (!DynamicParams.count(Name) || Actual == ParamMap.end()) + continue; + const auto Layout = + llvm::find_if(Callee.Layouts, [&](const VObjectLayout &L) { + return L.TypeIdentity == Identity; + }); + if (Layout == Callee.Layouts.end() || Layout->SizeBytes == 0) + continue; + auto Offset = [&](uint64_t Bytes) { + return addBytes(cloneVExpr(Actual->second.get()), + std::make_unique( + std::to_string(Bytes), pointerOffsetType(), Loc), + Loc); + }; + emitPassive(P, PassiveStmt::Assert, + std::make_unique( + VUnaryOp::ValidPtr, Offset(Layout->SizeBytes - 1), + VType::makeBool(), Loc, + stateHeapName(Ctx, VAllocationHeapName, ""), + stateHeapName(Ctx, VLivenessHeapName, "")), + nullptr, Loc, ProofObligationKind::PointerValidity); + std::vector Leaves; + for (const VObjectLeaf &Leaf : Layout->Leaves) { + std::vector Offsets = {Leaf.OffsetBytes}; + for (const VObjectRepeat &Repeat : Leaf.Repeats) { + std::vector Next; + for (uint64_t Base : Offsets) + for (uint64_t K = 0; K < Repeat.Count && Next.size() <= 4096; ++K) + Next.push_back(Base + K * Repeat.StrideBytes); + Offsets = std::move(Next); + } + Leaves.insert(Leaves.end(), Offsets.begin(), Offsets.end()); + } + std::unique_ptr Initialized; + if (Leaves.size() <= 4096) + for (uint64_t Leaf : Leaves) { + auto One = std::make_unique( + VUnaryOp::InitializedPtr, Offset(Leaf), VType::makeBool(), Loc, + "", "", stateHeapName(Ctx, VInitializationHeapName, "")); + Initialized = + Initialized ? makeAnd(std::move(Initialized), std::move(One), Loc) + : std::move(One); + } + emitPassive(P, PassiveStmt::Assert, + Initialized ? std::move(Initialized) + : makeBoolLiteral(Leaves.empty(), Loc), + nullptr, Loc, ProofObligationKind::Unsupported, + OutParameterNote); + } + } + /// The heap after a call of Callee whose footprints are ActualModifies /// (with ActualCounts) in the caller's state: each one it may write gets /// unknown contents, as a chain of stores when all are cells, and every @@ -4946,15 +5165,28 @@ class PassivizerImpl { const std::map> &ParamMap, const CloneCtx &Ctx, const std::string &EntryHeap, bool HasImplicitHeapEffect, SourceLocation Loc) { + // A footprint in a record that a parameter addresses is its bytes there, + // in local or dynamic storage too, and never the one cell at its start. + auto InRecordParam = [&](size_t I) { + const VExpr *Target = Callee.Modifies[I].Target.get(); + const VExpr *Root = + Target && Target->K == VExpr::Load + ? addressRoot(static_cast(Target)->Ptr.get()) + : nullptr; + return Root && Root->K == VExpr::Var && + Callee.RecordParams.count( + static_cast(Root)->Name); + }; bool CanFrameExactly = !ActualModifies.empty(); for (size_t I = 0; I < ActualModifies.size(); ++I) if (!ActualModifies[I] || ActualModifies[I]->K != VExpr::Load || ActualCounts[I] || (isRegionFootprint(Callee.Modifies[I], Callee.ScalarReferenceParams) && - !hasPointerProvenance( - static_cast(ActualModifies[I].get()) - ->Ptr.get()))) + (!hasPointerProvenance( + static_cast(ActualModifies[I].get()) + ->Ptr.get()) || + InRecordParam(I)))) CanFrameExactly = false; // A region footprint is its actual's object: the callee's extent, or one // object; in the object model the callee writes nowhere else. @@ -4965,9 +5197,10 @@ class PassivizerImpl { for (size_t I = 0; CanFrameRegions && I < ActualModifies.size(); ++I) { std::optional Bytes; if (ActualModifies[I] && ActualModifies[I]->K == VExpr::Load && - !hasPointerProvenance( - static_cast(ActualModifies[I].get()) - ->Ptr.get())) + (!hasPointerProvenance( + static_cast(ActualModifies[I].get()) + ->Ptr.get()) || + InRecordParam(I))) Bytes = footprintBytes(Callee, I, ActualModifies[I].get(), ActualCounts[I].get(), ParamMap, Ctx, EntryHeap, Loc); @@ -5052,15 +5285,24 @@ class PassivizerImpl { if (auto It = ParamMap.find(Param.first); It != ParamMap.end() && hasPointerProvenance(It->second.get())) { DynamicParams.insert(Param.first); + // A record a function in the object model reaches through the + // parameter is accessed only inside it, at any field or element. + const bool RecordObject = + Fn.ObjectModel && Callee->RecordParams.count(Param.first); if (Callee->IsProof || Callee->IsExternalContract || - !scalarDynamicCalleeSafe(*Callee, Param.first, FnMap, - ActiveScans)) { + !(RecordObject + ? keepsNoAddress(*Callee, Param.first, /*AllowExtent=*/false, + FnMap, ActiveScans) + : scalarDynamicCalleeSafe(*Callee, Param.first, FnMap, + ActiveScans))) { emitPassive(P, PassiveStmt::Assert, makeBoolLiteral(false, C.Loc), nullptr, C.Loc, ProofObligationKind::Unsupported, "the address of local or dynamic storage is passed to " + C.Callee + - ", which may keep, offset, or free it, or has no " - "verified body"); + (RecordObject + ? ", which may keep or free it" + : ", which may keep, offset, or free it") + + ", or has no verified body"); return; } } @@ -5300,6 +5542,7 @@ class PassivizerImpl { emitPassive(P, PassiveStmt::Assert, std::move(Part), nullptr, C.Loc, Kind, std::move(Note)); } + emitRecordActualChecks(P, *Callee, DynamicParams, ParamMap, Ctx, C.Loc); // A call within the caller's recursion cycle lowers the measure below its // value at the caller's entry, so assuming the callee's contract there is @@ -5361,9 +5604,12 @@ class PassivizerImpl { InnerBytes = calleeFootprintBytes(I); // An empty range writes nothing. if (ActualCounts[I] && InnerBytes) - Allowed = std::make_unique( - VBinOp::Le, cloneVExpr(InnerBytes->Hi.get()), - cloneVExpr(InnerBytes->Lo.get()), VType::makeBool(), C.Loc); + Allowed = makeOr(std::move(Allowed), + std::make_unique( + VBinOp::Le, cloneVExpr(InnerBytes->Hi.get()), + cloneVExpr(InnerBytes->Lo.get()), + VType::makeBool(), C.Loc), + C.Loc); for (size_t J = 0; J < CallerModifies.size(); ++J) { const auto &CallerM = CallerModifies[J]; const auto *CallerLoad = From 64bd1d2b78f5662d3879e01a737672a3c04b4a41 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 09:25:18 +0530 Subject: [PATCH 38/94] test: Check local records and addresses passed to callees. --- .../Verify/suite/local_record_arguments.cpp | 524 ++++++++++++++++++ 1 file changed, 524 insertions(+) create mode 100644 clang/test/Verify/suite/local_record_arguments.cpp diff --git a/clang/test/Verify/suite/local_record_arguments.cpp b/clang/test/Verify/suite/local_record_arguments.cpp new file mode 100644 index 0000000000..e43baefa16 --- /dev/null +++ b/clang/test/Verify/suite/local_record_arguments.cpp @@ -0,0 +1,524 @@ +// RUN: not %cpp-verify --jobs=1 %s -- 2>&1 | FileCheck %s +// RUN: not %cpp-verify --jobs=1 --backend=bmc --unroll=5 %s -- 2>&1 \ +// RUN: | FileCheck %s --check-prefix=BMC +// RUN: not %cpp-verify --jobs=1 --no-check-ub %s -- 2>&1 \ +// RUN: | FileCheck %s --check-prefix=NOUB +// RUN: %cpp-verify --lower-only --dump-ir=2 %s -- -DLOWER 2>&1 \ +// RUN: | FileCheck %s --check-prefix=PASSIVE +// +// A local record passed to a callee by reference, or by its address, keeps +// its lifetime and frame across the call. The callee is verified in the +// object model, so every access through its parameter, at any field or +// element, lies in the object; the call checks that the callee keeps no +// address of it (stores none, returns none, frees none, and passes it only to +// callees that keep none), that the whole record lies in the live object, and +// that every field is initialized. The callee then writes only its footprints +// in the record, by their bytes. + +struct Counter { + int n; + int limit; +}; + +struct Pair { + Counter left; + Counter right; +}; + +struct Ring { + int buf[4]; + int head; +}; + +void bump(Counter &c) + cppverify::pre(c.n < c.limit) + cppverify::modifies(c.n) + cppverify::post(c.n == cppverify::old(c.n) + 1) + cppverify::post(c.limit == cppverify::old(c.limit)) +{ + c.n = c.n + 1; +} +// CHECK-DAG: Verified: bump [backend=z3] + +void reset(Counter *c) + cppverify::pre(c != nullptr) + cppverify::modifies(*c) + cppverify::post(c->n == 0 && c->limit == 10) +{ + c->n = 0; + c->limit = 10; +} + +int by_reference() + cppverify::post(cppverify::result == 1) +{ + Counter c; + c.n = 0; + c.limit = 5; + bump(c); + return c.n; +} +// CHECK-DAG: Verified: by_reference [backend=z3] +// BMC-DAG: Verified: by_reference [backend=bmc, bound=0] +// NOUB-DAG: Unresolved: by_reference [backend=z3] [reason=construct.unsupported] (the verifier cannot model a construct here: the address of local or dynamic storage is passed to bump, which may keep, offset, or free it, or has no verified body) +// The generated clauses name the first byte; the call checks the last byte +// and every field. +// PASSIVE-LABEL: passive by_reference +// PASSIVE: assert precondition +// PASSIVE: assert pointer-validity +// PASSIVE: assert unsupported +// PASSIVE: assert pointer-validity +// PASSIVE-NEXT: || +// PASSIVE-NEXT: ! +// PASSIVE-NEXT: true +// PASSIVE-NEXT: valid_ptr +// PASSIVE-NEXT: + +// PASSIVE-NEXT: c_{{[0-9]+}} +// PASSIVE-NEXT: 7 +// PASSIVE-NEXT: assert unsupported +// PASSIVE-NEXT: || +// PASSIVE-NEXT: ! +// PASSIVE-NEXT: true +// PASSIVE-NEXT: && +// PASSIVE-NEXT: initialized_ptr +// PASSIVE-NEXT: + +// PASSIVE-NEXT: c_{{[0-9]+}} +// PASSIVE-NEXT: 0 +// PASSIVE-NEXT: initialized_ptr +// PASSIVE-NEXT: + +// PASSIVE-NEXT: c_{{[0-9]+}} +// PASSIVE-NEXT: 4 + +#ifndef LOWER +int keeps_limit() + cppverify::post(cppverify::result == 5) +{ + Counter c; + c.n = 0; + c.limit = 5; + bump(c); + return c.limit; +} +// CHECK-DAG: Verified: keeps_limit [backend=z3] + +int by_address() + cppverify::post(cppverify::result == 10) +{ + Counter c; + c.n = 3; + c.limit = 4; + reset(&c); + return c.limit; +} +// CHECK-DAG: Verified: by_address [backend=z3] + +int other_local() + cppverify::post(cppverify::result == 4) +{ + Counter c; + c.n = 0; + c.limit = 5; + int k = 4; + bump(c); + return k; +} +// CHECK-DAG: Verified: other_local [backend=z3] + +int wrong() + cppverify::post(cppverify::result == 5) +{ + Counter c; + c.n = 0; + c.limit = 5; + bump(c); + return c.n; +} +// CHECK-DAG: error: verification failed: wrong [{{.*}}::postcondition@[[@LINE-8]]:37] (counterexample: result [ssa=__result_1] [type=i32] = 1; + +// A member and an element of a local, and the address of a field. +int member() + cppverify::post(cppverify::result == 7) +{ + Pair p; + p.left.n = 0; + p.left.limit = 5; + p.right.n = 7; + p.right.limit = 9; + bump(p.left); + return p.right.n; +} +// CHECK-DAG: Verified: member [backend=z3] + +int element() + cppverify::post(cppverify::result == 1) +{ + Counter a[2]; + a[0].n = 0; + a[0].limit = 3; + a[1].n = 0; + a[1].limit = 3; + bump(a[1]); + return a[1].n - a[0].n; +} +// CHECK-DAG: Verified: element [backend=z3] + +void inc(int *p) + cppverify::pre(p != nullptr && *p < 100) + cppverify::modifies(*p) + cppverify::post(*p == cppverify::old(*p) + 1) +{ + *p = *p + 1; +} + +int field_address() + cppverify::post(cppverify::result == 1) +{ + Counter c; + c.n = 0; + c.limit = 1; + inc(&c.n); + return c.n; +} +// CHECK-DAG: Verified: field_address [backend=z3] + +int scalar_address() + cppverify::post(cppverify::result == 2) +{ + int x = 1; + inc(&x); + return x; +} +// CHECK-DAG: Verified: scalar_address [backend=z3] + +// The callee may reach the record through copies of the address, forward it +// to callees that keep none, and recurse. +void twice(Counter &c) + cppverify::pre(c.n + 1 < c.limit) + cppverify::modifies(c.n) + cppverify::post(c.n == cppverify::old(c.n) + 2) +{ + bump(c); + bump(c); +} + +int nested_calls() + cppverify::post(cppverify::result == 2) +{ + Counter c; + c.n = 0; + c.limit = 5; + twice(c); + return c.n; +} +// CHECK-DAG: Verified: nested_calls [backend=z3] + +void walk(Ring &r) + cppverify::modifies(r.buf[0 : 4]) + cppverify::post(cppverify::forall(k, 0, 4, r.buf[k] == 0)) +{ + for (int *q = r.buf; q != r.buf + 4; q = q + 1) + cppverify::invariant(r.buf <= q && q <= r.buf + 4) + cppverify::invariant(cppverify::forall(k, 0, q - r.buf, r.buf[k] == 0)) + cppverify::decreases(r.buf + 4 - q) + { + *q = 0; + } +} + +int walked() + cppverify::post(cppverify::result == 5) +{ + Ring r; + r.buf[0] = 1; + r.buf[1] = 2; + r.buf[2] = 3; + r.buf[3] = 4; + r.head = 5; + walk(r); + return r.buf[3] + r.head; +} +// CHECK-DAG: Verified: walked [backend=z3] + +void first_to(int *q, int v) + cppverify::pre(cppverify::valid(q, 4)) + cppverify::modifies(q[0]) + cppverify::post(q[0] == v) +{ + q[0] = v; +} + +void through_decay(Ring &r) + cppverify::modifies(r.buf[0]) + cppverify::post(r.buf[0] == 7) +{ + first_to(r.buf, 7); +} + +int decayed() + cppverify::post(cppverify::result == 7) +{ + Ring r; + r.buf[0] = 1; + r.buf[1] = 2; + r.buf[2] = 3; + r.buf[3] = 4; + r.head = 2; + through_decay(r); + return r.buf[0]; +} +// CHECK-DAG: Verified: decayed [backend=z3] + +void down(Counter &c, int k) + cppverify::pre(k >= 0) + cppverify::modifies(c.n) + cppverify::decreases(k) +{ + c.n = k; + if (k > 0) + down(c, k - 1); +} + +int recursive() + cppverify::post(cppverify::result == 9) +{ + Counter c; + c.n = 3; + c.limit = 9; + down(c, 3); + return c.limit; +} +// CHECK-DAG: Verified: recursive [backend=z3] + +// A range footprint in the caller's own object: before, the range's check +// that it may be empty replaced the check that the object is the caller's. +void clear(Ring &r) + cppverify::modifies(r.buf[0 : 4], r.head) + cppverify::post(r.head == 0 && cppverify::forall(k, 0, 4, r.buf[k] == 0)) +{ + for (int i = 0; i < 4; i = i + 1) + cppverify::invariant(0 <= i && i <= 4) + cppverify::invariant(cppverify::forall(k, 0, i, r.buf[k] == 0)) + cppverify::modifies(r.buf[0 : 4]) + cppverify::decreases(4 - i) + { + r.buf[i] = 0; + } + r.head = 0; +} + +int cleared() + cppverify::post(cppverify::result == 0) +{ + Ring r; + r.buf[0] = 1; + r.buf[1] = 2; + r.buf[2] = 3; + r.buf[3] = 4; + r.head = 2; + clear(r); + return r.buf[2] + r.head; +} +// CHECK-DAG: Verified: cleared [backend=z3] + +// The caller's own frame need not name its local. +void into_out(int *out) + cppverify::pre(out != nullptr) + cppverify::modifies(*out) + cppverify::post(*out == 1) +{ + Counter c; + c.n = 0; + c.limit = 5; + bump(c); + *out = c.n; +} +// CHECK-DAG: Verified: into_out [backend=z3] + +// Two locals are distinct objects; one passed twice is not. +void pair_bump(Counter &a, Counter &b) + cppverify::pre(a.n < 10 && b.n < 10) + cppverify::modifies(a.n, b.n) + cppverify::post(a.n == cppverify::old(a.n) + 1) + cppverify::post(b.n == cppverify::old(b.n) + 1) +{ + a.n = a.n + 1; + b.n = b.n + 1; +} + +int two_locals() + cppverify::post(cppverify::result == 2) +{ + Counter a; + a.n = 0; + a.limit = 0; + Counter b; + b.n = 0; + b.limit = 0; + pair_bump(a, b); + return a.n + b.n; +} +// CHECK-DAG: Verified: two_locals [backend=z3] + +int same_local() + cppverify::post(cppverify::result == 2) +{ + Counter a; + a.n = 0; + a.limit = 0; + pair_bump(a, a); + return a.n; +} +// CHECK-DAG: error: verification failed: same_local [{{.*}}::aliasing@[[@LINE-3]]:3] + +// Every field must be initialized: the callee reads its record as +// initialized storage, which C++ does not require of an out-parameter. +void set_n(Counter &c) + cppverify::modifies(c.n) + cppverify::post(c.n == 1) +{ + c.n = 1; +} + +int out_parameter() + cppverify::post(cppverify::result == 1) +{ + Counter c; + c.n = 0; + set_n(c); + return c.n; +} +// CHECK-DAG: [[@LINE-3]]:3: Unresolved: out_parameter [backend=z3] [reason=construct.unsupported] (the verifier cannot model a construct here: binding a reference or pointer parameter to storage that is not initialized, since the callee is verified as reading initialized storage) + +// A callee that keeps the address: in memory, or by returning it. +struct Node { + Node *self; + int v; +}; + +void selfref(Node &n) + cppverify::modifies(n.self) +{ + n.self = &n; +} + +int keeps_self() + cppverify::post(cppverify::result == 0) +{ + Node n; + n.self = nullptr; + n.v = 0; + selfref(n); + return 0; +} +// CHECK-DAG: [[@LINE-3]]:3: Unresolved: keeps_self [backend=z3] [reason=construct.unsupported] (the verifier cannot model a construct here: the address of local or dynamic storage is passed to selfref, which may keep or free it, or has no verified body) + +Counter *same(Counter *p) + cppverify::post(cppverify::result == p) +{ + return p; +} + +int returned() + cppverify::post(cppverify::result == 0) +{ + Counter c; + c.n = 0; + c.limit = 1; + Counter *q = same(&c); + return 0; +} +// CHECK-DAG: [[@LINE-3]]:3: Unresolved: returned [backend=z3] [reason=construct.unsupported] (the verifier cannot model a construct here: the address of local or dynamic storage is passed to same, which may keep or free it, or has no verified body) + +// A copy of the address is the address, and so is one passed on. +void keeps_copy(Node &n) + cppverify::modifies(n.self) +{ + Node *q = &n; + n.self = q; +} + +int copied() + cppverify::post(cppverify::result == 0) +{ + Node n; + n.self = nullptr; + n.v = 0; + keeps_copy(n); + return 0; +} +// CHECK-DAG: [[@LINE-3]]:3: Unresolved: copied [backend=z3] [reason=construct.unsupported] (the verifier cannot model a construct here: the address of local or dynamic storage is passed to keeps_copy, which may keep or free it, or has no verified body) + +void forwards(Node &n) + cppverify::modifies(n.self) +{ + selfref(n); +} + +int forwarded() + cppverify::post(cppverify::result == 0) +{ + Node n; + n.self = nullptr; + n.v = 0; + forwards(n); + return 0; +} +// CHECK-DAG: [[@LINE-3]]:3: Unresolved: forwarded [backend=z3] [reason=construct.unsupported] (the verifier cannot model a construct here: the address of local or dynamic storage is passed to forwards, which may keep or free it, or has no verified body) + +// An extent is not offered to a local: zero_two may write c and the record +// after it. +void zero_two(Counter *a, int n) + cppverify::pre(cppverify::valid(a, n) && 0 <= n && n <= 2) + cppverify::modifies(*a) +{ + for (int i = 0; i < n; i = i + 1) + cppverify::invariant(0 <= i && i <= n) + cppverify::decreases(n - i) + { + a[i].n = 0; + } +} + +int extent_two() + cppverify::post(cppverify::result == 0) +{ + Counter c; + c.n = 0; + c.limit = 1; + zero_two(&c, 2); + return 0; +} +// CHECK-DAG: [[@LINE-3]]:3: Unresolved: extent_two [backend=z3] [reason=construct.unsupported] (the verifier cannot model a construct here: the address of local or dynamic storage is passed to zero_two, which may keep or free it, or has no verified body) + +// A callee without a verified body, called directly or through another. +void opaque(Counter &c) + cppverify::modifies(c.n); + +void through_opaque(Counter &c) + cppverify::modifies(c.n) +{ + opaque(c); +} + +int nested_opaque() + cppverify::post(cppverify::result == 0) +{ + Counter c; + c.n = 0; + c.limit = 1; + through_opaque(c); + return 0; +} +// CHECK-DAG: [[@LINE-3]]:3: Unresolved: nested_opaque [backend=z3] [reason=construct.unsupported] (the verifier cannot model a construct here: the address of local or dynamic storage is passed to through_opaque, which may keep or free it, or has no verified body) + +int to_opaque() + cppverify::post(cppverify::result == 0) +{ + Counter c; + c.n = 0; + c.limit = 1; + opaque(c); + return 0; +} +// CHECK-DAG: [[@LINE-3]]:3: Unresolved: to_opaque [backend=z3] [reason=construct.unsupported] (the verifier cannot model a construct here: the address of local or dynamic storage is passed to opaque, which may keep or free it, or has no verified body) +#endif From fbf6564336c0e407ff910c3d8c4e810d3a164e9f Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 09:25:19 +0530 Subject: [PATCH 39/94] test: Expect a local record passed by reference to verify. --- clang/test/Verify/suite/record_references.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/clang/test/Verify/suite/record_references.cpp b/clang/test/Verify/suite/record_references.cpp index 87e778efcd..793013fc11 100644 --- a/clang/test/Verify/suite/record_references.cpp +++ b/clang/test/Verify/suite/record_references.cpp @@ -349,10 +349,10 @@ int by_reference(const Bounded &b) } // CHECK-DAG: error: verification failed: by_reference [{{.*}}::postcondition@[[@LINE-4]]:37] -// Not yet: a local passed to a callee by reference, whose non-escape is not -// established for records (B3a-4), a pointer formed from a local through a -// reference, a whole record read through one, a proof function's reference, -// and rvalue and volatile references. +// A local passed to a callee by reference is checked at the call (see +// local_record_arguments.cpp). Not yet: a pointer formed from a local through +// a reference, a whole record read through one, a proof function's +// reference, and rvalue and volatile references. int local_to_callee() cppverify::post(cppverify::result == 1) { @@ -362,7 +362,7 @@ int local_to_callee() bump(c); return c.n; } -// CHECK-DAG: [[@LINE-3]]:3: Unresolved: local_to_callee [backend=z3] [reason=construct.unsupported] (the verifier cannot model a construct here: the address of local or dynamic storage is passed to bump, which may keep, offset, or free it, or has no verified body) +// CHECK-DAG: Verified: local_to_callee [backend=z3] int local_address() cppverify::post(cppverify::result == 0) From 2e87fff8fb0f5a8a30e766d2c577ab0627e560f0 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 09:25:19 +0530 Subject: [PATCH 40/94] docs: Describe local records passed to callees, with a checked example. --- website/source/language/structs.rst | 105 ++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 7 deletions(-) diff --git a/website/source/language/structs.rst b/website/source/language/structs.rst index 6f82ceea6c..c3fdc86c43 100644 --- a/website/source/language/structs.rst +++ b/website/source/language/structs.rst @@ -259,15 +259,106 @@ may write, so ``p.right`` keeps its value. ``reset_count`` may write only footprint is every byte of the record, which only a footprint naming the record, or an object around it, contains. +Local records passed to callees +------------------------------- + +A local record may be passed to a reference parameter (``bump(c)``), and +the address of a local, of one of its fields, or of an element of a local +array (``&c``, ``&c.n``, ``&a[1]``) to a pointer parameter. The callee is +verified for an object it does not own, so every access it makes through +the parameter, at whatever field or element, is proved to lie in that +object. The call checks the rest: + +- the callee keeps no address of the local: it stores none in memory, + returns none, frees none, and passes one only to callees that keep none + themselves (recursion included). A callee without a verified body, or one + that may keep the address, makes the call ``Unresolved`` with reason + ``construct.unsupported`` (``the address of local or dynamic storage is + passed to f, which may keep or free it, or has no verified body``); +- the whole record lies in the live local; +- every field is initialized, since the callee reads its parameter's object + as initialized storage. C++ allows an out-parameter that is only written, + but the verifier does not model one yet: the call is ``Unresolved`` with + reason ``construct.unsupported``. + +The callee then writes only its footprints inside the local, by their bytes, +and every other local keeps its value. The caller's own +``cppverify::modifies`` need not name its locals. + +.. cppverify-example: unresolved half_initialized + +.. code-block:: cpp + + struct Counter { + int n; + int limit; + }; + + void bump(Counter &c) + cv::pre(c.n < c.limit) + cv::modifies(c.n) + cv::post(c.n == cv::old(c.n) + 1) + cv::post(c.limit == cv::old(c.limit)) + { + c.n = c.n + 1; + } + + void reset(Counter *c) + cv::pre(c != nullptr) + cv::modifies(*c) + cv::post(c->n == 0 && c->limit == 10) + { + c->n = 0; + c->limit = 10; + } + + void set_n(Counter &c) + cv::modifies(c.n) + cv::post(c.n == 1) + { + c.n = 1; + } + + int counted() + cv::post(cv::result == 11) + { + Counter c; + c.n = 0; + c.limit = 5; + bump(c); + int k = c.n; + reset(&c); + return k + c.limit; + } + + int half_initialized() + cv::post(cv::result == 1) + { + Counter c; + c.n = 0; + set_n(c); + return c.n; + } + +.. code-block:: text + + Verified: bump [backend=z3] + Verified: reset [backend=z3] + Verified: set_n [backend=z3] + Verified: counted [backend=z3] + counter.cpp:48:3: Unresolved: half_initialized [backend=z3] [reason=construct.unsupported] (the verifier cannot model a construct here: binding a reference or pointer parameter to storage that is not initialized, since the callee is verified as reading initialized storage) + +``half_initialized`` leaves ``c.limit`` uninitialized, and ``set_n`` would +read it as initialized. + +The address of a local is supported as a call argument only. Stored in a +pointer variable (``Counter *q = &c;``), or formed through a local reference +to a local (``&r.n``, ``r.buf``), it is still an error. Under +``--no-check-ub`` the callee's accesses are not proved to lie in its object, +so a call that passes a local record is ``construct.unsupported``. + Not yet supported: -- a local record passed to a reference or pointer parameter (``bump(c)`` with - ``Counter c;``): the verifier does not yet show that the callee keeps no - pointer to it, and the call is ``Unresolved`` with reason - ``construct.unsupported``. A local reference may bind a local record, and - reads and writes through it are checked against that object's lifetime and - initialization, but forming a pointer through such a reference (``&r.n``, - ``r.buf``) is an error; - reading a whole record through a reference, as through a pointer; - reference returns, rvalue and ``volatile`` references, and references in spec and proof functions. From d9595bbb1edaccaab7ee8900d77c05f7335028ec Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 09:25:20 +0530 Subject: [PATCH 41/94] docs: Extend reference arguments to local records and addresses in chapter 14. --- website/source/book/part-ii/ch14-pointers-frames-modifies.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/source/book/part-ii/ch14-pointers-frames-modifies.rst b/website/source/book/part-ii/ch14-pointers-frames-modifies.rst index e0c0cd1e5f..308aeffd70 100644 --- a/website/source/book/part-ii/ch14-pointers-frames-modifies.rst +++ b/website/source/book/part-ii/ch14-pointers-frames-modifies.rst @@ -54,7 +54,9 @@ is present. Reference formals can be forwarded from another reference, bound to a direct dereference such as ``set_value(*p, value)``, bound to a field or element of a local object or of a parameter's buffer (``set_value(pt.x, 7)``, -``set_value(a[1], 9)``), or passed an initialized ordinary scalar local. Local +``set_value(a[1], 9)``), or passed an initialized local, scalar or record, +as can the address of one to a pointer parameter (``inc(&x)``), when the +callee keeps no address of it (see :doc:`../../language/structs`). Local references may bind those same forms and chain: .. cppverify-example: with swap From 4f6c7e13c4ebf28f5527c36117455edb20f08a8d Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 09:25:20 +0530 Subject: [PATCH 42/94] docs: List local objects passed to callees as supported in the limitations. --- website/source/language/limitations.rst | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/website/source/language/limitations.rst b/website/source/language/limitations.rst index 70d3943bfa..7b2fb7dcc7 100644 --- a/website/source/language/limitations.rst +++ b/website/source/language/limitations.rst @@ -120,7 +120,9 @@ C++ feature boundaries reinterpretation. * - References - Scalar ``T&``/``const T&`` parameters on contracted executable free - functions, initialized ordinary scalar local actuals, local aliases for + functions, initialized local actuals (scalars and records, and + ``&x`` of one passed to a pointer parameter, when the callee keeps no + address of it), local aliases for boolean, integral, and enum referents, and bindings to a field or fixed array element of a promoted local object, or to a field or element of a parameter's buffer or of a record a pointer addresses @@ -130,8 +132,10 @@ C++ feature boundaries of the object it names (:doc:`structs`). - Conditional bindings, reference returns, rvalue references, temporaries/lifetime extension, reference members, and forwarding; a - local record passed to a reference parameter (``construct.unsupported``) - and a pointer formed through a local reference to a local record. + local not fully initialized passed to a callee + (``construct.unsupported``), the address of a local stored in a + variable, and a pointer formed through a local reference to a local + record. * - Arrays - Pointer-parameter indexing, quantified abstract buffers, fixed local arrays (including arrays of records and multidimensional arrays) @@ -293,8 +297,10 @@ order; heap-reading return values are materialized before teardown. Scalar references may bind supported fields and fixed-array elements of these promoted objects, and a local reference to a record may bind a promoted record. Conditional bindings, temporaries, reference returns, general raw -address-taking/array decay (through such a reference too), rvalue references, -and passing a promoted record to a callee by reference remain fail-closed. Addressable declarations inside loops and ``cppverify::old`` of local +address-taking/array decay (through such a reference too, and ``&x`` but as +a call argument), and rvalue references remain fail-closed. A promoted object +passed to a callee, by reference or ``&x``, keeps its lifetime: the callee +must keep no address of it, and every field must be initialized. Addressable declarations inside loops and ``cppverify::old`` of local objects/bindings are also rejected; outer automatic locals and loop-local aliases are supported. Pointer leaves may retain abstract pointer values, but storing a provenance-bearing local dynamic pointer fails closed until pointer provenance is From efa9ef16a5d9776d17611ff279fba4376bc3b123 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 09:25:21 +0530 Subject: [PATCH 43/94] docs: Note local objects passed to callees in the release notes. --- website/source/release-notes.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/website/source/release-notes.rst b/website/source/release-notes.rst index b731d2b6f0..507b9f1dd0 100644 --- a/website/source/release-notes.rst +++ b/website/source/release-notes.rst @@ -278,8 +278,18 @@ A reference to a record is now a pointer to it that is never null and never reseated: ``c.n`` and ``c.buf[i]`` are cells of the object, ``&c.n`` and ``c.buf`` point into it, ``cppverify::modifies(c)`` is the whole object, and a reference can be bound to ``*p``, ``p->left``, ``a[i]``, or another -reference. Passing a local record to a reference parameter is still -``construct.unsupported`` (:doc:`language/structs`). +reference (:doc:`language/structs`). + +**Local objects passed to callees.** A local record passed to a reference +parameter (``bump(c)``), and the address of any local passed to a pointer +parameter (``reset(&c)``, ``inc(&x)``, ``inc(&c.n)``), were +``construct.unsupported`` or errors (``raw address-of is unsupported until +lexical lifetime and escape effects are modeled``). They are verified now +when the callee keeps no address of the local, which the call checks: it +stores none in memory, returns none, frees none, and passes one only to +callees that keep none. The callee writes only its footprints in the local, +and every field of a record must be initialized, since the callee reads it as +initialized (:doc:`language/structs`). Running cpp-verify ~~~~~~~~~~~~~~~~~~ From 3b4e989e2c95c1468a71c82e00e1dfc5a867c5bf Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 09:25:22 +0530 Subject: [PATCH 44/94] docs: Describe local objects passed to callees in the design. --- docs/DESIGN.md | 49 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 17b800de43..1ee4485b3e 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1204,11 +1204,43 @@ void reset_left(Pair &p) and writes through it carry that object's lifetime and initialization checks, but a pointer formed through it would not, and is rejected (as `&local` is). -- Not yet: a local record passed to a reference or pointer parameter. The - non-escape scan that admits a local scalar actual does not cover records, - so the call is `Unresolved` (`construct.unsupported`). Also rejected: a - whole record read through a reference, reference returns, rvalue and - `volatile` references, and references in spec and proof functions. +- A local record may be passed to a reference parameter, and the address + of a local, a field, or an element (`&c`, `&c.n`, `&a[1]`) to a pointer + parameter: see 3d. Rejected: a whole record read through a reference, + reference returns, rvalue and `volatile` references, and references in + spec and proof functions. + +### 3d. Local objects passed to callees + +- A callee verified in the object model proves that every access through + its parameter, at any field or element, lies in the parameter's object (its + one object; its `valid` extent is not offered to a local). So a local can + be lent to it as long as no address of the local outlives the call, which + is a property of the callee's body: the call checks (`keepsNoAddress`) that + it stores no address derived from the parameter in memory, returns none, + frees none, and passes one only to callees that keep none, with a verified + body (not an external contract, a proof, or a function that allocates). + The addresses derived from the parameter are the variables assigned one, + closed to a fixpoint (copies, steps, subobject pointers, local references). + A scan that reaches itself again through recursion assumes the callee keeps + none, since an escape is a finite path to a statement that keeps one. An + address converted to an integer is no escape: no integer becomes a pointer + again. +- The generated validity clause of a reference or pointer parameter names + its first byte; for a record the call also checks that its last byte lies + in the same live allocation (an allocation's bytes are contiguous) and that + every leaf is initialized, since the callee reads it as initialized storage. + An out-parameter, which C++ allows uninitialized, is + `construct.unsupported`, as for a scalar. +- The callee's footprints inside the record are framed by their bytes + (`HeapFrame`), a region as the whole record and never only its first cell; + its writes need no footprint of the caller's, since the local is the + caller's own. A scalar local keeps the exact one-cell rule and its + stricter scan (direct accesses only), since its region is one cell. +- `&x` is admitted only as a call argument; stored in a variable it is still + rejected, as is a pointer formed through a local reference to a local. In + `--no-check-ub` nothing proves the callee's accesses inside its object, so + a record local passed to a callee is `construct.unsupported`. ### 4. `cppverify::modifies(...)` frame condition @@ -1296,9 +1328,10 @@ int compute() { ``` `swap(x, x)` from a verified function fails the call's `aliasing` check. -Binding a reference to a local is supported; taking a local's raw address -(`&x`) is not yet: lexical lifetimes are modeled, but where a pointer to a -local goes is not. +Binding a reference to a local is supported, and so is passing a local's +address to a callee that keeps none (3d); storing a local's raw address +(`int *q = &x;`) is not yet: lexical lifetimes are modeled, but where a +pointer variable holding it goes is not. ## Type Invariants From 90d335e2fc0f164924284a7b66d8ddb7738d62f3 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 09:25:22 +0530 Subject: [PATCH 45/94] docs: Describe the record scan and the call's checks in the architecture. --- docs/ARCHITECTURE.md | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 73a24a0946..87386d58f5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2375,6 +2375,32 @@ rejects offset/subscript access, pointer copies/rebinding, recursive scan cycles, borrowed pointer-result intermediates without an owned summary, ghost access, proof/external contracts, and deallocation. +A record in local or dynamic storage takes a second scan, for a parameter in +`VFunction::RecordParams` (a pointer or reference to a record, with its +layout's identity) of a callee in the object model, whose own checks prove +that every access through the parameter lies in its object. +`keepsNoAddress` (`Transform/Passivize.cpp`) closes the parameter's aliases +over the pointer variables assigned an address derived from one, to a +fixpoint, and rejects a store of such an address, a return of one, a free, a +call that passes one to a callee that keeps one (scanned the same way, its +extent allowed, since the function passing it proved the extent inside its +own object) or that returns a pointer, and any callee without a verified +body; re-entering a scan through recursion assumes it holds. Offsets, steps, +comparisons, conversions to integers, spec arguments, and ghost reads are +admitted. At the call `emitRecordActualChecks` asserts the validity of the +record's last byte (with the generated clause's first, the record lies in +one live allocation) and the initialization of every leaf of the callee's +layout, enumerated (an automatic object has at most 256 bytes), as an +`unsupported` obligation with the out-parameter note. `havocFootprints` +frames a footprint rooted at such a parameter by its bytes through +`footprintBytes`, never as the exact first cell the scalar rule uses. The +frame check of the caller accepts the callee's footprints in its own +allocations; its empty-range alternative was joined to them (it replaced +them before, so a range in the caller's own object failed `frame`). +`ASTConverter::convertExecCallArg` lowers `&place` rooted at a promoted local +(not an array) passed to a pointer parameter as a reference binding: the +address with its provenance, the access condition, and the binding check. + A pointer-returning callee may return a direct dynamic formal, null, or a conditional selection of direct dynamic formals. `VCallStmt` carries a `ResultProvenanceTarget` alongside `ResultTarget`; passivization freshens both @@ -2419,8 +2445,9 @@ order, after materializing any heap-reading return value. Scalar references may bind fields and fixed-array elements with access-local nested bounds, and a local reference to a record may bind a promoted record, its provenance companion carried like a scalar reference's. General escape, raw array -decay/address-taking (through such a reference too), passing a promoted record -to a callee, and addressable declarations inside loops remain fail-closed; so does the decay of an array +decay/address-taking (through such a reference too, and `&x` other than as a +call argument, above), and addressable declarations inside loops remain +fail-closed; so does the decay of an array member through a pointer (`ASTConverter::arrayDecayError` names which). Automatic metadata transitions are private and a modular caller may frame them away. From 95908fc5b9a1a96de97b4c6d3423155c1835052f Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:54:51 +0530 Subject: [PATCH 46/94] verify: Record the parameters for the entry values of pointer fields with extents. --- clang/lib/Verify/IR/VStmt.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/clang/lib/Verify/IR/VStmt.h b/clang/lib/Verify/IR/VStmt.h index 3164896888..380b5f31b5 100644 --- a/clang/lib/Verify/IR/VStmt.h +++ b/clang/lib/Verify/IR/VStmt.h @@ -427,6 +427,11 @@ struct VFunction { /// record's entry in Layouts: an object whose leaves lie at offsets inside /// it. std::map RecordParams; + /// Parameters for the entry values of pointer fields whose extents the + /// preconditions state (valid(s->data, s->len)), each with its field as an + /// expression over the function's parameters, which a call evaluates at + /// its arguments. They follow the declared parameters in Params. + std::vector>> FieldParams; /// Pointer and reference parameters to const objects: the function /// writes nothing through them. std::set ConstAddressParams; From d2deea177983fd4c13001c53def6564f72a7f4b3 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:54:52 +0530 Subject: [PATCH 47/94] verify: Clone a function's field parameters. --- clang/lib/Verify/IR/VStmt.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clang/lib/Verify/IR/VStmt.cpp b/clang/lib/Verify/IR/VStmt.cpp index 90348d6bc9..be8f995c66 100644 --- a/clang/lib/Verify/IR/VStmt.cpp +++ b/clang/lib/Verify/IR/VStmt.cpp @@ -199,6 +199,8 @@ VFunction verify::cloneVFunction(const VFunction &Fn) { Out.SourceVariables = Fn.SourceVariables; Out.ScalarReferenceParams = Fn.ScalarReferenceParams; Out.RecordParams = Fn.RecordParams; + for (const auto &[Name, Field] : Fn.FieldParams) + Out.FieldParams.emplace_back(Name, cloneVExpr(Field.get())); Out.ConstAddressParams = Fn.ConstAddressParams; Out.BoundedPointers = Fn.BoundedPointers; Out.ReturnFields = Fn.ReturnFields; From 676ba99fda214c6cb2931cdd306fa11031564110 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:54:52 +0530 Subject: [PATCH 48/94] verify: Keep a function's field parameters through loop unrolling. --- clang/lib/Verify/Transform/LoopUnroll.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clang/lib/Verify/Transform/LoopUnroll.cpp b/clang/lib/Verify/Transform/LoopUnroll.cpp index c22a67864d..ea32b88502 100644 --- a/clang/lib/Verify/Transform/LoopUnroll.cpp +++ b/clang/lib/Verify/Transform/LoopUnroll.cpp @@ -257,6 +257,8 @@ VFunction LoopUnroller::unroll(const VFunction &Fn, unsigned K) { Out.SourceVariables = Fn.SourceVariables; Out.ScalarReferenceParams = Fn.ScalarReferenceParams; Out.RecordParams = Fn.RecordParams; + for (const auto &[Name, Field] : Fn.FieldParams) + Out.FieldParams.emplace_back(Name, cloneVExpr(Field.get())); Out.ConstAddressParams = Fn.ConstAddressParams; Out.BoundedPointers = Fn.BoundedPointers; Out.ReturnFields = Fn.ReturnFields; From 62811c3f435d4936156cf04112f0b480cdabf8fb Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:54:53 +0530 Subject: [PATCH 49/94] verify: Declare the field extents a function's preconditions state. --- clang/lib/Verify/Frontend/ASTConverter.h | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/clang/lib/Verify/Frontend/ASTConverter.h b/clang/lib/Verify/Frontend/ASTConverter.h index e1eb288972..b4695a0f3a 100644 --- a/clang/lib/Verify/Frontend/ASTConverter.h +++ b/clang/lib/Verify/Frontend/ASTConverter.h @@ -85,6 +85,10 @@ class ASTConverter { std::vector> AutomaticScopeStack; std::vector ActiveAutomaticLocals; std::map LocalReferenceProvenanceVariables; + /// The first arguments of extent markers that name a pointer field of an + /// address parameter, by the synthetic parameter of the field's entry value + /// (VFunction::FieldParams). + std::map FieldExtentParameters; std::set GhostLocals; unsigned DynamicProvenanceId = 0; unsigned DynamicPointerAssignmentId = 0; @@ -336,6 +340,19 @@ class ASTConverter { /// Populate `Fn.Layouts` with canonical, deduplicated layouts for the record /// and constant-array types encountered in `FD`'s signature and body. void collectLayouts(const FunctionDecl *FD, VFunction &Fn); + /// An address the default separation covers: a pointer or reference + /// parameter, or the entry value of a pointer field with an extent. + struct AddressEntry { + std::string Name; + QualType Addressed; + bool Mutable; + }; + /// Gives each pointer field of an address parameter that an extent marker + /// of FCI's preconditions names a synthetic parameter for its entry value + /// (VFunction::FieldParams), and returns them for the default + /// separation. + std::vector collectFieldExtents(const FunctionContractInfo &FCI, + VFunction &Fn); void registerLayoutType(QualType QT, VFunction &Fn); void buildRecordLayout(QualType QT, VFunction &Fn); void buildArrayLayout(QualType QT, const ConstantArrayType *CAT, From e14c7cbcc7b38dc160f7b4e2df612c41fa3e604d Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:54:53 +0530 Subject: [PATCH 50/94] verify: Give each pointer field with an extent a parameter for its entry value, separated like the others. --- clang/lib/Verify/Frontend/ASTConverter.cpp | 163 ++++++++++++++++++--- 1 file changed, 141 insertions(+), 22 deletions(-) diff --git a/clang/lib/Verify/Frontend/ASTConverter.cpp b/clang/lib/Verify/Frontend/ASTConverter.cpp index ba762b436a..cce5f44590 100644 --- a/clang/lib/Verify/Frontend/ASTConverter.cpp +++ b/clang/lib/Verify/Frontend/ASTConverter.cpp @@ -1318,6 +1318,7 @@ void ASTConverter::beginInitializationTracking(const FunctionDecl *FD) { AutomaticScopeStack.clear(); ActiveAutomaticLocals.clear(); LocalReferenceProvenanceVariables.clear(); + FieldExtentParameters.clear(); GhostLocals.clear(); DynamicProvenanceId = 0; DynamicPointerAssignmentId = 0; @@ -2616,6 +2617,106 @@ bool ASTConverter::exceedsNestingLimit(const FunctionDecl *FD, return false; } +/// The address parameter and the fields of a pointer field of the object it +/// addresses (s->data, s.data for a reference s, s->inner.data, (*s).data), +/// whose pointee is a complete object type. +static std::optional< + std::pair>> +pointerFieldOfParameter(const Expr *E) { + const auto *Outer = dyn_cast(E->IgnoreParenImpCasts()); + if (!Outer || !Outer->getType()->isPointerType() || + Outer->getType()->getPointeeType()->isIncompleteType() || + Outer->getType()->getPointeeType()->isVoidType()) + return std::nullopt; + auto Parameter = [](const Expr *Base) -> const ParmVarDecl * { + const auto *Ref = dyn_cast(Base->IgnoreParenImpCasts()); + return Ref ? dyn_cast(Ref->getDecl()) : nullptr; + }; + std::vector Fields; + const Expr *Object = Outer; + while (const auto *M = dyn_cast(Object->IgnoreParenImpCasts())) { + const auto *Field = dyn_cast(M->getMemberDecl()); + if (!Field) + return std::nullopt; + Fields.insert(Fields.begin(), Field); + if (M->isArrow()) { + // p->f: the object p, a pointer parameter, addresses. + const ParmVarDecl *P = Parameter(M->getBase()); + if (!P || !P->getType()->isPointerType()) + return std::nullopt; + return std::make_pair(P, std::move(Fields)); + } + Object = M->getBase(); + } + Object = Object->IgnoreParenImpCasts(); + // (*p).f, or r.f for a reference parameter r. + if (const auto *U = dyn_cast(Object); + U && U->getOpcode() == UO_Deref) { + const ParmVarDecl *P = Parameter(U->getSubExpr()); + if (!P || !P->getType()->isPointerType()) + return std::nullopt; + return std::make_pair(P, std::move(Fields)); + } + const ParmVarDecl *P = Parameter(Object); + if (!P || !P->getType()->isLValueReferenceType()) + return std::nullopt; + return std::make_pair(P, std::move(Fields)); +} + +std::vector +ASTConverter::collectFieldExtents(const FunctionContractInfo &FCI, + VFunction &Fn) { + struct MarkerFinder : RecursiveASTVisitor { + std::vector Pointers; + bool VisitCallExpr(CallExpr *Call) { + const FunctionDecl *Callee = Call->getDirectCallee(); + if (Callee && Callee->getDeclName().isIdentifier() && + Callee->getName() == "valid" && Call->getNumArgs() == 2) + Pointers.push_back(Call->getArg(0)); + return true; + } + } Finder; + for (const Expr *Pre : FCI.Preconditions) + Finder.TraverseStmt(const_cast(Pre)); + std::vector Out; + for (const Expr *Pointer : Finder.Pointers) { + auto Field = pointerFieldOfParameter(Pointer); + if (!Field) + continue; + const auto &[Param, Fields] = *Field; + std::string Name = valueName(Param); + std::string Display = Param->getNameAsString(); + for (size_t I = 0; I != Fields.size(); ++I) { + Name += "->" + Fields[I]->getNameAsString(); + Display += (I == 0 && Param->getType()->isPointerType() ? "->" : ".") + + Fields[I]->getNameAsString(); + } + const Expr *Key = Pointer->IgnoreParenImpCasts(); + if (llvm::any_of(Fn.FieldParams, + [&](const auto &Entry) { return Entry.first == Name; })) { + FieldExtentParameters[Key] = Name; + continue; + } + bool SavedContract = InContractExpression; + InContractExpression = true; + std::unique_ptr Value = convertExpr(Pointer); + InContractExpression = SavedContract; + if (!Value) + continue; + // The field's own type: the marker's parameter converts it to a pointer + // to const. + VType Ty = VType::fromQualType(Key->getType(), IntMode, Ctx); + Fn.Params.emplace_back(Name, Ty); + Fn.SourceVariables[Name] = {Display, Ty, Pointer->getBeginLoc(), + Pointer->getEndLoc()}; + Fn.FieldParams.emplace_back(Name, std::move(Value)); + FieldExtentParameters[Key] = Name; + QualType Addressed = Key->getType()->getPointeeType(); + Out.push_back({Name, Addressed, !Addressed.isConstQualified()}); + } + return Out; +} + std::unique_ptr ASTConverter::convertFunction(const FunctionDecl *FD) { llvm::SaveAndRestore At(CurrentRange, SourceRange(FD->getLocation())); @@ -2750,6 +2851,18 @@ ASTConverter::convertFunction(const FunctionDecl *FD) { AddressParams.push_back(P); recordSourceVariable(P); } + // The entry value of a pointer field whose extent a precondition states + // (valid(s->data, s->len)) is an input of the function, as a parameter's + // value is: a synthetic parameter after the declared ones, bound to the + // field at entry and by each call at its arguments, takes the extent, its + // validity, and the default separation. + std::vector Addresses; + for (const ParmVarDecl *P : AddressParams) + Addresses.push_back( + {valueName(P), addressedType(P), isMutableAddressParam(P)}); + if (FCI && !Fn->IsSpec) + for (AddressEntry &Field : collectFieldExtents(*FCI, *Fn)) + Addresses.push_back(std::move(Field)); if (Fn->ReturnType.Kind == VTypeKind::Struct) { for (const auto &[Field, Type] : Fn->ReturnFields) Fn->SourceVariables["result." + Field] = { @@ -2922,39 +3035,38 @@ ASTConverter::convertFunction(const FunctionDecl *FD) { } // A mutable address parameter has exclusive access to its complete object. - for (unsigned I = 0; I < AddressParams.size(); ++I) { - for (unsigned J = I + 1; J < AddressParams.size(); ++J) { - if (!isMutableAddressParam(AddressParams[I]) && - !isMutableAddressParam(AddressParams[J])) + for (unsigned I = 0; I < Addresses.size(); ++I) { + for (unsigned J = I + 1; J < Addresses.size(); ++J) { + if (!Addresses[I].Mutable && !Addresses[J].Mutable) continue; - const bool MayAlias = aliasesListed(*FCI, valueName(AddressParams[I]), - valueName(AddressParams[J])); - QualType PointeeI = addressedType(AddressParams[I]).getUnqualifiedType(); - QualType PointeeJ = addressedType(AddressParams[J]).getUnqualifiedType(); + const bool MayAlias = + aliasesListed(*FCI, Addresses[I].Name, Addresses[J].Name); + QualType PointeeI = Addresses[I].Addressed.getUnqualifiedType(); + QualType PointeeJ = Addresses[J].Addressed.getUnqualifiedType(); if (MayAlias && !Ctx.hasSameType(PointeeI, PointeeJ)) { error(Fn->Name + ": aliases between different pointee types are unsupported"); continue; } - auto Ptr = [&](const ParmVarDecl *P) { - return std::make_unique(valueName(P), VType::makePtr(), + auto Ptr = [&](const AddressEntry &A) { + return std::make_unique(A.Name, VType::makePtr(), SourceLocation()); }; auto Null = [] { return std::make_unique(0, VType::makePtr(), SourceLocation()); }; - auto ANull = std::make_unique( - VBinOp::Eq, Ptr(AddressParams[I]), Null(), VType::makeBool(), - SourceLocation()); - auto BNull = std::make_unique( - VBinOp::Eq, Ptr(AddressParams[J]), Null(), VType::makeBool(), - SourceLocation()); + auto ANull = + std::make_unique(VBinOp::Eq, Ptr(Addresses[I]), Null(), + VType::makeBool(), SourceLocation()); + auto BNull = + std::make_unique(VBinOp::Eq, Ptr(Addresses[J]), Null(), + VType::makeBool(), SourceLocation()); auto EitherNull = std::make_unique( VBinOp::Or, std::move(ANull), std::move(BNull), VType::makeBool(), SourceLocation()); - auto End = [&](const ParmVarDecl *P, QualType Pointee) { + auto End = [&](const AddressEntry &P, QualType Pointee) { auto Size = std::make_unique( std::to_string(Ctx.getTypeSizeInChars(Pointee).getQuantity()), VType::makePtr(), SourceLocation()); @@ -2963,18 +3075,18 @@ ASTConverter::convertFunction(const FunctionDecl *FD) { SourceLocation()); }; auto IBeforeJ = std::make_unique( - VBinOp::Le, End(AddressParams[I], PointeeI), Ptr(AddressParams[J]), + VBinOp::Le, End(Addresses[I], PointeeI), Ptr(Addresses[J]), VType::makeBool(), SourceLocation()); auto JBeforeI = std::make_unique( - VBinOp::Le, End(AddressParams[J], PointeeJ), Ptr(AddressParams[I]), + VBinOp::Le, End(Addresses[J], PointeeJ), Ptr(Addresses[I]), VType::makeBool(), SourceLocation()); std::unique_ptr Relation = std::make_unique( VBinOp::Or, std::move(IBeforeJ), std::move(JBeforeI), VType::makeBool(), SourceLocation()); if (MayAlias) { auto Same = std::make_unique( - VBinOp::Eq, Ptr(AddressParams[I]), Ptr(AddressParams[J]), - VType::makeBool(), SourceLocation()); + VBinOp::Eq, Ptr(Addresses[I]), Ptr(Addresses[J]), VType::makeBool(), + SourceLocation()); Relation = std::make_unique( VBinOp::Or, std::move(Same), std::move(Relation), VType::makeBool(), SourceLocation()); @@ -2987,7 +3099,7 @@ ASTConverter::convertFunction(const FunctionDecl *FD) { ProofObligationKind::Aliasing); if (!MayAlias) Fn->DisjointAddresses.push_back( - {valueName(AddressParams[I]), valueName(AddressParams[J]), + {Addresses[I].Name, Addresses[J].Name, static_cast( Ctx.getTypeSizeInChars(PointeeI).getQuantity()), static_cast( @@ -4128,6 +4240,13 @@ std::unique_ptr ASTConverter::convertExpr(const Expr *E) { llvm::SaveAndRestore At(CurrentRange, E && E->getSourceRange().isValid() ? E->getSourceRange() : CurrentRange); + // An extent marker's pointer field is its entry value's parameter. + if (E && !FieldExtentParameters.empty()) + if (auto It = FieldExtentParameters.find(E->IgnoreParenImpCasts()); + It != FieldExtentParameters.end()) + return std::make_unique( + It->second, VType::fromQualType(E->getType(), IntMode, Ctx), + E->getExprLoc()); auto Result = convertExprImpl(E); if (Result && E) Result->EndLoc = E->getEndLoc(); From 24a857d41b358c4baf55c710bdbb6ead9dea12ce Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:54:54 +0530 Subject: [PATCH 51/94] verify: Bind a field parameter to its field at entry and at every call. --- clang/lib/Verify/Transform/Passivize.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/clang/lib/Verify/Transform/Passivize.cpp b/clang/lib/Verify/Transform/Passivize.cpp index e5851c138b..668f007ed4 100644 --- a/clang/lib/Verify/Transform/Passivize.cpp +++ b/clang/lib/Verify/Transform/Passivize.cpp @@ -4789,6 +4789,18 @@ class PassivizerImpl { P.EntryAssumes.push_back(makeNot(std::move(NullLive), SourceLocation())); P.EntryOrigins.insert(P.EntryOrigins.end(), 2, "null"); } + // A field parameter is its field's value at entry, by definition: a fact + // without definedness, since it says nothing where the field cannot be + // read, and which a call needs not establish, as it binds the parameter + // to the field itself. + for (const auto &[Name, Field] : Fn.FieldParams) { + CloneCtx PCtx{Renames, OldState, false}; + VVarExpr Parameter(Name, Field->Ty, Field->Loc); + P.EntryAssumes.push_back(makeEq(cloneExpr(&Parameter, PCtx), + cloneExpr(Field.get(), PCtx), + Field->Loc)); + P.EntryOrigins.push_back("field"); + } if (Fn.ObjectModel) { CloneCtx PCtx{Renames, OldState, false}; for (const AbstractObject &Object : abstractObjects(Fn)) { @@ -5279,6 +5291,9 @@ class PassivizerImpl { std::map> ParamMap; for (unsigned I = 0; I < Callee->Params.size() && I < C.Args.size(); ++I) ParamMap[Callee->Params[I].first] = cloneExpr(C.Args[I].get(), Ctx); + // A field's entry value is the field at the arguments, before the call. + for (const auto &[Name, Field] : Callee->FieldParams) + ParamMap[Name] = substParams(Field.get(), ParamMap, Ctx, EntryHeap); std::set DynamicParams; std::set> ActiveScans; for (const auto &Param : Callee->Params) @@ -5367,6 +5382,11 @@ class PassivizerImpl { for (unsigned I = 0; I < Callee->Params.size() && I < C.Args.size(); ++I) if (Callee->Params[I].first == Extent.Base) Arg = C.Args[I].get(); + // A field's value, read from memory, may address any caller object. + if (!Arg && llvm::any_of(Callee->FieldParams, [&](const auto &Entry) { + return Entry.first == Extent.Base; + })) + Arg = Actual->second.get(); auto Bytes = scaledExtent(Length.get(), Extent.PointerType.PointeeSizeBytes, C.Loc); auto NonNegative = asPointerOffset(Length.get()); From cc9bbcba5fa4c6baa9ff0bb1207f59ea56c0bb49 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:54:54 +0530 Subject: [PATCH 52/94] verify: Separate a pair with an extent over its extents alone. --- clang/lib/Verify/Transform/UBChecks.cpp | 60 +++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/clang/lib/Verify/Transform/UBChecks.cpp b/clang/lib/Verify/Transform/UBChecks.cpp index aeddc21e07..7324cc217b 100644 --- a/clang/lib/Verify/Transform/UBChecks.cpp +++ b/clang/lib/Verify/Transform/UBChecks.cpp @@ -493,6 +493,65 @@ struct UBInstrumenter { VType::makeBool(), Loc); } + // A pair with a declared extent is separated over its extents + // (appendExtentSeparation), not over one object each: where the extent is + // empty its pointer reaches nothing, so the one-object separation said + // only that p's first element, which no access reads, lies elsewhere, and + // a caller with an empty extent (n == 0) into another object failed it. + void dropSingleObjectSeparation(VFunction &Fn) const { + auto IsNullTest = [](const VExpr *E, std::string &Name) { + if (!E || E->K != VExpr::BinOp) + return false; + const auto *Eq = static_cast(E); + if (Eq->Op != VBinOp::Eq || Eq->Lhs->K != VExpr::Var) + return false; + Name = static_cast(Eq->Lhs.get())->Name; + return true; + }; + auto PairOf = [&](const VExpr *Pre) + -> std::optional> { + if (Pre->K != VExpr::BinOp || + static_cast(Pre)->Op != VBinOp::Or) + return std::nullopt; + const VExpr *Nulls = static_cast(Pre)->Lhs.get(); + if (Nulls->K != VExpr::BinOp || + static_cast(Nulls)->Op != VBinOp::Or) + return std::nullopt; + std::string A, B; + if (!IsNullTest(static_cast(Nulls)->Lhs.get(), A) || + !IsNullTest(static_cast(Nulls)->Rhs.get(), B)) + return std::nullopt; + return std::make_pair(A, B); + }; + std::vector> Kept; + std::vector KeptKinds; + for (size_t I = 0; I != Fn.Preconditions.size(); ++I) { + bool Drop = false; + if (I >= Fn.ExplicitPreconditionCount && + preconditionKind(Fn, I) == ProofObligationKind::Aliasing) + if (auto Pair = PairOf(Fn.Preconditions[I].get())) + Drop = llvm::any_of( + Fn.DisjointAddresses, [&](const VDisjointAddresses &D) { + return ((D.First == Pair->first && D.Second == Pair->second) || + (D.First == Pair->second && D.Second == Pair->first)) && + (ValidLen.count(D.First) || ValidLen.count(D.Second)); + }); + if (Drop) + continue; + Kept.push_back(std::move(Fn.Preconditions[I])); + KeptKinds.push_back(preconditionKind(Fn, I)); + } + Fn.Preconditions.clear(); + Fn.PreconditionKinds.clear(); + const unsigned Explicit = Fn.ExplicitPreconditionCount; + for (size_t I = 0; I != Kept.size(); ++I) { + if (I < Explicit) + Fn.Preconditions.push_back(std::move(Kept[I])); + else + addPrecondition(Fn, std::move(Kept[I]), KeptKinds[I]); + } + } + // A parameter with a declared extent addresses that extent, not a single // object: its validity is the extent's to state. void dropSingleObjectValidity(VFunction &Fn) const { @@ -1353,6 +1412,7 @@ std::optional verify::instrumentUBChecks(VFunction &Fn) { if (auto Misplaced = misplacedValidMarker(Fn)) return Misplaced; UB.appendValidSemantics(Fn); + UB.dropSingleObjectSeparation(Fn); UB.appendExtentSeparation(Fn); if (!Fn.IsProof) { Fn.ObjectModel = true; From 9f72e43b9ab77138d854e54b28efeaf716b9927a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:54:55 +0530 Subject: [PATCH 53/94] test: Check extents of pointer fields. --- .../Verify/suite/pointer_field_extents.cpp | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 clang/test/Verify/suite/pointer_field_extents.cpp diff --git a/clang/test/Verify/suite/pointer_field_extents.cpp b/clang/test/Verify/suite/pointer_field_extents.cpp new file mode 100644 index 0000000000..eae92770c9 --- /dev/null +++ b/clang/test/Verify/suite/pointer_field_extents.cpp @@ -0,0 +1,259 @@ +// RUN: not %cpp-verify --jobs=1 %s -- 2>&1 | FileCheck %s +// RUN: not %cpp-verify --jobs=1 --backend=bmc --unroll=3 %s -- 2>&1 \ +// RUN: | FileCheck %s --check-prefix=BMC +// RUN: %cpp-verify --lower-only --dump-ir=1 %s -- -DLOWER 2>&1 \ +// RUN: | FileCheck %s --check-prefix=IR +// +// An extent of a pointer field, cppverify::valid(s->data, s->len), describes +// the buffer the field addresses at entry. That entry value is an input of the +// function, as a parameter's value is: a synthetic parameter after the +// declared ones, the field's value at entry by definition, which a call binds +// to the field at its arguments. It takes the extent and its validity, and the +// default separation: a buffer through a pointer to non-const is disjoint from +// the other address parameters' objects, its own record's included. + +struct Buf { + int *data; + int len; +}; + +struct View { + const int *data; + int len; +}; + +struct Pair { + Buf in; + Buf out; +}; + +void zero(Buf &b) + cppverify::pre(cppverify::valid(b.data, b.len) && 0 <= b.len) + cppverify::modifies(b.data[0 : b.len]) + cppverify::post(cppverify::forall(k, 0, b.len, b.data[k] == 0)) +{ + for (int i = 0; i < b.len; i = i + 1) + cppverify::invariant(0 <= i && i <= b.len) + cppverify::invariant(cppverify::forall(k, 0, i, b.data[k] == 0)) + cppverify::modifies(b.data[0 : b.len]) + cppverify::decreases(b.len - i) + { + b.data[i] = 0; + } +} +// CHECK-DAG: Verified: zero [backend=z3] +// BMC-DAG: BoundedSafe: zero [backend=bmc, bound=3] +// IR-LABEL: fn zero +// IR: param b +// IR-NEXT: param b->data +// IR-NEXT: pre + +#ifndef LOWER +int count(const View *v, int x) + cppverify::pre(v != nullptr && cppverify::valid(v->data, v->len)) + cppverify::pre(0 <= v->len && v->len <= 100) + cppverify::post(0 <= cppverify::result && cppverify::result <= v->len) +{ + int n = 0; + for (int i = 0; i < v->len; i = i + 1) + cppverify::invariant(0 <= i && i <= v->len && 0 <= n && n <= i) + cppverify::decreases(v->len - i) + { + if (v->data[i] == x) + n = n + 1; + } + return n; +} +// CHECK-DAG: Verified: count [backend=z3] + +int past(const View *v) + cppverify::pre(v != nullptr && cppverify::valid(v->data, v->len)) + cppverify::pre(v->len >= 0) +{ + return v->data[v->len]; +} +// CHECK-DAG: error: verification failed: past [{{.*}}::bounds@[[@LINE-2]]:10] (counterexample: v->data [ssa=v->data_0] [type=pointer] = + +// Without an extent the field's buffer is no object of the function's. +void unknown(Buf &b) + cppverify::pre(b.len >= 1) + cppverify::modifies(b.data[0 : 1]) +{ + b.data[0] = 1; +} +// CHECK-DAG: error: verification failed: unknown [{{.*}}::bounds@[[@LINE-2]]:3] + +// The buffer is disjoint from its record, so the length keeps its value. +int keeps_len(Buf &b) + cppverify::pre(cppverify::valid(b.data, b.len) && 1 <= b.len && b.len < 100) + cppverify::modifies(b.data[0 : b.len]) + cppverify::post(cppverify::result == cppverify::old(b.len)) +{ + b.data[0] = 5; + return b.len; +} +// CHECK-DAG: Verified: keeps_len [backend=z3] + +// A field of a nested record, through a pointer. +void zero_out(Pair *p) + cppverify::pre(p != nullptr && cppverify::valid(p->out.data, p->out.len)) + cppverify::pre(0 <= p->out.len) + cppverify::modifies(p->out.data[0 : p->out.len]) + cppverify::post(p->out.len == cppverify::old(p->out.len)) +{ + zero(p->out); +} +// CHECK-DAG: Verified: zero_out [backend=z3] + +void zero_out_ref(Pair &p) + cppverify::pre(cppverify::valid(p.out.data, p.out.len) && 0 <= p.out.len) + cppverify::modifies(p.out.data[0 : p.out.len]) + cppverify::post(p.out.len == cppverify::old(p.out.len)) +{ + zero(p.out); +} +// CHECK-DAG: Verified: zero_out_ref [backend=z3] + +// At a call the field's value at the arguments has the extent: the +// caller's own field extent, or its own parameter's. +void zero_twice(Buf &b) + cppverify::pre(cppverify::valid(b.data, b.len) && 0 <= b.len) + cppverify::modifies(b.data[0 : b.len]) + cppverify::post(cppverify::forall(k, 0, b.len, b.data[k] == 0)) +{ + zero(b); + zero(b); +} +// CHECK-DAG: Verified: zero_twice [backend=z3] + +void from_param(int *p, int n) + cppverify::pre(cppverify::valid(p, n) && 0 <= n) + cppverify::modifies(p[0 : n]) + cppverify::post(cppverify::forall(k, 0, n, p[k] == 0)) +{ + Buf b; + b.data = p; + b.len = n; + zero(b); +} +// CHECK-DAG: Verified: from_param [backend=z3] + +void too_long(int *p, int n) + cppverify::pre(cppverify::valid(p, n) && 0 <= n && n < 100) + cppverify::modifies(p[0 : n]) +{ + Buf b; + b.data = p; + b.len = n + 1; + zero(b); +} +// CHECK-DAG: error: verification failed: too_long [{{.*}}::bounds@[[@LINE-2]]:3] +// BMC-DAG: error: verification failed: too_long [{{.*}}::bounds@[[@LINE-3]]:3] + +// The extent is the field's at entry: a field given another pointer +// addresses that pointer's object. +void reseat(Buf &b, int *other) + cppverify::pre(cppverify::valid(b.data, b.len) && 1 <= b.len) + cppverify::pre(other != nullptr) + cppverify::modifies(b.data, *other) +{ + b.data = other; + b.data[0] = 1; +} +// CHECK-DAG: Verified: reseat [backend=z3] + +void reseat_past(Buf &b, int *other) + cppverify::pre(cppverify::valid(b.data, b.len) && 2 <= b.len) + cppverify::pre(other != nullptr) + cppverify::modifies(b.data, *other) +{ + b.data = other; + b.data[1] = 1; +} +// CHECK-DAG: error: verification failed: reseat_past [{{.*}}::bounds@[[@LINE-2]]:3] + +// Two mutable buffers are disjoint by default, as two mutable parameters +// are; two read-only ones may be one. +void copy(Buf &dst, const Buf &src) + cppverify::pre(cppverify::valid(dst.data, dst.len)) + cppverify::pre(cppverify::valid(src.data, src.len)) + cppverify::pre(0 <= dst.len && dst.len <= src.len) + cppverify::modifies(dst.data[0 : dst.len]) + cppverify::post(cppverify::forall(k, 0, dst.len, dst.data[k] == src.data[k])) +{ + for (int i = 0; i < dst.len; i = i + 1) + cppverify::invariant(0 <= i && i <= dst.len) + cppverify::invariant(cppverify::forall(k, 0, i, dst.data[k] == src.data[k])) + cppverify::modifies(dst.data[0 : dst.len]) + cppverify::decreases(dst.len - i) + { + dst.data[i] = src.data[i]; + } +} +// CHECK-DAG: Verified: copy [backend=z3] + +void self_copy(Buf &b) + cppverify::pre(cppverify::valid(b.data, b.len) && 0 <= b.len) + cppverify::modifies(b.data[0 : b.len]) +{ + copy(b, b); +} +// CHECK-DAG: error: verification failed: self_copy [{{.*}}::aliasing@[[@LINE-2]]:3] (counterexample: b.data [ssa=b->data_0] [type=pointer] = + +int first_of_both(const View &a, const View &b) + cppverify::pre(cppverify::valid(a.data, a.len) && cppverify::valid(b.data, b.len)) + cppverify::pre(a.len >= 1 && b.len >= 1) + cppverify::pre(-1000 < a.data[0] && a.data[0] < 1000) + cppverify::pre(-1000 < b.data[0] && b.data[0] < 1000) + cppverify::post(cppverify::result == a.data[0] + b.data[0]) +{ + return a.data[0] + b.data[0]; +} + +int first_twice(const View &a) + cppverify::pre(cppverify::valid(a.data, a.len) && a.len >= 1) + cppverify::pre(-1000 < a.data[0] && a.data[0] < 1000) + cppverify::post(cppverify::result == 2 * a.data[0]) +{ + return first_of_both(a, a); +} +// CHECK-DAG: Verified: first_twice [backend=z3] + +// A pair with an extent is separated over its extents: an empty one may lie +// in another object, which no access through it reads. Before, the +// one-object separation of the pair was required as well. +void touch(Buf *b, int *p, int n) + cppverify::pre(b != nullptr && cppverify::valid(p, n) && 0 <= n) + cppverify::modifies(b->len) +{ + b->len = 0; +} + +void empty_inside(Buf *b) + cppverify::pre(b != nullptr) + cppverify::modifies(b->len) +{ + touch(b, &b->len, 0); +} +// CHECK-DAG: Verified: empty_inside [backend=z3] + +void nonempty_inside(Buf *b) + cppverify::pre(b != nullptr) + cppverify::modifies(b->len) +{ + touch(b, &b->len, 1); +} +// CHECK-DAG: error: verification failed: nonempty_inside [{{.*}}::aliasing@[[@LINE-2]]:3] + +// Not yet: a field of an element (s->items[k].data). +struct Shelf { + View items[2]; +}; + +int shelf_first(const Shelf *s) + cppverify::pre(s != nullptr && cppverify::valid(s->items[0].data, s->items[0].len)) +{ + return 0; +} +// CHECK-DAG: error: shelf_first: valid marker requires a bare complete-object pointer and an integer element count +#endif From 5a1e930e54c976001ff00bcdd2e47c3b40806bdc Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:54:56 +0530 Subject: [PATCH 54/94] test: Expect one separation of two extents at a call. --- clang/test/Verify/suite/extent_separation.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/clang/test/Verify/suite/extent_separation.cpp b/clang/test/Verify/suite/extent_separation.cpp index 3f7fd0ee7d..4219158d5d 100644 --- a/clang/test/Verify/suite/extent_separation.cpp +++ b/clang/test/Verify/suite/extent_separation.cpp @@ -130,15 +130,17 @@ int aliasing_caller(int *buffer) // CHECK-DAG: Verified: aliasing_caller // The callee's clauses are asserted at the call under their own kinds: the -// explicit precondition, element separation, each extent's validity, and -// extent separation. A parameter with a declared extent has no single-object -// validity of its own. +// explicit precondition, each extent's validity, and the separation of the +// two extents. A parameter with a declared extent has no single-object +// validity of its own, and a pair with one no single-object separation: +// the extents' separation replaces it, which an empty extent satisfies. // PASSIVE-LABEL: passive disjoint_caller // PASSIVE: assert precondition -// PASSIVE: assert aliasing +// PASSIVE-NOT: assert aliasing // PASSIVE: assert pointer-validity // PASSIVE: assert pointer-validity // PASSIVE: assert aliasing +// PASSIVE-NOT: assert aliasing // PASSIVE: assert missing-return // PASSIVE-LABEL: passive overlapping_caller From 265be830e1a3372d1941e0cf5100c3944dd9cb26 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:54:56 +0530 Subject: [PATCH 55/94] docs: Describe extents of pointer fields, with a checked example. --- website/source/language/structs.rst | 73 +++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/website/source/language/structs.rst b/website/source/language/structs.rst index c3fdc86c43..b77efb0487 100644 --- a/website/source/language/structs.rst +++ b/website/source/language/structs.rst @@ -363,6 +363,79 @@ Not yet supported: - reference returns, rvalue and ``volatile`` references, and references in spec and proof functions. +Extents of pointer fields +------------------------- + +A record often describes a buffer it does not hold: a pointer and a length. +A precondition states its extent as for a pointer parameter, +``cppverify::valid(b.data, b.len)``, for a pointer field of the object a +pointer or reference parameter addresses (``s->data``, ``s.data``, +``(*s).data``, ``s->inner.data``). The extent describes the buffer the field +addresses when the function is entered. That value is an input of the +function as a parameter's value is, and the verifier treats it so: an +access through ``b.data`` must lie in the extent while the field still +holds it, and at a call the field's value at the arguments must have the +extent in the caller's objects, as a pointer argument must. A buffer through +a pointer to non-const is disjoint from the other address parameters' +objects by default, its own record's included, so writing ``b.data[i]`` +leaves ``b.len`` alone; two buffers through pointers to const may be one. + +.. cppverify-example: fails zero_more + +.. code-block:: cpp + + struct Buf { + int *data; + int len; + }; + + void zero(Buf &b) + cv::pre(cv::valid(b.data, b.len) && 0 <= b.len) + cv::modifies(b.data[0 : b.len]) + cv::post(cv::forall(k, 0, b.len, b.data[k] == 0)) + { + for (int i = 0; i < b.len; i = i + 1) + cv::invariant(0 <= i && i <= b.len) + cv::invariant(cv::forall(k, 0, i, b.data[k] == 0)) + cv::modifies(b.data[0 : b.len]) + cv::decreases(b.len - i) + { + b.data[i] = 0; + } + } + + void zero_prefix(int *p, int n, int k) + cv::pre(cv::valid(p, n) && 0 <= k && k <= n) + cv::modifies(p[0 : n]) + cv::post(cv::forall(j, 0, k, p[j] == 0)) + { + Buf b; + b.data = p; + b.len = k; + zero(b); + } + + void zero_more(int *p, int n) + cv::pre(cv::valid(p, n) && 0 <= n && n < 100) + cv::modifies(p[0 : n]) + { + Buf b; + b.data = p; + b.len = n + 1; + zero(b); + } + +.. code-block:: text + + Verified: zero [backend=z3] + Verified: zero_prefix [backend=z3] + buf.cpp:39:3: error: verification failed: zero_more [...::bounds@39:3] (counterexample: n [ssa=n_0] [type=i32] = 0, p [ssa=p_0] [type=pointer] = 1; trace: allocation.automatic allocation@36:3 address=231056 provenance=18 -> heap-write.store@37:10 address=231056 value=1 -> heap-write.store@38:9 address=231064 value=1 -> call.zero@39:3 arg0=231056) [backend=z3] [reason=counterexample] + +``zero_more`` passes a buffer one element longer than the extent of ``p``. +A field given another pointer in the body (``b.data = q;``) addresses that +pointer's object from then on, not the extent. A field of an array element +(``s->items[k].data``) cannot take an extent yet. + ``cppverify::type_invariant`` ----------------------------- From 5d12760a55fd23b21678d5650dbc70239c4b72cd Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:54:57 +0530 Subject: [PATCH 56/94] docs: Extend the extent marker to pointer fields and state the separation of extents. --- website/source/language/pointers.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/website/source/language/pointers.rst b/website/source/language/pointers.rst index 797d84472d..aec75e1722 100644 --- a/website/source/language/pointers.rst +++ b/website/source/language/pointers.rst @@ -589,8 +589,11 @@ access rooted at that parameter must then prove ``0 <= i < n``, and pointer arithmetic must stay in ``[0, n]``: ``p + n`` is the one-past position, which may be formed and compared but not read. The marker must occur as a positive top-level conjunction clause of a ``cppverify::pre`` with a bare complete-object pointer, -and each pointer may have only one marker; ambiguous forms (under ``||``, -``!``, or a conditional, or a second marker for one pointer) fail closed. +or a pointer field of the object a parameter addresses (``valid(s->data, +s->len)``, see :doc:`structs`), and each pointer may have only one marker; +ambiguous forms (under ``||``, ``!``, or a conditional, or a second marker for +one pointer) fail closed. Two address parameters of which one has an extent +are disjoint over their extents, so an empty extent may lie anywhere. Contracts read under the same rule. An access in a precondition, a postcondition, a loop invariant, or a ``cppverify::check`` must lie in its From 010f4f90aec172dc4ce38fd135c0a906fdc50b25 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:54:57 +0530 Subject: [PATCH 57/94] docs: Extend the extent marker to pointer fields in chapter 14. --- .../source/book/part-ii/ch14-pointers-frames-modifies.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/website/source/book/part-ii/ch14-pointers-frames-modifies.rst b/website/source/book/part-ii/ch14-pointers-frames-modifies.rst index 308aeffd70..11713ac62b 100644 --- a/website/source/book/part-ii/ch14-pointers-frames-modifies.rst +++ b/website/source/book/part-ii/ch14-pointers-frames-modifies.rst @@ -361,8 +361,9 @@ user-declared ``cppverify::spec bool valid(int* p, int n)`` is the same marker; ``valid(p, n)`` entails ``n >= 0``. If ``n > 0``, ``p`` must be non-null and abstractly valid; ``n == 0`` permits null. Every access based on ``p`` must prove that its index lies in ``[0, n)``. The marker must be a positive top-level -conjunction clause on the bare complete-object pointer, with at most one marker -per pointer. A pointer without a marker addresses one object. With +conjunction clause on the bare complete-object pointer, or on a pointer field +of a parameter's object (``valid(s->data, s->len)``, see +:doc:`../../language/structs`), with at most one marker per pointer. A pointer without a marker addresses one object. With ``--no-check-ub``, dereference definedness is still mandatory, but accesses are not checked against objects. From 663d622d033f8c2ad266b4735f03c57044bd04ce Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:54:58 +0530 Subject: [PATCH 58/94] docs: Extend the extent marker to pointer fields in chapter 18. --- website/source/book/part-ii/ch18-undefined-behavior.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/website/source/book/part-ii/ch18-undefined-behavior.rst b/website/source/book/part-ii/ch18-undefined-behavior.rst index 20ed907b8b..b38a637989 100644 --- a/website/source/book/part-ii/ch18-undefined-behavior.rst +++ b/website/source/book/part-ii/ch18-undefined-behavior.rst @@ -160,8 +160,9 @@ Typed pointer offsets are scaled to target bytes using ``sizeof(T)`` while this obligation remains the half-open element bound ``0 <= i < n``. For sound discovery, ``valid(p, n)`` must be a positive top-level conjunction -clause, ``p`` must be the bare complete-object pointer, and each pointer may -have only one marker. Shifted, disjunctive, conditional, or duplicate markers +clause, ``p`` must be the bare complete-object pointer or a pointer field of +a parameter's object (``s->data``), and each pointer may have only one +marker. Shifted, disjunctive, conditional, or duplicate markers are rejected instead of being interpreted as unconditional extents. At a modular call, a callee extent ``valid(q, length)`` is instantiated by an From 173faee4d75e555fa93c8e0d0adb5b923e50de13 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:54:59 +0530 Subject: [PATCH 59/94] docs: List extents of pointer fields in the limitations. --- website/source/language/limitations.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/source/language/limitations.rst b/website/source/language/limitations.rst index 7b2fb7dcc7..f412110afd 100644 --- a/website/source/language/limitations.rst +++ b/website/source/language/limitations.rst @@ -255,7 +255,9 @@ alignment, provenance, or initialization. Bounds follow exact conditional and short-circuit evaluation, and fresh represented storage is kept disjoint from the complete declared incoming extent. Outside a precondition the marker means nothing and is rejected (:doc:`pointers`), so an extent cannot yet be -stated for a returned pointer. +stated for a returned pointer. A pointer field of a parameter's object may +take an extent (``valid(s->data, s->len)``, :doc:`structs`); a field of an +array element (``s->items[k].data``) may not yet. For pairs involving mutable pointees, CppVerify adds a generated complete-object disjointness precondition. ``cppverify::aliases(p, q)`` opts a pair into From 45590cf22cfbd9c124a23b1637b84f5a2e7925e3 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:54:59 +0530 Subject: [PATCH 60/94] docs: Note extents of pointer fields and the separation of empty extents in the release notes. --- website/source/release-notes.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/website/source/release-notes.rst b/website/source/release-notes.rst index 507b9f1dd0..b37798babb 100644 --- a/website/source/release-notes.rst +++ b/website/source/release-notes.rst @@ -212,6 +212,12 @@ although the callee writes only cells the caller may write, in two cases: The callee's footprint is now compared with the caller's frame cell by cell (:doc:`language/pointers`). +**An empty extent inside another object.** Two address parameters of which +one has a ``valid`` extent are disjoint over their extents, which allows an +empty extent anywhere; the check of the pair over one object each was made as +well, so ``touch(b, &b->len, 0)``, an empty extent into ``*b``, failed its +``aliasing`` check. Only the extents are compared now (:doc:`language/pointers`). + **A record member named whole in** ``cppverify::modifies``. ``cppverify::modifies(p->left)``, for a member that is a record or an array, was taken for one cell holding the whole member. A call ``reset(&p->left)`` @@ -280,6 +286,15 @@ reseated: ``c.n`` and ``c.buf[i]`` are cells of the object, ``&c.n`` and a reference can be bound to ``*p``, ``p->left``, ``a[i]``, or another reference (:doc:`language/structs`). +**Extents of pointer fields.** ``cv::valid(s->data, s->len)`` was an error +(``valid marker requires a bare complete-object pointer and an integer element +count``), so a record that describes a buffer by a pointer and a length (a +string view, a span) could not be verified. A pointer field of the object a +pointer or reference parameter addresses now takes an extent as a pointer +parameter does: accesses through it are checked against the extent, a call +checks the extent at its arguments, and the buffer is disjoint from the other +parameters' objects by default (:doc:`language/structs`). + **Local objects passed to callees.** A local record passed to a reference parameter (``bump(c)``), and the address of any local passed to a pointer parameter (``reset(&c)``, ``inc(&x)``, ``inc(&c.n)``), were From 2e803787bb12da1aede54301d144f40aaaf774d7 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:55:00 +0530 Subject: [PATCH 61/94] docs: Describe extents of pointer fields in the design. --- docs/DESIGN.md | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 1ee4485b3e..b1a28c95e6 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1021,7 +1021,9 @@ where a pointer came from: when a walker may be in either of two objects, relate it to the condition that chose, as in `s ? q == a + i : q == b + i`. The marker must be a positive top-level conjunction clause on a bare pointer to -a complete object type. At most one marker may describe each pointer. Shifted, +a complete object type, or on a pointer field of the object a pointer or +reference parameter addresses (`valid(s->data, s->len)`, 3e). At most one +marker may describe each pointer. Shifted, disjunctive, conditional, or duplicate markers fail closed rather than creating unconditional extent assumptions. Anywhere else (a postcondition, a loop invariant, a `cppverify::check`, ghost code, a spec function) the marker is its @@ -1042,9 +1044,11 @@ cppverify::pre(p != q && p != r && q != r && ...) // every distinct ptr/ref pa - The caller's verification must establish these inequalities. Calling `swap(p, p)` fails the call's `aliasing` check. - A pointer carrying a `valid(p, n)` extent contributes the whole extent (`n * sizeof(T)` bytes) as its complete object, so the pair is - disjoint unless either pointer is null or either extent is empty. Callers - prove disjointness of the extents they pass. An `cppverify::aliases` pair keeps the - single-object rule and may share storage, as `memmove` does. + disjoint unless either pointer is null or either extent is empty; the pair's + single-object separation is replaced by it, since a pointer with an empty + extent reaches nothing. Callers prove disjointness of the extents they + pass. An `cppverify::aliases` pair keeps the single-object rule and may + share storage, as `memmove` does. - **This is NOT the C++ `__restrict__` keyword.** `__restrict__` is a compiler optimization hint affecting codegen; the implicit assumption above is a verification-level precondition affecting correctness. The keyword `__restrict__`, if present, is a no-op for verification. ### 3. `cppverify::aliases(p, q)` opt-out @@ -1242,6 +1246,29 @@ void reset_left(Pair &p) `--no-check-ub` nothing proves the callee's accesses inside its object, so a record local passed to a callee is `construct.unsupported`. +### 3e. Extents of pointer fields + +- `cppverify::valid(s->data, s->len)` states the extent of the buffer a + pointer field addresses at entry, for a field of the object a pointer or + reference parameter addresses (`s->f`, `r.f`, `(*s).f`, `s->inner.f`). The + field's entry value is an input of the function, as Frama-C's + `\at(s->data, Pre)` is: a synthetic parameter `s->data` follows the + declared ones (`VFunction::FieldParams`), equal to the field at entry by + definition (an entry fact, without definedness, which no caller + establishes, since a call binds the parameter to the field at its + arguments). The extent, its validity, and the default separation then + apply to it as to a parameter: a buffer through a pointer to non-const is + disjoint from the other address parameters' objects, its record's + included; two through pointers to const may share. +- In the body an access through the field lies in the extent where the field + still holds its entry value (the solver relates the two through the + heap); a field given another pointer addresses that pointer's object. +- At a call the field's value at the arguments must have the extent in one + of the caller's objects, as a pointer argument must. +- Not yet: a field of an array element (`s->items[k].data`), a field + extent in a type invariant (B3's class invariants), and `reads` over a + field. + ### 4. `cppverify::modifies(...)` frame condition ```cpp From 16fdeb06ef9e9ec850c3678d689cbeea4dbd1224 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 10:55:00 +0530 Subject: [PATCH 62/94] docs: Describe field parameters and the separation of extents in the architecture. --- docs/ARCHITECTURE.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 87386d58f5..6884016ef8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -201,6 +201,28 @@ as its bytes; before, it was a cell holding the record, which held no callee's footprint inside the member and whose havoc at a call had no logic sort. +**Extents of pointer fields:** `ASTConverter::collectFieldExtents` finds +each extent marker of the preconditions whose pointer is a field of the +object a pointer or reference parameter addresses (`pointerFieldOfParameter`: +`s->f`, `r.f`, `(*s).f`, nested fields), and gives the field's entry value a +synthetic parameter after the declared ones, named `s->f` (fields joined by +`->`, so no name splits at a `.`), displayed as written, with the field's own +pointer type (the marker's parameter converts it to a pointer to const). +`VFunction::FieldParams` keeps each with its field as an expression over the +parameters; the marker's argument converts to the parameter +(`FieldExtentParameters`), so `instrumentUBChecks` sees a bare pointer and +records the extent as for any parameter. The passivizer's `enter` assumes +`s->f_0 == load(s + offset)` in the entry heap, an entry fact (origin +`field`, after the preconditions), and a call binds the parameter in +`ParamMap` to the field at its arguments in the call's state, which also +serves as the argument whose origins place the extent in the caller's +objects. The default separation takes the synthetic entries +(`AddressEntry`, mutable unless the field points to const) with the +declared address parameters. `UBInstrumenter::dropSingleObjectSeparation` +removes a pair's single-object separation where one of the pair has an +extent, so the extent separation (`appendExtentSeparation`) alone states +it. + **Pointers into objects:** `ASTConverter::subobjectPointer` lowers a decayed member array of a pointee and the address of a pointee's element, field, or nested record. In a statement (`SubobjectSink`) it asserts the place's access From 76aa3b920d1204f8b5839f6743f65c80c9d5a001 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 11:16:54 +0530 Subject: [PATCH 63/94] 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 8f975ccb5826ea8da41da80703820b4e3f6ab9b1 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 12:38:11 +0530 Subject: [PATCH 64/94] verify: Keep an in-class member function's clauses for its late-parsed declaration. --- clang/include/clang/Parse/Parser.h | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/clang/include/clang/Parse/Parser.h b/clang/include/clang/Parse/Parser.h index b658d418b7..7fd7812200 100644 --- a/clang/include/clang/Parse/Parser.h +++ b/clang/include/clang/Parse/Parser.h @@ -1311,6 +1311,12 @@ class Parser : public CodeCompletionHandler { /// The set of tokens that make up an exception-specification that /// has not yet been parsed. CachedTokens *ExceptionSpecTokens; + + /// CppVerify: the contract clauses of the declarator, parsed in the + /// complete-class context as a function contract specifier is. + std::unique_ptr ContractTokens; + bool ContractIsSpec = false; + bool ContractIsProof = false; }; /// LateParsedMemberInitializer - An initializer for a non-static class data @@ -7592,6 +7598,14 @@ class Parser : public CodeCompletionHandler { bool isFunctionContractClause(); void ParseFunctionContractClauses(ParsingDeclarator &D, FunctionContractClauses &Clauses); + /// The clauses at Tok, with the parameters and CurrentContractReturnType + /// already in place, and Clauses.Info.IsSpec and IsProof set. + void ParseFunctionContractClauseList(FunctionContractClauses &Clauses); + /// The clauses of an in-class member function declarator, cached for its + /// late-parsed declaration, where the class is complete. + std::unique_ptr MemberContractTokens; + bool MemberContractIsSpec = false; + bool MemberContractIsProof = false; void attachFunctionContract(Decl *Function, FunctionContractClauses &Clauses); /// A declaration ending in ';' after its clauses. Decl * From 2375cf7b3ddb394b405c07f09f1f1684d42881b5 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 12:38:12 +0530 Subject: [PATCH 65/94] verify: Cache the clauses of an in-class member function instead of ignoring them. --- clang/lib/Parse/ParseDeclCXX.cpp | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index 6b712f3603..79249f3970 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -2364,6 +2364,10 @@ void Parser::HandleMemberFunctionDeclDelays(Declarator &DeclaratorInfo, // If there was a late-parsed exception-specification, we'll need a // late parse bool NeedLateParse = FTI.getExceptionSpecType() == EST_Unparsed; + // So do contract clauses (CppVerify). + std::unique_ptr ContractTokens = + std::move(MemberContractTokens); + NeedLateParse = NeedLateParse || ContractTokens; if (!NeedLateParse) { // Look ahead to see if there are any default args @@ -2396,6 +2400,9 @@ void Parser::HandleMemberFunctionDeclDelays(Declarator &DeclaratorInfo, LateMethod->ExceptionSpecTokens = FTI.ExceptionSpecTokens; FTI.ExceptionSpecTokens = nullptr; } + LateMethod->ContractTokens = std::move(ContractTokens); + LateMethod->ContractIsSpec = MemberContractIsSpec; + LateMethod->ContractIsProof = MemberContractIsProof; } } @@ -2980,19 +2987,27 @@ Parser::DeclGroupPtrTy Parser::ParseCXXClassMemberDeclaration( return nullptr; } - // CppVerify: contract clauses (pre/post/...) on in-class member functions are - // not yet modeled by the verifier, which handles free functions. Rather than - // emit a confusing "expected ';'", diagnose once and skip the clauses so the - // rest of the class still parses and the function is treated as uncontracted. + // CppVerify: the contract clauses of an in-class member function are parsed + // in the complete-class context, as a function contract specifier is (C++26 + // [class.mem.general]), with the exception-specification and default + // arguments of its late-parsed declaration: their tokens are kept until + // then. + MemberContractTokens.reset(); if (getLangOpts().VerifyContracts && DeclaratorInfo.isFunctionDeclarator() && isFunctionContractClause()) { - Diag(Tok, diag::warn_contract_member_function_unsupported); + MemberContractTokens = std::make_unique(); + MemberContractIsSpec = + DeclaratorInfo.getDeclSpec().isSpecFunctionSpecified(); + MemberContractIsProof = + DeclaratorInfo.getDeclSpec().isProofFunctionSpecified(); while (isFunctionContractClause()) { + MemberContractTokens->push_back(Tok); ConsumeCppVerify(); if (Tok.is(tok::l_paren)) { - BalancedDelimiterTracker T(*this, tok::l_paren); - if (!T.consumeOpen()) - T.skipToEnd(); + MemberContractTokens->push_back(Tok); + ConsumeParen(); + ConsumeAndStoreUntil(tok::r_paren, *MemberContractTokens, + /*StopAtSemi=*/false, /*ConsumeFinalToken=*/true); } } } From 900012b4afc1ef6fd0030e2dffab36f3201141cb Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 12:38:13 +0530 Subject: [PATCH 66/94] verify: Parse a member function's clauses in the complete-class context. --- clang/lib/Parse/ParseCXXInlineMethods.cpp | 41 +++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/clang/lib/Parse/ParseCXXInlineMethods.cpp b/clang/lib/Parse/ParseCXXInlineMethods.cpp index 9949adf199..dc908d9aaf 100644 --- a/clang/lib/Parse/ParseCXXInlineMethods.cpp +++ b/clang/lib/Parse/ParseCXXInlineMethods.cpp @@ -566,6 +566,47 @@ void Parser::ParseLexedMethodDeclaration(LateParsedMethodDeclaration &LM) { LM.ExceptionSpecTokens = nullptr; } + // CppVerify: the contract clauses, with the parameters in scope and `this` + // as in the exception-specification. + if (std::unique_ptr Toks = std::move(LM.ContractTokens)) { + ParenBraceBracketBalancer BalancerRAIIObj(*this); + Token ContractEnd; + ContractEnd.startToken(); + ContractEnd.setKind(tok::eof); + ContractEnd.setLocation(Toks->back().getEndLoc()); + ContractEnd.setEofData(LM.Method); + Toks->push_back(ContractEnd); + Toks->push_back(Tok); + PP.EnterTokenStream(*Toks, true, /*IsReinject*/ true); + ConsumeAnyToken(/*ConsumeCodeCompletionTok=*/true); + + FunctionDecl *Function = LM.Method->getAsFunction(); + auto *Method = dyn_cast_or_null(Function); + ParseScope FnScope(this, Scope::FnScope); + Sema::ContextRAII FnContext(Actions, Function, /*NewThisContext=*/false); + Sema::CXXThisScopeRAII ThisScope( + Actions, Method ? Method->getParent() : nullptr, + Method ? Method->getMethodQualifiers() : Qualifiers{}, + Method && !Method->isStatic()); + + FunctionContractClauses Contract; + Contract.Info.IsSpec = LM.ContractIsSpec; + Contract.Info.IsProof = LM.ContractIsProof; + PendingClauseProofs.clear(); + CurrentContractReturnType = + Function ? Function->getReturnType() : Actions.getASTContext().IntTy; + ParseFunctionContractClauseList(Contract); + if (Tok.isNot(tok::eof) || Tok.getEofData() != LM.Method) + Diag(Tok.getLocation(), diag::err_expected) << tok::semi; + attachFunctionContract(LM.Method, Contract); + PendingClauseProofs.clear(); + + while (Tok.isNot(tok::eof)) + ConsumeAnyToken(); + if (Tok.is(tok::eof) && Tok.getEofData() == LM.Method) + ConsumeAnyToken(); + } + InFunctionTemplateScope.Scopes.Exit(); // Finish the delayed C++ method declaration. From 8b3e5c54a85858340397093f7bafb725e28df390 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 12:38:13 +0530 Subject: [PATCH 67/94] verify: Parse an out-of-line member's clauses in its class's scope, and name a class template for its members. --- clang/lib/Parse/ParseCppVerify.cpp | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/clang/lib/Parse/ParseCppVerify.cpp b/clang/lib/Parse/ParseCppVerify.cpp index f40f1b1336..8c5d38537e 100644 --- a/clang/lib/Parse/ParseCppVerify.cpp +++ b/clang/lib/Parse/ParseCppVerify.cpp @@ -358,12 +358,19 @@ void Parser::ParseFunctionContractClauses(ParsingDeclarator &D, return; // Re-enter function parameters into scope so contract conditions can - // reference them. This mirrors ParseTrailingRequiresClause in - // ParseDeclCXX.cpp: create a FunctionPrototypeScope and push params. + // reference them. This mirrors ParseTrailingRequiresClauseWithScope in + // ParseDeclCXX.cpp: an out-of-line member's clauses are in its class's + // scope, with `this`, and a FunctionPrototypeScope holds the parameters. + CXXScopeSpec &SS = D.getCXXScopeSpec(); + DeclaratorScopeObj DeclScopeObj(*this, SS); + if (SS.isValid() && Actions.ShouldEnterDeclaratorScope(getCurScope(), SS)) + DeclScopeObj.EnterDeclaratorScope(); ParseScope ContractParamScope(this, Scope::DeclScope | Scope::FunctionDeclarationScope | Scope::FunctionPrototypeScope); Actions.ActOnStartTrailingRequiresClause(getCurScope(), D); + std::optional ThisScope; + InitCXXThisScopeForDeclaratorIfRelevant(D, D.getDeclSpec(), ThisScope); // Compute the return type from the full Declarator (not just the DeclSpec) // so that result in postconditions gets the correct type: pointers, @@ -376,7 +383,13 @@ void Parser::ParseFunctionContractClauses(ParsingDeclarator &D, else CurrentContractReturnType = Actions.getASTContext().IntTy; } + ParseFunctionContractClauseList(Clauses); +} +void Parser::ParseFunctionContractClauseList(FunctionContractClauses &Clauses) { + FunctionContractInfo &I = Clauses.Info; + const bool IsSpecFn = I.IsSpec; + const bool IsProofFn = I.IsProof; llvm::SaveAndRestore ContextRAII(CppVerifyContextDepth, CppVerifyContextDepth + 1); // ACSL behaviors: pre and post clauses after behavior(name, assumes) @@ -663,8 +676,14 @@ void Parser::attachFunctionContract(Decl *Result, // Templates are not verified yet: their clauses are ignored, as a member // function's are. if (FD->isTemplated() && !I.IsSpec && !I.IsProof) { - Diag(FD->getLocation(), diag::warn_contract_template_unsupported) - << 0 << FD; + // A member of a class template is named by its class. + const auto *Method = dyn_cast(FD); + if (Method && Method->getParent()->isTemplated()) + Diag(FD->getLocation(), diag::warn_contract_template_unsupported) + << 1 << Method->getParent(); + else + Diag(FD->getLocation(), diag::warn_contract_template_unsupported) + << 0 << FD; return; } FunctionContractInfo &FCI = Ctx.getOrCreateFunctionContract(FD); From abbf0098fce1bcf6fdaa3221c136b9442e59fb20 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 12:38:14 +0530 Subject: [PATCH 68/94] verify: Remove the warning that contracts on member functions are ignored. --- clang/include/clang/Basic/DiagnosticParseKinds.td | 4 ---- 1 file changed, 4 deletions(-) diff --git a/clang/include/clang/Basic/DiagnosticParseKinds.td b/clang/include/clang/Basic/DiagnosticParseKinds.td index 1aa2a2d029..ba4e0f7dd4 100644 --- a/clang/include/clang/Basic/DiagnosticParseKinds.td +++ b/clang/include/clang/Basic/DiagnosticParseKinds.td @@ -1991,9 +1991,5 @@ def warn_contract_template_unsupported : Warning< "contracts in %select{function|class}0 template %1 are not yet supported " "and are ignored">, InGroup; -def warn_contract_member_function_unsupported : Warning< - "contracts on member functions are not yet supported and are ignored; " - "move the function out of the class or verify it as a free function">, - InGroup; } // end of Parser diagnostics From 0b7a4bb22e95379094d0b58feca01c5df0fe7eab Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 12:38:14 +0530 Subject: [PATCH 69/94] verify: Verify member functions as functions of this, and pass the object at a member call. --- clang/lib/Verify/Frontend/ASTConverter.cpp | 188 +++++++++++++++++++-- 1 file changed, 175 insertions(+), 13 deletions(-) diff --git a/clang/lib/Verify/Frontend/ASTConverter.cpp b/clang/lib/Verify/Frontend/ASTConverter.cpp index 24ef9e88a5..d72861cdae 100644 --- a/clang/lib/Verify/Frontend/ASTConverter.cpp +++ b/clang/lib/Verify/Frontend/ASTConverter.cpp @@ -933,17 +933,23 @@ static bool isMutableAddressParam(const ParmVarDecl *P) { static bool aliasesListed(const FunctionContractInfo &FCI, StringRef A, StringRef B) { + // A parameter of the pair, or `this`. + auto NameOf = [](const Expr *E) -> std::optional { + E = E->IgnoreParenImpCasts(); + if (isa(E)) + return StringRef("this"); + const auto *Ref = dyn_cast(E); + const auto *Param = Ref ? dyn_cast(Ref->getDecl()) : nullptr; + if (!Param) + return std::nullopt; + return Param->getName(); + }; for (const auto &Pair : FCI.Aliases) { - const auto *L = dyn_cast(Pair.first->IgnoreParenImpCasts()); - const auto *R = dyn_cast(Pair.second->IgnoreParenImpCasts()); + std::optional L = NameOf(Pair.first); + std::optional R = NameOf(Pair.second); if (!L || !R) continue; - const auto *LD = dyn_cast(L->getDecl()); - const auto *RD = dyn_cast(R->getDecl()); - if (!LD || !RD) - continue; - if ((LD->getName() == A && RD->getName() == B) || - (LD->getName() == B && RD->getName() == A)) + if ((*L == A && *R == B) || (*L == B && *R == A)) return true; } return false; @@ -1415,6 +1421,11 @@ void ASTConverter::beginInitializationTracking(const FunctionDecl *FD) { ->getType() ->isLValueReferenceType()) addRoot(Call->getArg(I)); + // A member call binds `this` to its object. + if (const auto *Member = dyn_cast(Call)) + if (const Expr *Object = Member->getImplicitObjectArgument(); + Object && !Object->getType()->isPointerType()) + addRoot(Object); return true; } @@ -1946,6 +1957,39 @@ static void markUnsupported(VFunction &Fn) { Fn.InductiveLoc = SourceLocation(); } +/// Why a member function is not verified yet, or nothing for one that is: a +/// static member is a free function, and a non-static one of a class without +/// bases or virtual functions is a function of `this`, a pointer to its +/// object. +static std::optional +unsupportedMemberFunction(const CXXMethodDecl *Method) { + if (Method->getParent()->isLambda()) + return std::string("lambdas are unsupported"); + if (isa(Method)) + return std::string("constructors are not verified yet"); + if (isa(Method)) + return std::string("destructors are not verified yet"); + if (isa(Method)) + return std::string("conversion functions are unsupported"); + if (Method->isStatic()) + return std::nullopt; + if (Method->isVirtual()) + return std::string("virtual functions are unsupported, since a call " + "may run an override"); + const CXXRecordDecl *Class = Method->getParent(); + if (!Class->hasDefinition() || Class->getNumBases() != 0 || + Class->isPolymorphic()) + return std::string("member functions of a class with bases or virtual " + "functions are unsupported"); + if (Method->isExplicitObjectMemberFunction()) + return std::string("explicit object parameters are unsupported"); + if (Method->getRefQualifier() == RQ_RValue) + return std::string("rvalue-qualified member functions are unsupported"); + if (Method->getMethodQualifiers().hasVolatile()) + return std::string("volatile member functions are unsupported"); + return std::nullopt; +} + std::vector> ASTConverter::convertTranslationUnit() { std::vector> Out; llvm::StringSet<> Identities; @@ -1953,15 +1997,18 @@ std::vector> ASTConverter::convertTranslationUnit() { collectFunctionCandidates(Ctx.getTranslationUnitDecl(), Ctx, Definitions); UsedConstexprSpecs.clear(); for (const FunctionDecl *FD : Definitions) { - if (isa(FD) || FD->isTemplated()) { + const auto *Method = dyn_cast(FD); + const std::optional UnsupportedMember = + Method && !FD->isTemplated() ? unsupportedMemberFunction(Method) + : std::nullopt; + if (UnsupportedMember || FD->isTemplated()) { llvm::SaveAndRestore Owner(ErrorFunction, functionIdentity(FD)); llvm::SaveAndRestore OwnerName(ErrorFunctionName, FD->getNameAsString()); llvm::SaveAndRestore InContract(ErrorInContract, true); - if (isa(FD)) { + if (UnsupportedMember) { if (functionContract(FD) || hasContractStatements(FD, Ctx)) error(FD->getLocation(), - FD->getNameAsString() + - ": member function verification is unsupported"); + FD->getQualifiedNameAsString() + ": " + *UnsupportedMember); continue; } // Only a template with contracts is reported here: one without @@ -2561,6 +2608,9 @@ void ASTConverter::registerLayoutType(QualType QT, VFunction &Fn) { void ASTConverter::collectLayouts(const FunctionDecl *FD, VFunction &Fn) { KnownLayoutIdentities.clear(); registerLayoutType(FD->getReturnType(), Fn); + if (const auto *Method = dyn_cast(FD); + Method && Method->isInstance()) + registerLayoutType(Method->getFunctionObjectParameterType(), Fn); for (const ParmVarDecl *P : FD->parameters()) registerLayoutType(P->getType(), Fn); @@ -2788,7 +2838,11 @@ ASTConverter::convertFunction(const FunctionDecl *FD) { } auto Fn = std::make_unique(); - Fn->Name = FD->getNameAsString(); + // A member function is named with its class, and a non-static one is a + // function of `this`, the pointer to its object. + const auto *Method = dyn_cast(FD); + const bool HasThis = Method && Method->isInstance(); + Fn->Name = Method ? FD->getQualifiedNameAsString() : FD->getNameAsString(); Fn->Identity = functionIdentity(FD); Fn->IsSpec = FCI->IsSpec; Fn->IsProof = FCI->IsProof; @@ -2836,6 +2890,19 @@ ASTConverter::convertFunction(const FunctionDecl *FD) { InContractExpression = false; SmallVector AddressParams; + // `this` addresses a live object, as a reference does: non-null, valid, + // and initialized at entry; in a const member function it points to const. + QualType ThisObject = + HasThis ? Method->getFunctionObjectParameterType() : QualType(); + if (HasThis) { + const VType ThisTy = + VType::fromQualType(Ctx.getPointerType(ThisObject), IntMode, Ctx); + Fn->Params.emplace_back("this", ThisTy); + if (ThisObject.isConstQualified()) + Fn->ConstAddressParams.insert("this"); + Fn->SourceVariables["this"] = {"this", ThisTy, FD->getLocation(), + FD->getLocation()}; + } for (const ParmVarDecl *P : FD->parameters()) { if (isSupportedScalarLValueReference(P->getType())) Fn->ScalarReferenceParams.insert(valueName(P)); @@ -2863,6 +2930,8 @@ ASTConverter::convertFunction(const FunctionDecl *FD) { // field at entry and by each call at its arguments, takes the extent, its // validity, and the default separation. std::vector Addresses; + if (HasThis) + Addresses.push_back({"this", ThisObject, !ThisObject.isConstQualified()}); for (const ParmVarDecl *P : AddressParams) Addresses.push_back( {valueName(P), addressedType(P), isMutableAddressParam(P)}); @@ -2924,6 +2993,19 @@ ASTConverter::convertFunction(const FunctionDecl *FD) { else error(Fn->Name + ": unsupported expression in modifies"); } + // Decision Q8: a verified non-const member function without modifies may + // write its object; a const one only its own storage. An explicit modifies + // replaces the default, and a trusted or bodiless one keeps the rule of a + // trusted contract. + if (HasThis && !ThisObject.isConstQualified() && FCI->Modifies.empty() && + FD->hasBody() && !Fn->IsTrusted) { + VFootprint Object; + Object.Target = std::make_unique( + std::make_unique("this", Fn->Params.front().second, + FD->getLocation()), + VType::fromQualType(ThisObject, IntMode, Ctx), FD->getLocation()); + Fn->Modifies.push_back(std::move(Object)); + } for (const auto &[Pointer, Count] : FCI->Reads) { const PointerType *PT = Pointer ? Pointer->getType()->getAs() : nullptr; @@ -2977,6 +3059,28 @@ ASTConverter::convertFunction(const FunctionDecl *FD) { InPost = false; if (!Fn->IsSpec) { + if (HasThis) { + auto This = [] { + return std::make_unique("this", VType::makePtr(), + SourceLocation()); + }; + auto NonNull = std::make_unique( + VBinOp::Ne, This(), + std::make_unique(0, VType::makePtr(), SourceLocation()), + VType::makeBool(), SourceLocation()); + auto Readable = std::make_unique( + VBinOp::And, + std::make_unique(VUnaryOp::ValidPtr, This(), + VType::makeBool(), SourceLocation()), + std::make_unique(VUnaryOp::InitializedPtr, This(), + VType::makeBool(), SourceLocation()), + VType::makeBool(), SourceLocation()); + addPrecondition(*Fn, + std::make_unique( + VBinOp::And, std::move(NonNull), std::move(Readable), + VType::makeBool(), SourceLocation()), + ProofObligationKind::PointerValidity); + } for (const ParmVarDecl *P : AddressParams) { auto Pointer = std::make_unique(valueName(P), VType::makePtr(), SourceLocation()); @@ -3206,6 +3310,20 @@ ASTConverter::convertFunction(const FunctionDecl *FD) { return nullptr; } collectLayouts(FD, *Fn); + auto RecordLayout = [&](QualType Object) -> std::optional { + const std::string Identity = canonicalTypeIdentity( + Object.getCanonicalType().getUnqualifiedType(), Ctx); + if (!Object->isRecordType() || + llvm::none_of(Fn->Layouts, [&](const VObjectLayout &Layout) { + return Layout.Kind == VObjectKind::Record && + Layout.TypeIdentity == Identity; + })) + return std::nullopt; + return Identity; + }; + if (HasThis) + if (auto Identity = RecordLayout(ThisObject)) + Fn->RecordParams["this"] = *Identity; for (const ParmVarDecl *P : FD->parameters()) { QualType Ty = P->getType(); if (!Ty->isPointerType() && !Ty->isReferenceType()) @@ -4909,6 +5027,17 @@ std::unique_ptr ASTConverter::convertExprImpl(const Expr *E) { if (const auto *MTE = dyn_cast(E)) return convertExpr(MTE->getSubExpr()); + // `this`, the parameter of a member function's object. + if (isa(E)) { + if (!CurrentFn || CurrentFn->Params.empty() || + CurrentFn->Params.front().first != "this") { + error(CurrentFn ? CurrentFn->Name + ": unsupported use of this" + : std::string("unsupported use of this")); + return nullptr; + } + return std::make_unique("this", CurrentFn->Params.front().second, + E->getExprLoc()); + } if (const auto *Decay = dyn_cast(E); Decay && Decay->getCastKind() == CK_ArrayToPointerDecay) { if (subobjectPointerRoot(Decay->getSubExpr())) @@ -5791,6 +5920,39 @@ void ASTConverter::convertExecCallArgs( } } + // A member call passes its object first, as the callee's `this`: the + // object a pointer addresses (p->m(), this->m()), or the object itself, + // bound as a reference argument is (obj.m()). + if (const auto *Member = dyn_cast(CE); + Member && Member->getMethodDecl() && + Member->getMethodDecl()->isInstance()) { + const Expr *Object = Member->getImplicitObjectArgument(); + std::unique_ptr This; + if (Object->getType()->isPointerType()) { + This = convertExpr(Object); + } else { + const size_t ErrorCount = Errors.size(); + std::unique_ptr AccessCondition; + This = convertLValueAddress(Object, &AccessCondition); + if (AccessCondition) + Prelude.push_back(std::make_unique( + std::move(AccessCondition), Object->getExprLoc(), + ProofObligationKind::Bounds)); + const Expr *Binding = Object->IgnoreParenImpCasts(); + if (This && + (isa(Binding) || isa(Binding))) + appendReferenceBindingCheck(Object, This.get(), Object->getExprLoc(), + Prelude); + if (!This && Errors.size() == ErrorCount) + error(CurrentFn->Name + + ": a member call's object must be a supported reference, " + "addressable local, or direct pointer dereference"); + } + if (!This) + return; + Args.push_back(std::move(This)); + } + unsigned ArgIndex = 0; for (const Expr *A : CE->arguments()) { const ParmVarDecl *Formal = Callee && ArgIndex < Callee->getNumParams() From 79bf37ae4c7b50f67a8f88697c9f7fe799527a36 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 12:38:15 +0530 Subject: [PATCH 70/94] test: Check member functions and calls on objects. --- clang/test/Verify/suite/member_functions.cpp | 277 +++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 clang/test/Verify/suite/member_functions.cpp diff --git a/clang/test/Verify/suite/member_functions.cpp b/clang/test/Verify/suite/member_functions.cpp new file mode 100644 index 0000000000..eed3582248 --- /dev/null +++ b/clang/test/Verify/suite/member_functions.cpp @@ -0,0 +1,277 @@ +// RUN: not %cpp-verify --jobs=1 %s -- 2>&1 | FileCheck %s +// RUN: not %cpp-verify --jobs=1 --backend=bmc --unroll=4 %s -- 2>&1 \ +// RUN: | FileCheck %s --check-prefix=BMC +// RUN: not %cpp-verify --backend=lean --lean-out=%t.lean %s -- 2>&1 \ +// RUN: | FileCheck %s --check-prefix=LEAN +// +// A member function is a function of `this`, a pointer to its object that is +// never null, as a reference is: its fields, written f or this->f, are cells +// of the object, and a call on an object passes the object's address. Its +// clauses are in the complete-class context, so they name members declared +// after them. A non-const member function without modifies may write its +// object (decision Q8), a const one only its own storage. + +struct Counter { + void bump() + cppverify::pre(n < limit) + cppverify::modifies(n) + cppverify::post(n == cppverify::old(n) + 1) + cppverify::post(limit == cppverify::old(limit)) + { + n = n + 1; + } + + int get() const + cppverify::post(cppverify::result == n) + { + return n; + } + + // No modifies: the whole object. + void reset() + cppverify::post(n == 0 && this->limit == 10) + { + n = 0; + limit = 10; + } + + void twice() + cppverify::pre(n + 1 < limit) + cppverify::modifies(n) + cppverify::post(n == cppverify::old(n) + 2) + { + bump(); + this->bump(); + } + + void copy_from(const Counter &o) + cppverify::modifies(n) + cppverify::post(n == o.n) + { + n = o.n; + } + + void copy_aliased(const Counter &o) + cppverify::aliases(this, o) + cppverify::modifies(n) + cppverify::post(n == cppverify::old(o.n)) + { + n = o.n; + } + + void bad() + cppverify::modifies(n) + { + limit = 0; + } + + int peek_counted() const + cppverify::modifies(hits) + cppverify::post(cppverify::result == n) + { + hits = 0; + return n; + } + + int peek_bad() const + cppverify::post(cppverify::result == n) + { + hits = 0; + return n; + } + + static int zero() + cppverify::post(cppverify::result == 0) + { + return 0; + } + + // A const member function reads its object, which another read-only + // parameter may share. + int plus(const Counter &o) const + cppverify::pre(-1000 < n && n < 1000 && -1000 < o.n && o.n < 1000) + cppverify::post(cppverify::result == n + o.n) + { + return n + o.n; + } + + // Declared here, defined below with the contract written here. + void set(int v) + cppverify::modifies(n) + cppverify::post(n == v); + + int doubled() const; + + int n; + int limit; + mutable int hits; +}; +// CHECK-DAG: Verified: Counter::bump [backend=z3] +// BMC-DAG: Verified: Counter::bump [backend=bmc, bound=0] +// LEAN-DAG: Exported: lean obligation: Counter::bump +// CHECK-DAG: Verified: Counter::get [backend=z3] +// CHECK-DAG: Verified: Counter::reset [backend=z3] +// CHECK-DAG: Verified: Counter::twice [backend=z3] +// CHECK-DAG: Verified: Counter::copy_from [backend=z3] +// CHECK-DAG: Verified: Counter::copy_aliased [backend=z3] +// CHECK-DAG: Verified: Counter::peek_counted [backend=z3] +// CHECK-DAG: Verified: Counter::zero [backend=z3] + +// CHECK-DAG: error: verification failed: Counter::bad [{{.*}}::frame@{{[0-9]+}}:11] (counterexample: this [ssa=this_0] [type=pointer] = +// A const member function writes only its own storage, mutable fields +// included, unless its modifies lists them. +// CHECK-DAG: error: verification failed: Counter::peek_bad [{{.*}}::frame@ + +void Counter::set(int v) +{ + n = v; +} +// CHECK-DAG: Verified: Counter::set [backend=z3] + +int Counter::doubled() const + cppverify::pre(-1000 < n && n < 1000) + cppverify::post(cppverify::result == 2 * n) +{ + return n + get(); +} +// CHECK-DAG: error: Counter::doubled: executable call is unsupported in this expression context: get + +// Calls on a local object, through a pointer, through a reference, and on +// a member of an object. +int use_local() + cppverify::post(cppverify::result == 11) +{ + Counter c; + c.n = 0; + c.limit = 5; + c.hits = 0; + c.bump(); + int k = c.get(); + c.reset(); + return k + c.limit; +} +// CHECK-DAG: Verified: use_local [backend=z3] +// BMC-DAG: Verified: use_local [backend=bmc, bound=0] + +void through_pointer(Counter *p) + cppverify::pre(p != nullptr && p->n < p->limit) + cppverify::modifies(p->n) + cppverify::post(p->n == cppverify::old(p->n) + 1) +{ + p->bump(); +} +// CHECK-DAG: Verified: through_pointer [backend=z3] + +void through_reference(Counter &c) + cppverify::pre(c.n < c.limit) + cppverify::modifies(c.n) + cppverify::post(c.n == cppverify::old(c.n) + 1) +{ + c.bump(); +} +// CHECK-DAG: Verified: through_reference [backend=z3] + +struct Pair { + Counter left; + Counter right; +}; + +void member_of(Pair &q) + cppverify::modifies(q.left.n) + cppverify::post(q.left.n == cppverify::old(q.right.n)) +{ + q.left.copy_from(q.right); +} +// CHECK-DAG: Verified: member_of [backend=z3] + +int static_call() + cppverify::post(cppverify::result == 0) +{ + return Counter::zero(); +} +// CHECK-DAG: Verified: static_call [backend=z3] + +// The default frame of reset is the object, which a caller must allow. +int cleared(Counter &c) + cppverify::modifies(c) + cppverify::post(cppverify::result == 10) +{ + c.reset(); + return c.n + c.limit; +} +// CHECK-DAG: Verified: cleared [backend=z3] + +void cleared_narrow(Counter &c) + cppverify::modifies(c.n) +{ + c.reset(); +} +// CHECK-DAG: error: verification failed: cleared_narrow [{{.*}}::frame@[[@LINE-2]]:5] + +// `this` and a mutable or read parameter are distinct objects by default. +void self_copy(Counter &c) + cppverify::modifies(c.n) +{ + c.copy_from(c); +} +// CHECK-DAG: error: verification failed: self_copy [{{.*}}::aliasing@[[@LINE-2]]:5] + +int self_plus(const Counter &c) + cppverify::pre(-1000 < c.n && c.n < 1000) + cppverify::post(cppverify::result == 2 * c.n) +{ + int r = c.plus(c); + return r; +} +// CHECK-DAG: Verified: self_plus [backend=z3] + +void self_copy_aliased(Counter &c) + cppverify::modifies(c.n) + cppverify::post(c.n == cppverify::old(c.n)) +{ + c.copy_aliased(c); +} +// CHECK-DAG: Verified: self_copy_aliased [backend=z3] + +// Not yet, each with its function's located error: virtual functions, +// members of classes with bases, constructors, destructors, and conversion +// functions. +struct Shape { + virtual int area() const + cppverify::post(cppverify::result >= 0) + { + return 0; + } +}; +// CHECK-DAG: error: Shape::area: virtual functions are unsupported, since a call may run an override + +struct Base { + int b; +}; + +struct Derived : Base { + int get() const + cppverify::post(cppverify::result == b) + { + return b; + } +}; +// CHECK-DAG: error: Derived::get: member functions of a class with bases or virtual functions are unsupported + +struct Made { + int v; + Made() + cppverify::post(v == 0) + : v(0) {} + ~Made() + cppverify::pre(v == 0) + {} + operator int() const + cppverify::post(cppverify::result == v) + { + return v; + } +}; +// CHECK-DAG: error: Made::Made: constructors are not verified yet +// CHECK-DAG: error: Made::~Made: destructors are not verified yet +// CHECK-DAG: error: Made::operator int: conversion functions are unsupported From 9f402e2a5151da9682dc3a997d474e20522e5f31 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 12:38:16 +0530 Subject: [PATCH 71/94] test: Check the parsing of member function clauses. --- .../test/Verify/member_function_contracts.cpp | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 clang/test/Verify/member_function_contracts.cpp diff --git a/clang/test/Verify/member_function_contracts.cpp b/clang/test/Verify/member_function_contracts.cpp new file mode 100644 index 0000000000..4d309d0034 --- /dev/null +++ b/clang/test/Verify/member_function_contracts.cpp @@ -0,0 +1,39 @@ +// RUN: %clang_cc1 -std=c++17 -fverify-contracts -fsyntax-only -verify %s +// +// The clauses of a member function are parsed in the complete-class context, +// as a function contract specifier is (C++26 [class.mem.general]): an +// in-class declaration's clauses name members declared after it and `this`, +// and an out-of-line definition's clauses are in its class's scope. + +struct Counter { + void bump() + cppverify::pre(n < limit) + cppverify::modifies(n) + cppverify::post(n == cppverify::old(n) + 1 && this->limit == cppverify::old(limit)) + { + n = n + 1; + } + + int get() const; + void set(int v) + cppverify::post(n == v); + + void wrong() + cppverify::pre(missing > 0) // expected-error {{use of undeclared identifier 'missing'}} + {} + + int n; + int limit; +}; + +int Counter::get() const + cppverify::post(cppverify::result == this->n && limit == cppverify::old(limit)) +{ + return n; +} + +void Counter::set(int v) // expected-error {{contract clauses may only be specified on one declaration of a function}} + cppverify::post(n == v) +{ + n = v; +} From c5f4aa34cd0d1323105f8f2d4d3e28cb7957d5c1 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 12:38:16 +0530 Subject: [PATCH 72/94] test: Locate the error of a virtual function. --- clang/test/Verify/located_errors.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/clang/test/Verify/located_errors.cpp b/clang/test/Verify/located_errors.cpp index 0db71d256a..205990d28d 100644 --- a/clang/test/Verify/located_errors.cpp +++ b/clang/test/Verify/located_errors.cpp @@ -18,12 +18,12 @@ int pick(int x) // CHECK-NEXT: {{^ +}}| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // 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; } +struct Shape { + virtual int get(int x) { cv::check(x >= 0 || x < 0); return x; } }; -// CHECK: {{.*}}located_errors.cpp:[[@LINE-2]]:7: error: get: member function verification is unsupported -// CHECK-NEXT: {{^ +}}[[@LINE-3]] | int get(int x) { cv::check(x >= 0 || x < 0); return x; } -// CHECK-NEXT: {{^ +}}| ^~~ +// CHECK: {{.*}}located_errors.cpp:[[@LINE-2]]:15: error: Shape::get: virtual functions are unsupported, since a call may run an override +// CHECK-NEXT: {{^ +}}[[@LINE-3]] | virtual int get(int x) { cv::check(x >= 0 || x < 0); return x; } +// CHECK-NEXT: {{^ +}}| ^~~ [[cppverify::trusted]] cv::spec int trusted_spec(int x) { return x; } // CHECK: {{.*}}located_errors.cpp:[[@LINE-1]]:37: error: trusted_spec: {{\[\[}}cppverify::trusted]] does not apply to a spec function, whose definition is its meaning From fbe4e0902bc2cc7f0e749ce50920d8ae866fcc76 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 12:38:17 +0530 Subject: [PATCH 73/94] test: Expect the class template's warning on its member function. --- clang/test/Verify/template_contracts.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clang/test/Verify/template_contracts.cpp b/clang/test/Verify/template_contracts.cpp index 1b52a185c9..4f0da96225 100644 --- a/clang/test/Verify/template_contracts.cpp +++ b/clang/test/Verify/template_contracts.cpp @@ -41,8 +41,8 @@ struct Box { T value; cppverify::type_invariant(value == value); // expected-warning {{contracts in class template 'Box' are not yet supported and are ignored}} - T get() const - cppverify::post(cppverify::result == value) // expected-warning {{contracts on member functions are not yet supported and are ignored}} + T get() const // expected-warning {{contracts in class template 'Box' are not yet supported and are ignored}} + cppverify::post(cppverify::result == value) { return value; } From 1e8572db6c2b822be91c063a888f86fb2519e688 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 12:38:18 +0530 Subject: [PATCH 74/94] docs: Describe member functions, with a checked example. --- website/source/language/structs.rst | 104 ++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/website/source/language/structs.rst b/website/source/language/structs.rst index b77efb0487..c1f81cade4 100644 --- a/website/source/language/structs.rst +++ b/website/source/language/structs.rst @@ -436,6 +436,110 @@ A field given another pointer in the body (``b.data = q;``) addresses that pointer's object from then on, not the extent. A field of an array element (``s->items[k].data``) cannot take an extent yet. +Member functions +---------------- + +A member function of a class without bases or virtual functions is verified +as a function of ``this``, a pointer to its object that is never null, as a +reference is: a field, written ``n`` or ``this->n``, is a cell of the object, +``cppverify::modifies(n)`` names it and ``cppverify::modifies(*this)`` the +object, and a call passes the object's address (``c.bump()`` passes ``&c``, +``p->bump()`` passes ``p``, and ``bump()`` in a member function passes +``this``). Its clauses are parsed once the class is complete, so they may +name members declared after them, as in C++26 contracts. ``this`` and the +other address parameters name distinct objects by default; +``cppverify::aliases(this, o)`` allows them to name one. + +A non-const member function without ``cppverify::modifies`` may write its +object, which is the C++ reading of a method that changes ``*this``; a +``const`` one writes only its own storage, its ``mutable`` fields included +unless its ``cppverify::modifies`` lists them. An explicit +``cppverify::modifies`` replaces the default. A static member function is a +free function. + +.. cppverify-example: fails reset_count self_copy + +.. code-block:: cpp + + struct Counter { + void bump() + cv::pre(n < limit) + cv::modifies(n) + cv::post(n == cv::old(n) + 1 && limit == cv::old(limit)) + { + n = n + 1; + } + + int get() const + cv::post(cv::result == n) + { + return n; + } + + void reset() + cv::post(n == 0 && limit == 10) + { + n = 0; + limit = 10; + } + + void copy_from(const Counter &o) + cv::modifies(n) + cv::post(n == o.n) + { + n = o.n; + } + + int n; + int limit; + }; + + int counted() + cv::post(cv::result == 11) + { + Counter c; + c.n = 0; + c.limit = 5; + c.bump(); + int k = c.get(); + c.reset(); + return k + c.limit; + } + + void reset_count(Counter &c) + cv::modifies(c.n) + { + c.reset(); + } + + void self_copy(Counter &c) + cv::modifies(c.n) + { + c.copy_from(c); + } + +.. code-block:: text + + Verified: Counter::bump [backend=z3] + Verified: Counter::get [backend=z3] + Verified: Counter::reset [backend=z3] + Verified: Counter::copy_from [backend=z3] + Verified: counted [backend=z3] + counter.cpp:49:5: error: verification failed: reset_count [...::frame@49:5] (counterexample: c [ssa=c_0] [type=pointer] = 1; trace: call.reset@49:5 arg0=1) [backend=z3] [reason=counterexample] + counter.cpp:55:5: error: verification failed: self_copy [...::aliasing@55:5] (counterexample: c [ssa=c_0] [type=pointer] = 1; trace: call.copy_from@55:5 arg0=1 arg1=1) [backend=z3] [reason=counterexample] + +``reset`` states no ``cppverify::modifies``, so it may write all of ``c``, +which ``reset_count`` may not. ``copy_from`` writes ``this->n`` and reads +``o``, which by default are distinct objects; ``c.copy_from(c)`` is the same +object twice. + +Not yet supported, each an error naming the function: virtual functions, +member functions of a class with bases, constructors and destructors, +conversion functions, and member functions of a class template (whose +contracts are ignored with a warning). A member call inside an expression +(``return n + get();``) is unsupported as a free function's is; assign its +result first. + ``cppverify::type_invariant`` ----------------------------- From ffb7284491309774e0a68f3308a4962ad6f729e3 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 12:38:18 +0530 Subject: [PATCH 75/94] docs: Leave only templates unverified in the functions page. --- website/source/language/functions-loops.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/source/language/functions-loops.rst b/website/source/language/functions-loops.rst index 5bae278634..81312437e9 100644 --- a/website/source/language/functions-loops.rst +++ b/website/source/language/functions-loops.rst @@ -218,16 +218,16 @@ Spec functions stay total, as in Verus and ACSL: a spec's division by zero is 0, so applying a spec in a contract adds no check, and ``cppverify::recommends`` remains a warning (see :doc:`ghost-proofs`). -Templates and member functions ------------------------------- +Templates +--------- -Templates and member functions compile with contracts, but they are not -verified yet: +Templates compile with contracts, but they are not verified yet (member +functions of ordinary classes are; see :doc:`structs`): - The contracts of a function template, of a class template's members, and a class template's type invariants compile, instantiate with the template, and are ignored, with a ``-Wcontract-unsupported`` warning that names the - template. Contracts on a member function are ignored the same way. + template. - A template without contracts changes nothing for the rest of the file, as long as verified code does not call it. From fc0b6a1abdfb7a37e244404ab8a01259e0f2af61 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 12:38:19 +0530 Subject: [PATCH 76/94] docs: List member functions as supported in the limitations. --- website/source/language/limitations.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/website/source/language/limitations.rst b/website/source/language/limitations.rst index f412110afd..c89d899ddb 100644 --- a/website/source/language/limitations.rst +++ b/website/source/language/limitations.rst @@ -156,11 +156,13 @@ C++ feature boundaries nested/pointer/array-bearing records used by value, and a whole record or array read through a pointer (``Ring copy = *r;``). * - Functions - - Free functions, overloads, namespaces, contracts, direct calls, and + - Free functions, member functions of classes without bases or virtual + functions (:doc:`structs`), overloads, namespaces, contracts, direct calls, and direct and mutual recursion, also through stores to pointer and reference parameters and through automatic objects, with mutual recursion among functions of one kind (spec, proof, or executable). - - Member functions (their contracts are ignored with a warning), + - Virtual functions, member functions of classes with bases, + constructors, destructors, conversion functions, templates (one without contracts stops nothing unless verified code calls it; the contracts of a function or class template compile, instantiate, and are ignored with a warning, and one with contract @@ -209,9 +211,9 @@ otherwise supported code: - ``cppverify::aliases`` between pointers to different types (``aliases between different pointee types are unsupported``). -Contracts on member functions are instead ignored with a warning -(``-Wcontract-unsupported``: "contracts on member functions are not yet -supported and are ignored ..."), and the run continues with the other +A member function of a class template has its contracts ignored with a +warning (``-Wcontract-unsupported``: "contracts in class template ... are +not yet supported and are ignored"), and the run continues with the other functions. Objects, classes, and RAII From 8771ab45156307e3ab8f0009b02ccd07c3efb39f Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 12:38:19 +0530 Subject: [PATCH 77/94] docs: Note member functions 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 b37798babb..a59272ee8e 100644 --- a/website/source/release-notes.rst +++ b/website/source/release-notes.rst @@ -286,6 +286,16 @@ reseated: ``c.n`` and ``c.buf[i]`` are cells of the object, ``&c.n`` and a reference can be bound to ``*p``, ``p->left``, ``a[i]``, or another reference (:doc:`language/structs`). +**Member functions.** Contracts on a member function written in its class +were ignored with a warning, and one defined outside its class was an error +(``member function verification is unsupported``). A member function of a +class without bases or virtual functions is verified now, as a function of +``this``: fields are cells of the object, ``c.bump()`` passes ``&c``, its +clauses may name members declared after them (the complete-class context of +C++26 contracts), and a non-const one without ``cppverify::modifies`` may +write its object. Virtual functions, constructors, destructors, and +conversion functions are errors naming the function (:doc:`language/structs`). + **Extents of pointer fields.** ``cv::valid(s->data, s->len)`` was an error (``valid marker requires a bare complete-object pointer and an integer element count``), so a record that describes a buffer by a pointer and a length (a From 3d45aef8a61ffe4ec8502a70bc1df0319f904823 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 12:38:20 +0530 Subject: [PATCH 78/94] docs: Describe member functions in the design. --- docs/DESIGN.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index b1a28c95e6..922e306a5a 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1269,6 +1269,35 @@ void reset_left(Pair &p) extent in a type invariant (B3's class invariants), and `reads` over a field. +### 3f. Member functions + +- A non-static member function of a class without bases or virtual + functions is a function whose first parameter is `this`, a pointer to its + object with the validity of a reference parameter (non-null, live, + initialized at entry), to const in a `const` member function, as Dafny's + methods take `this` and Verus's `&self`/`&mut self`. It takes part in the + default separation of address parameters; `cppverify::aliases(this, p)` + opts out. Fields, written `f` or `this->f`, are places through `this`. +- Clauses are parsed in the complete-class context, as a C++26 function + contract specifier is: an in-class declaration's tokens are kept with its + late-parsed declaration (as its exception specification is) and parsed + with its parameters and `this` once the class is complete; an out-of-line + definition's clauses are in its class's scope. +- Frame (decision Q8): a verified non-const member function without + `cppverify::modifies` may write `*this`; a `const` one only its own storage, + `mutable` fields only when listed; an explicit `cppverify::modifies` + replaces the default; a trusted or bodiless one keeps the trusted-contract + rule. +- A call passes the object's address first: `p->m()` passes `p`, `obj.m()` + binds `obj` as a reference argument does (a local object is lent to the + callee under 3d's rules), and `m()` in a member function passes `this`. A + static member function is a free function. +- Not yet, each an error naming the function: virtual functions (dispatch + needs behavioral subtyping), member functions of classes with bases, + constructors and destructors, conversion functions, explicit object + parameters, rvalue- and volatile-qualified member functions; members of + class templates keep the template rule. + ### 4. `cppverify::modifies(...)` frame condition ```cpp From 5cebae026df3665705ab41bd71d4a9a25476ca9d Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 12:38:21 +0530 Subject: [PATCH 79/94] docs: Describe member functions and the late parse of their clauses in the architecture. --- docs/ARCHITECTURE.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6884016ef8..64fbc1259c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -223,6 +223,32 @@ removes a pair's single-object separation where one of the pair has an extent, so the extent separation (`appendExtentSeparation`) alone states it. +**Member functions:** `unsupportedMemberFunction` decides which member +functions `convertTranslationUnit` converts: static ones, as free functions, +and non-static ones of a class without bases or virtual functions (not +constructors, destructors, conversion functions, explicit object, rvalue- or +volatile-qualified ones; a contracted one of these is an error of its +function). `convertFunction` names a member function with its class and +puts `this` first in `Params`, a pointer to `getFunctionObjectParameterType()` +(const in a const member function, `ConstAddressParams`), with the generated +validity of a reference (non-null, valid, initialized), an `AddressEntry` in +the separation, a `RecordParams` entry for its class (so a local object is +lent as 3d lends one), and, for a verified non-const one without +`cppverify::modifies`, the region footprint `*this` (decision Q8). +`CXXThisExpr` converts to the `this` variable, so `this->f` and an implicit +`f` are places through a pointer as `p->f` is. `convertExecCallArgs` +prepends the object of a `CXXMemberCallExpr`: the pointer of `p->m()` and +`this->m()`, else the address of the object, bound as a reference argument +is; address discovery promotes a local whose member function is called. The +parser keeps an in-class member function's clause tokens +(`Parser::MemberContractTokens`, then +`LateParsedMethodDeclaration::ContractTokens`) and parses them in +`ParseLexedMethodDeclaration` after the exception specification, with the +parameters and `this` in scope (`ParseFunctionContractClauseList`, the part +of `ParseFunctionContractClauses` after the declarator's scopes), before the +late-parsed body; an out-of-line definition's clauses enter its class's +scope and `this`, as its trailing requires-clause does. + **Pointers into objects:** `ASTConverter::subobjectPointer` lowers a decayed member array of a pointee and the address of a pointee's element, field, or nested record. In a statement (`SubobjectSink`) it asserts the place's access From 6e1b3f89df41ee3695d2920794de7ba41924d0a6 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 13:02:00 +0530 Subject: [PATCH 80/94] 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 81/94] 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 From 3513ea8ab136de83d0c3f46de73846907ac19b1e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 13:06:24 +0530 Subject: [PATCH 82/94] verify: Assume an invariant's definedness at its loop's head, where a loop that stores to local storage keeps what was initialized. --- clang/lib/Verify/Transform/Passivize.cpp | 99 ++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 6 deletions(-) diff --git a/clang/lib/Verify/Transform/Passivize.cpp b/clang/lib/Verify/Transform/Passivize.cpp index 668f007ed4..79535d10f8 100644 --- a/clang/lib/Verify/Transform/Passivize.cpp +++ b/clang/lib/Verify/Transform/Passivize.cpp @@ -4292,6 +4292,57 @@ class PassivizerImpl { P.ResultVarName = ResultName; } + /// Whether S starts a lifetime, whose storage is then uninitialized. + static bool startsLifetime(const VStmt &S) { + switch (S.K) { + case VStmt::Allocate: + return true; + case VStmt::If: { + const auto &I = static_cast(S); + return llvm::any_of(I.Then, + [](const auto &T) { return startsLifetime(*T); }) || + llvm::any_of(I.Else, + [](const auto &E) { return startsLifetime(*E); }); + } + case VStmt::While: + return llvm::any_of(static_cast(S).Body, + [](const auto &B) { return startsLifetime(*B); }); + case VStmt::GhostBlock: + return llvm::any_of(static_cast(S).Body, + [](const auto &B) { return startsLifetime(*B); }); + case VStmt::Seq: + return llvm::any_of(static_cast(S).Stmts, + [](const auto &B) { return startsLifetime(*B); }); + default: + return false; + } + } + + /// `forall a. initialized_before[a] -> initialized_after[a]`, triggered by + /// reads of the later state. + static std::unique_ptr initializationKept(const std::string &Before, + const std::string &After, + SourceLocation Loc) { + const std::string Binder = "__init_address"; + auto Address = [&] { + return std::make_unique( + VBinOp::Add, std::make_unique(0, VType::makePtr(), Loc), + std::make_unique(Binder, VType::makeInt32(VIntMode::Math), + Loc), + VType::makePtr(), Loc); + }; + auto WasInitialized = + std::make_unique(Address(), VType::makeBool(), Loc, Before); + auto IsInitialized = std::make_unique( + std::make_unique(Address(), VType::makeBool(), Loc, After), + VType::makeBool(), VType::makeBool(), Loc, /*IsTrigger=*/true); + return std::make_unique( + Binder, nullptr, nullptr, + makeOr(makeNot(std::move(WasInitialized), Loc), + std::move(IsInitialized), Loc), + Loc, VType::makeInt32(VIntMode::Math)); + } + void collectModified(const VStmt &S, std::set &Out) const { switch (S.K) { case VStmt::Assign: @@ -4299,6 +4350,9 @@ class PassivizerImpl { break; case VStmt::Store: Out.insert(VHeapName); + // A store to local or dynamic storage initializes the cell. + if (hasPointerProvenance(static_cast(S).Ptr.get())) + Out.insert(VInitializationHeapName); break; case VStmt::Allocate: { const auto &A = static_cast(S); @@ -4405,12 +4459,10 @@ class PassivizerImpl { /// A contract expression's memory checks, recorded before spec inlining, /// join its other checks, one obligation per kind. - void emitExprSafety(PassiveProgram &P, const VExpr *E, const VExpr *Guard, - SourceLocation Loc, - const std::map &Renames, - bool BridgeMachineValue = false, - const VExpr *SafetySource = nullptr, - const std::vector *MemoryChecks = nullptr) { + /// The definedness checks of E, or of SafetySource, the clause as written + /// that E instantiates, with the memory checks recorded for it. + SafetyChecks exprSafetyChecks(const VExpr *E, const VExpr *SafetySource, + const std::vector *MemoryChecks) { SafetyChecks Checks; collectSafety( SafetySource ? SafetySource : E, &FnMap, @@ -4420,6 +4472,16 @@ class PassivizerImpl { if (MemoryChecks && !Fn.TotalExpressions) for (const VMemoryCheck &Check : *MemoryChecks) Checks.push_back({Check.Kind, cloneVExpr(Check.Cond.get())}); + return Checks; + } + + void emitExprSafety(PassiveProgram &P, const VExpr *E, const VExpr *Guard, + SourceLocation Loc, + const std::map &Renames, + bool BridgeMachineValue = false, + const VExpr *SafetySource = nullptr, + const std::vector *MemoryChecks = nullptr) { + SafetyChecks Checks = exprSafetyChecks(E, SafetySource, MemoryChecks); groupByKind(Checks); CloneCtx Ctx{Renames, OldState, false}; for (const SafetyCheck &Check : Checks) @@ -4428,6 +4490,22 @@ class PassivizerImpl { emitMathBridge(P, E, Guard, Loc, BridgeMachineValue); } + /// The definedness of an invariant, assumed at its loop's head: every + /// path into the head (entry, the end of an iteration, a continue) asserts + /// it with the invariant, so at the head it holds by the same induction, + /// and the reads it makes, of memory written in the loop included, are + /// initialized and in bounds there. + void + assumeInvariantSafety(PassiveProgram &P, const VWhileStmt &W, + const VExpr *Inv, const VExpr *Guard, + const std::map &Renames) { + CloneCtx Ctx{Renames, OldState, false}; + for (const SafetyCheck &Check : + exprSafetyChecks(Inv, Inv, invariantChecks(W, Inv))) + emitPassive(P, PassiveStmt::Assume, cloneExpr(Check.Cond.get(), Ctx), + Guard, Inv->Loc); + } + void emitMathBridge(PassiveProgram &P, const VExpr *E, const VExpr *Guard, SourceLocation Loc, bool BridgeMachineValue = false) { llvm::SaveAndRestore Exact(Origin, "exact"); @@ -6436,6 +6514,8 @@ class PassivizerImpl { std::set Modified; for (const auto &Body : W.Body) collectModified(*Body, Modified); + const bool StartsLifetime = llvm::any_of( + W.Body, [](const auto &Body) { return startsLifetime(*Body); }); // A loop in the object model writes only the objects its stores and // calls reach; the rest of the heap keeps its values. std::vector WriteRegions; @@ -6468,6 +6548,12 @@ class PassivizerImpl { Renames[Name] = HeadName; emitInactiveFrame(P, PreviousName, HeadName, typeForName(Name), Enters.get(), W.Loc); + // Without a lifetime starting in it, a loop only initializes + // storage: what was initialized before it still is at its head. + if (Name == VInitializationHeapName && !StartsLifetime) + emitPassive(P, PassiveStmt::Assume, + initializationKept(PreviousName, HeadName, W.Loc), + Active.get(), W.Loc); if (Framed && Name == VHeapName) { const bool OnlyCells = !WriteRegions.empty() && @@ -6515,6 +6601,7 @@ class PassivizerImpl { } Origin = "inv"; for (const auto &Inv : W.Invariants) { + assumeInvariantSafety(P, W, Inv.get(), Active.get(), Renames); auto BoundInv = cloneExpr(Inv.get(), HeadCtx); emitPassive(P, PassiveStmt::Assume, std::move(BoundInv), Active.get(), W.Loc); From 962251b8ec2187ed248729365b25ee9603b6090c Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 13:06:24 +0530 Subject: [PATCH 83/94] test: Check loops that initialize local storage and invariants whose reads bound the head. --- .../test/Verify/suite/loop_initialization.cpp | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 clang/test/Verify/suite/loop_initialization.cpp diff --git a/clang/test/Verify/suite/loop_initialization.cpp b/clang/test/Verify/suite/loop_initialization.cpp new file mode 100644 index 0000000000..be551b7d30 --- /dev/null +++ b/clang/test/Verify/suite/loop_initialization.cpp @@ -0,0 +1,147 @@ +// RUN: not %cpp-verify --jobs=1 %s -- > %t 2>&1 +// RUN: FileCheck %s < %t +// RUN: FileCheck %s --check-prefix=SOUND < %t +// RUN: not %cpp-verify --jobs=1 --backend=bmc --unroll=5 %s -- 2>&1 \ +// RUN: | FileCheck %s --check-prefix=BMC +// +// An invariant holds at a loop's head together with its definedness: both +// are checked on entry and at the end of every iteration, so the reads it +// makes are initialized and in bounds there. A loop starts no lifetime, so +// it only initializes storage: what was initialized before it still is. +// +// No proof here holds only because no execution reaches its end. +// SOUND-NOT: vacuous + +int filled() + cppverify::post(cppverify::result == 0) +{ + int a[4]; + for (int i = 0; i < 4; i = i + 1) + cppverify::invariant(0 <= i && i <= 4) + cppverify::invariant(cppverify::forall(k, 0, i, a[k] == 0)) + cppverify::decreases(4 - i) + { + a[i] = 0; + } + return a[3]; +} +// CHECK-DAG: Verified: filled [backend=z3] +// BMC-DAG: Verified: filled [backend=bmc, bound=4] + +// The loop reads what was initialized before it, which no invariant states. +void incremented() +{ + int a[4]; + for (int j = 0; j < 4; j = j + 1) + cppverify::invariant(0 <= j && j <= 4) + cppverify::invariant(cppverify::forall(k, 0, j, a[k] == 1)) + cppverify::decreases(4 - j) + { + a[j] = 1; + } + for (int i = 0; i < 4; i = i + 1) + cppverify::invariant(0 <= i && i <= 4) + cppverify::decreases(4 - i) + { + a[i] = a[i] / 2; + } +} +// CHECK-DAG: Verified: incremented [backend=z3] + +// Storage the loop does not write stays initialized. +int kept() +{ + int x = 5; + int &r = x; + int a[2]; + for (int i = 0; i < 2; i = i + 1) + cppverify::invariant(0 <= i && i <= 2) + cppverify::decreases(2 - i) + { + a[i] = i; + } + return r; +} +// CHECK-DAG: Verified: kept [backend=z3] + +// An element the loop never writes is not initialized after it. +int partially_filled() +{ + int a[4]; + for (int i = 0; i < 3; i = i + 1) + cppverify::invariant(0 <= i && i <= 3) + cppverify::invariant(cppverify::forall(k, 0, i, a[k] == 0)) + cppverify::decreases(3 - i) + { + a[i] = 0; + } + return a[3]; +} +// CHECK-DAG: error: verification failed: partially_filled [{{.*}}::initialization@[[@LINE-2]]:3] + +// Nor is one it reads before writing. +int read_first() +{ + int a[4]; + int s = 0; + for (int i = 0; i < 4; i = i + 1) + cppverify::invariant(0 <= i && i <= 4) + cppverify::decreases(4 - i) + { + s = a[i]; + a[i] = s; + } + return 0; +} +// CHECK-DAG: error: verification failed: read_first [{{.*}}::initialization@[[@LINE-5]]:7] + +// Reading p[i] in an invariant bounds i at the head. +int last(const int *p, int n) + cppverify::pre(cppverify::valid(p, n) && n > 0) + cppverify::post(cppverify::result == p[n - 1]) +{ + int i = 0; + while (i + 1 < n) + cppverify::invariant(p[i] + 0 == p[i]) + cppverify::decreases(n - i) + { + i = i + 1; + } + return p[i]; +} +// CHECK-DAG: Verified: last [backend=z3] + +// So does a shift: its amount is in range at the head. +int log2_floor(int n) + cppverify::pre(n >= 1) + cppverify::post(0 <= cppverify::result && cppverify::result <= 31) +{ + int k = 0; + while (k < 30 && (1 << (k + 1)) <= n) + cppverify::invariant((1 << k) <= n) + cppverify::decreases(30 - k) + { + k = k + 1; + } + return k; +} +// CHECK-DAG: Verified: log2_floor [backend=z3] + +// The definedness is still checked where the invariant is. +void overrun() +{ + int c[4]; + c[0] = 0; + c[1] = 0; + c[2] = 0; + c[3] = 0; + int i = 0; + while (i < 4) + cppverify::invariant(0 <= i && i <= 4) + cppverify::invariant(c[i] == 0) + cppverify::decreases(4 - i) + { + i = i + 1; + } +} +// CHECK-DAG: error: verification failed: overrun [{{.*}}::dereference@[[@LINE-6]]:31#2] From cf024f055493178d1c28316f529bf00477f84b2b Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 13:40:51 +0530 Subject: [PATCH 84/94] verify: Assume at a loop's head only the definedness checks whose condition reads what the loop changes. --- clang/lib/Verify/Transform/Passivize.cpp | 96 ++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 8 deletions(-) diff --git a/clang/lib/Verify/Transform/Passivize.cpp b/clang/lib/Verify/Transform/Passivize.cpp index 79535d10f8..40b60a45ce 100644 --- a/clang/lib/Verify/Transform/Passivize.cpp +++ b/clang/lib/Verify/Transform/Passivize.cpp @@ -2927,6 +2927,82 @@ applicationKey(const VSpecCallExpr &C, return true; } +/// The condition a definedness check asks for, without the guards it holds +/// under: `!g || c` as built by makeImplies, and the range of a quantified +/// check. +static const VExpr *checkedCondition(const VExpr *E) { + while (E) { + if (E->K == VExpr::BinOp) { + const auto *B = static_cast(E); + if (B->Op == VBinOp::Or && B->Lhs->K == VExpr::UnaryOp && + static_cast(B->Lhs.get())->Op == + VUnaryOp::Not) { + E = B->Rhs.get(); + continue; + } + } + if (E->K == VExpr::Forall) { + E = static_cast(E)->Body.get(); + continue; + } + return E; + } + return E; +} + +/// Whether \p E may have another value at a loop's head than before the +/// loop: it reads a variable, a field, or a heap the loop modifies, outside +/// cppverify::old, which reads the entry state. +static bool readsLoopState(const VExpr *E, + const std::set &Modified) { + if (!E) + return false; + switch (E->K) { + case VExpr::Old: + return false; + case VExpr::Var: { + const auto *V = static_cast(E); + return Modified.count(V->Name) || Modified.count(V->ProvenanceVariable); + } + case VExpr::FieldAccess: { + const auto *F = static_cast(E); + if (F->Base->K == VExpr::Var) { + const std::string Prefix = + static_cast(F->Base.get())->Name + "."; + for (const std::string &Name : Modified) + if (llvm::StringRef(Name).starts_with(Prefix)) + return true; + } + break; + } + case VExpr::Load: + if (Modified.count(VHeapName)) + return true; + break; + case VExpr::SpecCall: + if (static_cast(E)->ReadsHeap && + Modified.count(VHeapName)) + return true; + break; + case VExpr::UnaryOp: { + const auto *U = static_cast(E); + if ((U->Op == VUnaryOp::ValidPtr && (Modified.count(VAllocationHeapName) || + Modified.count(VLivenessHeapName))) || + (U->Op == VUnaryOp::InitializedPtr && + Modified.count(VInitializationHeapName))) + return true; + break; + } + default: + break; + } + bool Found = false; + forEachVExprChild(E, [&](const VExpr *Child) { + Found = Found || readsLoopState(Child, Modified); + }); + return Found; +} + /// Whether \p E mentions the variable \p Name free. static bool mentionsFree(const VExpr *E, const std::string &Name) { if (!E) @@ -4494,16 +4570,20 @@ class PassivizerImpl { /// path into the head (entry, the end of an iteration, a continue) asserts /// it with the invariant, so at the head it holds by the same induction, /// and the reads it makes, of memory written in the loop included, are - /// initialized and in bounds there. - void - assumeInvariantSafety(PassiveProgram &P, const VWhileStmt &W, - const VExpr *Inv, const VExpr *Guard, - const std::map &Renames) { + /// initialized and in bounds there. A check whose condition reads nothing + /// the loop modifies is left out: what it asks for holds at the head as it + /// did before the loop, and its guards alone, of state the loop changes, + /// are what an invariant states itself. + void assumeInvariantSafety(PassiveProgram &P, const VWhileStmt &W, + const VExpr *Inv, const VExpr *Guard, + const std::map &Renames, + const std::set &Modified) { CloneCtx Ctx{Renames, OldState, false}; for (const SafetyCheck &Check : exprSafetyChecks(Inv, Inv, invariantChecks(W, Inv))) - emitPassive(P, PassiveStmt::Assume, cloneExpr(Check.Cond.get(), Ctx), - Guard, Inv->Loc); + if (readsLoopState(checkedCondition(Check.Cond.get()), Modified)) + emitPassive(P, PassiveStmt::Assume, cloneExpr(Check.Cond.get(), Ctx), + Guard, Inv->Loc); } void emitMathBridge(PassiveProgram &P, const VExpr *E, const VExpr *Guard, @@ -6601,7 +6681,7 @@ class PassivizerImpl { } Origin = "inv"; for (const auto &Inv : W.Invariants) { - assumeInvariantSafety(P, W, Inv.get(), Active.get(), Renames); + assumeInvariantSafety(P, W, Inv.get(), Active.get(), Renames, Modified); auto BoundInv = cloneExpr(Inv.get(), HeadCtx); emitPassive(P, PassiveStmt::Assume, std::move(BoundInv), Active.get(), W.Loc); From d0a17ceda99da37fded50b6f7c3f05d270e1c2b8 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 14:05:02 +0530 Subject: [PATCH 85/94] docs: Describe an invariant's definedness at the head and initialization across loops, with a checked example. --- website/source/language/functions-loops.rst | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/website/source/language/functions-loops.rst b/website/source/language/functions-loops.rst index 81312437e9..bd8e833288 100644 --- a/website/source/language/functions-loops.rst +++ b/website/source/language/functions-loops.rst @@ -458,6 +458,56 @@ After a normal exit the verifier knows ``I && !c`` about what the loop may change; variables the loop does not assign and memory outside its write set keep their values. A ``break`` or ``return`` leaves in its own state instead. +An invariant holds together with its definedness: establishment and +preservation check that what it reads is in bounds and initialized, and that +its shifts and divisions are defined, so the head of every iteration knows +both. A loop that fills a local array states what it has written, and the +reads that state it are what show those elements initialized, which no +contract can say directly: + +.. cppverify-example: fails partially_filled + +.. code-block:: cpp + + int filled() + cv::post(cv::result == 0) + { + int a[4]; + for (int i = 0; i < 4; i = i + 1) + cv::invariant(0 <= i && i <= 4) + cv::invariant(cv::forall(k, 0, i, a[k] == 0)) + cv::decreases(4 - i) + { + a[i] = 0; + } + return a[3]; + } + + int partially_filled() + { + int a[4]; + for (int i = 0; i < 3; i = i + 1) + cv::invariant(0 <= i && i <= 3) + cv::invariant(cv::forall(k, 0, i, a[k] == 0)) + cv::decreases(3 - i) + { + a[i] = 0; + } + return a[3]; + } + +.. code-block:: text + + Verified: filled [backend=z3] + fill.cpp:26:3: error: verification failed: partially_filled [...::initialization@26:3] (counterexample: ...) [backend=z3] [reason=counterexample] + +No lifetime begins or ends inside a loop, so what was initialized before it +stays initialized in every iteration and after it; an element the loop may +write is initialized at its head only where an invariant reads it. Before +0.2 the head kept the initialization of the state before the loop: every +iteration after the first saw the elements the earlier ones wrote as +uninitialized, and ``filled`` failed its ``initialization`` check. + .. note:: Contracts are mathematical, but the loop body runs on machine integers. From fcd1d6c5ec8f10b03f9e9d4e33801baca8c2d499 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 14:05:02 +0530 Subject: [PATCH 86/94] docs: Note loops that initialize local storage in the release notes. --- website/source/release-notes.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/website/source/release-notes.rst b/website/source/release-notes.rst index a59272ee8e..03051ee5b4 100644 --- a/website/source/release-notes.rst +++ b/website/source/release-notes.rst @@ -135,6 +135,14 @@ correct. after it could fail for values it never produces. Its state now carries over (:doc:`language/functions-loops`). +**Loops that initialize local storage.** Every iteration of a loop after the +first saw the local storage the earlier ones wrote as uninitialized, so a +loop filling a local array failed its ``initialization`` check, at the +invariant that stated what it had written or at a read after the loop. What +was initialized before a loop now stays initialized, and an invariant holds +at the head of each iteration with its definedness, which shows the elements +it reads initialized and in bounds (:doc:`language/functions-loops`). + **Null minus null.** The difference of two null pointers failed its ``pointer-difference`` check; it is 0, as C++ defines (:doc:`language/pointers`). From b2e73ca748feb126b5ccc8e0728c571457685eef Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 14:05:03 +0530 Subject: [PATCH 87/94] docs: State that an invariant holds with its definedness at the head in the design. --- docs/DESIGN.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 922e306a5a..373beb197a 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -231,6 +231,13 @@ while (i < n) - `cppverify::invariant(expr)`: must hold on entry and be preserved by each iteration. A loop whose condition is the constant false keeps every value it would change: after `while (false) { ... }` the state is the one before it. +- An invariant holds at the head of every iteration with its definedness, + since both are checked on entry and at the end of each iteration: the + reads it makes are in bounds and initialized there. A loop that fills a + local array states what it has written, + `cppverify::forall(k, 0, i, a[k] == 0)`, and that is what shows `a[k]` + initialized for `k < i`. No lifetime begins or ends in a loop, so what was + initialized before it stays initialized. - `cppverify::decreases(expr [, expr...])`: termination measure. Single expression must be non-negative and strictly decreasing. Tuple form is lex-ordered: `cppverify::decreases(a, b)` means `(a, b)` strictly decreases lexicographically, and only the first component that changes must stay non-negative. - Verification is total correctness, as in Verus: every executable loop needs `cppverify::decreases`, unless its condition is the constant false, so that From 57b9e8e3a8e982c6409fea7805c73058b71cf68c Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 14:05:03 +0530 Subject: [PATCH 88/94] docs: Describe definedness and initialization at a loop's head in the architecture. --- docs/ARCHITECTURE.md | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 64fbc1259c..7d28924600 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -491,13 +491,14 @@ y_0 = x_3 + 1; ``` // while (cond) invariant(I) decreases(D) { body } -assert(I); // 1. invariant on entry +assert(defined(I)); assert(I); // 1. invariant on entry havoc(modified_vars + mem); // 2. forget loop-modified state + heap assume(frame(mem_entry, mem)); // cells outside the loop's write set kept -assume(I); // 3. inductive hypothesis +assume(init_entry => init); // initialization kept +assume(defined(I)); assume(I); // 3. inductive hypothesis if (cond) { // 4. if loop continues: [body in SSA] // execute one iteration - assert(I); // invariant preserved + assert(defined(I)); assert(I); // invariant preserved assert(D_new < D_old); // termination measure decreases assert(frame(mem_entry, mem)); // explicit loop modifies respected assume(false); // cut path @@ -518,6 +519,28 @@ frame is a chain of stores of fresh values; otherwise it is a An explicit loop `cppverify::modifies` is also asserted at the end of each iteration and at each `continue` (obligation kind `frame`, located at the footprint). +**Definedness and initialization at the head.** Entry, the end of an +iteration, and each `continue` assert an invariant's definedness before the +invariant (`emitExprSafety`: its core checks and the memory checks +`instrumentUBChecks` recorded in `VWhileStmt::InvariantChecks`), so the head +assumes both (`assumeInvariantSafety`, over the same `exprSafetyChecks`): +what an invariant reads is in bounds and initialized there. Initialization +cannot be stated in a contract, so this is how a loop that fills local +storage knows what it has written. A store through local or dynamic storage +sets its cell's initialization bit, so it modifies the initialization heap +(`collectModified`), whose head version only grows: no lifetime begins or +ends in a loop (the converter rejects addressable declarations, allocation, +and deallocation there, and `startsLifetime` withholds the fact from a loop +that starts one), so the head assumes `forall a. init_entry[a] -> +init_head[a]`, triggered by reads of the head version +(`initializationKept`). Nothing frames the rest, since no claim needs a cell +to be uninitialized. Before 0.2 the head kept the initialization heap from +before the loop, and every iteration after the first saw the storage the +earlier ones wrote as uninitialized. Dafny checks an invariant's +well-formedness once, in the head's arbitrary state, from the invariants +before it, which suffices where storage is always initialized; here it is +proved where the invariant is. + **Termination.** An executable loop without `cppverify::decreases` contributes no measure obligation, and the driver reports the function `Unresolved` with `decreases.missing` unless BMC proved the unwinding or the loop's condition is From d879449f4dec9b96e9f517fca4c1ca15f26b0b96 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 14:15:35 +0530 Subject: [PATCH 89/94] verify: Frame a loop that writes local or dynamic storage by the cells of the objects it writes. --- clang/lib/Verify/Transform/Passivize.cpp | 120 +++++++++++++++++++++-- 1 file changed, 111 insertions(+), 9 deletions(-) diff --git a/clang/lib/Verify/Transform/Passivize.cpp b/clang/lib/Verify/Transform/Passivize.cpp index 40b60a45ce..63ce11db4a 100644 --- a/clang/lib/Verify/Transform/Passivize.cpp +++ b/clang/lib/Verify/Transform/Passivize.cpp @@ -3517,6 +3517,9 @@ class PassivizerImpl { /// parameter without one. std::vector OriginObjects; std::set RepresentedAllocationPointers; + /// The type and size of the object each of those pointers was defined to + /// address. + std::map> RepresentedObjects; std::vector StoredPointerCells; std::set HeapBases; std::set HeapVariables; @@ -3890,19 +3893,107 @@ class PassivizerImpl { return true; } + /// The one cell a store through \p Address writes, when the address is a + /// pointer into local or dynamic storage (a reference to a local, say) that + /// the loop assigns neither nor its lifetime companion. + std::optional + representedCell(const VExpr *Address, const VType &CellType, + const std::map &Renames, + const std::set &Assigned, SourceLocation Loc) { + while (Address && Address->K == VExpr::Cast) + Address = static_cast(Address)->Inner.get(); + if (!Address || Address->K != VExpr::Var) + return std::nullopt; + const auto *Pointer = static_cast(Address); + if (Pointer->ProvenanceVariable.empty() || Assigned.count(Pointer->Name) || + Assigned.count(Pointer->ProvenanceVariable)) + return std::nullopt; + CloneCtx Ctx{Renames, OldState, false}; + auto Cell = cloneExpr(Address, Ctx); + return cellRegion(Cell.get(), CellType, Loc); + } + + /// The local or dynamic object that the pointer its allocation defined + /// addresses, when the loop assigns neither the pointer nor its lifetime + /// companion: every access through it lies in the object, at a field of + /// its layout and an index its fixed array bounds (a dynamic object is one + /// scalar). The object is its scalar leaves, each one cell, so the loop's + /// frame stays a chain of stores; without a layout it is its bytes. + bool representedRegions(const VExpr *Root, + const std::map &Renames, + const std::set &Assigned, + SourceLocation Loc, std::vector &Out) { + if (!Root || Root->K != VExpr::Var) + return false; + const auto *Pointer = static_cast(Root); + auto Object = RepresentedObjects.find(Pointer->Name); + if (Object == RepresentedObjects.end() || + Pointer->ProvenanceVariable.empty() || Assigned.count(Pointer->Name) || + Assigned.count(Pointer->ProvenanceVariable)) + return false; + const auto &[Type, Bytes] = Object->second; + CloneCtx Ctx{Renames, OldState, false}; + auto Base = cloneExpr(Root, Ctx); + auto At = [&](uint64_t Offset) { + return addBytes(cloneVExpr(Base.get()), + std::make_unique(std::to_string(Offset), + pointerOffsetType(), Loc), + Loc); + }; + if (Type.isInt() || Type.Kind == VTypeKind::Bool || + Type.Kind == VTypeKind::Ptr) { + Out.push_back(cellRegion(Base.get(), Type, Loc)); + return true; + } + const auto Layout = llvm::find_if(Fn.Layouts, [&](const VObjectLayout &L) { + return L.TypeIdentity == Type.TypeIdentity; + }); + std::vector Cells; + if (Layout != Fn.Layouts.end() && Layout->SizeBytes == Bytes) { + for (const VObjectLeaf &Leaf : Layout->Leaves) { + std::vector Offsets = {Leaf.OffsetBytes}; + for (const VObjectRepeat &Repeat : Leaf.Repeats) { + std::vector Next; + for (uint64_t Start : Offsets) + for (uint64_t K = 0; K < Repeat.Count && Next.size() <= Bytes; ++K) + Next.push_back(Start + K * Repeat.StrideBytes); + Offsets = std::move(Next); + } + for (uint64_t Offset : Offsets) + if (Cells.size() <= Bytes) + Cells.push_back(cellRegion(At(Offset).get(), Leaf.Ty, Loc)); + } + if (!Cells.empty() && Cells.size() <= Bytes) { + for (Region &Cell : Cells) + Out.push_back(std::move(Cell)); + return true; + } + } + Out.push_back( + Region{cloneVExpr(Base.get()), At(std::max(Bytes, 1))}); + return true; + } + bool loopWriteRegions(const std::vector> &Stmts, const std::map &Renames, + const std::set &Assigned, std::vector &Out) { for (const auto &S : Stmts) { switch (S->K) { case VStmt::Store: { const auto &St = static_cast(*S); auto R = parameterCell(St.Ptr.get(), St.Value->Ty, Renames, St.Loc); + if (!R) + R = representedCell(St.Ptr.get(), St.Value->Ty, Renames, Assigned, + St.Loc); if (!R && originRegions(addressRoot(St.Ptr.get()), Renames, St.Loc, Out)) break; if (!R) R = rootRegion(addressRoot(St.Ptr.get()), Renames, St.Loc); + if (!R && representedRegions(addressRoot(St.Ptr.get()), Renames, + Assigned, St.Loc, Out)) + break; if (!R) return false; Out.push_back(std::move(*R)); @@ -3953,14 +4044,22 @@ class PassivizerImpl { [&](const VValidExtent &Extent) { return Extent.Base == Param; }))); - if (OneCell) - if (auto Cell = parameterCell(Actual, Target->Ty, Renames, C.Loc)) { + if (OneCell) { + auto Cell = parameterCell(Actual, Target->Ty, Renames, C.Loc); + if (!Cell) + Cell = + representedCell(Actual, Target->Ty, Renames, Assigned, C.Loc); + if (Cell) { Out.push_back(std::move(*Cell)); continue; } + } if (originRegions(addressRoot(Actual), Renames, C.Loc, Out)) continue; auto R = rootRegion(addressRoot(Actual), Renames, C.Loc); + if (!R && representedRegions(addressRoot(Actual), Renames, Assigned, + C.Loc, Out)) + continue; if (!R) return false; Out.push_back(std::move(*R)); @@ -3972,19 +4071,19 @@ class PassivizerImpl { return false; case VStmt::If: { const auto &I = static_cast(*S); - if (!loopWriteRegions(I.Then, Renames, Out) || - !loopWriteRegions(I.Else, Renames, Out)) + if (!loopWriteRegions(I.Then, Renames, Assigned, Out) || + !loopWriteRegions(I.Else, Renames, Assigned, Out)) return false; break; } case VStmt::While: - if (!loopWriteRegions(static_cast(*S).Body, - Renames, Out)) + if (!loopWriteRegions(static_cast(*S).Body, Renames, + Assigned, Out)) return false; break; case VStmt::Seq: - if (!loopWriteRegions(static_cast(*S).Stmts, - Renames, Out)) + if (!loopWriteRegions(static_cast(*S).Stmts, Renames, + Assigned, Out)) return false; break; default: @@ -4636,6 +4735,8 @@ class PassivizerImpl { OwnedAllocationIdentities.push_back(ProvenanceName.str()); RepresentedAllocationPointers.insert(ResultTarget.str()); + RepresentedObjects[ResultTarget.str()] = {Summary.AllocatedType, + Summary.SizeBytes}; auto NonzeroProvenance = std::make_unique( VBinOp::Ne, cloneVExpr(Provenance.get()), std::make_unique(0, VType::makePtr(), Loc), @@ -6321,6 +6422,7 @@ class PassivizerImpl { } PointerValueVariables.insert(A.Target); RepresentedAllocationPointers.insert(A.Target); + RepresentedObjects[A.Target] = {A.AllocatedType, A.SizeBytes}; const std::string AllocationBefore = Renames[VAllocationHeapName]; const std::string LivenessBefore = Renames[VLivenessHeapName]; @@ -6600,7 +6702,7 @@ class PassivizerImpl { // calls reach; the rest of the heap keeps its values. std::vector WriteRegions; bool Framed = Fn.ObjectModel && Modified.count(VHeapName) && - loopWriteRegions(W.Body, Renames, WriteRegions); + loopWriteRegions(W.Body, Renames, Modified, WriteRegions); // Every store and call in the loop is checked against the function's // own frame, so that frame bounds what the loop writes too; with both, // only cells in both may change. From 92f0ab71a1858c3e4d206a41885ded8dd264eea8 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 14:15:35 +0530 Subject: [PATCH 90/94] test: Check that a loop writing local storage keeps every other object. --- clang/test/Verify/suite/loop_local_frames.cpp | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 clang/test/Verify/suite/loop_local_frames.cpp diff --git a/clang/test/Verify/suite/loop_local_frames.cpp b/clang/test/Verify/suite/loop_local_frames.cpp new file mode 100644 index 0000000000..9e8c660749 --- /dev/null +++ b/clang/test/Verify/suite/loop_local_frames.cpp @@ -0,0 +1,205 @@ +// RUN: not %cpp-verify --jobs=1 %s -- > %t 2>&1 +// RUN: FileCheck %s < %t +// RUN: FileCheck %s --check-prefix=SOUND < %t +// RUN: not %cpp-verify --jobs=1 --backend=bmc --unroll=5 %s -- 2>&1 \ +// RUN: | FileCheck %s --check-prefix=BMC +// +// A loop that writes local or dynamic storage through the pointer its +// allocation defined writes only that object: every other object keeps its +// value across the loop without an invariant saying so. +// +// No proof here holds only because no execution reaches its end. +// SOUND-NOT: vacuous + +int kept_value() + cppverify::post(cppverify::result == 5) +{ + int x = 5; + int &r = x; + int a[2]; + for (int i = 0; i < 2; i = i + 1) + cppverify::invariant(0 <= i && i <= 2) + cppverify::decreases(2 - i) + { + a[i] = i; + } + return r; +} +// CHECK-DAG: Verified: kept_value [backend=z3] +// BMC-DAG: Verified: kept_value [backend=bmc, bound=2] + +int other_array() + cppverify::post(cppverify::result == 7) +{ + int a[3]; + int b[2]; + b[0] = 3; + b[1] = 4; + for (int i = 0; i < 3; i = i + 1) + cppverify::invariant(0 <= i && i <= 3) + cppverify::decreases(3 - i) + { + a[i] = 0; + } + return b[0] + b[1]; +} +// CHECK-DAG: Verified: other_array [backend=z3] + +struct Pair { + int a; + int b; +}; + +// The loop reads one local record, kept in memory for its reference, and +// writes another object. +int record_read() + cppverify::post(cppverify::result == 2) +{ + Pair p; + p.a = 1; + p.b = 2; + int &pb = p.b; + int a[3]; + for (int i = 0; i < 3; i = i + 1) + cppverify::invariant(0 <= i && i <= 3) + cppverify::decreases(3 - i) + { + a[i] = p.a; + } + return pb; +} +// CHECK-DAG: Verified: record_read [backend=z3] + +// A loop that writes a parameter's object and a local one keeps a third. +int with_parameter(int *q) + cppverify::pre(q != nullptr) + cppverify::modifies(*q) + cppverify::post(cppverify::result == 9) +{ + int keep[1]; + keep[0] = 9; + int a[2]; + for (int i = 0; i < 2; i = i + 1) + cppverify::invariant(0 <= i && i <= 2) + cppverify::decreases(2 - i) + { + a[i] = i; + *q = i; + } + return keep[0]; +} +// CHECK-DAG: Verified: with_parameter [backend=z3] + +void set(int &r, int v) + cppverify::modifies(r) + cppverify::post(r == v) +{ + r = v; +} + +// A call in the loop writes an element of the local it is given. +int through_call() + cppverify::post(cppverify::result == 4) +{ + int x = 4; + int &r = x; + int a[2]; + a[0] = 1; + a[1] = 1; + for (int i = 0; i < 2; i = i + 1) + cppverify::invariant(0 <= i && i <= 2) + cppverify::decreases(2 - i) + { + set(a[i], 0); + } + return r; +} +// CHECK-DAG: Verified: through_call [backend=z3] + +// A dynamic object the loop does not write keeps its value too. +int dynamic_kept() + cppverify::post(cppverify::result == 6) +{ + int *p = new int(6); + int a[2]; + for (int i = 0; i < 2; i = i + 1) + cppverify::invariant(0 <= i && i <= 2) + cppverify::decreases(2 - i) + { + a[i] = i; + } + int v = *p; + delete p; + return v; +} +// CHECK-DAG: Verified: dynamic_kept [backend=z3] + +// A store through a reference to a local writes the one cell it binds. +int reference_written() + cppverify::post(cppverify::result == 3) +{ + int x = 0; + int &r = x; + int y = 3; + int &s = y; + for (int i = 0; i < 2; i = i + 1) + cppverify::invariant(0 <= i && i <= 2) + cppverify::decreases(2 - i) + { + r = i; + } + return s; +} +// CHECK-DAG: Verified: reference_written [backend=z3] + +// So does a call given that reference. +int reference_call() + cppverify::post(cppverify::result == 3) +{ + int x = 0; + int &r = x; + int y = 3; + int &s = y; + for (int i = 0; i < 2; i = i + 1) + cppverify::invariant(0 <= i && i <= 2) + cppverify::decreases(2 - i) + { + set(r, i); + } + return s; +} +// CHECK-DAG: Verified: reference_call [backend=z3] + +// What the loop writes is not kept. +int overwritten() + cppverify::post(cppverify::result == 7) +{ + int a[4]; + a[3] = 7; + for (int i = 0; i < 4; i = i + 1) + cppverify::invariant(0 <= i && i <= 4) + cppverify::decreases(4 - i) + { + a[i] = 0; + } + return a[3]; +} +// CHECK-DAG: error: verification failed: overwritten [{{.*}}::postcondition@ + +// Nor is a local written through a reference to it. +int through_reference() + cppverify::post(cppverify::result == 5) +{ + int x = 5; + int &r = x; + int a[2]; + for (int i = 0; i < 2; i = i + 1) + cppverify::invariant(0 <= i && i <= 2) + cppverify::decreases(2 - i) + { + a[i] = 0; + r = i; + } + return x; +} +// CHECK-DAG: error: verification failed: through_reference [{{.*}}::postcondition@ From a0698a7a84158e93046d9239cba037ef0a211058 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 14:40:07 +0530 Subject: [PATCH 91/94] docs: Describe the local and dynamic objects a loop writes, with a checked example. --- website/source/language/functions-loops.rst | 48 +++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/website/source/language/functions-loops.rst b/website/source/language/functions-loops.rst index bd8e833288..1c585c9cd4 100644 --- a/website/source/language/functions-loops.rst +++ b/website/source/language/functions-loops.rst @@ -454,6 +454,54 @@ increment, so ``a[0 : i]`` then covers the cell just written; the function's ``cppverify::modifies(a[0 : k])`` is the range ``[0, k)``, and the cells from ``k`` on keep their values without an invariant saying so. +Local and dynamic objects follow the same rule. A store through a local +array, a local record, or a ``new`` pointer writes that object, a store or a +call through a reference to a local writes the cell it binds, and every +other object keeps its value: + +.. cppverify-example: fails overwritten + +.. code-block:: cpp + + int other_array() + cv::post(cv::result == 7) + { + int a[3]; + int b[2]; + b[0] = 3; + b[1] = 4; + for (int i = 0; i < 3; i = i + 1) + cv::invariant(0 <= i && i <= 3) + cv::decreases(3 - i) + { + a[i] = 0; + } + return b[0] + b[1]; + } + + int overwritten() + cv::post(cv::result == 7) + { + int a[4]; + a[3] = 7; + for (int i = 0; i < 4; i = i + 1) + cv::invariant(0 <= i && i <= 4) + cv::decreases(4 - i) + { + a[i] = 0; + } + return a[3]; + } + +.. code-block:: text + + Verified: other_array [backend=z3] + frames.cpp:18:23: error: verification failed: overwritten [...::postcondition@18:23] (counterexample: ...) [backend=z3] [reason=counterexample] + +``overwritten`` fails because the loop writes ``a[3]``, which no invariant +describes. Before 0.2 a loop that wrote local or dynamic storage forgot the +value of every object, so ``other_array`` failed its postcondition. + After a normal exit the verifier knows ``I && !c`` about what the loop may change; variables the loop does not assign and memory outside its write set keep their values. A ``break`` or ``return`` leaves in its own state instead. From 6f9a7ca7faabfd3a508e391434ec312e7fd90b1e Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 14:40:07 +0530 Subject: [PATCH 92/94] docs: Note loops that write local storage in the release notes. --- website/source/release-notes.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/source/release-notes.rst b/website/source/release-notes.rst index 03051ee5b4..3acbdd942c 100644 --- a/website/source/release-notes.rst +++ b/website/source/release-notes.rst @@ -143,6 +143,12 @@ was initialized before a loop now stays initialized, and an invariant holds at the head of each iteration with its definedness, which shows the elements it reads initialized and in bounds (:doc:`language/functions-loops`). +**Loops that write local storage.** A loop that wrote a local array, a local +record, a ``new`` object, or a local through a reference forgot the value of +every object, so a claim about a local the loop never touches failed after +it. Such a loop now writes only the objects it stores to, as a loop over +parameters always did (:doc:`language/functions-loops`). + **Null minus null.** The difference of two null pointers failed its ``pointer-difference`` check; it is 0, as C++ defines (:doc:`language/pointers`). From 7d5c1232c10ec90eb51c0b9ecb3213a4acd43545 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 14:40:08 +0530 Subject: [PATCH 93/94] docs: Count local and dynamic objects in a loop's write set in the design. --- docs/DESIGN.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 373beb197a..fb85477034 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -268,7 +268,9 @@ for (int i = 0; i < n; i = i + 1) Without `cppverify::modifies`, a loop writes only the objects its stores and calls reach, inferred from the body: every other object keeps its value without - an invariant saying so. + an invariant saying so. Local and dynamic objects count alike: a store + through a local array, a local record, or a `new` pointer writes that + object, and one through a reference to a local writes the cell it binds. `do` loops place the clauses between the trailing condition and its semicolon: From 991d0ef41c3e7b888a1d73ab81d42185f0709700 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Sat, 24 Oct 2026 14:40:08 +0530 Subject: [PATCH 94/94] docs: Describe loop frames of local and dynamic storage in the architecture. --- docs/ARCHITECTURE.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7d28924600..ed64f75518 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -516,6 +516,21 @@ origin is unknown the function's frame alone bounds the loop, since every store is checked against it. When every written region is a single cell the frame is a chain of stores of fresh values; otherwise it is a `HeapFrame(mem_entry, mem_head, regions)` relation. +Local and dynamic storage has no origin, so its writes are bounded by its +lifetime companion's pointer instead, when the loop assigns neither that +pointer nor its companion: a store whose address is such a pointer, a +reference to a local say, writes that one cell (`representedCell`); one +rooted at the pointer an allocation defined (`RepresentedObjects`: the +allocated type and size, recorded at `VAllocateStmt` and at a fresh-owned +call) writes that object (`representedRegions`), since its fields lie in the +layout and its fixed-array indices are checked. The object is its scalar +leaves from `VFunction::Layouts`, each a cell, so the frame stays a chain of +stores; with a byte range instead, Z3 found no counterexample to a claim the +loop falsifies within the timeout, and the leaves bound the same bytes. Without +a layout the object is its bytes. The function's own frame is never used +here, since a store to its own storage is not checked against it. Before +0.2 any store to local or dynamic storage left the loop unframed, so it +forgot every object. An explicit loop `cppverify::modifies` is also asserted at the end of each iteration and at each `continue` (obligation kind `frame`, located at the footprint).