Skip to content

Link and run 32-bit programs (32-bit support, phase 2) - #335

Merged
ASDAlexander77 merged 11 commits into
mainfrom
phase2-32bit-native-libs
Sep 21, 2026
Merged

ASDAlexander77 merged 11 commits into
mainfrom
phase2-32bit-native-libs

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Summary

This is phase 2 of 32-bit support. 32-bit Windows programs (-mtriple=i686-pc-windows-msvc) now link and run under -mm=gc, -mm=rc and -mm=none. The plan is in docs/superpowers/plans/2026-09-21-32-bit-phase-2-native-libs.md. The spec's Phase 2 section has an "As built" table and the open issues.

  • Lowering uses the target's data layout. LLVMTypeConverter is now built from the module's llvm.data_layout, not from LLVM's default layout. This is what makes i686 struct and union sizes and getIntPtrType correct. The number of i686 modules with ptrtoint ptr … to i64 goes from 288 to 0. A missing layout is now a hard error, and so is a malformed one; unit tests cover both. The wasm32 layout that was hardcoded here is gone; it was wrong (it lacked i128:128).
  • Some triples are refused. For x86_64-pc-linux-gnux32, mips64 gnuabin32 and aarch64 ilp32, the pointer width of the architecture and of the ABI differ. MLIRGen and lowering would then disagree, so the compiler now refuses these triples and names the triple in the error.
  • x86 Boehm. prepare_3rdParty.bat <cfg> x86 builds it. This is opt-in; with no second argument, nothing changes. It runs the new scripts\build_gc_<cfg>[_shared]_vs_x86.bat, which copy their output into 3rdParty\gc[dll]\x64\<cfg>\lib\x86\. The --gc-lib-path / --gc-shared-lib-path values already used for x64 therefore work for x86 without change. The x64 files stay byte-identical; only the x86\ subdirectory is added.
  • x86 async runtime. tslang/runtime/CMakeLists.txt is a standalone project that builds TypeScriptAsyncRuntime for any arch, with no LLVM or MLIR link dependency. It uses the same sources as the in-tree library. I checked it against the in-tree library at x64 first: three async tests gave identical output with each. Build it with scripts\build_tslang_runtime_<cfg>_x86.bat, then pass --tslang-lib-path=__build\tslang-runtime\<cfg>.
  • The compiler finds and checks x86 libraries.
    • For a Windows x86 target it looks in <lib path>\x86\.
    • If a library is missing there, the error names the script that builds it and the path to pass.
    • Dump::coffMachine reads the COFF machine of gc.lib, gc.dll and TypeScriptAsyncRuntime.lib. A library built for the wrong arch is an error before the link, and that applies in both directions: an x86 lib found by an x64 build is caught too.
    • Non-Windows targets and wasm are unchanged.

Behavior change on x64: fixes a miscompile

Lowering used to size types with LLVM's default layout, which aligns i64 at 4 bytes. That under-sized a union member such as {i1, i64} at 12 bytes; its real size is 16. So in {flag; a: s32; b: s32} | {flag; v: bigint} the bigint lost its high half: 9007199254740993 read back as 1. This happened on JIT and exe, at every opt level and under every memory model. The new corpus test 00bigint_struct_layout covers it.

Apart from that fix, the x64 IR is unchanged. The other targets were checked too, by compiling the whole corpus with the compiler from before and after this change:

  • Linux x64 and wasm64: the only difference is 00bigint_struct_layout.
  • wasm32: the datalayout line changes.
  • On all three targets, no file that compiled before fails now.

The remaining differences are run-to-run noise. Some generated names contain hash IDs, and equal-size union members are ordered by hash. tslang/test/mask-ir.sh masks most of this, and lists the files it can't mask.

Not in this PR (open issues, recorded in the spec)

  • Importing an x86 DLL fails at compile time with ./lib.dll: Can't open: Unknown error (0xC1). mlirGenImportSharedLib loads the DLL into the 64-bit compiler to read __decls. Building the x86 DLL itself works. The fix is to read __decls from the file instead, planned for phase 4.
  • No x86 TypeScriptRuntime (the JIT DLL), because --emit=jit refuses x86 targets.
  • Win32 x86 exceptions are phase 3. The default library, async and the full suite at 32-bit are phase 4. A likely cause of the i686 00for_await unrealized_conversion_cast: upstream ConvertAsyncToLLVMPass uses LLVM's default layout.
  • Bugs found along the way, not fixed here. None is specific to 32-bit, and none is filed yet:
    • A union member's field that lies in the padding of the member used for storage reads back 0. This is x64 too and existed before this change. 00bigint_struct_layout documents it and does not assert that field.
    • bigint: printing a bigint, or using one in a template string, crashes. An optional bigint parameter fails materialization. Unions of tuple or object members can't be narrowed back to a member.
    • A let p: Node | null is not narrowed in a loop condition or body; for (…; p; …) produces an invalid op.
    • const arr: T[] = []; arr.push(…) is rejected.

Test plan

  • Windows Release suite: ctest -j 16 -C Release passes 2766/2766. That is the old 2765 plus 00bigint_struct_layout.
  • MLIRGenTests passes 144/144, including the new LoweringDataLayout tests.
  • tslang/test/check-x86-run.sh passes 6/6: hello and gc_stress × gc/rc/none, each a PE with machine 0x14C, exiting 0 with exact output. Under gc, gc_stress ran 102 collections and freed 31.7 MB in a 628 KiB heap, and the list it keeps alive still sums correctly. The script runs release builds only; debug x86 builds were checked by hand.
  • tslang/test/check-x86-libs.sh passes 11/11: a missing x86 directory, a wrong-arch lib or DLL in either direction, and correct x86 exe and DLL layouts.
  • tslang/test/check-datalayout.sh passes 6/6, including the wasm32 i128:128 layout and the refusal of x86_64-pc-linux-gnux32.
  • x64 IR diffed with tslang/test/compare-ir.sh against the pre-change compiler; results are above.
  • Linux CI. Not run locally.

Only the ctest suite and MLIRGenTests run in CI. The four scripts (check-datalayout.sh, compare-ir.sh, check-x86-libs.sh and check-x86-run.sh) are run by hand. The two x86 scripts need the x86 libraries built first, and CI does not build them.

🤖 Generated with Claude Code

ASDAlexander77 and others added 11 commits September 21, 2026 10:58
Six tasks: lowering takes the target's data layout, a guard against
ABI-by-environment triples whose widths disagree, Boehm for x86, a
standalone x86 build of the async runtime, per-arch library resolution
with a machine-type check, and 32-bit programs that link and run under
gc, rc and none.

x86 libraries live in an x86 subdirectory of each existing library
path, so x64 layouts, packages and CI are unchanged. x86 builds are
opt-in through prepare_3rdParty.bat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The type converter sized pointers at 8 bytes and aligned i64 at 4 for every target but wasm32,
because it was built with LLVM's default layout. MLIRGenModule already puts the TargetMachine's
layout on the module; lowering now reads it. On i686 that ends 64-bit pointer arithmetic
(288 of 513 corpus modules had ptrtoint ptr to i64; now 0). On x64 no corpus file changes:
compare-ir.sh reports 3 DIFFs (00funcs, 00object_func, 00union_bin_ops2), and each is run-to-run
nondeterminism the mask does not cover - rerunning the new compiler reproduces the baseline form.

The hardcoded wasm32 layout is gone. It carried f128:64 and lacked i128:128, matching neither of
LLVM's wasm32 derivations.

Adds test/compare-ir.sh, which diffs masked --emit=llvm output of the test corpus against a
stored baseline, and a wasm32 i128:128 check to test/check-datalayout.sh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Lowering now fails with a diagnostic when the module has no llvm.data_layout, instead of falling
back to LLVM's default, and parses the layout with DataLayout::parse so a malformed string is an
error rather than an abort. Two unit tests run the pass on hand-built modules to cover both, since
the CLI cannot produce such a module.

00bigint_struct_layout.ts covers the only values whose layout the old default could move on x64:
a bigint after a smaller field in a non-packed struct (optionals, classes, tuples, object
literals, arrays of them). It also covers the one case the old layout miscompiled on x64: in the
union {flag, a: s32, b: s32} | {flag, v: bigint} both members measured 12 bytes, so the storage
went to the s32 member and the bigint lost its top half when stored (read back as 1, not
9007199254740993). With the real layout {i1, i64} is 16 bytes and is the storage.

compare-ir.sh now uses test/mask-ir.sh (committed here, extended to mask FH<n> of any length) and
stops if the mask is missing, instead of reporting every file as different.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds build_gc_{release,debug}_vs_x86.bat and their _shared twins, each a
faithful copy of the x64 script with only -A x64 -> -A Win32 and the
x64 -> x86 path segments changed (build dir + CMAKE_INSTALL_PREFIX).
Runtime library, threads and cplusplus settings are unchanged.

prepare_3rdParty.bat gains an optional second argument (ARCH, defaults
to x64) and an opt-in x86 block that builds
3rdParty/gc/x86/{release,debug} and 3rdParty/gcdll/x86/{release,debug}
via `prepare_3rdParty.bat <release|debug> x86`, mirroring the existing
x64 gc/gcdll blocks. With no second argument, ARCH stays x64 and the
new block is skipped, so existing invocations are unaffected.

Verified: all four x86 builds (static release/debug, shared
release/debug) produce IMAGE_FILE_MACHINE_I386 binaries (gc.lib,
import gc.lib, gc.dll), no compiler warnings, and the x64 trees under
3rdParty/gc/x64 and 3rdParty/gcdll/x64 are byte-for-byte and
timestamp-identical before and after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The in-tree lib/TypeScriptAsyncRuntime target is an add_mlir_library inside
a project that requires the x64-only MLIR/Clang packages, so it cannot be
configured as Win32. The library itself has no LLVM/MLIR link dependency
(its one MLIR header is header-only), so tslang/runtime/CMakeLists.txt
builds the same sources standalone, without LLVM or MLIR link deps, for
any target arch. scripts/build_tslang_runtime_{release,debug}_x86.bat
drive it for x86, installing into __build/tslang-runtime/<cfg>/x86.

Verified at x64 first: compiled and ran 00async_await.ts,
00async_result_types.ts and 00owned_async.ts once against the in-tree
runtime and once against this standalone build (--tslang-lib-path pointed
at each); stdout and exit codes matched, and --verbose linker output
confirmed each link picked up its own TypeScriptAsyncRuntime.lib via a
distinct -libpath. The x86 outputs are IMAGE_FILE_MACHINE_I386 and link
LIBCMT (release) / LIBCMTD (debug), matching the static-CRT release/debug
split used everywhere else in the project.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
For an x86 target the compiler looks in the x86 subdirectory of --tslang-lib-path, so the path to pass is the parent, not the x86 folder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
For Windows x86 targets, the gc, shared gc and runtime lib paths now
resolve to their x86 subdirectory, however the path was obtained (flag,
environment or the gcdll default). A missing x86 build is a hard error
that names the build script, and a library built for another machine
is refused before the linker runs; x64 targets get the mirror check on
their flat path. Dump::coffMachine reads the machine of an object, a
static library or an import library. The flat-path existence checks
in the getters are skipped for x86, where the flat path holds x64
libraries by design.

check-x86-libs.sh covers the error paths and a correctly staged layout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
check-x86-run.sh builds test/x86/hello.ts and test/x86/gc_stress.ts for
i686-pc-windows-msvc with --opt --no-default-lib under -mm=gc, -mm=rc and
-mm=none, and checks each executable is I386 (0x14C), exits 0 and prints
exactly the expected text. gc_stress keeps a 1000-node list alive across
about a million garbage nodes plus strings and arrays; its sum is wrong if
a collection freed any of it.

The script unsets the *_LIB_PATH environment variables and stages the x86
gc.lib into <tmp>/gc/x86, so it depends only on explicit flags.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The x86 gc scripts now also copy their output into the x86
subdirectory of the matching x64 lib directory (gc: lib/x86/gc.lib;
gcdll: lib/x86/gc.lib and gc.dll), so the lib path users already pass
for x64 serves x86 too. prepare_3rdParty.bat checks the copied
location, so an older x86 build is re-run and gets the copy. The
"no x86 build" error names the directory to pass, and the gc.dll the
link picked gets the same COFF machine check as the libraries.
check-x86-run.sh uses that real layout instead of staging gc.lib.

Also: check-x86-libs.sh takes the x64 runtime from the in-tree build
(or a second argument) and checks a wrong-machine gc.dll; both check
scripts are executable; mask-ir.sh masks td_/tsrel_/tsret_ IDs of any
length and lists 00array8_tuple_spread as noisy; the debug x86 runtime
uses the debug LLVM headers; lowering checks the data layout before
the @dllname renames; comments and the design spec record the layout
as built and two Phase 4 findings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit adbd38b into main Sep 21, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the phase2-32bit-native-libs branch September 21, 2026 14:55
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