From acf23737e58498458260e32b26fb4b3c566110c8 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 10:34:27 +0100 Subject: [PATCH] rc: a const bound method with a declared type holds its object `const c: () => number = G.items[0].get`: the declared type casts the bound method to a hybrid function, and viewedValue did not look through that cast, so processConstRef saw no bound method over a read of a field, an element, a global or a local. The const was folded with no count, and `G = mk()` freed the object `c()` still ran on. viewedValue now strips casts first. Fuzzer (#646), 80194. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 8 +- tslang/test/tester/CMakeLists.txt | 3 + .../00const_bound_method_declared_type.ts | 115 ++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 tslang/test/tester/tests/00const_bound_method_declared_type.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index daf2c9e35..585b71e6d 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1702,9 +1702,15 @@ class MLIRGenImpl } // The value an interface or a bound method is made over: `v` seen through `I` in - // `const i: I = v`, or `this` of `const m = v.get`. + // `const i: I = v`, or `this` of `const m = v.get`, through any casts: a declared type + // (`const m: () => number = v.get`) casts the bound method to a hybrid function. static mlir::Value viewedValue(mlir::Value value) { + while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + { + value = castOp.getIn(); + } + auto *definingOp = value ? value.getDefiningOp() : nullptr; if (auto newInterfaceOp = dyn_cast_or_null(definingOp)) { diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index a52ac3044..f5e90c742 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -408,6 +408,7 @@ tslang_add_test(NAME test-compile-00-global-argument-reassigned COMMAND test-run 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-const-interface-field-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_interface_field_read.ts") tslang_add_test(NAME test-compile-00-argument-place-overwritten COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00argument_place_overwritten.ts") +tslang_add_test(NAME test-compile-00-const-bound-method-declared-type COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_bound_method_declared_type.ts") tslang_add_test(NAME test-compile-00-temporary-read-in-finally COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00temporary_read_in_finally.ts") tslang_add_test(NAME test-compile-00-unowned-local-store COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00unowned_local_store.ts") tslang_add_test(NAME test-compile-00-array-argument-held COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_argument_held.ts") @@ -993,6 +994,7 @@ tslang_add_test(NAME test-jit-00-global-argument-reassigned COMMAND test-runner 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-const-interface-field-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_interface_field_read.ts") tslang_add_test(NAME test-jit-00-argument-place-overwritten COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00argument_place_overwritten.ts") +tslang_add_test(NAME test-jit-00-const-bound-method-declared-type COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_bound_method_declared_type.ts") tslang_add_test(NAME test-jit-00-temporary-read-in-finally COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00temporary_read_in_finally.ts") tslang_add_test(NAME test-jit-00-unowned-local-store COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00unowned_local_store.ts") tslang_add_test(NAME test-jit-00-array-argument-held COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_argument_held.ts") @@ -1968,6 +1970,7 @@ set(TSLANG_CORPUS 00global_argument_view.ts 00const_interface_field_read.ts 00argument_place_overwritten.ts + 00const_bound_method_declared_type.ts 00temporary_read_in_finally.ts 00unowned_local_store.ts 00array_argument_held.ts diff --git a/tslang/test/tester/tests/00const_bound_method_declared_type.ts b/tslang/test/tester/tests/00const_bound_method_declared_type.ts new file mode 100644 index 000000000..d3cc51046 --- /dev/null +++ b/tslang/test/tester/tests/00const_bound_method_declared_type.ts @@ -0,0 +1,115 @@ +// A const bound method with a declared type (`const c: () => number = G.items[0].get`) keeps its +// object alive, as one without does. Under rc a const made over a read of a field, an element or a +// global holds a count of its own; the declared type casts the bound method to a hybrid function, +// and that cast hid the read, so the const was folded with no count, and `G = mk()` freed the +// object `c()` still ran on. Each read follows allocations of the same size, so a freed block is +// reused. +class W { + constructor(public x: number) {} +} + +class V { + x: number; + w: W; + a: number[]; + + constructor(x: number) { + this.x = x; + this.w = new W(x); + this.a = [x]; + } + + get(): number { + return this.x; + } +} + +class N { + v: V; + items: V[]; + + constructor(x: number) { + this.v = new V(x); + this.items = [new V(x)]; + } +} + +@noinline function mk(i: number): N { + return new N(i); +} + +function churn() { + const junk: V[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new V(777)); + } +} + +let G: N = mk(2); + +interface IN { + v: V; + items: V[]; +} + +@noinline function mv(x: number): V { + return new V(x); +} + +function take(n: N, i: IN) { + return i.items[0].x; +} + +function fieldOfGlobal(x: number) { + G = mk(x); + const c: () => number = G.v.get; + G = mk(0); + churn(); + return c(); +} + +function fieldOfLocal(x: number) { + let n = mk(x); + const c: () => number = n.v.get; + n = mk(0); + churn(); + return c(); +} + +function local(x: number) { + let v = new V(x); + const c: () => number = v.get; + v = new V(0); + churn(); + return c(); +} + +function inLoop(x: number) { + G = mk(x); + const c: () => number = G.items[0].get; + for (let i = 0; i < 2; i++) { + G = mk(i); + churn(); + } + + return c(); +} + +function main() { + // an element of a global, the fuzzer's shape + const c: () => number = G.items[0].get; + let s: number = 0; + for (let i = 0; i < 1; i++) { + G = mk(4); + s += take(mk(5), { v: mv(i + 5), items: [mv(2)] }); + } + + assert(c() == 2, "an element of a global"); + assert(s == 2, "the call's own element"); + + assert(fieldOfGlobal(4) == 4, "a field of a global"); + assert(fieldOfLocal(5) == 5, "a field of a local"); + assert(local(6) == 6, "a local"); + assert(inLoop(7) == 7, "the global reassigned in a loop"); + print("done."); +}