diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 539bdeae0..157dc7b8f 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -173,6 +173,11 @@ // INTERFACE_VTABLE_HEADER_SLOTS. #define INTERFACE_VTABLE_INSTANCEOF_SLOT 0 #define INTERFACE_VTABLE_HEADER_SLOTS 1 +// 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 +// value a vtable built from its own one, kept once per content (CastOpLowering). +#define INTERFACE_VTABLE_MAP_ATTR_NAME "__vtable_map" // An interface `v?: T` field's slot is the field's offset from `this`, or -1 when absent. A class // stores its `v?: T` as optional - {T, flag}, value first - and its slot carries this bit on top // of the offset: an access then reads the flag (false is undefined), and a write sets it. diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index e8be34c68..775eed78c 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -2889,6 +2889,221 @@ struct DialectCastOpLowering : public TsLlvmPattern } }; +// The vtable an interface value gets when it is cast to an interface whose members are not where its +// own interface has them (INTERFACE_VTABLE_MAP_ATTR_NAME): slot i is slot map[i] of the value's vtable, +// or -1 where map[i] is -1. A vtable with the same content is one vtable: it is built the first time it +// is asked for and kept in a list for as long as the program runs, so a cast made again (in a loop) +// allocates nothing, and nothing has to free one. A vtable holds function pointers, field offsets and +// addresses of globals, no heap pointer. A null vtable (a null interface) stays null. +// ptr ts_remap_vtable(ptr vtable, ptr map, index count) +static LLVM::LLVMFuncOp getOrCreateRemapVTableFunction(Operation *op, ConversionPatternRewriter &rewriter, + const TypeConverter *typeConverter, CompileOptions &compileOptions) +{ + StringRef name = "ts_remap_vtable"; + StringRef listName = "ts_remap_vtable_list"; + + auto parentModule = op->getParentOfType(); + if (auto existing = parentModule.lookupSymbol(name)) + { + return existing; + } + + TypeHelper th(rewriter); + TypeConverterHelper tch(typeConverter); + LLVMCodeHelper ch(op, rewriter, typeConverter, compileOptions); + + auto loc = rewriter.getUnknownLoc(); + auto ptrType = th.getPtrType(); + auto i32Type = th.getI32Type(); + auto wordType = tch.convertType(th.getIndexType()); + auto wordBytes = wordType.getIntOrFloatBitWidth() / 8; + + OpBuilder::InsertionGuard insertGuard(rewriter); + rewriter.setInsertionPointToStart(parentModule.getBody()); + + // the first vtable built; each starts with the next one and its slot count: {next, count, slots...} + auto list = rewriter.create(loc, ptrType, /*isConstant=*/false, LLVM::Linkage::Internal, listName, + mlir::Attribute{}); + { + OpBuilder::InsertionGuard initGuard(rewriter); + rewriter.setInsertionPointToStart(rewriter.createBlock(&list.getInitializerRegion())); + mlir::Value nullList = rewriter.create(loc, ptrType); + rewriter.create(loc, ValueRange{nullList}); + } + + auto funcOp = rewriter.create( + loc, name, th.getFunctionType(ptrType, {ptrType, ptrType, wordType}), LLVM::Linkage::Internal); + + auto *entryBlock = funcOp.addEntryBlock(rewriter); + auto vtable = entryBlock->getArgument(0); + auto map = entryBlock->getArgument(1); + auto count = entryBlock->getArgument(2); + + auto &body = funcOp.getBody(); + auto newBlock = [&](ArrayRef types) { + SmallVector locations(types.size(), loc); + return rewriter.createBlock(&body, body.end(), types, locations); + }; + + auto *returnNullBlock = newBlock({}); + auto *scanStartBlock = newBlock({}); + auto *scanBlock = newBlock({ptrType}); + auto *compareCountBlock = newBlock({}); + auto *compareBlock = newBlock({wordType}); + auto *compareSlotBlock = newBlock({}); + auto *foundBlock = newBlock({}); + auto *nextNodeBlock = newBlock({}); + auto *insertBlock = newBlock({}); + auto *fillBlock = newBlock({wordType}); + auto *fillSlotBlock = newBlock({}); + auto *linkStartBlock = newBlock({}); + auto *linkBlock = newBlock({ptrType}); + auto *linkedBlock = newBlock({}); + + auto word = [&](int64_t value) -> mlir::Value { + return rewriter.create(loc, wordType, rewriter.getIntegerAttr(wordType, value)); + }; + auto wordAt = [&](mlir::Value base, mlir::Value index) -> mlir::Value { + return rewriter.create(loc, ptrType, wordType, base, ValueRange{index}); + }; + auto nodeCount = [&](mlir::Value node) { return wordAt(node, word(1)); }; + auto nodeSlots = [&](mlir::Value node) { return wordAt(node, word(2)); }; + auto listAddress = [&]() -> mlir::Value { return rewriter.create(loc, ptrType, listName); }; + auto loadList = [&](LLVM::AtomicOrdering ordering) -> mlir::Value { + return rewriter.create(loc, ptrType, listAddress(), wordBytes, /*isVolatile=*/false, + /*isNonTemporal=*/false, /*isInvariant=*/false, + /*isInvariantGroup=*/false, ordering); + }; + // slot `index` of the vtable being asked for + auto mappedSlot = [&](mlir::Value index) -> mlir::Value { + mlir::Value fromAddress = rewriter.create(loc, ptrType, i32Type, map, ValueRange{index}); + mlir::Value from = rewriter.create(loc, i32Type, fromAddress); + if (wordType != i32Type) + { + from = rewriter.create(loc, wordType, from); + } + + mlir::Value absent = rewriter.create(loc, LLVM::ICmpPredicate::slt, from, word(0)); + mlir::Value readFrom = rewriter.create(loc, absent, word(0), from); + mlir::Value slot = rewriter.create(loc, wordType, wordAt(vtable, readFrom)); + return rewriter.create(loc, absent, word(-1), slot); + }; + + rewriter.setInsertionPointToEnd(entryBlock); + mlir::Value nullPtr = rewriter.create(loc, ptrType); + mlir::Value isNull = rewriter.create(loc, LLVM::ICmpPredicate::eq, vtable, nullPtr); + rewriter.create(loc, isNull, returnNullBlock, scanStartBlock); + + rewriter.setInsertionPointToEnd(returnNullBlock); + rewriter.create(loc, ValueRange{nullPtr}); + + rewriter.setInsertionPointToEnd(scanStartBlock); + rewriter.create(loc, ValueRange{loadList(LLVM::AtomicOrdering::acquire)}, scanBlock); + + // look for a vtable with this content + rewriter.setInsertionPointToEnd(scanBlock); + auto node = scanBlock->getArgument(0); + mlir::Value isEnd = rewriter.create(loc, LLVM::ICmpPredicate::eq, node, nullPtr); + rewriter.create(loc, isEnd, insertBlock, compareCountBlock); + + rewriter.setInsertionPointToEnd(compareCountBlock); + mlir::Value nodeCountValue = rewriter.create(loc, wordType, nodeCount(node)); + mlir::Value sameCount = rewriter.create(loc, LLVM::ICmpPredicate::eq, nodeCountValue, count); + rewriter.create(loc, sameCount, compareBlock, ValueRange{word(0)}, nextNodeBlock, ValueRange{}); + + rewriter.setInsertionPointToEnd(compareBlock); + auto compareIndex = compareBlock->getArgument(0); + mlir::Value compared = rewriter.create(loc, LLVM::ICmpPredicate::eq, compareIndex, count); + rewriter.create(loc, compared, foundBlock, compareSlotBlock); + + rewriter.setInsertionPointToEnd(compareSlotBlock); + mlir::Value nodeSlot = rewriter.create(loc, wordType, wordAt(nodeSlots(node), compareIndex)); + mlir::Value sameSlot = rewriter.create(loc, LLVM::ICmpPredicate::eq, nodeSlot, mappedSlot(compareIndex)); + mlir::Value nextCompareIndex = rewriter.create(loc, compareIndex, word(1)); + rewriter.create(loc, sameSlot, compareBlock, ValueRange{nextCompareIndex}, nextNodeBlock, ValueRange{}); + + rewriter.setInsertionPointToEnd(foundBlock); + rewriter.create(loc, ValueRange{nodeSlots(node)}); + + rewriter.setInsertionPointToEnd(nextNodeBlock); + mlir::Value nextNode = rewriter.create(loc, ptrType, node); + rewriter.create(loc, ValueRange{nextNode}, scanBlock); + + // none: build it + rewriter.setInsertionPointToEnd(insertBlock); + mlir::Value slotsWithHeader = rewriter.create(loc, count, word(2)); + mlir::Value size = rewriter.create(loc, slotsWithHeader, word(wordBytes)); + auto newNode = ch.MemoryAlloc(size); + rewriter.create(loc, count, nodeCount(newNode)); + rewriter.create(loc, ValueRange{word(0)}, fillBlock); + + rewriter.setInsertionPointToEnd(fillBlock); + auto fillIndex = fillBlock->getArgument(0); + mlir::Value filled = rewriter.create(loc, LLVM::ICmpPredicate::eq, fillIndex, count); + rewriter.create(loc, filled, linkStartBlock, fillSlotBlock); + + rewriter.setInsertionPointToEnd(fillSlotBlock); + rewriter.create(loc, mappedSlot(fillIndex), wordAt(nodeSlots(newNode), fillIndex)); + mlir::Value nextFillIndex = rewriter.create(loc, fillIndex, word(1)); + rewriter.create(loc, ValueRange{nextFillIndex}, fillBlock); + + // put it first in the list; a cast running at the same time on another thread may add the same + // content too, which costs a block and nothing else + rewriter.setInsertionPointToEnd(linkStartBlock); + rewriter.create(loc, ValueRange{loadList(LLVM::AtomicOrdering::monotonic)}, linkBlock); + + rewriter.setInsertionPointToEnd(linkBlock); + auto expectedFirst = linkBlock->getArgument(0); + rewriter.create(loc, expectedFirst, newNode); + auto exchange = rewriter.create(loc, listAddress(), expectedFirst, newNode, + LLVM::AtomicOrdering::acq_rel, + LLVM::AtomicOrdering::monotonic); + mlir::Value seenFirst = rewriter.create(loc, exchange, ArrayRef{0}); + mlir::Value linked = rewriter.create(loc, exchange, ArrayRef{1}); + rewriter.create(loc, linked, linkedBlock, ValueRange{}, linkBlock, ValueRange{seenFirst}); + + rewriter.setInsertionPointToEnd(linkedBlock); + rewriter.create(loc, ValueRange{nodeSlots(newNode)}); + + return funcOp; +} + +static mlir::Value remapInterfaceVTable(Operation *op, mlir::Value interfaceValue, ArrayRef vtableMap, + ConversionPatternRewriter &rewriter, const TypeConverter *typeConverter, + CompileOptions &compileOptions) +{ + TypeHelper th(rewriter); + TypeConverterHelper tch(typeConverter); + + auto loc = op->getLoc(); + auto ptrType = th.getPtrType(); + auto i32Type = th.getI32Type(); + auto wordType = tch.convertType(th.getIndexType()); + + auto remapFunction = getOrCreateRemapVTableFunction(op, rewriter, typeConverter, compileOptions); + + // the map, one constant per content + auto mapName = "ts_vtable_map_" + std::to_string((size_t)llvm::hash_combine_range(vtableMap.begin(), vtableMap.end())); + auto parentModule = op->getParentOfType(); + if (!parentModule.lookupSymbol(mapName)) + { + OpBuilder::InsertionGuard insertGuard(rewriter); + rewriter.setInsertionPointToStart(parentModule.getBody()); + + auto mapType = LLVM::LLVMArrayType::get(i32Type, vtableMap.size()); + auto mapValue = mlir::DenseIntElementsAttr::get( + mlir::RankedTensorType::get({(int64_t)vtableMap.size()}, i32Type), vtableMap); + rewriter.create(loc, mapType, /*isConstant=*/true, LLVM::Linkage::Internal, mapName, mapValue); + } + + mlir::Value vtable = rewriter.create(loc, interfaceValue, ArrayRef{DATA_VALUE_INDEX}); + mlir::Value mapAddress = rewriter.create(loc, ptrType, mapName); + mlir::Value count = rewriter.create(loc, wordType, rewriter.getIntegerAttr(wordType, vtableMap.size())); + auto remapped = rewriter.create(loc, remapFunction, ValueRange{vtable, mapAddress, count}); + return rewriter.create(loc, interfaceValue, remapped.getResult(), + ArrayRef{DATA_VALUE_INDEX}); +} + struct CastOpLowering : public TsLlvmPattern { using TsLlvmPattern::TsLlvmPattern; @@ -2903,6 +3118,13 @@ struct CastOpLowering : public TsLlvmPattern auto in = transformed.getIn(); auto resType = op.getRes().getType(); + if (auto vtableMap = op->getAttrOfType(INTERFACE_VTABLE_MAP_ATTR_NAME)) + { + rewriter.replaceOp(op, remapInterfaceVTable(op, in, vtableMap.asArrayRef(), rewriter, getTypeConverter(), + tsLlvmContext->compileOptions)); + return success(); + } + CastLogicHelper castLogic(op, rewriter, tch, tsLlvmContext->compileOptions); // in case of Union we need mlir_ts::UnionType value auto result = castLogic.cast(isa(op.getIn().getType()) ? op.getIn() : in, op.getIn().getType(), resType); diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index 56dad6aa7..606216ea3 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -1070,6 +1070,11 @@ namespace mlirgen return castObjectToInterface(location, value, objectType, interfaceType, genContext); } + if (auto srcInterfaceType = dyn_cast(valueType)) + { + return castInterfaceToInterface(location, value, srcInterfaceType, interfaceType, genContext); + } + // A number, string, boolean or array has no fields an interface's members could be read // from: passed for an `{ kind: number }` it compiled, without a word, to a cast that // lowering could not make, and the callee read garbage. An interface without members @@ -1092,6 +1097,133 @@ namespace mlirgen return std::nullopt; } + // 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 + // `thing`, the first slot of a `Hit { thing; ray; dist }`, and a method slot called whichever method + // came first. For any other interface the cast carries where each of the target's slots is in the + // source's vtable, and lowering builds the target's vtable from the value's own one. A slot is copied + // as it is - a field's offset, a method, a cell, -1 for an absent member - so a member is taken only + // where both name it alike: a field for a field of the same type, a method for a method. A member the + // source lacks is absent, as an optional one is: such a cast (`tube = ball`, a Ball having no + // `length`) was always let through. + std::optional MLIRGenImpl::castInterfaceToInterface(mlir::Location location, mlir::Value value, + mlir_ts::InterfaceType srcInterfaceType, + mlir_ts::InterfaceType interfaceType, + const GenContext &genContext) + { + if (srcInterfaceType == interfaceType || genContext.allowPartialResolve) + { + return std::nullopt; + } + + auto srcInterfaceInfo = getInterfaceInfoByFullName(srcInterfaceType.getName().getValue()); + auto interfaceInfo = getInterfaceInfoByFullName(interfaceType.getName().getValue()); + if (!srcInterfaceInfo || !interfaceInfo) + { + return std::nullopt; + } + + SmallVector vtableMap(interfaceInfo->getVTableSize() + INTERFACE_VTABLE_HEADER_SLOTS, -1); + 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 + mlir::Attribute problemMember; + StringRef problem; + auto cannotCast = [&](mlir::Attribute memberId, StringRef reason) { + if (!problemMember) + { + problemMember = memberId; + problem = reason; + } + }; + + // the slots of the target, in its vtable's order: each `extends` block, then its own methods and + // fields (InterfaceInfo::getAllMethodsWithVTableOffset) + std::function mapSlots = [&](InterfaceInfo::TypePtr info, int baseOffset) { + auto offset = baseOffset; + for (auto &extent : info->extends) + { + mapSlots(std::get<1>(extent), offset); + offset += std::get<1>(extent)->getVTableSize(); + } + + for (auto &method : info->methods) + { + auto slot = method.virtualIndex + baseOffset + INTERFACE_VTABLE_HEADER_SLOTS; + auto srcOffset = 0; + if (auto *srcMethod = srcInterfaceInfo->findMethod(method.name, srcOffset)) + { + if (srcMethod->funcType != method.funcType) + { + cannotCast(builder.getStringAttr(method.name), "has another type"); + } + + vtableMap[slot] = srcMethod->virtualIndex + srcOffset + INTERFACE_VTABLE_HEADER_SLOTS; + } + else if (srcInterfaceInfo->findField(builder.getStringAttr(method.name))) + { + cannotCast(builder.getStringAttr(method.name), "is a method of one and a field of the other"); + } + } + + for (auto &field : info->fields) + { + auto slot = field.virtualIndex + baseOffset + INTERFACE_VTABLE_HEADER_SLOTS; + auto srcOffset = 0; + if (auto *srcField = srcInterfaceInfo->findField(field.id, srcOffset)) + { + if (mth.stripLiteralType(srcField->type) != mth.stripLiteralType(field.type)) + { + cannotCast(field.id, "has another type"); + } + + if (srcField->isConditional && !field.isConditional) + { + cannotCast(field.id, "is optional"); + } + + vtableMap[slot] = srcField->virtualIndex + srcOffset + INTERFACE_VTABLE_HEADER_SLOTS; + } + else if (auto fieldName = dyn_cast(field.id); + fieldName && srcInterfaceInfo->findMethod(fieldName.getValue())) + { + cannotCast(field.id, "is a method of one and a field of the other"); + } + } + }; + + mapSlots(interfaceInfo, 0); + + // an interface the source extends, or one laid out the same: the vtable serves as it is + if (llvm::all_of(llvm::enumerate(vtableMap), [](auto slot) { return slot.value() == (int32_t)slot.index(); })) + { + return std::nullopt; + } + + if (problemMember) + { + auto diagnostic = emitError(location); + diagnostic << "interface '" << to_print(srcInterfaceType) << "' can't be cast to interface '" + << to_print(interfaceType) << "': member "; + if (auto memberName = dyn_cast(problemMember)) + { + diagnostic << "'" << memberName.getValue() << "' "; + } + else + { + diagnostic << problemMember << " "; + } + + diagnostic << problem; + return mlir::failure(); + } + + auto castOp = builder.create(location, interfaceType, value); + castOp->setAttr(INTERFACE_VTABLE_MAP_ATTR_NAME, builder.getDenseI32ArrayAttr(vtableMap)); + return V(castOp); + } + // A constant array keeps the element type its literal was built with: `const c = [1, 2]` is a // const_array, and so is `[1, 2]` meeting a union such as `number[] | string`, where no single // array type guides the literal. Lowering turns a const array into an array by copying its data as it diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index ce82cca98..329261e29 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -13331,6 +13331,9 @@ 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); + std::optional castInterfaceToInterface(mlir::Location location, mlir::Value value, mlir_ts::InterfaceType srcInterfaceType, + mlir_ts::InterfaceType interfaceType, const GenContext &genContext); + // casts between tuple-like types (tuple, const tuple, class storage, interface fields) std::optional castTupleLikeVariants(mlir::Location location, mlir::Type type, mlir::Value value, mlir::Type valueType, const GenContext &genContext); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 2dc4d3043..bc09ce5d9 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -400,6 +400,7 @@ tslang_add_test(NAME test-compile-00-const-interface-of-local COMMAND test-runne tslang_add_test(NAME test-compile-00-string-view-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.ts") tslang_add_test(NAME test-compile-00-global-copy-counted COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_copy_counted.ts") tslang_add_test(NAME test-compile-00-global-interface-initializer COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_interface_initializer.ts") +tslang_add_test(NAME test-compile-00-interface-cast-unrelated COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_cast_unrelated.ts") tslang_add_test(NAME test-compile-00-c-string-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00c_string_copy.ts") tslang_add_test(NAME test-compile-00-const-field-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_field_read.ts") tslang_add_test(NAME test-compile-00-const-element-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_element_read.ts") @@ -991,6 +992,7 @@ tslang_add_test(NAME test-jit-00-const-interface-of-local COMMAND test-runner -j tslang_add_test(NAME test-jit-00-string-view-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.ts") tslang_add_test(NAME test-jit-00-global-copy-counted COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_copy_counted.ts") tslang_add_test(NAME test-jit-00-global-interface-initializer COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_interface_initializer.ts") +tslang_add_test(NAME test-jit-00-interface-cast-unrelated COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_cast_unrelated.ts") tslang_add_test(NAME test-jit-00-c-string-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00c_string_copy.ts") tslang_add_test(NAME test-jit-00-const-field-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_field_read.ts") tslang_add_test(NAME test-jit-00-const-element-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_element_read.ts") @@ -1972,6 +1974,7 @@ set(TSLANG_CORPUS 00string_view_copy.ts 00global_copy_counted.ts 00global_interface_initializer.ts + 00interface_cast_unrelated.ts 00c_string_copy.ts 00const_field_read.ts 00const_element_read.ts @@ -3098,6 +3101,26 @@ add_test(NAME test-compile-interface-cast-empty set_tests_properties(test-compile-interface-cast-empty PROPERTIES FAIL_REGULAR_EXPRESSION "error:|Stack dump|Assertion failed") +# An interface cast to another one it does not extend gets a vtable built from its own one, slot for +# slot (00interface_cast_unrelated.ts); a member that is a method in one and a field in the other, or a +# field of another type, has no slot to copy. +set(interface_cast_error_cases + "method_field|interface 'F' can't be cast to interface 'M': member 'g' is a method of one and a field of the other" + "field_type|interface 'S1' can't be cast to interface 'T1': member 'b' has another type") +foreach(interface_cast_error_case ${interface_cast_error_cases}) + string(FIND "${interface_cast_error_case}" "|" interface_cast_error_sep) + string(SUBSTRING "${interface_cast_error_case}" 0 ${interface_cast_error_sep} interface_cast_error_name) + math(EXPR interface_cast_error_sep "${interface_cast_error_sep} + 1") + string(SUBSTRING "${interface_cast_error_case}" ${interface_cast_error_sep} -1 interface_cast_error_message) + add_test(NAME test-compile-interface-cast-${interface_cast_error_name} + COMMAND $ --emit=obj --no-default-lib -mm=none + "${PROJECT_SOURCE_DIR}/test/tester/interface-cast/${interface_cast_error_name}.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/interface-cast-${interface_cast_error_name}.obj") + set_tests_properties(test-compile-interface-cast-${interface_cast_error_name} + PROPERTIES PASS_REGULAR_EXPRESSION "${interface_cast_error_message}" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") +endforeach() + # `instanceof` with a number, a string, a boolean or a bigint value on the right is an error: the # test compared the two types. A type there (`type str = TypeOf<"">`) stays an extension. add_test(NAME test-compile-diagnostic-location-instanceof-primitive diff --git a/tslang/test/tester/interface-cast/field_type.ts b/tslang/test/tester/interface-cast/field_type.ts new file mode 100644 index 000000000..b4220dd51 --- /dev/null +++ b/tslang/test/tester/interface-cast/field_type.ts @@ -0,0 +1,15 @@ +// T1's `b` is a number, S1's a string: the slot is the string's offset, read as a number. +interface S1 { + a: number; + b: string; +} + +interface T1 { + b: number; +} + +function main() { + const s: S1 = { a: 1, b: "x" }; + const t: T1 = s; + print(t.b); +} diff --git a/tslang/test/tester/interface-cast/method_field.ts b/tslang/test/tester/interface-cast/method_field.ts new file mode 100644 index 000000000..a4fc01518 --- /dev/null +++ b/tslang/test/tester/interface-cast/method_field.ts @@ -0,0 +1,16 @@ +// M's `g` is a method, F's a field holding a function: their vtable slots hold different things (a +// function, an offset into the object), so the one can't stand for the other. +interface F { + k: number; + g: () => number; +} + +interface M { + g(): number; +} + +function main() { + const f: F = { k: 1, g: (): number => 5 }; + const m: M = f; + print(m.g()); +} diff --git a/tslang/test/tester/tests/00interface_cast_unrelated.ts b/tslang/test/tester/tests/00interface_cast_unrelated.ts new file mode 100644 index 000000000..096d483e9 --- /dev/null +++ b/tslang/test/tester/tests/00interface_cast_unrelated.ts @@ -0,0 +1,131 @@ +// An interface value cast to another interface that it does not extend - one that only has some of its +// members, in other places - kept its own vtable, which the other interface read by its own slot +// numbers: `HasDist.dist` read `Hit.thing`, its first slot, and `B.g()` called `A.f()`, the method in +// B's slot. The cast now gives the value a vtable laid out for the interface it is cast to. +interface Hit { + thing: number; + ray: number; + dist: number; +} + +interface HasDist { + dist: number; +} + +interface RayDist { + dist: number; + ray: number; +} + +interface Labelled { + dist: number; + label?: string; +} + +class HitC implements Hit { + thing: number = 10; + ray: number = 20; + dist: number = 30; +} + +function mkHit(): Hit { + return { thing: 1, ray: 2, dist: 3 }; +} + +function distOf(d: HasDist) { + return d.dist; +} + +interface A { + f(): number; + g(): number; +} + +interface B { + g(): number; +} + +class AC implements A { + base = 100; + f(): number { + return this.base + 1; + } + g(): number { + return this.base + 2; + } +} + +// a diamond: B2's members come after A2's in AB's vtable +interface A2 { + a: number; +} + +interface B2 { + b: number; +} + +interface AB extends A2, B2 { + c: number; +} + +function main() { + // an object literal, through a local, a call's result, an argument, ``, an element + const h: Hit = { thing: 1, ray: 2, dist: 3 }; + const d: HasDist = h; + assert(d.dist == 3, "a field of an object literal"); + const d2: HasDist = mkHit(); + assert(d2.dist == 3, "a field of a call's result"); + assert(distOf(h) == 3, "a field of an argument"); + assert((h).dist == 3, "a field through "); + const ds: HasDist[] = [h]; + assert(ds[0].dist == 3, "a field of an element"); + + // two members in another order + const rd: RayDist = h; + assert(rd.dist == 3 && rd.ray == 2, "two fields in another order"); + + // a class object + const hc: Hit = new HitC(); + const dc: HasDist = hc; + assert(dc.dist == 30, "a field of a class object"); + assert(distOf(hc) == 30, "a field of a class object as an argument"); + + // a value already cast, cast again + const again: HasDist = rd; + assert(again.dist == 3, "a value cast twice"); + + // an optional member the source does not have is absent + const l: Labelled = h; + assert(l.dist == 3, "a field next to an absent optional one"); + assert(l.label === undefined, "an absent optional field"); + + // methods, of a class object and of an object literal + const a: A = new AC(); + const b: B = a; + assert(b.g() == 102, "a method of a class object"); + const o: A = { + f(): number { + return 10; + }, + g(): number { + return 20; + } + }; + const b2: B = o; + assert(b2.g() == 20, "a method of an object literal"); + + // the second interface of a diamond + const ab: AB = { a: 1, b: 2, c: 3 }; + const b3: B2 = ab; + assert(b3.b == 2, "the second interface of a diamond"); + + // casts made again and again share one vtable per content and keep reading right + let sum: number = 0; + for (let i = 0; i < 1000; i++) { + const each: HasDist = i % 2 == 0 ? h : hc; + sum += each.dist; + } + assert(sum == 500 * 3 + 500 * 30, "casts in a loop"); + + print("done."); +}