From 7591dd1e04bf0f44a7692bd9b055a2017382383d Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 4 Oct 2026 12:54:26 +0100 Subject: [PATCH] Arrays in an inferred tuple or record literal are heap copies (#479) A tuple or record literal whose values are all constants is folded to a ts.Constant, and each array in it was lowered to a header in constant global data over the literal's constant data. push reallocated that global; since #482 pop, shift, splice and `length =` wrote into it, and the optimiser folded the reads back to the initializer, so they gave silently wrong results. Such a literal is now rebuilt with heap copies of its arrays wherever it becomes a value that can be changed: - a `const` of it gets storage and is widened to a tuple, as a `const` of a constant array is (processConstRef, adjustLocalVariableType, adjustGlobalVariableType); - its cast to a tuple or a class rebuilds it from the literal's attributes (copyArraysOfConstTuple), nested tuple literals included; - a constant array of such tuples is built element by element (castConstArrayToArray); - inside a tuple literal built at run time, it is cast to a tuple; - a record literal built in a slot (a field known only at run time, or boxed) sets its constant array fields as heap copies. Under rc a module-level array or tuple literal is born at count 0 and nothing took a count for the global, so the first local that read it and let go freed it: `const ga = [6, 7]` read through a `const` twice crashed (since #480 the length lives in the freed header). The global now retains what its initializer builds (OwnedReturnConsumptionPass, claimGlobalInitializers), as it already did for a call's result. Closes #479 Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/LowerToLLVM/ArrayLayout.h | 10 +-- .../TypeScript/MLIRLogic/MLIRTypeHelper.h | 27 +++++++ tslang/lib/TypeScript/MLIRGenCast.cpp | 76 ++++++++++++++++- tslang/lib/TypeScript/MLIRGenExpressions.cpp | 49 +++++++++-- tslang/lib/TypeScript/MLIRGenImpl.h | 27 +++++-- .../TypeScript/OwnedReturnConsumptionPass.cpp | 38 +++++++++ tslang/test/tester/CMakeLists.txt | 3 + .../test/tester/tests/00array_literal_copy.ts | 81 +++++++++++++++++++ .../tester/tests/00array_static_nested.ts | 5 +- 9 files changed, 296 insertions(+), 20 deletions(-) create mode 100644 tslang/test/tester/tests/00array_literal_copy.ts diff --git a/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h index 8ea46756f..a3549f38b 100644 --- a/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h +++ b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h @@ -106,11 +106,11 @@ class ArrayLayout : public LLVMCodeHelperBase // MemoryRealloc, which keeps an rc data block's count across the move and births one grown // from null with the header's reference. // - // A static header (makeStatic) is broken under the ops that change an array in place (#479): - // it lives in a constant global with capacity = length over static data, and pop, shift, - // splice and `length =` store the new length into that constant global and zero the - // vacated slots in the static data - undefined behaviour, which today reads back silently - // wrong values. Nothing here guards against it. + // A static header (makeStatic) must never reach the ops that change an array in place: it lives + // in a constant global with capacity = length over static data, and pop, shift, splice and + // `length =` would store into that global and zero slots of the static data. MLIRGen copies + // a literal's arrays to the heap before anything can change them (#479); nothing here guards + // against it. mlir::Value ensureCapacity(mlir_ts::ArrayType arrayType, mlir::Value header, mlir::Value needed) { TypeHelper th(rewriter); diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h index c60903023..b3790fbd9 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h @@ -435,6 +435,33 @@ class MLIRTypeHelper return mlir::Attribute(); } + // A tuple literal folded to a constant (`[1, [6, 7]]`, `{ items: [1, 2] }`) keeps each array + // in it as a static header over constant data (#479): such a tuple has to be rebuilt with + // heap copies before anything can change one of its arrays. + static bool constTupleHoldsArray(mlir::Type type) + { + ArrayRef fields; + if (auto constTupleType = dyn_cast(type)) + { + fields = constTupleType.getFields(); + } + else if (auto tupleType = dyn_cast(type)) + { + fields = tupleType.getFields(); + } + + return llvm::any_of(fields, [](auto &field) { + return isa(field.type) || constTupleHoldsArray(field.type); + }); + } + + // a `const` of such a tuple gets storage and is widened to a tuple, as a `const` of a constant + // array is widened to an array (processConstRef, adjustLocalVariableType) + static bool isConstTupleHoldingArray(mlir::Type type) + { + return isa(type) && constTupleHoldsArray(type); + } + mlir::Type wideStorageType(mlir::Type type) { auto actualType = type; diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index 2a47e6afd..5d97a3483 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -223,6 +223,54 @@ namespace mlirgen return mlir::success(); } + // A tuple literal folded to a constant keeps each array in it as a static header over constant + // data, so `pop`, `length =` and element writes changed a constant global and `push` reallocated + // one (#479). It is rebuilt here as a tuple whose arrays are heap copies, made from the + // literal's attributes the way castConstArrayToArray makes a nested array literal; a nested + // tuple literal holding an array is rebuilt in turn. Returns no value for anything else. + ValueOrLogicalResult MLIRGenImpl::copyArraysOfConstTuple(mlir::Location location, mlir::Value value, + mlir_ts::ConstTupleType constTupleType, const GenContext &genContext) + { + auto constOp = value.getDefiningOp(); + auto fieldAttrs = constOp ? dyn_cast(constOp.getValue()) : mlir::ArrayAttr(); + if (!fieldAttrs || fieldAttrs.size() != constTupleType.size() || !MLIRTypeHelper::constTupleHoldsArray(constTupleType)) + { + return mlir::Value(); + } + + SmallVector values; + for (auto [index, fieldInfo] : enumerate(constTupleType.getFields())) + { + auto fieldAttr = dyn_cast(fieldAttrs[index]); + mlir::Value literal; + if (auto arrayType = dyn_cast(fieldInfo.type); arrayType && fieldAttr) + { + literal = builder.create( + location, getConstArrayType(arrayType.getElementType(), fieldAttr.size()), fieldAttr); + } + else if (MLIRTypeHelper::constTupleHoldsArray(fieldInfo.type) && fieldAttr) + { + literal = builder.create( + location, mth.convertTupleTypeToConstTupleType(fieldInfo.type), fieldAttr); + } + + if (!literal) + { + MLIRPropertyAccessCodeLogic cl(compileOptions, builder, location, value, builder.getI32IntegerAttr(index)); + auto fieldValue = cl.Tuple(constTupleType, true); + VALIDATE(fieldValue, location) + values.push_back(fieldValue); + continue; + } + + CAST_A(copied, location, mth.convertConstTupleTypeToTupleType(fieldInfo.type), literal, genContext); + values.push_back(copied); + } + + auto tupleType = mlir::cast(mth.convertConstTupleTypeToTupleType(constTupleType)); + return V(builder.create(location, tupleType, values)); + } + ValueOrLogicalResult MLIRGenImpl::castTupleToTuple(mlir::Location location, mlir::Value value, mlir_ts::TupleType srcTupleType, ArrayRef fields, const GenContext &genContext, bool errorAsWarning) { @@ -1042,7 +1090,8 @@ namespace mlirgen // copies only the outer data to the heap, and the inner arrays kept pointing at the literal's // constant data, so `nested[1].push(4)` reallocated a global and `nested[1][0] = 9` wrote one if (constArrayType.getElementType() == arrayType.getElementType() - && !isa(arrayType.getElementType())) + && !isa(arrayType.getElementType()) + && !MLIRTypeHelper::constTupleHoldsArray(arrayType.getElementType())) { return std::nullopt; } @@ -1072,7 +1121,14 @@ namespace mlirgen for (auto elementAttr : elementAttrs) { mlir::Value element; - if (auto nestedAttrs = dyn_cast(elementAttr)) + if (auto nestedAttrs = dyn_cast(elementAttr); + nestedAttrs && MLIRTypeHelper::constTupleHoldsArray(sourceElementType)) + { + // a tuple holding an array (`[[1, [2]]]`): rebuilt with copies by its cast (#479) + element = builder.create( + location, mth.convertTupleTypeToConstTupleType(sourceElementType), nestedAttrs); + } + else if (auto nestedAttrs = dyn_cast(elementAttr)) { if (!nestedElementType) { @@ -1183,6 +1239,22 @@ namespace mlirgen // const tuple to tuple if (auto srcConstTupleType = dyn_cast(valueType)) { + if (isa(type)) + { + // the copy is a tuple of the type the casts below read the constant as + auto copied = copyArraysOfConstTuple(location, value, srcConstTupleType, genContext); + EXIT_IF_FAILED(copied) + if (auto copiedValue = V(copied)) + { + if (copiedValue.getType() == type) + { + return copied; + } + + value = copiedValue; + } + } + ::llvm::ArrayRef<::mlir::typescript::FieldInfo> fields; if (auto tupleType = dyn_cast(type)) { diff --git a/tslang/lib/TypeScript/MLIRGenExpressions.cpp b/tslang/lib/TypeScript/MLIRGenExpressions.cpp index a5af69853..3753a8c12 100644 --- a/tslang/lib/TypeScript/MLIRGenExpressions.cpp +++ b/tslang/lib/TypeScript/MLIRGenExpressions.cpp @@ -1821,12 +1821,6 @@ namespace mlirgen return mlir::failure(); } - auto constTupleTypeWithReplacedThis = getConstTupleType(oli.fieldInfos); - - auto arrayAttr = mlir::ArrayAttr::get(builder.getContext(), oli.values); - auto constantVal = - builder.create(location, constTupleTypeWithReplacedThis, arrayAttr); - // box any literal with a method/accessor as a reference-typed ObjectType, not just // synthetic wrappers carrying the explicit flag (see docs/object-literal-boxing-design.md // §3 gap 4): a method can only mutate its object through `this`, and predicting which @@ -1838,6 +1832,49 @@ namespace mlirgen (objectLiteral->internalFlags & InternalFlags::BoxAsObject) == InternalFlags::BoxAsObject || !oli.methodInfos.empty() || !oli.methodInfosWithCaptures.empty(); + // A literal built in a slot (a field known only at run time, or boxed) starts from the + // constant below, and an array kept in it would be a static header over constant data, + // which `push`, `pop` and element writes change (#479): such a field is set in the slot as a + // heap copy instead. A literal that stays a constant is copied where it is widened to a + // tuple (copyArraysOfConstTuple). + if (!oli.fieldsToSet.empty() || boxAsObject) + { + for (auto [index, fieldInfo] : enumerate(oli.fieldInfos)) + { + auto fieldAttr = dyn_cast(oli.values[index]); + if (!fieldAttr) + { + continue; + } + + mlir::Value literal; + if (auto arrayType = dyn_cast(fieldInfo.type)) + { + literal = builder.create( + location, getConstArrayType(arrayType.getElementType(), fieldAttr.size()), fieldAttr); + } + else if (MLIRTypeHelper::constTupleHoldsArray(fieldInfo.type)) + { + literal = builder.create( + location, mth.convertTupleTypeToConstTupleType(fieldInfo.type), fieldAttr); + } + else + { + continue; + } + + CAST_A(copied, location, fieldInfo.type, literal, genContext); + oli.fieldsToSet.push_back({fieldInfo.id, copied}); + oli.values[index] = builder.getUnitAttr(); + } + } + + auto constTupleTypeWithReplacedThis = getConstTupleType(oli.fieldInfos); + + auto arrayAttr = mlir::ArrayAttr::get(builder.getContext(), oli.values); + auto constantVal = + builder.create(location, constTupleTypeWithReplacedThis, arrayAttr); + if (oli.fieldsToSet.empty() && !boxAsObject) { return V(constantVal); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 178e28c78..a242b28f0 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1587,8 +1587,13 @@ class MLIRGenImpl // hands its fields over unretained - its receiver is the one that retains them, as a // `let` does - so folded, `o` held none of them, and `p.child = d` freed what `o.s` // still read (#460). + // + // And a tuple literal folded to a constant with an array in it (`const st = [1, [6, 7]]`, + // `const o = { items: [1, 2] }`): its arrays are static headers over constant data, so + // it is widened to a tuple of heap copies once, here, like a const array literal (#479). MLIRTypeHelper mth(builder.getContext(), compileOptions); needsIdentityStorage = mth.hasBoundMethodField(type) || isa(type) + || MLIRTypeHelper::isConstTupleHoldingArray(type) || ((isa(type) || MLIRTypeHelper::isSharedHandleType(type) || (readsLocalSlot(initial) && mth.ownsHeapMemory(location, type)) || (readsLiteralSlot(mth, location, initial) && isa(type) @@ -1806,10 +1811,11 @@ class MLIRGenImpl // write, ...) would still `ts.Cast` a fresh, disposable !ts.array copy // out of it instead of sharing one heap array through the slot. Every // other const type (generator wrapper tuples, plain values) keeps the - // early-return: this widening is only valid/needed for const_array. See + // early-return: this widening is only valid/needed for const_array (and a constant + // tuple holding an array, which castTupleLikeVariants rebuilds with copies, #479). See // docs/const-let-storage-design.md and the const-array note above // processConstRef. - if (isa(type) && variableDeclarationInfo.needsIdentityStorage) + if ((isa(type) || MLIRTypeHelper::isConstTupleHoldingArray(type)) && variableDeclarationInfo.needsIdentityStorage) { auto actualType = mth.removeConstType(type); if (variableDeclarationInfo.initial && actualType != type) @@ -1858,7 +1864,7 @@ class MLIRGenImpl // ` snapshot, or every mutating method/element write still operates // on a disposable ts.Cast copy or the read-only original. See the note // above processConstRef / const-let-storage-design.md. - if (isa(variableDeclarationInfo.type)) + if (isa(variableDeclarationInfo.type) || MLIRTypeHelper::isConstTupleHoldingArray(variableDeclarationInfo.type)) { auto type = variableDeclarationInfo.type; auto actualType = mth.removeConstType(type); @@ -9634,8 +9640,16 @@ class MLIRGenImpl SmallVector fieldInfos; for (auto val : values) { - fieldInfos.push_back({mlir::Attribute(), val.value.getType(), false, mlir_ts::AccessLevel::Public}); - arrayValues.push_back(val.value); + auto value = val.value; + // a constant tuple literal inside (`[x, [2, [3, 4]]]`) holds its arrays as static + // headers: it becomes a tuple of heap copies (#479) + if (MLIRTypeHelper::isConstTupleHoldingArray(value.getType())) + { + CAST(value, location, mth.convertConstTupleTypeToTupleType(value.getType()), value, genContext); + } + + fieldInfos.push_back({mlir::Attribute(), value.getType(), false, mlir_ts::AccessLevel::Public}); + arrayValues.push_back(value); } return V(builder.create(location, getTupleType(fieldInfos), arrayValues)); @@ -12063,6 +12077,9 @@ class MLIRGenImpl ValueOrLogicalResult mapTupleToFields(mlir::Location location, SmallVector &values, mlir::Value value, mlir_ts::TupleType srcTupleType, ::llvm::ArrayRef<::mlir::typescript::FieldInfo> fields, bool filterSpecialCases, const GenContext &genContext, bool errorAsWarning = false); + ValueOrLogicalResult copyArraysOfConstTuple(mlir::Location location, mlir::Value value, mlir_ts::ConstTupleType constTupleType, + const GenContext &genContext); + ValueOrLogicalResult castTupleToTuple(mlir::Location location, mlir::Value value, mlir_ts::TupleType srcTupleType, ArrayRef fields, const GenContext &genContext, bool errorAsWarning = false); diff --git a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp index acf10bb3e..de4770954 100644 --- a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp +++ b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp @@ -304,6 +304,12 @@ class OwnedReturnConsumptionPass return; } + if (bornUncounted(value)) + { + retainForGlobal(resultOp, resultOp->getOperand(0)); + return; + } + // An optional widened into a union (`let g: C | null | undefined = mkU()`, #462) is a // ts.If over the call made before it: each branch yields that call's value through // views, or a value that holds nothing (the empty optional). The global takes the call's @@ -405,6 +411,34 @@ class OwnedReturnConsumptionPass }); } + // An array or tuple built in a global's region (`const a = [1, 2]`, `const t = [1, [6, 7]]`, or + // a field of `const o = { items: [1, 2] }`) is born at count 0 under rc: a local takes its first + // count with RetainSlot, and nothing took one for the global, so the first local that read it + // and let go freed it. A global is a root and holds a count of its own (§9.49). Under own the + // global owns what it holds without a count, and a retain there is an error. + // A constant array reaches the global through its cast to an array, which copies it to the heap; + // any other constant (a string literal, an RTTI name) is static and immortal, and its global + // stays a constant initializer. + static bool bornUncounted(mlir::Value value) + { + auto *definingOp = value.getDefiningOp(); + return definingOp && (mlir::isa(definingOp) || + mlir::isa(value.getType())); + } + + void retainForGlobal(mlir::Operation *before, mlir::Value value) + { + if (!compileOptions.isRefCounted()) + { + return; + } + + mlir::OpBuilder builder(before->getContext()); + builder.setInsertionPoint(before); + builder.create(before->getLoc(), value); + } + // Each owned producer stored into a field of an object literal's temporary slot, through the // views of claimGlobalInitializers, is taken over by whoever takes the literal; a field that is // itself a literal read out of its own temporary slot is followed into. @@ -452,6 +486,10 @@ class OwnedReturnConsumptionPass stored.getDefiningOp()->setAttr(OWNED_RESULT_CONSUMED_ATTR_NAME, mlir::UnitAttr::get(&getContext())); } + else if (bornUncounted(stored)) + { + retainForGlobal(storeOp, storeOp.getValue()); + } else if (auto innerLoad = stored.getDefiningOp()) { if (auto innerSlot = innerLoad.getReference().getDefiningOp(); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index e60b0def0..8c41a7592 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -347,6 +347,7 @@ tslang_add_test(NAME test-compile-00-arrays5-deconstruct COMMAND test-runner "${ tslang_add_test(NAME test-compile-00-array-rest-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_rest_copy.ts") 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-array-data-string-alias COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_data_string_alias.ts") tslang_add_test(NAME test-compile-00-arrays7 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array7.ts") tslang_add_test(NAME test-compile-00-arrays8-tuple-spread COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array8_tuple_spread.ts") @@ -848,6 +849,7 @@ tslang_add_test(NAME test-jit-00-arrays5-deconstruct COMMAND test-runner -jit "$ tslang_add_test(NAME test-jit-00-array-rest-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_rest_copy.ts") 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-array-data-string-alias COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_data_string_alias.ts") tslang_add_test(NAME test-jit-00-arrays7 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array7.ts") tslang_add_test(NAME test-jit-00-arrays8-tuple-spread COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array8_tuple_spread.ts") @@ -1678,6 +1680,7 @@ set(TSLANG_CORPUS 00array_rest_copy.ts 00array6.ts 00array_static_nested.ts + 00array_literal_copy.ts 00array_data_string_alias.ts 00array7.ts 00array8_tuple_spread.ts diff --git a/tslang/test/tester/tests/00array_literal_copy.ts b/tslang/test/tester/tests/00array_literal_copy.ts new file mode 100644 index 000000000..15543060e --- /dev/null +++ b/tslang/test/tester/tests/00array_literal_copy.ts @@ -0,0 +1,81 @@ +// Arrays inside an inferred tuple or record literal can be changed (#479). Such a literal is +// folded to a constant, and its arrays used to be headers in constant global data over the +// literal's constant data: push reallocated a global, pop and `length =` wrote one. +const gst = [1, [6, 7]]; +const go = { name: "g", items: ["a", "b"] }; +const ga = [6, 7]; + +class Holder { + n: number; + items: number[]; +} + +function grow(t: [number, number[]]) { + t[1].push(9); + return t[1].length; +} + +function fresh() { + const ft = [1, [2]]; + return ft[1]; +} + +function main() { + const lt = [1, [6, 7]]; + const x = lt[1]; + const p = x.pop(); + assert(p == 7 && x.length == 1 && x[0] == 6, "pop"); + x.length = 0; + assert(x.length == 0 && lt[1].length == 0, "length =, seen through the tuple"); + x.push(8); + assert(lt[1].length == 1 && lt[1][0] == 8, "push, seen through the tuple"); + + gst[1].push(8); + assert(gst[1].length == 3 && gst[1][2] == 8, "module-level tuple"); + go.items.pop(); + assert(go.items.length == 1 && go.items[0] == "a", "module-level record"); + + // a global holds a count of its own under rc: a local that reads it and lets go + // does not free it + for (let i = 0; i < 3; i++) { + const items = go.items; + const inner = gst[1]; + const whole = ga; + assert(items.length == 1 && inner.length == 3 && whole.length == 2, "a global read through a local, repeatedly"); + } + + const o = { items: [1, 2] }; + o.items.push(3); + assert(o.items.length == 3, "record literal"); + + let n = 5; + const mo = { inner: { items: [1] }, n: n }; + mo.inner.items.push(2); + assert(mo.inner.items.length == 2, "record in a record"); + + let nt = [1, [2, [3, 4]]]; + nt[1][1].push(5); + assert(nt[1][1].length == 3, "tuple in a tuple"); + + const at = [[1, [2]], [3, [4, 5]]]; + at[1][1].push(6); + assert(at[1][1].length == 3 && at[0][1].length == 1, "array of tuples"); + + assert(grow([1, [2, 3]]) == 3, "a literal passed to a tuple parameter"); + + const h: Holder = { n: 1, items: [1, 2] }; + h.items.push(3); + assert(h.items.length == 3, "a record literal cast to a class"); + + const a = fresh(); + a[0] = 9; + assert(fresh()[0] == 2, "each evaluation makes a new array"); + + for (let i = 0; i < 3; i++) { + const lst = [1, [6, 7]]; + lst[1].push(i); + assert(lst[1].length == 3, "a new array on each pass"); + } + + print("done."); +} diff --git a/tslang/test/tester/tests/00array_static_nested.ts b/tslang/test/tester/tests/00array_static_nested.ts index 0bf812350..f9a15f47d 100644 --- a/tslang/test/tester/tests/00array_static_nested.ts +++ b/tslang/test/tester/tests/00array_static_nested.ts @@ -1,7 +1,8 @@ // Constant arrays inside global data keep their elements, and are never freed. const aa: number[][] = [[1], [2, 3]]; const t: [number, number[]] = [1, [4, 5]]; -// inferred, so the inner array stays in global data: a static header over static data +// inferred: a tuple literal folded to a constant, whose array is a heap copy (#479), which the +// global holds a count of under rc const st = [1, [6, 7]]; function main() { @@ -11,7 +12,7 @@ function main() { const inner = aa[1]; assert(inner[0] == 2, "read through a local, repeatedly"); const sinner = st[1]; - assert(sinner.length == 2 && sinner[1] == 7, "a static header, read through a local repeatedly"); + assert(sinner.length == 2 && sinner[1] == 7, "an array of a constant tuple, read through a local repeatedly"); } print("done."); }