Skip to content

Two GC allocations are two blocks again under optimization - #392

Merged
ASDAlexander77 merged 1 commit into
mainfrom
jit-gc-string-append
Sep 28, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
jit-gc-string-append

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Under optimization (every JIT run, and --opt builds) with -mm=gc, this printed an empty line where it should print 1,:

const src: number[] = [1, 2];
let nums: number[] = [];
const t: number[] = [...src];
for (const v of t) nums.push(v);
let s = "";
s += nums[0] + ",";
print(s);   // "" instead of "1,"

Cause: the optimizer merged two GC_malloc(8) calls, the storage of the two empty arrays nums and the temporary built for [...src], into one. Both arrays then grew through GC_realloc of that one block, the second time through a pointer the first realloc had already freed. The heap was corrupted, and the corruption showed up later as a string that came out empty.

The unoptimized IR has two calls; after opt -O3 there is one. The allocators were declared memory(write, inaccessiblemem: none) plus allockind("alloc,zeroed"). #188 relied on that allockind to prevent exactly this merge, and with the current LLVM it no longer does. On the saved IR, declaring inaccessiblemem: readwrite, which is how LLVM declares malloc (the allocator reads and writes its own hidden state), keeps both calls.

Fix: that memory effect is set on GC_malloc, GC_malloc_atomic and GC_memalign (GCPass.cpp), and on GC_malloc_explicitly_typed, used for new (LowerToLLVM.cpp).

Test: 00gc_allocations_distinct.ts, registered for compile, jit and the corpus. It covers:

  • the repro above;
  • two empty arrays grown separately;
  • two instances of one class.

The repro part fails on main.

Results: the suite passes locally, 3017/3017, and the DefaultLib tests pass 156/156 in both jit and compile modes.

🤖 Generated with Claude Code

The collector's allocators (GC_malloc, GC_malloc_atomic, GC_memalign,
GC_malloc_explicitly_typed) were declared `memory(write, inaccessiblemem:
none)` plus `allockind`, which #188 relied on to keep LLVM from merging
two identical calls. With the current LLVM that no longer holds: two
GC_malloc(8) calls - the storage of two empty arrays - became one, both
arrays grew through GC_realloc of the same block, and the heap was
corrupted. It surfaced as `s += a` producing an empty string, only with
optimization (every JIT run, and --opt builds).

The allocator's state is now inaccessible memory the call reads and
writes, as LLVM declares malloc, and the calls stay apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit dcf3d76 into main Sep 28, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the jit-gc-string-append branch September 28, 2026 13:09
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