Skip to content

Run async programs at 32 bits (32-bit support, phase 4b) - #338

Merged
ASDAlexander77 merged 5 commits into
mainfrom
phase4b-async-i686
Sep 21, 2026
Merged

ASDAlexander77 merged 5 commits into
mainfrom
phase4b-async-i686

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Summary

Phase 4b of 32-bit support: async/await programs, for await included, now compile and run as 32-bit executables. Before this PR, every async program crashed at i686 (exit 139) and 00for_await failed to compile. The plan is in docs/superpowers/plans/2026-09-22-32-bit-phase-4b-async.md.

Root cause

Upstream ConvertAsyncToLLVMPass (mlir/lib/Conversion/AsyncToLLVM) assumes 64 bits in two places.

  1. The coroutine frame allocator. CoroBeginOpConversion declares aligned_alloc(i64, i64) for every target. GCPass renames it to GC_memalign; without GC it resolves to the runtime's aligned_alloc shim. Both take (size_t, size_t). At i686 (cdecl) the callee reads alignment = 4, size = 0, and the 16-byte frame overflows the block it gets back.
    • A crash dump showed a worker thread faulting on a read of 0x1c in mlirAsyncRuntimeEmplaceToken, which async_execute_fn had called with a null token.
  2. Index values passed into i64 runtime parameters. The upstream pass converts index with a 64-bit converter, and tslang's i686 converter makes it i32.
    • async.runtime.create_group(%size : index) therefore leaves unrealized_conversion_cast %x : i32 to index followed by : index to i64, and LLVM translation rejects that.
    • The runtime genuinely takes int64_t there, so the i64 is right. Only the bridge from the index value is missing.

Fix

The upstream pass is part of the prebuilt LLVM, so two small tslang passes repair its output. They are added only when sizeBits() < 64, so x64 never runs them.

  • AsyncTargetWidthPass runs right after ConvertAsyncToLLVM and before GCPass, so the rename keeps the corrected signature. It also has to run before LowerToLLVM: that pass's memref alloc lowering would reject the (i64, i64) declaration as a redefinition. The pass retypes aligned_alloc to pointer width and truncates the arguments at each call. Those values are a coroutine frame's size and alignment. Any other shape is a hard error.
  • AsyncIndexCastPass runs right after LowerToLLVM, the first point where both halves of the chain exist. It folds iA -> index -> iB chains into llvm.sext, llvm.trunc or the source value. Today the only widened value is create_group's size, the constant 0 that for-await passes.

The index width equals sizeBits() by construction (dlti.dl_spec), so gating on the pointer width is exact.

Effect on wasm32

wasm32 had the same allocator bug. The frame allocator is now (i32, i32) there, and 00for_await compiles for wasm32 too. wasm32 async is not run-tested.

Not in this PR

These are recorded in the spec's open issues.

  • Async under -mm=rc / -mm=none. The async runtime needs Boehm's thread API, so these fail to link at x64 as well.
  • mlirAsyncRuntimGetNumWorkerThreads is declared () -> i32 at 32 bits, while the runtime returns int64_t.
    • It is harmless at i686: the low half is returned in EAX.
    • At wasm32 it would be a signature mismatch.
    • It cannot be reached from TypeScript today: only upstream's async-parallel-for creates it.
  • wasm32 -mm=none async programs usually crash the compiler (0xC0000005). This happens before and after this PR.
  • await inside a non-async main does not wait. "after await" prints before the awaited function's body. That is today's behaviour on x64 too. The new await_order test pins it down, with a comment saying a future fix is expected to change it.

Test plan

  • tslang/test/check-x86-run.sh, all ok:

    • the six async corpus files (00async_await, 00async_gc_threading, 00async_result_types, 00owned_async, 00for_await, 00for_await_yield) and tslang/test/x86/await_order.ts, as 32-bit exes under gc;
    • IR checks that the frame allocator is (i32, i32) under gc and none.

    The previous compiler fails all of these cases.

  • Windows Release suite: ctest -j 16 -C Release passes 2769/2769.

  • x64 --emit=llvm against the previous compiler (compare-ir.sh): unchanged apart from the known nondeterministic files.

  • probe-corpus.sh --compare: 0 regressions and 6 newly passing, which are exactly the async files at i686 gc. i686 gc now passes 423 corpus files, up from 417.

  • check-x86-eh.sh 53/53 and check-x86-libs.sh 11/11.

  • Linux CI. Not run locally. The new passes are never scheduled at 64 bits, and ctest is x64-only, so CI does not exercise them.

🤖 Generated with Claude Code

ASDAlexander77 and others added 5 commits September 21, 2026 20:39
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extends the phase 2 gate with the phase 4b cases: the six async corpus
files (00async_await, 00async_gc_threading, 00async_result_types,
00owned_async, 00for_await, 00for_await_yield) built at i686 under
-mm=gc, plus a new minimal repro test/x86/await_order.ts, and an IR
check that the coroutine frame allocator is declared at pointer width
(GC_memalign/aligned_alloc(i32, i32), not (i64, i64)).

Confirmed against the Release baseline (main 9a521c3): all new checks
FAIL as expected (four corpus files crash 139, two fail to compile,
the repro crashes 139, and the IR check sees (i64, i64)), while the
existing hello/gc_stress cases under gc/rc/none still pass. Also added
--entry-point and a 30s timeout to the existing run_case helper, since
the async cases need --entry-point (main() has no export) and a hang
must not look like a run in progress; verified this is a no-op for the
existing cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Upstream ConvertAsyncToLLVM declares the coroutine frame allocator as
aligned_alloc(i64, i64) whatever the target. aligned_alloc (and
GC_memalign, its GCPass rename) takes size_t, so at i686 the callee read
(alignment, size = 0) and the frame overflowed its block: the worker
thread then faulted in mlirAsyncRuntimeEmplaceToken with a null token.

A new module pass, AsyncTargetWidthPass, runs directly after
ConvertAsyncToLLVM when the pointer width is below 64. It retypes the
declaration to (iP, iP) -> ptr and truncates both arguments at each
call; any other shape is a hard error. x64 never schedules it.

check-x86-run.sh: the IR check now matches parameter types only. Once
the declaration has the target's size_t signature, LLVM recognizes it
as the libc function and adds allocalign/noundef attributes to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
for await failed to compile at i686 with "LLVM Translation failed for
operation: builtin.unrealized_conversion_cast". Upstream
ConvertAsyncToLLVM lowers runtime calls such as
mlirAsyncRuntimeCreateGroup(int64_t) with a 64-bit index and bridges
the operand with `index -> i64`; LowerToLLVM then converts the
producer with our pointer-width index and bridges back with
`i32 -> index`. Neither cast is an identity, so nothing removes them.

A second module pass, AsyncIndexCastPass, runs right after LowerToLLVM
(the first point where both halves exist) when the pointer width is
below 64. It folds each `iA -> index -> iB` chain into a sext, a trunc
or the source value, and leaves every other unrealized cast alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The spec marks 4b implemented, closes the two async open issues and adds the GetNumWorkerThreads width mismatch and the pre-existing wasm32 -mm=none async compiler crash. Comments now say where the frame allocator misread happens (i686 cdecl), which values the index fold widens, why AsyncTargetWidthPass must run before LowerToLLVM, that --entry-point is only mirrored from the suite, and that await_order's expected order is current behaviour, not TypeScript semantics.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit e0039a6 into main Sep 21, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the phase4b-async-i686 branch September 21, 2026 21:00
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