Skip to content
Closed
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
87 changes: 69 additions & 18 deletions gen/llvmhelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,64 @@ void DtoResolveVariable(VarDeclaration *vd) {
}
}

namespace {
bool eval(VarDeclaration *vd, Expression *e) {

auto ve = e->isVarExp();
if (!ve) {
return false;
}
VarDeclaration *v = nullptr;
if (ve)
v = ve->var->isVarDeclaration();
return vd == v;
}
bool walk(VarDeclaration *vd, CommaExp *ce) {
IF_LOG Logger::println("ce = %s", ce->toChars());
if (auto ce2 = ce->e2->isCommaExp()) {
if (walk(vd, ce2))
return true;
}
if (auto ce1 = ce->e1->isCommaExp()) {
if (walk(vd, ce1))
return true;
}
if (eval(vd, ce->e2))
return true;
if (eval(vd, ce->e1))
return true;
return false;
}
bool varIsSret(VarDeclaration *vd, IrFunction *f) {
if (!f->sretArg)
return false;
auto fd = f->decl;
auto rets = fd->returns;
if (!rets)
return false;
#if LDC_LLVM_VER >= 1800
#define startswith starts_with
#endif
llvm::StringRef name = vd->ident->toChars();
if (name.startswith("__tmpfordtor") ||name.startswith("__sl")) {
return true;
}
#if LDC_LLVM_VER >= 1800
#undef startswith
#endif
for (d_size_t i = 0; i < rets->length; i++) {
auto rs = (*rets)[i];
Expression *e = rs->exp;
CommaExp *ce = e->isCommaExp();
if (ce) {
if (walk(vd, ce)) return true;
}
if (eval(vd, e))
return true;
}
return false;
}
}
/******************************************************************************
* DECLARATION EXP HELPER
******************************************************************************/
Expand Down Expand Up @@ -906,29 +964,22 @@ void DtoVarDeclaration(VarDeclaration *vd) {

// We also allocate a variable for zero-sized variables, because they are technically not `null` when loaded.
// The x86_64 ABI "loads" zero-sized function arguments, and without an allocation ASan will report an error (Github #4816).
llvm::Value *allocainst;
bool isRealAlloca = false;
LLType *lltype = DtoType(type); // void for noreturn
if (lltype->isVoidTy()) {
allocainst = getNullPtr();
} else if (type != vd->type) {
allocainst = DtoAlloca(type, vd->toChars());
isRealAlloca = true;
} else {
allocainst = DtoAlloca(vd, vd->toChars());
isRealAlloca = true;
irLocal->value = getNullPtr();
}
Copy link
Member

@kinke kinke Nov 5, 2025

Choose a reason for hiding this comment

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

The missing else here compared to my commit now means that this line above has no more effect for IR-void-typed vars; irLocal->value is immediately overwritten below with a dummy void-alloca (probably promoted to some dummy i8 storage). And now gets DI infos.

Edit: And additionally caused you to come up with a weird logic for the lifetime-start annotation apparently. So please just try my commit.

Copy link
Contributor Author

@thewilsonator thewilsonator Nov 5, 2025

Choose a reason for hiding this comment

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

Noted wr.t. DI infos, fixed that. Nope:

LLType *lltype = DtoType(type); // void for noreturn
if (lltype->isVoidTy()) {
  irLocal->value = getNullPtr();
} else {
  auto allocainst = type != vd->type ? DtoAlloca(type, vd->toChars())
                                     : DtoAlloca(vd, vd->toChars());
  irLocal->value = allocainst;
      
  gIR->DBuilder.EmitLocalVariable(allocainst, vd);
      
   // The lifetime of a stack variable starts from the point it is declared
    gIR->funcGen().localVariableLifetimeAnnotator.addLocalVariable(
           allocainst, DtoConstUlong(size(type)));
}

still fails with

llvm.lifetime.start can only be used on alloca or poison
  call void @llvm.lifetime.start.p0(ptr captures(none) %0) #4

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding back in the if

 if (!vd->isParameter() && !varIsSret(vd, gIR->func())) {
  gIR->funcGen().localVariableLifetimeAnnotator.addLocalVariable(
             allocainst, DtoConstUlong(size(type)));
}

then causes it to pass.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

alternately if (AFAIU) the entire point of these lifetime annotations is to aid debugging, then we could restrict them to only user variables (i.e. ig more all compiler generated ones beginning with __, e.g. __copytemp which in addition to __tmpfordtor and __sl seems to cover all the bases).

Copy link
Member

Choose a reason for hiding this comment

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

Well IIRC, Phobos couldn't be compiled successfully in #4990 after cherry-picking an earlier version of this PR's commit, but applying both linked commits I linked made it work with LLVM 21 and older versions at least. I think I understood what you were after with the earlier unrelated refactoring (combining the DI and lifetime-start conditions to non-IR-void-typed vars only, without the realAlloca helper var and a 2nd void check), that made sense; it's just that the early return instead of an else branch caused skipping some stuff at the end of the function.

Copy link
Member

@kinke kinke Nov 6, 2025

Choose a reason for hiding this comment

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

Also note that the sret NRVO case is handled just above in an earlier if:

ldc/gen/llvmhelpers.cpp

Lines 887 to 900 in b5f7bb0

} else if (gIR->func()->sretArg &&
((gIR->func()->decl->isNRVO() &&
gIR->func()->decl->nrvo_var == vd) ||
(vd->isResult() && !isSpecialRefVar(vd)))) {
// Named Return Value Optimization (NRVO):
// T f() {
// T ret; // &ret == hidden pointer
// ret = ...
// return ret; // NRVO.
// }
assert(!isSpecialRefVar(vd) && "Can this happen?");
getIrLocal(vd, true)->value = gIR->func()->sretArg;
gIR->DBuilder.EmitLocalVariable(gIR->func()->sretArg, vd);
} else {

That's where the lval is set to the sret argument instead of an alloca. And where no lifetime-start annotation is added.

Copy link
Member

@kinke kinke Nov 6, 2025

Choose a reason for hiding this comment

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

Okay, but as your interesting reduced test case shows, that's not enough. In-place construction can replace a temporary alloca later with an sret argument, as the lvalue of a variable:

ldc/gen/toir.cpp

Lines 3002 to 3015 in b5f7bb0

// and temporaries
else if (isTemporaryVar(rhs)) {
Logger::println("success, in-place-constructing temporary");
auto lhsLVal = DtoLVal(lhs);
auto rhsLVal = DtoLVal(rhs);
if (!llvm::isa<llvm::AllocaInst>(rhsLVal)) {
error(rhs->loc, "lvalue of temporary is not an alloca, please "
"file an LDC issue");
fatal();
}
if (lhsLVal != rhsLVal)
rhsLVal->replaceAllUsesWith(lhsLVal);
return true;
}

According to the -vv log, this is what happens in the last memoizeExpr return statement:

ExpStatement::toIR(): <source>(15)
* D to dwarf stoppoint at line 15, column 5
* DeclarationExp::toElem: (CodepointSet slot = 0;) | T=void
* * DtoDeclarationExp: slot
* * * VarDeclaration
* * * DtoVarDeclaration(vdtype = CodepointSet)
* * * * llvm value for decl:   %2 = alloca %example.CodepointSet, align 1
* * * * expression initializer
* * * * AssignExp::toElem: slot = 0 | (CodepointSet)(CodepointSet = int)
* * * * * VarExp::toElem: slot @ CodepointSet
* * * * * * DtoSymbolAddress ('slot' of type 'CodepointSet')
* * * * * * * a normal variable
* * * * * attempting in-place construction
* * * * * * aborted due to different base types without modifiers
* * * * * IntegerExp::toElem: 0 @ int
* * * * * * IntegerExp::toConstElem: 0 @ int
* * * * * * * value = i32 0
* * * * * performing aggregate zero initialization
ReturnStatement::toIR(): <source>(16)
* D to dwarf stoppoint at line 16, column 5
* attempting in-place construction
* * success, in-place-constructing temporary
* * CommaExp::toElem: (CodepointSet __copytmp3 = (__copytmp3 = slot).this(this)();) , __copytmp3 @ CodepointSet
* * * DeclarationExp::toElem: (CodepointSet __copytmp3 = (__copytmp3 = slot).this(this)();) | T=void
* * * * DtoDeclarationExp: __copytmp3
* * * * * VarDeclaration
* * * * * DtoVarDeclaration(vdtype = CodepointSet)
* * * * * * llvm value for decl:   %3 = alloca %example.CodepointSet, align 1
* * * * * * expression initializer
* * * * * * CallExp::toElem: (__copytmp3 = slot).this(this)() @ void
* * * * * * * DotVarExp::toElem: (__copytmp3 = slot).this(this) @ void()
* * * * * * * * AssignExp::toElem: __copytmp3 = slot | (CodepointSet)(CodepointSet = CodepointSet)
* * * * * * * * * VarExp::toElem: __copytmp3 @ CodepointSet
* * * * * * * * * * DtoSymbolAddress ('__copytmp3' of type 'CodepointSet')
* * * * * * * * * * * a normal variable
* * * * * * * * * attempting in-place construction
* * * * * * * * * VarExp::toElem: slot @ CodepointSet
* * * * * * * * * * DtoSymbolAddress ('slot' of type 'CodepointSet')
* * * * * * * * * * * a normal variable
* * * * * * * * * performing normal assignment (rhs has lvalue elems = 1)
* * * * * * * * * DtoAssign()
* * * * * * * DtoCallFunction()
* * * * * * * * Building type: void()
* * * * * * * * * DtoFunctionType(void())
* * * * * * * * * * x86-64 ABI: Transforming argument types
* * * * * * * * * * Final function type: void ()
* * * * * * * * doing normal arguments
* * * * * * * * Arguments so far: (1)
* * * * * * * * *   %3 = alloca %example.CodepointSet, align 1
* * * * * * * * Function type: void()
* * * VarExp::toElem: __copytmp3 @ CodepointSet
* * * * DtoSymbolAddress ('__copytmp3' of type 'CodepointSet')
* * * * * a normal variable

So __copytmp3 first gets an alloca, and then that lval is IR-replaced with the sret argument later, incl. for the lifetime-start call. Your diagnosis might be right wrt. __copytmp3 apparently not being the frontend-NRVO variable directly; the return expression in the if (__ctfe) block might break it (as the NRVO var must normally be returned in every return statement of the function).

Edit: But there are always going to be remaining non-NRVO cases. So for these lifetime annotations, we might have to skip this in-place construction of sret values from non-NRVO temporaries (edit: oh well, the extra memcpy and different address might break code depending on RVO though...). Or scan the existing uses, to remove the lifetime intrinsic calls (possibly missing the lifetime end calls if they are pushed later on...), before replacing all uses.

Edit2: I think what would probably be best is to pre-set the lval (i.e., getIrLocal(vd, true)->value) of the temporary to the sret argument in toInPlaceConstruction(), before evaluating the comma-expression. [Incl. emitting DI there, as DtoVarDeclaration() wouldn't do it anymore due to isIrLocalCreated(vd).] Instead of replacing all IR uses after a normal evaluation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Your diagnosis might be right wrt. __copytmp3 apparently not being the frontend-NRVO variable directly; the return expression in the if (__ctfe) block might break it (as the NRVO var must normally be returned in every return statement of the function).

Yeah, an if(false) return ...; also breaks NRVO. I opened a DMD issue for this.

The removal of isRealAlloca was mostly to try to use the type system to fix/make sure I wasn't doing anything stupid with it, but it seems that the replaceAllUsesWith is what is causing it to change type from an AllocaInst to Argument. Thank you for finding that, I thought I was going mad!

It seems to me that the easiest solution then would be to strip the calls to @llvm.lifetime prior to the replaceAllUsesWith, though I don't quite understand what you are trying to say in your Edit2.

Anyway I've added two of the reductions to the test suite (there is another one with __tmpfordtor that I ned to re-run). I'm going away for the weekend, so if you feel like implementing any of your suggestions, please do.

Copy link
Member

Choose a reason for hiding this comment

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

good find!

Copy link
Member

Choose a reason for hiding this comment

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

so if you feel like implementing any of your suggestions, please do.

Okay, I've tried: #5015. I'm pretty sure I've added that replace-all-uses-with hack, so only fair to clean it up now. :)


irLocal->value = allocainst;

if (!lltype->isVoidTy())
else {
auto allocainst = type != vd->type ? DtoAlloca(type, vd->toChars())
: DtoAlloca(vd, vd->toChars());
irLocal->value = allocainst;

gIR->DBuilder.EmitLocalVariable(allocainst, vd);

// Lifetime annotation is only valid on alloca.
if (isRealAlloca) {

// The lifetime of a stack variable starts from the point it is declared
gIR->funcGen().localVariableLifetimeAnnotator.addLocalVariable(
allocainst, DtoConstUlong(size(type)));
if (!vd->isParameter() && !varIsSret(vd, gIR->func())) {
gIR->funcGen().localVariableLifetimeAnnotator.addLocalVariable(
allocainst, DtoConstUlong(size(type)));
}
}
}

Expand Down
21 changes: 16 additions & 5 deletions gen/variable_lifetime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ LocalVariableLifetimeAnnotator::LocalVariableLifetimeAnnotator(IRState &irs)

void LocalVariableLifetimeAnnotator::pushScope() { scopes.emplace_back(); }

void LocalVariableLifetimeAnnotator::addLocalVariable(llvm::Value *address,
void LocalVariableLifetimeAnnotator::addLocalVariable(llvm::AllocaInst *address,
llvm::Value *size) {
assert(address);
assert(size);
Expand All @@ -52,8 +52,13 @@ void LocalVariableLifetimeAnnotator::addLocalVariable(llvm::Value *address,
scopes.back().variables.emplace_back(size, address);

// Emit lifetime start
irs.CreateCallOrInvoke(getLLVMLifetimeStartFn(), {size, address}, "",
true /*nothrow*/);
irs.CreateCallOrInvoke(getLLVMLifetimeStartFn(),
#if LDC_LLVM_VER >= 2100
{address},
#else
{size, address},
#endif
"", true /*nothrow*/);
}

// Emits end-of-lifetime annotation for all variables in current scope.
Expand All @@ -67,8 +72,14 @@ void LocalVariableLifetimeAnnotator::popScope() {

assert(address);

irs.CreateCallOrInvoke(getLLVMLifetimeEndFn(), {size, address}, "",
true /*nothrow*/);
irs.CreateCallOrInvoke(getLLVMLifetimeEndFn(),
#if LDC_LLVM_VER >= 2100
{address},
#else
{size, address},
#endif
"", true /*nothrow*/);

}
scopes.pop_back();
}
Expand Down
5 changes: 3 additions & 2 deletions gen/variable_lifetime.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ namespace llvm {
class Function;
class Type;
class Value;
class AllocaInst;
}
struct IRState;

struct LocalVariableLifetimeAnnotator {
struct LocalVariableScope {
std::vector<std::pair<llvm::Value *, llvm::Value *>> variables;
std::vector<std::pair<llvm::Value *, llvm::AllocaInst *>> variables;
};
/// Stack of scopes, each scope can have multiple variables.
std::vector<LocalVariableScope> scopes;
Expand All @@ -52,5 +53,5 @@ struct LocalVariableLifetimeAnnotator {
void popScope();

/// Register a new local variable for lifetime annotation.
void addLocalVariable(llvm::Value *address, llvm::Value *size);
void addLocalVariable(llvm::AllocaInst *address, llvm::Value *size);
};
Loading