Repository navigation
Run async programs at 32 bits (32-bit support, phase 4b) - #338
Merged
Merged
Conversation
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>
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
Phase 4b of 32-bit support: async/await programs,
for awaitincluded, now compile and run as 32-bit executables. Before this PR, every async program crashed at i686 (exit 139) and00for_awaitfailed to compile. The plan is indocs/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.CoroBeginOpConversiondeclaresaligned_alloc(i64, i64)for every target.GCPassrenames it toGC_memalign; without GC it resolves to the runtime'saligned_allocshim. Both take(size_t, size_t). At i686 (cdecl) the callee readsalignment = 4, size = 0, and the 16-byte frame overflows the block it gets back.0x1cinmlirAsyncRuntimeEmplaceToken, whichasync_execute_fnhad called with a null token.i64runtime parameters. The upstream pass convertsindexwith a 64-bit converter, and tslang's i686 converter makes iti32.async.runtime.create_group(%size : index)therefore leavesunrealized_conversion_cast %x : i32 to indexfollowed by: index to i64, and LLVM translation rejects that.int64_tthere, so thei64is 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.AsyncTargetWidthPassruns right afterConvertAsyncToLLVMand beforeGCPass, so the rename keeps the corrected signature. It also has to run beforeLowerToLLVM: that pass's memref alloc lowering would reject the(i64, i64)declaration as a redefinition. The pass retypesaligned_allocto 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.AsyncIndexCastPassruns right afterLowerToLLVM, the first point where both halves of the chain exist. It foldsiA -> index -> iBchains intollvm.sext,llvm.truncor the source value. Today the only widened value iscreate_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, and00for_awaitcompiles for wasm32 too. wasm32 async is not run-tested.Not in this PR
These are recorded in the spec's open issues.
-mm=rc/-mm=none. The async runtime needs Boehm's thread API, so these fail to link at x64 as well.mlirAsyncRuntimGetNumWorkerThreadsis declared() -> i32at 32 bits, while the runtime returnsint64_t.-mm=noneasync programs usually crash the compiler (0xC0000005). This happens before and after this PR.awaitinside a non-asyncmaindoes not wait. "after await" prints before the awaited function's body. That is today's behaviour on x64 too. The newawait_ordertest 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:00async_await,00async_gc_threading,00async_result_types,00owned_async,00for_await,00for_await_yield) andtslang/test/x86/await_order.ts, as 32-bit exes under gc;(i32, i32)under gc and none.The previous compiler fails all of these cases.
Windows Release suite:
ctest -j 16 -C Releasepasses 2769/2769.x64
--emit=llvmagainst 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.sh53/53 andcheck-x86-libs.sh11/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