Repository navigation
rc: string resize, rest-parameter packs and grown array slots (DefaultLib rc failures 52 -> 20) - #413
Merged
Merged
Conversation
Assigning a string's length reallocated the block in place (ts.SetStringLength -> realloc). realloc frees the block when it moves, while something else can still hold it: the caller whose string a `this: string` parameter borrowed, a temporary, another variable. Under rc the holder then released the freed block (heap corruption, 0xC0000374). The default library's `"".clone().resize(n)` does exactly this, and it is behind padStart, padEnd, repeat, the regexp match results and more - most of the DefaultLib tests that failed under -mm=rc. Under none a second variable was left pointing at freed memory, and under gc, where GC_realloc can grow a block in place, a second variable saw the resized string's writes. A string is a value, so the resize is now `s = <copy of s at size n>`: a new ts.StringResize op allocates the new block and copies what fits, and the result goes through the ordinary assignment, which gives up the old string only if `s` owned it. ts.SetStringLength is gone. -mm=own counts the new op as fresh and as a borrow of its operand, as it does ts.StringConcat. New test 00string_length_resize.ts (a parameter, an alias, a field, and a shrink), registered under gc and in the rc/none corpus; it fails on main under all three models. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment The array a rest parameter gets is built at the call from the trailing arguments and, like an array literal's, releases every element when it dies. The literal paths take a reference to each element first (§9.21); the packing in mlirGenCallFunction - reached by a call through an extension function, such as `fruits.concat(["x", "y"])` - did not. The callee's first `for (const item of other)` then took the only reference and gave it back, freeing the argument array, and the second loop read freed memory: the default library's Array.concat printed garbage for the first element it copied and corrupted the heap (array_concat under -mm=rc). New test 00rest_args_pack_owned.ts, registered under gc and in the rc/none corpus; it fails on main under rc (0xC0000374). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`arr.length = n` reallocates the array's data block, and the slots it adds were zeroed only under rc and own, and only for an element type that owns heap memory (§9.37). Everywhere else they held whatever the allocator's memory held last. The default library's Set and Map grow their `int[]` buckets this way and take an unwritten bucket for an empty one, so outside gc their hash chains started from garbage: `new Set([1, 2]).union(new Set([2, 3]))` lost elements in about one run in two under -mm=rc, depending on what the heap had been used for (the set_* DefaultLib tests). gc was never affected: the collector hands back cleared memory, and GC_realloc clears what a shrink gives up. The grown tail is now zeroed for every element type under every model but gc. New test 00array_length_grow_zeroed.ts (grown over a dirtied heap, and shrunk then grown), registered under gc and in the rc/none corpus; it fails on main under rc and none. 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.
Summary
Three compiler bugs behind most of the DefaultLib test failures under
-mm=rc. Each has its own commit and its own regression test, and each test fails onmain. Before this PR, 52 of the 314 rc DefaultLib test runs failed.1.
s.length = nmakes a new string and leaves the old one aloneAssigning a string's length used to reallocate the block in place (
ts.SetStringLength→realloc). Whenreallocmoves a block it frees the old one, even though something else may still hold it:this: stringparameter;Under rc, that holder later released the freed block, giving 0xC0000374.
DefaultLib's
"".clone().resize(n)does exactly this. It is behindpadStart,padEnd,repeat, the regexp match results and more, so this accounted for most of the string and regexp failures. The other models were affected too:GC_realloccan grow a block in place, so a second variable saw the resized string's writes.A string is a value, so the fix treats the resize as
s = <copy of s at size n>:ts.StringResizeop allocates the new block and copies what fits;sowned it;ts.SetStringLengthis removed;-mm=owncounts the new op as fresh and as a borrow of its operand, the same asts.StringConcat.Test:
00string_length_resize.ts. Onmainit fails under all three models: rc with heap corruption, none and gc with the alias assert.2. The array a rest parameter receives takes a reference to each element
A rest parameter's array is built at the call and, like an array literal's, releases every element when it dies. The packing done for calls through an extension function took no reference to the elements:
fruits.concat(["x", "y"]), the callee's firstfor (const item of other)took the only reference and gave it back, freeing the argument array.This is the
array_concatfailure. Test:00rest_args_pack_owned.ts, which fails onmainunder rc.3. A grown array's new slots read as zero under every model but gc
The slots added by
arr.length = nused to be zeroed only under rc/own, and only when the element type owns heap memory. Everywhere else they held whatever was left in that memory.DefaultLib's Set and Map grow their
int[]buckets this way and treat an unwritten bucket as empty. Outside gc, their hash chains therefore started from garbage, andnew Set([1, 2]).union(new Set([2, 3]))lost elements in about half the runs (theset_*tests). gc was never affected: the collector returns cleared memory, andGC_reallocclears what a shrink gives up.Test:
00array_length_grow_zeroed.ts, which fails onmainunder rc and none.Results
owning_copies.tsis new)The DefaultLib numbers are with the companion DefaultLib PR (ASDAlexander77/TypeScriptCompilerDefaultLib#14). That PR fixes the last memory bug on the library side: raw
memcpyof owning elements inconcat,slice,copyWithinand the Set/MapresizeHelper.How the Set bug was found
The Set failures were intermittent. To make them reproducible, I temporarily replaced
ProcessHeap.hwith a quarantine heap for diagnosis only; it is not part of this PR. That heap:With it the failures became deterministic, and a
0xCDCDCDCCread in a bucket pointed straight at the unwritten slots.Not in this PR
These remain:
sleep.ts,weakref_basic.ts: link errors. The async runtime's thread registration and weak refs referenceGC_*, and an rc AOT program links no collector. This is a configuration issue, not a memory bug.regexp_indices,regexp_test2/3,string_match,string_matchAll, which spins, andstring_replace*), plus intermittent heap corruption (0xC0000374) inarray_fillandstring_search. The quarantine heap never frees, and Windows only validates heap metadata on free, so it could not see these. They are most likely writes past the end of a block. Guard bytes are the next step.new Set([2, 4, 6, 8])infersSet<any>: an inference gap. As a result, the DefaultLib Set tests exercise theanypath.-mm=ownrejectsreturn box.text.length:StringLengthOpis missing from own's borrow list. This was already the case before this PR.🤖 Generated with Claude Code