diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 5b2b39b24..ae9c3f86e 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -159,6 +159,14 @@ // 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. #define INTERFACE_OPTIONAL_STORAGE_TAG(sizeBits) (1LL << ((sizeBits) - 2)) +// A function-typed interface field (`fn: () => int`) a class fills with a method (`fn(): int`) has +// no storage in the object: its slot is instead the address of a global cell holding the method, +// plus this bit. An access reads the cell (InterfaceSymbolRefOpLowering), then binds `this` as for +// any function-typed field. The bit is free in every other function-typed slot: an offset of a +// pointer-sized field (or of optional, which starts with one) is pointer-aligned, and so +// is the cell; -1 (absent) is tested before it. A write through such a field changes the cell, +// so it changes the method for every instance of the class seen through that interface. +#define INTERFACE_METHOD_CELL_TAG 1 // on an InterfaceSymbolRefOp of a `v?: T` field that is stored to: true sets the flag of a tagged slot's // field, false (a store of undefined) clears it #define INTERFACE_OPTIONAL_WRITE_ATTR "__optional_write" diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 5251e7310..8ec50fb75 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -6475,6 +6475,30 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(interfaceSymbolRefOp.getType()); + auto isFunctionField = fieldRefType && isa(fieldRefType.getElementType()); + auto orMethodCell = [&](std::function offsetAddrFunc) { + return [&, offsetAddrFunc](OpBuilder &builder, Location location) -> mlir::Value { + auto intPtrType = rewriter.getIntegerType(tsLlvmContext->compileOptions.sizeBits()); + auto slotValue = rewriter.create(loc, intPtrType, methodOrFieldPtr); + auto tag = rewriter.create( + loc, intPtrType, rewriter.getIntegerAttr(intPtrType, INTERFACE_METHOD_CELL_TAG)); + auto tagBit = rewriter.create(loc, intPtrType, slotValue, tag); + auto zero = rewriter.create(loc, intPtrType, rewriter.getIntegerAttr(intPtrType, 0)); + auto isMethodCell = rewriter.create(loc, LLVM::ICmpPredicate::ne, tagBit, zero); + auto methodCellAddrFunc = [&](OpBuilder &builder, Location location) -> mlir::Value { + auto cellInt = rewriter.create(loc, intPtrType, slotValue, tag); + return rewriter.create(loc, fieldLLVMTypeRef, cellInt.getResult()); + }; + + return clh.conditionalExpressionLowering(loc, fieldLLVMTypeRef, isMethodCell, methodCellAddrFunc, + offsetAddrFunc); + }; + }; + mlir::Value fieldAddr; if (isOptional) { @@ -6530,10 +6554,17 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(presentOrMethodCellAddrFunc) + : mlir::function_ref(presentAddrFunc)); fieldAddr = result; } + else if (isFunctionField) + { + fieldAddr = orMethodCell(calcFieldTotalAddrFunc)(rewriter, loc); + } else { auto typedPtr = calcFieldTotalAddrFunc(rewriter, loc); diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index 79c7bd597..c855b543e 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -2352,11 +2352,87 @@ genContext); llvm::SmallVector virtualTable; // the fields stored as optional for an interface's `v?: T`: their slots are tagged llvm::SmallVector optionalStorageFields; + // the interface's function-typed fields (`fn: () => int`) the class implements with a method + // (`fn(): int`): their slots point at a cell holding the method (see INTERFACE_METHOD_CELL_TAG) + llvm::SmallVector> methodsForFields; auto result = newInterfacePtr->getVirtualTable( virtualTable, [&](mlir::Attribute id, mlir::Type fieldType, bool isConditional) -> std::pair { auto found = false; auto foundField = newClassPtr->findField(id, found); + if (!found && isa(fieldType)) + { + auto fieldName = dyn_cast(id); + auto *foundMethodPtr = fieldName ? newClassPtr->findMethod(fieldName.getValue()) : nullptr; + if (foundMethodPtr && !foundMethodPtr->isStatic) + { + // the method's first input is its `this`, the field's type has none + auto match = mth.TestFunctionTypesMatchWithObjectMethods( + location, foundMethodPtr->funcType, mlir::cast(fieldType), 1, 0); + if (match.result != MatchResultType::Match) + { + emitError(location) << "method signature not matching '" << fieldName.getValue() << ":" + << to_print(fieldType) << "' for interface '" << newInterfacePtr->fullName + << "' in class '" << newClassPtr->fullName << "'." + << " Found method: " << fieldName.getValue() << ":" + << to_print(foundMethodPtr->funcType); + return {emptyFieldInfo, mlir::failure()}; + } + + // a field reached twice through a diamond `extends` has two slots and one cell + if (llvm::none_of(methodsForFields, [&](auto &methodForField) { return methodForField.first == id; })) + { + methodsForFields.push_back({id, *foundMethodPtr}); + } + + return {mlir_ts::FieldInfo{id, fieldType, false, mlir_ts::AccessLevel::Public}, mlir::success()}; + } + } + + // a class field holding a function (`fn: () => int = ...`) stores a hybrid function - + // {function, `this` or null} - or, unannotated (`fn = () => 7`), a plain one: neither can be handed out + // as a function taking the object as its `this`. A thunk calling `this.fn(...)` fills the + // slot as a method does; it reads the field at each call, so a field assigned later is seen. + // Not an optional field (`fn?: () => int`): the cell is shared, so the slot would read as present + // on every instance, set or not; that stays "field type not matching" below. + if (found && isa(fieldType) && fieldType != foundField.type + && !isa(foundField.type) && mth.isAnyFunctionType(foundField.type)) + { + auto fieldName = dyn_cast(id); + auto match = mth.TestFunctionTypesMatchWithObjectMethods(location, foundField.type, fieldType); + if (!fieldName || match.result != MatchResultType::Match) + { + emitError(location) << "field type not matching for " << id << ":" << to_print(fieldType) + << " for interface '" << newInterfacePtr->fullName << "' in class '" + << newClassPtr->fullName << "'. Found field: " << id << ":" + << to_print(foundField.type); + return {emptyFieldInfo, mlir::failure()}; + } + + // a field reached twice through a diamond `extends` has two slots and one cell + if (llvm::none_of(methodsForFields, [&](auto &methodForField) { return methodForField.first == id; })) + { + auto funcType = mlir::cast(fieldType); + auto thunkName = concat(interfaceVTableNameForClass(newClassPtr, newInterfacePtr), + fieldName.getValue(), "thunk"); + if (mlir::failed(generateSynthMethodToCallField(location, newClassPtr, fieldName.getValue(), + thunkName, funcType, genContext))) + { + return {emptyFieldInfo, mlir::failure()}; + } + + MethodInfo thunkInfo{}; + thunkInfo.name = thunkName.str(); + thunkInfo.funcType = funcType; + thunkInfo.funcName = thunkName.str(); + thunkInfo.virtualIndex = -1; + thunkInfo.accessLevel = mlir_ts::AccessLevel::Public; + methodsForFields.push_back({id, thunkInfo}); + } + + return {mlir_ts::FieldInfo{id, fieldType, false, mlir_ts::AccessLevel::Public}, mlir::success()}; + } + // a class's `v?: T` is stored as optional; the interface's `v?: T` reads it by its flag // (a tagged slot, see isStoredAsOptional below) auto optionalStorage = found && isConditional && isa(foundField.type) @@ -2475,8 +2551,73 @@ genContext); return owner && owner->isDynamicImport; }); + // the symbol of a method implementing an interface slot - resolved at runtime when the method is owned + // by a dynamically imported class (`location` is then UnknownLoc, see needsRuntimeSymbolResolution) + auto methodSymbolValue = [&](mlir::Location location, MethodInfo &methodInfo, const GenContext &genContext) -> mlir::Value { + auto owner = findMethodOwner(newClassPtr, methodInfo.funcName); + if (owner && owner->isDynamicImport) + { + // `location` is already UnknownLoc here (see + // needsRuntimeSymbolResolution above, computed from this same + // owner->isDynamicImport condition) - the resulting + // SearchForAddressOfSymbolOp forces this whole global's + // initializer into a synthesized __cctor function via + // GlobalOpLowering, and a "live" (main-scoped) location on any op + // living there would attach the wrong DISubprogram to it. + auto symbolNameValue = V(mlirGenStringValue(location, + methodInfo.dllName.empty() ? methodInfo.funcName : methodInfo.dllName, true)); + auto referenceToSymbolOpaque = builder.create( + location, getOpaqueType(), symbolNameValue); + auto castResult = cast(location, methodInfo.funcType, referenceToSymbolOpaque, genContext); + if (castResult.failed_or_no_value()) + { + return mlir::Value(); + } + + return V(castResult); + } + + return builder.create( + location, methodInfo.funcType, + mlir::FlatSymbolRefAttr::get(builder.getContext(), methodInfo.funcName)); + }; + // register global auto fullClassInterfaceVTableFieldName = interfaceVTableNameForClass(newClassPtr, newInterfacePtr); + + // one cell per method filling a function-typed field: the field's slot holds its address + // (tagged), since the object itself has no field holding the method. The variable keeps the + // name it is registered with, so the name is allocated like the vtable's own. + auto methodCellName = [&](mlir::Attribute id) { + return concat(fullClassInterfaceVTableFieldName, mlir::cast(id).getValue(), "method"); + }; + + for (auto &[id, methodInfo] : methodsForFields) + { + auto owner = findMethodOwner(newClassPtr, methodInfo.funcName); + auto cellType = registerVariable( + location, methodCellName(id), true, VariableType::Var, + [&](mlir::Location location, const GenContext &genContext) { + if (owner && owner->isDynamicImport) + { + location = mlir::UnknownLoc::get(builder.getContext()); + } + + auto methodValue = methodSymbolValue(location, methodInfo, genContext); + if (!methodValue) + { + return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; + } + + return TypeValueInitType{methodInfo.funcType, methodValue, TypeProvided::Yes}; + }, + genContext); + if (!cellType) + { + return mlir::failure(); + } + } + auto registeredType = registerVariable( location, fullClassInterfaceVTableFieldName, true, VariableType::Var, [&](mlir::Location location, const GenContext &genContext) { @@ -2529,6 +2670,33 @@ genContext); location, virtTuple, castedPtr, vtableValue, MLIRHelper::getStructIndex(builder, fieldIndex)); } + else if (methodOrField.isField && llvm::any_of(methodsForFields, [&](auto &methodForField) { + return methodForField.first == methodOrField.fieldInfo.id; + })) + { + // a method filling a function-typed field: the slot is the address of its cell plus + // INTERFACE_METHOD_CELL_TAG, so an access reads the cell instead of `this` + offset + // (InterfaceSymbolRefOpLowering) + auto cellRef = resolveFullNameIdentifier(location, methodCellName(methodOrField.fieldInfo.id), true, genContext); + if (!cellRef) + { + return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; + } + + auto ptrIntType = builder.getIntegerType(compileOptions.sizeBits()); + auto cellAddress = builder.create(location, ptrIntType, cellRef); + auto tagValue = builder.create( + location, ptrIntType, builder.getIntegerAttr(ptrIntType, INTERFACE_METHOD_CELL_TAG)); + auto taggedValue = builder.create( + location, ptrIntType, builder.getI32IntegerAttr((int)SyntaxKind::PlusToken), cellAddress, + tagValue); + auto slotValue = builder.create( + location, mlir_ts::RefType::get(methodOrField.fieldInfo.type), taggedValue); + + vtableValue = builder.create( + location, virtTuple, slotValue, vtableValue, + MLIRHelper::getStructIndex(builder, fieldIndex)); + } else if (methodOrField.isField) { auto nullObj = builder.create(location, getNullType()); @@ -2581,35 +2749,10 @@ genContext); // "not a link-time constant, resolve at runtime" situation // mlirGenClassVirtualTableDefinition already handles for its own // (non-interface) vtable slots, just not previously mirrored here. - auto owner = findMethodOwner(newClassPtr, methodOrField.methodInfo.funcName); - mlir::Value methodValueRef; - if (owner && owner->isDynamicImport) + auto methodValueRef = methodSymbolValue(location, methodOrField.methodInfo, genContext); + if (!methodValueRef) { - // `location` is already UnknownLoc here (see - // needsRuntimeSymbolResolution above, computed from this same - // owner->isDynamicImport condition) - the resulting - // SearchForAddressOfSymbolOp forces this whole global's - // initializer into a synthesized __cctor function via - // GlobalOpLowering, and a "live" (main-scoped) location on any op - // living there would attach the wrong DISubprogram to it. - auto symbolNameValue = V(mlirGenStringValue(location, - methodOrField.methodInfo.dllName.empty() ? methodOrField.methodInfo.funcName : methodOrField.methodInfo.dllName, true)); - auto referenceToSymbolOpaque = builder.create( - location, getOpaqueType(), symbolNameValue); - auto castResult = cast(location, methodOrField.methodInfo.funcType, referenceToSymbolOpaque, genContext); - if (castResult.failed_or_no_value()) - { - return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; - } - - methodValueRef = V(castResult); - } - else - { - methodValueRef = builder.create( - location, methodOrField.methodInfo.funcType, - mlir::FlatSymbolRefAttr::get(builder.getContext(), - methodOrField.methodInfo.funcName)); + return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; } vtableValue = builder.create( diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index e47d0deba..1fcb3b174 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -8247,10 +8247,36 @@ class MLIRGenImpl // if it is FuncType, we need to create BoundMethod again if (auto funcType = dyn_cast(fieldInfo->type)) { - auto thisVal = - builder.create(location, getOpaqueType(), interfaceValue); - value = builder.create(location, getBoundFunctionType(funcType), - thisVal, value); + auto bindThis = [&](mlir::Value value) -> mlir::Value { + auto thisVal = + builder.create(location, getOpaqueType(), interfaceValue); + value = builder.create(location, getBoundFunctionType(funcType), + thisVal, value); + return value; + }; + + // an optional field (`fn?: () => int`) loads as optional: bound when present, undefined when not + if (auto optionalType = dyn_cast(value.getType())) + { + auto optionalValue = value; + auto hasValue = builder.create(location, getBooleanType(), optionalValue); + MLIRCodeLogicHelper mclh(builder, location, compileOptions); + auto result = mclh.conditionalValue( + hasValue, + [&]() -> ValueOrLogicalResult { + auto funcValue = + builder.create(location, optionalType.getElementType(), optionalValue); + auto boundFuncValue = bindThis(funcValue); + return V(builder.create( + location, getOptionalType(boundFuncValue.getType()), boundFuncValue)); + }, + [&](mlir::Type trueValueType) -> ValueOrLogicalResult { + return V(builder.create(location, trueValueType)); + }); + return V(result); + } + + value = bindThis(value); } return value; @@ -12130,6 +12156,72 @@ class MLIRGenImpl return fullClassStaticName.str(); } + // `(this, p0..pn) { return this.(p0..pn); }`, typed as the interface's + // function-typed field (`this` opaque): the method an interface slot calls for a class whose + // field of that name holds the function (see mlirGenClassVirtualTableDefinitionForInterface) + mlir::LogicalResult generateSynthMethodToCallField(mlir::Location location, ClassInfo::TypePtr newClassPtr, + StringRef fieldName, StringRef thunkName, + mlir_ts::FunctionType funcType, const GenContext &genContext) + { + auto retType = mth.getReturnTypeFromFuncRef(funcType); + + mlir::OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToStart(theModule.getBody()); + + GenContext funcGenContext(genContext); + funcGenContext.clearScopeVars(); + funcGenContext.thisType = newClassPtr->classType; + funcGenContext.thisClassType = newClassPtr->classType; + funcGenContext.disableSpreadParams = true; + + return mlirGenFunctionBody( + location, fieldName, thunkName, funcType, + [&](mlir::Location location, const GenContext &genContext) -> mlir::LogicalResult { + // the interface passes the object as an opaque `this` + auto funcOp = genContext.funcOp; + auto opaqueThis = funcOp.getBody().front().getArgument(0); + auto thisValue = builder.create(location, newClassPtr->classType, opaqueThis); + auto thisDecl = std::make_shared(THIS_NAME, newClassPtr->classType, location); + DECLARE(thisDecl, thisValue); + + NodeFactory nf(NodeFactoryFlags::None); + + NodeArray argumentsArray; + for (auto index = 1; index < funcType.getNumInputs(); index++) + { + std::string paramName("p"); + paramName += std::to_string(index - 1); + argumentsArray.push_back(nf.createIdentifier(stows(paramName))); + } + + auto fieldAccess = nf.createPropertyAccessExpression( + nf.createToken(SyntaxKind::ThisKeyword), nf.createIdentifier(stows(fieldName.str()))); + auto callResult = mlirGen(nf.createCallExpression(fieldAccess, undefined, argumentsArray), genContext); + EXIT_IF_FAILED(callResult); + + if (!retType || isa(retType)) + { + return mlir::success(); + } + + auto castToRet = cast(location, retType, V(callResult), genContext); + EXIT_IF_FAILED_OR_NO_VALUE(castToRet); + auto retVarInfo = symbolTable.lookup(RETURN_VARIABLE_NAME); + if (!retVarInfo.second) + { + return mlir::failure(); + } + + // built op by op, so the retain a `return` performs is repeated here (as in + // generateSynthMethodToCallNewCtor) + auto returnValue = V(castToRet); + mlirGenRetainCaptured(location, mlir::ValueRange{returnValue}); + builder.create(location, returnValue, retVarInfo.first); + return mlir::success(); + }, + funcGenContext, 1/*to skip This*/); + } + mlir::LogicalResult mlirGenClassBaseInterfaces(mlir::Location location, ClassInfo::TypePtr newClassPtr, const GenContext &genContext); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 175c8f916..c90517699 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -611,6 +611,9 @@ tslang_add_test(NAME test-compile-00-interface-partial COMMAND test-runner "${PR tslang_add_test(NAME test-compile-00-interface-optional COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional.ts") tslang_add_test(NAME test-compile-00-interface-optional-cast-order COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_cast_order.ts") tslang_add_test(NAME test-compile-00-interface-function-typed-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field.ts") +tslang_add_test(NAME test-compile-00-interface-function-typed-field-class COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field_class.ts") +tslang_add_test(NAME test-compile-00-interface-function-typed-field-class-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field_class_field.ts") +tslang_add_test(NAME test-compile-00-interface-optional-function-typed-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_function_typed_field.ts") tslang_add_test(NAME test-compile-00-interface-captures COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_captures.ts") tslang_add_test(NAME test-compile-00-object-annotated-method COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method.ts") tslang_add_test(NAME test-compile-00-object-annotated-method-params COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_params.ts") @@ -1164,6 +1167,9 @@ tslang_add_test(NAME test-jit-00-interface-partial COMMAND test-runner -jit "${P tslang_add_test(NAME test-jit-00-interface-optional COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional.ts") tslang_add_test(NAME test-jit-00-interface-optional-cast-order COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_cast_order.ts") tslang_add_test(NAME test-jit-00-interface-function-typed-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field.ts") +tslang_add_test(NAME test-jit-00-interface-function-typed-field-class COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field_class.ts") +tslang_add_test(NAME test-jit-00-interface-function-typed-field-class-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field_class_field.ts") +tslang_add_test(NAME test-jit-00-interface-optional-function-typed-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_function_typed_field.ts") tslang_add_test(NAME test-jit-00-interface-captures COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_captures.ts") tslang_add_test(NAME test-jit-00-object-annotated-method COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method.ts") tslang_add_test(NAME test-jit-00-object-annotated-method-params COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_params.ts") @@ -2025,6 +2031,8 @@ set(TSLANG_CORPUS 00interface_captures.ts 00interface_conjunction.ts 00interface_function_typed_field.ts + 00interface_function_typed_field_class.ts + 00interface_function_typed_field_class_field.ts 00interface_generic.ts 00interface_global_method.ts 00interface_indexer.ts @@ -2037,6 +2045,7 @@ set(TSLANG_CORPUS 00interface_object5.ts 00interface_optional_cast_order.ts 00interface_optional_extends.ts + 00interface_optional_function_typed_field.ts 00interface_optional_method_extends.ts 00interface_optional.ts 00interface_partial.ts @@ -2752,6 +2761,17 @@ set_tests_properties(test-compile-diagnostic-location-rest-parameter-type PROPERTIES PASS_REGULAR_EXPRESSION "rest_parameter_type\\.ts:6:12: error: A rest parameter must be of an array type, not 's32'" FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed|rest_parameter_type\\.ts:1:|rest_parameter_type\\.ts:4:|verification") +# A class's optional function-typed field cannot fill an interface's optional function-typed +# field yet: the method cell the slot would point at holds a thunk, present on every instance, +# so it is an error until the cell carries a per-instance presence check. +add_test(NAME test-compile-interface-optional-class-function-field + COMMAND $ --emit=obj --no-default-lib -mm=none + "${PROJECT_SOURCE_DIR}/test/tester/interface-errors/optional_class_function_field.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/interface-optional-class-function-field.obj") +set_tests_properties(test-compile-interface-optional-class-function-field + PROPERTIES PASS_REGULAR_EXPRESSION "field type not matching for \"fn\" for interface 'MaybeFn' in class 'OptField'" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") + # A call with fewer arguments than the callee's required parameters is an error (TS2554), # not padded with undef; so is one with more arguments than the callee has parameters # (too_many_*), which went silently into a slot the callee does not have (a rest parameter diff --git a/tslang/test/tester/interface-errors/optional_class_function_field.ts b/tslang/test/tester/interface-errors/optional_class_function_field.ts new file mode 100644 index 000000000..e4a5d5d35 --- /dev/null +++ b/tslang/test/tester/interface-errors/optional_class_function_field.ts @@ -0,0 +1,17 @@ +// A class's OPTIONAL function-typed field (`fn?: () => int`) cannot fill an interface's +// optional function-typed field yet: the slot would point at the shared method cell, a +// thunk calling `this.fn(...)`, so the field would read as present on every instance - +// set or not - and an unset one would be called. Until the cell carries a per-instance +// presence check it is an error, as it was before class fields could fill such a slot. + +interface MaybeFn { + fn?: () => int; +} + +class OptField { + fn?: () => int; +} + +function main() { + const m: MaybeFn = new OptField(); +} diff --git a/tslang/test/tester/tests/00interface_function_typed_field_class.ts b/tslang/test/tester/tests/00interface_function_typed_field_class.ts new file mode 100644 index 000000000..d02042c47 --- /dev/null +++ b/tslang/test/tester/tests/00interface_function_typed_field_class.ts @@ -0,0 +1,128 @@ +// regression test: a class method fills an interface member declared as a +// function-typed FIELD (`fn: () => int`). The class has no field holding the +// method, so the cast to the interface used to fail ("field type not matching +// for "fn""), which then surfaced as "not a function to call" at the caller. +// The slot now points at a cell holding the method (INTERFACE_METHOD_CELL_TAG, +// Defines.h), and reading the field binds `this` as for any function-typed field. + +interface HasFn { + fn: () => int; +} + +class C { + constructor(public v: int) {} + fn(): int { + return this.v; + } +} + +// under -mm=rc a bound method does not hold its receiver yet, so the caller keeps +// the instance alive while the returned value is used +function fromClass(c: C): () => int { + const o: HasFn = c; + return o.fn; +} + +interface Adder { + base: int; + add: (x: int) => int; +} + +class A { + constructor(public base: int) {} + add(x: int): int { + return this.base + x; + } +} + +class Base { + constructor(public v: int) {} + fn(): int { + return this.v * 2; + } +} + +class Derived extends Base { + constructor(v: int) { + super(v); + } +} + +function callFn(h: HasFn): int { + return h.fn(); +} + +// declared with `implements`, inherited from a base that declares it, and reached twice +// through a diamond `extends` (two slots, one cell) +interface P1 extends HasFn { + a: int; +} + +interface P2 extends HasFn { + b: int; +} + +interface Both extends P1, P2 {} + +class Impl implements HasFn { + constructor(public v: int) {} + fn(): int { + return this.v + 1; + } +} + +class ImplBase implements HasFn { + constructor(public v: int) {} + fn(): int { + return this.v * 3; + } +} + +class ImplDerived extends ImplBase { + constructor(v: int) { + super(v); + } +} + +class Diamond { + a = 1; + b = 2; + constructor(public v: int) {} + fn(): int { + return this.v + 100; + } +} + +function main() { + const c42 = new C(42); + const f = fromClass(c42); + print(f()); + assert(f() == 42); + + const o: HasFn = new C(5); + print(o.fn()); + assert(o.fn() == 5); + + const a: Adder = new A(10); + assert(a.base == 10); + assert(a.add(3) == 13); + const add = a.add; + assert(add(4) == 14); + + const d: HasFn = new Derived(21); + assert(d.fn() == 42); + + // one access site: a class's method cell, an object literal's field + assert(callFn(new C(1)) == 1); + assert(callFn({ v: 41, fn() { return this.v; } }) == 41); + + const i: HasFn = new Impl(6); + assert(i.fn() == 7); + const id: HasFn = new ImplDerived(4); + assert(id.fn() == 12); + const both: Both = new Diamond(5); + assert(both.fn() == 105); + assert(both.a == 1 && both.b == 2); + + print("done."); +} diff --git a/tslang/test/tester/tests/00interface_function_typed_field_class_field.ts b/tslang/test/tester/tests/00interface_function_typed_field_class_field.ts new file mode 100644 index 000000000..47a22b0f6 --- /dev/null +++ b/tslang/test/tester/tests/00interface_function_typed_field_class_field.ts @@ -0,0 +1,143 @@ +// regression test: a class FIELD holding a function fills an interface member +// declared as a function-typed field (`fn: () => int`). The class stores the +// field as a hybrid function ({function, `this` or null}), which the interface +// slot can't hand out as a function taking the object as its `this`, so the +// cast used to fail ("field type not matching for "fn""). The slot now points +// at a cell holding a thunk that calls `this.fn(...)` (INTERFACE_METHOD_CELL_TAG, +// Defines.h), so the field is read at every call. + +interface HasFn { + fn: () => int; +} + +class WithField { + fn: () => int = () => 7; +} + +// unannotated: stored as a plain function +class Unannotated { + fn = () => 6; +} + +class Capturing { + fn: () => int; + constructor(public x: int, k: int) { + this.fn = () => this.x + k; + } +} + +class BoundMethod { + fn: () => int; + constructor(public v: int) { + this.fn = this.m; + } + m(): int { + return this.v * 2; + } +} + +interface Fmt { + fmt: (a: int, s: string) => string; +} + +class FmtField { + constructor(public prefix: string) {} + fmt: (a: int, s: string) => string = (a: int, s: string) => `${s}=${a}`; +} + +let calls = 0; + +interface Sink { + put: (v: int) => void; +} + +class SinkField { + put: (v: int) => void = (v: int) => { + calls += v; + }; +} + +class Implements implements HasFn { + fn: () => int = () => 11; +} + +class FieldBase { + fn: () => int = () => 12; +} + +class FieldDerived extends FieldBase {} + +interface P1 extends HasFn { + a: int; +} + +interface P2 extends HasFn { + b: int; +} + +interface Both extends P1, P2 {} + +class Diamond { + a = 1; + b = 2; + fn: () => int = () => 13; +} + +function callFn(h: HasFn): int { + return h.fn(); +} + +// under -mm=rc a bound method does not hold its receiver yet, so the caller keeps +// the instance alive while the returned value is used +function fromField(w: WithField): () => int { + const h: HasFn = w; + return h.fn; +} + +function main() { + const h: HasFn = new WithField(); + print(h.fn()); + assert(h.fn() == 7); + + const u: HasFn = new Unannotated(); + assert(u.fn() == 6); + + const c: HasFn = new Capturing(30, 5); + assert(c.fn() == 35); + + const b: HasFn = new BoundMethod(21); + assert(b.fn() == 42); + + // the thunk reads the field at each call + const w = new WithField(); + const hw: HasFn = w; + w.fn = () => 8; + assert(hw.fn() == 8); + + const f: Fmt = new FmtField("p"); + print(f.fmt(3, "x")); + assert(f.fmt(3, "x") == "x=3"); + + const s: Sink = new SinkField(); + s.put(4); + s.put(5); + assert(calls == 9); + + const i: HasFn = new Implements(); + assert(i.fn() == 11); + const d: HasFn = new FieldDerived(); + assert(d.fn() == 12); + const both: Both = new Diamond(); + assert(both.fn() == 13); + assert(both.a == 1 && both.b == 2); + + // one access site: a class field, a class with a method, an object literal + assert(callFn(new WithField()) == 7); + assert(callFn({ fn() { return 41; } }) == 41); + + const wf = new WithField(); + const g = fromField(wf); + assert(g() == 7); + + print("done."); +} diff --git a/tslang/test/tester/tests/00interface_optional_function_typed_field.ts b/tslang/test/tester/tests/00interface_optional_function_typed_field.ts new file mode 100644 index 000000000..3ca2e6a79 --- /dev/null +++ b/tslang/test/tester/tests/00interface_optional_function_typed_field.ts @@ -0,0 +1,117 @@ +// regression test: reading an OPTIONAL function-typed interface field +// (`fn?: () => int`) as a value. The field loads as optional, and +// binding the interface's `this` to it directly failed to compile +// ('ts.CreateBoundFunction' op operand #1 must be ...). A present field now +// reads as an optional bound function, an absent one as undefined - for an +// object literal and for a class whose method fills the field +// (INTERFACE_METHOD_CELL_TAG, Defines.h) alike. +// +// A class's own optional function-typed field (`fn?: () => int`) filling the +// interface's is a compile error for now: interface-errors/optional_class_function_field.ts. + +interface MaybeFn { + fn?: () => int; +} + +function maybe(m: MaybeFn): int { + const g = m.fn; + if (g) { + return g(); + } + + return -1; +} + +function callOpt(m: MaybeFn): int { + return m.fn?.() ?? -2; +} + +function isSet(m: MaybeFn): boolean { + return m.fn !== undefined; +} + +function getFn(m: MaybeFn): (() => int) | undefined { + return m.fn; +} + +class WithFn { + constructor(public v: int) {} + fn(): int { + return this.v; + } +} + +class WithoutFn { + constructor(public v: int) {} +} + +interface MaybeAdd { + base: int; + add?: (x: int) => int; +} + +function addOr(m: MaybeAdd, x: int): int { + const add = m.add; + if (add) { + return add(x); + } + + return m.base; +} + +class Adder { + constructor(public base: int) {} + add(x: int): int { + return this.base + x; + } +} + +function main() { + // object literal + print(maybe({ fn() { return 3; } }), maybe({})); + assert(maybe({ fn() { return 3; } }) == 3); + assert(maybe({}) == -1); + assert(callOpt({ fn() { return 4; } }) == 4); + assert(callOpt({}) == -2); + assert(isSet({ fn() { return 5; } })); + assert(!isSet({})); + + // the bound value keeps the literal's `this` + assert(maybe({ v: 40, fn() { return this.v + 2; } }) == 42); + + // a class whose method fills the field, and one that lacks it + assert(maybe(new WithFn(7)) == 7); + assert(maybe(new WithoutFn(8)) == -1); + assert(callOpt(new WithFn(9)) == 9); + assert(callOpt(new WithoutFn(10)) == -2); + assert(isSet(new WithFn(11))); + assert(!isSet(new WithoutFn(12))); + + // one access site, mixed implementers + const all: MaybeFn[] = [new WithFn(1), new WithoutFn(2), { fn() { return 3; } }, {}]; + let sum = 0; + for (const m of all) { + sum += maybe(m); + } + + print(sum); + assert(sum == 1 - 1 + 3 - 1); + + // the bound value outlives the access; under -mm=rc a bound method does not hold its + // receiver yet, so the instance is kept alive while the returned value is used + const w13 = new WithFn(13); + const f = getFn(w13); + assert(f !== undefined); + if (f) { + assert(f() == 13); + } + + assert(getFn(new WithoutFn(14)) === undefined); + + // with a parameter + assert(addOr(new Adder(10), 5) == 15); + assert(addOr({ base: 20, add(x: int) { return this.base * x; } }, 2) == 40); + assert(addOr({ base: 30 }, 2) == 30); + + print("done."); +}