Arrays in an inferred tuple or record literal are heap copies (#479) - #484
Merged
Merged
Conversation
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 <noreply@anthropic.com>
This was referenced Oct 4, 2026
Closed
ASDAlexander77
added a commit
that referenced
this pull request
Oct 4, 2026
…#491) Two shapes failed to compile: - A constant array in a tuple that has a run-time value (`let t = [x, [1]]`) kept its constant-array type, so `t[1].push(2)` was "Can't resolve property 'push'". createTupleFromArrayLiteral now makes it an array of its own, a heap copy, as it already did for a constant tuple holding an array (#484). - A `const` of a tuple built at run time (`const nt = [x, [2, [3, 4]]]`) was folded into the CreateTuple value, so `nt[1][1].push(5)` had no reference to change the array through ("Can't get reference of the array"), and nothing counted the arrays in it. Such a const now has storage when the tuple owns a block, as a `let` has and as a record literal's const has since #485. Under own, `const r = [i, [i, i]]; r[1].push(5)` now gives the error the `let` form already gave ("'this value' takes a second reference") instead of "Can't get reference of the array". Closes #486 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #479: an array inside an inferred tuple or record literal can now be changed.
The bug
A tuple or record literal whose values are all constants (
const st = [1, [6, 7]],{ items: [1, 2] }) is folded to ats.Constant, and each array in it was lowered to a static header in a constant global over the literal's constant data.pushreallocated that global (access violation under gc, exit 127 under rc and none); since #482,pop,shift,spliceandlength =wrote into it and the optimiser folded the reads back to the initializer, so they gave silently wrong results.The fix (MLIRGen)
The literal is rebuilt with heap copies of its arrays wherever it becomes a value that can be changed:
constof it gets storage and is widened to a tuple, as aconstof a constant array already is (processConstRef,adjustLocalVariableType,adjustGlobalVariableType);copyArraysOfConstTuple), nested tuple literals included;[[1, [2]], [3, [4]]]) is built element by element (castConstArrayToArray);[x, [2, [3, 4]]]), it is cast to a tuple;const p: P = { x: 1, items: [1, 2] }still casts to the class.Static headers are now only read;
ArrayLayout::ensureCapacity's comment says so.Also fixed: a module-level literal array under rc (a #480 regression)
Under rc an array or tuple literal built in a global's initializer is born at count 0, and nothing took a count for the global: the first local that read it and let go freed it. On main,
const ga = [6, 7]read throughconst s = gatwice exits with 127 (before #480 the length lived in the global's own{ data, length }, so reads survived the freed block).OwnedReturnConsumptionPass::claimGlobalInitializersnow retains what the initializer builds (rc only), as it already did for a call's result. The #479 fix needs it:00array_static_nested.ts'sconst stnow holds a heap array, and read in a loop it crashed under rc without this.Tests
00array_literal_copy.ts(compile, JIT, rc and none corpora): pop /length =/ push through a tuple's array, module-level tuple and record, a global read through a local repeatedly, record in a record, tuple in a tuple, array of tuples, a literal passed to a tuple parameter, a record literal cast to a class, a new array on each evaluation. On main it does not compile (the record shape was a compile error); the tuple shapes crash there.00array_static_nested.ts: comment only (itsstis no longer a static header).Gates
ctest -C Release -j 12 --timeout 300,DEFAULT_LIB_PATH= the staged default library)ctest -j 8 --timeout 300)--emit=llvm -mm=own --no-default-lib, every file oftest/tester/tests)--opt: 457 / 456). One flip, see belowmeasure.ps1, AOT)own_fresh_string,own_return_new,own_try_local,own_fresh_array,own_literals,own_array_param_pushidentical to #482's numbersown corpus flip, accepted:
00funcs_bindings.tspassesconst item1 = { text: "someText", location: [1, 2, 3], style: "italics" }to a destructuring parameter.locationused to be a static array; it is now a heap arrayitem1owns, and own rejects the conversion ("'this value' borrows a field and cannot be stored"), exactly as it does on main for the run-time formlocation: [n, 2, 3]. Please overrule if own should keep accepting this file.Not fixed here (pre-existing on main, found while testing; filed)
const o = { items: [i, 2], n: i }) leaks the array in its fields (AOT, 1M iterations: rc and own 96.5 MB). The constant form{ items: [1, 2], n: i }now takes the same path and leaks the same way.let t = [x, [1]]; t[1].push(2)is "Can't resolve property 'push'" (the field keeps its constant-array type), andconst nt = [x, [2, [3, 4]]]; nt[1][1].push(5)is "Can't get reference" (aconstof a run-time tuple has no storage; withletit works after this PR).const p: P = { x: 1, items: [i, 2] }is "invalid cast" from the tuple to the class.Closes #479
🤖 Generated with Claude Code