From f1dfd2acecf7d63ad4420e8e5b9d507c284bfefb Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 06:48:04 +0100 Subject: [PATCH 1/2] rc: a method bound through an interface, or by a global's initializer, counts its object Under rc a bound method counts the object it is bound to through that object's type tag, and only the bound value's receivers take a count (getBoundObjectTypeTag). Two holes: - bound through an interface (`c = ig.get`), the object's type is not known where the bound value is built, so it got no tag and counted nothing: `c()` ran on a freed object once the interface went. It now takes the tag the interface carries for what it points at (null for a missing optional method). - a global initialized with a bound method (`let c = mk().get`, `let c = G.v.get`) took no count: a temporary object was given back at the end of the initializer, and the first `c = d` released it a second time. The global now takes one, as it does for a field read. Fuzzer (#646), found past the first 1000 programs (40181). Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/LowerToLLVM.cpp | 28 ++++++- .../TypeScript/OwnedReturnConsumptionPass.cpp | 17 +++++ tslang/test/tester/CMakeLists.txt | 3 + .../tests/00bound_method_counts_object.ts | 75 +++++++++++++++++++ 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 tslang/test/tester/tests/00bound_method_counts_object.ts diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 0b8ef48da..e8a1ae663 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -6464,7 +6464,33 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(loc, boundFunc, thisOpaque, methodTypedPtr); - rewriter.replaceOp(interfaceSymbolRefOp, ValueRange{boundFuncVal}); + if (!tsLlvmContext->compileOptions.isRefCounted()) + { + rewriter.replaceOp(interfaceSymbolRefOp, ValueRange{boundFuncVal}); + return success(); + } + + // Under rc a method bound to an object counts it through the object's type tag + // (getBoundObjectTypeTag), and a receiver of the bound value - `c = ix.get` - holds the + // object. Bound through an interface, the object's type is not known here, so the bound + // value had no tag, counted nothing, and `c()` ran on a freed object once the interface + // went. The interface carries the tag of what it points at (NewInterfaceOpLowering), and + // the bound value takes that one: null for a missing optional method, whose `this` is. + mlir::Value tag = rewriter.create( + loc, th.getPtrType(), transformed.getInterfaceVal(), MLIRHelper::getStructIndex(rewriter, INTERFACE_TYPE_INDEX)); + if (isOptional) + { + auto isNullThis = rewriter.create(loc, LLVM::ICmpPredicate::eq, thisValEffective, + rewriter.create(loc, th.getPtrType())); + tag = rewriter.create(loc, isNullThis, rewriter.create(loc, th.getPtrType()), tag); + } + + auto llvmBoundFunctionType = tch.convertType(boundFunc); + auto boundLLVM = + rewriter.create(loc, llvmBoundFunctionType, boundFuncVal.getResult()).getResult(0); + auto tagged = rewriter.create(loc, boundLLVM, tag, + MLIRHelper::getStructIndex(rewriter, CLOSURE_TYPE_INDEX)); + rewriter.replaceOp(interfaceSymbolRefOp, ValueRange{tagged}); } else { diff --git a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp index a2ad4ea4b..ddf3c6fa8 100644 --- a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp +++ b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp @@ -857,6 +857,23 @@ class OwnedReturnConsumptionPass mlir::OpBuilder builder(loadOp->getContext()); builder.setInsertionPointAfter(loadOp); builder.create(loadOp->getLoc(), value); + return; + } + + // A method bound to an object (`let c = mk().get`, `let c = G.v.get`, `let c = ix.get`) + // is born with no count, and only its receivers take one; under rc the bound value counts + // the object it is bound to. The global is the receiver here: without a count of its + // own, a temporary object was given back at the end of the region, and the first + // `c = d` released it a second time. + auto *boundOp = value.getDefiningOp(); + auto isBoundMethod = + mlir::isa_and_nonnull(boundOp) || + (mlir::isa_and_nonnull(boundOp) && + mlir::isa(value.getType())) || + (mlir::isa_and_nonnull(boundOp) && !boundOp->hasAttr(OWNS_CAPTURE_ATTR_NAME)); + if (isBoundMethod) + { + retainForGlobal(resultOp, resultOp->getOperand(0)); } }); } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 9661bd9c4..3d9133499 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -405,6 +405,7 @@ tslang_add_test(NAME test-compile-00-const-element-read COMMAND test-runner "${P tslang_add_test(NAME test-compile-00-const-global-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_global_read.ts") tslang_add_test(NAME test-compile-00-global-argument-reassigned COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_argument_reassigned.ts") tslang_add_test(NAME test-compile-00-global-argument-view COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_argument_view.ts") +tslang_add_test(NAME test-compile-00-bound-method-counts-object COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00bound_method_counts_object.ts") tslang_add_test(NAME test-compile-00-array-data-string-alias COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_data_string_alias.ts") tslang_add_test(NAME test-compile-00-arrays7 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array7.ts") tslang_add_test(NAME test-compile-00-arrays8-tuple-spread COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array8_tuple_spread.ts") @@ -984,6 +985,7 @@ tslang_add_test(NAME test-jit-00-const-element-read COMMAND test-runner -jit "${ tslang_add_test(NAME test-jit-00-const-global-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_global_read.ts") tslang_add_test(NAME test-jit-00-global-argument-reassigned COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_argument_reassigned.ts") tslang_add_test(NAME test-jit-00-global-argument-view COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_argument_view.ts") +tslang_add_test(NAME test-jit-00-bound-method-counts-object COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00bound_method_counts_object.ts") tslang_add_test(NAME test-jit-00-array-data-string-alias COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_data_string_alias.ts") tslang_add_test(NAME test-jit-00-arrays7 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array7.ts") tslang_add_test(NAME test-jit-00-arrays8-tuple-spread COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array8_tuple_spread.ts") @@ -1953,6 +1955,7 @@ set(TSLANG_CORPUS 00const_global_read.ts 00global_argument_reassigned.ts 00global_argument_view.ts + 00bound_method_counts_object.ts 00array_data_string_alias.ts 00array7.ts 00array8_tuple_spread.ts diff --git a/tslang/test/tester/tests/00bound_method_counts_object.ts b/tslang/test/tester/tests/00bound_method_counts_object.ts new file mode 100644 index 000000000..d67dd4c6e --- /dev/null +++ b/tslang/test/tester/tests/00bound_method_counts_object.ts @@ -0,0 +1,75 @@ +// A method bound to an object keeps that object alive while it is held: bound through an +// interface (`c = ig.get`), or held by a global from its initializer (`let c = mv(3).get`, +// `let c = G.v.get`). Under rc a bound value counts its object through the object's type tag, and +// only its receivers take a count. Through an interface it had no tag and counted nothing, and a +// global's initializer took no count, so the object was given back while the bound value still +// ran on it. Each read follows allocations of the same size, so a freed block is reused. +class V { + constructor(public x: number) {} + + get(): number { + return this.x; + } +} + +class N { + v = new V(4); +} + +interface IG { + get(): number; +} + +function mv(x: number) { + return new V(x); +} + +function churn() { + const junk: V[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new V(777)); + } +} + +let G = new N(); +let fromTemporary: () => number = mv(3).get; +let fromField: () => number = G.v.get; + +function boundThroughInterface(x: number) { + let c: () => number = mv(0).get; + { + const ig: IG = mv(x); + c = ig.get; + } + + churn(); + return c(); +} + +function globalBoundThroughInterface(x: number) { + { + let ig: IG = mv(x); + fromTemporary = ig.get; + } + + churn(); + return fromTemporary(); +} + +function main() { + churn(); + assert(fromTemporary() == 3, "a global bound to a temporary object by its initializer"); + G.v = mv(9); + churn(); + assert(fromField() == 4, "a global bound to a global's field by its initializer"); + fromField = mv(6).get; + churn(); + assert(fromField() == 6, "a global bound again"); + + for (let i = 0; i < 3; i++) { + assert(boundThroughInterface(i + 10) == i + 10, "a local bound through an interface"); + assert(globalBoundThroughInterface(i + 20) == i + 20, "a global bound through an interface"); + } + + print("done."); +} From 8415647e07f3fcd5b61d13de1adf6b6e981287e3 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 09:32:24 +0100 Subject: [PATCH 2/2] rc: build a method bound through an interface from LLVM values The tagged bound value was an UnrealizedConversionCast over a ts.CreateBoundFunction still waiting for its own conversion. Under --opt that corrupted the compiler's heap on Linux (36interfaces, 00owned_construct_interface). Build the struct directly, as CreateBoundFunctionOpLowering does: method, this, then the tag. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/LowerToLLVM.cpp | 30 +++++++++++++++------------ 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index e8a1ae663..5fe055730 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -6457,15 +6457,15 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(loc, isMissing, nullThisVal, thisVal); } - auto thisOpaque = - rewriter.create(loc, mlir_ts::OpaqueType::get(rewriter.getContext()), thisValEffective); - auto methodTypedPtr = rewriter.create( - loc, mlir_ts::FunctionType::get(rewriter.getContext(), boundFunc.getInputs(), boundFunc.getResults()), - methodOrFieldPtr); - auto boundFuncVal = - rewriter.create(loc, boundFunc, thisOpaque, methodTypedPtr); if (!tsLlvmContext->compileOptions.isRefCounted()) { + auto thisOpaque = rewriter.create( + loc, mlir_ts::OpaqueType::get(rewriter.getContext()), thisValEffective); + auto methodTypedPtr = rewriter.create( + loc, mlir_ts::FunctionType::get(rewriter.getContext(), boundFunc.getInputs(), boundFunc.getResults()), + methodOrFieldPtr); + auto boundFuncVal = + rewriter.create(loc, boundFunc, thisOpaque, methodTypedPtr); rewriter.replaceOp(interfaceSymbolRefOp, ValueRange{boundFuncVal}); return success(); } @@ -6485,12 +6485,16 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(loc, isNullThis, rewriter.create(loc, th.getPtrType()), tag); } - auto llvmBoundFunctionType = tch.convertType(boundFunc); - auto boundLLVM = - rewriter.create(loc, llvmBoundFunctionType, boundFuncVal.getResult()).getResult(0); - auto tagged = rewriter.create(loc, boundLLVM, tag, - MLIRHelper::getStructIndex(rewriter, CLOSURE_TYPE_INDEX)); - rewriter.replaceOp(interfaceSymbolRefOp, ValueRange{tagged}); + // built here as CreateBoundFunctionOpLowering builds it, rather than over a + // ts.CreateBoundFunction still waiting for its own conversion + auto structVal = rewriter.create(loc, tch.convertType(boundFunc)); + auto structVal2 = rewriter.create(loc, structVal, methodOrFieldPtr, + MLIRHelper::getStructIndex(rewriter, DATA_VALUE_INDEX)); + auto structVal3 = rewriter.create(loc, structVal2, thisValEffective, + MLIRHelper::getStructIndex(rewriter, THIS_VALUE_INDEX)); + auto structVal4 = rewriter.create(loc, structVal3, tag, + MLIRHelper::getStructIndex(rewriter, CLOSURE_TYPE_INDEX)); + rewriter.replaceOp(interfaceSymbolRefOp, ValueRange{structVal4}); } else {