From e0cc30435f4895c207224161a8904e8b2d8c9fb9 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 21:59:53 +0100 Subject: [PATCH] -mm=own: a tuple of owned values, and a record a call returns, are values Three refusals of one shape, a tuple or record of owned values as a value: - `[mk(1), mk(2)]` as `[N, N]`, built in a local or returned: `ts.CreateTuple` took each value beside the temporary's own release, a second reference own could not place. Under own a tuple literal holding a value the expression made is built in a slot, as an object literal is (createTupleInSlot), which own reads as the literal taking it. A tuple of literals or of reads (`[items[1]]`, own_facts::isConstantData) stays one `ts.CreateTuple`. - `const r = rec(); r.a.x + r.b.x`: a field taken out of the record value (`ts.ExtractProperty`) moved it, so the second read was a use after the move. A field only read is a read of the record (fieldOnlyRead), and so is each read of it, so a use after a move of the record is still one. - `let [a, b] = pair()`: each `let` takes a field out of the returned record. It borrows the field, bounded by the record's release, as a `let` borrows a temporary (fieldTakersOf in checkValueMoves); decideRetains skips the `let`s' retains that borrow already erased. Still errors (pinned): returning a field or a destructured borrow, a field read after the record moved into a callee that keeps it, and a spec-32 borrowing record's field read after its argument is overwritten. Own corpus 535 -> 537 (00tuple_runtime_arrays, arrayLiterals3; same output as gc), none lost. Tests: own_tuple_record_values under every model and own's no-counting check, four negatives. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 63 ++++++++++++++- .../TypeScript/OwnershipInferenceMoves.cpp | 51 ++++++++++++- .../lib/TypeScript/OwnershipInferencePass.cpp | 5 ++ .../lib/TypeScript/OwnershipInferencePass.h | 9 +++ tslang/test/tester/CMakeLists.txt | 17 ++++- .../own_err_borrowing_record_const_field.ts | 21 +++++ .../own_err_destructured_field_returned.ts | 22 ++++++ .../own/own_err_record_field_after_move.ts | 25 ++++++ .../own/own_err_record_field_returned.ts | 18 +++++ .../tester/own/own_tuple_record_values.ts | 76 +++++++++++++++++++ 10 files changed, 303 insertions(+), 4 deletions(-) create mode 100644 tslang/test/tester/own/own_err_borrowing_record_const_field.ts create mode 100644 tslang/test/tester/own/own_err_destructured_field_returned.ts create mode 100644 tslang/test/tester/own/own_err_record_field_after_move.ts create mode 100644 tslang/test/tester/own/own_err_record_field_returned.ts create mode 100644 tslang/test/tester/own/own_tuple_record_values.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index d2a4f340a..e386789fb 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -67,6 +67,9 @@ #include "TypeScript/MLIRLogic/MLIRGenContextDefines.h" +// own_facts::isConstantData: -mm=own builds a tuple of values that own blocks in a slot +#include "OwnershipFacts.h" + #include #include #include @@ -10722,8 +10725,66 @@ class MLIRGenImpl arrayValues.push_back(value); } + // -mm=own: a tuple of values that own blocks (`[mk(1), mk(2)]` as `[N, N]`) is built as an object + // literal is, in a slot each value is stored into, which own reads as the literal taking them + // (spec 26). Made at once by `ts.CreateTuple`, each value had a second reference own could not + // place: the tuple's, and the temporary's own release. + // A tuple of data nothing owns (literals, `[[1, "x"], 2]`), or of reads (`[items[1]]`), stays one value. + auto isRead = [](mlir::Value value) { + while (auto *def = value.getDefiningOp()) + { + if (!mlir::isa(def) && !own_facts::isView(def)) + { + break; + } + + value = def->getOperand(0); + } + + return !!value.getDefiningOp(); + }; + if (compileOptions.memoryModel == MemoryModelOwn && + llvm::any_of(arrayValues, [&](mlir::Value value) { return !own_facts::isConstantData(value) && !isRead(value); })) + { + return createTupleInSlot(location, fieldInfos, arrayValues, genContext); + } + return V(builder.create(location, getTupleType(fieldInfos), arrayValues)); - } + } + + // A tuple built in a slot, as mlirGenCreateTuple builds an object literal: a constant seed holds each + // constant value, and every other is stored into its field. + ValueOrLogicalResult createTupleInSlot(mlir::Location location, SmallVector &fieldInfos, + ArrayRef values, const GenContext &genContext) + { + SmallVector seed; + for (auto value : values) + { + auto constOp = value.getDefiningOp(); + seed.push_back(constOp ? constOp.getValueAttr() : mlir::Attribute(builder.getUnitAttr())); + } + + auto tupleType = getTupleType(fieldInfos); + auto seedValue = builder.create(location, getConstTupleType(fieldInfos), + mlir::ArrayAttr::get(builder.getContext(), seed)); + auto tupleVar = builder.create(location, mlir_ts::RefType::get(tupleType), seedValue, + builder.getBoolAttr(false), builder.getIndexAttr(0)); + for (auto [index, value] : llvm::enumerate(values)) + { + if (value.getDefiningOp()) + { + continue; + } + + auto fieldRef = builder.create( + location, mlir_ts::RefType::get(fieldInfos[index].type), tupleVar, builder.getI32IntegerAttr(index)); + auto field = builder.create(location, fieldInfos[index].type, fieldRef); + auto saved = mlirGenSaveLogicOneItem(location, field, value, genContext); + EXIT_IF_FAILED(saved) + } + + return V(builder.create(location, tupleType, tupleVar)); + } ValueOrLogicalResult createFixedSizeArrayFromArrayLiteral(mlir::Location location, ArrayRef values, struct ArrayInfo arrayInfo, const GenContext &genContext) { diff --git a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp index 8f4e74c56..9f2ce51d6 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp @@ -521,10 +521,17 @@ namespace own_inference } }); - // a merge that only borrows it (movesOf) takes nothing + // a merge that only borrows it (movesOf) takes nothing; a field taken out of a record (`let [a, b] = + // pair()`) is taken by what takes the field llvm::SmallVector takers; for (auto [user, used] : notReads) { + if (mlir::isa(user)) + { + fieldTakersOf(user, takers); + continue; + } + llvm::SmallVector uses; if (!borrowsThroughMerge(user, used, value.getDefiningOp(), releases, uses, toErase)) { @@ -686,6 +693,42 @@ namespace own_inference return true; } + bool OwnershipInferencePass::fieldOnlyRead(mlir::Operation *extract, llvm::SmallVectorImpl &reads) + { + auto onlyRead = true; + forEachUse(extract->getResult(0), [&](mlir::Operation *user, mlir::Value used) { + if (mlir::isa(user)) + { + onlyRead = fieldOnlyRead(user, reads) && onlyRead; + return; + } + + if (!isBorrow(user, used)) + { + onlyRead = false; + return; + } + + reads.push_back(user); + }); + + return onlyRead; + } + + void OwnershipInferencePass::fieldTakersOf(mlir::Operation *extract, llvm::SmallVectorImpl &takers) + { + forEachUse(extract->getResult(0), [&](mlir::Operation *user, mlir::Value used) { + if (mlir::isa(user)) + { + fieldTakersOf(user, takers); + } + else if (!mlir::isa(user) && !isBorrow(user, used)) + { + takers.push_back(user); + } + }); + } + bool OwnershipInferencePass::movesOf(mlir::Value value, llvm::SetVector &toErase) { llvm::SmallVector takers; @@ -699,6 +742,7 @@ namespace own_inference return; } + llvm::SmallVector fieldReads; if (mlir::isa(user)) { releases.push_back(user); @@ -708,6 +752,11 @@ namespace own_inference reads.push_back(user); readOf.try_emplace(user, used); } + else if (mlir::isa(user) && fieldOnlyRead(user, fieldReads)) + { + reads.push_back(user); + reads.append(fieldReads.begin(), fieldReads.end()); + } else if (!mergedValuesOf(user, used).empty()) { merges.push_back({user, used}); diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index eb8685567..e174204f5 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -326,6 +326,11 @@ namespace own_inference continue; // a `let` that borrows a merge: borrowsThroughMerge erased it } + if (toErase.contains(op)) + { + continue; // decided already: a `let` that borrows a field of a record (checkValueMoves) + } + // a retain of what holds no block takes nothing: a union's number payload made into // another union is a view of the union it came from, but a number all the way if (auto retainOp = mlir::dyn_cast(op); retainOp && holdsNoBlock(retainOp.getReference())) diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 58f0986a9..edd683081 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -933,6 +933,15 @@ class OwnershipInferencePass // The move verdict for an SSA value (phase 1). Reports and returns false when it is not one. bool movesOf(mlir::Value value, llvm::SetVector &toErase); + // A field taken out of a record value (`ts.ExtractProperty`, `const r = rec(); r.a.x`) only to be + // read, through any depth of fields: a read of the record, as a field read through a slot is. + // Collects what reads it, each a read of the record that can come after a move of it. + bool fieldOnlyRead(mlir::Operation *extract, llvm::SmallVectorImpl &reads); + + // What takes a field taken out of a record value, through any depth of fields: each use that is not a + // read. checkValueMoves lets them borrow it when each is a `let` its record's release bounds. + void fieldTakersOf(mlir::Operation *extract, llvm::SmallVectorImpl &takers); + // What a merge of branches makes of `used` (`c ? h : h2`): the result of an `arith.select` it // is a choice of, or each block argument a branch passes it as. Empty for any other use. static llvm::SmallVector mergedValuesOf(mlir::Operation *user, mlir::Value used); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 5f52cdbc2..09c5b4bdb 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3603,7 +3603,8 @@ 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_destructure_elements) + own_reduce own_fresh_call_result own_ternary_outer_loop own_destructure_elements + own_tuple_record_values) 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. @@ -3706,6 +3707,14 @@ 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 tuple of owned values built or returned as a value, and a record a call returned read through a +# `const` or destructured: own builds the tuple in a slot as an object literal, and a field read out of +# the record is a read of it. The same program under every other model. +foreach(tuple_record_mm rc gc none) + tslang_add_test(NAME test-jit-${tuple_record_mm}-own_tuple_record_values COMMAND test-runner -jit -mm=${tuple_record_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_tuple_record_values.ts") + tslang_add_test(NAME test-compile-${tuple_record_mm}-own_tuple_record_values COMMAND test-runner -mm=${tuple_record_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_tuple_record_values.ts") +endforeach() + # 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) @@ -4228,7 +4237,11 @@ set(own_error_cases "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_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") + "own_err_destructure_element_returned|:9:5: error: '[^']*' borrows an element and cannot be stored, returned or captured" + "own_err_record_field_returned|:12:5: error: '[^']*' borrows a field and cannot be stored, returned or captured" + "own_err_destructured_field_returned|:16:5: error: '[^']*' borrows '[^']*' and cannot be stored, returned or captured" + "own_err_record_field_after_move|:24:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_borrowing_record_const_field|:20:11: error: '[^']*' borrows the result of 'hit' but is used here after it may be released or overwritten") 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_err_borrowing_record_const_field.ts b/tslang/test/tester/own/own_err_borrowing_record_const_field.ts new file mode 100644 index 000000000..46c2d4d2b --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowing_record_const_field.ts @@ -0,0 +1,21 @@ +// A record a call returns that borrows its argument (spec 32), held in a `const`: a field read out of +// it reads the borrowed argument, so it cannot come after the argument is overwritten. +class Ray { + constructor(public x: number) {} +} + +interface Hit { + ray: Ray; + t: number; +} + +function hit(r: Ray): Hit { + return { ray: r, t: 1 }; +} + +function main() { + let r = new Ray(1); + const h = hit(r); + r = new Ray(2); + print(h.ray.x); +} diff --git a/tslang/test/tester/own/own_err_destructured_field_returned.ts b/tslang/test/tester/own/own_err_destructured_field_returned.ts new file mode 100644 index 000000000..7973377c7 --- /dev/null +++ b/tslang/test/tester/own/own_err_destructured_field_returned.ts @@ -0,0 +1,22 @@ +// `let [a] = pair()` borrows the returned tuple's first field, which the end of the function releases. +class N { + constructor(public x: number) {} +} + +function mk(i: number) { + return new N(i); +} + +function pair(): [N, N] { + return [mk(1), mk(2)]; +} + +function first() { + let [a] = pair(); + return a; +} + +function main() { + print(first().x); +} + diff --git a/tslang/test/tester/own/own_err_record_field_after_move.ts b/tslang/test/tester/own/own_err_record_field_after_move.ts new file mode 100644 index 000000000..664a0496f --- /dev/null +++ b/tslang/test/tester/own/own_err_record_field_after_move.ts @@ -0,0 +1,25 @@ +// A field read out of a record is a read of the record: given away to a callee that keeps it, the record +// is gone, and the field read after that reads freed memory. +class N { + constructor(public x: number) {} +} + +function mk(i: number) { + return new N(i); +} + +class K { + recs: { a: N; b: N }[] = []; +} + +function keep(k: K, r: { a: N; b: N }) { + k.recs.push(r); +} + +function main() { + const k = new K(); + const r = { a: mk(1), b: mk(2) }; + const a = r.a; + keep(k, r); + print(a.x); +} diff --git a/tslang/test/tester/own/own_err_record_field_returned.ts b/tslang/test/tester/own/own_err_record_field_returned.ts new file mode 100644 index 000000000..d75a76ce1 --- /dev/null +++ b/tslang/test/tester/own/own_err_record_field_returned.ts @@ -0,0 +1,18 @@ +// A field of a record the function made is the record's: returning it alone would outlive the record. +class N { + constructor(public x: number) {} +} + +function mk(i: number) { + return new N(i); +} + +function first() { + const r = { a: mk(1), b: mk(2) }; + return r.a; +} + +function main() { + print(first().x); +} + diff --git a/tslang/test/tester/own/own_tuple_record_values.ts b/tslang/test/tester/own/own_tuple_record_values.ts new file mode 100644 index 000000000..9a64574c7 --- /dev/null +++ b/tslang/test/tester/own/own_tuple_record_values.ts @@ -0,0 +1,76 @@ +// A tuple or record of owned values as a value: an array literal typed as a tuple of objects +// (`[mk(1), mk(2)]` as `[N, N]`), built in a local or returned, and a returned record read through a +// `const` or destructured into `let`s. Under own the tuple literal took a second reference of each +// value (`ts.CreateTuple` beside the temporaries' releases), and a field taken out of a `const` record +// moved it, so a second read was a use after the move. 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 new N(i); +} + +function pair(): [N, N] { + return [mk(1), mk(2)]; +} + +function mixed(): [N, number] { + return [mk(3), 4]; +} + +function rec() { + return { a: mk(5), b: mk(6) }; +} + +function check(n: N, x: number, what: string) { + assert(n.x == x && n.tag == "n" + x, what); +} + +function main() { + const local: [N, N] = [mk(7), mk(8)]; + churn(); + check(local[0], 7, "a local tuple's first"); + check(local[1], 8, "a local tuple's second"); + + const p = pair(); + churn(); + check(p[0], 1, "a returned tuple's first"); + check(p[1], 2, "a returned tuple's second"); + + const m = mixed(); + churn(); + check(m[0], 3, "a mixed tuple's object"); + assert(m[1] == 4, "a mixed tuple's number"); + + const r = rec(); + churn(); + check(r.a, 5, "a const record's first field"); + check(r.b, 6, "a const record's second field"); + check(r.a, 5, "a const record's first field, read again"); + + let [a, b] = pair(); + churn(); + check(a, 1, "destructured first"); + check(b, 2, "destructured second"); + + let sum = 0; + for (let i = 0; i < 3; i++) { + const q = pair(); + churn(); + sum += q[0].x + q[1].x; + } + + assert(sum == 9, "a tuple per iteration"); + print("done."); +}