fold/tests/unit/test_summary_encoder_phi_nonnull.cpp
trvon 1fa0612f6e
feat(encoder): M28 phi-merge non-null facts in summary encoder
When both arms of an if-statement prove the same variable non-null
(via M22 null-check inference or M23 address-of marking), the
summary encoder's `mergeBindings` now intersects the two branches'
nonNullVars sets and marks the survivors in the post-if env.
Mirrors the same logic already present in
security_walker.cpp::walkIf so summary mode and the main walker
stay in parity.

Added tests/unit/test_summary_encoder_phi_nonnull.cpp covering
the rename-pair address-of case. Pre-fix the test FAILs; post-fix
PASSes. 44/44 still green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:35:00 -04:00

83 lines
2.8 KiB
C++

// M28 phi-join: verify that non-null facts proven in BOTH arms of an
// if-statement survive the phi-merge in the summary encoder.
//
// security_walker.cpp already does this intersection in its walkIf;
// this test pins the same behavior for `summary_encoder::mergeBindings`
// so the two code paths stay in parity.
#include <clang/AST/Decl.h>
#include <clang/Frontend/ASTUnit.h>
#include <clang/Tooling/Tooling.h>
#include <catch2/catch_test_macros.hpp>
#include <memory>
#include <string>
#include "fold/symbolic/summary_encoder.hpp"
namespace {
std::unique_ptr<clang::ASTUnit> parse(const std::string& code, const std::string& file) {
return clang::tooling::buildASTFromCodeWithArgs(code, {"-std=c++20"}, file);
}
const clang::FunctionDecl* findFunction(clang::ASTUnit& unit, const std::string& name) {
auto& ctx = unit.getASTContext();
for (auto* decl : ctx.getTranslationUnitDecl()->decls()) {
auto* fd = clang::dyn_cast<clang::FunctionDecl>(decl);
if (!fd || !fd->hasBody())
continue;
if (fd->getQualifiedNameAsString() == name)
return fd;
}
return nullptr;
}
const clang::VarDecl* findGlobalVar(clang::ASTUnit& unit, const std::string& name) {
auto& ctx = unit.getASTContext();
for (auto* decl : ctx.getTranslationUnitDecl()->decls()) {
auto* vd = clang::dyn_cast<clang::VarDecl>(decl);
if (!vd || !vd->hasGlobalStorage())
continue;
if (vd->getNameAsString() == name)
return vd;
}
return nullptr;
}
} // namespace
TEST_CASE("M28: non-null facts intersected at phi-join survive in summary env",
"[summary][phi][m28]") {
// `f` takes only int params (summarizeFunction rejects pointer params via
// isModeledType), but its body null-checks a global pointer `g_a` in both
// arms of an if/else. Without M28 in the summary encoder, the post-if env
// loses the non-null mark; with M28, it retains it (the intersection of
// both arms' nonNullVars is non-empty).
auto unit = parse(R"(
int x;
int y;
int* g_p;
int f(int cond) {
if (cond) {
g_p = &x; // M23: address-of marks g_p non-null in then_env
} else {
g_p = &y; // M23: address-of marks g_p non-null in else_env
}
// Post-if: both branches mark g_p non-null. M28 should retain.
return 0;
}
)",
"m28.cpp");
REQUIRE(unit);
const auto* fd = findFunction(*unit, "f");
REQUIRE(fd != nullptr);
auto summary = fold::summarizeFunction(fd, unit->getASTContext());
REQUIRE(summary);
const clang::VarDecl* g_p = findGlobalVar(*unit, "g_p");
REQUIRE(g_p != nullptr);
REQUIRE(summary->env.isDefinitelyNonNull(g_p));
}