llvm-project/clang/lib/AST/ByteCode/InterpFrame.cpp
Timm Baeder 95f0fab7fa
[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.
2025-11-24 07:34:48 +01:00

316 lines
9.8 KiB
C++

//===--- InterpFrame.cpp - Call Frame implementation for the VM -*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "InterpFrame.h"
#include "Boolean.h"
#include "Function.h"
#include "InterpStack.h"
#include "InterpState.h"
#include "MemberPointer.h"
#include "Pointer.h"
#include "PrimType.h"
#include "Program.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/DeclCXX.h"
#include "clang/AST/ExprCXX.h"
using namespace clang;
using namespace clang::interp;
InterpFrame::InterpFrame(InterpState &S)
: Caller(nullptr), S(S), Depth(0), Func(nullptr), RetPC(CodePtr()),
ArgSize(0), Args(nullptr), FrameOffset(0) {}
InterpFrame::InterpFrame(InterpState &S, const Function *Func,
InterpFrame *Caller, CodePtr RetPC, unsigned ArgSize)
: Caller(Caller), S(S), Depth(Caller ? Caller->Depth + 1 : 0), Func(Func),
RetPC(RetPC), ArgSize(ArgSize), Args(static_cast<char *>(S.Stk.top())),
FrameOffset(S.Stk.size()) {
if (!Func)
return;
unsigned FrameSize = Func->getFrameSize();
if (FrameSize == 0)
return;
Locals = std::make_unique<char[]>(FrameSize);
for (auto &Scope : Func->scopes()) {
for (auto &Local : Scope.locals()) {
new (localBlock(Local.Offset)) Block(S.Ctx.getEvalID(), Local.Desc);
// Note that we are NOT calling invokeCtor() here, since that is done
// via the InitScope op.
new (localInlineDesc(Local.Offset)) InlineDescriptor(Local.Desc);
}
}
}
InterpFrame::InterpFrame(InterpState &S, const Function *Func, CodePtr RetPC,
unsigned VarArgSize)
: InterpFrame(S, Func, S.Current, RetPC, Func->getArgSize() + VarArgSize) {
// As per our calling convention, the this pointer is
// part of the ArgSize.
// If the function has RVO, the RVO pointer is first.
// If the fuction has a This pointer, that one is next.
// Then follow the actual arguments (but those are handled
// in getParamPointer()).
if (Func->hasRVO()) {
// RVO pointer offset is always 0.
}
if (Func->hasThisPointer())
ThisPointerOffset = Func->hasRVO() ? sizeof(Pointer) : 0;
}
InterpFrame::~InterpFrame() {
for (auto &Param : Params)
S.deallocate(reinterpret_cast<Block *>(Param.second.get()));
// When destroying the InterpFrame, call the Dtor for all block
// that haven't been destroyed via a destroy() op yet.
// This happens when the execution is interruped midway-through.
destroyScopes();
}
void InterpFrame::destroyScopes() {
if (!Func)
return;
for (auto &Scope : Func->scopes()) {
for (auto &Local : Scope.locals()) {
S.deallocate(localBlock(Local.Offset));
}
}
}
void InterpFrame::initScope(unsigned Idx) {
if (!Func)
return;
for (auto &Local : Func->getScope(Idx).locals()) {
localBlock(Local.Offset)->invokeCtor();
}
}
void InterpFrame::enableLocal(unsigned Idx) {
assert(Func);
// FIXME: This is a little dirty, but to avoid adding a flag to
// InlineDescriptor that's only ever useful on the toplevel of local
// variables, we reuse the IsActive flag for the enabled state. We should
// probably use a different struct than InlineDescriptor for the block-level
// inline descriptor of local varaibles.
localInlineDesc(Idx)->IsActive = true;
}
void InterpFrame::destroy(unsigned Idx) {
for (auto &Local : Func->getScope(Idx).locals_reverse()) {
S.deallocate(localBlock(Local.Offset));
}
}
template <typename T>
static void print(llvm::raw_ostream &OS, const T &V, ASTContext &ASTCtx,
QualType Ty) {
if constexpr (std::is_same_v<Pointer, T>) {
if (Ty->isPointerOrReferenceType())
V.toAPValue(ASTCtx).printPretty(OS, ASTCtx, Ty);
else {
if (std::optional<APValue> RValue = V.toRValue(ASTCtx, Ty))
RValue->printPretty(OS, ASTCtx, Ty);
else
OS << "...";
}
} else {
V.toAPValue(ASTCtx).printPretty(OS, ASTCtx, Ty);
}
}
static bool shouldSkipInBacktrace(const Function *F) {
if (F->isLambdaStaticInvoker())
return true;
const FunctionDecl *FD = F->getDecl();
if (FD->getDeclName().getCXXOverloadedOperator() == OO_New ||
FD->getDeclName().getCXXOverloadedOperator() == OO_Array_New)
return true;
if (const auto *MD = dyn_cast<CXXMethodDecl>(FD);
MD && MD->getParent()->isAnonymousStructOrUnion())
return true;
if (const auto *Ctor = dyn_cast<CXXConstructorDecl>(FD);
Ctor && Ctor->isDefaulted() && Ctor->isTrivial() &&
Ctor->isCopyOrMoveConstructor() && Ctor->inits().empty())
return true;
return false;
}
void InterpFrame::describe(llvm::raw_ostream &OS) const {
// For lambda static invokers, we would just print __invoke().
if (const auto *F = getFunction(); F && shouldSkipInBacktrace(F))
return;
const Expr *CallExpr = Caller->getExpr(getRetPC());
const FunctionDecl *F = getCallee();
bool IsMemberCall = isa<CXXMethodDecl>(F) && !isa<CXXConstructorDecl>(F) &&
cast<CXXMethodDecl>(F)->isImplicitObjectMemberFunction();
if (Func->hasThisPointer() && IsMemberCall) {
if (const auto *MCE = dyn_cast_if_present<CXXMemberCallExpr>(CallExpr)) {
const Expr *Object = MCE->getImplicitObjectArgument();
Object->printPretty(OS, /*Helper=*/nullptr,
S.getASTContext().getPrintingPolicy(),
/*Indentation=*/0);
if (Object->getType()->isPointerType())
OS << "->";
else
OS << ".";
} else if (const auto *OCE =
dyn_cast_if_present<CXXOperatorCallExpr>(CallExpr)) {
OCE->getArg(0)->printPretty(OS, /*Helper=*/nullptr,
S.getASTContext().getPrintingPolicy(),
/*Indentation=*/0);
OS << ".";
} else if (const auto *M = dyn_cast<CXXMethodDecl>(F)) {
print(OS, getThis(), S.getASTContext(),
S.getASTContext().getLValueReferenceType(
S.getASTContext().getCanonicalTagType(M->getParent())));
OS << ".";
}
}
F->getNameForDiagnostic(OS, S.getASTContext().getPrintingPolicy(),
/*Qualified=*/false);
OS << '(';
unsigned Off = 0;
Off += Func->hasRVO() ? primSize(PT_Ptr) : 0;
Off += Func->hasThisPointer() ? primSize(PT_Ptr) : 0;
for (unsigned I = 0, N = F->getNumParams(); I < N; ++I) {
QualType Ty = F->getParamDecl(I)->getType();
PrimType PrimTy = S.Ctx.classify(Ty).value_or(PT_Ptr);
TYPE_SWITCH(PrimTy, print(OS, stackRef<T>(Off), S.getASTContext(), Ty));
Off += align(primSize(PrimTy));
if (I + 1 != N)
OS << ", ";
}
OS << ")";
}
SourceRange InterpFrame::getCallRange() const {
if (!Caller->Func) {
if (SourceRange NullRange = S.getRange(nullptr, {}); NullRange.isValid())
return NullRange;
return S.EvalLocation;
}
// Move up to the frame that has a valid location for the caller.
for (const InterpFrame *C = this; C; C = C->Caller) {
if (!C->RetPC)
continue;
SourceRange CallRange =
S.getRange(C->Caller->Func, C->RetPC - sizeof(uintptr_t));
if (CallRange.isValid())
return CallRange;
}
return S.EvalLocation;
}
const FunctionDecl *InterpFrame::getCallee() const {
if (!Func)
return nullptr;
return Func->getDecl();
}
Pointer InterpFrame::getLocalPointer(unsigned Offset) const {
assert(Offset < Func->getFrameSize() && "Invalid local offset.");
return Pointer(localBlock(Offset));
}
Block *InterpFrame::getLocalBlock(unsigned Offset) const {
return localBlock(Offset);
}
Pointer InterpFrame::getParamPointer(unsigned Off) {
// Return the block if it was created previously.
if (auto Pt = Params.find(Off); Pt != Params.end())
return Pointer(reinterpret_cast<Block *>(Pt->second.get()));
assert(!isBottomFrame());
// Allocate memory to store the parameter and the block metadata.
const auto &Desc = Func->getParamDescriptor(Off);
size_t BlockSize = sizeof(Block) + Desc.second->getAllocSize();
auto Memory = std::make_unique<char[]>(BlockSize);
auto *B = new (Memory.get()) Block(S.Ctx.getEvalID(), Desc.second);
B->invokeCtor();
// Copy the initial value.
TYPE_SWITCH(Desc.first, new (B->data()) T(stackRef<T>(Off)));
// Record the param.
Params.insert({Off, std::move(Memory)});
return Pointer(B);
}
static bool funcHasUsableBody(const Function *F) {
assert(F);
if (F->isConstructor() || F->isDestructor())
return true;
return !F->getDecl()->isImplicit();
}
SourceInfo InterpFrame::getSource(CodePtr PC) const {
// Implicitly created functions don't have any code we could point at,
// so return the call site.
if (Func && !funcHasUsableBody(Func) && Caller)
return Caller->getSource(RetPC);
// Similarly, if the resulting source location is invalid anyway,
// point to the caller instead.
SourceInfo Result = S.getSource(Func, PC);
if (Result.getLoc().isInvalid() && Caller)
return Caller->getSource(RetPC);
return Result;
}
const Expr *InterpFrame::getExpr(CodePtr PC) const {
if (Func && !funcHasUsableBody(Func) && Caller)
return Caller->getExpr(RetPC);
return S.getExpr(Func, PC);
}
SourceLocation InterpFrame::getLocation(CodePtr PC) const {
if (Func && !funcHasUsableBody(Func) && Caller)
return Caller->getLocation(RetPC);
return S.getLocation(Func, PC);
}
SourceRange InterpFrame::getRange(CodePtr PC) const {
if (Func && !funcHasUsableBody(Func) && Caller)
return Caller->getRange(RetPC);
return S.getRange(Func, PC);
}
bool InterpFrame::isStdFunction() const {
if (!Func)
return false;
for (const DeclContext *DC = Func->getDecl(); DC; DC = DC->getParent())
if (DC->isStdNamespace())
return true;
return false;
}