From a2c44f2cf616374b8f83c7b324b48a296e13b76d Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 05:31:58 +0100 Subject: [PATCH] A closure over a const read out of a place captures the value it read `const v = n.v; n.v = d; const c = () => v.x;` gave `d.x` under gc and none. A const read out of a field, an element or a global is folded into the read and has no storage of its own (under rc it does, see processConstRef). A by-value capture was taken through the reference GetReferenceFromValue found for it - for a folded read, the place it was read from - and CaptureOpLowering dereferenced that place when the closure was made, after the overwrite. A capture now goes through a reference only when that reference is the variable's own storage (a variable, a parameter, a captured cell); anything else captures the value itself. Fuzzer (#646) G1. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenFunctions.cpp | 15 +++++++ tslang/test/tester/CMakeLists.txt | 3 ++ .../test/tester/tests/00closure_const_read.ts | 45 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 tslang/test/tester/tests/00closure_const_read.ts diff --git a/tslang/lib/TypeScript/MLIRGenFunctions.cpp b/tslang/lib/TypeScript/MLIRGenFunctions.cpp index 1967c9551..5367bd62a 100644 --- a/tslang/lib/TypeScript/MLIRGenFunctions.cpp +++ b/tslang/lib/TypeScript/MLIRGenFunctions.cpp @@ -2013,6 +2013,21 @@ namespace mlirgen // review capturing by ref. it should match storage type auto refValue = mcl.GetReferenceFromValue(location, varValue); + + // A copy is taken through the reference only when that reference is the variable's own + // storage. A `const` folded into a read (`const v = n.v`, `const v = G`) has none: the + // reference is where it was read from, and dereferenced now it gave the closure what the + // place holds by the time the closure is made - `n.v = d` in between, and `v` in the + // closure was `d`. The value itself is what the const names. + auto isOwnStorage = refValue && (refValue.getDefiningOp() || + refValue.getDefiningOp() || + refValue.getDefiningOp() || + isCapturedCellSlot(refValue)); + if (refValue && !capturedByRef && !isOwnStorage) + { + refValue = mlir::Value(); + } + if (refValue && !capturedByRef) { // the box holds a copy, exactly as it does for a value with no reference at all diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 9661bd9c4..c7322afca 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-closure-const-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00closure_const_read.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-closure-const-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00closure_const_read.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 + 00closure_const_read.ts 00array_data_string_alias.ts 00array7.ts 00array8_tuple_spread.ts diff --git a/tslang/test/tester/tests/00closure_const_read.ts b/tslang/test/tester/tests/00closure_const_read.ts new file mode 100644 index 000000000..accaa733c --- /dev/null +++ b/tslang/test/tester/tests/00closure_const_read.ts @@ -0,0 +1,45 @@ +// A closure over a `const` read out of a field, an element or a global sees +// the value the const was given, not what the place holds by the time the closure is made. Folded +// into the read, the const had no storage of its own, and the capture was taken through the +// place it was read from: `const v = n.v; n.v = d; const c = () => v.x;` gave `d.x` under gc and +// none (rc gives such a const storage, and was right). +class V { + constructor(public x: number) {} +} + +class N { + v = new V(3); + items: V[] = [new V(4)]; +} + +let G = new V(5); + +function field() { + const n = new N(); + const v = n.v; + n.v = new V(2); + const c = (): number => v.x; + return c(); +} + +function element() { + const n = new N(); + const v = n.items[0]; + n.items[0] = new V(2); + const c = (): number => v.x; + return c(); +} + +function global() { + const v = G; + G = new V(2); + const c = (): number => v.x; + return c(); +} + +function main() { + assert(field() == 3, "a closure over a const read out of a field"); + assert(element() == 4, "a closure over a const read out of an element"); + assert(global() == 5, "a closure over a const read out of a global"); + print("done."); +}