From 228d1f9bf8c3613caa57b4eda7f1cf76aa023038 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 19 Jul 2026 18:30:45 +0100 Subject: [PATCH] Enhance method member handling to preserve 'this' reference in type-literal annotations --- .../TypeScript/LowerToLLVM/CastLogicHelper.h | 20 +++++++++-- .../TypeScript/MLIRLogic/MLIRPrinter.h | 12 +++++++ tslang/lib/TypeScript/MLIRGenTypes.cpp | 20 +++++++++++ tslang/test/tester/CMakeLists.txt | 2 ++ .../tester/tests/00object_annotated_method.ts | 36 +++++++++++++++++++ 5 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 tslang/test/tester/tests/00object_annotated_method.ts diff --git a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h index 713ba8c8b..b0b66809a 100644 --- a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h @@ -216,6 +216,16 @@ class CastLogicHelper if (auto resFuncType = dyn_cast(resType)) { + // dropping a bound/hybrid value to a plain FunctionType only loses the + // receiver when the target has no `this` slot of its own. A target + // whose first input is opaque/object-typed (method-member convention, + // see isBoundReference) keeps the this param in the funcptr signature + // and the receiver is re-bound from the base object at every property + // access - nothing is lost, so no warning. + auto resKeepsThisParam = resFuncType.getNumInputs() > 0 && + (isa(resFuncType.getInput(0)) || + isa(resFuncType.getInput(0))); + if (auto inBoundFunc = dyn_cast(inType)) { // somehow llvm.trampoline accepts only direct method symbol @@ -224,7 +234,10 @@ class CastLogicHelper auto methodVal = rewriter.create(loc, resFuncType, in); return rewriter.create(loc, resFuncType, methodVal, thisVal); */ - op->emitWarning("losing this reference"); + if (!resKeepsThisParam) + { + op->emitWarning("losing this reference"); + } /* // you can wrap into () => {} lambda call to capture vars const user = { @@ -249,7 +262,10 @@ class CastLogicHelper // and re-supplied by the caller (e.g. an interface vtable call passes its own // thisVal) - this is the vtable-method-pointer path for a cross-module tuple // value cast to a method-bearing interface. - op->emitWarning("losing this reference"); + if (!resKeepsThisParam) + { + op->emitWarning("losing this reference"); + } return rewriter.create(loc, resFuncType, in); } } diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRPrinter.h b/tslang/include/TypeScript/MLIRLogic/MLIRPrinter.h index cfdb82108..b3fe2195e 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRPrinter.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRPrinter.h @@ -33,6 +33,18 @@ class MLIRPrinter auto isVar = t.getIsVarArg(); for (auto subType : t.getInputs()) { + // an opaque/object-typed first input is an implicit `this` param + // (method-member convention, see isBoundReference) - it is not + // declarable in source syntax ("Opaque" doesn't parse back), so + // omit it like DeclarationPrinter::printParams omits `this`. + if (index == 0 && + (isa(subType) || isa(subType))) + { + index++; + size--; + continue; + } + if (!first) { out << ", "; diff --git a/tslang/lib/TypeScript/MLIRGenTypes.cpp b/tslang/lib/TypeScript/MLIRGenTypes.cpp index 5c99a21ff..9119a1ec3 100644 --- a/tslang/lib/TypeScript/MLIRGenTypes.cpp +++ b/tslang/lib/TypeScript/MLIRGenTypes.cpp @@ -2603,6 +2603,26 @@ namespace mlirgen return mlir::failure(); } + // a method member is called through the object, so its field type + // must declare an implicit `this` first param (opaque, same + // convention as interface method funcTypes): that is what makes + // property access create a bound reference (isBoundReference) + // binding the receiver. A bare this-less FunctionType here made + // the annotation cast drop `this` ("losing this reference") and + // later calls pass a garbage receiver. + if (auto funcType = dyn_cast(type)) + { + if (funcType.getNumInputs() == 0 || + !(isa(funcType.getInput(0)) || isa(funcType.getInput(0)))) + { + SmallVector inputs; + inputs.push_back(mlir_ts::OpaqueType::get(builder.getContext())); + inputs.append(funcType.getInputs().begin(), funcType.getInputs().end()); + type = mlir_ts::FunctionType::get(builder.getContext(), inputs, funcType.getResults(), + funcType.isVarArg()); + } + } + types.push_back({TupleFieldName(methodSignature->name, genContext), type, false, mlir_ts::AccessLevel::Public}); } else if (kind == SyntaxKind::ConstructSignature) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index e342d9d1b..216da22ba 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -287,6 +287,7 @@ add_test(NAME test-compile-00-interface-optional COMMAND test-runner "${PROJECT_ add_test(NAME test-compile-00-interface-optional-cast-order COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_cast_order.ts") add_test(NAME test-compile-00-interface-function-typed-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field.ts") add_test(NAME test-compile-00-interface-captures COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_captures.ts") +add_test(NAME test-compile-00-object-annotated-method COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method.ts") add_test(NAME test-compile-00-interface-generic COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_generic.ts") add_test(NAME test-compile-00-interface-new COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_new.ts") add_test(NAME test-compile-00-interface-indexer COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_indexer.ts") @@ -640,6 +641,7 @@ add_test(NAME test-jit-00-interface-optional COMMAND test-runner -jit "${PROJECT 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") 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") add_test(NAME test-jit-00-interface-captures COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_captures.ts") +add_test(NAME test-jit-00-object-annotated-method COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method.ts") add_test(NAME test-jit-00-interface-generic COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_generic.ts") add_test(NAME test-jit-00-interface-new COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_new.ts") add_test(NAME test-jit-00-interface-indexer COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_indexer.ts") diff --git a/tslang/test/tester/tests/00object_annotated_method.ts b/tslang/test/tester/tests/00object_annotated_method.ts new file mode 100644 index 000000000..40684c067 --- /dev/null +++ b/tslang/test/tester/tests/00object_annotated_method.ts @@ -0,0 +1,36 @@ +// A type-literal annotation with a METHOD member (`inc(): void`, MethodSignature) +// must keep `this` callable through the annotated value: the member's field type +// carries an implicit opaque `this` param (same convention as interface method +// funcTypes), so property access binds the receiver. Previously the member +// degraded to a this-less funcptr: the annotation cast dropped the receiver +// ("losing this reference") and calls silently passed garbage as `this` - +// mutations went nowhere (count stayed 0). + +var counterObj: { count: number; inc(): void } = { count: 0, inc() { this.count = this.count + 1; } }; + +function localAnnotated() { + let c: { count: number; inc(): void } = { count: 10, inc() { this.count = this.count + 1; } }; + c.inc(); + c.inc(); + assert(c.count == 12); + print(c.count); +} + +function reader() { + let obj: { base: number; twice(): number } = { base: 21, twice() { return this.base * 2; } }; + const v = obj.twice(); + assert(v == 42); + print(v); +} + +function main() { + counterObj.inc(); + counterObj.inc(); + print(counterObj.count); + assert(counterObj.count == 2); + + localAnnotated(); + reader(); + + print("done."); +}