From 26139a7242ee736fa7a5957542980140f1d9aac1 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 06:41:45 +0100 Subject: [PATCH] An element is stored into once the value is ready `a[0] = f()` with `f` growing `a`: the element's address was taken while the left side was read, before the right side ran, and growing moved the elements to a new block - the store went into the one given back. The value was lost under every model, and under rc a freed block was written to (fuzzer #646, 6930). The assignment now takes the element's address again once the value is ready, from the array and the index the left side evaluated first, as JavaScript has it: a right side that assigns the variable itself (`a[1] = (a = [5], 9)`) still stores into the old array. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 10 +++ tslang/test/tester/CMakeLists.txt | 3 + .../tests/00element_store_after_value.ts | 67 +++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 tslang/test/tester/tests/00element_store_after_value.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index c03a4244a..9b04fe9ef 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -6486,6 +6486,16 @@ class MLIRGenImpl return mlir::failure(); } + // An element's address is taken anew once the value is ready. The one the left side was + // read through was taken before the right side ran, and the right side may grow the array: + // `a[0] = f()` with `f` pushing to `a` moved its elements to a new block, and the store went + // into the one given back (a value lost under every model, a freed block written to under + // rc). The array and the index are the ones the left side evaluated, as JavaScript has it. + if (auto elementRefOp = storeReference.getDefiningOp()) + { + storeReference = builder.clone(*elementRefOp)->getResult(0); + } + // Overwriting owning storage hands the count over: the incoming value gains an // owner and the outgoing one loses it. Retaining first is what makes `x = x` safe - // releasing first could drop the last reference and free the value about to be diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 9661bd9c4..d3f421aa5 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-element-store-after-value COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00element_store_after_value.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-element-store-after-value COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00element_store_after_value.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 + 00element_store_after_value.ts 00array_data_string_alias.ts 00array7.ts 00array8_tuple_spread.ts diff --git a/tslang/test/tester/tests/00element_store_after_value.ts b/tslang/test/tester/tests/00element_store_after_value.ts new file mode 100644 index 000000000..10129b36d --- /dev/null +++ b/tslang/test/tester/tests/00element_store_after_value.ts @@ -0,0 +1,67 @@ +// `a[i] = f()` stores into the array once `f()` has run, even when `f` grows the array. The +// element's address was taken before the right side ran, and growing moved the elements to a new +// block: the store went into the one given back. The value was lost under every model, and under +// rc a freed block was written to. The array and the index are still the ones the left side +// evaluated first, as in JavaScript. +class V { + constructor(public x: number) {} +} + +class H { + a: number[] = [1]; + items: V[] = [new V(1)]; +} + +function growNumbers(a: number[]) { + for (let i = 0; i < 16; i++) a.push(i); + return 7; +} + +function growItems(items: V[]) { + for (let i = 0; i < 16; i++) items.push(new V(100 + i)); + return new V(8); +} + +function numbers() { + const a: number[] = [1]; + a[0] = growNumbers(a); + return a.length == 17 && a[0] == 7 && a[1] == 0; +} + +function objects() { + const items = [new V(1)]; + items[0] = growItems(items); + return items.length == 17 && items[0].x == 8 && items[16].x == 115; +} + +function compound() { + const a: number[] = [1]; + a[0] += growNumbers(a); + return a[0] == 8; +} + +function field() { + const h = new H(); + h.a[0] = growNumbers(h.a); + h.items[0] = growItems(h.items); + return h.a[0] == 7 && h.items[0].x == 8; +} + +function otherArray() { + let a: number[] = [1, 2]; + const old = a; + a[1] = (a = [5], 9); + return old[1] == 9 && a.length == 1 && a[0] == 5; +} + +function main() { + for (let i = 0; i < 3; i++) { + assert(numbers(), "a number stored after the call grows the array"); + assert(objects(), "an object stored after the call grows the array"); + assert(compound(), "a compound assignment after the call grows the array"); + assert(field(), "an element of a field stored after the call grows the array"); + assert(otherArray(), "the left side's array, though the right side assigns the variable"); + } + + print("done."); +}