Skip to content

rc: a const read out of an element keeps a count of its own - #641

Merged
ASDAlexander77 merged 1 commit into
mainfrom
fix-rc-element-const-read
Oct 22, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
fix-rc-element-const-read

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

const c = arr[0] (and const [c] = arr) was folded into the read itself and held no reference, so under rc arr[0] = d freed the block c still read. Such a const now gets storage like a let under rc, as a const read out of a field already does (#635).

That needed an out-of-bounds destructuring fixed first, which was why #635 left elements out:

  • const [a, b, c] = pair read whatever followed the array's data. Under rc a count taken of it faulted (for a let as well). Under gc and none it was undefined behaviour that LLVM made use of: with print(c == null) after it, main printed nothing at all and exited 127.
  • An element past the end of an array is now the default of its element type: null for a reference (TS has undefined there), 0 for a number. A runtime array compares its length; a const array literal's size is known at compile time, so no read is emitted. Tuples past their length were already a compile error, and still are.
  • The const rule (readsElement) sees through the if the bounds check makes, so a destructured const is covered too. It applies under rc only. Under own the folded read stays a borrow.

Behaviour change in every model: reading past the end of an array in a destructuring now gives null/0, where before it read whatever memory followed. No in-bounds output changes.

Tests

  • 00const_element_read.ts, in the default compile/jit lists and in TSLANG_CORPUS (rc + none). It covers element reads, destructured reads and string elements under churn, plus past-the-end class/string/number/literal elements. On main it fails in all three models (rc: the element's overwrite frees the const; gc/none: a past-the-end string is not null).
  • Teeth: making readsElement return false fails test-compile-rc-corpus-00const-element-read (and its jit twin). Without the switch, it passes.
  • Windows ctest -C Release: 4920/4920. WSL Linux full run: 4902/4902.

Merge note: this and #642 both edit the rc condition line and the comment paragraph in processConstRef (MLIRGenImpl.h), so the second to merge needs a small rebase. Once both are in, const m = arr[0].get needs a one-line follow-up: readsElement(viewedValue(initial)).

🤖 Generated with Claude Code

`const c = arr[0]` (and `const [c] = arr`) was folded into the read itself,
holding no reference, so `arr[0] = d` freed the block `c` still read. Under
rc such a const now gets storage like a `let`, as a const read out of a
field already does (#635).

That needed an out-of-bounds destructuring fixed first: `const [a, b, c] =
pair` read whatever followed the array's data. Under rc a count taken of it
faulted (for a `let` too), and under gc/none it was undefined behaviour that
LLVM used - `print(c == null)` after it made main print nothing at all. An
element past the end of an array is now the default of its type: null for a
reference (TS has undefined there), 0 for a number. A runtime array checks
its length; a const array literal's size is known, so no read is emitted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 7ccd7c1 into main Oct 22, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-rc-element-const-read branch October 22, 2026 18:18
ASDAlexander77 added a commit that referenced this pull request Oct 22, 2026
Both changed the rc const rule in processConstRef: it now takes a field
read, an element read (#641) and a view over a field read (this PR).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77 added a commit that referenced this pull request Oct 22, 2026
… a count (#644)

`const m = arr[0].get` and `const i: I = arr[1]` were folded into the view
itself, holding no reference of their own, so `arr[0] = d` freed the object
`m()` and `i.get()` then ran on. #641 gave a const read out of an element
storage and #642 gave a view over a field read the same; a view over an
element read is the remaining case. Under rc it now gets storage like a
`let`. Under own the folded read stays a borrow.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77 added a commit that referenced this pull request Oct 23, 2026
…global a count (#647)

Two shapes the memory-model fuzzer (#646) found double-freeing under rc
since #639.

- holdGlobalArguments retained the argument right before the call. A
  later argument of the same call could give the global a new value
  first (`f(G, (G = mk(), x))`, `f(H.n, (H.n = mk(), x))`): its release
  freed the block, and the hold then retained and released a freed
  block. The retain (and the #645 unwind slot) now follow the read.
- `const n = G` was folded into the read, holding no count. Once the
  global got a new value, here or in a call, `n` dangled (fuzzer R1).
  #639 made that a crash: `run(n.v)`, with `run` assigning G, held and
  released a field of the freed block. Under rc a const read out of a
  global, or an interface or bound method made over one, now gets
  storage like a const read out of a field or an element (#635, #641,
  #642, #644). Under own the folded read stays a borrow.

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.

1 participant