Skip to content

-mm=own: an importer can build objects of an imported class - #410

Merged
ASDAlexander77 merged 3 commits into
mainfrom
own-import-class-new
Sep 29, 2026
Merged

ASDAlexander77 merged 3 commits into
mainfrom
own-import-class-new

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Fixes the issue found in #409: an importer built under -mm=own couldn't create an object of a library's class.

Cause

new H() of an imported class calls the library's H..new, then runs H.constructor through a method bound to the new object: CreateBoundFunction(Cast(h to opaque), ctor), split straight back into GetThis/GetMethod for the call. The inference read that cast as taking h, so every later use of h reported "used after its value was moved".

Fix

  • The cast is a borrow when that bound-method call is its only use, and the object comes out again only as a call's argument. This is what ThisSymbolRef already is for a method of this module.
  • The object is fresh, and owned here, only when the library was built under own. Its ..new is on the library's __own_no_drops list (-mm=own: a library exports which of its functions destroy nothing #409), so the signature pass marks the call __own_fresh_result, which isFresh reads.
  • rc libraries keep the old conservative rule. An rc library lists nothing, and its blocks carry a count their module holds, so there a borrow under the object still ends at any unknown call.

test-runner fix (affects every -shared -mm=... test)

In -shared mode, -mm= reached only the library, so every -shared -mm=rc/none/own test built its program under gc and ran a mixed link. That includes #409's two -shared tests; only its Windows script test actually checked the facts. The flag now goes into tslang_opt, as -x86's triple does, and all 302 -shared tests pass with it.

Tests

  • New own/import_own_class.ts + own/export_own_class.ts, as a -shared pair under own, rc, none and gc, AOT and JIT. They cover construction with arguments, a field store, method and getter calls, and an imported function, with each object read after a churn.
  • own-shared-no-drops.cmake also checks that the program is rejected against the same library built under rc.
  • Windows Release: 3253/3253.
  • Linux (WSL): 3239/3239.
  • -mm=own corpus: unchanged at 303.

JIT heap fix (CI failure on the first push)

test-jit-rc-shared-import-class and test-jit-own-shared-import-class crashed on Windows CI with 0xC0000005. It was a heap mismatch, not the change above:

  • The JIT bound the program's malloc/free to tslang.exe's own.
  • With CI's prebuilt LLVM, those are rpmalloc's: LLVMSupport brings it as the process malloc (scripts/llvm_prebuilt_common.ps1).
  • The library, like every module tslang builds and TypeScriptRuntime.dll, allocates with the static release CRT, which uses the process heap.
  • Under rc and own, an object made in the library is freed by the program, so the free faults. gc and none never free across the boundary, and a locally built LLVM has no rpmalloc, so only CI saw it.

That first fix exposed a second pairing on CI. test-jit-{rc,none}-corpus-00async-gc-threading failed with 0xC0000374:

  • A coroutine frame comes from TypeScriptRuntime.dll's aligned_alloc shim, which called that DLL's malloc.
  • TypeScriptRuntime.dll links LLVMSupport, so on CI that malloc is rpmalloc too.
  • The frame goes back through plain free, which the JIT now binds to the process heap.

The runtime's four allocation exports (Alloc/AlignedAlloc/Free/AlignedFree) now use ucrtbase.dll in release builds as well. Checked by putting them on a private heap of their own, which is CI's condition: exactly those two tests fail, and with the fix both pass.

In release builds the JIT now takes malloc/calloc/realloc/free from ucrtbase.dll, the release CRT on the process heap. Debug builds are unchanged. Checked locally by binding the JIT to a private heap: that reproduces the CI pattern exactly (own and rc fail, none and gc pass), and with the fix all four pass. Windows 3253/3253.

Known gap, recorded in §15.7, not fixed here

A value an own importer receives from an rc library is destroyed at the end of its owner's scope, because own's release skips only immortal blocks. That was already true of call results, and now applies to new objects too. If the library kept a reference of its own, that is a use-after-free, where the mixed-link policy promises a leak.

🤖 Generated with Claude Code

ASDAlexander77 and others added 3 commits September 29, 2026 23:10
`new H()` of a class from a library calls the library's H..new and then
H.constructor through a method bound to the new object:
CreateBoundFunction(Cast(h to opaque), ctor), split straight back into
GetThis and GetMethod. The inference read the cast as taking h, so every
later use was "used after its value was moved". The cast is now a
borrow when that is its only use and the object comes out again only as
a call's argument - what ThisSymbolRef is for a method of this module.

The object is also fresh when the library was built under own: its
..new is listed among the library's __own_no_drops, and the signature
pass marks such a call __own_fresh_result, which isFresh reads. A
library built under rc lists nothing, and its blocks carry a count its
own module holds, so there a borrow under the object still ends at any
unknown call.

test-runner: in -shared mode -mm= reached only the library, and every
`-shared -mm=...` test built its program under gc. It now goes into
tslang_opt, like -x86's triple; the 302 -shared tests pass with it.

Tests: import_own_class.ts + export_own_class.ts as a -shared pair under
own, rc, none and gc, AOT and JIT; own-shared-no-drops.cmake checks the
program against the same library built under rc is rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI failed test-jit-rc-shared-import-class and test-jit-own-shared-import-class
with 0xC0000005. An object of a library's class under rc or own is made in
the library and destroyed by the program, and the JIT bound the program's
malloc and free to tslang.exe's own. With the prebuilt LLVM CI uses, those
are rpmalloc's (LLVMSupport brings it as the process malloc, see
scripts/llvm_prebuilt_common.ps1), while the library - like every module
tslang builds, and TypeScriptRuntime.dll - allocates with the static
release CRT, on the process heap. A block of one freed by the other faults.
gc and none never free across the boundary, and a locally built LLVM has
no rpmalloc, so only CI saw it.

In a release build the JIT now takes malloc, calloc, realloc and free from
ucrtbase.dll, the release CRT on the process heap. A debug build keeps its
own: a debug CRT puts a header in front of every block, and the libraries
it builds link the debug CRT too.

Checked by binding the JIT to a private heap: exactly the CI pattern (own
and rc fail, none and gc pass); with the fix all four pass.

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

After the previous commit, CI failed test-jit-rc-corpus-00async-gc-threading
and test-jit-none-corpus-00async-gc-threading with 0xC0000374. The coroutine
lowering takes a frame from `aligned_alloc` - TypeScriptRuntime.dll's shim,
which called that DLL's `malloc` - and gives it back with plain `free`, which
the JIT now binds to the process heap. TypeScriptRuntime.dll links
LLVMSupport (add_mlir_library), so against the prebuilt LLVM its `malloc` is
rpmalloc too. Before, both sides happened to be rpmalloc.

Alloc, AlignedAlloc, Free and AlignedFree - the runtime's only exports that
hand out memory - now take malloc and free from ucrtbase.dll in a release
build, as jit.cpp does; a debug build keeps its own.

Checked by putting those four on a private heap of their own, CI's
condition: exactly those two tests fail; with the fix both pass.
Windows 3253/3253.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit da5aa8e into main Sep 29, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the own-import-class-new branch September 29, 2026 23:27
ASDAlexander77 added a commit that referenced this pull request Oct 2, 2026
…s, spreads, declaration retains (#449)

The catch-all ("takes a second reference; -mm=own cannot prove a move or a borrow here yet") was
the first error of 100 corpus files. reportSecondReference now names the shape it is given, from
the retained value's root, for all four places that report it: a function returning a borrow of
its argument on some paths and another value on others (___unbox<string>, ___cast<A, string>,
user code), a parameter returned or kept where its callers cannot learn the fact, a parameter
assigned, a local that owns nothing given a value, a value merged from branches, a global's value,
a captured variable's value, and an object literal holding a value it owns.

Accepted on the way:
- a string or an array widened to a union of it and null or undefined, or narrowed back, is a view;
- a view of a value whose type owns no block holds none (a union's number payload made into another
  union), and a retain of it is erased;
- a local's declaration retain of constant data is erased wherever the slot still holds its
  initializer, whatever is stored later (MLIRGen emits it only at the declaration);
- a field of constant data is constant data;
- an array a spread builds in a local of its own moves into the owning local it is read into;
- an interface's `this` bound for a call of a function-typed field is a borrow (#410's shape).

Corpus: 402 -> 416 of 592 (415 with --opt), none lost; the catch-all 100 -> 25. Spec section 21.

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