mirror of
https://github.com/ldc-developers/ldc.git
synced 2025-04-27 13:40:33 +03:00
1108 lines
36 KiB
C++
1108 lines
36 KiB
C++
//===-- arrays.cpp --------------------------------------------------------===//
|
||
//
|
||
// LDC – the LLVM D compiler
|
||
//
|
||
// This file is distributed under the BSD-style LDC license. See the LICENSE
|
||
// file for details.
|
||
//
|
||
//===----------------------------------------------------------------------===//
|
||
|
||
#include "gen/arrays.h"
|
||
|
||
#include "dmd/aggregate.h"
|
||
#include "dmd/declaration.h"
|
||
#include "dmd/dsymbol.h"
|
||
#include "dmd/errors.h"
|
||
#include "dmd/expression.h"
|
||
#include "dmd/init.h"
|
||
#include "dmd/module.h"
|
||
#include "dmd/mtype.h"
|
||
#include "gen/dvalue.h"
|
||
#include "gen/funcgenstate.h"
|
||
#include "gen/irstate.h"
|
||
#include "gen/llvm.h"
|
||
#include "gen/llvmhelpers.h"
|
||
#include "gen/logger.h"
|
||
#include "gen/runtime.h"
|
||
#include "gen/tollvm.h"
|
||
#include "ir/irfunction.h"
|
||
#include "ir/irmodule.h"
|
||
#include <llvm/Analysis/ConstantFolding.h>
|
||
#include <llvm/IR/Constant.h>
|
||
|
||
using namespace dmd;
|
||
|
||
static void DtoSetArray(DValue *array, DValue *rhs);
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
|
||
LLArrayType *DtoStaticArrayType(Type *t) {
|
||
t = t->toBasetype();
|
||
assert(t->ty == TY::Tsarray);
|
||
TypeSArray *tsa = static_cast<TypeSArray *>(t);
|
||
Type *tnext = tsa->nextOf();
|
||
|
||
return LLArrayType::get(DtoMemType(tnext), tsa->dim->toUInteger());
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
|
||
void DtoSetArrayToNull(DValue *v) {
|
||
IF_LOG Logger::println("DtoSetArrayToNull");
|
||
LOG_SCOPE;
|
||
|
||
DtoStore(LLConstant::getNullValue(DtoType(v->type)), DtoLVal(v));
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
|
||
static void DtoArrayInit(const Loc &loc, LLValue *ptr, LLValue *length,
|
||
DValue *elementValue) {
|
||
IF_LOG Logger::println("DtoArrayInit");
|
||
LOG_SCOPE;
|
||
|
||
// Let's first optimize all zero/i8 initializations down to a memset.
|
||
// This simplifies codegen later on as llvm null's have no address!
|
||
if (!elementValue->isLVal() || !DtoIsInMemoryOnly(elementValue->type)) {
|
||
LLValue *val = DtoRVal(elementValue);
|
||
LLConstant *constantVal = isaConstant(val);
|
||
bool isNullConstant = (constantVal && constantVal->isNullValue());
|
||
if (isNullConstant || val->getType() == LLType::getInt8Ty(gIR->context())) {
|
||
LLValue *size = length;
|
||
size_t elementSize = getTypeAllocSize(val->getType());
|
||
if (elementSize != 1) {
|
||
size = gIR->ir->CreateMul(length, DtoConstSize_t(elementSize),
|
||
".arraysize");
|
||
}
|
||
DtoMemSet(ptr, isNullConstant ? DtoConstUbyte(0) : val, size);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// create blocks
|
||
llvm::BasicBlock *condbb = gIR->insertBB("arrayinit.cond");
|
||
llvm::BasicBlock *bodybb = gIR->insertBBAfter(condbb, "arrayinit.body");
|
||
llvm::BasicBlock *endbb = gIR->insertBBAfter(bodybb, "arrayinit.end");
|
||
|
||
// initialize iterator
|
||
LLValue *itr = DtoAllocaDump(DtoConstSize_t(0), 0, "arrayinit.itr");
|
||
|
||
// move into the for condition block, ie. start the loop
|
||
assert(!gIR->scopereturned());
|
||
llvm::BranchInst::Create(condbb, gIR->scopebb());
|
||
|
||
// replace current scope
|
||
gIR->ir->SetInsertPoint(condbb);
|
||
|
||
LLType *sz = DtoSize_t();
|
||
// create the condition
|
||
LLValue *cond_val =
|
||
gIR->ir->CreateICmpNE(DtoLoad(sz, itr), length, "arrayinit.condition");
|
||
|
||
// conditional branch
|
||
assert(!gIR->scopereturned());
|
||
llvm::BranchInst::Create(bodybb, endbb, cond_val, gIR->scopebb());
|
||
|
||
// rewrite scope
|
||
gIR->ir->SetInsertPoint(bodybb);
|
||
|
||
LLValue *itr_val = DtoLoad(sz,itr);
|
||
// assign array element value
|
||
Type *elemty = elementValue->type->toBasetype();
|
||
DLValue arrayelem(elemty, DtoGEP1(i1ToI8(DtoType(elemty)), ptr, itr_val, "arrayinit.arrayelem"));
|
||
DtoAssign(loc, &arrayelem, elementValue, EXP::blit);
|
||
|
||
// increment iterator
|
||
DtoStore(gIR->ir->CreateAdd(itr_val, DtoConstSize_t(1), "arrayinit.new_itr"),
|
||
itr);
|
||
|
||
// loop
|
||
llvm::BranchInst::Create(condbb, gIR->scopebb());
|
||
|
||
// rewrite the scope
|
||
gIR->ir->SetInsertPoint(endbb);
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
|
||
static Type *DtoArrayElementType(Type *arrayType) {
|
||
assert(arrayType->toBasetype()->nextOf());
|
||
Type *t = arrayType->toBasetype()->nextOf()->toBasetype();
|
||
while (t->ty == TY::Tsarray) {
|
||
t = t->nextOf()->toBasetype();
|
||
}
|
||
return t;
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
|
||
static LLValue *computeSize(LLValue *length, size_t elementSize) {
|
||
return elementSize == 1
|
||
? length
|
||
: gIR->ir->CreateMul(length, DtoConstSize_t(elementSize));
|
||
};
|
||
|
||
static void copySlice(const Loc &loc, LLValue *dstarr, LLValue *dstlen,
|
||
LLValue *srcarr, LLValue *srclen, size_t elementSize,
|
||
bool knownInBounds) {
|
||
const bool checksEnabled =
|
||
global.params.useAssert == CHECKENABLEon || gIR->emitArrayBoundsChecks();
|
||
if (checksEnabled && !knownInBounds) {
|
||
LLFunction *fn = getRuntimeFunction(loc, gIR->module, "_d_array_slice_copy");
|
||
gIR->CreateCallOrInvoke(
|
||
fn, {dstarr, dstlen, srcarr, srclen, DtoConstSize_t(elementSize)}, "",
|
||
/*isNothrow=*/true);
|
||
} else {
|
||
// We might have dstarr == srcarr at compile time, but as long as
|
||
// sz1 == 0 at runtime, this would probably still be legal (the C spec
|
||
// is unclear here).
|
||
LLValue *size = computeSize(dstlen, elementSize);
|
||
DtoMemCpy(dstarr, srcarr, size);
|
||
}
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
|
||
// Determine whether t is an array of structs that need a postblit.
|
||
static bool arrayNeedsPostblit(Type *t) {
|
||
t = DtoArrayElementType(t);
|
||
if (t->ty == TY::Tstruct) {
|
||
return static_cast<TypeStruct *>(t)->sym->postblit != nullptr;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Does array assignment (or initialization) from another array of the same
|
||
// element type or from an appropriate single element.
|
||
void DtoArrayAssign(const Loc &loc, DValue *lhs, DValue *rhs, EXP op,
|
||
bool canSkipPostblit) {
|
||
IF_LOG Logger::println("DtoArrayAssign");
|
||
LOG_SCOPE;
|
||
|
||
Type *t = lhs->type->toBasetype();
|
||
Type *t2 = rhs->type->toBasetype();
|
||
assert(t->nextOf());
|
||
|
||
// reference assignment for dynamic array?
|
||
if (t->ty == TY::Tarray && !lhs->isSlice()) {
|
||
assert(t2->ty == TY::Tarray || t2->ty == TY::Tsarray);
|
||
if (rhs->isNull()) {
|
||
DtoSetArrayToNull(lhs);
|
||
} else {
|
||
DtoSetArray(lhs, rhs);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// EXP::blit is generated by the frontend for (default) initialization of
|
||
// static arrays of structs with a single element.
|
||
const bool isConstructing = (op == EXP::construct || op == EXP::blit);
|
||
|
||
Type *const elemType = t->nextOf()->toBasetype();
|
||
const bool needsDestruction =
|
||
(!isConstructing && elemType->needsDestruction());
|
||
LLValue *lhsPtr = DtoArrayPtr(lhs);
|
||
LLValue *lhsLength = DtoArrayLen(lhs);
|
||
|
||
// Be careful to handle void arrays correctly when modifying this (see tests
|
||
// for DMD issue 7493).
|
||
// TODO: This should use AssignExp::memset.
|
||
LLValue *rhsArrayPtr = (t2->ty == TY::Tarray || t2->ty == TY::Tsarray)
|
||
? DtoArrayPtr(rhs)
|
||
: nullptr;
|
||
if (rhsArrayPtr && DtoMemType(t2->nextOf()) == DtoMemType(t->nextOf())) {
|
||
// T[] = T[] T[] = T[n]
|
||
// T[n] = T[n] T[n] = T[]
|
||
LLValue *rhsPtr = rhsArrayPtr;
|
||
LLValue *rhsLength = DtoArrayLen(rhs);
|
||
|
||
const bool needsPostblit = (op != EXP::blit && arrayNeedsPostblit(t) &&
|
||
(!canSkipPostblit || t2->ty == TY::Tarray));
|
||
|
||
if (!needsDestruction && !needsPostblit) {
|
||
// fast version
|
||
const size_t elementSize = getTypeAllocSize(DtoMemType(elemType));
|
||
if (rhs->isNull()) {
|
||
LLValue *lhsSize = computeSize(lhsLength, elementSize);
|
||
DtoMemSetZero(getI8Type(), lhsPtr, lhsSize);
|
||
} else {
|
||
bool knownInBounds =
|
||
isConstructing || (t->ty == TY::Tsarray && t2->ty == TY::Tsarray);
|
||
if (!knownInBounds) {
|
||
if (auto constLhsLength = llvm::dyn_cast<LLConstantInt>(lhsLength)) {
|
||
if (auto constRhsLength =
|
||
llvm::dyn_cast<LLConstantInt>(rhsLength)) {
|
||
if (constLhsLength->getValue() == constRhsLength->getValue()) {
|
||
knownInBounds = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
copySlice(loc, lhsPtr, lhsLength, rhsPtr, rhsLength, elementSize,
|
||
knownInBounds);
|
||
}
|
||
} else if (isConstructing) {
|
||
error(
|
||
loc,
|
||
"ICE: array construction should have been lowered to `_d_arrayctor`");
|
||
fatal();
|
||
} else { // assigning
|
||
LLValue *tmpSwap = DtoAlloca(elemType, "arrayAssign.tmpSwap");
|
||
LLFunction *fn = getRuntimeFunction(
|
||
loc, gIR->module,
|
||
!canSkipPostblit ? "_d_arrayassign_l" : "_d_arrayassign_r");
|
||
gIR->CreateCallOrInvoke(fn, DtoTypeInfoOf(loc, elemType),
|
||
DtoAggrPair(rhsLength, rhsPtr),
|
||
DtoAggrPair(lhsLength, lhsPtr), tmpSwap);
|
||
}
|
||
} else {
|
||
// scalar rhs:
|
||
// T[] = T T[n][] = T
|
||
// T[n] = T T[n][m] = T
|
||
const bool needsPostblit =
|
||
(op != EXP::blit && !canSkipPostblit && arrayNeedsPostblit(t));
|
||
|
||
if (!needsDestruction && !needsPostblit) {
|
||
// fast version
|
||
const size_t lhsElementSize =
|
||
getTypeAllocSize(DtoMemType(lhs->type->nextOf()));
|
||
LLType *rhsType = DtoMemType(t2);
|
||
const size_t rhsSize = getTypeAllocSize(rhsType);
|
||
LLValue *actualLength = lhsLength;
|
||
if (rhsSize != lhsElementSize) {
|
||
LLValue *lhsSize = computeSize(lhsLength, lhsElementSize);
|
||
actualLength =
|
||
rhsSize == 1
|
||
? lhsSize
|
||
: gIR->ir->CreateExactUDiv(lhsSize, DtoConstSize_t(rhsSize));
|
||
}
|
||
DtoArrayInit(loc, lhsPtr, actualLength, rhs);
|
||
} else if (isConstructing) {
|
||
error(loc, "ICE: array construction should have been lowered to "
|
||
"`_d_arraysetctor`");
|
||
fatal();
|
||
} else {
|
||
LLFunction *fn =
|
||
getRuntimeFunction(loc, gIR->module, "_d_arraysetassign");
|
||
gIR->CreateCallOrInvoke(
|
||
fn, lhsPtr, makeLValue(loc, rhs),
|
||
gIR->ir->CreateTruncOrBitCast(lhsLength,
|
||
LLType::getInt32Ty(gIR->context())),
|
||
DtoTypeInfoOf(loc, stripModifiers(t2)));
|
||
}
|
||
}
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
|
||
static void DtoSetArray(DValue *array, DValue *rhs) {
|
||
IF_LOG Logger::println("SetArray");
|
||
LLValue *arr = DtoLVal(array);
|
||
LLType *s = DtoType(array->type);
|
||
assert(s);
|
||
DtoStore(DtoArrayLen(rhs), DtoGEP(s, arr, 0u, 0));
|
||
DtoStore(DtoArrayPtr(rhs), DtoGEP(s, arr, 0, 1));
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
|
||
LLConstant *DtoConstArrayInitializer(ArrayInitializer *arrinit,
|
||
Type *targetType, const bool isCfile) {
|
||
IF_LOG Logger::println("DtoConstArrayInitializer: %s | %s",
|
||
arrinit->toChars(), targetType->toChars());
|
||
LOG_SCOPE;
|
||
|
||
assert(arrinit->value.length == arrinit->index.length);
|
||
|
||
// get base array type
|
||
Type *arrty = targetType->toBasetype();
|
||
size_t arrlen = arrinit->dim;
|
||
|
||
// for statis arrays, dmd does not include any trailing default
|
||
// initialized elements in the value/index lists
|
||
if (arrty->ty == TY::Tsarray) {
|
||
TypeSArray *tsa = static_cast<TypeSArray *>(arrty);
|
||
arrlen = static_cast<size_t>(tsa->dim->toInteger());
|
||
}
|
||
|
||
// make sure the number of initializers is sane
|
||
if (arrinit->index.length > arrlen || arrinit->dim > arrlen) {
|
||
error(arrinit->loc, "too many initializers, %llu, for array[%llu]",
|
||
static_cast<unsigned long long>(arrinit->index.length),
|
||
static_cast<unsigned long long>(arrlen));
|
||
fatal();
|
||
}
|
||
|
||
// get elem type
|
||
Type *elemty;
|
||
if (arrty->ty == TY::Tvector) {
|
||
elemty = static_cast<TypeVector *>(arrty)->elementType();
|
||
} else {
|
||
elemty = arrty->nextOf();
|
||
}
|
||
LLType *llelemty = DtoMemType(elemty);
|
||
|
||
// true if array elements differ in type, can happen with array of unions
|
||
bool mismatch = false;
|
||
|
||
// allocate room for initializers
|
||
std::vector<LLConstant *> initvals(arrlen, nullptr);
|
||
|
||
// go through each initializer, they're not sorted by index by the frontend
|
||
size_t j = 0;
|
||
for (size_t i = 0; i < arrinit->index.length; i++) {
|
||
// get index
|
||
Expression *idx = arrinit->index[i];
|
||
|
||
// idx can be null, then it's just the next element
|
||
if (idx) {
|
||
j = idx->toInteger();
|
||
}
|
||
assert(j < arrlen);
|
||
|
||
// get value
|
||
Initializer *val = arrinit->value[i];
|
||
assert(val);
|
||
|
||
// error check from dmd
|
||
if (initvals[j] != nullptr) {
|
||
error(arrinit->loc, "duplicate initialization for index %llu",
|
||
static_cast<unsigned long long>(j));
|
||
}
|
||
|
||
LLConstant *c = DtoConstInitializer(val->loc, elemty, val, isCfile);
|
||
assert(c);
|
||
if (c->getType() != llelemty) {
|
||
mismatch = true;
|
||
}
|
||
|
||
initvals[j] = c;
|
||
j++;
|
||
}
|
||
|
||
// die now if there was errors
|
||
if (global.errors) {
|
||
fatal();
|
||
}
|
||
|
||
// Fill out any null entries still left with default values.
|
||
|
||
// Element default initializer. Compute lazily to be able to avoid infinite
|
||
// recursion for types with members that are default initialized to empty
|
||
// arrays of themselves.
|
||
LLConstant *elemDefaultInit = nullptr;
|
||
for (size_t i = 0; i < arrlen; i++) {
|
||
if (initvals[i] != nullptr) {
|
||
continue;
|
||
}
|
||
|
||
if (!elemDefaultInit) {
|
||
elemDefaultInit =
|
||
DtoConstInitializer(arrinit->loc, elemty, nullptr, isCfile);
|
||
if (elemDefaultInit->getType() != llelemty) {
|
||
mismatch = true;
|
||
}
|
||
}
|
||
|
||
initvals[i] = elemDefaultInit;
|
||
}
|
||
|
||
LLConstant *constarr;
|
||
if (mismatch) {
|
||
constarr = LLConstantStruct::getAnon(gIR->context(),
|
||
initvals); // FIXME should this pack?
|
||
} else {
|
||
if (arrty->ty == TY::Tvector) {
|
||
constarr = llvm::ConstantVector::get(initvals);
|
||
} else {
|
||
constarr =
|
||
LLConstantArray::get(LLArrayType::get(llelemty, arrlen), initvals);
|
||
}
|
||
}
|
||
|
||
// std::cout << "constarr: " << *constarr << std::endl;
|
||
|
||
// if the type is a static array, we're done
|
||
if (arrty->ty == TY::Tsarray || arrty->ty == TY::Tvector) {
|
||
return constarr;
|
||
}
|
||
|
||
// we need to make a global with the data, so we have a pointer to the array
|
||
// Important: don't make the gvar constant, since this const initializer might
|
||
// be used as an initializer for a static T[] - where modifying contents is
|
||
// allowed.
|
||
auto gvar = new LLGlobalVariable(gIR->module, constarr->getType(), false,
|
||
LLGlobalValue::InternalLinkage, constarr,
|
||
".constarray");
|
||
|
||
if (arrty->ty == TY::Tpointer) {
|
||
// we need to return pointer to the static array.
|
||
return gvar;
|
||
}
|
||
|
||
return DtoConstSlice(DtoConstSize_t(arrlen), gvar);
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
|
||
Expression *indexArrayLiteral(ArrayLiteralExp *ale, unsigned idx) {
|
||
assert(idx < ale->elements->length);
|
||
auto e = (*ale->elements)[idx];
|
||
if (!e) {
|
||
return ale->basis;
|
||
}
|
||
return e;
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
|
||
bool isConstLiteral(Expression *e, bool immutableType) {
|
||
// We have to check the return value of isConst specifically for '1',
|
||
// as SymOffExp is classified as '2' and the address of a local variable is
|
||
// not an LLVM constant.
|
||
//
|
||
// Examine the ArrayLiteralExps and the StructLiteralExps element by element
|
||
// as isConst always returns 0 on those.
|
||
switch (e->op) {
|
||
case EXP::arrayLiteral: {
|
||
auto ale = static_cast<ArrayLiteralExp *>(e);
|
||
|
||
if (!immutableType) {
|
||
// If dynamic array: assume not constant because the array is expected to
|
||
// be newly allocated. See GH 1924.
|
||
Type *arrayType = ale->type->toBasetype();
|
||
if (arrayType->ty == TY::Tarray)
|
||
return false;
|
||
}
|
||
|
||
for (auto el : *ale->elements) {
|
||
if (!isConstLiteral(el ? el : ale->basis, immutableType))
|
||
return false;
|
||
}
|
||
} break;
|
||
|
||
case EXP::structLiteral: {
|
||
auto sle = static_cast<StructLiteralExp *>(e);
|
||
if (sle->sd->isNested())
|
||
return false;
|
||
for (auto el : *sle->elements) {
|
||
if (el && !isConstLiteral(el, immutableType))
|
||
return false;
|
||
}
|
||
} break;
|
||
|
||
// isConst also returns 0 for string literals that are obviously constant.
|
||
case EXP::string_:
|
||
return true;
|
||
|
||
case EXP::symbolOffset: {
|
||
// Note: dllimported symbols are not link-time constant.
|
||
auto soe = static_cast<SymOffExp *>(e);
|
||
if (VarDeclaration *vd = soe->var->isVarDeclaration()) {
|
||
return vd->isDataseg() && !vd->isImportedSymbol();
|
||
}
|
||
if (FuncDeclaration *fd = soe->var->isFuncDeclaration()) {
|
||
return !fd->isImportedSymbol();
|
||
}
|
||
// Assume the symbol is non-const if we can't prove it is const.
|
||
return false;
|
||
} break;
|
||
|
||
default:
|
||
if (e->isConst() != 1)
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
|
||
llvm::Constant *arrayLiteralToConst(IRState *p, ArrayLiteralExp *ale) {
|
||
// Build the initializer. We have to take care as due to unions in the
|
||
// element types (with different fields being initialized), we can end up
|
||
// with different types for the initializer values. In this case, we
|
||
// generate a packed struct constant instead of an array constant.
|
||
LLType *elementType = nullptr;
|
||
bool differentTypes = false;
|
||
|
||
std::vector<LLConstant *> vals;
|
||
vals.reserve(ale->elements->length);
|
||
for (unsigned i = 0; i < ale->elements->length; ++i) {
|
||
llvm::Constant *val = toConstElem(indexArrayLiteral(ale, i), p);
|
||
// extend i1 to i8
|
||
if (val->getType()->isIntegerTy(1)) {
|
||
LLType *I8PtrTy = LLType::getInt8Ty(p->context());
|
||
#if LDC_LLVM_VER < 1800
|
||
val = llvm::ConstantExpr::getZExt(val, I8PtrTy);
|
||
#else
|
||
val = llvm::ConstantFoldCastOperand(llvm::Instruction::ZExt, val, I8PtrTy, *gDataLayout);
|
||
#endif
|
||
}
|
||
if (!elementType) {
|
||
elementType = val->getType();
|
||
} else {
|
||
differentTypes |= (elementType != val->getType());
|
||
}
|
||
vals.push_back(val);
|
||
}
|
||
|
||
if (differentTypes) {
|
||
return llvm::ConstantStruct::getAnon(vals, true);
|
||
}
|
||
|
||
if (!elementType) {
|
||
assert(ale->elements->length == 0);
|
||
elementType = DtoMemType(ale->type->toBasetype()->nextOf());
|
||
return llvm::ConstantArray::get(LLArrayType::get(elementType, 0), vals);
|
||
}
|
||
|
||
llvm::ArrayType *t = llvm::ArrayType::get(elementType, ale->elements->length);
|
||
return llvm::ConstantArray::get(t, vals);
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
|
||
void initializeArrayLiteral(IRState *p, ArrayLiteralExp *ale,
|
||
LLValue *dstMem, LLType *dstType) {
|
||
size_t elemCount = ale->elements->length;
|
||
|
||
// Don't try to write nothing to a zero-element array, we might represent it
|
||
// as a null pointer.
|
||
if (elemCount == 0)
|
||
return;
|
||
|
||
if (isConstLiteral(ale)) {
|
||
llvm::Constant *constarr = arrayLiteralToConst(p, ale);
|
||
|
||
// Emit a global for longer arrays, as an inline constant is always
|
||
// lowered to a series of movs or similar at the asm level. The
|
||
// optimizer can still decide to promote the memcpy intrinsic, so
|
||
// the cutoff merely affects compilation speed.
|
||
if (elemCount <= 4) {
|
||
DtoStore(constarr, dstMem);
|
||
} else {
|
||
auto gvar = new llvm::GlobalVariable(gIR->module, constarr->getType(),
|
||
true, LLGlobalValue::InternalLinkage,
|
||
constarr, ".arrayliteral");
|
||
gvar->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
|
||
DtoMemCpy(dstMem, gvar,
|
||
DtoConstSize_t(getTypeAllocSize(constarr->getType())));
|
||
}
|
||
} else {
|
||
// Store the elements one by one.
|
||
for (size_t i = 0; i < elemCount; ++i) {
|
||
Expression *rhsExp = indexArrayLiteral(ale, i);
|
||
|
||
LLValue *lhsPtr = DtoGEP(dstType, dstMem, 0, i, "", p->scopebb());
|
||
DLValue lhs(rhsExp->type, lhsPtr);
|
||
|
||
// try to construct it in-place
|
||
if (!toInPlaceConstruction(&lhs, rhsExp))
|
||
DtoAssign(ale->loc, &lhs, toElem(rhsExp), EXP::blit);
|
||
}
|
||
}
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
LLConstant *DtoConstSlice(LLConstant *dim, LLConstant *ptr) {
|
||
LLConstant *values[2] = {dim, ptr};
|
||
LLStructType *lltype =
|
||
LLConstantStruct::getTypeForElements(gIR->context(), values);
|
||
return LLConstantStruct::get(lltype, values);
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
DSliceValue *DtoNewDynArray(const Loc &loc, Type *arrayType, DValue *dim,
|
||
bool defaultInit) {
|
||
IF_LOG Logger::println("DtoNewDynArray : %s", arrayType->toChars());
|
||
LOG_SCOPE;
|
||
|
||
Type *eltType = arrayType->toBasetype()->nextOf();
|
||
|
||
if (size(eltType) == 0)
|
||
return DtoNullValue(arrayType, loc)->isSlice();
|
||
|
||
// get runtime function
|
||
bool zeroInit = eltType->isZeroInit();
|
||
const char *fnname = defaultInit
|
||
? (zeroInit ? "_d_newarrayT" : "_d_newarrayiT")
|
||
: "_d_newarrayU";
|
||
LLFunction *fn = getRuntimeFunction(loc, gIR->module, fnname);
|
||
|
||
// typeinfo arg
|
||
LLValue *arrayTypeInfo = DtoTypeInfoOf(loc, arrayType);
|
||
|
||
// dim arg
|
||
assert(DtoType(dim->type) == DtoSize_t());
|
||
LLValue *arrayLen = DtoRVal(dim);
|
||
|
||
// call allocator
|
||
LLValue *newArray =
|
||
gIR->CreateCallOrInvoke(fn, arrayTypeInfo, arrayLen, ".gc_mem");
|
||
|
||
// return a DSliceValue with the well-known length for better optimizability
|
||
auto ptr = DtoExtractValue(newArray, 1, ".ptr");
|
||
return new DSliceValue(arrayType, arrayLen, ptr);
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
|
||
DSliceValue *DtoAppendDChar(const Loc &loc, DValue *arr, Expression *exp,
|
||
const char *func) {
|
||
LLValue *valueToAppend = DtoRVal(exp);
|
||
|
||
// Prepare arguments
|
||
LLFunction *fn = getRuntimeFunction(loc, gIR->module, func);
|
||
|
||
// Call function (ref string x, dchar c)
|
||
LLValue *newArray = gIR->CreateCallOrInvoke(
|
||
fn, DtoLVal(arr),
|
||
DtoBitCast(valueToAppend, fn->getFunctionType()->getParamType(1)),
|
||
".appendedArray");
|
||
|
||
return new DSliceValue(arr->type, newArray);
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
|
||
DSliceValue *DtoAppendDCharToString(const Loc &loc, DValue *arr,
|
||
Expression *exp) {
|
||
IF_LOG Logger::println("DtoAppendDCharToString");
|
||
LOG_SCOPE;
|
||
return DtoAppendDChar(loc, arr, exp, "_d_arrayappendcd");
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
|
||
DSliceValue *DtoAppendDCharToUnicodeString(const Loc &loc, DValue *arr,
|
||
Expression *exp) {
|
||
IF_LOG Logger::println("DtoAppendDCharToUnicodeString");
|
||
LOG_SCOPE;
|
||
return DtoAppendDChar(loc, arr, exp, "_d_arrayappendwd");
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
namespace {
|
||
// helper for eq and cmp
|
||
LLValue *DtoArrayEqCmp_impl(const Loc &loc, const char *func, DValue *l,
|
||
DValue *r, bool useti) {
|
||
IF_LOG Logger::println("comparing arrays");
|
||
LLFunction *fn = getRuntimeFunction(loc, gIR->module, func);
|
||
assert(fn);
|
||
|
||
// find common dynamic array type
|
||
Type *commonType = arrayOf(l->type->toBasetype()->nextOf());
|
||
|
||
// cast static arrays to dynamic ones, this turns them into DSliceValues
|
||
Logger::println("casting to dynamic arrays");
|
||
l = DtoCastArray(loc, l, commonType);
|
||
r = DtoCastArray(loc, r, commonType);
|
||
|
||
LLSmallVector<LLValue *, 3> args;
|
||
|
||
args.push_back(DtoRVal(l));
|
||
args.push_back(DtoRVal(r));
|
||
|
||
// pass array typeinfo ?
|
||
if (useti) {
|
||
LLValue *tival = DtoTypeInfoOf(loc, l->type);
|
||
args.push_back(tival);
|
||
}
|
||
|
||
return gIR->CreateCallOrInvoke(fn, args);
|
||
}
|
||
|
||
/// When `true` is returned, the type can be compared using `memcmp`.
|
||
/// See `validCompareWithMemcmp`.
|
||
bool validCompareWithMemcmpType(Type *t) {
|
||
switch (t->ty) {
|
||
case TY::Tsarray: {
|
||
auto *elemType = t->baseElemOf();
|
||
return validCompareWithMemcmpType(elemType);
|
||
}
|
||
|
||
case TY::Tstruct:
|
||
// TODO: Implement when structs can be compared with memcmp. Remember that
|
||
// structs can have a user-defined opEquals, alignment padding bytes (in
|
||
// arrays), and padding bytes.
|
||
return false;
|
||
|
||
case TY::Tvoid:
|
||
case TY::Tint8:
|
||
case TY::Tuns8:
|
||
case TY::Tint16:
|
||
case TY::Tuns16:
|
||
case TY::Tint32:
|
||
case TY::Tuns32:
|
||
case TY::Tint64:
|
||
case TY::Tuns64:
|
||
case TY::Tint128:
|
||
case TY::Tuns128:
|
||
case TY::Tbool:
|
||
case TY::Tchar:
|
||
case TY::Twchar:
|
||
case TY::Tdchar:
|
||
case TY::Tpointer:
|
||
return true;
|
||
|
||
// TODO: Determine whether this can be "return true" too:
|
||
// case TY::Tvector:
|
||
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// When `true` is returned, `l` and `r` can be compared using `memcmp`.
|
||
///
|
||
/// This function may return `false` even though `memcmp` would be valid.
|
||
/// It may only return `true` if it is 100% certain.
|
||
///
|
||
/// Comparing with memcmp is often not valid, for example due to
|
||
/// - Floating point types
|
||
/// - Padding bytes
|
||
/// - User-defined opEquals
|
||
bool validCompareWithMemcmp(DValue *l, DValue *r) {
|
||
auto *lElemType = l->type->toBasetype()->nextOf()->toBasetype();
|
||
auto *rElemType = r->type->toBasetype()->nextOf()->toBasetype();
|
||
|
||
// Only memcmp equivalent element types (memcmp should be used for
|
||
// `const int[3] == int[]`, but not for `int[3] == short[3]`).
|
||
if (!equivalent(lElemType, rElemType))
|
||
return false;
|
||
|
||
return validCompareWithMemcmpType(lElemType);
|
||
}
|
||
|
||
// Create a call instruction to memcmp.
|
||
llvm::CallInst *callMemcmp(const Loc &loc, IRState &irs, LLValue *l_ptr,
|
||
LLValue *r_ptr, LLValue *numElements, LLType *elemty) {
|
||
assert(l_ptr && r_ptr && numElements);
|
||
LLFunction *fn = getRuntimeFunction(loc, gIR->module, "memcmp");
|
||
assert(fn);
|
||
auto sizeInBytes = numElements;
|
||
size_t elementSize = getTypeAllocSize(elemty);
|
||
if (elementSize != 1) {
|
||
sizeInBytes = irs.ir->CreateMul(sizeInBytes, DtoConstSize_t(elementSize));
|
||
}
|
||
// Call memcmp.
|
||
LLValue *args[] = {l_ptr, r_ptr, sizeInBytes};
|
||
return irs.ir->CreateCall(fn, args);
|
||
}
|
||
|
||
/// Compare `l` and `r` using memcmp. No checks are done for validity.
|
||
///
|
||
/// This function can deal with comparisons of static and dynamic arrays
|
||
/// with memcmp.
|
||
///
|
||
/// Note: the dynamic array length check is not covered by (LDC's) PGO.
|
||
LLValue *DtoArrayEqCmp_memcmp(const Loc &loc, DValue *l, DValue *r,
|
||
IRState &irs) {
|
||
IF_LOG Logger::println("Comparing arrays using memcmp");
|
||
|
||
auto *l_ptr = DtoArrayPtr(l);
|
||
auto *r_ptr = DtoArrayPtr(r);
|
||
auto *l_length = DtoArrayLen(l);
|
||
|
||
// Early return for the simple case of comparing two static arrays.
|
||
const bool staticArrayComparison =
|
||
(l->type->toBasetype()->ty == TY::Tsarray) &&
|
||
(r->type->toBasetype()->ty == TY::Tsarray);
|
||
if (staticArrayComparison) {
|
||
// TODO: simply codegen when comparing static arrays with different length (int[3] == int[2])
|
||
return callMemcmp(loc, irs, l_ptr, r_ptr, l_length, DtoMemType(l->type->nextOf()));
|
||
}
|
||
|
||
// First compare the array lengths
|
||
auto lengthsCompareEqual =
|
||
irs.ir->CreateICmp(llvm::ICmpInst::ICMP_EQ, l_length, DtoArrayLen(r));
|
||
|
||
llvm::BasicBlock *incomingBB = irs.scopebb();
|
||
llvm::BasicBlock *memcmpBB = irs.insertBB("domemcmp");
|
||
llvm::BasicBlock *memcmpEndBB = irs.insertBBAfter(memcmpBB, "memcmpend");
|
||
irs.ir->CreateCondBr(lengthsCompareEqual, memcmpBB, memcmpEndBB);
|
||
|
||
// If lengths are equal: call memcmp.
|
||
// Note: no extra null checks are needed before passing the pointers to memcmp.
|
||
// The array comparison is UB for non-zero length, and memcmp will correctly
|
||
// return 0 (equality) when the length is zero.
|
||
irs.ir->SetInsertPoint(memcmpBB);
|
||
auto memcmpAnswer = callMemcmp(loc, irs, l_ptr, r_ptr, l_length, DtoMemType(l->type->nextOf()));
|
||
irs.ir->CreateBr(memcmpEndBB);
|
||
|
||
// Merge the result of length check and memcmp call into a phi node.
|
||
irs.ir->SetInsertPoint(memcmpEndBB);
|
||
llvm::PHINode *phi =
|
||
irs.ir->CreatePHI(LLType::getInt32Ty(gIR->context()), 2, "cmp_result");
|
||
phi->addIncoming(DtoConstInt(1), incomingBB);
|
||
phi->addIncoming(memcmpAnswer, memcmpBB);
|
||
|
||
return phi;
|
||
}
|
||
} // end anonymous namespace
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
LLValue *DtoArrayEquals(const Loc &loc, EXP op, DValue *l, DValue *r) {
|
||
LLValue *res = nullptr;
|
||
|
||
if (r->isNull()) {
|
||
// optimize comparisons against null by rewriting to `l.length op 0`
|
||
const auto predicate = eqTokToICmpPred(op);
|
||
res = gIR->ir->CreateICmp(predicate, DtoArrayLen(l), DtoConstSize_t(0));
|
||
} else if (validCompareWithMemcmp(l, r)) {
|
||
// Use memcmp directly if possible. This avoids typeinfo lookup, and enables
|
||
// further optimization because LLVM understands the semantics of C's
|
||
// `memcmp`.
|
||
const auto predicate = eqTokToICmpPred(op);
|
||
const auto memcmp_result = DtoArrayEqCmp_memcmp(loc, l, r, *gIR);
|
||
res = gIR->ir->CreateICmp(predicate, memcmp_result, DtoConstInt(0));
|
||
} else {
|
||
res = DtoArrayEqCmp_impl(loc, "_adEq2", l, r, true);
|
||
const auto predicate = eqTokToICmpPred(op, /* invert = */ true);
|
||
res = gIR->ir->CreateICmp(predicate, res, DtoConstInt(0));
|
||
}
|
||
|
||
return res;
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
LLValue *DtoDynArrayIs(EXP op, DValue *l, DValue *r) {
|
||
assert(l);
|
||
assert(r);
|
||
|
||
LLValue *len1 = DtoArrayLen(l);
|
||
LLValue *ptr1 = DtoArrayPtr(l);
|
||
|
||
LLValue *len2 = DtoArrayLen(r);
|
||
LLValue *ptr2 = DtoArrayPtr(r);
|
||
|
||
return createIPairCmp(op, len1, ptr1, len2, ptr2);
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
LLValue *DtoArrayLen(DValue *v) {
|
||
IF_LOG Logger::println("DtoArrayLen");
|
||
LOG_SCOPE;
|
||
|
||
Type *t = v->type->toBasetype();
|
||
if (t->ty == TY::Tarray) {
|
||
if (v->isNull()) {
|
||
return DtoConstSize_t(0);
|
||
}
|
||
if (v->isLVal()) {
|
||
return DtoLoad(DtoSize_t(),
|
||
DtoGEP(DtoType(v->type), DtoLVal(v), 0u, 0), ".len");
|
||
}
|
||
auto slice = v->isSlice();
|
||
assert(slice);
|
||
return slice->getLength();
|
||
}
|
||
if (t->ty == TY::Tsarray) {
|
||
assert(!v->isSlice());
|
||
assert(!v->isNull());
|
||
TypeSArray *sarray = static_cast<TypeSArray *>(t);
|
||
return DtoConstSize_t(sarray->dim->toUInteger());
|
||
}
|
||
llvm_unreachable("unsupported array for len");
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
LLValue *DtoArrayPtr(DValue *v) {
|
||
IF_LOG Logger::println("DtoArrayPtr");
|
||
LOG_SCOPE;
|
||
|
||
Type *t = v->type->toBasetype();
|
||
LLValue *ptr = nullptr;
|
||
|
||
if (t->ty == TY::Tarray) {
|
||
if (v->isNull()) {
|
||
ptr = getNullPtr();
|
||
} else if (v->isLVal()) {
|
||
ptr = DtoLoad(getOpaquePtrType(), DtoGEP(DtoType(v->type), DtoLVal(v), 0, 1), ".ptr");
|
||
} else {
|
||
auto slice = v->isSlice();
|
||
assert(slice);
|
||
ptr = slice->getPtr();
|
||
}
|
||
} else if (t->ty == TY::Tsarray) {
|
||
assert(!v->isSlice());
|
||
assert(!v->isNull());
|
||
ptr = DtoLVal(v);
|
||
} else {
|
||
llvm_unreachable("Unexpected array type.");
|
||
}
|
||
|
||
return ptr;
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
DValue *DtoCastArray(const Loc &loc, DValue *u, Type *to) {
|
||
IF_LOG Logger::println("DtoCastArray");
|
||
LOG_SCOPE;
|
||
|
||
Type *totype = to->toBasetype();
|
||
Type *fromtype = u->type->toBasetype();
|
||
if (fromtype->ty != TY::Tarray && fromtype->ty != TY::Tsarray) {
|
||
error(loc, "can't cast `%s` to `%s`", u->type->toChars(), to->toChars());
|
||
fatal();
|
||
}
|
||
|
||
IF_LOG Logger::cout() << "from array or sarray" << '\n';
|
||
|
||
if (totype->ty == TY::Tpointer) {
|
||
IF_LOG Logger::cout() << "to pointer" << '\n';
|
||
LLValue *ptr = DtoArrayPtr(u);
|
||
return new DImValue(to, ptr);
|
||
}
|
||
|
||
if (totype->ty == TY::Tarray) {
|
||
IF_LOG Logger::cout() << "to array" << '\n';
|
||
|
||
LLValue *length = nullptr;
|
||
LLValue *ptr = nullptr;
|
||
if (fromtype->ty == TY::Tsarray) {
|
||
length = DtoConstSize_t(
|
||
static_cast<TypeSArray *>(fromtype)->dim->toUInteger());
|
||
ptr = DtoLVal(u);
|
||
} else {
|
||
length = DtoArrayLen(u);
|
||
ptr = DtoArrayPtr(u);
|
||
}
|
||
|
||
const auto fsize = size(fromtype->nextOf());
|
||
const auto tsize = size(totype->nextOf());
|
||
if (fsize != tsize) {
|
||
if (auto constLength = isaConstantInt(length)) {
|
||
// compute new constant length: (constLength * fsize) / tsize
|
||
const auto totalSize = constLength->getZExtValue() * fsize;
|
||
if (totalSize % tsize != 0) {
|
||
error(loc,
|
||
"invalid cast from `%s` to `%s`, the element sizes don't "
|
||
"line up",
|
||
fromtype->toChars(), totype->toChars());
|
||
fatal();
|
||
}
|
||
length = DtoConstSize_t(totalSize / tsize);
|
||
} else if (fsize % tsize == 0) {
|
||
// compute new dynamic length: length * (fsize / tsize)
|
||
length = gIR->ir->CreateMul(length, DtoConstSize_t(fsize / tsize));
|
||
} else {
|
||
llvm_unreachable("should have been lowered to `__ArrayCast`");
|
||
}
|
||
}
|
||
|
||
return new DSliceValue(to, length, ptr);
|
||
}
|
||
|
||
if (totype->ty == TY::Tsarray) {
|
||
IF_LOG Logger::cout() << "to sarray" << '\n';
|
||
|
||
LLValue *ptr = nullptr;
|
||
if (fromtype->ty == TY::Tsarray) {
|
||
ptr = DtoLVal(u);
|
||
} else {
|
||
size_t tosize = static_cast<TypeSArray *>(totype)->dim->toInteger();
|
||
size_t i =
|
||
(tosize * size(totype->nextOf()) - 1) / size(fromtype->nextOf());
|
||
DConstValue index(Type::tsize_t, DtoConstSize_t(i));
|
||
DtoIndexBoundsCheck(loc, u, &index);
|
||
ptr = DtoArrayPtr(u);
|
||
}
|
||
|
||
return new DLValue(to, ptr);
|
||
}
|
||
|
||
if (totype->ty == TY::Tbool) {
|
||
// return (arr.ptr !is null)
|
||
LLValue *ptr = DtoArrayPtr(u);
|
||
LLConstant *nul = getNullPtr();
|
||
return new DImValue(to, gIR->ir->CreateICmpNE(ptr, nul));
|
||
}
|
||
|
||
const auto castedPtr = DtoArrayPtr(u);
|
||
return new DLValue(to, castedPtr);
|
||
}
|
||
|
||
void DtoIndexBoundsCheck(const Loc &loc, DValue *arr, DValue *index) {
|
||
Type *arrty = arr->type->toBasetype();
|
||
assert((arrty->ty == TY::Tsarray || arrty->ty == TY::Tarray ||
|
||
arrty->ty == TY::Tpointer) &&
|
||
"Can only array bounds check for static or dynamic arrays");
|
||
|
||
if (!index) {
|
||
// Caller supplied no index, known in-bounds.
|
||
return;
|
||
}
|
||
|
||
if (arrty->ty == TY::Tpointer) {
|
||
// Length of pointers is unknown, ignore.
|
||
return;
|
||
}
|
||
if (auto ts = arrty->isTypeSArray()) {
|
||
if (ts->isIncomplete()) // importC
|
||
return;
|
||
}
|
||
|
||
LLValue *const llIndex = DtoRVal(index);
|
||
LLValue *const llLength = DtoArrayLen(arr);
|
||
LLValue *const cond = gIR->ir->CreateICmp(llvm::ICmpInst::ICMP_ULT, llIndex,
|
||
llLength, "bounds.cmp");
|
||
|
||
llvm::BasicBlock *okbb = gIR->insertBB("bounds.ok");
|
||
llvm::BasicBlock *failbb = gIR->insertBBAfter(okbb, "bounds.fail");
|
||
gIR->ir->CreateCondBr(cond, okbb, failbb);
|
||
|
||
// set up failbb to call the array bounds error runtime function
|
||
gIR->ir->SetInsertPoint(failbb);
|
||
emitArrayIndexError(gIR, loc, llIndex, llLength);
|
||
|
||
// if ok, proceed in okbb
|
||
gIR->ir->SetInsertPoint(okbb);
|
||
}
|
||
|
||
static void emitRangeErrorImpl(IRState *irs, const Loc &loc,
|
||
const char *cAssertMsg, const char *dFnName,
|
||
llvm::ArrayRef<LLValue *> extraArgs) {
|
||
Module *const module = irs->func()->decl->getModule();
|
||
|
||
switch (global.params.checkAction) {
|
||
case CHECKACTION_C:
|
||
DtoCAssert(module, loc, DtoConstCString(cAssertMsg));
|
||
break;
|
||
case CHECKACTION_halt:
|
||
irs->ir->CreateCall(GET_INTRINSIC_DECL(trap, {}), {});
|
||
irs->ir->CreateUnreachable();
|
||
break;
|
||
case CHECKACTION_context:
|
||
case CHECKACTION_D: {
|
||
auto fn = getRuntimeFunction(loc, irs->module, dFnName);
|
||
LLSmallVector<LLValue *, 5> args;
|
||
args.reserve(2 + extraArgs.size());
|
||
args.push_back(DtoModuleFileName(module, loc));
|
||
args.push_back(DtoConstUint(loc.linnum()));
|
||
args.insert(args.end(), extraArgs.begin(), extraArgs.end());
|
||
irs->CreateCallOrInvoke(fn, args);
|
||
irs->ir->CreateUnreachable();
|
||
break;
|
||
}
|
||
default:
|
||
llvm_unreachable("Unhandled checkAction");
|
||
}
|
||
}
|
||
|
||
void emitRangeError(IRState *irs, const Loc &loc) {
|
||
emitRangeErrorImpl(irs, loc, "array overflow", "_d_arraybounds", {});
|
||
}
|
||
|
||
void emitArraySliceError(IRState *irs, const Loc &loc, LLValue *lower,
|
||
LLValue *upper, LLValue *length) {
|
||
emitRangeErrorImpl(irs, loc, "array slice out of bounds",
|
||
"_d_arraybounds_slice", {lower, upper, length});
|
||
}
|
||
|
||
void emitArrayIndexError(IRState *irs, const Loc &loc, LLValue *index,
|
||
LLValue *length) {
|
||
emitRangeErrorImpl(irs, loc, "array index out of bounds",
|
||
"_d_arraybounds_index", {index, length});
|
||
}
|