Skip to content

A closure over a const read out of a place captures the value it read - #654

Merged
ASDAlexander77 merged 6 commits into
mainfrom
fix-capture-const-place-read
Oct 23, 2026
Merged

ASDAlexander77 merged 6 commits into
mainfrom
fix-capture-const-place-read

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Fixes fuzzer bug G1 (#646): under gc and none, a closure over a const read out of a field saw a later write to that field.

let n15: N = mk(3);
const v16: V = n15.v;
n15.v = mv(2);
const c = (): number => v16.a[0];
print("closure", c());      // gc/none printed 2; the answer is 3 (rc was right)

Why

A const read out of a field, an element or a global is folded into the read, so it has no storage of its own under gc and none. Under rc it does (processConstRef), which is why rc was right. A by-value capture used GetReferenceFromValue, which for a folded read is the place it was read from, and CaptureOpLowering dereferenced that place when the closure was made, after the overwrite.

Fix

A capture goes through a reference only when that reference is the variable's own storage: a ts.Variable, a parameter, or a captured cell. Anything else captures the value itself. Read-write captures (by reference) are unchanged.

Tests

tests/00closure_const_read.ts: a closure over a const read out of a field, an element and a global, each overwritten before the closure is made. It fails under gc and none on main. It runs compiled, under JIT, and in the rc/none corpus.

The interface-field variant (const v = ix.v) is left out of the test until #651 merges. Without #651 that const has no count under rc (R7), and capturing the value it actually read turns R7's dangling const into a crash. Before this change the capture happened to re-read the live new value.

Verification

  • ctest -C Release: Windows 4982/4982, WSL Linux 4964/4964.
  • Replaying the fuzzer's repros on Linux: G1-closure-const-field now matches gc in every model.
  • mm_fuzz.py --jobs 10 run --count 500 --seed 1: only the 3 known own failures (R1, O4).

🤖 Generated with Claude Code

ASDAlexander77 and others added 6 commits October 23, 2026 05:31
`const v = n.v; n.v = d; const c = () => v.x;` gave `d.x` under gc and
none. A const read out of a field, an element or a global is folded
into the read and has no storage of its own (under rc it does, see
processConstRef). A by-value capture was taken through the reference
GetReferenceFromValue found for it - for a folded read, the place it
was read from - and CaptureOpLowering dereferenced that place when the
closure was made, after the overwrite.

A capture now goes through a reference only when that reference is the
variable's own storage (a variable, a parameter, a captured cell);
anything else captures the value itself. Fuzzer (#646) G1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ce-read

# Conflicts:
#	tslang/test/tester/CMakeLists.txt
@ASDAlexander77
ASDAlexander77 merged commit 37970f0 into main Oct 23, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-capture-const-place-read branch October 23, 2026 10:06
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