From 633674f2e8760b82e72541e9407c9d7a221557ef Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 4 Oct 2026 17:08:30 +0100 Subject: [PATCH] A const record whose field is made for it has storage, as a let does (#485) A record literal with a field known only at run time is built in a temporary slot and read out whole, and the literal takes no count for what its fields are given: its receiver does, as a `let` does with RetainSlot. A `const` of it was folded into that read unless a field was read out of another object or an owning local (#454, #460), so for a field made for the literal nothing took a count and nothing gave one back: - an array or tuple built in the literal (`{ items: [i, 2], n: i }`), a constant array copied for it, or a new array, born with no count, was never freed under rc and own (AOT, 1M iterations: 96.5 MB, 4.4 with `let`); - under own, a string, a call's result or a new instance made for it leaked as well (35.2 MB for `{ s: "a" + i, n: i }`), and the call and the instance did not compile. Such a const now has storage. A value taken out of another record (`{ ...o }`, ts.ExtractProperty / ts.DeconstructTuple) stays with that record's count, as before, and a literal string is immortal. Under own, a record whose field is a tuple or record holding an array gives the compile error a `let` of it already gave, where it used to compile and leak. Closes #485 Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 42 +++++++--- tslang/test/tester/CMakeLists.txt | 9 +++ tslang/test/tester/const-record-release.cmake | 28 +++++++ .../tests/00const_record_owned_fields.ts | 79 +++++++++++++++++++ 4 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 tslang/test/tester/const-record-release.cmake create mode 100644 tslang/test/tester/tests/00const_record_owned_fields.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 4ac35df3d..0863c8e67 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1476,9 +1476,16 @@ class MLIRGenImpl } // A record read out whole from the temporary slot an object literal is built in (one that - // owns nothing of its own), with a field given a value read out of another object's field - // or element, or out of an owning local: what `{ s: p.child }` and `{ x: a }` are. Such a - // value carries no reference, and the literal takes none for it. + // owns nothing of its own), with a field that owns a block and that only the literal's + // receiver counts - a `let` does, with RetainSlot - so a `const` of it needs storage like a + // `let`. Folded: + // - a value read out of another object's field or element, or out of an owning local + // (`{ s: p.child }`, `{ x: a }`), was held with no count at all (#454, #460); + // - an array or tuple built in the literal (`{ items: [i, 2] }`), or a new array, born with + // no count, was never given back, and under own neither was a string, a call's result or + // a new instance made for it (#485). + // A value taken out of another record (`{ ...o }`) stays with that record's count, and a + // literal string is immortal. static bool readsLiteralSlot(MLIRTypeHelper &mth, mlir::Location location, mlir::Value value) { auto loadOp = value ? value.getDefiningOp() : mlir_ts::LoadOp(); @@ -1520,19 +1527,32 @@ class MLIRGenImpl stored = castOp.getIn(); } - auto storedLoad = stored.getDefiningOp(); - auto *place = storedLoad ? storedLoad.getReference().getDefiningOp() : nullptr; - if (place && isa(place)) + auto *producer = stored.getDefiningOp(); + if (!producer || isa(producer)) { - return true; + continue; } - // a local that owns what it holds (`{ x: a }`, #454's shape), or a literal - // nested in this one - if (readsLocalSlot(stored) || readsLiteralSlot(mth, location, stored)) + if (auto storedLoad = dyn_cast(producer)) { - return true; + auto *place = storedLoad.getReference().getDefiningOp(); + if ((place && isa(place)) || + readsLocalSlot(stored) || readsLiteralSlot(mth, location, stored)) + { + return true; + } + + continue; } + + if (isa(producer) && + !isa(stored.getType())) + { + continue; + } + + // made for the literal + return true; } } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 91e1c62fa..aaefe1548 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -348,6 +348,7 @@ tslang_add_test(NAME test-compile-00-array-rest-copy COMMAND test-runner "${PROJ tslang_add_test(NAME test-compile-00-arrays6 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array6.ts") tslang_add_test(NAME test-compile-00-array-static-nested COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_static_nested.ts") tslang_add_test(NAME test-compile-00-array-literal-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_literal_copy.ts") +tslang_add_test(NAME test-compile-00-const-record-owned-fields COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.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-string-view-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.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") @@ -852,6 +853,7 @@ tslang_add_test(NAME test-jit-00-array-rest-copy COMMAND test-runner -jit "${PRO tslang_add_test(NAME test-jit-00-arrays6 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array6.ts") tslang_add_test(NAME test-jit-00-array-static-nested COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_static_nested.ts") tslang_add_test(NAME test-jit-00-array-literal-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_literal_copy.ts") +tslang_add_test(NAME test-jit-00-const-record-owned-fields COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.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-string-view-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.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") @@ -1685,6 +1687,7 @@ set(TSLANG_CORPUS 00array6.ts 00array_static_nested.ts 00array_literal_copy.ts + 00const_record_owned_fields.ts 00array_length_valid.ts 00string_view_copy.ts 00array_data_string_alias.ts @@ -2703,6 +2706,12 @@ add_test(NAME test-own-string-copy-in-store-block "-DFILE=${PROJECT_SOURCE_DIR}/test/tester/own/own_string_copy_branch.ts" -P "${CMAKE_CURRENT_SOURCE_DIR}/own-string-copy-block.cmake") +# A `const` record literal's slot is released under rc (#485); no run can see the leak, so the IR is read. +add_test(NAME test-rc-const-record-release + COMMAND ${CMAKE_COMMAND} -DTSLANG=$ + "-DFILE=${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.ts" + -P "${CMAKE_CURRENT_SOURCE_DIR}/const-record-release.cmake") + # 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/const-record-release.cmake b/tslang/test/tester/const-record-release.cmake new file mode 100644 index 000000000..64eddc246 --- /dev/null +++ b/tslang/test/tester/const-record-release.cmake @@ -0,0 +1,28 @@ +# A `const` record literal built at run time is a variable whose slot is released (#485): under rc +# each of the seven records in 00const_record_owned_fields.ts has a `ts.ReleaseSlot` of its tuple +# slot. Folded, as before, nothing counted an array or tuple built in the literal, and no run shows +# the leak that left, so the IR is read. +cmake_minimum_required(VERSION 3.17.3) +execute_process(COMMAND "${TSLANG}" --emit=mlir-affine --no-default-lib -mm=rc "${FILE}" + OUTPUT_VARIABLE out ERROR_VARIABLE err RESULT_VARIABLE result) +# the IR is printed to stderr +string(APPEND out "${err}") +if(NOT result EQUAL 0) + message(FATAL_ERROR "tslang failed (${result}):\n${out}") +endif() +foreach(function arrayField stringField callField newField tupleField newArrayField nestedField) + string(FIND "${out}" "ts.Func @${function} " begin) + if(begin EQUAL -1) + message(FATAL_ERROR "no function `${function}` in:\n${out}") + endif() + string(SUBSTRING "${out}" ${begin} -1 body) + string(SUBSTRING "${body}" 1 -1 rest) + string(FIND "${rest}" "ts.Func @" end) + if(NOT end EQUAL -1) + math(EXPR end "${end} + 1") + string(SUBSTRING "${body}" 0 ${end} body) + endif() + if(NOT body MATCHES "\"ts[.]ReleaseSlot\"[(]%[0-9]+[)] : [(]!ts[.]ref