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."); +}