From 0e63430e44c9b10b2c6d2b863eb09289072f1f30 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 19:43:38 +0100 Subject: [PATCH] -mm=own: `let [a, b] = pair` borrows each element, as `let a = pair[0]` does A destructuring declaration reads each element behind a test of the array's length whose other side is the default, so the local's value arrived through a merge, and own refused it ("merged from several branches") though the plain read `let a = pair[0]` is an element borrow. placeReadOf now sees through a merge whose every other way in passes a default or what holds no block (soleBlockMergedInto): the local is that element read, and checkPlaceRead's walk from the read already follows the branch to it, so reassigning or shrinking the array, storing, returning or capturing the local is still an error. A string element is copied, as any string read out of an element is. Own corpus 533 -> 534 (00owned_locals), none lost. Tests: own_destructure_elements under every model, 00owned_locals under own, two negatives with pinned messages. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/OwnershipFacts.h | 33 +++++++++++ .../OwnershipInferencePlaceReads.cpp | 7 +++ tslang/test/tester/CMakeLists.txt | 16 +++++- .../tester/own/own_destructure_elements.ts | 57 +++++++++++++++++++ .../own_err_destructure_element_reassigned.ts | 11 ++++ .../own_err_destructure_element_returned.ts | 14 +++++ 6 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 tslang/test/tester/own/own_destructure_elements.ts create mode 100644 tslang/test/tester/own/own_err_destructure_element_reassigned.ts create mode 100644 tslang/test/tester/own/own_err_destructure_element_returned.ts diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 992003b45..7b461da4c 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -831,6 +831,39 @@ inline bool argumentHoldsNoBlock(mlir::Value arg) return !mlir::isa(arg.getType()) && holdsNoBlock(arg); } +// The one value a merge can bring a block in by: every other way into the block passes a default +// (`ts.Default`, `ts.Null`, `ts.Undef`) or what holds no block. `let [x, y] = a` reads each element +// behind a test of the array's length whose other side is the default, so `x` is that element read. +// Null when no way in, or more than one, passes a block. +inline mlir::Value soleBlockMergedInto(mlir::Value value) +{ + auto argument = mlir::dyn_cast(value); + llvm::SmallVector passed; + if (!argument || !mergedInto(argument, passed)) + { + return {}; + } + + mlir::Value sole; + for (auto incoming : passed) + { + if (mlir::isa_and_nonnull(incoming.getDefiningOp()) || + holdsNoBlock(incoming)) + { + continue; + } + + if (sole && sole != incoming) + { + return {}; + } + + sole = incoming; + } + + return sole; +} + // A value -mm=own copies rather than give a second owner (spec 22.1): a string, a string literal, // a union of strings and null that needs no tag (one pointer), or an optional of one of those // (`{ptr, i1}`). diff --git a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp index 7a004e6e2..6bf732325 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp @@ -11,6 +11,13 @@ namespace own_inference mlir::Operation *OwnershipInferencePass::placeReadOf(mlir::Value value) { auto root = rootOf(value); + // a read behind a test whose other side is the default (`let [x] = a`) is that read; the + // walk from it (checkPlaceRead) goes on through the branch to what takes the merge + if (auto sole = soleBlockMergedInto(root)) + { + root = sole; + } + auto *def = root.getDefiningOp(); // a handle read out of a place that rc gives an owner of its own is a counted copy, not a // borrow (spec 23.3); any other is a borrow of the place, as a field read is diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 4253ea47e..a05c5de54 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3600,7 +3600,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_field_chain_let own_read_after_move own_conditional_move own_interface_facts own_declared_scalar_no_drops own_borrowing_record own_borrowing_record_arg - own_reduce own_fresh_call_result own_ternary_outer_loop) + own_reduce own_fresh_call_result own_ternary_outer_loop own_destructure_elements) tslang_add_test(NAME test-jit-own-${own_test} COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") tslang_add_test(NAME test-compile-own-${own_test} COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") # The promise: no counting at all in what own emits. @@ -3686,6 +3686,16 @@ foreach(ternary_loop_mm rc gc none) tslang_add_test(NAME test-compile-${ternary_loop_mm}-own_ternary_outer_loop COMMAND test-runner -mm=${ternary_loop_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_ternary_outer_loop.ts") endforeach() +# `let [a, b] = pair` reads each element behind a length test; under own the local is a borrow of the +# element, as `let a = pair[0]` is. The same program under every other model, and the corpus file +# whose destructuring own refused. +foreach(destructure_mm rc gc none) + tslang_add_test(NAME test-jit-${destructure_mm}-own_destructure_elements COMMAND test-runner -jit -mm=${destructure_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_destructure_elements.ts") + tslang_add_test(NAME test-compile-${destructure_mm}-own_destructure_elements COMMAND test-runner -mm=${destructure_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_destructure_elements.ts") +endforeach() +tslang_add_test(NAME test-jit-own-00-owned-locals COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_locals.ts") +tslang_add_test(NAME test-compile-own-00-owned-locals COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_locals.ts") + # A call through an interface closed in its module gets its implementations' facts (spec 31); the # same program under every other model. foreach(interface_facts_mm rc gc none) @@ -4201,7 +4211,9 @@ set(own_error_cases "own_err_borrowing_record_kept_param|:30:12: error: '[^']*' borrows '[^']*' but is used here after it may be released or overwritten" "own_err_borrowing_record_place_overwritten|:28:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_borrowing_record_open|:17:31: error: '[^']*' borrows '[^']*' and cannot be stored, returned or captured" - "own_err_borrowing_record_borrowed_again|:29:11: error: '[^']*' borrows the result of 'A[.]hit' but is used here after it may be released or overwritten") + "own_err_borrowing_record_borrowed_again|:29:11: error: '[^']*' borrows the result of 'A[.]hit' but is used here after it may be released or overwritten" + "own_err_destructure_element_reassigned|:10:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_destructure_element_returned|:9:5: error: '[^']*' borrows an element and cannot be stored, returned or captured") foreach(own_error_case ${own_error_cases}) string(FIND "${own_error_case}" "|" own_error_sep) string(SUBSTRING "${own_error_case}" 0 ${own_error_sep} own_error_name) diff --git a/tslang/test/tester/own/own_destructure_elements.ts b/tslang/test/tester/own/own_destructure_elements.ts new file mode 100644 index 000000000..d260db708 --- /dev/null +++ b/tslang/test/tester/own/own_destructure_elements.ts @@ -0,0 +1,57 @@ +// `let [a, b] = pair` reads each element behind a test of the array's length whose other side is +// the default. Under own that merge was refused ("merged from several branches"), though the plain +// read `let a = pair[0]` is a borrow of the element: the destructured local is that borrow, and a +// string is copied, as a string read out of an element always is (spec 22). Each read follows +// allocations of the same size, so a freed block would be reused. +class N { + tag = ""; + constructor(public x: number) { + this.tag = "n" + x; + } +} + +function churn() { + const keep: N[] = []; + for (let i = 0; i < 1000; i++) keep.push(new N(777)); + return keep.length; +} + +function mk(i: number) { + return "elem" + i; +} + +function classes() { + let pair = [new N(1), new N(2)]; + let [a, b] = pair; + churn(); + assert(a.tag == "n1" && b.tag == "n2", "class elements"); + return a.x + b.x; +} + +function strings() { + let s = [mk(1), mk(2)]; + let [x, y] = s; + s = [mk(9)]; + churn(); + assert(x == "elem1" && y == "elem2", "strings outlive their array"); + return x.length + y.length; +} + +function inLoop() { + let pair = [new N(3), new N(4)]; + let sum = 0; + for (let i = 0; i < 3; i++) { + let [a, b] = pair; + churn(); + sum += a.x * b.x; + } + + return sum; +} + +function main() { + assert(classes() == 3, "classes"); + assert(strings() == 10, "strings"); + assert(inLoop() == 36, "in a loop"); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_destructure_element_reassigned.ts b/tslang/test/tester/own/own_err_destructure_element_reassigned.ts new file mode 100644 index 000000000..423aedcc9 --- /dev/null +++ b/tslang/test/tester/own/own_err_destructure_element_reassigned.ts @@ -0,0 +1,11 @@ +// `let [a] = pair` borrows pair's first element: giving pair a new array frees the one `a` reads. +class N { + constructor(public x: number) {} +} + +function main() { + let pair = [new N(1), new N(2)]; + let [a] = pair; + pair = [new N(3)]; + print(a.x); +} diff --git a/tslang/test/tester/own/own_err_destructure_element_returned.ts b/tslang/test/tester/own/own_err_destructure_element_returned.ts new file mode 100644 index 000000000..5c951cf59 --- /dev/null +++ b/tslang/test/tester/own/own_err_destructure_element_returned.ts @@ -0,0 +1,14 @@ +// `let [a] = pair` borrows pair's first element, which the end of the function releases with pair. +class N { + constructor(public x: number) {} +} + +function first() { + let pair = [new N(1), new N(2)]; + let [a] = pair; + return a; +} + +function main() { + print(first().x); +}