Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/clad/Differentiator/Version.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace clad {
std::string getCladRevision();
std::string getCladRepositoryPath();
std::string getCladRepositoryURL();
std::string getCladFullRepositoryVersion();
std::string getCladFullVersion();
} // end namespace clad
Expand Down
46 changes: 46 additions & 0 deletions lib/Differentiator/ASTIntegrity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,52 @@
return nullptr;
}

const ValueDecl* findOriginalRef(const Stmt* Derivative,
const FunctionDecl* Original) {
if (!Derivative || !Original)
return nullptr;

Check warning on line 68 in lib/Differentiator/ASTIntegrity.cpp

View check run for this annotation

Codecov / codecov/patch

lib/Differentiator/ASTIntegrity.cpp#L68

Added line #L68 was not covered by tests
// A generated derivative owns fresh clones of every param/local it needs; a
// reference still bound to one of Original's own decls means its remap was
// forgotten (the primal clone was never registered in m_DeclReplacements).
// Walk the finished body and flag the first such reference.
struct Finder : RecursiveASTVisitor<Finder> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: constructor does not initialize these fields: Original [cppcoreguidelines-pro-type-member-init]

lib/Differentiator/ASTIntegrity.cpp:73:

-     const FunctionDecl* Original;
+     const FunctionDecl* Original{};

const FunctionDecl* Original;
const ValueDecl* Stray = nullptr;
bool shouldVisitImplicitCode() const { return true; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'shouldVisitImplicitCode' should be marked [[nodiscard]] [modernize-use-nodiscard]

Suggested change
bool shouldVisitImplicitCode() const { return true; }
[[nodiscard]] bool shouldVisitImplicitCode() const { return true; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: method 'shouldVisitImplicitCode' can be made static [readability-convert-member-functions-to-static]

Suggested change
bool shouldVisitImplicitCode() const { return true; }
static bool shouldVisitImplicitCode() { return true; }

bool VisitDeclRefExpr(DeclRefExpr* DRE) {
const ValueDecl* D = DRE->getDecl();
// Flag only params/locals declared DIRECTLY in Original -- those are what
// BuildParams/VisitDeclStmt clone and must remap. A nested lambda's own
// parameter (context is the lambda's CXXMethod, not Original) is
// referenced by design when the lambda is preserved, not a forgotten
// clone, so exact-context match excludes it.
const auto* DC = dyn_cast<FunctionDecl>(D->getDeclContext());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "clang::dyn_cast" is directly included [misc-include-cleaner]

lib/Differentiator/ASTIntegrity.cpp:18:

+ #include <clang/Basic/LLVM.h>

if (isa<VarDecl>(D) && DC &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "clang::isa" is directly included [misc-include-cleaner]

      if (isa<VarDecl>(D) && DC &&
          ^

DC->getCanonicalDecl() == Original->getCanonicalDecl()) {
Stray = D;
return false; // stop at the first offender
}
return true;
}
} F;
F.Original = Original;
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
F.TraverseStmt(const_cast<Stmt*>(Derivative));
return F.Stray;
}

IntegrityReport verifyDerivative(const Stmt* Derivative,
const FunctionDecl* Original) {
IntegrityReport R;
R.SharedNode = findSharedNode(Derivative);
if (Original) {
if (const Stmt* PrimalBody = Original->getBody())
R.PrimalNode = findPrimalSharedNode(Derivative, PrimalBody);
R.StrayRef = findOriginalRef(Derivative, Original);
}
return R;
}

const Stmt* findSharedNode(const Stmt* Root) {
if (!Root)
return nullptr;
Expand Down
35 changes: 35 additions & 0 deletions lib/Differentiator/ASTIntegrity.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

namespace clang {
class Stmt;
class ValueDecl;
class FunctionDecl;
} // namespace clang

namespace clad {
Expand All @@ -37,6 +39,39 @@ const clang::Stmt* findSharedNode(const clang::Stmt* Root);
const clang::Stmt* findPrimalSharedNode(const clang::Stmt* Derivative,
const clang::Stmt* Primal);

/// Return the first local variable or parameter of \p Original that a
/// DeclRefExpr in \p Derivative still references, or nullptr if every reference
/// was remapped to a derivative-owned decl. A generated derivative must not
/// reference the original function's own params/locals -- they do not exist in
/// it; such a reference is a forgotten reference-remap (a primal clone that was
/// never registered in m_DeclReplacements) and would miscompile. Only VarDecls
/// are gated: function self-references (recursion) and decls declared outside
/// \p Original have a context \p Original does not enclose.
const clang::ValueDecl* findOriginalRef(const clang::Stmt* Derivative,
const clang::FunctionDecl* Original);

/// The structural violations a generated derivative body may exhibit, each the
/// first offending node/decl or null. Purely a function of the AST -- the
/// caller supplies the differentiation context (whether the derivation was
/// clean) and decides how to react (assert in debug, diagnose in release).
struct IntegrityReport {
/// A node that is the child of two parents within the derivative.
const clang::Stmt* SharedNode = nullptr;
/// A node spliced from the original function's still-live AST.
const clang::Stmt* PrimalNode = nullptr;
/// A reference left bound to one of the original function's own
/// params/locals.
const clang::ValueDecl* StrayRef = nullptr;
};

/// Run every structural integrity check on a generated \p Derivative body.
/// \p Original is the function being differentiated (its body is the primal),
/// or null when there is none (e.g. a synthesized overload). This only reads
/// the AST; StrayRef in particular is meaningful only for a clean derivation,
/// which the caller must establish before acting on it.
IntegrityReport verifyDerivative(const clang::Stmt* Derivative,
const clang::FunctionDecl* Original);

} // namespace clad

#endif // CLAD_AST_INTEGRITY_H
92 changes: 59 additions & 33 deletions lib/Differentiator/DerivativeBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "clad/Differentiator/Timers.h"
#include "clad/Differentiator/VectorForwardModeVisitor.h"
#include "clad/Differentiator/VectorPushForwardModeVisitor.h"
#include "clad/Differentiator/Version.h"
#include "clad/Differentiator/VisitorBase.h"

#include "clang/AST/ASTContext.h"
Expand Down Expand Up @@ -586,6 +587,15 @@
<< VD << L;
}

#if CLANG_VERSION_MAJOR > 16

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "CLANG_VERSION_MAJOR" is directly included [misc-include-cleaner]

lib/Differentiator/DerivativeBuilder.cpp:51:

- #include <cstddef>
+ #include <clang/Basic/Version.h>
+ #include <cstddef>

// Snapshot the diagnostic tally so the integrity check below can tell a
// clean differentiation from one that hit an unsupported construct (which
// is cloned wholesale and knowingly keeps un-remapped references). Guarded
// with the check itself: below clang-17 it is unused (-Werror=unused).
DiagnosticsEngine& Diags = m_Sema.getDiagnostics();
unsigned DiagsBefore = Diags.getNumWarnings() + Diags.getNumErrors();
#endif

DerivativeAndOverload result{};
if (request.Mode == DiffMode::forward) {
BaseForwardModeVisitor V(*this, request);
Expand Down Expand Up @@ -646,44 +656,60 @@
}

#if CLANG_VERSION_MAJOR > 16
// A generated derivative must be a proper tree in its Stmt child-edge
// structure: no node is the child (Stmt::children()) of two parents,
// because a later in-place edit of a shared node leaks into its other
// users (and a shared aggregate initializer breaks CodeGen). Below
// A generated derivative must satisfy several structural invariants; below
// clang-17 buildClonedLambda cannot synthesize a fresh closure, so lambda
// derivatives legitimately share and this check is unreachable.
// derivatives legitimately share and these checks are unreachable.
if (auto* FD = dyn_cast_or_null<clang::FunctionDecl>(result.derivative))
if (clang::Stmt* Body = FD->getBody()) {
const clang::Stmt* Shared = findSharedNode(Body);
// Debug asserts builds abort here; release builds keep the diagnostic
// so a sharing regression is not silently shipped.
assert(!Shared && "clad generated a derivative with a shared AST node");
if (Shared)
// Compute cleanliness before the diagnostics below inflate the tally.
bool CleanDerivation =
Diags.getNumWarnings() + Diags.getNumErrors() == DiagsBefore;
IntegrityReport Report = verifyDerivative(Body, request.Function);

// A derivative must be a proper tree in its Stmt child-edge structure:
// no node the child of two parents, because a later in-place edit of a
// shared node leaks into its other users (and a shared aggregate
// initializer breaks CodeGen). Debug builds abort here; release builds
// keep the diagnostic so a regression is not silently shipped.
assert(!Report.SharedNode &&
"clad generated a derivative with a shared AST node");
if (Report.SharedNode)
diag(DiagnosticsEngine::Warning, FD->getLocation(),
"clad internally reused a '%0' AST node while differentiating "
"%1; this is a clad bug -- please report it at "
"https://github.com/vgvassilev/clad")
<< Shared->getStmtClassName() << FD;

// A derivative must also not splice a node owned by its primal. The
// original function's AST outlives differentiation, so a shared node
// exposes the user's own code to any later in-place edit of the
// derivative -- the same corruption risk findSharedNode guards against,
// across the primal/derivative boundary it cannot see. Enforce it too.
if (const clang::FunctionDecl* PrimalFD = request.Function)
if (const clang::Stmt* PrimalBody = PrimalFD->getBody()) {
const clang::Stmt* FromPrimal =
findPrimalSharedNode(Body, PrimalBody);
assert(!FromPrimal &&
"clad spliced a primal AST node into a derivative");
if (FromPrimal)
diag(DiagnosticsEngine::Warning, FD->getLocation(),
"clad reused a '%0' AST node from the original function "
"while "
"differentiating %1; this is a clad bug -- please report it "
"at https://github.com/vgvassilev/clad")
<< FromPrimal->getStmtClassName() << FD;
}
"%1; this is a clad bug -- please report it at %2")
<< Report.SharedNode->getStmtClassName() << FD
<< getCladRepositoryURL();

Check warning on line 681 in lib/Differentiator/DerivativeBuilder.cpp

View check run for this annotation

Codecov / codecov/patch

lib/Differentiator/DerivativeBuilder.cpp#L680-L681

Added lines #L680 - L681 were not covered by tests

// It must also not splice a node owned by its primal: the original
// function's AST outlives differentiation, so a later in-place edit of
// a shared node would corrupt the user's own code.
assert(!Report.PrimalNode &&
"clad spliced a primal AST node into a derivative");
if (Report.PrimalNode)
diag(DiagnosticsEngine::Warning, FD->getLocation(),

Check warning on line 689 in lib/Differentiator/DerivativeBuilder.cpp

View check run for this annotation

Codecov / codecov/patch

lib/Differentiator/DerivativeBuilder.cpp#L689

Added line #L689 was not covered by tests
"clad reused a '%0' AST node from the original function while "
"differentiating %1; this is a clad bug -- please report it at "
"%2")
<< Report.PrimalNode->getStmtClassName() << FD
<< getCladRepositoryURL();

Check warning on line 694 in lib/Differentiator/DerivativeBuilder.cpp

View check run for this annotation

Codecov / codecov/patch

lib/Differentiator/DerivativeBuilder.cpp#L693-L694

Added lines #L693 - L694 were not covered by tests

// And it must reference only decls it owns. A DeclRefExpr still bound
// to one of the original function's own params/locals is a forgotten
// reference-remap. Only meaningful for a clean derivation: an
// unsupported construct is cloned wholesale and knowingly keeps such
// references in a derivative that is not used.
if (CleanDerivation) {
assert(!Report.StrayRef &&
"derivative references an un-remapped decl of the original");
if (Report.StrayRef)
diag(

Check warning on line 705 in lib/Differentiator/DerivativeBuilder.cpp

View check run for this annotation

Codecov / codecov/patch

lib/Differentiator/DerivativeBuilder.cpp#L705

Added line #L705 was not covered by tests
DiagnosticsEngine::Warning, FD->getLocation(),
"clad left a reference to '%0' bound to the original function "
"while differentiating %1; this is a clad bug -- please report "
"it at %2")
<< Report.StrayRef->getNameAsString() << FD
<< getCladRepositoryURL();

Check warning on line 711 in lib/Differentiator/DerivativeBuilder.cpp

View check run for this annotation

Codecov / codecov/patch

lib/Differentiator/DerivativeBuilder.cpp#L710-L711

Added lines #L710 - L711 were not covered by tests
}
}
#endif

Expand Down
4 changes: 4 additions & 0 deletions lib/Differentiator/Version.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
#endif // CLAD_REPOSITORY
}

std::string getCladRepositoryURL() {
return "https://github.com/vgvassilev/clad";

Check warning on line 35 in lib/Differentiator/Version.cpp

View check run for this annotation

Codecov / codecov/patch

lib/Differentiator/Version.cpp#L34-L35

Added lines #L34 - L35 were not covered by tests
}

std::string getCladFullRepositoryVersion() {
std::string buf;
llvm::raw_string_ostream OS(buf);
Expand Down
Loading