Skip to content

Arrays in an inferred tuple or record literal are heap copies (#479) - #484

Merged
ASDAlexander77 merged 1 commit into
mainfrom
fix-static-nested-array
Oct 4, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
fix-static-nested-array

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

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 a ts.Constant, and each array in it was lowered to a static header in a constant global over the literal's constant data. push reallocated that global (access violation under gc, exit 127 under rc and none); 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.

The fix (MLIRGen)

The literal is 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 already 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 ([[1, [2]], [3, [4]]]) is built element by element (castConstArrayToArray);
  • inside a tuple literal built at run time ([x, [2, [3, 4]]]), 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. A record that stays a constant is copied where it is widened, so 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 through const s = ga twice exits with 127 (before #480 the length lived in the global's own { data, length }, so reads survived the freed block). OwnedReturnConsumptionPass::claimGlobalInitializers now 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's const st now holds a heap array, and read in a loop it crashed under rc without this.

Tests

  • New 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 (its st is no longer a static header).

Gates

Gate Result
Windows Release suite (ctest -C Release -j 12 --timeout 300, DEFAULT_LIB_PATH = the staged default library) 3749 / 3749 passed
Linux (WSL, this diff on main 77d9a6a, ninja release, ctest -j 8 --timeout 300) 3734 / 3734 passed (1 pre-existing disabled test)
own corpus (--emit=llvm -mm=own --no-default-lib, every file of test/tester/tests) main 459 / 611, this PR 458 / 611 (--opt: 457 / 456). One flip, see below
own memory (measure.ps1, AOT) own_fresh_string, own_return_new, own_try_local, own_fresh_array, own_literals, own_array_param_push identical to #482's numbers

own corpus flip, accepted: 00funcs_bindings.ts passes const item1 = { text: "someText", location: [1, 2, 3], style: "italics" } to a destructuring parameter. location used to be a static array; it is now a heap array item1 owns, and own rejects the conversion ("'this value' borrows a field and cannot be stored"), exactly as it does on main for the run-time form location: [n, 2, 3]. Please overrule if own should keep accepting this file.

Not fixed here (pre-existing on main, found while testing; filed)

Closes #479

🤖 Generated with Claude Code

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>
@ASDAlexander77
ASDAlexander77 merged commit d91b36b into main Oct 4, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-static-nested-array branch October 4, 2026 12:15
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pushing onto an array inside a constant tuple literal crashes (realloc of static data)

1 participant