Skip to content

Commit 5e7d547

Browse files
committed
Diagnose derivatives that use a variable before its declaration
clad assembles derivative bodies by hand, so nothing enforces that a declaration precedes its uses the way parsing does for user code, and the generated function is never handed to Sema::ActOnFinishFunctionBody, which would re-check it. Such a body can therefore name a variable that is not in scope at that point and still compile. Add findUseBeforeDecl alongside the existing structural checks. It walks the body in execution order carrying the block-scope locals declared so far, which also validates lambda bodies against what is visible at the lambda's definition point -- notably catching references built outside a closure that never became captures, and which LambdaExpr::captures() therefore cannot report. It is gated on a clean derivation like StrayRef: an unsupported construct is diagnosed and its body is not expected to be well-formed. The check found three violations, fixed here. Cloning a lambda body created fresh VarDecls but left later statements referring to the original lambda's variables, so register the clones for remapping. A range-for declared its adjoint iterator and loop variable inside the forward loop while the reverse sweep, a sibling block, restored them -- for a nested loop no block encloses both short of the function body, so declare them there and initialize in place.
1 parent 94115ef commit 5e7d547

6 files changed

Lines changed: 241 additions & 107 deletions

File tree

lib/Differentiator/ASTIntegrity.cpp

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,82 @@ const ValueDecl* findOriginalRef(const Stmt* Derivative,
9696
return F.Stray;
9797
}
9898

99+
/// Visit a block, scoping the declarations it introduces to it.
100+
static const ValueDecl* walkScope(const Stmt* S,
101+
llvm::DenseSet<const VarDecl*> Declared);
102+
103+
/// Visit \p S in order, threading \p Declared through so later siblings see
104+
/// what earlier ones declared. Returns the first offender, or null.
105+
static const ValueDecl* walkStmt(const Stmt* S,
106+
llvm::DenseSet<const VarDecl*>& Declared) {
107+
if (!S)
108+
return nullptr;
109+
110+
if (const auto* DRE = dyn_cast<DeclRefExpr>(S)) {
111+
const auto* VD = dyn_cast<VarDecl>(DRE->getDecl());
112+
// Only block-scope locals can be used before their declaration. Parameters
113+
// and globals are live for the whole body.
114+
if (VD && VD->isLocalVarDecl() && !Declared.count(VD))
115+
return VD;
116+
return nullptr;
117+
}
118+
119+
if (const auto* LE = dyn_cast<LambdaExpr>(S)) {
120+
llvm::DenseSet<const VarDecl*> Inner = Declared;
121+
if (const CXXMethodDecl* Call = LE->getCallOperator())
122+
for (const ParmVarDecl* P : Call->parameters())
123+
Inner.insert(P);
124+
return walkScope(LE->getBody(), std::move(Inner));
125+
}
126+
127+
// A block's declarations do not escape it.
128+
if (isa<CompoundStmt>(S))
129+
return walkScope(S, Declared);
130+
131+
if (const auto* DS = dyn_cast<DeclStmt>(S)) {
132+
// An initializer is evaluated before its own variable is in scope.
133+
for (const Decl* D : DS->decls())
134+
if (const auto* VD = dyn_cast<VarDecl>(D)) {
135+
if (const ValueDecl* Bad = walkStmt(VD->getInit(), Declared))
136+
return Bad;
137+
Declared.insert(VD);
138+
}
139+
return nullptr;
140+
}
141+
142+
for (const Stmt* Child : S->children())
143+
if (const ValueDecl* Bad = walkStmt(Child, Declared))
144+
return Bad;
145+
return nullptr;
146+
}
147+
148+
static const ValueDecl* walkScope(const Stmt* S,
149+
llvm::DenseSet<const VarDecl*> Declared) {
150+
if (!S)
151+
return nullptr;
152+
if (!isa<CompoundStmt>(S))
153+
return walkStmt(S, Declared);
154+
for (const Stmt* Child : cast<CompoundStmt>(S)->body())
155+
if (const ValueDecl* Bad = walkStmt(Child, Declared))
156+
return Bad;
157+
return nullptr;
158+
}
159+
160+
const ValueDecl* findUseBeforeDecl(const Stmt* Derivative,
161+
const FunctionDecl* Derived) {
162+
llvm::DenseSet<const VarDecl*> Declared;
163+
if (Derived)
164+
for (const ParmVarDecl* P : Derived->parameters())
165+
Declared.insert(P);
166+
return walkScope(Derivative, std::move(Declared));
167+
}
168+
99169
IntegrityReport verifyDerivative(const Stmt* Derivative,
100-
const FunctionDecl* Original) {
170+
const FunctionDecl* Original,
171+
const FunctionDecl* Derived) {
101172
IntegrityReport R;
102173
R.SharedNode = findSharedNode(Derivative);
174+
R.UseBeforeDecl = findUseBeforeDecl(Derivative, Derived);
103175
if (Original) {
104176
if (const Stmt* PrimalBody = Original->getBody())
105177
R.PrimalNode = findPrimalSharedNode(Derivative, PrimalBody);

lib/Differentiator/ASTIntegrity.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@ const clang::Stmt* findPrimalSharedNode(const clang::Stmt* Derivative,
5050
const clang::ValueDecl* findOriginalRef(const clang::Stmt* Derivative,
5151
const clang::FunctionDecl* Original);
5252

53+
/// Return the first block-scope variable \p Derivative references before its
54+
/// declaration, or nullptr if every use is preceded by its decl. clad builds
55+
/// bodies by hand, so nothing enforces the ordering that parsing guarantees for
56+
/// user code. Also validates lambda bodies against what is visible at the
57+
/// lambda's definition point, which catches references built outside the
58+
/// closure's scope that never became captures. \p Derived supplies the
59+
/// generated function whose parameters are in scope throughout.
60+
const clang::ValueDecl* findUseBeforeDecl(const clang::Stmt* Derivative,
61+
const clang::FunctionDecl* Derived);
62+
5363
/// The structural violations a generated derivative body may exhibit, each the
5464
/// first offending node/decl or null. Purely a function of the AST -- the
5565
/// caller supplies the differentiation context (whether the derivation was
@@ -62,6 +72,8 @@ struct IntegrityReport {
6272
/// A reference left bound to one of the original function's own
6373
/// params/locals.
6474
const clang::ValueDecl* StrayRef = nullptr;
75+
/// A variable referenced before its declaration.
76+
const clang::ValueDecl* UseBeforeDecl = nullptr;
6577
};
6678

6779
/// Run every structural integrity check on a generated \p Derivative body.
@@ -70,7 +82,8 @@ struct IntegrityReport {
7082
/// the AST; StrayRef in particular is meaningful only for a clean derivation,
7183
/// which the caller must establish before acting on it.
7284
IntegrityReport verifyDerivative(const clang::Stmt* Derivative,
73-
const clang::FunctionDecl* Original);
85+
const clang::FunctionDecl* Original,
86+
const clang::FunctionDecl* Derived);
7487

7588
} // namespace clad
7689

lib/Differentiator/DerivativeBuilder.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@ static void registerDerivative(Decl* D, Sema& S, const DiffRequest& R) {
666666
// Compute cleanliness before the diagnostics below inflate the tally.
667667
bool CleanDerivation =
668668
Diags.getNumWarnings() + Diags.getNumErrors() == DiagsBefore;
669-
IntegrityReport Report = verifyDerivative(Body, request.Function);
669+
IntegrityReport Report = verifyDerivative(Body, request.Function, FD);
670670

671671
// A derivative must be a proper tree in its Stmt child-edge structure:
672672
// no node the child of two parents, because a later in-place edit of a
@@ -701,6 +701,21 @@ static void registerDerivative(Decl* D, Sema& S, const DiffRequest& R) {
701701
// unsupported construct is cloned wholesale and knowingly keeps such
702702
// references in a derivative that is not used.
703703
if (CleanDerivation) {
704+
// Nothing enforces declaration-before-use in a hand-assembled body
705+
// the way parsing does for user code, so a reference can name a
706+
// variable whose declaration has not been reached -- or one owned by
707+
// the original function. Gated with StrayRef: an unsupported
708+
// construct is diagnosed and its body is not expected to be
709+
// well-formed.
710+
assert(!Report.UseBeforeDecl &&
711+
"clad generated a use of a variable before its declaration");
712+
if (Report.UseBeforeDecl)
713+
diag(DiagnosticsEngine::Warning, FD->getLocation(),
714+
"clad referenced '%0' before its declaration while "
715+
"differentiating %1; this is a clad bug -- please report it "
716+
"at %2")
717+
<< Report.UseBeforeDecl << FD << getCladRepositoryURL();
718+
704719
assert(!Report.StrayRef &&
705720
"derivative references an un-remapped decl of the original");
706721
if (Report.StrayRef)

lib/Differentiator/ReverseModeVisitor.cpp

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,8 +1078,18 @@ Expr* ReverseModeVisitor::getStdInitListSizeExpr(const Expr* E) {
10781078
if (VisitRange.getDecl_dx())
10791079
addToCurrentBlock(BuildDeclStmt(VisitRange.getDecl_dx()));
10801080
addToCurrentBlock(BuildDeclStmt(VisitBegin.getDecl()));
1081-
if (VisitBegin.getDecl_dx())
1082-
addToCurrentBlock(BuildDeclStmt(VisitBegin.getDecl_dx()));
1081+
// The reverse sweep decrements the adjoint iterator, but its loop is a
1082+
// sibling of the forward one -- for a nested loop no block encloses both
1083+
// short of the function body. Hoist the declaration to function scope so
1084+
// both sweeps name the same object, and keep the initialization here,
1085+
// where the adjoint range it reads is in scope.
1086+
if (VarDecl* DBegin = VisitBegin.getDecl_dx()) {
1087+
Expr* Init = DBegin->getInit();
1088+
DBegin->setInit(nullptr);
1089+
addToBlock(BuildDeclStmt(DBegin), m_Globals);
1090+
if (Init)
1091+
addToCurrentBlock(BuildOp(BO_Assign, BuildDeclRef(DBegin), Init));
1092+
}
10831093

10841094
const auto* EndDecl = cast<VarDecl>(FRS->getEndStmt()->getSingleDecl());
10851095
QualType endType = CloneType(EndDecl->getType());
@@ -1119,13 +1129,17 @@ Expr* ReverseModeVisitor::getStdInitListSizeExpr(const Expr* E) {
11191129
StmtDiff storeAdjLoop;
11201130
if (LoopVDDiff.getDecl_dx())
11211131
storeAdjLoop = StoreAndRestore(BuildDeclRef(LoopVDDiff.getDecl_dx()));
1132+
// The reverse sweep restores the loop variable and its adjoint from the
1133+
// tape, but it does so from a sibling block of the forward loop. Declare
1134+
// both at function scope so the restore names the same object; each is
1135+
// zero-initialized, so nothing in the initializer needs the loop's scope.
11221136
if (LoopVDDiff.getDecl_dx())
1123-
addToCurrentBlock(BuildDeclStmt(LoopVDDiff.getDecl_dx()));
1137+
addToBlock(BuildDeclStmt(LoopVDDiff.getDecl_dx()), m_Globals);
11241138
Expr* loopInit = LoopVDDiff.getDecl()->getInit();
11251139
SetDeclInit(LoopVDDiff.getDecl(),
11261140
getZeroInit(LoopVDDiff.getDecl()->getType()));
11271141
if (LoopVDDiff.getDecl())
1128-
addToCurrentBlock(BuildDeclStmt(LoopVDDiff.getDecl()));
1142+
addToBlock(BuildDeclStmt(LoopVDDiff.getDecl()), m_Globals);
11291143
Expr* assignLoop =
11301144
BuildOp(BO_Assign, BuildDeclRef(LoopVDDiff.getDecl()), loopInit);
11311145

lib/Differentiator/VisitorBase.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,20 @@ namespace clad {
10291029
utils::ReferencesUpdater up(m_Sema, getCurrentScope(), CallOp,
10301030
m_DeclReplacements);
10311031
up.TraverseStmt(clonedS);
1032+
// Cloning a DeclStmt produces a fresh VarDecl, but StmtClone records that
1033+
// only in its own decl mapping. Register it here too, or a later
1034+
// statement's clone keeps referring to the original lambda's variable --
1035+
// a reference into the user's AST that outlives differentiation.
1036+
if (const auto* DS = dyn_cast<DeclStmt>(S))
1037+
if (auto* ClonedDS = dyn_cast<DeclStmt>(clonedS)) {
1038+
auto O = DS->decl_begin();
1039+
auto C = ClonedDS->decl_begin();
1040+
for (; O != DS->decl_end() && C != ClonedDS->decl_end(); ++O, ++C)
1041+
if (const auto* OVD = dyn_cast<VarDecl>(*O))
1042+
if (auto* CVD = dyn_cast<VarDecl>(*C))
1043+
if (OVD != CVD)
1044+
m_DeclReplacements[OVD] = CVD;
1045+
}
10321046
addToCurrentBlock(clonedS);
10331047
}
10341048
CompoundStmt* ClonedBody = endBlock();

0 commit comments

Comments
 (0)