Skip to content

Commit 0736656

Browse files
committed
Encode reverse-mode early returns with a named lambda
The reverse-mode early-return encoding emitted a label inside the master reverse and a goto into it from each early-return point. That violates [stmt.dcl]/2 whenever a local with a non-trivial initializer or destructor sits between the goto and its target, which is why DifferentiateWithClad had to bypass Sema::ActOnFinishFunctionBody on the generated function. Materialise the master reverse as a `[&]` lambda bound to an `auto` VarDecl instead, called at each early return and at the tail. The body is assembled outside the closure scope, so its references to captured variables are rebuilt in scope (rebuildLambdaCaptures) where the `[&]` default registers the capture; the reverse statements are emitted into the closure directly. An early return is a NullStmt marker in the forward block, patched with `{ _rev(); return; }` at finalization so no subtree gains a second parent. Whether a function has a non-tail return is a fact about the primal, so DiffRequest carries it as HasEarlyReturns; the encoder reads it to pick the lambda vs plain shape up front. A branch condition read by the master reverse is zero-initialized so an early return that skips its forward store leaves it false -- the same zero-initialized additive state clad already relies on for adjoints and loop counters. Fixes #367
1 parent 4684e78 commit 0736656

17 files changed

Lines changed: 996 additions & 333 deletions

include/clad/Differentiator/DiffPlanner.h

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ using OwnedAnalysisContexts =
4242
llvm::SmallVector<std::unique_ptr<clang::AnalysisDeclContext>, 4>;
4343
using ParamSet = std::set<const clang::ParmVarDecl*>;
4444
using ParamInfo = std::map<const clang::FunctionDecl*, ParamSet>;
45-
/// A struct containing information about request to differentiate a function.
45+
/// A read-only, AD-oriented view over the primal being differentiated: it
46+
/// wraps the primal FunctionDecl and surfaces the AD-relevant facts the
47+
/// FunctionDecl itself does not. Recording such facts here, rather than
48+
/// rediscovering them inside a visitor, keeps them available to every visitor
49+
/// and correct after a request is copied and re-pointed at another Function.
4650
struct DiffRequest {
4751
private:
4852
/// Based on To-Be-Recorded analysis performed before differentiation, tells
@@ -66,7 +70,28 @@ struct DiffRequest {
6670
bool HasAnalysisRun = false;
6771
} m_UsefulRunInfo;
6872

73+
/// Cache for hasEarlyReturns(): whether the primal body has a return that is
74+
/// not in tail position. A property of the Function, computed once on demand.
75+
mutable struct EarlyReturnInfo {
76+
bool HasEarlyReturns = false;
77+
bool HasAnalysisRun = false;
78+
} m_EarlyReturnInfo;
79+
6980
public:
81+
/// The primal body's tail-position return -- the one an early-return encoder
82+
/// lets control fall through to, as opposed to an early return that needs a
83+
/// jump/call. Null when the body does not end in a return. O(1): reads
84+
/// Function's body directly, so a copied request re-pointed at a new Function
85+
/// (a lambda's operator(), a pullback callee) answers for its own Function.
86+
const clang::ReturnStmt* getTailReturn() const;
87+
88+
/// Whether the primal body has a return that is not the tail return -- one
89+
/// the reverse mode must encode with the early-return lambda. A fact about
90+
/// the primal, read directly off Function's body (returns inside nested
91+
/// lambdas belong to their own function and are skipped). A void function
92+
/// seeds no return value, so its returns skip the encoding.
93+
bool hasEarlyReturns() const;
94+
7095
/// Function to be differentiated.
7196
const clang::FunctionDecl* Function = nullptr;
7297
/// Name of the base function to be differentiated. Can be different from
@@ -243,17 +268,17 @@ struct DiffRequest {
243268
bool HasTbrAnalysisRun() const { return m_TbrRunInfo.HasAnalysisRun; }
244269
};
245270

246-
using DiffInterval = std::vector<clang::SourceRange>;
271+
using DiffInterval = std::vector<clang::SourceRange>;
247272

248-
// FIXME: These are translation-unit-wide defaults taken from the compiler
249-
// invocation, not the options of a request; rename to InvocationOptions.
250-
struct RequestOptions {
251-
/// This is a flag to indicate the default behaviour to enable/disable
252-
/// TBR analysis during reverse-mode differentiation.
253-
bool EnableTBRAnalysis = false;
254-
bool EnableVariedAnalysis = false;
255-
bool EnableUsefulAnalysis = false;
256-
};
273+
// FIXME: These are translation-unit-wide defaults taken from the compiler
274+
// invocation, not the options of a request; rename to InvocationOptions.
275+
struct RequestOptions {
276+
/// This is a flag to indicate the default behaviour to enable/disable
277+
/// TBR analysis during reverse-mode differentiation.
278+
bool EnableTBRAnalysis = false;
279+
bool EnableVariedAnalysis = false;
280+
bool EnableUsefulAnalysis = false;
281+
};
257282

258283
class DiffCollector: public clang::RecursiveASTVisitor<DiffCollector> {
259284
/// The source interval where clad was activated.
@@ -298,6 +323,10 @@ struct DiffRequest {
298323
/// or constructor initializers. If we use Visit they would be processed
299324
/// under the parent DiffRequest which is not in the lambda scope.
300325
bool TraverseLambdaExpr(clang::LambdaExpr* LE);
326+
/// Plan a lazily-scheduled nested request the static TU walk never reaches
327+
/// (built in DerivativeBuilder::HandleNestedDiffRequest). Currently records
328+
/// its early-return flag by walking the request's own body.
329+
bool PlanNestedRequest(DiffRequest& request);
301330
bool TraverseFunctionDeclOnce(const clang::FunctionDecl* FD) {
302331
llvm::SaveAndRestore<bool> Saved(m_IsTraversingTopLevelDecl, false);
303332
if (m_Traversed.count(FD))

include/clad/Differentiator/DiffScheduler.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ class DiffScheduler {
3838

3939
/// Static planning pass over a group of top-level declarations.
4040
void Plan(clang::DeclGroupRef DGR) { m_Collector.Walk(DGR); }
41+
42+
/// Plan a single lazily-scheduled request (a nested or higher-order
43+
/// derivative) that the static walk never reached.
44+
void Plan(DiffRequest& R) { m_Collector.PlanNestedRequest(R); }
4145
};
4246

4347
} // namespace clad

include/clad/Differentiator/ReverseModeVisitor.h

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "clang/Sema/Sema.h"
3030
#include "llvm/ADT/ArrayRef.h"
3131
#include "llvm/ADT/DenseSet.h"
32+
#include "llvm/ADT/SmallPtrSet.h"
3233
#include "llvm/ADT/SmallVector.h"
3334

3435
#include <array>
@@ -75,6 +76,10 @@ namespace clad {
7576
/// the reverse mode we also accumulate Stmts for the reverse pass which
7677
/// will be executed on return.
7778
std::vector<Stmts> m_Reverse;
79+
/// Markers emitted at early-return sites in the forward block, patched
80+
/// with `{ _rev(); return; }` at finalization, where `_rev` is the lambda
81+
/// wrapping the master reverse sweep.
82+
llvm::SmallPtrSet<clang::Stmt*, 4> m_EarlyReturnMarkers;
7883
/// Storing expressions to delete/free memory in the reverse pass.
7984
Stmts m_DeallocExprs;
8085
/// A dfdx seed: eager expression, or a deferred builder materialized on
@@ -129,6 +134,15 @@ namespace clad {
129134
// Function to Differentiate with Enzyme as Backend
130135
void DifferentiateWithEnzyme();
131136

137+
/// Walk the current forward block and replace every Stmt in
138+
/// m_EarlyReturnMarkers with a fresh Stmt built by `MakeReplacement`.
139+
/// Each marker gets its own node — sharing one replacement across sites
140+
/// would give it multiple parents and break the single-parent AST
141+
/// invariant. Called once at function-body finalization, after the
142+
/// forward sweep is fully assembled and before the body is handed to Sema.
143+
void
144+
patchEarlyReturnMarkers(llvm::function_ref<clang::Stmt*()> MakeReplacement);
145+
132146
public:
133147
using direction = rmv::direction;
134148
virtual clang::Expr* dfdx() {
@@ -327,10 +341,10 @@ namespace clad {
327341
/// of the stack (clad::back(S)).
328342
clang::Expr* GlobalStoreAndRef(clang::Expr* E, clang::QualType Type,
329343
llvm::StringRef prefix = "_t",
330-
bool force = false);
344+
bool force = false, bool zeroInit = false);
331345
clang::Expr* GlobalStoreAndRef(clang::Expr* E,
332346
llvm::StringRef prefix = "_t",
333-
bool force = false);
347+
bool force = false, bool zeroInit = false);
334348
virtual StmtDiff StoreAndRestore(clang::Expr* E,
335349
llvm::StringRef prefix = "_t",
336350
bool moveToTape = false);

include/clad/Differentiator/VisitorBase.h

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "clang/AST/StmtVisitor.h"
1919
#include "clang/AST/Type.h"
2020
#include "clang/Basic/Diagnostic.h"
21+
#include "clang/Basic/Lambda.h"
2122
#include "clang/Basic/OperatorKinds.h"
2223
#include "clang/Basic/Specifiers.h"
2324
#include "clang/Sema/DeclSpec.h"
@@ -266,23 +267,56 @@ namespace clad {
266267
/// The currently visited statement. Useful for crash pretty-printing.
267268
const clang::Stmt* m_CurVisitedStmt = nullptr;
268269

270+
/// Resolves the captures of a lambda synthesized from a body that was
271+
/// built outside the closure scope, so Sema never saw the uses. collect()
272+
/// finds the body's free variables -- locals and parameters it references
273+
/// but does not itself declare. resolve() re-creates those references in
274+
/// the current scope, where BuildDeclRef under the `[&]` default registers
275+
/// the capture and marks the reference for codegen; it must run with the
276+
/// lambda scope active. contains() exposes the set for a caller that must
277+
/// place the captured decls before the lambda.
278+
class LambdaCaptures {
279+
VisitorBase& m_V;
280+
llvm::SmallPtrSet<clang::VarDecl*, 8> m_Captures;
281+
282+
public:
283+
explicit LambdaCaptures(VisitorBase& V) : m_V(V) {}
284+
void collect(llvm::ArrayRef<clang::Stmt*> Body);
285+
/// A `[&]` capture binds a variable at the lambda's definition point, so
286+
/// every captured decl must precede the lambda. \p Prefix and \p Suffix
287+
/// are the forward block split at the lambda's insertion point; move each
288+
/// captured DeclStmt from Suffix to the end of Prefix when its
289+
/// initializer references only names already live there -- the function's
290+
/// parameters, \p AlreadyLive (decls emitted earlier), and Prefix.
291+
void orderCaptureDecls(llvm::SmallVectorImpl<clang::Stmt*>& Prefix,
292+
llvm::SmallVectorImpl<clang::Stmt*>& Suffix,
293+
llvm::ArrayRef<clang::Stmt*> AlreadyLive);
294+
void resolve(llvm::ArrayRef<clang::Stmt*> Body);
295+
bool contains(clang::VarDecl* VD) const { return m_Captures.count(VD); }
296+
};
297+
269298
/// Build a lambda whose body is produced by `func`. Returns the
270299
/// LambdaExpr without invoking it, so the caller can bind it to a VarDecl
271300
/// and call it from multiple sites. `func` is invoked inside the lambda's
272301
/// scope and block; statements are expected to be added via
273302
/// addToCurrentBlock from func's invocation.
303+
///
304+
/// The lambda uses the `[&]` capture-default; Sema resolves captures from
305+
/// the body's ODR-uses. A pre-built body whose DeclRefExprs were made
306+
/// outside this scope must have those references rebuilt in scope so Sema
307+
/// sees the uses (see LambdaCaptures::resolve).
274308
// FIXME: This will become problematic when we try to support C.
275309
template <typename F>
276310
static clang::Expr* buildLambda(VisitorBase& V, clang::Sema& S,
277-
const clang::Expr* E, F&& func) {
311+
const clang::Stmt* LocSrc, F&& func) {
278312
// FIXME: Here we use some of the things that are used from Parser, it
279313
// seems to be the easiest way to create lambda.
280314
clang::LambdaIntroducer Intro;
281315
Intro.Default = clang::LCD_ByRef;
282316
// FIXME: Using noLoc here results in assert failure. Any other valid
283317
// SourceLocation seems to work fine.
284-
Intro.Range.setBegin(E->getBeginLoc());
285-
Intro.Range.setEnd(E->getEndLoc());
318+
Intro.Range.setBegin(LocSrc->getBeginLoc());
319+
Intro.Range.setEnd(LocSrc->getEndLoc());
286320
clang::AttributeFactory AttrFactory;
287321
const clang::DeclSpec DS(AttrFactory);
288322
clang::Declarator D(
@@ -329,8 +363,8 @@ namespace clad {
329363
/// added by addToCurrentBlock from func invocation.
330364
template <typename F>
331365
static clang::Expr* wrapInLambda(VisitorBase& V, clang::Sema& S,
332-
const clang::Expr* E, F&& func) {
333-
clang::Expr* lambda = buildLambda(V, S, E, std::forward<F>(func));
366+
const clang::Stmt* LocSrc, F&& func) {
367+
clang::Expr* lambda = buildLambda(V, S, LocSrc, std::forward<F>(func));
334368
return S.ActOnCallExpr(V.getCurrentScope(), lambda, noLoc, {}, noLoc)
335369
.get();
336370
}
@@ -341,13 +375,29 @@ namespace clad {
341375
/// more sites via DeclRefExpr + ActOnCallExpr. Use this when the same
342376
/// lambda body must be invoked from multiple paths (e.g. a reverse-pass
343377
/// segment shared between an early-return path and the natural tail).
378+
///
379+
/// The binding uses `auto` deduction so the pretty-printer renders it as
380+
/// `auto X = [&] {...};` rather than the closure type's unspellable
381+
/// `(lambda at ...)` form. Sema deduces the concrete closure type from
382+
/// the initializer; the TypeSourceInfo retains the `auto` keyword.
383+
///
384+
/// \p func emits the closure body; \p Captures then resolves that body's
385+
/// references to enclosing variables (its collect() must have already run),
386+
/// so callers hand over a pure body-emission callback.
344387
template <typename F>
345-
clang::VarDecl* buildAndBindLambda(const clang::Expr* LocE,
346-
llvm::StringRef NameHint, F&& func) {
347-
clang::Expr* lambda =
348-
buildLambda(*this, m_Sema, LocE, std::forward<F>(func));
388+
clang::VarDecl* buildAndBindLambda(const clang::Stmt* LocSrc,
389+
llvm::StringRef NameHint,
390+
LambdaCaptures& Captures, F&& func) {
391+
clang::Expr* lambda = buildLambda(*this, m_Sema, LocSrc, [&] {
392+
std::forward<F>(func)();
393+
// Resolve captures while the closure scope is active and its body is
394+
// the current block.
395+
Captures.resolve(getCurrentBlock());
396+
});
349397
clang::IdentifierInfo* II = CreateUniqueIdentifier(NameHint);
350-
return BuildVarDecl(lambda->getType(), II, lambda);
398+
clang::QualType AutoTy = m_Context.getAutoDeductType();
399+
clang::TypeSourceInfo* TSI = m_Context.getTrivialTypeSourceInfo(AutoTy);
400+
return BuildVarDecl(AutoTy, II, lambda, /*DirectInit=*/false, TSI);
351401
}
352402

353403
/// For a qualtype QT returns if it's type is Array or Pointer Type

lib/Differentiator/DerivativeBuilder.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,9 +397,11 @@ static void registerDerivative(Decl* D, Sema& S, const DiffRequest& R) {
397397

398398
clang::FunctionDecl*
399399
DerivativeBuilder::HandleNestedDiffRequest(DiffRequest& request) {
400-
// FIXME: Find a way to do this without accessing plugin namespace functions
401400
bool alreadyDerived = true;
402401
request.UpdateDiffParamsInfo(m_Sema);
402+
// Plan this lazily-scheduled request statically, so it carries the planning
403+
// info (currently the early-return flag) the static TU walk never produced.
404+
m_Scheduler.Plan(request);
403405
FunctionDecl* derivative = this->FindDerivedFunction(request);
404406
if (!derivative) {
405407
alreadyDerived = false;

lib/Differentiator/DiffPlanner.cpp

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,24 @@ static QualType GetDerivedFunctionType(const CallExpr* CE) {
348348
return true;
349349
}
350350

351+
bool DiffCollector::PlanNestedRequest(DiffRequest& request) {
352+
// Lazily-scheduled requests (pushforward/pullback/higher-order, built in
353+
// DerivativeBuilder::HandleNestedDiffRequest) never pass through the static
354+
// TU walk, so their planning must be done here on demand. For now this only
355+
// records the early-return flag: m_TopMostReq is left null, so
356+
// VisitCallExpr and VisitDeclRefExpr bail and no sub-requests are spawned
357+
// -- only VisitReturnStmt runs over this request's body.
358+
// FIXME: This is where a nested request should also get its m_AnalysisDC
359+
// and the analyses currently force-disabled in HandleNestedDiffRequest.
360+
const FunctionDecl* Def =
361+
request.Function ? request.Function->getDefinition() : nullptr;
362+
if (!Def || !Def->hasBody())
363+
return true;
364+
llvm::SaveAndRestore<DiffRequest*> Saved(m_ParentReq, &request);
365+
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
366+
return TraverseStmt(const_cast<Stmt*>(Def->getBody()));
367+
}
368+
351369
bool DiffCollector::isInInterval(SourceLocation Loc) const {
352370
const SourceManager &SM = m_Sema.getSourceManager();
353371
for (size_t i = 0, e = m_Interval.size(); i < e; ++i) {
@@ -364,6 +382,43 @@ static QualType GetDerivedFunctionType(const CallExpr* CE) {
364382
return false;
365383
}
366384

385+
const ReturnStmt* DiffRequest::getTailReturn() const {
386+
const FunctionDecl* Def = Function ? Function->getDefinition() : nullptr;
387+
if (!Def || !Def->hasBody())
388+
return nullptr;
389+
const Stmt* Body = Def->getBody();
390+
// The tail return is the body's last statement, when that is a return.
391+
if (const auto* CS = dyn_cast<CompoundStmt>(Body))
392+
return CS->body_empty() ? nullptr
393+
: dyn_cast<ReturnStmt>(*CS->body_rbegin());
394+
return dyn_cast<ReturnStmt>(Body);
395+
}
396+
397+
bool DiffRequest::hasEarlyReturns() const {
398+
if (m_EarlyReturnInfo.HasAnalysisRun)
399+
return m_EarlyReturnInfo.HasEarlyReturns;
400+
const FunctionDecl* Def = Function ? Function->getDefinition() : nullptr;
401+
if (!Def || !Def->hasBody() || Def->getReturnType()->isVoidType())
402+
return false;
403+
// An early-return body has a return that is not the tail return. Returns
404+
// inside a nested lambda belong to that lambda's own function, so do not
405+
// descend into one.
406+
struct Finder : RecursiveASTVisitor<Finder> {
407+
const ReturnStmt* Tail;
408+
bool Found = false;
409+
bool TraverseLambdaExpr(LambdaExpr*) { return true; }
410+
bool VisitReturnStmt(ReturnStmt* RS) {
411+
if (RS != Tail)
412+
Found = true;
413+
return !Found; // stop at the first early return
414+
}
415+
} F;
416+
F.Tail = getTailReturn();
417+
F.TraverseStmt(const_cast<Stmt*>(cast<Stmt>(Def->getBody())));
418+
m_EarlyReturnInfo = {F.Found, /*HasAnalysisRun=*/true};
419+
return F.Found;
420+
}
421+
367422
void DiffRequest::UpdateDiffParamsInfo(Sema& semaRef) {
368423
// Diff info for pullbacks is generated automatically,
369424
// its parameters are not provided by the user.
@@ -1504,7 +1559,10 @@ static QualType GetDerivedFunctionType(const CallExpr* CE) {
15041559
}
15051560

15061561
bool DiffCollector::VisitDeclRefExpr(DeclRefExpr* DRE) {
1507-
if (!m_ParentReq)
1562+
// m_TopMostReq is dereferenced below; it is null when PlanNestedRequest
1563+
// walks a lazy request's body just to record its early-return flag, and no
1564+
// global-adjoint discovery is wanted there.
1565+
if (!m_ParentReq || !m_TopMostReq)
15081566
return true;
15091567
// FIXME: Add support for globals in other modes.
15101568
if (m_ParentReq->Mode != DiffMode::reverse &&

0 commit comments

Comments
 (0)