From 88a7cf0b1317d17eccc3f7a2aebe4f757da6a734 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 15:32:31 +0100 Subject: [PATCH] An interface is { vtable, this }: the type tag is the vtable's first slot An interface value was { vtable, this, type }: the third word was the runtime type tag of what `this` points at, which rc and own release and retain the object through. Every interface vtable is made for one concrete type - a class's (a subclass has its own), an object literal's - so the tag now lives in the vtable instead, and the value is two pointers again (16 bytes, not 24). The interface vtable header is { type tag, `.instanceOf` } (INTERFACE_VTABLE_TYPE_SLOT 0, INTERFACE_VTABLE_INSTANCEOF_SLOT 1, INTERFACE_VTABLE_HEADER_SLOTS 2); members follow it as before. A class's own vtable still starts with its `.instanceOf` (CLASS_VTABLE_INSTANCEOF_SLOT), so `instanceof` names the slot of the vtable it reads. - The class, object-literal and null vtable builders put the tag in (mlirGenInterfaceVTableTypeTag): the type's descriptor, or null when the type owns no heap memory, and in a null vtable. - NewInterface no longer makes a tag. releaseViaInterfaceTag reads it through the vtable (a zeroed slot has no vtable, a null interface a null tag: both release nothing); a method bound through an interface takes the tag from the same vtable. - A cast to an interface the value's type does not extend (#666) keeps the header: the tag is the value's, whatever it is seen through. - The own signature pass reads interface vtables past the two-slot header. - -mm=own: a record box that borrows its fields (spec 32) is released by a routine of its own, which the tag must name. Its vtable is the object literal's, shared by every record of its shape, so the signature pass gives such a box a copy of that vtable (`.r...vtbl`) whose descriptor names the borrowing routine (giveBorrowingRecordsTheirVTables). DefaultLib needs no source change; it is rebuilt by the new compiler. Co-Authored-By: Claude Opus 5.5 --- tslang/docs/reference-counting-evaluation.md | 11 +- tslang/include/TypeScript/Defines.h | 40 ++++--- .../LowerToLLVM/OwnershipRoutineLogic.h | 32 ++++- .../TypeScript/MLIRLogic/MLIRTypeHelper.h | 6 +- tslang/lib/TypeScript/LowerToLLVM.cpp | 111 +++++------------- tslang/lib/TypeScript/MLIRGenCast.cpp | 2 + tslang/lib/TypeScript/MLIRGenClasses.cpp | 15 ++- tslang/lib/TypeScript/MLIRGenImpl.h | 31 +++-- tslang/lib/TypeScript/MLIRGenInterfaces.cpp | 57 ++++++++- .../OwnershipSignatureClosedWorld.cpp | 2 +- .../OwnershipSignatureInterfaces.cpp | 14 ++- .../lib/TypeScript/OwnershipSignaturePass.cpp | 3 + .../lib/TypeScript/OwnershipSignaturePass.h | 1 + .../TypeScript/OwnershipSignatureRecords.cpp | 73 ++++++++++++ .../TypeScript/OwnershipSignatureResolve.cpp | 12 +- tslang/test/tester/CMakeLists.txt | 3 + .../tests/00interface_type_tag_in_vtable.ts | 105 +++++++++++++++++ 17 files changed, 383 insertions(+), 135 deletions(-) create mode 100644 tslang/test/tester/tests/00interface_type_tag_in_vtable.ts diff --git a/tslang/docs/reference-counting-evaluation.md b/tslang/docs/reference-counting-evaluation.md index c9351df21..c5fe197d5 100644 --- a/tslang/docs/reference-counting-evaluation.md +++ b/tslang/docs/reference-counting-evaluation.md @@ -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: @@ -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. diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 9df651dfd..7d4a5f10d 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -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 `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 `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" @@ -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 diff --git a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h index 2a89bae6a..c43da2872 100644 --- a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h @@ -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(loc, ptrTy, llvmType, slotPtr, + ArrayRef{0, DATA_VALUE_INDEX}); + auto vtableValue = rewriter.create(loc, ptrTy, vtableSlot); + auto thisSlot = rewriter.create(loc, ptrTy, llvmType, slotPtr, + ArrayRef{0, THIS_VALUE_INDEX}); + + emitIfNonNull(vtableValue, [&]() { + auto tagSlot = rewriter.create(loc, ptrTy, ptrTy, vtableValue, + ArrayRef{INTERFACE_VTABLE_TYPE_SLOT}); + auto tagValue = rewriter.create(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 diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h index baf90bb08..cb8909b81 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h @@ -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(type)) { return true; diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 775eed78c..fe9c3de42 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -413,10 +413,27 @@ class TypeDescriptorOpLowering : public TsLlvmPattern 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 borrowed; + if (auto attr = op->getAttrOfType(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 @@ -429,7 +446,7 @@ class TypeDescriptorOpLowering : public TsLlvmPattern rewriter.replaceOp(op, ch.getOrCreateTypeDescriptorName(descriptorType, name, TypeOfOpHelper::typeKindFromName(name), - releaseRoutineName, retainRoutineName)); + releaseRoutineName, retainRoutineName, variant)); return success(); } @@ -6680,7 +6697,8 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(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( loc, th.getPtrType(), vtable, interfaceSymbolRefOp.getIndex() + INTERFACE_VTABLE_HEADER_SLOTS); @@ -6729,10 +6747,10 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern( - 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(loc, th.getPtrType(), vtable, INTERFACE_VTABLE_TYPE_SLOT); if (isOptional) { auto isNullThis = rewriter.create(loc, LLVM::ICmpPredicate::eq, thisValEffective, @@ -6906,74 +6924,13 @@ struct NewInterfaceOpLowering : public TsLlvmPattern { using TsLlvmPattern::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(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(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 borrowed; - if (auto attr = newInterfaceOp->getAttrOfType(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()); @@ -6984,14 +6941,7 @@ struct NewInterfaceOpLowering : public TsLlvmPattern auto structVal3 = rewriter.create(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(loc, structVal3, getTypeTag(newInterfaceOp, rewriter), - MLIRHelper::getStructIndex(rewriter, INTERFACE_TYPE_INDEX)); - - rewriter.replaceOp(newInterfaceOp, ValueRange{structVal4}); + rewriter.replaceOp(newInterfaceOp, ValueRange{structVal3}); return success(); } @@ -8326,12 +8276,11 @@ static void populateTypeScriptConversionPatterns(LLVMTypeConverter &converter, m TypeHelper th(m.getContext()); SmallVector 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); }); diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index ff0dfd522..344523c4a 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -1171,6 +1171,8 @@ namespace mlirgen } SmallVector 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 diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index c74a77e30..b3358ea4f 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -2582,8 +2582,9 @@ genContext); return result; } - // slot 0: the class's own `.instanceOf` (INTERFACE_VTABLE_HEADER_SLOTS), ahead of the - // interface's members, so an interface value can be asked what class it holds. A class + // the header (INTERFACE_VTABLE_HEADER_SLOTS), ahead of the interface's members: the class's + // type tag, through which an interface value over an object of it is released and retained, + // and its own `.instanceOf`, so an interface value can be asked what class it holds. A class // with no `.instanceOf` (no vtable of its own) is an instance of nothing it can answer for. if (auto *instanceOf = newClassPtr->findMethod(INSTANCEOF_NAME)) { @@ -2595,6 +2596,8 @@ genContext); virtualTable.insert(virtualTable.begin(), VirtualMethodOrFieldInfo(noInstanceOf)); } + virtualTable.insert(virtualTable.begin(), interfaceVTableTypeSlot()); + // a method implementing this interface can itself be owned by a dynamically imported // class (not just a base of it) - such a slot can't be a link-time constant SymbolRefOp // and must be resolved at runtime instead (see the dynamic-import handling in the method @@ -2772,7 +2775,13 @@ genContext); auto fieldIndex = 0; for (auto methodOrField : virtualTable) { - if (methodOrField.isMissing) + if (isInterfaceVTableTypeSlot(methodOrField)) + { + auto typeTag = mlirGenInterfaceVTableTypeTag(location, newClassPtr->classType); + vtableValue = builder.create( + location, virtTuple, typeTag, vtableValue, MLIRHelper::getStructIndex(builder, fieldIndex)); + } + else if (methodOrField.isMissing) { // a genuinely-absent optional (`?`) interface field/method the class // doesn't implement - mirrors the object-literal handling in diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 4c5bddb28..3bdc2b6f0 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -3724,6 +3724,9 @@ class MLIRGenImpl mlir::LogicalResult mlirGenCatchCopyThunk(mlir::Location location, StringRef name, mlir::Type source, mlir::Type target); MethodInfo mlirGenNoInstanceOfFunction(mlir::Location location); + VirtualMethodOrFieldInfo interfaceVTableTypeSlot(); + bool isInterfaceVTableTypeSlot(const VirtualMethodOrFieldInfo &methodOrField); + mlir::Value mlirGenInterfaceVTableTypeTag(mlir::Location location, mlir::Type implementerType); mlir::LogicalResult mlirGenThrowingMethodStub(mlir::Location location, StringRef name, StringRef stubName, mlir_ts::FunctionType funcType, const GenContext &genContext); ValueOrLogicalResult mlirGenNullInterfaceVTable(mlir::Location location, InterfaceInfo::TypePtr newInterfacePtr, @@ -5940,7 +5943,8 @@ class MLIRGenImpl // an instance's first word is its class's vtable auto vtablePtr = builder.create(location, getOpaqueType(), thisPtrValue, 0 /*VTABLE index*/); - return mlirGenInstanceOfThroughVTable(location, vtablePtr, thisPtrValue, classRefVal, genContext); + return mlirGenInstanceOfThroughVTable(location, vtablePtr, CLASS_VTABLE_INSTANCEOF_SLOT, thisPtrValue, + classRefVal, genContext); } // `i instanceof C` for an interface value: its own vtable answers, since what it holds may be @@ -5960,7 +5964,8 @@ class MLIRGenImpl [&]() { auto vtable = builder.create(location, getOpaqueType(), interfaceValue); auto thisPtr = builder.create(location, getOpaqueType(), interfaceValue); - return mlirGenInstanceOfThroughVTable(location, vtable, thisPtr, classRefVal, genContext); + return mlirGenInstanceOfThroughVTable(location, vtable, INTERFACE_VTABLE_INSTANCEOF_SLOT, thisPtr, + classRefVal, genContext); }, [&](mlir::Type) { return ValueOrLogicalResult( @@ -6051,10 +6056,12 @@ class MLIRGenImpl notInstance); } - // Asks `thisPtrValue` whether it is a `classRefVal` through slot 0 of `vtablePtr`: the class's - // `.instanceOf` for a class vtable, and the implementer's for an interface vtable. - ValueOrLogicalResult mlirGenInstanceOfThroughVTable(mlir::Location location, mlir::Value vtablePtr, mlir::Value thisPtrValue, - mlir::Value classRefVal, const GenContext &genContext) + // Asks `thisPtrValue` whether it is a `classRefVal` through slot `instanceOfSlot` of `vtablePtr`: + // the class's `.instanceOf` for a class vtable (CLASS_VTABLE_INSTANCEOF_SLOT), and the implementer's + // for an interface vtable (INTERFACE_VTABLE_INSTANCEOF_SLOT). + ValueOrLogicalResult mlirGenInstanceOfThroughVTable(mlir::Location location, mlir::Value vtablePtr, int32_t instanceOfSlot, + mlir::Value thisPtrValue, mlir::Value classRefVal, + const GenContext &genContext) { auto classType = dyn_cast(classRefVal.getType()); if (!classType) @@ -6063,9 +6070,7 @@ class MLIRGenImpl return mlir::failure(); } - // `.instanceOf` is slot 0 of a class vtable and of an interface vtable alike - auto instanceOfPtr = builder.create( - location, getOpaqueType(), vtablePtr, INTERFACE_VTABLE_INSTANCEOF_SLOT); + auto instanceOfPtr = builder.create(location, getOpaqueType(), vtablePtr, instanceOfSlot); auto classInfo = getClassInfoByFullName(classType.getName().getValue()); @@ -12552,9 +12557,11 @@ class MLIRGenImpl { fields.push_back( { - vtableRecord.fieldInfo.id, - mlir_ts::RefType::get(vtableRecord.fieldInfo.type), - false, + vtableRecord.fieldInfo.id, + // the type tag is a pointer of its own, not a field's offset (interfaceVTableTypeSlot) + isInterfaceVTableTypeSlot(vtableRecord) ? vtableRecord.fieldInfo.type + : mlir_ts::RefType::get(vtableRecord.fieldInfo.type), + false, mlir_ts::AccessLevel::Public }); } diff --git a/tslang/lib/TypeScript/MLIRGenInterfaces.cpp b/tslang/lib/TypeScript/MLIRGenInterfaces.cpp index d17e86cd5..56772fa61 100644 --- a/tslang/lib/TypeScript/MLIRGenInterfaces.cpp +++ b/tslang/lib/TypeScript/MLIRGenInterfaces.cpp @@ -468,6 +468,39 @@ namespace mlirgen return methodInfo; } + // The header slot of an interface vtable holding the runtime type tag (INTERFACE_VTABLE_TYPE_SLOT), + // as the vtable's tuple names it: an opaque pointer, not a field's offset. + VirtualMethodOrFieldInfo MLIRGenImpl::interfaceVTableTypeSlot() + { + return VirtualMethodOrFieldInfo(mlir_ts::FieldInfo{ + MLIRHelper::TupleFieldName(INTERFACE_VTABLE_TYPE_NAME, builder.getContext()), getOpaqueType(), false, + mlir_ts::AccessLevel::Public}); + } + + bool MLIRGenImpl::isInterfaceVTableTypeSlot(const VirtualMethodOrFieldInfo &methodOrField) + { + auto name = methodOrField.isField ? dyn_cast_or_null(methodOrField.fieldInfo.id) : mlir::StringAttr(); + return name && name.getValue() == INTERFACE_VTABLE_TYPE_NAME; + } + + // What the type tag slot of an interface vtable made for `implementerType` holds: the type's + // descriptor, through which an interface value over it is released and retained. Null when the + // type owns no heap memory - nothing to release - or has no `typeof` name to key a descriptor on, + // and in an interface's null vtable (no `implementerType`). + mlir::Value MLIRGenImpl::mlirGenInterfaceVTableTypeTag(mlir::Location location, mlir::Type implementerType) + { + if (implementerType && mth.ownsHeapMemory(location, implementerType)) + { + TypeOfOpHelper toh(builder); + if (auto tag = toh.typeDescriptorValue(location, implementerType)) + { + return builder.create(location, getOpaqueType(), tag); + } + } + + return builder.create(location, getOpaqueType(), builder.create(location, getNullType())); + } + // `(this, ...) { throw new Error("method is abstract"); }`, typed as the interface's // method (`this` opaque): what a null interface's vtable holds for each method // (mlirGenNullInterfaceVTable). Without the default library there is no `Error`, and the message @@ -585,7 +618,8 @@ namespace mlirgen return mlir::failure(); } - // slot 0: null is an instance of nothing (INTERFACE_VTABLE_HEADER_SLOTS) + // the header (INTERFACE_VTABLE_HEADER_SLOTS): null has no type to release through, and is an + // instance of nothing auto noInstanceOf = mlirGenNoInstanceOfFunction(location); if (!noInstanceOf.funcType) { @@ -593,6 +627,7 @@ namespace mlirgen } virtualTable.insert(virtualTable.begin(), VirtualMethodOrFieldInfo(noInstanceOf)); + virtualTable.insert(virtualTable.begin(), interfaceVTableTypeSlot()); auto registeredType = registerVariable( location, vtableName, true, VariableType::Var, @@ -604,7 +639,11 @@ namespace mlirgen for (auto [fieldIndex, methodOrField] : llvm::enumerate(virtualTable)) { mlir::Value slotValue; - if (methodOrField.isMissing) + if (isInterfaceVTableTypeSlot(methodOrField)) + { + slotValue = mlirGenInterfaceVTableTypeTag(location, mlir::Type()); + } + else if (methodOrField.isMissing) { // the -1 sentinel of an absent member, as in mlirGenClassVirtualTableDefinitionForInterface auto negative1 = builder.create(location, ptrIntType, @@ -709,7 +748,8 @@ namespace mlirgen return result; } - // slot 0: an object literal is an instance of no class (INTERFACE_VTABLE_HEADER_SLOTS) + // the header (INTERFACE_VTABLE_HEADER_SLOTS): the object's type tag, and an object literal is an + // instance of no class auto noInstanceOf = mlirGenNoInstanceOfFunction(location); if (!noInstanceOf.funcType) { @@ -717,6 +757,7 @@ namespace mlirgen } virtualTable.insert(virtualTable.begin(), VirtualMethodOrFieldInfo(noInstanceOf)); + virtualTable.insert(virtualTable.begin(), interfaceVTableTypeSlot()); auto fullClassInterfaceVTableFieldName = interfaceVTableNameForObject(objectType, newInterfacePtr); @@ -820,7 +861,13 @@ namespace mlirgen auto fieldIndex = 0; for (auto methodOrField : virtualTable) { - if (methodOrField.isField) + if (isInterfaceVTableTypeSlot(methodOrField)) + { + auto typeTag = mlirGenInterfaceVTableTypeTag(location, objectType); + vtableValue = builder.create( + location, virtTuple, typeTag, vtableValue, MLIRHelper::getStructIndex(builder, fieldIndex)); + } + else if (methodOrField.isField) { // an object-literal method is resolved via the FIELD path too // (methodsAsFields=true, getInterfaceVirtualTableForObject) since it's @@ -979,7 +1026,7 @@ namespace mlirgen } else if (!methodOrField.methodInfo.funcName.empty()) { - // the `.instanceOf` slot this function puts first. Every other entry is + // the `.instanceOf` header slot this function puts in. Every other entry is // a field: getInterfaceVirtualTableForObject hardcodes methodsAsFields. auto methodValueRef = builder.create( location, methodOrField.methodInfo.funcType, diff --git a/tslang/lib/TypeScript/OwnershipSignatureClosedWorld.cpp b/tslang/lib/TypeScript/OwnershipSignatureClosedWorld.cpp index 0a86467a3..1f94439ee 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureClosedWorld.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureClosedWorld.cpp @@ -96,7 +96,7 @@ namespace own_signature } return llvm::any_of(found->second, [&](mlir_ts::TupleType vtableType) { - return llvm::any_of(llvm::drop_begin(vtableType.getFields(), 1), + return llvm::any_of(llvm::drop_begin(vtableType.getFields(), INTERFACE_VTABLE_HEADER_SLOTS), [&](const mlir_ts::FieldInfo &info) { return holdsFunction(info.type, /*fields=*/true, seen); }); }); } diff --git a/tslang/lib/TypeScript/OwnershipSignatureInterfaces.cpp b/tslang/lib/TypeScript/OwnershipSignatureInterfaces.cpp index 25c0d1f07..9d2c1566f 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureInterfaces.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureInterfaces.cpp @@ -750,7 +750,11 @@ namespace own_signature } }); - auto header = slotFunction(values.lookup(0)); + // a class's own vtable starts with its `..instanceOf`; an interface vtable has the type tag + // ahead of it (INTERFACE_VTABLE_TYPE_SLOT) + auto classHeader = slotFunction(values.lookup(0)); + auto isClassVTable = classHeader && classHeader.getValue() == (owner + "..instanceOf").str(); + auto header = isClassVTable ? classHeader : slotFunction(values.lookup(INTERFACE_VTABLE_INSTANCEOF_SLOT)); auto tupleType = mlir::dyn_cast(globalOp.getType()); if (!header || !tupleType) { @@ -758,7 +762,7 @@ namespace own_signature return; } - if (header.getValue() == (owner + "..instanceOf").str()) + if (isClassVTable) { // an exported class (not in an executable, whose exports no module sees) auto exposed = !executable && (!globalOp.isPrivate() || globalOp->hasAttr("export") || globalOp->hasAttr(DLL_EXPORT)); @@ -799,7 +803,7 @@ namespace own_signature vtable.type = tupleType; for (auto &[position, value] : values) { - if (position > 0) + if (position >= INTERFACE_VTABLE_HEADER_SLOTS) { vtable.slots[position] = slotFunction(value); if (auto field = slotField(value); field.first) @@ -1165,7 +1169,7 @@ namespace own_signature { for (auto [position, field] : llvm::enumerate(found->second.type.getFields())) { - if (position > 0) + if (position >= INTERFACE_VTABLE_HEADER_SLOTS) { memberTypeInterfaces(field.type, scan.memberInterfaces[name]); } @@ -1261,7 +1265,7 @@ namespace own_signature auto &vtable = scan.vtables[vtableName]; for (auto [position, field] : llvm::enumerate(vtable.type.getFields())) { - if (position == 0) + if (position < INTERFACE_VTABLE_HEADER_SLOTS) { continue; } diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index 553ec9c36..e6b3b889f 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -136,6 +136,9 @@ namespace own_signature mlir::DenseI32ArrayAttr::get(&getContext(), assigned.getArrayRef())); } }); + + // last: the boxes that borrow are settled + giveBorrowingRecordsTheirVTables(module); } mlir_ts::FuncOp OwnershipSignaturePass::closureBody(mlir_ts::CreateBoundFunctionOp boundOp) diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index 231a21ac1..6f60cb09f 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -802,6 +802,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper recordBoxes; void classifyRecordBoxes(); + void giveBorrowingRecordsTheirVTables(mlir::ModuleOp module); void classifyRecordBox(mlir_ts::NewOp newOp); diff --git a/tslang/lib/TypeScript/OwnershipSignatureRecords.cpp b/tslang/lib/TypeScript/OwnershipSignatureRecords.cpp index 909ea7bb0..217a1357b 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureRecords.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureRecords.cpp @@ -194,6 +194,79 @@ namespace own_signature recordBoxes[newOp.getOperation()] = std::move(info); } + // A borrowing record's box is released by a routine of its own, which leaves the fields it + // borrows to their owners (spec 32) - through the type tag of the vtable it is seen through as an + // interface (INTERFACE_VTABLE_TYPE_SLOT). That vtable is the object literal's, which every record + // of its shape shares, so a box that borrows is seen through a copy of it whose tag names the + // borrowing routine (TypeDescriptorOpLowering), one copy per set of fields borrowed. + void OwnershipSignaturePass::giveBorrowingRecordsTheirVTables(mlir::ModuleOp module) + { + mlir::OpBuilder builder(&getContext()); + module.walk([&](mlir_ts::NewInterfaceOp newInterfaceOp) { + auto attr = newInterfaceOp->getAttrOfType(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME); + if (!attr) + { + return; + } + + // where the vtable's address is taken: straight (through casts), or for the copy an object + // literal's absent members are patched in, the load it is copied from + mlir::OpOperand *sourceUse = &newInterfaceOp.getInterfaceVTableMutable(); + while (auto castOp = sourceUse->get().getDefiningOp()) + { + sourceUse = &castOp.getInMutable(); + } + + if (auto newOp = sourceUse->get().getDefiningOp()) + { + sourceUse = nullptr; + for (auto *user : newOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + auto loadOp = storeOp && storeOp.getReference() == newOp.getResult() + ? storeOp.getValue().getDefiningOp() + : mlir_ts::LoadOp(); + if (loadOp) + { + sourceUse = &loadOp.getReferenceMutable(); + } + } + } + + auto addressOfOp = sourceUse ? sourceUse->get().getDefiningOp() : mlir_ts::AddressOfOp(); + auto globalOp = addressOfOp ? module.lookupSymbol(addressOfOp.getGlobalName()) : mlir_ts::GlobalOp(); + if (!globalOp || !globalOp.getSymName().ends_with("..vtbl")) + { + newInterfaceOp.emitError("-mm=own: the vtable of a record that borrows its fields can't be given the box's own type tag"); + signalPassFailure(); + return; + } + + std::string variantName = globalOp.getSymName().drop_back(llvm::StringRef("..vtbl").size()).str(); + for (auto index : attr.asArrayRef()) + { + variantName += ".r" + std::to_string(index); + } + + variantName += "..vtbl"; + + auto variantOp = module.lookupSymbol(variantName); + if (!variantOp) + { + builder.setInsertionPointAfter(globalOp); + variantOp = mlir::cast(builder.clone(*globalOp)); + variantOp.setSymName(variantName); + variantOp.getInitializerRegion().walk([&](mlir_ts::TypeDescriptorOp descriptorOp) { + descriptorOp->setAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, attr); + }); + } + + builder.setInsertionPoint(addressOfOp); + auto variantAddressOfOp = builder.create(addressOfOp.getLoc(), variantOp); + sourceUse->set(variantAddressOfOp); + }); + } + OwnershipSignaturePass::RecordFlow OwnershipSignaturePass::recordParamsOf(mlir::Value value, llvm::SmallVectorImpl ¶ms) { llvm::SmallVector work{value}; diff --git a/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp b/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp index 2fa6e57e7..d4d50bc24 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp @@ -345,20 +345,22 @@ namespace own_signature { auto castOp = mlir::dyn_cast_or_null(def); auto slot = castOp ? castOp.getIn().getDefiningOp() : mlir_ts::VTableOffsetRefOp(); - if (!slot || slot.getIndex() != INTERFACE_VTABLE_INSTANCEOF_SLOT) + if (!slot) { return false; } - // a class vtable, read out of an object's first word, or an interface's own vtable, - // whose slot 0 is its implementer's `.instanceOf` (INTERFACE_VTABLE_HEADER_SLOTS) + // a class vtable, read out of an object's first word, whose slot 0 is the class's + // `.instanceOf` (CLASS_VTABLE_INSTANCEOF_SLOT), or an interface's own vtable, whose header + // holds its implementer's (INTERFACE_VTABLE_INSTANCEOF_SLOT) auto *vtable = slot.getVtable().getDefiningOp(); if (auto classVTable = mlir::dyn_cast_or_null(vtable)) { - return classVTable.getIndex() == 0; + return classVTable.getIndex() == 0 && slot.getIndex() == CLASS_VTABLE_INSTANCEOF_SLOT; } - return mlir::isa_and_nonnull(vtable); + return mlir::isa_and_nonnull(vtable) && + slot.getIndex() == INTERFACE_VTABLE_INSTANCEOF_SLOT; } } // namespace own_signature diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index ec453ee48..743e415d1 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -416,6 +416,7 @@ tslang_add_test(NAME test-compile-00-const-bound-method-declared-type COMMAND te tslang_add_test(NAME test-compile-00-nullable-class-to-interface COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00nullable_class_to_interface.ts") tslang_add_test(NAME test-compile-00-class-to-interface-null COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_to_interface_null.ts") tslang_add_test(NAME test-compile-00-null-interface-to-interface COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00null_interface_to_interface.ts") +tslang_add_test(NAME test-compile-00-interface-type-tag-in-vtable COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_type_tag_in_vtable.ts") tslang_add_test(NAME test-compile-00-argument-place-aliased COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00argument_place_aliased.ts") tslang_add_test(NAME test-compile-00-temporary-read-in-finally COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00temporary_read_in_finally.ts") tslang_add_test(NAME test-compile-00-unowned-local-store COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00unowned_local_store.ts") @@ -1011,6 +1012,7 @@ tslang_add_test(NAME test-jit-00-const-bound-method-declared-type COMMAND test-r tslang_add_test(NAME test-jit-00-nullable-class-to-interface COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00nullable_class_to_interface.ts") tslang_add_test(NAME test-jit-00-class-to-interface-null COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_to_interface_null.ts") tslang_add_test(NAME test-jit-00-null-interface-to-interface COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00null_interface_to_interface.ts") +tslang_add_test(NAME test-jit-00-interface-type-tag-in-vtable COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_type_tag_in_vtable.ts") tslang_add_test(NAME test-jit-00-argument-place-aliased COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00argument_place_aliased.ts") tslang_add_test(NAME test-jit-00-temporary-read-in-finally COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00temporary_read_in_finally.ts") tslang_add_test(NAME test-jit-00-unowned-local-store COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00unowned_local_store.ts") @@ -1996,6 +1998,7 @@ set(TSLANG_CORPUS 00nullable_class_to_interface.ts 00class_to_interface_null.ts 00null_interface_to_interface.ts + 00interface_type_tag_in_vtable.ts 00argument_place_aliased.ts 00temporary_read_in_finally.ts 00unowned_local_store.ts diff --git a/tslang/test/tester/tests/00interface_type_tag_in_vtable.ts b/tslang/test/tester/tests/00interface_type_tag_in_vtable.ts new file mode 100644 index 000000000..1603b1254 --- /dev/null +++ b/tslang/test/tester/tests/00interface_type_tag_in_vtable.ts @@ -0,0 +1,105 @@ +// An interface is { vtable, this }: the type tag the reference-counting models retain and release +// the object by is the vtable's first slot, not a third field of the interface. Every kind of +// vtable carries it - a class's (and a subclass's own, whose object holds more), an object +// literal's, one laid out for an interface the value's type does not extend (#666), and the null +// vtable, whose tag is null and releases nothing. Each object is reached only through interfaces +// once its class reference is gone; a count taken or dropped through a wrong tag frees it early, +// and the reads after other blocks are made see what reuses its memory. +// Fields are read before any method call: one through a wrong vtable runs whatever it lands on. +interface Shape { + id: number; + area(): number; +} + +interface HasId { + id: number; +} + +class Box { + constructor(public w: number, public h: number) {} +} + +class Rect implements Shape { + constructor(public id: number, public box: Box) {} + + area(): number { + return this.box.w * this.box.h; + } +} + +class Square extends Rect { + constructor(id: number, side: number, public label: Box) { + super(id, new Box(side, side)); + } +} + +@noinline function mkRect(id: number, w: number, h: number): Shape { + return new Rect(id, new Box(w, h)); +} + +@noinline function mkSquare(id: number, side: number): Shape { + return new Square(id, side, new Box(id, id)); +} + +@noinline function mkLiteral(id: number, w: number): Shape { + const box = new Box(w, 1); + return { id: id, area: () => box.w }; +} + +@noinline function mkNull(): Shape | null { + return null; +} + +// blocks the size of what was freed, written over it if it was +@noinline function churn(): number { + let sum: number = 0; + for (let i = 0; i < 64; i++) { + const b = new Box(-1, -1); + const r = new Rect(-1, b); + sum += r.id; + } + + return sum; +} + +function keep(shapes: Shape[]): HasId[] { + const ids: HasId[] = []; + for (const s of shapes) { + ids.push(s); + } + + return ids; +} + +function main() { + let shapes: Shape[] = [mkRect(1, 2, 3), mkSquare(2, 4), mkLiteral(3, 5)]; + assert(shapes[0].id == 1, "a class object's field"); + assert(shapes[1].id == 2, "a subclass object's field"); + assert(shapes[2].id == 3, "an object literal's field"); + + // each seen through an interface its type does not extend, then the first views dropped + const ids = keep(shapes); + shapes = []; + churn(); + + assert(ids[0].id == 1, "a class object held through another interface"); + assert(ids[1].id == 2, "a subclass object held through another interface"); + assert(ids[2].id == 3, "an object literal held through another interface"); + + // a copy, then the original given null: the copy holds the object + let a: Shape | null = mkSquare(7, 3); + const b: Shape | null = a; + a = mkNull(); + churn(); + assert(b.id == 7, "a copy of an interface holds its object"); + assert(b.area() == 9, "and calls through its vtable"); + assert(a === null, "a null interface"); + + // the null interface dropped in turn: its vtable's tag is null and releases nothing + a = mkRect(8, 1, 1); + churn(); + assert(a.id == 8, "given an object after null"); + assert(b.id == 7, "the copy still holds its object"); + + print("done."); +}