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
11 changes: 10 additions & 1 deletion tslang/docs/reference-counting-evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2517,7 +2517,8 @@ remaining leak sat (§9.30).

The layout does not have to be recoverable from the type, because **an interface has exactly the
problem an `any` box has, and the same answer was already in the tree**. An interface value grows
a third word holding the runtime type tag of whatever `this` points at (`INTERFACE_TYPE_INDEX`),
a third word holding the runtime type tag of whatever `this` points at (since moved into the
vtable - see the end of this section),
and its release and retain routines read that tag's descriptor and call the concrete type's own
routines - `releaseViaDescriptor`/`retainViaDescriptor` unchanged from what §9.6 built for `any`
and for tagged unions. Two properties make the tag safe to rely on:
Expand All @@ -2534,6 +2535,14 @@ and for tagged unions. Two properties make the tag safe to rely on:
The interface value going from 16 to 24 bytes is why `-mm=none`'s numbers in this section are
about 8% above §9.30's.

Later the tag moved out of the value and into the vtable, whose first slot it now is
(`INTERFACE_VTABLE_TYPE_SLOT`, ahead of the implementer's `.instanceOf`): an interface vtable is
made for one concrete type - a class's, a subclass's own, an object literal's - so it can carry
that type's tag, and the interface value is `{ vtable, this }` again, 16 bytes. Release and retain
read the tag through the vtable; an interface's null vtable has a null tag. Under `-mm=own`, a
record box that borrows its fields (own spec 32) has a release routine of its own, so it is seen
through a copy of its object literal's vtable whose tag names that routine.

**Two dormant omissions became live over-releases the moment an interface became an owner, and
both were freeing memory that was still in use.** Neither is a consequence of this slice; both
were reachable before it and simply had nothing that walked the block.
Expand Down
40 changes: 22 additions & 18 deletions tslang/include/TypeScript/Defines.h
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,23 @@
#define SIZE_NAME ".size"
#define INSTANCEOF_NAME ".instanceOf"
#define INSTANCEOF_PARAM_NAME "rttiParam"
// Every interface vtable starts with the `.instanceOf` of what implements it, ahead of the
// interface's own members: a class's own `.instanceOf`, or, for an object literal, which is no
// instance of any class, INSTANCEOF_NONE_NAME, which answers false. That lets `i instanceof C`
// and `<C>anyHoldingAnInterface` ask an interface value what it is. An interface member's slot
// (InterfaceSymbolRefOp's index) is its index among the members, so the vtable slot is that plus
// INTERFACE_VTABLE_HEADER_SLOTS.
#define INTERFACE_VTABLE_INSTANCEOF_SLOT 0
#define INTERFACE_VTABLE_HEADER_SLOTS 1
// Every interface vtable starts with a header ahead of the interface's own members:
// - the runtime type tag of what implements it (INTERFACE_VTABLE_TYPE_SLOT): the type descriptor of
// the class or the object literal the vtable is for, through which an interface value is released
// and retained (see OwnershipRoutineLogic and section 9.31). Null when that type owns no heap
// memory, and in an interface's null vtable (mlirGenNullInterfaceVTable).
// - the `.instanceOf` of what implements it: a class's own `.instanceOf`, or, for an object literal,
// which is no instance of any class, INSTANCEOF_NONE_NAME, which answers false. That lets
// `i instanceof C` and `<C>anyHoldingAnInterface` ask an interface value what it is.
// An interface member's slot (InterfaceSymbolRefOp's index) is its index among the members, so the
// vtable slot is that plus INTERFACE_VTABLE_HEADER_SLOTS.
// a class's own vtable starts with its `.instanceOf`
#define CLASS_VTABLE_INSTANCEOF_SLOT 0
#define INTERFACE_VTABLE_TYPE_SLOT 0
#define INTERFACE_VTABLE_INSTANCEOF_SLOT 1
#define INTERFACE_VTABLE_HEADER_SLOTS 2
// the name of the type tag's slot in an interface vtable's tuple
#define INTERFACE_VTABLE_TYPE_NAME ".type"
// on the global of an interface's null vtable (mlirGenNullInterfaceVTable): a null interface is seen
// through it, and nothing implements the interface by it
#define NULL_INTERFACE_VTABLE_ATTR_NAME "__null_interface_vtable"
Expand Down Expand Up @@ -344,16 +353,11 @@
#define DATA_VALUE_INDEX 0
#define THIS_VALUE_INDEX 1

// An interface value is { vtable, this, type } - the first two share the indexes above with
// every other pair-shaped value. The third is the runtime type tag of whatever `this` points
// at, which an interface needs for the same reason an `any` box does: the interface type
// carries only a name, so the layout behind `this` is not recoverable from it. With the tag
// there, an interface can be released and retained like anything else, through the concrete
// type's own routines (see OwnershipRoutineLogic and section 9.31).
//
// Null when `this` owns no heap memory - a null interface, or one made from a value that
// carries nothing.
#define INTERFACE_TYPE_INDEX 2
// An interface value is { vtable, this }, sharing the indexes above with every other pair-shaped
// value. The runtime type tag of whatever `this` points at, which an interface needs for the same
// reason an `any` box does - the interface type carries only a name, so the layout behind `this`
// is not recoverable from it - is the vtable's first slot (INTERFACE_VTABLE_TYPE_SLOT): the vtable
// is made for that type, the class's or the object literal's.

// A bound or hybrid function value is { func, this, type }, and the third word is there for the
// same reason as an interface's: the `this` of a closure is its capture box, heap-allocated and
Expand Down
32 changes: 31 additions & 1 deletion tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h
Original file line number Diff line number Diff line change
Expand Up @@ -945,9 +945,39 @@ class OwnershipRoutineLogic
});
}

// An interface is { vtable, this }, and the tag of what `this` points at is the vtable's first
// slot (INTERFACE_VTABLE_TYPE_SLOT). Zeroed storage has no vtable, and a null interface's vtable
// a null tag: both release nothing.
void releaseViaInterfaceTag(mlir::Type type, mlir::Value slotPtr, bool retaining)
{
releaseViaTagBesideThis(type, slotPtr, INTERFACE_TYPE_INDEX, retaining);
TypeHelper th(rewriter);
TypeConverterHelper tch(typeConverter);

auto loc = op->getLoc();
auto ptrTy = th.getPtrType();
auto llvmType = tch.convertType(type);

auto vtableSlot = rewriter.create<LLVM::GEPOp>(loc, ptrTy, llvmType, slotPtr,
ArrayRef<LLVM::GEPArg>{0, DATA_VALUE_INDEX});
auto vtableValue = rewriter.create<LLVM::LoadOp>(loc, ptrTy, vtableSlot);
auto thisSlot = rewriter.create<LLVM::GEPOp>(loc, ptrTy, llvmType, slotPtr,
ArrayRef<LLVM::GEPArg>{0, THIS_VALUE_INDEX});

emitIfNonNull(vtableValue, [&]() {
auto tagSlot = rewriter.create<LLVM::GEPOp>(loc, ptrTy, ptrTy, vtableValue,
ArrayRef<LLVM::GEPArg>{INTERFACE_VTABLE_TYPE_SLOT});
auto tagValue = rewriter.create<LLVM::LoadOp>(loc, ptrTy, tagSlot);
emitIfNonNull(tagValue, [&]() {
if (retaining)
{
retainViaDescriptor(tagValue, thisSlot);
}
else
{
releaseViaDescriptor(tagValue, thisSlot);
}
});
});
}

// Body of the capture-box routines: they take the storage holding the box pointer, like
Expand Down
6 changes: 3 additions & 3 deletions tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -3978,9 +3978,9 @@ class MLIRTypeHelper

// An interface is a reference to whatever it was made from, and holds it alive the
// same way a class reference does. The concrete layout is not recoverable from the
// type - which is why this used to answer no - but it does not have to be: the value
// carries the runtime type tag of its `this` (INTERFACE_TYPE_INDEX), and release and
// retain go through that, exactly as they do for an `any` box.
// type - which is why this used to answer no - but it does not have to be: the value's
// vtable carries the runtime type tag of its `this` (INTERFACE_VTABLE_TYPE_SLOT), and
// release and retain go through that, exactly as they do for an `any` box.
if (isa<mlir_ts::InterfaceType>(type))
{
return true;
Expand Down
111 changes: 30 additions & 81 deletions tslang/lib/TypeScript/LowerToLLVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -413,10 +413,27 @@ class TypeDescriptorOpLowering : public TsLlvmPattern<mlir_ts::TypeDescriptorOp>
return mlir::failure();
}

// -mm=own: a borrowing record's box (own spec 32) has a routine, and so a descriptor, of its
// own, which leaves the fields it borrows to their owners: the tag of the vtable it is seen
// through as an interface (OwnershipSignaturePass::giveBorrowingRecordsTheirVTables)
llvm::ArrayRef<int32_t> borrowed;
if (auto attr = op->getAttrOfType<mlir::DenseI32ArrayAttr>(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME);
attr && tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn)
{
borrowed = attr.asArrayRef();
}

std::string variant;
for (auto index : borrowed)
{
variant += "r" + std::to_string(index);
}

// generated first: the descriptor's initializer takes the routine's address, so the
// symbol has to exist before the global is built
OwnershipRoutineLogic orl(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
auto releaseRoutineName = orl.getOrCreateReleaseRoutine(descriptorType);
auto releaseRoutineName = borrowed.empty() ? orl.getOrCreateReleaseRoutine(descriptorType)
: orl.getOrCreateRecordReleaseRoutine(descriptorType, borrowed);
// Under own nothing retains: the descriptor's retain slot is read only from inside
// retain routines (retainViaDescriptor), none of which can run, and building one would
// bring `__tslang_inc_ref` into a model that promises it is never referenced. A Shared<T>
Expand All @@ -429,7 +446,7 @@ class TypeDescriptorOpLowering : public TsLlvmPattern<mlir_ts::TypeDescriptorOp>

rewriter.replaceOp(op, ch.getOrCreateTypeDescriptorName(descriptorType, name,
TypeOfOpHelper::typeKindFromName(name),
releaseRoutineName, retainRoutineName));
releaseRoutineName, retainRoutineName, variant));

return success();
}
Expand Down Expand Up @@ -6680,7 +6697,8 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern<mlir_ts::InterfaceSym
auto thisVal = rewriter.create<LLVM::ExtractValueOp>(loc, th.getPtrType(), transformed.getInterfaceVal(),
MLIRHelper::getStructIndex(rewriter, THIS_VALUE_INDEX));

// the vtable starts with the implementer's `.instanceOf` - see INTERFACE_VTABLE_HEADER_SLOTS
// the vtable starts with its header, the implementer's type tag and `.instanceOf` - see
// INTERFACE_VTABLE_HEADER_SLOTS
auto methodOrFieldPtr = rewriter.create<mlir_ts::VTableOffsetRefOp>(
loc, th.getPtrType(), vtable, interfaceSymbolRefOp.getIndex() + INTERFACE_VTABLE_HEADER_SLOTS);

Expand Down Expand Up @@ -6729,10 +6747,10 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern<mlir_ts::InterfaceSym
// (getBoundObjectTypeTag), and a receiver of the bound value - `c = ix.get` - holds the
// object. Bound through an interface, the object's type is not known here, so the bound
// value had no tag, counted nothing, and `c()` ran on a freed object once the interface
// went. The interface carries the tag of what it points at (NewInterfaceOpLowering), and
// the bound value takes that one: null for a missing optional method, whose `this` is.
mlir::Value tag = rewriter.create<LLVM::ExtractValueOp>(
loc, th.getPtrType(), transformed.getInterfaceVal(), MLIRHelper::getStructIndex(rewriter, INTERFACE_TYPE_INDEX));
// went. The interface's vtable carries the tag of what it points at (INTERFACE_VTABLE_TYPE_SLOT),
// and the bound value takes that one: null for a missing optional method, whose `this` is.
// The method's slot is read out of the same vtable, so it is not a null one.
mlir::Value tag = rewriter.create<mlir_ts::VTableOffsetRefOp>(loc, th.getPtrType(), vtable, INTERFACE_VTABLE_TYPE_SLOT);
if (isOptional)
{
auto isNullThis = rewriter.create<LLVM::ICmpOp>(loc, LLVM::ICmpPredicate::eq, thisValEffective,
Expand Down Expand Up @@ -6906,74 +6924,13 @@ struct NewInterfaceOpLowering : public TsLlvmPattern<mlir_ts::NewInterfaceOp>
{
using TsLlvmPattern<mlir_ts::NewInterfaceOp>::TsLlvmPattern;

// The runtime type tag for whatever `this` points at, to go in INTERFACE_TYPE_INDEX.
//
// Null when that type owns no heap memory - which covers `null` and `undefined` cast to
// an interface, where `this` is a null pointer and there would be nothing to release
// anyway. A null tag states that positively, the same way a null descriptor slot does.
//
// The descriptor this returns is keyed by the concrete type, not by the `typeof` name, so
// two object literals of different shapes get their own records rather than sharing the
// one every "object" would otherwise map to.
mlir::Value getTypeTag(mlir_ts::NewInterfaceOp newInterfaceOp, ConversionPatternRewriter &rewriter) const
{
TypeHelper th(rewriter);
auto loc = newInterfaceOp.getLoc();
auto thisType = newInterfaceOp.getThisVal().getType();

MLIRTypeHelper mth(rewriter.getContext(), tsLlvmContext->compileOptions);
if (!mth.ownsHeapMemory(loc, thisType))
{
return rewriter.create<LLVM::ZeroOp>(loc, th.getPtrType());
}

TypeOfOpHelper toh(rewriter);
auto name = toh.typeOfAsString(thisType);
if (name.empty())
{
// an interface over a type with no `typeof` name: nothing to key a descriptor on,
// so it goes untracked and leaks rather than guessing at a record
return rewriter.create<LLVM::ZeroOp>(loc, th.getPtrType());
}

// generated first: the descriptor's initializer takes their addresses
OwnershipRoutineLogic orl(newInterfaceOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
// -mm=own: a borrowing record's box (own spec 32) has a routine, and so a descriptor, of its
// own, which leaves the fields it borrows to their owners
llvm::ArrayRef<int32_t> borrowed;
if (auto attr = newInterfaceOp->getAttrOfType<mlir::DenseI32ArrayAttr>(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME);
attr && tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn)
{
borrowed = attr.asArrayRef();
}

auto releaseRoutineName = borrowed.empty() ? orl.getOrCreateReleaseRoutine(thisType)
: orl.getOrCreateRecordReleaseRoutine(thisType, borrowed);
// under own nothing retains: see TypeDescriptorOpLowering
auto retainRoutineName = tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn
? std::string()
: orl.getOrCreateRetainRoutine(thisType);

std::string variant;
for (auto index : borrowed)
{
variant += "r" + std::to_string(index);
}

LLVMCodeHelper ch(newInterfaceOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
return ch.getOrCreateTypeDescriptorName(thisType, name, TypeOfOpHelper::typeKindFromName(name),
releaseRoutineName, retainRoutineName, variant);
}

// { vtable, this }: the runtime type tag of what `this` points at is the vtable's, made for
// that type (INTERFACE_VTABLE_TYPE_SLOT, mlirGenInterfaceVTableTypeTag)
LogicalResult matchAndRewrite(mlir_ts::NewInterfaceOp newInterfaceOp, Adaptor transformed,
ConversionPatternRewriter &rewriter) const final
{


Location loc = newInterfaceOp.getLoc();

TypeHelper th(rewriter);
CodeLogicHelper clh(newInterfaceOp, rewriter);
TypeConverterHelper tch(getTypeConverter());

auto llvmInterfaceType = tch.convertType(newInterfaceOp.getType());
Expand All @@ -6984,14 +6941,7 @@ struct NewInterfaceOpLowering : public TsLlvmPattern<mlir_ts::NewInterfaceOp>
auto structVal3 = rewriter.create<LLVM::InsertValueOp>(loc, structVal2, transformed.getThisVal(),
MLIRHelper::getStructIndex(rewriter, THIS_VALUE_INDEX));

// Every interface value in the program is built here - a cast from a class, from an
// object literal, and even `null`/`undefined` as an interface all end up at this op
// (CastLogicHelper) - which is what makes the type tag safe to rely on: there is no
// other way to produce an interface value with the slot left undefined.
auto structVal4 = rewriter.create<LLVM::InsertValueOp>(loc, structVal3, getTypeTag(newInterfaceOp, rewriter),
MLIRHelper::getStructIndex(rewriter, INTERFACE_TYPE_INDEX));

rewriter.replaceOp(newInterfaceOp, ValueRange{structVal4});
rewriter.replaceOp(newInterfaceOp, ValueRange{structVal3});

return success();
}
Expand Down Expand Up @@ -8326,12 +8276,11 @@ static void populateTypeScriptConversionPatterns(LLVMTypeConverter &converter, m
TypeHelper th(m.getContext());

SmallVector<mlir::Type> rtInterfaceType;
// vtable
// vtable, whose header holds the runtime type tag of what `this` points at - see
// INTERFACE_VTABLE_TYPE_SLOT
rtInterfaceType.push_back(th.getPtrType());
// this
rtInterfaceType.push_back(th.getPtrType());
// runtime type tag of what `this` points at - see INTERFACE_TYPE_INDEX
rtInterfaceType.push_back(th.getPtrType());

return LLVM::LLVMStructType::getLiteral(type.getContext(), rtInterfaceType, false);
});
Expand Down
2 changes: 2 additions & 0 deletions tslang/lib/TypeScript/MLIRGenCast.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1171,6 +1171,8 @@ namespace mlirgen
}

SmallVector<int32_t> vtableMap(interfaceInfo->getVTableSize() + INTERFACE_VTABLE_HEADER_SLOTS, -1);
// the header is the value's, whatever interface it is seen through
vtableMap[INTERFACE_VTABLE_TYPE_SLOT] = INTERFACE_VTABLE_TYPE_SLOT;
vtableMap[INTERFACE_VTABLE_INSTANCEOF_SLOT] = INTERFACE_VTABLE_INSTANCEOF_SLOT;

// the first member the cast can't map, reported only when the vtable can't serve as it is
Expand Down
Loading
Loading