From b5585a57b2ffe0afdd57060bddd218da5907f6f4 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 19 Oct 2026 12:22:40 +0100 Subject: [PATCH 1/5] A class method can fill an interface's function-typed field `interface HasFn { fn: () => int }` with `class C { fn(): int }` failed at the cast (`const o: HasFn = new C(42)`): mlirGenClassVirtualTableDefinitionForInterface looked the interface's field up among the class's fields only, reported "field type not matching for "fn"", and the caller then saw the cascade "not a function to call". TS accepts a method for a function-typed property. The class has no field holding the method, so the slot can't be the usual this-relative offset. It now holds the address of a private global cell (`...vtbl..method`) holding the method, plus INTERFACE_METHOD_CELL_TAG (bit 0, free in every function-typed slot: field offsets and the cell are pointer-aligned). InterfaceSymbolRefOpLowering reads the cell when the bit is set, ahead of the optional-storage tag. The read in InterfaceFieldAccess is unchanged (Load + CreateBoundFunction over ExtractInterfaceThis), so the rc receiver tag work applies to it as is. A write through such a field changes the shared cell, i.e. the method for every instance seen through that interface. Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/Defines.h | 8 + tslang/lib/TypeScript/LowerToLLVM.cpp | 35 +++- tslang/lib/TypeScript/MLIRGenClasses.cpp | 150 ++++++++++++++---- tslang/test/tester/CMakeLists.txt | 3 + .../00interface_function_typed_field_class.ts | 76 +++++++++ 5 files changed, 242 insertions(+), 30 deletions(-) create mode 100644 tslang/test/tester/tests/00interface_function_typed_field_class.ts 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..f2ad679b3 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -2352,11 +2352,38 @@ 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()}; + } + + methodsForFields.push_back({id, *foundMethodPtr}); + 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 +2502,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 +2621,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 +2700,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/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 175c8f916..53b91af32 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -611,6 +611,7 @@ 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-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 +1165,7 @@ 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-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 +2027,7 @@ set(TSLANG_CORPUS 00interface_captures.ts 00interface_conjunction.ts 00interface_function_typed_field.ts + 00interface_function_typed_field_class.ts 00interface_generic.ts 00interface_global_method.ts 00interface_indexer.ts 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..832cfa398 --- /dev/null +++ b/tslang/test/tester/tests/00interface_function_typed_field_class.ts @@ -0,0 +1,76 @@ +// 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; + } +} + +function fromClass(): () => int { + const o: HasFn = new C(42); + 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(); +} + +function main() { + const f = fromClass(); + 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); + + print("done."); +} From 8fc4a69e75930db8fa2c6e238018fec71daf6cbc Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 19 Oct 2026 12:26:57 +0100 Subject: [PATCH 2/5] One method cell for a field reached twice through a diamond extends An interface inheriting the same function-typed field from two parents gives the field two vtable slots; registering a cell per slot failed with "redefinition of symbol". The test now also covers `implements`, a base class that implements the interface, and the diamond. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenClasses.cpp | 7 ++- .../00interface_function_typed_field_class.ts | 49 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index f2ad679b3..7550422b6 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -2379,7 +2379,12 @@ genContext); return {emptyFieldInfo, mlir::failure()}; } - methodsForFields.push_back({id, *foundMethodPtr}); + // 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()}; } } diff --git a/tslang/test/tester/tests/00interface_function_typed_field_class.ts b/tslang/test/tester/tests/00interface_function_typed_field_class.ts index 832cfa398..90f8c3251 100644 --- a/tslang/test/tester/tests/00interface_function_typed_field_class.ts +++ b/tslang/test/tester/tests/00interface_function_typed_field_class.ts @@ -50,6 +50,47 @@ 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 f = fromClass(); print(f()); @@ -72,5 +113,13 @@ function main() { 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."); } From 2a523c61d534eb92a3d27f83ee2a36d59281a2d1 Mon Sep 17 00:00:00 2001 From: Alex D Date: Mon, 19 Oct 2026 13:07:00 +0100 Subject: [PATCH 3/5] A class field holding a function can fill an interface's function-typed field (#613) * A class field holding a function can fill an interface's function-typed field `class W { fn: () => int = () => 7 }` cast to `interface HasFn { fn: () => int }` failed with "field type not matching": the class stores the field as a hybrid function ({this-or-null, fn}), while the interface slot is read as a function taking the object as its `this`. Neither a null-this hybrid (no `this` parameter) nor a bound one (its own receiver) can be read that way. The field callback of mlirGenClassVirtualTableDefinitionForInterface now accepts a class field of any function kind whose signature matches, and synthesizes a private thunk `...vtbl..thunk(this, ...)` that calls `this.(...)`. The thunk fills the slot through the method cell added for class methods (INTERFACE_METHOD_CELL_TAG), so the field is read at every call and a later assignment to it is seen through the interface. Co-Authored-By: Claude Opus 5.5 * Cover an unannotated function-typed class field filling an interface field `fn = () => 6` is stored as a plain function rather than a hybrid one; it reaches the interface through the same thunk. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenClasses.cpp | 42 ++++++ tslang/lib/TypeScript/MLIRGenImpl.h | 66 +++++++++ tslang/test/tester/CMakeLists.txt | 3 + ...erface_function_typed_field_class_field.ts | 140 ++++++++++++++++++ 4 files changed, 251 insertions(+) create mode 100644 tslang/test/tester/tests/00interface_function_typed_field_class_field.ts diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index 7550422b6..ce2a8e802 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -2389,6 +2389,48 @@ genContext); } } + // 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. + if (found && isa(fieldType) && fieldType != 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) diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index e47d0deba..e3f733d3f 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -12130,6 +12130,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 53b91af32..89d6c17ba 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -612,6 +612,7 @@ tslang_add_test(NAME test-compile-00-interface-optional COMMAND test-runner "${P 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-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") @@ -1166,6 +1167,7 @@ tslang_add_test(NAME test-jit-00-interface-optional COMMAND test-runner -jit "${ 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-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") @@ -2028,6 +2030,7 @@ set(TSLANG_CORPUS 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 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..52a02d374 --- /dev/null +++ b/tslang/test/tester/tests/00interface_function_typed_field_class_field.ts @@ -0,0 +1,140 @@ +// 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(); +} + +function fromField(): () => int { + const h: HasFn = new WithField(); + 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 g = fromField(); + assert(g() == 7); + + print("done."); +} From f12b53f93df6023828eacab01e3330b079b4f934 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 19 Oct 2026 13:35:31 +0100 Subject: [PATCH 4/5] Keep the receiver alive where the interface tests return a bound field Under -mm=rc a bound method does not hold its receiver yet: `return o.fn` from a function that made `new C(42)` handed back a value pointing into a freed instance. Linux CI's JIT rc run read the freed block (assert in 00interface_function_typed_field_class.ts); with glibc's tcache off the class-field test crashed calling through the freed field. The caller now owns the instance, as the rest of these tests already do. Co-Authored-By: Claude Opus 5.5 --- .../tests/00interface_function_typed_field_class.ts | 9 ++++++--- .../00interface_function_typed_field_class_field.ts | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/tslang/test/tester/tests/00interface_function_typed_field_class.ts b/tslang/test/tester/tests/00interface_function_typed_field_class.ts index 90f8c3251..d02042c47 100644 --- a/tslang/test/tester/tests/00interface_function_typed_field_class.ts +++ b/tslang/test/tester/tests/00interface_function_typed_field_class.ts @@ -16,8 +16,10 @@ class C { } } -function fromClass(): () => int { - const o: HasFn = new C(42); +// 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; } @@ -92,7 +94,8 @@ class Diamond { } function main() { - const f = fromClass(); + const c42 = new C(42); + const f = fromClass(c42); print(f()); assert(f() == 42); 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 index 52a02d374..47a22b0f6 100644 --- a/tslang/test/tester/tests/00interface_function_typed_field_class_field.ts +++ b/tslang/test/tester/tests/00interface_function_typed_field_class_field.ts @@ -87,8 +87,10 @@ function callFn(h: HasFn): int { return h.fn(); } -function fromField(): () => int { - const h: HasFn = new WithField(); +// 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; } @@ -133,7 +135,8 @@ function main() { assert(callFn(new WithField()) == 7); assert(callFn({ fn() { return 41; } }) == 41); - const g = fromField(); + const wf = new WithField(); + const g = fromField(wf); assert(g() == 7); print("done."); From 74eb7ac7ba8503c97de153ffb7ab6474f9ad3f70 Mon Sep 17 00:00:00 2001 From: Alex D Date: Mon, 19 Oct 2026 13:39:36 +0100 Subject: [PATCH 5/5] Reading an optional function-typed interface field binds this when present (#614) * Reading an optional function-typed interface field binds `this` when present `interface M { fn?: () => int }`: `m.fn` loads as optional, and InterfaceFieldAccess bound the interface's `this` to that optional directly, which failed verification ('ts.CreateBoundFunction' op operand #1 must be ...) for every implementer, object literal included - `const g = m.fn`, `m.fn?.()`, `m.fn !== undefined` and `return m.fn` all hit it. A present field now reads as optional (bound to ExtractInterfaceThis), an absent one as undefined. The lowering needed no change: a -1 slot already gives a null address and a class's method cell (INTERFACE_METHOD_CELL_TAG) its cell, and an optional load of a null address is undefined. The test covers object literals with and without the field, a class whose method fills it and a class lacking it, a mixed array at one access site, the bound value returned as `(() => int) | undefined`, and a field taking a parameter (AOT, JIT, rc/none corpus). Co-Authored-By: Claude Opus 5.5 * A class's optional function field stays an error for an interface's optional one Since class fields holding a function can fill an interface's function-typed field (a thunk in the shared method cell), `class C { fn?: () => int }` cast to `interface M { fn?: () => int }` took that path too: isAnyFunctionType strips the optional. The cell is the same for every instance, so the field read as present whether it was set or not, and an unset one was called. The thunk path now skips an optional class field, which stays "field type not matching" as before, until the cell can carry a per-instance presence check. A compile-error test pins it. Co-Authored-By: Claude Opus 5.5 * Keep the receiver alive where the optional-field test returns a bound field Under -mm=rc a bound method does not hold its receiver yet, so `getFn(new WithFn(13))` handed back a value pointing into a freed instance (as #612's tests did, fixed there in f12b53f9). Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenClasses.cpp | 4 +- tslang/lib/TypeScript/MLIRGenImpl.h | 34 ++++- tslang/test/tester/CMakeLists.txt | 14 +++ .../optional_class_function_field.ts | 17 +++ ...interface_optional_function_typed_field.ts | 117 ++++++++++++++++++ 5 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 tslang/test/tester/interface-errors/optional_class_function_field.ts create mode 100644 tslang/test/tester/tests/00interface_optional_function_typed_field.ts diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index ce2a8e802..c855b543e 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -2393,8 +2393,10 @@ genContext); // {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 - && mth.isAnyFunctionType(foundField.type)) + && !isa(foundField.type) && mth.isAnyFunctionType(foundField.type)) { auto fieldName = dyn_cast(id); auto match = mth.TestFunctionTypesMatchWithObjectMethods(location, foundField.type, fieldType); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index e3f733d3f..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; diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 89d6c17ba..c90517699 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -613,6 +613,7 @@ tslang_add_test(NAME test-compile-00-interface-optional-cast-order COMMAND test- 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") @@ -1168,6 +1169,7 @@ tslang_add_test(NAME test-jit-00-interface-optional-cast-order COMMAND test-runn 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") @@ -2043,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 @@ -2758,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_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."); +}