From f62811d923a186f988b198781a79130506209bf9 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 19:32:01 +0100 Subject: [PATCH] A local given its own value stores nothing: `n = n` under -mm=own `n = n`, or `n = (n = mk(5), n)`, read the local and stored the value back. Under own the read counted as moving the value out, so the release of what the local held came after the move and the program was refused ("used here after its value was moved"). MLIRGen now emits no store when the value is a plain read of the same local made in the current block with only reads after it. This holds in every model - under rc the retain and release it skips were a pair - and an atomic or volatile local keeps its store. Test 00self_assign in the corpus (gc, rc, none) and under own. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 32 +++++++++++++++ tslang/test/tester/CMakeLists.txt | 7 ++++ tslang/test/tester/tests/00self_assign.ts | 50 +++++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 tslang/test/tester/tests/00self_assign.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 3bdc2b6f0..d2a4f340a 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1289,6 +1289,30 @@ class MLIRGenImpl definingOp->setAttr(OWNED_RESULT_ATTR_NAME, builder.getUnitAttr()); } + // A store of a local's own value back into it: `value` is a plain read of `reference`, a local's slot, made + // in this block with only reads after it, so the slot still holds what was read. An atomic or volatile + // access is the program's to keep. + bool isSelfStore(mlir::Value value, mlir::Value reference) + { + auto loadOp = value.getDefiningOp(); + if (!loadOp || loadOp.getReference() != reference || !reference.getDefiningOp() || + loadOp->getBlock() != builder.getInsertionBlock() || loadOp->hasAttr(ATOMIC_ATTR_NAME) || + loadOp->hasAttr(VOLATILE_ATTR_NAME)) + { + return false; + } + + for (auto *op = loadOp->getNextNode(); op; op = op->getNextNode()) + { + if (!mlir::isa(op)) + { + return false; + } + } + + return true; + } + // Storage that hands ownership over when it is overwritten: the incoming value gains an // owner and the outgoing one loses one. // @@ -6517,6 +6541,14 @@ class MLIRGenImpl storeReference = builder.clone(*elementRefOp)->getResult(0); } + // `n = n`, or `n = (n = f(), n)`: the value was just read out of this local and nothing since has + // written anywhere, so storing it back changes nothing. Emitting it anyway costs -mm=own the + // local: the read moves the value out, and the release of what the slot held came after it. + if (isSelfStore(savingValue, storeReference)) + { + return mlir::success(); + } + // 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 4253ea47e..2822f6deb 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -398,6 +398,7 @@ tslang_add_test(NAME test-compile-00-generator-temporaries COMMAND test-runner " tslang_add_test(NAME test-compile-00-conditional-named-const-release COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00conditional_named_const_release.ts") tslang_add_test(NAME test-compile-00-array-length-valid COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_valid.ts") tslang_add_test(NAME test-compile-00-const-interface-of-local COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_interface_of_local.ts") +tslang_add_test(NAME test-compile-00-self-assign COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00self_assign.ts") tslang_add_test(NAME test-compile-00-string-view-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.ts") tslang_add_test(NAME test-compile-00-global-copy-counted COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_copy_counted.ts") tslang_add_test(NAME test-compile-00-global-interface-initializer COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_interface_initializer.ts") @@ -997,6 +998,7 @@ tslang_add_test(NAME test-jit-00-generator-temporaries COMMAND test-runner -jit tslang_add_test(NAME test-jit-00-conditional-named-const-release COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00conditional_named_const_release.ts") tslang_add_test(NAME test-jit-00-array-length-valid COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_valid.ts") tslang_add_test(NAME test-jit-00-const-interface-of-local COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_interface_of_local.ts") +tslang_add_test(NAME test-jit-00-self-assign COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00self_assign.ts") tslang_add_test(NAME test-jit-00-string-view-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.ts") tslang_add_test(NAME test-jit-00-global-copy-counted COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_copy_counted.ts") tslang_add_test(NAME test-jit-00-global-interface-initializer COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_interface_initializer.ts") @@ -1986,6 +1988,7 @@ set(TSLANG_CORPUS 00conditional_named_const_release.ts 00array_length_valid.ts 00const_interface_of_local.ts + 00self_assign.ts 00string_view_copy.ts 00global_copy_counted.ts 00global_interface_initializer.ts @@ -3853,6 +3856,10 @@ tslang_add_test(NAME test-compile-own-00-generator-temporaries COMMAND test-runn tslang_add_test(NAME test-jit-own-00-const-interface-of-local COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_interface_of_local.ts") tslang_add_test(NAME test-compile-own-00-const-interface-of-local COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_interface_of_local.ts") +# A local given its own value keeps it under own: `n = n` stores nothing. +tslang_add_test(NAME test-jit-own-00-self-assign COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00self_assign.ts") +tslang_add_test(NAME test-compile-own-00-self-assign COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00self_assign.ts") + # Shared (spec 23): counted handles. These are not in the own_test list, whose # test-own-no-counting check forbids __tslang_inc_ref: a handle is counted under own by design. # own_shared_user_named: a program's own class named Shared wins over the built-in (spec 23.1). diff --git a/tslang/test/tester/tests/00self_assign.ts b/tslang/test/tester/tests/00self_assign.ts new file mode 100644 index 000000000..a968d5e5a --- /dev/null +++ b/tslang/test/tester/tests/00self_assign.ts @@ -0,0 +1,50 @@ +// A local given its own value - `n = n`, or `n = (n = mk(5), n)` - keeps it, and can be moved or read +// afterwards. Under own the store back read the value out of the local first, which counted as moving +// it, so the release of what the local held came after the move and the program was refused. Each +// read follows allocations of the same size, so a freed block would be reused. +class V { + tag = ""; + constructor(public x: number) { + this.tag = "v" + x; + } +} + +class Junk { + a = -1; + b = -2; + tag = "junk"; +} + +function churn() { + let keep: Junk[] = []; + for (let i = 0; i < 1000; i++) keep.push(new Junk()); + return keep.length; +} + +function mk(x: number) { + return new V(x); +} + +function take(v: V) { + churn(); + return v.tag == "v" + v.x ? v.x : -1; +} + +function main() { + let s = 0; + for (let i = 0; i < 3; i++) { + let n = mk(i); + n = n; + const m = n; + churn(); + s += take(m); + } + assert(s == 3, "n = n, then moved"); + + let k = mk(1); + k = (k = mk(5), k); + churn(); + assert(take(k) == 5, "n = (n = mk(5), n)"); + + print("done."); +}