diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index a6af46f5c..66b21d01e 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -85,6 +85,8 @@ #define CONSTRUCTOR_NAME "constructor" #define CONSTRUCTOR_TEMPVAR_NAME ".ctor" #define VTABLE_NAME ".vtbl" +// the vtable an object cast to an interface keeps in its own block (castTupleWithOwnVTableToInterface) +#define OWN_INTERFACE_VTABLE_FIELD_NAME ".ivtbl" #define RTTI_NAME ".rtti" #define SIZE_NAME ".size" #define INSTANCEOF_NAME ".instanceOf" diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h index d6eea410a..c37868a05 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h @@ -1383,6 +1383,18 @@ class MLIRTypeHelper auto test = isa(foundField.type) && isa(fieldType) ? TestFunctionTypesMatchWithObjectMethods(location, foundField.type, fieldType).result == MatchResultType::Match : stripLiteralType(fieldType) == stripLiteralType(foundField.type); + + // a `?` member is read through the vtable as its T, and an optional field (`v?: T` of a + // type literal) holds T first: the slot points at that value, and is patched to absent when + // the field has none (mlirGenCreateInterfaceVTableForObject) + if (!test && isConditional) + { + if (auto optionalType = dyn_cast(foundField.type)) + { + test = stripLiteralType(fieldType) == stripLiteralType(optionalType.getElementType()); + } + } + if (!test) { LLVM_DEBUG(llvm::dbgs() << "field " << id << " not matching type: " << fieldType << " and " diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index 4307b67d0..2ecb6e0cd 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -114,6 +114,25 @@ namespace mlirgen return mlir::success(); } + // the value of a field the source does not have, when the field's type admits undefined: `v?: T` + // is optional, and `v?: any` stays any (any | undefined is any) - both read as undefined + mlir::Value MLIRGenImpl::absentFieldValue(mlir::Location location, mlir::Type fieldType, const GenContext &genContext) + { + if (isa(fieldType)) + { + return builder.create(location, fieldType); + } + + if (isa(fieldType)) + { + auto undefValue = builder.create(location, getUndefinedType()); + auto result = cast(location, fieldType, undefValue, genContext); + return result.failed() ? mlir::Value() : V(result); + } + + return mlir::Value(); + } + ValueOrLogicalResult MLIRGenImpl::mapTupleToFields(mlir::Location location, SmallVector &values, mlir::Value value, mlir_ts::TupleType srcTupleType, ::llvm::ArrayRef<::mlir::typescript::FieldInfo> fields, bool filterSpecialCases, const GenContext &genContext, bool errorAsWarning) { @@ -137,12 +156,13 @@ namespace mlirgen count ++; if (fieldInfo.id == mlir::Attribute() || (index < srcTupleType.size() && srcTupleType.getFieldInfo(index).id == mlir::Attribute())) { - if (index >= srcTupleType.size() && isa(fieldInfo.type)) + if (index >= srcTupleType.size()) { - // add undefined value - auto undefVal = builder.create(location, fieldInfo.type); - values.push_back(undefVal); - continue; + if (auto undefVal = absentFieldValue(location, fieldInfo.type, genContext)) + { + values.push_back(undefVal); + continue; + } } MLIRPropertyAccessCodeLogic cl(compileOptions, builder, location, value, builder.getI32IntegerAttr(index)); @@ -156,10 +176,8 @@ namespace mlirgen auto fieldIndex = srcTupleType.getIndex(fieldInfo.id); if (fieldIndex < 0) { - if (isa(fieldInfo.type)) + if (auto undefVal = absentFieldValue(location, fieldInfo.type, genContext)) { - // add undefined value - auto undefVal = builder.create(location, fieldInfo.type); values.push_back(undefVal); continue; } @@ -2302,6 +2320,14 @@ namespace mlirgen emitWarning(location, "") << "Cloned object is used. Ensure all types are matching to interface: " << interfaceInfo->fullName; } + // a `?` member held as optional needs a vtable of this object's own (patchAbsentOptionalFieldSlots): + // it goes in a field after the object's own ones, so the block the interface holds frees it as well + if (!getOptionalFieldSlots(mlir::cast(tupleType), interfaceInfo).empty()) + { + return castTupleWithOwnVTableToInterface(location, inEffective, mlir::cast(tupleType), + interfaceInfo, genContext); + } + // TODO: finish it, what to finish it? maybe optimization not to create extra object? // convert Tuple to Object auto objType = mlir_ts::ObjectType::get(tupleType); @@ -2325,8 +2351,72 @@ namespace mlirgen return castObjectToInterface(location, in, objType, interfaceInfo, genContext); } + // the object is copied into a block with one more field, `.vtable`, after its own: the vtable patched for + // its `?` members (patchAbsentOptionalFieldSlots) is stored there, so it lives and dies with the block the + // interface holds. The object's own fields keep their offsets, which are what the vtable holds. + ValueOrLogicalResult MLIRGenImpl::castTupleWithOwnVTableToInterface(mlir::Location location, mlir::Value in, + mlir_ts::TupleType tupleType, InterfaceInfo::TypePtr interfaceInfo, const GenContext &genContext) + { + auto inEffective = in; + if (inEffective.getType() != tupleType) + { + CAST(inEffective, location, tupleType, inEffective, genContext); + } + + // the vtable's type: the interface's slots typed by this object's fields + auto plainObjType = mlir_ts::ObjectType::get(tupleType); + auto vtableName = interfaceVTableNameForObject(plainObjType, interfaceInfo); + auto globalVTable = resolveFullNameIdentifier(location, vtableName, true, genContext); + if (!globalVTable) + { + if (mlir::failed(mlirGenObjectVirtualTableDefinitionForInterface(location, plainObjType, interfaceInfo, genContext))) + { + return mlir::failure(); + } + + globalVTable = resolveFullNameIdentifier(location, vtableName, true, genContext); + if (!globalVTable) + { + return mlir::failure(); + } + } + + auto vtableType = mlir::cast(globalVTable.getType()).getElementType(); + + SmallVector fields(tupleType.getFields().begin(), tupleType.getFields().end()); + fields.push_back({MLIRHelper::TupleFieldName(OWN_INTERFACE_VTABLE_FIELD_NAME, builder.getContext()), vtableType, false, mlir_ts::AccessLevel::Public}); + auto withVTableType = getTupleType(fields); + + SmallVector types; + for (auto &field : tupleType.getFields()) + { + types.push_back(field.type); + } + + // as in castTupleToInterface: the block owns what its fields hold + mlirGenRetainCaptured(location, mlir::ValueRange{inEffective}); + + auto parts = builder.create(location, types, inEffective); + mlir::Value withVTable = builder.create(location, withVTableType); + for (auto [index, part] : llvm::enumerate(parts.getResults())) + { + withVTable = builder.create(location, withVTableType, part, withVTable, + MLIRHelper::getStructIndex(builder, index)); + } + + auto objType = mlir_ts::ObjectType::get(withVTableType); + auto valueAddr = builder.create(location, mlir_ts::ValueRefType::get(withVTableType), builder.getBoolAttr(false)); + builder.create(location, withVTable, valueAddr); + auto inCasted = builder.create(location, objType, valueAddr); + + auto vtableStorage = builder.create(location, mlir_ts::RefType::get(vtableType), inCasted, + (int)fields.size() - 1); + + return castObjectToInterface(location, inCasted, objType, interfaceInfo, genContext, vtableStorage); + } + ValueOrLogicalResult MLIRGenImpl::castObjectToInterface(mlir::Location location, mlir::Value in, mlir_ts::ObjectType objType, - InterfaceInfo::TypePtr interfaceInfo, const GenContext &genContext) + InterfaceInfo::TypePtr interfaceInfo, const GenContext &genContext, mlir::Value vtableStorage) { auto inEffective = in; auto effectiveObjType = objType; @@ -2364,7 +2454,21 @@ namespace mlirgen } } - auto result = mlirGenCreateInterfaceVTableForObject(location, inEffective, effectiveObjType, interfaceInfo, genContext); + // a boxed object literal's block is not made by this cast, so it has no room for a vtable of its own: + // its fields are copied into one that has (castTupleWithOwnVTableToInterface) + if (!vtableStorage) + { + if (auto storageTuple = dyn_cast(effectiveObjType.getStorageType())) + { + if (!getOptionalFieldSlots(storageTuple, interfaceInfo).empty()) + { + CAST_A(unboxed, location, storageTuple, inEffective, genContext); + return castTupleWithOwnVTableToInterface(location, unboxed, storageTuple, interfaceInfo, genContext); + } + } + } + + auto result = mlirGenCreateInterfaceVTableForObject(location, inEffective, effectiveObjType, interfaceInfo, genContext, vtableStorage); EXIT_IF_FAILED_OR_NO_VALUE(result) auto createdInterfaceVTableForObject = V(result); diff --git a/tslang/lib/TypeScript/MLIRGenExpressions.cpp b/tslang/lib/TypeScript/MLIRGenExpressions.cpp index 05c456b74..6c90a8c14 100644 --- a/tslang/lib/TypeScript/MLIRGenExpressions.cpp +++ b/tslang/lib/TypeScript/MLIRGenExpressions.cpp @@ -1583,8 +1583,24 @@ namespace mlirgen llvm::SmallVector destTupleFields; if (mlir::succeeded(mth.getFields(oli.receiverType, destTupleFields))) { - auto tupleType = getTupleType(destTupleFields); - return V(builder.create(location, tupleType)); + // `{}` does not have an optional member: an interface's `?` member is left out (an absent + // member reads as undefined), and a tuple with a field that admits undefined is built by the + // cast of the empty literal below, which gives such a field undefined rather than undef + auto isInterface = isa(oli.receiverType); + if (isInterface) + { + llvm::erase_if(destTupleFields, [](const mlir_ts::FieldInfo &fieldInfo) { return fieldInfo.isConditional; }); + } + + auto hasAbsentableField = !isInterface && llvm::any_of(destTupleFields, [](const mlir_ts::FieldInfo &fieldInfo) { + return isa(fieldInfo.type) || isa(fieldInfo.type); + }); + + if (!hasAbsentableField) + { + auto tupleType = getTupleType(destTupleFields); + return V(builder.create(location, tupleType)); + } } } } diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 8a2b12ce3..9423397d3 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -9609,6 +9609,27 @@ class MLIRGenImpl EXIT_IF_FAILED_OR_NO_VALUE(result) auto tupleValue = V(result); + // `...opt` of a `v?: T` field: spread T's fields - with their defaults when it has no value, as for + // a plain T field before `?` made it optional (`{...undefined}` adds no field, which a fixed + // literal type cannot express) + if (auto optType = dyn_cast(tupleValue.getType())) + { + auto hasValue = builder.create(location, getBooleanType(), tupleValue); + auto ifOp = builder.create(location, optType.getElementType(), hasValue, true); + { + mlir::OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToStart(&ifOp.getThenRegion().front()); + auto value = builder.create(location, optType.getElementType(), tupleValue); + builder.create(location, mlir::ValueRange{value}); + + builder.setInsertionPointToStart(&ifOp.getElseRegion().front()); + auto defaultValue = builder.create(location, optType.getElementType()); + builder.create(location, mlir::ValueRange{defaultValue}); + } + + tupleValue = ifOp.getResult(0); + } + LLVM_DEBUG(llvm::dbgs() << "\n!! SpreadAssignment value: " << tupleValue << "\n";); auto tupleFields = [&] (::llvm::ArrayRef fields) -> mlir::LogicalResult { @@ -10697,8 +10718,15 @@ class MLIRGenImpl InterfaceInfo::TypePtr newInterfacePtr, const GenContext &genContext); - ValueOrLogicalResult mlirGenCreateInterfaceVTableForObject(mlir::Location location, mlir::Value in, - mlir_ts::ObjectType objectType, InterfaceInfo::TypePtr newInterfacePtr, const GenContext &genContext); + ValueOrLogicalResult mlirGenCreateInterfaceVTableForObject(mlir::Location location, mlir::Value in, + mlir_ts::ObjectType objectType, InterfaceInfo::TypePtr newInterfacePtr, const GenContext &genContext, + mlir::Value vtableStorage = mlir::Value()); + + SmallVector> getOptionalFieldSlots(mlir_ts::TupleType storeType, InterfaceInfo::TypePtr newInterfacePtr); + + mlir::Value patchAbsentOptionalFieldSlots(mlir::Location location, mlir::Value in, mlir_ts::ObjectType objectType, + InterfaceInfo::TypePtr newInterfacePtr, mlir::Value vtableRef, bool ownsVTable, mlir::Value vtableStorage, + const GenContext &genContext); StringRef interfaceVTableNameForClass(ClassInfo::TypePtr newClassPtr, InterfaceInfo::TypePtr newInterfacePtr) { @@ -11552,6 +11580,8 @@ class MLIRGenImpl ValueOrLogicalResult selectFieldsValues(mlir::Location location, SmallVector &values, mlir::Value value, ::llvm::ArrayRef<::mlir::typescript::FieldInfo> fields, bool filterSpecialCases, const GenContext &genContext, bool errorAsWarning = false); + mlir::Value absentFieldValue(mlir::Location location, mlir::Type fieldType, const GenContext &genContext); + // TODO: needs to unified with selectFieldsValues ValueOrLogicalResult mapTupleToFields(mlir::Location location, SmallVector &values, mlir::Value value, mlir_ts::TupleType srcTupleType, ::llvm::ArrayRef<::mlir::typescript::FieldInfo> fields, bool filterSpecialCases, const GenContext &genContext, bool errorAsWarning = false); @@ -11651,6 +11681,10 @@ class MLIRGenImpl mlir_ts::InterfaceType interfaceType, const GenContext &genContext); ValueOrLogicalResult castObjectToInterface(mlir::Location location, mlir::Value in, mlir_ts::ObjectType objType, + InterfaceInfo::TypePtr interfaceInfo, const GenContext &genContext, + mlir::Value vtableStorage = mlir::Value()); + + ValueOrLogicalResult castTupleWithOwnVTableToInterface(mlir::Location location, mlir::Value in, mlir_ts::TupleType tupleType, InterfaceInfo::TypePtr interfaceInfo, const GenContext &genContext); mlir_ts::CreateBoundFunctionOp createBoundMethodFromExtensionMethod(mlir::Location location, mlir_ts::CreateExtensionFunctionOp createExtentionFunction); diff --git a/tslang/lib/TypeScript/MLIRGenInterfaces.cpp b/tslang/lib/TypeScript/MLIRGenInterfaces.cpp index 34de958b9..ebeb39c4a 100644 --- a/tslang/lib/TypeScript/MLIRGenInterfaces.cpp +++ b/tslang/lib/TypeScript/MLIRGenInterfaces.cpp @@ -181,13 +181,14 @@ namespace mlirgen } ValueOrLogicalResult MLIRGenImpl::mlirGenCreateInterfaceVTableForObject(mlir::Location location, mlir::Value in, - mlir_ts::ObjectType objectType, InterfaceInfo::TypePtr newInterfacePtr, const GenContext &genContext) + mlir_ts::ObjectType objectType, InterfaceInfo::TypePtr newInterfacePtr, const GenContext &genContext, + mlir::Value vtableStorage) { auto fullObjectInterfaceVTableFieldName = interfaceVTableNameForObject(objectType, newInterfacePtr); auto existValue = resolveFullNameIdentifier(location, fullObjectInterfaceVTableFieldName, true, genContext); if (existValue) { - return existValue; + return patchAbsentOptionalFieldSlots(location, in, objectType, newInterfacePtr, existValue, false, vtableStorage, genContext); } if (mlir::succeeded( @@ -255,7 +256,7 @@ namespace mlirgen { // every method slot is already a compile-time constant - no per-cast // heap allocation needed, same footing as a method-less interface. - return globalVTableRefValue; + return patchAbsentOptionalFieldSlots(location, in, objectType, newInterfacePtr, globalVTableRefValue, false, vtableStorage, genContext); } // match VTable @@ -300,15 +301,121 @@ namespace mlirgen } // patched VTable - return V(varVTable); + return patchAbsentOptionalFieldSlots(location, in, objectType, newInterfacePtr, varVTable, true, vtableStorage, genContext); } - return globalVTableRefValue; + return patchAbsentOptionalFieldSlots(location, in, objectType, newInterfacePtr, globalVTableRefValue, false, vtableStorage, genContext); } return mlir::failure(); } + // {field index in the object, slot index in the vtable} of each `?` member the object holds as optional + SmallVector> MLIRGenImpl::getOptionalFieldSlots(mlir_ts::TupleType storeType, InterfaceInfo::TypePtr newInterfacePtr) + { + SmallVector> optionalSlots; + for (auto [index, fieldInfo] : llvm::enumerate(storeType.getFields())) + { + auto optionalType = dyn_cast(fieldInfo.type); + if (!optionalType) + { + continue; + } + + auto vtableOffset = 0; + auto interfaceField = newInterfacePtr->findField(fieldInfo.id, vtableOffset); + if (!interfaceField || !interfaceField->isConditional + || mth.stripLiteralType(interfaceField->type) != mth.stripLiteralType(optionalType.getElementType())) + { + continue; + } + + optionalSlots.push_back({(int)index, interfaceField->virtualIndex + vtableOffset + INTERFACE_VTABLE_HEADER_SLOTS}); + } + + return optionalSlots; + } + + // A `?` member matched by an optional field (`v?: T` of a type literal) has a slot holding the field's + // offset - optional holds its T first, which is what the member is read as. Whether the field has a value + // is known only at run time, so a field without one gets its slot set to -1 in a copy of the vtable made for + // this object: the member is then absent and reads as undefined, as for an object without the field. The slot + // is decided at the cast: a value given to the field later is not seen through the interface. + // The copy goes to vtableStorage when the cast provides one - a field of the object's own block + // (castTupleToInterface), so it is freed with the object; otherwise to a block of its own, which nothing frees. + mlir::Value MLIRGenImpl::patchAbsentOptionalFieldSlots(mlir::Location location, mlir::Value in, mlir_ts::ObjectType objectType, + InterfaceInfo::TypePtr newInterfacePtr, mlir::Value vtableRef, + bool ownsVTable, mlir::Value vtableStorage, const GenContext &genContext) + { + mlir_ts::TupleType storeType; + if (auto objectStoreType = dyn_cast(objectType.getStorageType())) + { + storeType = mlir_ts::TupleType::get(builder.getContext(), objectStoreType.getFields()); + } + else + { + storeType = dyn_cast(mth.convertConstTupleTypeToTupleType(objectType.getStorageType())); + } + + if (!storeType) + { + return vtableRef; + } + + auto optionalSlots = getOptionalFieldSlots(storeType, newInterfacePtr); + if (optionalSlots.empty()) + { + return vtableRef; + } + + auto vtableType = mlir::cast(mlir::cast(vtableRef.getType()).getElementType()); + + auto varVTable = vtableRef; + if (!ownsVTable && vtableStorage) + { + auto valueVTable = builder.create(location, vtableType, vtableRef); + builder.create(location, valueVTable, vtableStorage); + varVTable = vtableStorage.getType() == vtableRef.getType() + ? vtableStorage + : builder.create(location, vtableRef.getType(), vtableStorage).getResult(); + } + else if (!ownsVTable) + { + // on the heap for the same reason as the method-patched copy above + auto valueVTable = builder.create(location, vtableType, vtableRef); + auto heapVTable = builder.create(location, mlir_ts::ValueRefType::get(vtableType), builder.getBoolAttr(false)); + builder.create(location, valueVTable, heapVTable); + varVTable = builder.create(location, vtableRef.getType(), heapVTable); + } + + for (auto [fieldIndex, slotIndex] : optionalSlots) + { + auto fieldType = storeType.getFieldInfo(fieldIndex).type; + auto fieldRef = builder.create(location, mlir_ts::RefType::get(fieldType), in, fieldIndex); + auto fieldValue = builder.create(location, fieldType, fieldRef); + auto hasValue = builder.create(location, getBooleanType(), fieldValue); + + auto ifOp = builder.create(location, hasValue, true); + + mlir::OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToStart(&ifOp.getElseRegion().front()); + + auto slotType = vtableType.getFieldInfo(slotIndex).type; + auto slotRef = builder.create(location, mlir_ts::RefType::get(slotType), varVTable, slotIndex); + auto ptrIntType = builder.getIntegerType(compileOptions.sizeBits()); + auto negative1 = builder.create(location, ptrIntType, builder.getIntegerAttr(ptrIntType, -1)); + auto absentSlot = cast(location, slotType, negative1, genContext); + if (absentSlot.failed()) + { + return vtableRef; + } + + builder.create(location, V(absentSlot), slotRef); + } + + return varVTable; + } + // The `.instanceOf` of an object literal cast to an interface: it is an instance of no class. // One per module, private, with the same signature as a class's `.instanceOf` reached through // a vtable (`this`, the asked class's rtti) - see INTERFACE_VTABLE_HEADER_SLOTS. diff --git a/tslang/lib/TypeScript/MLIRGenTypes.cpp b/tslang/lib/TypeScript/MLIRGenTypes.cpp index 81a4b7d2e..d5febf363 100644 --- a/tslang/lib/TypeScript/MLIRGenTypes.cpp +++ b/tslang/lib/TypeScript/MLIRGenTypes.cpp @@ -2649,6 +2649,13 @@ namespace mlirgen auto type = mcl.getEffectiveFunctionTypeForTupleField(originalType); assert(type); + + // `v?: T` is `v: T | undefined`: an absent field reads as undefined, not as T's zero + if (propertySignature->questionToken) + { + type = getUnionType(loc(propertySignature), type, getUndefinedType()); + } + types.push_back({TupleFieldName(propertySignature->name, genContext), type, false, mlir_ts::AccessLevel::Public}); } else if (kind == SyntaxKind::MethodSignature) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index f570460d0..0a625dbd1 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -393,6 +393,7 @@ tslang_add_test(NAME test-compile-00-union-value-equality COMMAND test-runner "$ tslang_add_test(NAME test-compile-00-strict-equals-mismatched-types COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00strict_equals_mismatched_types.ts") tslang_add_test(NAME test-compile-00-length-with-negative-merge COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00length_with_negative_merge.ts") tslang_add_test(NAME test-compile-00-length-signed-arithmetic COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00length_signed_arithmetic.ts") +tslang_add_test(NAME test-compile-00-optional-fields COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional_fields.ts") tslang_add_test(NAME test-compile-00-intersection-type COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00intersection_type.ts") tslang_add_test(NAME test-compile-00-intersection-type-generic COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00intersection_type_generic.ts") tslang_add_test(NAME test-compile-00-length COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00length.ts") @@ -869,6 +870,7 @@ tslang_add_test(NAME test-jit-00-union-value-equality COMMAND test-runner -jit " tslang_add_test(NAME test-jit-00-strict-equals-mismatched-types COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00strict_equals_mismatched_types.ts") tslang_add_test(NAME test-jit-00-length-with-negative-merge COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00length_with_negative_merge.ts") tslang_add_test(NAME test-jit-00-length-signed-arithmetic COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00length_signed_arithmetic.ts") +tslang_add_test(NAME test-jit-00-optional-fields COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional_fields.ts") tslang_add_test(NAME test-jit-00-union-global-init COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_global_init.ts") tslang_add_test(NAME test-jit-00-intersection-type COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00intersection_type.ts") tslang_add_test(NAME test-jit-00-intersection-type-generic COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00intersection_type_generic.ts") diff --git a/tslang/test/tester/tests/00optional_fields.ts b/tslang/test/tester/tests/00optional_fields.ts new file mode 100644 index 000000000..d155e7d20 --- /dev/null +++ b/tslang/test/tester/tests/00optional_fields.ts @@ -0,0 +1,104 @@ +// A `?` field is `T | undefined`: absent, it reads as undefined - not as T's zero - read directly, through +// an interface, after `{}`, and spread +type Options = { label?: string, size?: number, extra?: any }; + +interface IOptions { + label?: string; + size?: number; +} + +interface ITagged extends IOptions { + tag?: string; +} + +type Tagged = { label?: string, tag?: string, kind: string }; + +function typeLiteral() { + const none: Options = {}; + assert(none.label === undefined, "{}.label"); + assert(none.size === undefined, "{}.size"); + assert(none.extra === undefined, "{}.extra"); + + const some: Options = { label: "x" }; + assert(some.label == "x", "label"); + assert(some.size === undefined, "absent size"); + assert((some.size ?? -1) == -1, "size ?? -1"); + + let changed: Options = {}; + changed.size = 2.5; + assert(changed.size == 2.5, "set size"); + changed.size = undefined; + assert(changed.size === undefined, "unset size"); + + if (some.label) { + assert(some.label.length == 1, "narrowed label"); + } +} + +function readThrough(o: IOptions, label: string, size: number) { + assert(label == "-" ? o.label === undefined : o.label == label, "interface label"); + assert(size < 0 ? o.size === undefined : o.size == size, "interface size"); +} + +function throughInterface() { + const empty: IOptions = {}; + assert(empty.label === undefined, "interface {}.label"); + assert(empty.size === undefined, "interface {}.size"); + + readThrough({}, "-", -1); + readThrough({ label: "l" }, "l", -1); + + // a tuple with `?` fields: the interface reads the value when it is there, undefined when it is not + const none: Options = {}; + readThrough(none, "-", -1); + const some: Options = { label: "v", size: 1.5 }; + readThrough(some, "v", 1.5); + + const viaInterface: IOptions = some; + viaInterface.label = "w"; + assert(viaInterface.label == "w", "write through interface"); + + const all: IOptions[] = [some, none, { size: 2.5 }]; + assert(all[1].label === undefined && all[2].size == 2.5, "array of interfaces"); +} + +function extended() { + const withTag: Tagged = { kind: "k", tag: "t" }; + const t: ITagged = withTag; + assert(t.tag == "t", "extends: own member"); + assert(t.label === undefined, "extends: inherited absent member"); + + const withLabel: Tagged = { kind: "k", label: "l" }; + const l: ITagged = withLabel; + assert(l.label == "l", "extends: inherited member"); + assert(l.tag === undefined, "extends: own absent member"); +} + +interface IWithMethod { + label?: string; + name(): string; +} + +function withMethod() { + const o = { label: undefined as string | undefined, name() { return "n"; } }; + const m: IWithMethod = o; + assert(m.label === undefined, "method-bearing object: absent"); + assert(m.name() == "n", "method-bearing object: method"); +} + +type Descriptor = { data?: D }; + +function spread() { + const d: Descriptor<{ x: number }> = { data: { x: 1.5 } }; + const copy = { ...d.data }; + assert(copy.x == 1.5, "spread optional"); +} + +function main() { + typeLiteral(); + throughInterface(); + extended(); + withMethod(); + spread(); + print("done."); +} diff --git a/tslang/test/tester/tests/export_type_alias.ts b/tslang/test/tester/tests/export_type_alias.ts index 86487c6dd..ce0d7a32c 100644 --- a/tslang/test/tester/tests/export_type_alias.ts +++ b/tslang/test/tester/tests/export_type_alias.ts @@ -19,6 +19,16 @@ namespace M { export type Status = number | string; + // a `?` field is `T | undefined` on both sides of the module boundary + export type Options = { + label?: string; + size?: number; + }; + + export function labelOf(o: Options): string { + return o.label ?? "none"; + } + export function makePoint(x: number, y: number): Point { return { x: x, y: y }; } diff --git a/tslang/test/tester/tests/import_type_alias.ts b/tslang/test/tester/tests/import_type_alias.ts index f0598b933..520854a2a 100644 --- a/tslang/test/tester/tests/import_type_alias.ts +++ b/tslang/test/tester/tests/import_type_alias.ts @@ -16,5 +16,11 @@ function main() { s = "error"; assert(s == "error"); + const none: M.Options = {}; + assert(none.label === undefined); + assert(none.size === undefined); + assert(M.labelOf(none) == "none"); + assert(M.labelOf({ label: "l" }) == "l"); + print("done."); }