diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 157dc7b8f..9df651dfd 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -173,6 +173,12 @@ // INTERFACE_VTABLE_HEADER_SLOTS. #define INTERFACE_VTABLE_INSTANCEOF_SLOT 0 #define INTERFACE_VTABLE_HEADER_SLOTS 1 +// 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" +// on the C++ runtime's type_info vtable a `throw`'s exception tables name (`??_7type_info@@6B@`, +// `_ZTVN10__cxxabiv1...`): a foreign global, but no program value is ever read out of it or stored into it +#define EH_TYPE_INFO_ATTR_NAME "__eh_type_info" // on a ts.Cast from one interface to another whose members are not where the source has them (an // interface that does not extend the source): for each slot of the target's vtable, the slot of the // source's that holds it, or -1 for an optional member the source lacks. The cast then gives the diff --git a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h index 2058c08fb..b22809eab 100644 --- a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h @@ -59,6 +59,32 @@ class CastLogicHelper external = true; } + // The address of the vtable a null interface is seen through: the one MLIRGen named on the cast + // (mlirGenNullInterfaceVTableCast), whose methods throw; none when it named none, as for a cast + // the lowering makes itself. + mlir::Value nullInterfaceVTableAddress() + { + auto vtableRef = op ? op->getAttrOfType(NULL_INTERFACE_VTABLE_ATTR_NAME) : mlir::FlatSymbolRefAttr(); + if (!vtableRef) + { + return mlir::Value(); + } + + return rewriter.create(loc, th.getPtrType(), vtableRef.getValue()); + } + + // the same, as an operand of an interface made here, or `noVTable` + mlir::Value nullInterfaceVTable(mlir::Value noVTable) + { + auto vtableAddress = nullInterfaceVTableAddress(); + if (!vtableAddress) + { + return noVTable; + } + + return rewriter.create(loc, mlir_ts::OpaqueType::get(rewriter.getContext()), vtableAddress); + } + mlir::Value cast(mlir::Value in, mlir::Type inType, mlir::Type resType) { auto inLLVMType = tch.convertType(inType); @@ -332,7 +358,16 @@ class CastLogicHelper if (auto ifaceType = dyn_cast(resType)) { // create null interface - return rewriter.create(loc, ifaceType, in, in); + return rewriter.create(loc, ifaceType, in, nullInterfaceVTable(in)); + } + + // the vtable a null class object is seen through as an interface (castToInterfaceSpecialCases) + if (isa(resType)) + { + if (auto vtableAddress = nullInterfaceVTableAddress()) + { + return vtableAddress; + } } if (auto resHybridFunc = dyn_cast(resType)) @@ -424,10 +459,11 @@ class CastLogicHelper return rewriter.create(loc, llvmBoolType, rewriter.getBoolAttr(true)); } + // set when its `this` is: a null interface may be seen through a vtable (UnaryBinLogicalOrHelper) if (auto ifaceType = dyn_cast(inType)) { auto ptrValue = - rewriter.create(loc, mlir_ts::OpaqueType::get(rewriter.getContext()), in); + rewriter.create(loc, mlir_ts::OpaqueType::get(rewriter.getContext()), in); auto inLLVMType = tch.convertType(ptrValue.getType()); auto llvmBoolType = tch.convertType(boolType); return castLLVMTypes(ptrValue, inLLVMType, boolType, llvmBoolType); @@ -556,7 +592,7 @@ class CastLogicHelper { // create null interface auto nullVal = rewriter.create(loc, mlir_ts::NullType::get(ifaceType.getContext())); - return rewriter.create(loc, ifaceType, nullVal, nullVal); + return rewriter.create(loc, ifaceType, nullVal, nullInterfaceVTable(nullVal)); } if (auto classType = dyn_cast(resType)) diff --git a/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h b/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h index e60eac808..364aeeafa 100644 --- a/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h @@ -270,24 +270,26 @@ mlir::Value LogicOp(Operation *binOp, SyntaxKind op, mlir::Value left, mlir::Typ } else if (isa(leftType)) { - // TODO, extract interface VTable to compare - auto leftVtableValue = + // an interface is the object it is made over: compared by its `this`. A null interface's + // `this` is null whatever vtable it is seen through (the interface's null vtable, whose + // methods throw - mlirGenNullInterfaceVTable - or none at all, zeroed storage) + auto leftThisValue = left.getDefiningOp() || left.getDefiningOp() || matchPattern(left, m_Zero()) ? left - : builder.create(loc, mlir_ts::OpaqueType::get(leftType.getContext()), left); - auto rightVtableValue = + : builder.create(loc, mlir_ts::OpaqueType::get(leftType.getContext()), left); + auto rightThisValue = right.getDefiningOp() || right.getDefiningOp() || matchPattern(right, m_Zero()) ? right - : builder.create(loc, mlir_ts::OpaqueType::get(rightType.getContext()), right); + : builder.create(loc, mlir_ts::OpaqueType::get(rightType.getContext()), right); // excluded string auto intPtrType = llvmtch.getIntPtrType(0); - mlir::Value leftVtableValueAsLLVMType = builder.create(loc, typeConverter.convertType(leftVtableValue.getType()), leftVtableValue); - mlir::Value rightVtableValueAsLLVMType = builder.create(loc, typeConverter.convertType(rightVtableValue.getType()), rightVtableValue); + mlir::Value leftThisValueAsLLVMType = builder.create(loc, typeConverter.convertType(leftThisValue.getType()), leftThisValue); + mlir::Value rightThisValueAsLLVMType = builder.create(loc, typeConverter.convertType(rightThisValue.getType()), rightThisValue); - mlir::Value leftPtrValue = builder.create(loc, intPtrType, leftVtableValueAsLLVMType); - mlir::Value rightPtrValue = builder.create(loc, intPtrType, rightVtableValueAsLLVMType); + mlir::Value leftPtrValue = builder.create(loc, intPtrType, leftThisValueAsLLVMType); + mlir::Value rightPtrValue = builder.create(loc, intPtrType, rightThisValueAsLLVMType); auto value = builder.create(loc, v1, leftPtrValue, rightPtrValue); return value; diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCLinux.h b/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCLinux.h index 46dc00ac0..0cd798672 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCLinux.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCLinux.h @@ -382,7 +382,8 @@ class MLIRRTTIHelperVCLinux return mlir::failure(); } - rewriter.create(loc, mth.getOpaqueType(), true, name, LLVM::Linkage::External); + auto globalOp = rewriter.create(loc, mth.getOpaqueType(), true, name, LLVM::Linkage::External); + globalOp->setAttr(EH_TYPE_INFO_ATTR_NAME, rewriter.getUnitAttr()); return mlir::success(); } diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h b/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h index caeb893fe..231330f55 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h @@ -458,7 +458,8 @@ class MLIRRTTIHelperVCWin32 return mlir::failure(); } - rewriter.create(loc, mth.getOpaqueType(), true, name, LLVM::Linkage::External); + auto globalOp = rewriter.create(loc, mth.getOpaqueType(), true, name, LLVM::Linkage::External); + globalOp->setAttr(EH_TYPE_INFO_ATTR_NAME, rewriter.getUnitAttr()); return mlir::success(); } diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index 606216ea3..ff0dfd522 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -1011,46 +1011,57 @@ namespace mlirgen { if (auto interfaceType = dyn_cast(type)) { - if (auto classType = dyn_cast(valueType)) + // null (or undefined) is an interface over a null `this`, seen through the interface's null + // vtable, whose methods throw (mlirGenNullInterfaceVTable). It stays the cast the ownership + // passes know holds nothing; the cast names the vtable, and the lowering puts it in. + if (isa(valueType)) { - auto result = mlirGenPropertyAccessExpression(location, value, VTABLE_NAME, genContext); - auto vtableAccess = V(result); - - auto classInfo = getClassInfoByFullName(classType.getName().getValue()); - assert(classInfo); - - auto implementIndex = classInfo->getImplementIndex(interfaceType.getName().getValue()); - if (implementIndex >= 0) + auto interfaceInfo = getInterfaceInfoByFullName(interfaceType.getName().getValue()); + if (!interfaceInfo) { - auto interfaceVirtTableIndex = classInfo->implements[implementIndex].virtualIndex; - - assert(genContext.allowPartialResolve || interfaceVirtTableIndex >= 0); - - auto interfaceVTablePtr = builder.create( - location, mth.getInterfaceVTableType(interfaceType), vtableAccess, interfaceVirtTableIndex); - - auto newInterface = builder.create( - location, mlir::TypeRange{interfaceType}, value, interfaceVTablePtr); - return V(newInterface); + return std::nullopt; } - // create interface vtable from current class + return mlirGenNullInterfaceVTableCast(location, interfaceType, interfaceInfo, genContext, value); + } + + // A class object is a pointer that may be null (`C | null` is stored as `C`), and null + // stays null: an interface over a null `this`. The vtable may be read through the object, + // so it is chosen by a test for null: a null object is seen through the interface's null + // vtable, whose methods throw (mlirGenNullInterfaceVTable). The interface is built over + // the object outside the test, so that the ownership passes see it is the object (a view), + // not a value merged from branches. + if (auto classType = dyn_cast(valueType)) + { auto interfaceInfo = getInterfaceInfoByFullName(interfaceType.getName().getValue()); assert(interfaceInfo); - if (auto createdInterfaceVTableForClass = - mlirGenCreateInterfaceVTableForClass(location, classInfo, interfaceInfo, genContext)) - { - LLVM_DEBUG(llvm::dbgs() << "\n!!" - << "@ created interface:" << V(createdInterfaceVTableForClass) << "\n";); - auto newInterface = builder.create( - location, mlir::TypeRange{interfaceType}, value, createdInterfaceVTableForClass); + auto nullValue = builder.create(location, getNullType()); + auto isNotNull = builder.create( + location, getBooleanType(), builder.getI32IntegerAttr((int)SyntaxKind::ExclamationEqualsEqualsToken), value, nullValue); + + auto vtableType = mth.getInterfaceVTableType(interfaceType); + MLIRCodeLogicHelper mclh(builder, location, compileOptions); + auto interfaceVTable = mclh.conditionalValue(isNotNull, + [&]() -> ValueOrLogicalResult { + auto classVTable = mlirGenClassInterfaceVTable(location, value, classType, interfaceType, genContext); + EXIT_IF_FAILED_OR_NO_VALUE(classVTable) + if (V(classVTable).getType() == vtableType) + { + return classVTable; + } - return V(newInterface); - } + return V(builder.create(location, vtableType, V(classVTable))); + }, + [&](mlir::Type) -> ValueOrLogicalResult { + // null, as the ownership passes know it, naming the null vtable the lowering puts in + return mlirGenNullInterfaceVTableCast(location, vtableType, interfaceInfo, genContext); + }); + EXIT_IF_FAILED_OR_NO_VALUE(interfaceVTable) - emitError(location) << "type: " << classType.getName() << " missing interface: " << interfaceType.getName(); - return mlir::failure(); + auto newInterface = builder.create( + location, mlir::TypeRange{interfaceType}, value, V(interfaceVTable)); + return V(newInterface); } // tuple to interface @@ -1097,6 +1108,41 @@ namespace mlirgen return std::nullopt; } + ValueOrLogicalResult MLIRGenImpl::mlirGenClassInterfaceVTable(mlir::Location location, mlir::Value value, mlir_ts::ClassType classType, mlir_ts::InterfaceType interfaceType, const GenContext &genContext) + { + auto classInfo = getClassInfoByFullName(classType.getName().getValue()); + assert(classInfo); + + auto implementIndex = classInfo->getImplementIndex(interfaceType.getName().getValue()); + if (implementIndex >= 0) + { + auto result = mlirGenPropertyAccessExpression(location, value, VTABLE_NAME, genContext); + auto vtableAccess = V(result); + + auto interfaceVirtTableIndex = classInfo->implements[implementIndex].virtualIndex; + + assert(genContext.allowPartialResolve || interfaceVirtTableIndex >= 0); + + return V(builder.create( + location, mth.getInterfaceVTableType(interfaceType), vtableAccess, interfaceVirtTableIndex)); + } + + // create interface vtable from current class + auto interfaceInfo = getInterfaceInfoByFullName(interfaceType.getName().getValue()); + assert(interfaceInfo); + + if (auto createdInterfaceVTableForClass = + mlirGenCreateInterfaceVTableForClass(location, classInfo, interfaceInfo, genContext)) + { + LLVM_DEBUG(llvm::dbgs() << "\n!!" + << "@ created interface:" << V(createdInterfaceVTableForClass) << "\n";); + return createdInterfaceVTableForClass; + } + + emitError(location) << "type: " << classType.getName() << " missing interface: " << interfaceType.getName(); + return mlir::failure(); + } + // An interface value keeps the vtable it was made with, laid out for its own interface. Another // interface it is cast to reads that vtable by its own slot numbers, which match only when it is the // value's interface or one it extends (whose members come first): `interface HasDist { dist }` read @@ -1644,6 +1690,30 @@ namespace mlirgen return std::nullopt; } + // `C | null` given to `I | null`: both are stored as their one member (null is a null pointer + // in it), so this is the class-to-interface cast of `C` to `I`. A plain cast read the class + // pointer as an interface. + if (auto srcUnionType = dyn_cast(valueType); srcUnionType && isa(baseType)) + { + mlir::Type srcBaseType; + if (mth.isUnionTypeNeedsTag(location, srcUnionType, srcBaseType) || !isa_and_nonnull(srcBaseType)) + { + return std::nullopt; + } + + auto member = builder.create(location, srcBaseType, value); + CAST_A(casted, location, baseType, member, genContext); + return V(builder.create(location, type, casted)); + } + + // null given to `I | null` is the null interface, as null given to `I` is (castToInterfaceSpecialCases) + if (isa(valueType) && isa(baseType)) + { + auto casted = cast(location, baseType, value, genContext, /*disableStrictNullCheck=*/true); + EXIT_IF_FAILED_OR_NO_VALUE(casted) + return V(builder.create(location, type, V(casted))); + } + if (isa(valueType) || isa(valueType) || isa(valueType) || isa(valueType) || isa(valueType)) { diff --git a/tslang/lib/TypeScript/MLIRGenFunctions.cpp b/tslang/lib/TypeScript/MLIRGenFunctions.cpp index 5367bd62a..9b052214f 100644 --- a/tslang/lib/TypeScript/MLIRGenFunctions.cpp +++ b/tslang/lib/TypeScript/MLIRGenFunctions.cpp @@ -1948,9 +1948,12 @@ namespace mlirgen return mlir::failure(); } - // add exit code + // add exit code: a function returning nothing declares no return variable of its own, and one + // made while another function is being generated (a null interface's method stub for a cast + // in it) finds that function's instead auto retVarInfo = symbolTable.lookup(RETURN_VARIABLE_NAME); - if (retVarInfo.first) + auto *retVarDef = retVarInfo.first ? retVarInfo.first.getDefiningOp() : nullptr; + if (retVarDef && funcOp->isProperAncestor(retVarDef)) { builder.create(location, retVarInfo.first); } diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index b784dfaa6..4c5bddb28 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1703,7 +1703,8 @@ class MLIRGenImpl // The value an interface or a bound method is made over: `v` seen through `I` in // `const i: I = v`, or `this` of `const m = v.get`, through any casts: a declared type - // (`const m: () => number = v.get`) casts the bound method to a hybrid function. + // (`const m: () => number = v.get`) casts the bound method to a hybrid function. An + // interface over a `C | null` (`const i: I | null = v`) is made over the class read out of it. static mlir::Value viewedValue(mlir::Value value) { while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) @@ -1714,7 +1715,13 @@ class MLIRGenImpl auto *definingOp = value ? value.getDefiningOp() : nullptr; if (auto newInterfaceOp = dyn_cast_or_null(definingOp)) { - return newInterfaceOp.getThisVal(); + auto thisVal = newInterfaceOp.getThisVal(); + if (auto getValueFromUnionOp = thisVal.getDefiningOp()) + { + return getValueFromUnionOp.getIn(); + } + + return thisVal; } if (isa_and_nonnull(definingOp)) @@ -3717,6 +3724,13 @@ class MLIRGenImpl mlir::LogicalResult mlirGenCatchCopyThunk(mlir::Location location, StringRef name, mlir::Type source, mlir::Type target); MethodInfo mlirGenNoInstanceOfFunction(mlir::Location location); + 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, + const GenContext &genContext); + ValueOrLogicalResult mlirGenNullInterfaceVTableCast(mlir::Location location, mlir::Type type, + InterfaceInfo::TypePtr newInterfacePtr, const GenContext &genContext, + mlir::Value value = mlir::Value()); void setCatchCopyThunkBuilder(MLIRRTTIHelperVC &rtti); @@ -12316,6 +12330,12 @@ class MLIRGenImpl return concat(newClassPtr->fullName, newInterfacePtr->fullName, VTABLE_NAME); } + // the vtable a null interface is seen through (mlirGenNullInterfaceVTable) + StringRef nullInterfaceVTableName(InterfaceInfo::TypePtr newInterfacePtr) + { + return concat(newInterfacePtr->fullName, "null", VTABLE_NAME); + } + StringRef interfaceVTableNameForObject(mlir_ts::ObjectType objectType, InterfaceInfo::TypePtr newInterfacePtr) { std::stringstream ss; @@ -13345,6 +13365,8 @@ class MLIRGenImpl // class or tuple or object to interface std::optional castToInterfaceSpecialCases(mlir::Location location, mlir::Type type, mlir::Value value, mlir::Type valueType, const GenContext &genContext); + // the vtable a class object is seen through as an interface + ValueOrLogicalResult mlirGenClassInterfaceVTable(mlir::Location location, mlir::Value value, mlir_ts::ClassType classType, mlir_ts::InterfaceType interfaceType, const GenContext &genContext); std::optional castInterfaceToInterface(mlir::Location location, mlir::Value value, mlir_ts::InterfaceType srcInterfaceType, mlir_ts::InterfaceType interfaceType, const GenContext &genContext); diff --git a/tslang/lib/TypeScript/MLIRGenInterfaces.cpp b/tslang/lib/TypeScript/MLIRGenInterfaces.cpp index 26790167f..d17e86cd5 100644 --- a/tslang/lib/TypeScript/MLIRGenInterfaces.cpp +++ b/tslang/lib/TypeScript/MLIRGenInterfaces.cpp @@ -468,6 +468,222 @@ namespace mlirgen return methodInfo; } + // `(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 + // itself is thrown. + mlir::LogicalResult MLIRGenImpl::mlirGenThrowingMethodStub(mlir::Location location, StringRef name, StringRef stubName, + mlir_ts::FunctionType funcType, const GenContext &genContext) + { + if (auto existing = theModule.lookupSymbol(stubName)) + { + // one a discovery run left without its body (GenContext::clean) is made again + if (!existing.isDeclaration()) + { + return mlir::success(); + } + + existing->erase(); + } + + mlir::OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToStart(theModule.getBody()); + + // a function of its own, made wherever the first null interface is: the throw's walk outwards + // (mlirGenReleaseOwnedOnUnwind) must not reach the scopes of the function that asked for it + GenContext funcGenContext(genContext); + funcGenContext.clearScopeVars(); + funcGenContext.disableSpreadParams = true; + funcGenContext.parentBlockContext = nullptr; + funcGenContext.finallyGuardScope = nullptr; + funcGenContext.finallyEscapingJumps = nullptr; + funcGenContext.isLoop = false; + funcGenContext.isLoopBodyScope = false; + funcGenContext.isSwitchScope = false; + funcGenContext.loopLabel.clear(); + funcGenContext.loopBodyLabel.clear(); + funcGenContext.labelledScopeLabel.clear(); + + auto result = mlirGenFunctionBody( + location, name, stubName, funcType, + [&](mlir::Location location, const GenContext &genContext) -> mlir::LogicalResult { + NodeFactory nf(NodeFactoryFlags::None); + Expression thrown = nf.createStringLiteral(S("method is abstract")); + if (getClassInfoByFullName("Error")) + { + NodeArray argumentsArray; + argumentsArray.push_back(thrown); + thrown = nf.createNewExpression(nf.createIdentifier(S("Error")), undefined, argumentsArray); + } + + return mlirGen(nf.createThrowStatement(thrown), genContext); + }, + funcGenContext, 1/*to skip This*/); + + // it returns nothing at all: what the ownership passes conclude about the functions that do + // (OwnedReturnConsumptionPass) does not wait on it + if (auto stubOp = theModule.lookupSymbol(stubName)) + { + stubOp->setAttr(NULL_INTERFACE_VTABLE_ATTR_NAME, builder.getUnitAttr()); + } + + return result; + } + + // The vtable a null interface is seen through: `I | null` holding null, a null class object seen + // through `I` (castToInterfaceSpecialCases). Its `this` is null, and `this` is what an interface + // is tested for null by. Each method is a stub that throws, as a call of an abstract method would; + // a field's slot is offset 0, so a read of one reads through the null `this`, as a null class + // object's field does; an optional member is absent (-1), as one a class does not implement. + // One per interface and module. + ValueOrLogicalResult MLIRGenImpl::mlirGenNullInterfaceVTable(mlir::Location location, InterfaceInfo::TypePtr newInterfacePtr, + const GenContext &genContext) + { + auto vtableName = nullInterfaceVTableName(newInterfacePtr); + if (auto existValue = resolveFullNameIdentifier(location, vtableName, true, genContext)) + { + return existValue; + } + + MethodInfo emptyMethod; + mlir_ts::FieldInfo emptyFieldInfo; + // the slots' MethodInfo are handed out by reference (getVirtualTable) + std::deque stubs; + llvm::SmallVector virtualTable; + auto result = newInterfacePtr->getVirtualTable( + virtualTable, + [&](mlir::Attribute id, mlir::Type fieldType, bool isConditional) -> std::pair { + if (isConditional) + { + return {emptyFieldInfo, mlir::success()}; + } + + return {mlir_ts::FieldInfo{id, fieldType, false, mlir_ts::AccessLevel::Public}, mlir::success()}; + }, + [&](std::string name, mlir_ts::FunctionType funcType, bool isConditional, int) -> std::pair { + if (isConditional) + { + return {emptyMethod, mlir::success()}; + } + + auto stubName = concat(newInterfacePtr->fullName, "null", name); + if (mlir::failed(mlirGenThrowingMethodStub(location, name, stubName, funcType, genContext))) + { + return {emptyMethod, mlir::failure()}; + } + + auto &stub = stubs.emplace_back(); + stub.name = name; + stub.funcName = stubName.str(); + stub.funcType = funcType; + stub.virtualIndex = -1; + stub.accessLevel = mlir_ts::AccessLevel::Public; + return {stub, mlir::success()}; + }); + if (mlir::failed(result)) + { + return mlir::failure(); + } + + // slot 0: null is an instance of nothing (INTERFACE_VTABLE_HEADER_SLOTS) + auto noInstanceOf = mlirGenNoInstanceOfFunction(location); + if (!noInstanceOf.funcType) + { + return mlir::failure(); + } + + virtualTable.insert(virtualTable.begin(), VirtualMethodOrFieldInfo(noInstanceOf)); + + auto registeredType = registerVariable( + location, vtableName, true, VariableType::Var, + [&](mlir::Location location, const GenContext &genContext) { + auto virtTuple = getVirtualTableType(virtualTable); + auto ptrIntType = builder.getIntegerType(compileOptions.sizeBits()); + + mlir::Value vtableValue = builder.create(location, virtTuple); + for (auto [fieldIndex, methodOrField] : llvm::enumerate(virtualTable)) + { + mlir::Value slotValue; + if (methodOrField.isMissing) + { + // the -1 sentinel of an absent member, as in mlirGenClassVirtualTableDefinitionForInterface + auto negative1 = builder.create(location, ptrIntType, + builder.getIntegerAttr(ptrIntType, -1)); + auto slotType = methodOrField.isField ? methodOrField.fieldInfo.type : methodOrField.methodInfo.funcType; + auto castedPtr = cast(location, mlir_ts::RefType::get(slotType), negative1, genContext); + if (castedPtr.failed_or_no_value()) + { + return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; + } + + slotValue = V(castedPtr); + } + else if (methodOrField.isField) + { + // offset 0: the field is read at the null `this` + auto nullValue = builder.create(location, getNullType()); + slotValue = builder.create(location, mlir_ts::RefType::get(methodOrField.fieldInfo.type), nullValue); + } + else + { + slotValue = builder.create( + location, methodOrField.methodInfo.funcType, + mlir::FlatSymbolRefAttr::get(builder.getContext(), methodOrField.methodInfo.funcName)); + } + + vtableValue = builder.create( + location, virtTuple, slotValue, vtableValue, MLIRHelper::getStructIndex(builder, fieldIndex)); + } + + return TypeValueInitType{virtTuple, vtableValue, TypeProvided::Yes}; + }, + genContext); + if (!registeredType) + { + return mlir::failure(); + } + + if (auto globalOp = theModule.lookupSymbol(vtableName)) + { + globalOp->setAttr(NULL_INTERFACE_VTABLE_ATTR_NAME, builder.getUnitAttr()); + } + + return resolveFullNameIdentifier(location, vtableName, true, genContext); + } + + // null (`value`, or a new one) cast to `type` - the interface, or its vtable for a null class + // object (castToInterfaceSpecialCases) - naming the interface's null vtable, which the lowering + // puts in (CastLogicHelper::nullInterfaceVTable). The ownership passes see a cast of null, which + // holds nothing, as they always have. + ValueOrLogicalResult MLIRGenImpl::mlirGenNullInterfaceVTableCast(mlir::Location location, mlir::Type type, + InterfaceInfo::TypePtr newInterfacePtr, + const GenContext &genContext, mlir::Value value) + { + if (!value) + { + value = builder.create(location, getNullType()); + } + + // a discovery run's IR is thrown away, and with it whatever it made: the run that keeps its IR + // makes the vtable + if (genContext.dummyRun || genContext.allowPartialResolve) + { + return V(builder.create(location, type, value)); + } + + auto nullVTable = mlirGenNullInterfaceVTable(location, newInterfacePtr, genContext); + EXIT_IF_FAILED_OR_NO_VALUE(nullVTable) + if (auto addressOfOp = V(nullVTable).getDefiningOp(); addressOfOp && addressOfOp->use_empty()) + { + addressOfOp->erase(); + } + + auto castOp = builder.create(location, type, value); + castOp->setAttr(NULL_INTERFACE_VTABLE_ATTR_NAME, + mlir::FlatSymbolRefAttr::get(builder.getContext(), nullInterfaceVTableName(newInterfacePtr))); + return V(castOp); + } + mlir::LogicalResult MLIRGenImpl::mlirGenObjectVirtualTableDefinitionForInterface(mlir::Location location, mlir_ts::ObjectType objectType, InterfaceInfo::TypePtr newInterfacePtr, diff --git a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp index 97808fd9c..5aa2459d0 100644 --- a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp +++ b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp @@ -89,6 +89,13 @@ class OwnedReturnConsumptionPass // See callThroughValueReturnsOwned. auto anyUnclassifiedOwningReturn = false; module.walk([&](mlir_ts::FuncOp funcOp) { + // a method of an interface's null vtable throws, and returns nothing to anyone + // (mlirGenNullInterfaceVTable) + if (funcOp->hasAttr(NULL_INTERFACE_VTABLE_ATTR_NAME)) + { + return; + } + auto name = funcOp.getName(); auto classified = functionReturnsOwned(mth, funcOp, compileOptions.isRefCounted()); if (classified) @@ -122,9 +129,11 @@ class OwnedReturnConsumptionPass // virtualCallReturnsOwned. Only slots initialised from a symbol are recorded; a slot that // holds anything else - a field's offset, say - is simply absent, and absent means // unclassifiable there rather than ignorable. + // An interface's null vtable is not one: its methods throw, and never return anything + // (mlirGenNullInterfaceVTable). llvm::StringMap> vtableSlots; module.walk([&](mlir_ts::GlobalOp globalOp) { - if (!globalOp.getSymName().ends_with(VTABLE_NAME)) + if (!globalOp.getSymName().ends_with(VTABLE_NAME) || globalOp->hasAttr(NULL_INTERFACE_VTABLE_ATTR_NAME)) { return; } @@ -1011,7 +1020,8 @@ class OwnedReturnConsumptionPass // straight is never released as a discarded temporary (ยง9.30): `let g = mk()` keeps the call's // +1. A value given to it through a view of the global's type is: `ts.Cast` to `C | null` or // `Shared | null`, `ts.OptionalValue` to `C | undefined`, `ts.CreateUnionInstance`, - // `ts.NewInterface` over a class object for an interface-typed global (`let g: I = mk()`) - the view + // `ts.NewInterface` over a class object for an interface-typed global (`let g: I = mk()`), and + // over the class read out of a `C | null` (`let g: I | null = maybe()`) - the view // is the one use the producer has, and it is releasable, so the producer's reference was given // back at the end of the region and the global was born at count 0, pointing at freed memory // (#459; own spec 23.1 for handles). @@ -1060,6 +1070,10 @@ class OwnedReturnConsumptionPass { value = newInterfaceOp.getThisVal(); } + else if (auto getValueFromUnionOp = mlir::dyn_cast(definingOp)) + { + value = getValueFromUnionOp.getIn(); + } else { break; diff --git a/tslang/lib/TypeScript/OwnershipSignatureClosedWorld.cpp b/tslang/lib/TypeScript/OwnershipSignatureClosedWorld.cpp index 749d60f0d..0a86467a3 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureClosedWorld.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureClosedWorld.cpp @@ -186,6 +186,12 @@ namespace own_signature for (auto &entry : globals) { auto globalOp = entry.second; + // the C++ runtime's type_info vtable a `throw`'s tables name: nothing is read out of it + if (globalOp->hasAttr(EH_TYPE_INFO_ATTR_NAME)) + { + continue; + } + auto foreign = globalOp->hasAttr("import") || globalOp->hasAttr(DLL_IMPORT) || globalOp.getInitializerRegion().empty() || globalOp.getInitializerRegion().front().empty(); if (foreign && !scalarType(globalOp.getType())) @@ -395,6 +401,14 @@ namespace own_signature continue; } + // a method of an interface's null vtable, which throws (mlirGenNullInterfaceVTable): called + // through a null interface only, never with an object + if (auto globalOp = user->getParentOfType(); + mlir::isa(user) && globalOp && globalOp->hasAttr(NULL_INTERFACE_VTABLE_ATTR_NAME)) + { + continue; + } + // passed only to a parameter whose family is closed: its calls are resolved (spec 33) if (callbackPassing.contains(user)) { diff --git a/tslang/lib/TypeScript/OwnershipSignatureInterfaces.cpp b/tslang/lib/TypeScript/OwnershipSignatureInterfaces.cpp index 58e6b18ec..25c0d1f07 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureInterfaces.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureInterfaces.cpp @@ -734,7 +734,9 @@ namespace own_signature { module.walk([&](mlir_ts::GlobalOp globalOp) { auto name = globalOp.getSymName(); - if (!name.ends_with("..vtbl")) + // an interface's null vtable implements nothing (mlirGenNullInterfaceVTable): a null + // interface is seen through it, and its methods throw + if (!name.ends_with("..vtbl") || globalOp->hasAttr(NULL_INTERFACE_VTABLE_ATTR_NAME)) { return; } @@ -879,6 +881,21 @@ namespace own_signature return use.getOperandNumber() == 0 && user->getParentOfType() != nullptr; } + // the vtable a class object is seen through, chosen by a test for null + // (castToInterfaceSpecialCases): what the choice makes, a `ts.If`'s result or, once + // lowered, the block argument the branches pass it to + if (mlir::isa(user)) + { + auto ifOp = mlir::dyn_cast(user->getParentOp()); + return ifOp && onlyMakesValues(ifOp.getResult(use.getOperandNumber())); + } + + if (auto branchOp = mlir::dyn_cast(user)) + { + auto blockArgument = branchOp.getSuccessorBlockArgument(use.getOperandNumber()); + return blockArgument && onlyMakesValues(*blockArgument); + } + // the copy: read whole, and stored into a block `ts.New` made if (auto loadOp = mlir::dyn_cast(user)) { @@ -898,27 +915,107 @@ namespace own_signature if (auto newInterfaceOp = mlir::dyn_cast(op)) { auto name = mlir::cast(newInterfaceOp.getType()).getName().getValue(); - auto vtable = newInterfaceOp.getInterfaceVTable(); - if (auto found = vtableOf(vtable); !found.empty()) + + // a class object's vtable is chosen by a test for null (castToInterfaceSpecialCases): + // a `ts.If` yields one from each branch or, once lowered, the branches pass one each + // to a block argument. A null object's is a cast of null (the lowering puts in the + // interface's null vtable, whose methods throw), made by nothing that implements the + // interface. + llvm::SmallVector vtableChoices; + auto addChoice = [&](mlir::Value yielded) { + auto stripped = yielded; + while (auto castOp = stripped.getDefiningOp()) + { + stripped = castOp.getIn(); + } + + if (!stripped.getDefiningOp()) + { + vtableChoices.push_back(yielded); + } + }; + + auto vtableValue = newInterfaceOp.getInterfaceVTable(); + if (auto ifOp = vtableValue.getDefiningOp()) { - scan.madeWith[name].insert(found); + auto resultIndex = mlir::cast(vtableValue).getResultNumber(); + for (auto *region : {&ifOp.getThenRegion(), &ifOp.getElseRegion()}) + { + auto resultOp = region->empty() ? mlir_ts::ResultOp() + : mlir::dyn_cast(region->front().getTerminator()); + if (!resultOp) + { + scan.allOpen = true; + return; + } + + addChoice(resultOp.getOperand(resultIndex)); + } } - else if (readFromClass(vtable)) + else if (auto blockArgument = mlir::dyn_cast(vtableValue)) { - // any class's vtable for the interface (classHeld, all in its families below) - if (!scan.classHeld.count(name)) + auto *block = blockArgument.getOwner(); + if (block->hasNoPredecessors()) { - scan.open.insert(name); + scan.allOpen = true; + return; + } + + for (auto *predecessor : block->getPredecessors()) + { + auto branchOp = mlir::dyn_cast(predecessor->getTerminator()); + if (!branchOp) + { + scan.allOpen = true; + return; + } + + for (auto successorIndex : llvm::seq(0, predecessor->getTerminator()->getNumSuccessors())) + { + if (predecessor->getTerminator()->getSuccessor(successorIndex) != block) + { + continue; + } + + auto passed = branchOp.getSuccessorOperands(successorIndex)[blockArgument.getArgNumber()]; + if (!passed) + { + scan.allOpen = true; + return; + } + + addChoice(passed); + } } } - else if (auto extractOp = vtable.getDefiningOp()) + else { - scan.castOpened.insert(name); - valueInterfaces(extractOp.getInterfaceVal().getType(), scan.castOpened); + addChoice(vtableValue); } - else + + for (auto vtable : vtableChoices) { - scan.allOpen = true; + if (auto found = vtableOf(vtable); !found.empty()) + { + scan.madeWith[name].insert(found); + } + else if (readFromClass(vtable)) + { + // any class's vtable for the interface (classHeld, all in its families below) + if (!scan.classHeld.count(name)) + { + scan.open.insert(name); + } + } + else if (auto extractOp = vtable.getDefiningOp()) + { + scan.castOpened.insert(name); + valueInterfaces(extractOp.getInterfaceVal().getType(), scan.castOpened); + } + else + { + scan.allOpen = true; + } } return; diff --git a/tslang/lib/TypeScript/StackEscapeFacts.h b/tslang/lib/TypeScript/StackEscapeFacts.h index 5a7e05320..f081378e2 100644 --- a/tslang/lib/TypeScript/StackEscapeFacts.h +++ b/tslang/lib/TypeScript/StackEscapeFacts.h @@ -388,7 +388,8 @@ class StackEscapeFacts for (auto globalOp : module.getOps()) { auto name = globalOp.getSymName(); - if (!name.ends_with("..vtbl")) + // an interface's null vtable is no class's (mlirGenNullInterfaceVTable) + if (!name.ends_with("..vtbl") || globalOp->hasAttr(NULL_INTERFACE_VTABLE_ATTR_NAME)) { continue; } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 2b62e4837..ec453ee48 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -413,6 +413,9 @@ tslang_add_test(NAME test-compile-00-element-store-after-value COMMAND test-runn tslang_add_test(NAME test-compile-00-const-interface-field-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_interface_field_read.ts") tslang_add_test(NAME test-compile-00-argument-place-overwritten COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00argument_place_overwritten.ts") tslang_add_test(NAME test-compile-00-const-bound-method-declared-type COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_bound_method_declared_type.ts") +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-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") @@ -1005,6 +1008,9 @@ tslang_add_test(NAME test-jit-00-element-store-after-value COMMAND test-runner - tslang_add_test(NAME test-jit-00-const-interface-field-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_interface_field_read.ts") tslang_add_test(NAME test-jit-00-argument-place-overwritten COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00argument_place_overwritten.ts") tslang_add_test(NAME test-jit-00-const-bound-method-declared-type COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_bound_method_declared_type.ts") +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-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") @@ -1987,6 +1993,9 @@ set(TSLANG_CORPUS 00const_interface_field_read.ts 00argument_place_overwritten.ts 00const_bound_method_declared_type.ts + 00nullable_class_to_interface.ts + 00class_to_interface_null.ts + 00null_interface_to_interface.ts 00argument_place_aliased.ts 00temporary_read_in_finally.ts 00unowned_local_store.ts @@ -3747,6 +3756,16 @@ foreach(iface_mm own rc none gc) tslang_add_test(NAME test-compile-${iface_mm}-own-global-interface-fresh COMMAND test-runner -mm=${iface_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_global_interface_fresh.ts") endforeach() +# An interface-or-null global given a class object or null holds an interface over the object, or +# null; the same program under every model (own takes only a global's initializer for now). +foreach(iface_mm own rc none gc) + tslang_add_test(NAME test-jit-${iface_mm}-own-nullable-class-interface COMMAND test-runner -jit -mm=${iface_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_nullable_class_interface.ts") + tslang_add_test(NAME test-compile-${iface_mm}-own-nullable-class-interface COMMAND test-runner -mm=${iface_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_nullable_class_interface.ts") +endforeach() +# a null class object given to an interface (also in TSLANG_CORPUS: rc, none) +tslang_add_test(NAME test-jit-own-00-class-to-interface-null COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_to_interface_null.ts") +tslang_add_test(NAME test-compile-own-00-class-to-interface-null COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_to_interface_null.ts") + # A `let` given a new value after its value moved owns the new one (#528); the same program under # rc and gc (not none: nothing is freed there, and the loops would hold about 800 MB). foreach(reassign_mm rc gc) diff --git a/tslang/test/tester/own/own_nullable_class_interface.ts b/tslang/test/tester/own/own_nullable_class_interface.ts new file mode 100644 index 000000000..7ada0e837 --- /dev/null +++ b/tslang/test/tester/own/own_nullable_class_interface.ts @@ -0,0 +1,52 @@ +// A global `I | null` initialized with a `C | null` - a class object or null - holds an interface +// over the object, or null. The conversion was a plain cast that read the class pointer as an +// interface, under every model. Each read follows allocations of the same size, so a freed block +// is reused. +interface I { + x: number; + get(): number; +} + +class C { + pad: number = 7; + constructor(public x: number) {} + + get(): number { + return this.x * 10; + } +} + +class Junk { + a = -1; + b = -2; + c = -3; +} + +function churn() { + let keep: Junk[] = []; + for (let i = 0; i < 1000; i++) keep.push(new Junk()); + return keep.length; +} + +@noinline function maybe(flag: boolean, x: number): C | null { + if (flag) { + return new C(x); + } + + return null; +} + +let some: I | null = maybe(true, 3); +let none: I | null = maybe(false, 4); + +function main() { + // fields first, before any allocation or method call: the plain cast left the globals pointing + // at whatever it made of the class pointer, and the churn after it allocated without end + assert(some !== null && some.x == 3, "a class object's field"); + assert(none === null, "null"); + + churn(); + assert(some !== null && some.x == 3 && some.get() == 30, "a class object"); + assert(none === null, "null after the churn"); + print("done."); +} diff --git a/tslang/test/tester/tests/00class_to_interface_null.ts b/tslang/test/tester/tests/00class_to_interface_null.ts new file mode 100644 index 000000000..819bc4aa6 --- /dev/null +++ b/tslang/test/tester/tests/00class_to_interface_null.ts @@ -0,0 +1,93 @@ +// @strict-null false +// A null interface is an interface over a null `this`, seen through the interface's null vtable, +// whose methods throw, as a call of an abstract method would. A null class object given to an +// interface is one, and so are `null` and `undefined`. An interface is tested for null, and +// compared, by its `this`. +// +// A null class object was paired with its class's vtable, so the interface was not `=== null`; for a +// class naming the interface, whose vtable is read through the object, the cast read through the +// null pointer and crashed. +interface I { + x: number; + get(): number; +} + +class C { + constructor(public x: number) {} + + get(): number { + return this.x * 10; + } +} + +class D implements I { + constructor(public x: number) {} + + get(): number { + return this.x * 100; + } +} + +@noinline function mkC(flag: boolean, x: number): C { + return flag ? new C(x) : null; +} + +@noinline function mkD(flag: boolean, x: number): D { + return flag ? new D(x) : null; +} + +function read(i: I): number { + if (i === null) { + return -1; + } + + return i.x + i.get(); +} + +// what a method of the interface does: -2 when it throws (an Error, or without the default library, +// which has none, its message) +function call(i: I): number { + try { + return i.get(); + } catch (e) { + return -2; + } +} + +function main() { + // a field first, and no method: a method called through a wrong vtable runs whatever code it lands on + const first: I = mkC(true, 1); + assert(first !== null && first.x == 1, "a field read first"); + + const nc: I = mkC(false, 2); + assert(nc === null, "a null class object"); + assert(nc == null, "a null class object, loosely"); + assert(!nc, "a null class object is false"); + const nd: I = mkD(false, 3); + assert(nd === null, "a null object of a class naming the interface"); + const nl: I = null; + assert(nl === null, "null"); + const nu: I = undefined; + assert(nu == null, "undefined is loosely null"); + assert(nu === undefined, "undefined"); + + assert(call(nc) == -2, "a method of a null class object throws"); + assert(call(nd) == -2, "a method of a null object of a class naming the interface throws"); + assert(call(nl) == -2, "a method of null throws"); + assert(call(nu) == -2, "a method of undefined throws"); + assert(call(mkC(true, 5)) == 50, "a method of a class object"); + + assert(read(mkC(true, 4)) == 44, "an argument"); + assert(read(mkC(false, 4)) == -1, "a null argument"); + assert(read(mkD(true, 5)) == 505, "an argument of a class naming the interface"); + assert(read(mkD(false, 5)) == -1, "a null argument of a class naming the interface"); + + // compared by the object + const c = new C(6); + const i1: I = c; + const i2: I = mkC(true, 6); + assert(i1 !== i2, "two objects"); + assert(i1 === i1, "the same object"); + + print("done."); +} diff --git a/tslang/test/tester/tests/00null_interface_to_interface.ts b/tslang/test/tester/tests/00null_interface_to_interface.ts new file mode 100644 index 000000000..e9950d9b7 --- /dev/null +++ b/tslang/test/tester/tests/00null_interface_to_interface.ts @@ -0,0 +1,56 @@ +// @strict-null false +// A null interface cast to an interface it does not extend, whose members are elsewhere (#666 builds +// that interface's vtable out of the value's own one) is null still, and its methods throw: the null +// vtable's slots, stubs that throw, are what the new vtable is made of. Not under -mm=own, which +// does not take an interface value of one interface as another yet. +interface Hit { + thing: number; + dist: number; + get(): number; +} + +interface HasDist { + dist: number; + get(): number; +} + +class E { + constructor(public thing: number, public dist: number) {} + + get(): number { + return this.dist * 10; + } +} + +@noinline function mkE(flag: boolean, thing: number, dist: number): E { + return flag ? new E(thing, dist) : null; +} + +// what a method of the interface does: -2 when it throws +function call(h: HasDist): number { + try { + return h.get(); + } catch (e) { + return -2; + } +} + +function main() { + // a field first, and no method: a method called through a wrong vtable runs whatever code it lands on + const liveHit: Hit = mkE(true, 1, 2); + const liveDist: HasDist = liveHit; + assert(liveDist.dist == 2, "a field read first"); + assert(call(liveDist) == 20, "a class object seen through another interface"); + + const nullHit: Hit = mkE(false, 1, 2); + const nullDist: HasDist = nullHit; + assert(nullDist === null, "a null class object seen through another interface"); + assert(call(nullDist) == -2, "its method throws"); + + const nullLit: Hit = null; + const nullLitDist: HasDist = nullLit; + assert(nullLitDist === null, "null seen through another interface"); + assert(call(nullLitDist) == -2, "its method throws too"); + + print("done."); +} diff --git a/tslang/test/tester/tests/00nullable_class_to_interface.ts b/tslang/test/tester/tests/00nullable_class_to_interface.ts new file mode 100644 index 000000000..e4d2cc4b8 --- /dev/null +++ b/tslang/test/tester/tests/00nullable_class_to_interface.ts @@ -0,0 +1,137 @@ +// A class object or null (`C | null`) given to an interface or null (`I | null`) becomes an +// interface - the class object paired with the vtable it is seen through - and null stays null. +// The conversion was a plain cast that read the class pointer as an interface, under every model: +// the lowering could not make it, and the program crashed or read garbage. A class object alone +// (`let a: I = mk(5)`) was already converted; a value of the class type with null beside it was +// not. The `held` shapes read the interface after the class-typed local let go of the object and +// allocations of the same size reused anything freed. +interface I { + x: number; + get(): number; +} + +class C { + pad: number = 7; + constructor(public x: number) {} + + get(): number { + return this.x * 10; + } +} + +// a class that names the interface: its vtable is read through the object +class D implements I { + constructor(public x: number) {} + + get(): number { + return this.x * 100; + } +} + +@noinline function maybeD(flag: boolean, x: number): D | null { + return flag ? new D(x) : null; +} + +@noinline function mk(x: number): C { + return new C(x); +} + +@noinline function maybe(flag: boolean, x: number): C | null { + return flag ? mk(x) : null; +} + +function read(i: I | null): number { + if (i === null) { + return -1; + } + + return i.x + i.get(); +} + +function viewed(c: C | null): I | null { + return c; +} + +function conditional(flag: boolean) { + let opt: I | null = flag ? mk(5) : null; + if (opt === null) { + return -1; + } + + return opt.x + opt.get(); +} + +function constConditional(flag: boolean) { + const opt: I | null = flag ? mk(6) : null; + return opt !== null ? opt.x : -1; +} + +function assigned(flag: boolean) { + let opt: I | null = null; + const c = maybe(flag, 8); + opt = c; + return read(opt); +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new C(777)); + } +} + +function held(flag: boolean) { + let c: C | null = maybe(flag, 11); + const opt: I | null = c; + c = null; + churn(); + return read(opt); +} + +let G: I | null = maybe(true, 12); +let H: I | null = maybe(false, 12); + +function main() { + // A field first, and no method: a method called through a wrong vtable runs whatever code it + // lands on, which allocated without end (gigabytes) when the conversion was a plain cast. + const first: I | null = maybe(true, 1); + assert(first !== null && first.x == 1, "a field read first"); + + assert(held(true) == 121, "held after the class-typed local let go"); + assert(held(false) == -1, "a held null"); + + churn(); + assert(read(G) == 132, "a global"); + assert(H === null, "a null global"); + G = maybe(true, 13); + churn(); + assert(read(G) == 143, "a global reassigned"); + + assert(conditional(true) == 55, "a conditional, class side"); + assert(conditional(false) == -1, "a conditional, null side"); + + assert(constConditional(true) == 6, "a const, class side"); + assert(constConditional(false) == -1, "a const, null side"); + + assert(read(maybe(true, 3)) == 33, "an argument"); + assert(read(maybe(false, 3)) == -1, "a null argument"); + + const v = viewed(maybe(true, 4)); + assert(v !== null && v.x == 4 && v.get() == 40, "a returned value"); + assert(viewed(maybe(false, 4)) === null, "a returned null"); + + assert(assigned(true) == 88, "an assignment"); + assert(assigned(false) == -1, "a null assignment"); + + assert(read(maybeD(true, 2)) == 202, "a class implementing the interface"); + assert(read(maybeD(false, 2)) == -1, "a null of a class implementing the interface"); + const d: I | null = maybeD(true, 3); + assert(d !== null && d.get() == 300, "a const of a class implementing the interface"); + + let u: I | undefined = undefined; + const cu: C | undefined = mk(9); + u = cu; + assert(u !== undefined && u.x == 9 && u.get() == 90, "an optional"); + + print("done."); +}