From db144be78e77c7334eadf636c11a9780b2f4438b Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 19 Oct 2026 12:57:59 +0100 Subject: [PATCH 1/2] 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 --- 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 | 132 ++++++++++++++++++ 4 files changed, 243 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..cfd85a9c2 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 a bound or plain one, none of which the slot can hand 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..99efa3101 --- /dev/null +++ b/tslang/test/tester/tests/00interface_function_typed_field_class_field.ts @@ -0,0 +1,132 @@ +// 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; +} + +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 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 fcd8bb76cbbc753cfa9c37512ab398a8f0a36f28 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 19 Oct 2026 13:00:33 +0100 Subject: [PATCH 2/2] 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 --- tslang/lib/TypeScript/MLIRGenClasses.cpp | 2 +- .../tests/00interface_function_typed_field_class_field.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index cfd85a9c2..ce2a8e802 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -2390,7 +2390,7 @@ genContext); } // a class field holding a function (`fn: () => int = ...`) stores a hybrid function - - // {function, `this` or null} - or a bound or plain one, none of which the slot can hand out + // {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 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 99efa3101..52a02d374 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 @@ -14,6 +14,11 @@ 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) { @@ -92,6 +97,9 @@ function main() { 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);