Skip to content

Throw and catch in 32-bit programs (32-bit support, phase 3) - #336

Merged
ASDAlexander77 merged 5 commits into
mainfrom
phase3-32bit-x86-eh
Sep 21, 2026
Merged

ASDAlexander77 merged 5 commits into
mainfrom
phase3-32bit-x86-eh

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Summary

This is phase 3 of 32-bit support. 32-bit Windows programs (-mtriple=i686-pc-windows-msvc) now throw and catch correctly under -mm=gc, -mm=rc and -mm=none. Before this PR, every x86 program that threw failed to link with unresolved external symbol __CxxThrowException.

The plan is in docs/superpowers/plans/2026-09-22-32-bit-phase-3-x86-eh.md. The spec's Phase 3 section now describes what was built.

  • EH cross-references are absolute on x86.
    • ThrowInfo, CatchableType and CatchableTypeArray keep their i32 fields. On x64 those fields hold image-base-relative RVAs; on 32-bit x86 they hold the absolute address, which is also 4 bytes.
    • Each Win32 RTTI helper now has one ehReference function where it used to repeat the ptrtoint/sub/trunc sequence three times. On x64 it emits exactly the old sequence. On x86 it emits ptrtoint … to i32, and __ImageBase is not declared. The x86 object gets only IMAGE_REL_I386_DIR32 relocations, with no DIR32NB.
    • Only the MLIR-level helper (MLIRRTTIHelperVCWin32.h) emits these globals. The LLVM-dialect helper's table emitters (LLVMRTTIHelperVCWin32::setRTTIForType and what it calls) have no callers. They were changed only so that neither helper can compute RVAs on x86.
  • _CxxThrowException is __stdcall on x86, so its symbol is __CxxThrowException@8.
    • A new module pass, CxxThrowCallingConvPass, sets x86_stdcallcc on the declaration and on every call and invoke of it.
    • transform.cpp runs it after Win32ExceptionPass and before the optimization pipeline, and only when stdcallDecoratesCxxThrow is set.
    • It is one pass because three places create these calls: the MLIR lowering, the rethrow that Win32ExceptionPass synthesizes, and its ToInvoke. A call whose convention differs from its callee's is undefined behaviour, which InstCombine turns into unreachable.
    • Any use of the function other than as a direct callee is a hard error.
    • Every emit path goes through this pipeline: llvm, bc, obj, asm, exe and dll.
  • usesImageBaseRelativeEH is decided by pointer width. clang's MicrosoftCXXABI::isImageRelative() decides it that way too, so 32-bit ARM Windows now gets absolute references as well. i686 and x64 are unchanged.

No other compiler change was needed. With these two fixes, every exception corpus file ran correctly at x86.

x64 output

The x64 output is unchanged. compare-ir.sh against the pre-change compiler finds only the known nondeterministic files. 00typeof_union_narrowing is now listed among them, because the base compiler alone gives two different outputs for it.

Found, not fixed here

These are recorded in the spec's open issues.

  • An untyped catch (e) never catches a thrown number or string. This happens on x64 too and is not caused by this PR.
    • throw 1 and throw "s" escape the catch, and the process exits 127. Output printed before the throw is lost, because it is never flushed.
    • The cause: the ThrowInfo follows the thrown value's static type, while an untyped catch variable gets a void* handler.
    • throw <any>x and class objects are caught.
    • eh_order.ts uses catch (e: TypeOf<1>) for this reason.
    • This deserves its own issue, because catch (e) is the usual TypeScript idiom.
  • Async at i686 is phase 4. Every async program exits 139 as a 32-bit exe, even one with no try. The cause is not EH.
  • EH type names use 64-bit mangling on x86 (??_R0PEAD@8, @88). tslang's throw and catch share these names, so tslang programs are unaffected. A throw from an x86 tslang DLL is caught by a C++ catch (int) in the host. Catching a pointer type thrown by MSVC-compiled x86 code would not match.
  • 00catch_value and 00catch_value_minimal exit 0 at i686 under gc, rc and none, in both release and debug.

Test plan

  • Windows Release suite: ctest -j 16 -C Release passes 2766/2766, including MLIRGenTests. The TargetInfo tests add aarch64 and thumbv7 Windows cases.

  • tslang/test/check-x86-eh.sh passes 53/53 in about 20 s. It checks:

    • the IR and relocation form of the EH tables;
    • __CxxThrowException@8 at x86 and _CxxThrowException at x64;
    • that no throw call is left at the C convention, with and without --opt;
    • tslang/test/x86/eh_order.ts, whose exact finally/catch/rethrow trace is checked;
    • eleven exception corpus files, run as 32-bit exes under gc, rc and none.

    Before the fixes, the IR and object checks failed with the old compiler. The script is run by hand, not in ctest; it needs the phase 2 x86 libraries, and it builds release only.

  • Checked by hand at i686:

    • 17 EH corpus files × gc/rc/none × release/debug, which adds the using/dispose and 01try_* files;
    • a debug build of eh_order.ts;
    • a throw crossing an x86 --emit=dll boundary, caught by a C++ catch (int);
    • generators and async functions with try/catch compile (--emit=obj) without a compiler crash.
  • check-x86-run.sh passes 6/6 and check-x86-libs.sh passes 11/11.

  • Linux CI. Not run locally. The new pass is compiled on every platform but only added for i686 MSVC.

🤖 Generated with Claude Code

ASDAlexander77 and others added 5 commits September 21, 2026 16:00
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ThrowInfo, CatchableType and CatchableTypeArray hold i32 cross-references.
64-bit MSVC stores image-base-relative RVAs there; 32-bit x86 stores the
absolute address, which is also 4 bytes, so only the value changes.

Both RTTI helpers (MLIR-level and LLVM-dialect) now build these through one
ehReference() helper gated on targetInfo.usesImageBaseRelativeEH, and
declare __ImageBase only when it is referenced. x64 output is unchanged.

Adds tslang/test/check-x86-eh.sh, which checks the x86 and x64 IR and the
x86 object's relocations (DIR32, not DIR32NB).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On i686-pc-windows-msvc, _CxxThrowException is __stdcall, so its symbol
is __CxxThrowException@8 and the link failed on the undecorated name.

A new module pass, CxxThrowCallingConvPass, sets x86_stdcallcc on the
declaration and on every call and invoke of it. It runs after
Win32ExceptionPass (which synthesizes rethrows and turns calls into
invokes) and before the optimization pipeline, since a call whose
convention differs from its callee's is UB that InstCombine turns into
unreachable. Any non-callee use is a hard error. It is gated on
targetInfo.stdcallDecoratesCxxThrow, so x64 output is unchanged.

check-x86-eh.sh gains the decorated-symbol check at x86 and x64 and an
IR check that no call site keeps the C convention, with and without
--opt. mask-ir.sh lists 00typeof_union_narrowing as known-noisy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
check-x86-eh.sh now links and runs x86 programs that throw. eh_order.ts
prints a trace that pins the unwinding order: finally blocks run
innermost-first, a throw from a catch reaches the outer try, a rethrow is
caught, and execution continues after each. Eleven exception corpus files
run beside it; they assert internally, so exit 0 is the check. Every case
runs under gc, rc and none, checks the exe is I386, and has a 20 s limit.

The rethrow in eh_order.ts uses a typed catch. An untyped catch variable
gets a void* handler, which does not match a thrown int, at x64 as well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The spec now describes the calling-convention pass and ehReference as
built, records the Phase 4 findings (async exits 139 at i686, 64-bit EH
name mangling, untyped catch misses thrown numbers) and the catch_value
results. usesImageBaseRelativeEH follows pointer width, as clang's
MicrosoftCXXABI does, so 32-bit ARM Windows is absolute too.
check-x86-eh.sh requires timeout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit b61e6c6 into main Sep 21, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the phase3-32bit-x86-eh branch September 21, 2026 17:05
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