Skip to content

Commit 95f0fab

Browse files
authored
[clang][bytecode] Fix conditional operator scoping wrt. local variables (#169030)
We used to create a scope for the true- and false expression of a conditional operator. This was done so e.g. in this example: ```c++ struct A { constexpr A(){}; ~A(); constexpr int get() { return 10; } }; // all-note 2{{declared here}} static_assert( (false ? A().get() : 1) == 1); ``` we did _not_ evaluate the true branch at all, meaning we did not register the local variable for the temporary of type `A`, which means we also didn't call it destructor. However, this breaks the case where the temporary needs to outlive the conditional operator and instead be destroyed via the surrounding `ExprWithCleanups`: ``` constexpr bool test2(bool b) { unsigned long __ms = b ? (const unsigned long &)0 : __ms; return true; } static_assert(test2(true)); ``` Before this patch, we diagnosed this example: ```console ./array.cpp:180:15: error: static assertion expression is not an integral constant expression 180 | static_assert(test2(true)); | ^~~~~~~~~~~ ./array.cpp:177:24: note: read of temporary whose lifetime has ended 177 | unsigned long __ms = b ? (const unsigned long &)0 : __ms; | ^ ./array.cpp:180:15: note: in call to 'test2(true)' 180 | static_assert(test2(true)); | ^~~~~~~~~~~ ./array.cpp:177:51: note: temporary created here 177 | unsigned long __ms = b ? (const unsigned long &)0 : __ms; | ^ 1 error generated. ``` because the temporary created for the true branch got immediately destroyed. The problem in essence is that since the conditional operator doesn't create a scope at all, we register the local variables for both its branches, but we later only execute one of them, which means we should also only destroy the locals of one of the branches. We fix this similar to clang codgen's `is_active` flag: In the case of a conditional operator (which is so far the only case where this is problematic, and this also helps minimize the performance impact of this change), we make local variables as disabled-by-default and then emit a `EnableLocal` opcode later, which marks them as enabled. The code calling their destructors checks whether the local was enabled at all.
1 parent 9ce6fad commit 95f0fab

File tree

10 files changed

+148
-19
lines changed

10 files changed

+148
-19
lines changed

clang/lib/AST/ByteCode/ByteCodeEmitter.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ enum Opcode : uint32_t;
2525
/// An emitter which links the program to bytecode for later use.
2626
class ByteCodeEmitter {
2727
protected:
28-
using LabelTy = uint32_t;
2928
using AddrTy = uintptr_t;
3029
using Local = Scope::Local;
3130

3231
public:
32+
using LabelTy = uint32_t;
3333
/// Compiles the function into the module.
3434
void compileFunc(const FunctionDecl *FuncDecl, Function *Func = nullptr);
3535

clang/lib/AST/ByteCode/Compiler.cpp

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "PrimType.h"
1717
#include "Program.h"
1818
#include "clang/AST/Attr.h"
19+
#include "llvm/Support/SaveAndRestore.h"
1920

2021
using namespace clang;
2122
using namespace clang::interp;
@@ -2500,17 +2501,18 @@ bool Compiler<Emitter>::VisitAbstractConditionalOperator(
25002501
const Expr *TrueExpr = E->getTrueExpr();
25012502
const Expr *FalseExpr = E->getFalseExpr();
25022503

2503-
auto visitChildExpr = [&](const Expr *E) -> bool {
2504-
LocalScope<Emitter> S(this);
2505-
if (!this->delegate(E))
2506-
return false;
2507-
return S.destroyLocals();
2508-
};
2504+
// The TrueExpr and FalseExpr of a conditional operator do _not_ create a
2505+
// scope, which means the local variables created within them unconditionally
2506+
// always exist. However, we need to later differentiate which branch was
2507+
// taken and only destroy the varibles of the active branch. This is what the
2508+
// "enabled" flags on local variables are used for.
2509+
llvm::SaveAndRestore LAAA(this->VarScope->LocalsAlwaysEnabled,
2510+
/*NewValue=*/false);
25092511

25102512
if (std::optional<bool> BoolValue = getBoolValue(Condition)) {
25112513
if (*BoolValue)
2512-
return visitChildExpr(TrueExpr);
2513-
return visitChildExpr(FalseExpr);
2514+
return this->delegate(TrueExpr);
2515+
return this->delegate(FalseExpr);
25142516
}
25152517

25162518
bool IsBcpCall = false;
@@ -2542,13 +2544,15 @@ bool Compiler<Emitter>::VisitAbstractConditionalOperator(
25422544

25432545
if (!this->jumpFalse(LabelFalse))
25442546
return false;
2545-
if (!visitChildExpr(TrueExpr))
2547+
if (!this->delegate(TrueExpr))
25462548
return false;
2549+
25472550
if (!this->jump(LabelEnd))
25482551
return false;
25492552
this->emitLabel(LabelFalse);
2550-
if (!visitChildExpr(FalseExpr))
2553+
if (!this->delegate(FalseExpr))
25512554
return false;
2555+
25522556
this->fallthrough(LabelEnd);
25532557
this->emitLabel(LabelEnd);
25542558

@@ -2955,10 +2959,15 @@ bool Compiler<Emitter>::VisitMaterializeTemporaryExpr(
29552959
bool IsVolatile = SubExpr->getType().isVolatileQualified();
29562960
unsigned LocalIndex = allocateLocalPrimitive(
29572961
E, *SubExprT, IsConst, IsVolatile, E->getExtendingDecl());
2962+
if (!this->VarScope->LocalsAlwaysEnabled &&
2963+
!this->emitEnableLocal(LocalIndex, E))
2964+
return false;
2965+
29582966
if (!this->visit(SubExpr))
29592967
return false;
29602968
if (!this->emitSetLocal(*SubExprT, LocalIndex, E))
29612969
return false;
2970+
29622971
return this->emitGetPtrLocal(LocalIndex, E);
29632972
}
29642973

@@ -2968,6 +2977,11 @@ bool Compiler<Emitter>::VisitMaterializeTemporaryExpr(
29682977
if (UnsignedOrNone LocalIndex =
29692978
allocateLocal(E, Inner->getType(), E->getExtendingDecl())) {
29702979
InitLinkScope<Emitter> ILS(this, InitLink::Temp(*LocalIndex));
2980+
2981+
if (!this->VarScope->LocalsAlwaysEnabled &&
2982+
!this->emitEnableLocal(*LocalIndex, E))
2983+
return false;
2984+
29712985
if (!this->emitGetPtrLocal(*LocalIndex, E))
29722986
return false;
29732987
return this->visitInitializer(SubExpr) && this->emitFinishInit(E);

clang/lib/AST/ByteCode/Compiler.h

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -477,12 +477,14 @@ template <class Emitter> class VariableScope {
477477
VariableScope(Compiler<Emitter> *Ctx, const ValueDecl *VD,
478478
ScopeKind Kind = ScopeKind::Block)
479479
: Ctx(Ctx), Parent(Ctx->VarScope), ValDecl(VD), Kind(Kind) {
480+
if (Parent)
481+
this->LocalsAlwaysEnabled = Parent->LocalsAlwaysEnabled;
480482
Ctx->VarScope = this;
481483
}
482484

483485
virtual ~VariableScope() { Ctx->VarScope = this->Parent; }
484486

485-
virtual void addLocal(const Scope::Local &Local) {
487+
virtual void addLocal(Scope::Local Local) {
486488
llvm_unreachable("Shouldn't be called");
487489
}
488490

@@ -519,7 +521,6 @@ template <class Emitter> class VariableScope {
519521
if (!P)
520522
break;
521523
}
522-
523524
// Add to this scope.
524525
this->addLocal(Local);
525526
}
@@ -529,6 +530,11 @@ template <class Emitter> class VariableScope {
529530
VariableScope *getParent() const { return Parent; }
530531
ScopeKind getKind() const { return Kind; }
531532

533+
/// Whether locals added to this scope are enabled by default.
534+
/// This is almost always true, except for the two branches
535+
/// of a conditional operator.
536+
bool LocalsAlwaysEnabled = true;
537+
532538
protected:
533539
/// Compiler instance.
534540
Compiler<Emitter> *Ctx;
@@ -566,29 +572,48 @@ template <class Emitter> class LocalScope : public VariableScope<Emitter> {
566572
return Success;
567573
}
568574

569-
void addLocal(const Scope::Local &Local) override {
575+
void addLocal(Scope::Local Local) override {
570576
if (!Idx) {
571577
Idx = static_cast<unsigned>(this->Ctx->Descriptors.size());
572578
this->Ctx->Descriptors.emplace_back();
573579
this->Ctx->emitInitScope(*Idx, {});
574580
}
575581

582+
Local.EnabledByDefault = this->LocalsAlwaysEnabled;
576583
this->Ctx->Descriptors[*Idx].emplace_back(Local);
577584
}
578585

579586
bool emitDestructors(const Expr *E = nullptr) override {
580587
if (!Idx)
581588
return true;
589+
assert(!this->Ctx->Descriptors[*Idx].empty());
590+
582591
// Emit destructor calls for local variables of record
583592
// type with a destructor.
584593
for (Scope::Local &Local : llvm::reverse(this->Ctx->Descriptors[*Idx])) {
585594
if (Local.Desc->hasTrivialDtor())
586595
continue;
587-
if (!this->Ctx->emitGetPtrLocal(Local.Offset, E))
588-
return false;
589596

590-
if (!this->Ctx->emitDestructionPop(Local.Desc, Local.Desc->getLoc()))
591-
return false;
597+
if (!Local.EnabledByDefault) {
598+
typename Emitter::LabelTy EndLabel = this->Ctx->getLabel();
599+
if (!this->Ctx->emitGetLocalEnabled(Local.Offset, E))
600+
return false;
601+
if (!this->Ctx->jumpFalse(EndLabel))
602+
return false;
603+
604+
if (!this->Ctx->emitGetPtrLocal(Local.Offset, E))
605+
return false;
606+
607+
if (!this->Ctx->emitDestructionPop(Local.Desc, Local.Desc->getLoc()))
608+
return false;
609+
610+
this->Ctx->emitLabel(EndLabel);
611+
} else {
612+
if (!this->Ctx->emitGetPtrLocal(Local.Offset, E))
613+
return false;
614+
if (!this->Ctx->emitDestructionPop(Local.Desc, Local.Desc->getLoc()))
615+
return false;
616+
}
592617

593618
removeIfStoredOpaqueValue(Local);
594619
}

clang/lib/AST/ByteCode/EvalEmitter.cpp

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ Scope::Local EvalEmitter::createLocal(Descriptor *D) {
113113
InlineDescriptor &Desc = *reinterpret_cast<InlineDescriptor *>(B->rawData());
114114
Desc.Desc = D;
115115
Desc.Offset = sizeof(InlineDescriptor);
116-
Desc.IsActive = true;
116+
Desc.IsActive = false;
117117
Desc.IsBase = false;
118118
Desc.IsFieldMutable = false;
119119
Desc.IsConst = false;
@@ -322,6 +322,33 @@ bool EvalEmitter::emitDestroy(uint32_t I, SourceInfo Info) {
322322
return true;
323323
}
324324

325+
bool EvalEmitter::emitGetLocalEnabled(uint32_t I, SourceInfo Info) {
326+
if (!isActive())
327+
return true;
328+
329+
Block *B = getLocal(I);
330+
const InlineDescriptor &Desc =
331+
*reinterpret_cast<InlineDescriptor *>(B->rawData());
332+
333+
S.Stk.push<bool>(Desc.IsActive);
334+
return true;
335+
}
336+
337+
bool EvalEmitter::emitEnableLocal(uint32_t I, SourceInfo Info) {
338+
if (!isActive())
339+
return true;
340+
341+
// FIXME: This is a little dirty, but to avoid adding a flag to
342+
// InlineDescriptor that's only ever useful on the toplevel of local
343+
// variables, we reuse the IsActive flag for the enabled state. We should
344+
// probably use a different struct than InlineDescriptor for the block-level
345+
// inline descriptor of local varaibles.
346+
Block *B = getLocal(I);
347+
InlineDescriptor &Desc = *reinterpret_cast<InlineDescriptor *>(B->rawData());
348+
Desc.IsActive = true;
349+
return true;
350+
}
351+
325352
/// Global temporaries (LifetimeExtendedTemporary) carry their value
326353
/// around as an APValue, which codegen accesses.
327354
/// We set their value once when creating them, but we don't update it

clang/lib/AST/ByteCode/Function.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class Scope final {
4141
unsigned Offset;
4242
/// Descriptor of the local.
4343
Descriptor *Desc;
44+
/// If the cleanup for this local should be emitted.
45+
bool EnabledByDefault = true;
4446
};
4547

4648
using LocalVectorTy = llvm::SmallVector<Local, 8>;

clang/lib/AST/ByteCode/Interp.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2474,6 +2474,18 @@ inline bool InitScope(InterpState &S, CodePtr OpPC, uint32_t I) {
24742474
return true;
24752475
}
24762476

2477+
inline bool EnableLocal(InterpState &S, CodePtr OpPC, uint32_t I) {
2478+
assert(!S.Current->isLocalEnabled(I));
2479+
S.Current->enableLocal(I);
2480+
return true;
2481+
}
2482+
2483+
inline bool GetLocalEnabled(InterpState &S, CodePtr OpPC, uint32_t I) {
2484+
assert(S.Current);
2485+
S.Stk.push<bool>(S.Current->isLocalEnabled(I));
2486+
return true;
2487+
}
2488+
24772489
//===----------------------------------------------------------------------===//
24782490
// Cast, CastFP
24792491
//===----------------------------------------------------------------------===//

clang/lib/AST/ByteCode/InterpFrame.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,23 @@ void InterpFrame::destroyScopes() {
8989
void InterpFrame::initScope(unsigned Idx) {
9090
if (!Func)
9191
return;
92+
9293
for (auto &Local : Func->getScope(Idx).locals()) {
9394
localBlock(Local.Offset)->invokeCtor();
9495
}
9596
}
9697

98+
void InterpFrame::enableLocal(unsigned Idx) {
99+
assert(Func);
100+
101+
// FIXME: This is a little dirty, but to avoid adding a flag to
102+
// InlineDescriptor that's only ever useful on the toplevel of local
103+
// variables, we reuse the IsActive flag for the enabled state. We should
104+
// probably use a different struct than InlineDescriptor for the block-level
105+
// inline descriptor of local varaibles.
106+
localInlineDesc(Idx)->IsActive = true;
107+
}
108+
97109
void InterpFrame::destroy(unsigned Idx) {
98110
for (auto &Local : Func->getScope(Idx).locals_reverse()) {
99111
S.deallocate(localBlock(Local.Offset));

clang/lib/AST/ByteCode/InterpFrame.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ class InterpFrame final : public Frame {
5555
void destroy(unsigned Idx);
5656
void initScope(unsigned Idx);
5757
void destroyScopes();
58+
void enableLocal(unsigned Idx);
59+
bool isLocalEnabled(unsigned Idx) const {
60+
return localInlineDesc(Idx)->IsActive;
61+
}
5862

5963
/// Describes the frame with arguments for diagnostic purposes.
6064
void describe(llvm::raw_ostream &OS) const override;

clang/lib/AST/ByteCode/Opcodes.td

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,16 @@ def InitScope : Opcode {
251251
let Args = [ArgUint32];
252252
}
253253

254+
def GetLocalEnabled : Opcode {
255+
let Args = [ArgUint32];
256+
let HasCustomEval = 1;
257+
}
258+
259+
def EnableLocal : Opcode {
260+
let Args = [ArgUint32];
261+
let HasCustomEval = 1;
262+
}
263+
254264
//===----------------------------------------------------------------------===//
255265
// Constants
256266
//===----------------------------------------------------------------------===//

clang/test/AST/ByteCode/cxx23.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,3 +473,26 @@ namespace AIEWithIndex0Narrows {
473473
}
474474
static_assert(test());
475475
}
476+
477+
#if __cplusplus >= 202302L
478+
namespace InactiveLocalsInConditionalOp {
479+
struct A { constexpr A(){}; ~A(); constexpr int get() { return 10; } }; // all-note 2{{declared here}}
480+
constexpr int get(bool b) {
481+
return b ? A().get() : 1; // all-note {{non-constexpr function '~A' cannot be used in a constant expression}}
482+
}
483+
static_assert(get(false) == 1, "");
484+
static_assert(get(true) == 10, ""); // all-error {{not an integral constant expression}} \
485+
// all-note {{in call to}}
486+
487+
static_assert( (false ? A().get() : 1) == 1);
488+
static_assert( (true ? A().get() : 1) == 1); // all-error {{not an integral constant expression}} \
489+
// all-note {{non-constexpr function '~A' cannot be used in a constant expression}}
490+
491+
constexpr bool test2(bool b) {
492+
unsigned long __ms = b ? (const unsigned long &)0 : __ms;
493+
return true;
494+
}
495+
static_assert(test2(true));
496+
497+
}
498+
#endif

0 commit comments

Comments
 (0)