Skip to content
Merged
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
26 changes: 19 additions & 7 deletions clang/lib/CIR/CodeGen/CIRGenCXX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,27 @@ static void emitDeclDestroy(CIRGenFunction &cgf, const VarDecl *vd,
// call right here.
auto gd = GlobalDecl(dtor, Dtor_Complete);
fnOp = cgm.getAddrAndTypeOfCXXStructor(gd).second;
cgf.getBuilder().createCallOp(
cgf.getLoc(vd->getSourceRange()),
mlir::FlatSymbolRefAttr::get(fnOp.getSymNameAttr()),
mlir::ValueRange{cgm.getAddrOfGlobalVar(vd)});
builder.createCallOp(cgf.getLoc(vd->getSourceRange()),
mlir::FlatSymbolRefAttr::get(fnOp.getSymNameAttr()),
mlir::ValueRange{cgm.getAddrOfGlobalVar(vd)});
assert(fnOp && "expected cir.func");
// TODO(cir): This doesn't do anything but check for unhandled conditions.
// What it is meant to do should really be happening in LoweringPrepare.
cgm.getCXXABI().registerGlobalDtor(vd, fnOp, nullptr);
} else {
cgm.errorNYI(vd->getSourceRange(), "array destructor");
// Otherwise, a custom destroyed is needed. Classic codegen creates a helper
// function here and emits the destroy into the helper function, which is
// called from __cxa_atexit.
// In CIR, we just emit the destroy into the dtor region. It will be moved
// into a separate function during the LoweringPrepare pass.
// FIXME(cir): We should create a new operation here to explicitly get the
// address of the global into whose dtor region we are emiiting the destroy.
// The same applies to code above where it is calling getAddrOfGlobalVar.
mlir::Value globalVal = builder.createGetGlobal(addr);
CharUnits alignment = cgf.getContext().getDeclAlign(vd);
Address globalAddr{globalVal, cgf.convertTypeForMem(type), alignment};
cgf.emitDestroy(globalAddr, type, cgf.getDestroyer(dtorKind));
}
assert(fnOp && "expected cir.func");
cgm.getCXXABI().registerGlobalDtor(vd, fnOp, nullptr);

builder.setInsertionPointToEnd(block);
if (block->empty()) {
Expand Down
123 changes: 111 additions & 12 deletions clang/lib/CIR/Dialect/Transforms/LoweringPrepare.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ struct LoweringPreparePass
/// Build the function that initializes the specified global
cir::FuncOp buildCXXGlobalVarDeclInitFunc(cir::GlobalOp op);

/// Handle the dtor region by registering destructor with __cxa_atexit
cir::FuncOp getOrCreateDtorFunc(CIRBaseBuilderTy &builder, cir::GlobalOp op,
mlir::Region &dtorRegion,
cir::CallOp &dtorCall);

/// Build a module init function that calls all the dynamic initializers.
void buildCXXGlobalInitFunc();

Expand Down Expand Up @@ -691,6 +696,101 @@ void LoweringPreparePass::lowerUnaryOp(cir::UnaryOp op) {
op.erase();
}

cir::FuncOp LoweringPreparePass::getOrCreateDtorFunc(CIRBaseBuilderTy &builder,
cir::GlobalOp op,
mlir::Region &dtorRegion,
cir::CallOp &dtorCall) {
assert(!cir::MissingFeatures::astVarDeclInterface());
assert(!cir::MissingFeatures::opGlobalThreadLocal());

cir::VoidType voidTy = builder.getVoidTy();
auto voidPtrTy = cir::PointerType::get(voidTy);

// Look for operations in dtorBlock
mlir::Block &dtorBlock = dtorRegion.front();

// The first operation should be a get_global to retrieve the address
// of the global variable we're destroying.
auto opIt = dtorBlock.getOperations().begin();
cir::GetGlobalOp ggop = mlir::cast<cir::GetGlobalOp>(*opIt);

// The simple case is just a call to a destructor, like this:
//
// %0 = cir.get_global %globalS : !cir.ptr<!rec_S>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Its a little weird that this global is required... could we make a helper for 'createDestroy' (or whatever it is?) to take the 'location' of the get and synthesize it? It just seems odd we have a 'get_global' .

Alternatively, can we assert that the global IS the current variable as a sanity assert?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree that it's weird, but we need an mlir::Value to reference in subsequent operations. We could introduce a special operation that's only valid in GlobalOp ctor and dtor regions, like cir.get_current_global but there would still need to be an operation here. A special purpose op might be good because otherwise, it would be tricky to decide during lowering which get_global ops should refer to the current global and which should not in cases like this:

int someGlobal;
float anotherGlobal = (float)someGlobal;

which produces

cir.global external dso_local @someGlobal = #cir.int<0> : !s32i {alignment = 4 : i64}
cir.global external dso_local @anotherGlobal = ctor : !cir.float {
  %0 = cir.get_global @anotherGlobal : !cir.ptr<!cir.float>
  %1 = cir.get_global @someGlobal : !cir.ptr<!s32i>
  %2 = cir.load align(4) %1 : !cir.ptr<!s32i>, !s32i
  %3 = cir.cast int_to_float %2 : !s32i -> !cir.float
  cir.store align(4) %3, %0 : !cir.float, !cir.ptr<!cir.float>
}

Obviously, I'd want to defer that to a future change.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh yeah, that seems sensible. Just seemed a little odd. I guess I was envisioning some sort of 'get-self' type thing...

I guess I see what you mean, any call (in the non-array case) would need that. The way I could think of would be to have the block take an argument that has that value (like array.dtor's bb0).

But I can see that being fine in a followup/with a FIXME put somewhere.

// cir.call %_ZN1SD1Ev(%0) : (!cir.ptr<!rec_S>) -> ()
// (implicit cir.yield)
//
// That is, if the second operation is a call that takes the get_global result
// as its only operand, and the only other operation is a yield, then we can
// just return the called function.
if (dtorBlock.getOperations().size() == 3) {
auto callOp = mlir::dyn_cast<cir::CallOp>(&*(++opIt));
auto yieldOp = mlir::dyn_cast<cir::YieldOp>(&*(++opIt));
if (yieldOp && callOp && callOp.getNumOperands() == 1 &&
callOp.getArgOperand(0) == ggop) {
dtorCall = callOp;
return getCalledFunction(callOp);
}
}

// Otherwise, we need to create a helper function to replace the dtor region.
// This name is kind of arbitrary, but it matches the name that classic
// codegen uses, based on the expected case that gets us here.
builder.setInsertionPointAfter(op);
SmallString<256> fnName("__cxx_global_array_dtor");
uint32_t cnt = dynamicInitializerNames[fnName]++;
if (cnt)
fnName += "." + std::to_string(cnt);

// Create the helper function.
auto fnType = cir::FuncType::get({voidPtrTy}, voidTy);
cir::FuncOp dtorFunc =
buildRuntimeFunction(builder, fnName, op.getLoc(), fnType,
cir::GlobalLinkageKind::InternalLinkage);
mlir::Block *entryBB = dtorFunc.addEntryBlock();

// Move everything from the dtor region into the helper function.
entryBB->getOperations().splice(entryBB->begin(), dtorBlock.getOperations(),
dtorBlock.begin(), dtorBlock.end());

// Before erasing this, clone it back into the dtor region
cir::GetGlobalOp dtorGGop =
mlir::cast<cir::GetGlobalOp>(entryBB->getOperations().front());
builder.setInsertionPointToStart(&dtorBlock);
builder.clone(*dtorGGop.getOperation());

// Replace all uses of the help function's get_global with the function
// argument.
mlir::Value dtorArg = entryBB->getArgument(0);
dtorGGop.replaceAllUsesWith(dtorArg);
dtorGGop.erase();

// Replace the yield in the final block with a return
mlir::Block &finalBlock = dtorFunc.getBody().back();
auto yieldOp = cast<cir::YieldOp>(finalBlock.getTerminator());
builder.setInsertionPoint(yieldOp);
cir::ReturnOp::create(builder, yieldOp->getLoc());
yieldOp->erase();

// Create a call to the helper function, passing the original get_global op
// as the argument.
cir::GetGlobalOp origGGop =
mlir::cast<cir::GetGlobalOp>(dtorBlock.getOperations().front());
builder.setInsertionPointAfter(origGGop);
mlir::Value ggopResult = origGGop.getResult();
dtorCall = builder.createCallOp(op.getLoc(), dtorFunc, ggopResult);

// Add a yield after the call.
auto finalYield = cir::YieldOp::create(builder, op.getLoc());

// Erase everything after the yield.
dtorBlock.getOperations().erase(std::next(mlir::Block::iterator(finalYield)),
dtorBlock.end());
dtorRegion.getBlocks().erase(std::next(dtorRegion.begin()), dtorRegion.end());

return dtorFunc;
}

cir::FuncOp
LoweringPreparePass::buildCXXGlobalVarDeclInitFunc(cir::GlobalOp op) {
// TODO(cir): Store this in the GlobalOp.
Expand All @@ -699,7 +799,7 @@ LoweringPreparePass::buildCXXGlobalVarDeclInitFunc(cir::GlobalOp op) {
// Get a unique name
uint32_t cnt = dynamicInitializerNames[fnName]++;
if (cnt)
fnName += "." + llvm::Twine(cnt).str();
fnName += "." + std::to_string(cnt);

// Create a variable initialization function.
CIRBaseBuilderTy builder(getContext());
Expand All @@ -722,22 +822,20 @@ LoweringPreparePass::buildCXXGlobalVarDeclInitFunc(cir::GlobalOp op) {
if (!dtorRegion.empty()) {
assert(!cir::MissingFeatures::astVarDeclInterface());
assert(!cir::MissingFeatures::opGlobalThreadLocal());

// Create a variable that binds the atexit to this shared object.
builder.setInsertionPointToStart(&mlirModule.getBodyRegion().front());
cir::GlobalOp handle = buildRuntimeVariable(
builder, "__dso_handle", op.getLoc(), builder.getI8Type(),
cir::GlobalLinkageKind::ExternalLinkage, cir::VisibilityKind::Hidden);

// Look for the destructor call in dtorBlock
mlir::Block &dtorBlock = dtorRegion.front();
// If this is a simple call to a destructor, get the called function.
// Otherwise, create a helper function for the entire dtor region,
// replacing the current dtor region body with a call to the helper
// function.
cir::CallOp dtorCall;
for (auto op : reverse(dtorBlock.getOps<cir::CallOp>())) {
dtorCall = op;
break;
}
assert(dtorCall && "Expected a dtor call");
cir::FuncOp dtorFunc = getCalledFunction(dtorCall);
assert(dtorFunc && "Expected a dtor call");
cir::FuncOp dtorFunc =
getOrCreateDtorFunc(builder, op, dtorRegion, dtorCall);

// Create a runtime helper function:
// extern "C" int __cxa_atexit(void (*f)(void *), void *p, void *d);
Expand All @@ -751,8 +849,8 @@ LoweringPreparePass::buildCXXGlobalVarDeclInitFunc(cir::GlobalOp op) {
cir::FuncOp fnAtExit =
buildRuntimeFunction(builder, nameAtExit, op.getLoc(), fnAtExitType);

// Replace the dtor call with a call to __cxa_atexit(&dtor, &var,
// &__dso_handle)
// Replace the dtor (or helper) call with a call to
// __cxa_atexit(&dtor, &var, &__dso_handle)
builder.setInsertionPointAfter(dtorCall);
mlir::Value args[3];
auto dtorPtrTy = cir::PointerType::get(dtorFunc.getFunctionType());
Expand All @@ -768,6 +866,7 @@ LoweringPreparePass::buildCXXGlobalVarDeclInitFunc(cir::GlobalOp op) {
handle.getSymName());
builder.createCallOp(dtorCall.getLoc(), fnAtExit, args);
dtorCall->erase();
mlir::Block &dtorBlock = dtorRegion.front();
entryBB->getOperations().splice(entryBB->end(), dtorBlock.getOperations(),
dtorBlock.begin(),
std::prev(dtorBlock.end()));
Expand Down
106 changes: 101 additions & 5 deletions clang/test/CIR/CodeGen/global-init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
// LLVM: @needsCtor = global %struct.NeedsCtor zeroinitializer, align 1
// LLVM: @needsDtor = global %struct.NeedsDtor zeroinitializer, align 1
// LLVM: @needsCtorDtor = global %struct.NeedsCtorDtor zeroinitializer, align 1
// LLVM: @arrDtor = global [16 x %struct.ArrayDtor] zeroinitializer, align 16
// LLVM: @llvm.global_ctors = appending global [1 x { i32, ptr, ptr }] [{ i32, ptr, ptr } { i32 65535, ptr @_GLOBAL__sub_I_[[FILENAME:.*]], ptr null }]

// OGCG: @needsCtor = global %struct.NeedsCtor zeroinitializer, align 1
// OGCG: @needsDtor = global %struct.NeedsDtor zeroinitializer, align 1
// OGCG: @__dso_handle = external hidden global i8
// OGCG: @needsCtorDtor = global %struct.NeedsCtorDtor zeroinitializer, align 1
// OGCG: @arrDtor = global [16 x %struct.ArrayDtor] zeroinitializer, align 16
// OGCG: @llvm.global_ctors = appending global [1 x { i32, ptr, ptr }] [{ i32, ptr, ptr } { i32 65535, ptr @_GLOBAL__sub_I_[[FILENAME:.*]], ptr null }]

struct NeedsCtor {
Expand Down Expand Up @@ -145,11 +147,11 @@ float fp;
int i = (int)fp;

// CIR-BEFORE-LPP: cir.global external @i = ctor : !s32i {
// CIR-BEFORE-LPP: %0 = cir.get_global @i : !cir.ptr<!s32i>
// CIR-BEFORE-LPP: %1 = cir.get_global @fp : !cir.ptr<!cir.float>
// CIR-BEFORE-LPP: %2 = cir.load{{.*}} %1 : !cir.ptr<!cir.float>, !cir.float
// CIR-BEFORE-LPP: %3 = cir.cast float_to_int %2 : !cir.float -> !s32i
// CIR-BEFORE-LPP: cir.store{{.*}} %3, %0 : !s32i, !cir.ptr<!s32i>
// CIR-BEFORE-LPP: %[[I:.*]] = cir.get_global @i : !cir.ptr<!s32i>
// CIR-BEFORE-LPP: %[[FP:.*]] = cir.get_global @fp : !cir.ptr<!cir.float>
// CIR-BEFORE-LPP: %[[FP_VAL:.*]] = cir.load{{.*}} %[[FP]] : !cir.ptr<!cir.float>, !cir.float
// CIR-BEFORE-LPP: %[[FP_I32:.*]] = cir.cast float_to_int %[[FP_VAL]] : !cir.float -> !s32i
// CIR-BEFORE-LPP: cir.store{{.*}} %[[FP_I32]], %[[I]] : !s32i, !cir.ptr<!s32i>
// CIR-BEFORE-LPP: }

// CIR: cir.func internal private @__cxx_global_var_init.4()
Expand All @@ -169,6 +171,97 @@ int i = (int)fp;
// OGCG: %[[FP_I32:.*]] = fptosi float %[[TMP_FP]] to i32
// OGCG: store i32 %[[FP_I32]], ptr @i, align 4

struct ArrayDtor {
~ArrayDtor();
};

ArrayDtor arrDtor[16];

// CIR-BEFORE-LPP: cir.global external @arrDtor = #cir.zero : !cir.array<!rec_ArrayDtor x 16>
// CIR-BEFORE-LPP-SAME: dtor {
// CIR-BEFORE-LPP: %[[THIS:.*]] = cir.get_global @arrDtor : !cir.ptr<!cir.array<!rec_ArrayDtor x 16>>
// CIR-BEFORE-LPP: cir.array.dtor %[[THIS]] : !cir.ptr<!cir.array<!rec_ArrayDtor x 16>> {
// CIR-BEFORE-LPP: ^bb0(%[[ELEM:.*]]: !cir.ptr<!rec_ArrayDtor>):
// CIR-BEFORE-LPP: cir.call @_ZN9ArrayDtorD1Ev(%[[ELEM]]) nothrow : (!cir.ptr<!rec_ArrayDtor>) -> ()
// CIR-BEFORE-LPP: cir.yield
// CIR-BEFORE-LPP: }
// CIR-BEFORE-LPP: }

// CIR: cir.global external @arrDtor = #cir.zero : !cir.array<!rec_ArrayDtor x 16> {alignment = 16 : i64}
// CIR: cir.func internal private @__cxx_global_array_dtor(%[[ARR_ARG:.*]]: !cir.ptr<!void> {{.*}}) {
// CIR: %[[CONST15:.*]] = cir.const #cir.int<15> : !u64i
// CIR: %[[BEGIN:.*]] = cir.cast array_to_ptrdecay %[[ARR_ARG]] : !cir.ptr<!void> -> !cir.ptr<!rec_ArrayDtor>
// CIR: %[[END:.*]] = cir.ptr_stride %[[BEGIN]], %[[CONST15]] : (!cir.ptr<!rec_ArrayDtor>, !u64i) -> !cir.ptr<!rec_ArrayDtor>
// CIR: %[[CUR_ADDR:.*]] = cir.alloca !cir.ptr<!rec_ArrayDtor>, !cir.ptr<!cir.ptr<!rec_ArrayDtor>>, ["__array_idx"]
// CIR: cir.store %[[END]], %[[CUR_ADDR]] : !cir.ptr<!rec_ArrayDtor>, !cir.ptr<!cir.ptr<!rec_ArrayDtor>>
// CIR: cir.do {
// CIR: %[[CUR:.*]] = cir.load %[[CUR_ADDR]] : !cir.ptr<!cir.ptr<!rec_ArrayDtor>>, !cir.ptr<!rec_ArrayDtor>
// CIR: cir.call @_ZN9ArrayDtorD1Ev(%[[CUR]]) nothrow : (!cir.ptr<!rec_ArrayDtor>) -> ()
// CIR: %[[NEG_ONE:.*]] = cir.const #cir.int<-1> : !s64i
// CIR: %[[NEXT:.*]] = cir.ptr_stride %[[CUR]], %[[NEG_ONE]] : (!cir.ptr<!rec_ArrayDtor>, !s64i) -> !cir.ptr<!rec_ArrayDtor>
// CIR: cir.store %[[NEXT]], %[[CUR_ADDR]] : !cir.ptr<!rec_ArrayDtor>, !cir.ptr<!cir.ptr<!rec_ArrayDtor>>
// CIR: cir.yield
// CIR: } while {
// CIR: %[[CUR:.*]] = cir.load %[[CUR_ADDR]] : !cir.ptr<!cir.ptr<!rec_ArrayDtor>>, !cir.ptr<!rec_ArrayDtor>
// CIR: %[[CMP:.*]] = cir.cmp(ne, %[[CUR]], %[[BEGIN]]) : !cir.ptr<!rec_ArrayDtor>, !cir.bool
// CIR: cir.condition(%[[CMP]])
// CIR: }
// CIR: cir.return
// CIR: }
//
// CIR: cir.func internal private @__cxx_global_var_init.5() {
// CIR: %[[ARR:.*]] = cir.get_global @arrDtor : !cir.ptr<!cir.array<!rec_ArrayDtor x 16>>
// CIR: %[[DTOR:.*]] = cir.get_global @__cxx_global_array_dtor : !cir.ptr<!cir.func<(!cir.ptr<!void>)>>
// CIR: %[[DTOR_CAST:.*]] = cir.cast bitcast %[[DTOR]] : !cir.ptr<!cir.func<(!cir.ptr<!void>)>> -> !cir.ptr<!cir.func<(!cir.ptr<!void>)>>
// CIR: %[[ARR_CAST:.*]] = cir.cast bitcast %[[ARR]] : !cir.ptr<!cir.array<!rec_ArrayDtor x 16>> -> !cir.ptr<!void>
// CIR: %[[HANDLE:.*]] = cir.get_global @__dso_handle : !cir.ptr<i8>
// CIR: cir.call @__cxa_atexit(%[[DTOR_CAST]], %[[ARR_CAST]], %[[HANDLE]]) : (!cir.ptr<!cir.func<(!cir.ptr<!void>)>>, !cir.ptr<!void>, !cir.ptr<i8>) -> ()

// LLVM: define internal void @__cxx_global_array_dtor(ptr %[[ARR_ARG:.*]]) {
// LLVM: %[[BEGIN:.*]] = getelementptr %struct.ArrayDtor, ptr %[[ARR_ARG]], i32 0
// LLVM: %[[END:.*]] = getelementptr %struct.ArrayDtor, ptr %[[BEGIN]], i64 15
// LLVM: %[[CUR_ADDR:.*]] = alloca ptr
// LLVM: store ptr %[[END]], ptr %[[CUR_ADDR]]
// LLVM: br label %[[LOOP_BODY:.*]]
// LLVM: [[LOOP_COND:.*]]:
// LLVM: %[[CUR:.*]] = load ptr, ptr %[[CUR_ADDR]]
// LLVM: %[[CMP:.*]] = icmp ne ptr %[[CUR]], %[[BEGIN]]
// LLVM: br i1 %[[CMP]], label %[[LOOP_BODY]], label %[[LOOP_END:.*]]
// LLVM: [[LOOP_BODY]]:
// LLVM: %[[CUR:.*]] = load ptr, ptr %[[CUR_ADDR]]
// LLVM: call void @_ZN9ArrayDtorD1Ev(ptr %[[CUR]]) #0
// LLVM: %[[PREV:.*]] = getelementptr %struct.ArrayDtor, ptr %[[CUR]], i64 -1
// LLVM: store ptr %[[PREV]], ptr %[[CUR_ADDR]]
// LLVM: br label %[[LOOP_COND]]
// LLVM: [[LOOP_END]]:
// LLVM: ret void
// LLVM: }
//
// LLVM: define internal void @__cxx_global_var_init.5() {
// LLVM: call void @__cxa_atexit(ptr @__cxx_global_array_dtor, ptr @arrDtor, ptr @__dso_handle)

// Note: OGCG defines these functions in reverse order of CIR->LLVM.
// Note also: OGCG doesn't pass the address of the array to the destructor function.
// Instead, it uses the global directly in the helper function.

// OGCG: define internal void @__cxx_global_var_init.5() {{.*}} section ".text.startup" {
// OGCG: call i32 @__cxa_atexit(ptr @__cxx_global_array_dtor, ptr null, ptr @__dso_handle)

// OGCG: define internal void @__cxx_global_array_dtor(ptr noundef %[[ARG:.*]]) {{.*}} section ".text.startup" {
// OGCG: entry:
// OGCG: %[[UNUSED_ADDR:.*]] = alloca ptr
// OGCG: store ptr %[[ARG]], ptr %[[UNUSED_ADDR]]
// OGCG: br label %[[LOOP_BODY:.*]]
// OGCG: [[LOOP_BODY]]:
// OGCG: %[[PREV:.*]] = phi ptr [ getelementptr inbounds (%struct.ArrayDtor, ptr @arrDtor, i64 16), %entry ], [ %[[CUR:.*]], %[[LOOP_BODY]] ]
// OGCG: %[[CUR]] = getelementptr inbounds %struct.ArrayDtor, ptr %[[PREV]], i64 -1
// OGCG: call void @_ZN9ArrayDtorD1Ev(ptr noundef nonnull align 1 dereferenceable(1) %[[CUR]])
// OGCG: %[[DONE:.*]] = icmp eq ptr %[[CUR]], @arrDtor
// OGCG: br i1 %[[DONE]], label %[[LOOP_END:.*]], label %[[LOOP_BODY]]
// OGCG: [[LOOP_END]]:
// OGCG: ret void
// OGCG: }

// Common init function for all globals with default priority

// CIR: cir.func private @_GLOBAL__sub_I_[[FILENAME:.*]]() {
Expand All @@ -177,17 +270,20 @@ int i = (int)fp;
// CIR: cir.call @__cxx_global_var_init.2() : () -> ()
// CIR: cir.call @__cxx_global_var_init.3() : () -> ()
// CIR: cir.call @__cxx_global_var_init.4() : () -> ()
// CIR: cir.call @__cxx_global_var_init.5() : () -> ()

// LLVM: define void @_GLOBAL__sub_I_[[FILENAME]]()
// LLVM: call void @__cxx_global_var_init()
// LLVM: call void @__cxx_global_var_init.1()
// LLVM: call void @__cxx_global_var_init.2()
// LLVM: call void @__cxx_global_var_init.3()
// LLVM: call void @__cxx_global_var_init.4()
// LLVM: call void @__cxx_global_var_init.5()

// OGCG: define internal void @_GLOBAL__sub_I_[[FILENAME]]() {{.*}} section ".text.startup" {
// OGCG: call void @__cxx_global_var_init()
// OGCG: call void @__cxx_global_var_init.1()
// OGCG: call void @__cxx_global_var_init.2()
// OGCG: call void @__cxx_global_var_init.3()
// OGCG: call void @__cxx_global_var_init.4()
// OGCG: call void @__cxx_global_var_init.5()