From d58d5a500273e11de9e65d3e96bb11d7bb6ffd84 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 21 Sep 2026 16:00:10 +0100 Subject: [PATCH 1/5] Add the Phase 3 plan for 32-bit support (x86 exception handling) Co-Authored-By: Claude Opus 5 --- .../plans/2026-09-22-32-bit-phase-3-x86-eh.md | 415 ++++++++++++++++++ 1 file changed, 415 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-22-32-bit-phase-3-x86-eh.md diff --git a/docs/superpowers/plans/2026-09-22-32-bit-phase-3-x86-eh.md b/docs/superpowers/plans/2026-09-22-32-bit-phase-3-x86-eh.md new file mode 100644 index 000000000..14f27d7ce --- /dev/null +++ b/docs/superpowers/plans/2026-09-22-32-bit-phase-3-x86-eh.md @@ -0,0 +1,415 @@ +# 32-bit Phase 3: Win32 x86 Exception Handling Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 32-bit Windows programs (`-mtriple=i686-pc-windows-msvc`) that throw and catch link and behave correctly under `-mm=gc`, `-mm=rc` and `-mm=none`. + +**Architecture:** There are two narrow changes, both gated on `TargetInfo` policy predicates that Phase 1 introduced and nothing reads yet. +1. The EH cross-references in `ThrowInfo`, `CatchableType` and `CatchableTypeArray` are image-base-relative RVAs on x64 and absolute pointers on x86. The struct layouts are already identical, so only the stored value changes. +2. `_CxxThrowException` is `__stdcall` on x86. A small LLVM module pass sets that calling convention on the declaration and on every call site before optimization. + +A third task links and runs the exception corpus as 32-bit binaries and fixes whatever that uncovers within scope. + +**Tech Stack:** C++17, MLIR LLVM dialect, LLVM new pass manager, MSVC C++ EH (`__CxxFrameHandler3`), bash test scripts run by hand under Git Bash. + +**Spec:** `docs/superpowers/specs/2026-09-20-32-bit-compilation-design.md`, section "Phase 3 — Win32 x86 exception handling", plus "Two findings that reduced scope". + +## Global Constraints + +- The compiler stays an x64 process that cross-compiles. `--emit=jit` still refuses x86. Nothing in `tslang/tslang/jit.cpp` changes. +- Policy predicates are named for the decision, never for the arch: read `compileOptions.targetInfo.usesImageBaseRelativeEH` and `compileOptions.targetInfo.stdcallDecoratesCxxThrow`. Do not add `arch == x86` tests at call sites. +- **x64 output is unchanged by every task.** Before editing, capture a masked x64 `--emit=llvm` baseline from the compiler at this plan's base commit. After each task, diff against it with `tslang/test/compare-ir.sh x86_64-pc-windows-msvc `. The only differences allowed are the hash-order noisy files listed in `tslang/test/mask-ir.sh`. Rerun any that differ, to show they are noise. +- Hard errors, not silent fallbacks. +- Tests assert that control flow reaches the catch, and the order of unwinding. **No test asserts on a catch variable's contents**: reading a catch variable's value is known to be unreliable, independently of this work. +- Suite: `ctest -j 16 -C Release` from `__build/tslang/windows-msbuild-2026-release` stays at 2766/2766. `MLIRGenTests` stays green. +- The x86 libraries come from Phase 2's layout: + - `--gc-lib-path=3rdParty/gc/x64//lib` + - `--tslang-lib-path=__build/tslang-runtime/` + Both hold an `x86/` subdirectory. Scripts unset `GC_LIB_PATH`, `TSLANG_LIB_PATH`, `GC_SHARED_LIB_PATH` and `DEFAULT_LIB_PATH`, and pass `--opt` for release, as `tslang/test/check-x86-run.sh` already does. +- Commits are GPG-signed. Never bypass signing; if it times out, leave the changes staged and report. The commit trailer is `Co-Authored-By: Claude Opus 5 `. + +## Measured starting point (main adbd38bc) + +The exception corpus files `00try_catch`, `00nested_catch`, `00try_finally`, `00try_catch_rethrow`, `00throw_in_catch` and `51exceptions` all compile, link and run at x64 (`--opt -mm=gc --no-default-lib`). At i686 every one of them fails to link: + +``` +error LNK2019: unresolved external symbol __CxxThrowException referenced in function _main1 +``` + +The linker's hint, `__CxxThrowException@8`, is the stdcall name. Nothing past the link has been observed yet. + +## File structure + +| File | Change | Task | +| --- | --- | --- | +| `tslang/include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h` | One helper, `ehReference`, replaces the three copies of ptrtoint/sub/trunc. `__ImageBase` is declared only when used. | 1 | +| `tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h` | The same change in the MLIR-level helper. | 1 | +| `tslang/test/check-x86-eh.sh` | New. IR and object checks for x86 EH (Task 1), stdcall checks (Task 2), then link-and-run (Task 3). | 1, 2, 3 | +| `tslang/include/TypeScript/Pass/CxxThrowCallingConvPass.h`, `tslang/lib/TypeScriptExceptionPass/CxxThrowCallingConvPass.cpp` | New module pass. | 2 | +| `tslang/lib/TypeScriptExceptionPass/CMakeLists.txt` | Adds the new source. | 2 | +| `tslang/tslang/transform.cpp` | Adds the pass after `Win32ExceptionPass`, before the optimization pipeline. | 2 | +| `tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp:695-702` | The commented-out stdcall block is removed and replaced by a pointer to the new pass. | 2 | +| `tslang/test/x86/eh_order.ts` | New. Unwinding-order program with exact expected output. | 3 | + +--- + +### Task 1: EH cross-references are absolute on x86 + +**Files:** +- Modify: `tslang/include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h`. The three sites are near lines 328-341 (`catchableType`, field 2), 372-384 (catchable-array item) and 460-472 (`throwInfo`, field 4). `imageBase()` is near line 279; its call is near line 204. +- Modify: `tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h`. The three sites are near lines 446-460, 491-505 and 603-617. `imageBase()` is near line 399; its call is near line 307. +- Create: `tslang/test/check-x86-eh.sh` + +**Interfaces:** +- Consumes: + - `compileOptions.targetInfo.usesImageBaseRelativeEH`: false exactly for `i686-*-windows-msvc`, among the targets that select these helpers, because the helpers are chosen by `compileOptions.isWindows = TheTriple.isKnownWindowsMSVCEnvironment()`. + - `compileOptions.sizeBits()`. +- Produces: `tslang/test/check-x86-eh.sh ` with the helpers `emit_ir`, `ok`, `fail` and `need`. Tasks 2 and 3 append cases to it. + +Background, from the spec: `ThrowInfo` is `{i32 x 4}`, `CatchableType` is `{i32 x 7}` and `CatchableTypeArray` is `{i32, [N x i32]}`. On x64 the i32 cross-reference fields hold `ptr - __ImageBase`. On 32-bit x86 MSVC the same fields hold the absolute address, which is also 4 bytes. Today both helpers always emit `trunc(sub(ptrtoint ptr to i64, ptrtoint __ImageBase to i64)) to i32`. + +- [ ] **Step 1: Capture the x64 baseline.** Before editing anything, run `compare-ir.sh` with the current Release `tslang.exe` against an empty baseline directory. Read the script to see how it fills the baseline. Keep the output outside the repo. It is the reference for every task in this plan. + +- [ ] **Step 2: Write the failing check.** Create `tslang/test/check-x86-eh.sh`: + +```bash +#!/usr/bin/env bash +# Checks for 32-bit Windows (i686-pc-windows-msvc) exception handling. Hand-run, like the other +# check-x86-*.sh scripts: tslang/test/check-x86-eh.sh +set -u +TSLANG="${1:?usage: check-x86-eh.sh }" +HERE="$(cd "$(dirname "$0")" && pwd)" +REPO="$(cd "$HERE/../.." && pwd)" +TESTS="$REPO/tslang/test/tester/tests" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +unset GC_LIB_PATH TSLANG_LIB_PATH GC_SHARED_LIB_PATH DEFAULT_LIB_PATH +fail=0 +ok() { echo "ok $1"; } +bad() { echo "FAIL $1"; fail=1; } +need() { [ -e "$1" ] || { echo "FAIL missing prerequisite: $1"; exit 1; }; } +need "$TSLANG" + +# emit_ir -> path of the .ll +emit_ir() { + local out="$TMP/$2.$1.ll" + "$TSLANG" --emit=llvm --opt -mm=none --no-default-lib -mtriple="$1" "$TESTS/$2.ts" -o "$out" 2>"$out.err" \ + || { echo "compile failed: $1 $2"; cat "$out.err"; return 1; } + echo "$out" +} + +# --- Task 1: EH cross-references --------------------------------------------------------- +x86ll="$(emit_ir i686-pc-windows-msvc 00try_catch)" || bad "x86 IR: 00try_catch compiles" +x64ll="$(emit_ir x86_64-pc-windows-msvc 00try_catch)" || bad "x64 IR: 00try_catch compiles" +if [ -n "${x86ll:-}" ]; then + grep -q "__ImageBase" "$x86ll" && bad "x86 IR: no __ImageBase" || ok "x86 IR: no __ImageBase" + grep -q "ptrtoint ptr @.* to i32" "$x86ll" && ok "x86 IR: absolute i32 references" \ + || bad "x86 IR: absolute i32 references" +fi +if [ -n "${x64ll:-}" ]; then + grep -q "__ImageBase" "$x64ll" && ok "x64 IR: still image-base relative" \ + || bad "x64 IR: still image-base relative" +fi + +exit "$fail" +``` + +Constant folding decides the exact spelling in `--emit=llvm` output. The cross-references are global initializers, so they may print as `i32 ptrtoint (ptr @... to i32)` rather than an instruction. Run the script once, read the real x86 and x64 IR, and adjust the two `grep` patterns to the real spelling. Each pattern must still distinguish the old form (`sub` with `__ImageBase`) from the new one. Run it: the x86 checks FAIL today. + +- [ ] **Step 3: Implement in the LLVM-dialect helper.** Add one private method and use it at all three sites: + +```cpp + // A cross-reference inside ThrowInfo, CatchableType or CatchableTypeArray: an i32 field. + // 64-bit MSVC stores an image-base-relative RVA there, 32-bit x86 the absolute address, which + // is also 4 bytes - so the struct layouts coincide and only the value differs. + mlir::Value ehReference(mlir::Location loc, mlir::Value ptrValue) + { + if (!compileOptions.targetInfo.usesImageBaseRelativeEH) + { + if (compileOptions.sizeBits() != 32) + { + llvm::report_fatal_error("absolute EH references need 32-bit pointers"); + } + + return rewriter.create(loc, th.getI32Type(), ptrValue); + } + + auto intPtrTy = rewriter.getIntegerType(compileOptions.sizeBits()); + auto ptrInt = rewriter.create(loc, intPtrTy, ptrValue); + auto imageBasePtr = rewriter.create(loc, th.getPtrType(), + FlatSymbolRefAttr::get(rewriter.getContext(), imageBaseRef)); + auto imageBaseInt = rewriter.create(loc, intPtrTy, imageBasePtr); + auto rva = rewriter.create(loc, intPtrTy, ptrInt, imageBaseInt); + return rewriter.create(loc, th.getI32Type(), rva); + } +``` + +Each site becomes `auto itemValueN = ehReference(loc, );`. At x64 the result has exactly today's op sequence and types: `sizeBits()` is 64 there, which is the `i64` currently hard-coded. In `imageBase(loc)`, return `success()` without creating the global when `!usesImageBaseRelativeEH`. On x86 nothing references it, and an unused external `__ImageBase` would needlessly pull in `___ImageBase`. Check that this class has `compileOptions` and `th` members with these names. If the names differ, adapt them and say so in your report. + +- [ ] **Step 4: Implement in the MLIR-level helper.** Use the same method shape with the `mlir_ts` ops that the three sites already use: + - `mlir_ts::CastOp` to an integer type for the ptrtoint; + - `mlir_ts::ArithmeticBinaryOp` with `SyntaxKind::MinusToken` for the sub; + - `mlir_ts::CastOp` to `mth.getI32Type()` for the trunc. + + For the integer type, use the same width source as the Phase 1 sites in this file: line ~143 already uses `compileOptions.sizeBits()`. The x86 branch is a single `mlir_ts::CastOp` of the pointer to `mth.getI32Type()`. Gate `imageBase(loc)` the same way. + + Then find out which of the two helpers produces the globals actually emitted, or whether both do for different paths: grep the callers of `MLIRRTTIHelperVC` and `LLVMRTTIHelperVC` (`setRTTIForType`, `throwInfoPtrValue`, `setPersonality`), and emit `--emit=mlir` for `00try_catch` at i686. Record the answer in your report. Both helpers are changed either way, so neither is left computing RVAs on x86. + +- [ ] **Step 5: Verify.** + - `check-x86-eh.sh` → all ok. + - `compare-ir.sh` for x64 against the Step 1 baseline → noise only. + - `ctest -j 16 -C Release` → 2766/2766. + - Also build `--emit=obj` for `00try_catch` at i686. Confirm with `3rdParty/llvm/x64/release/bin/llvm-readobj.exe --relocations` that the relocations in the `_TI`/`_CT`/`_CTA` globals are `IMAGE_REL_I386_DIR32` (absolute), not `DIR32NB` (image-relative). + +- [ ] **Step 6: Commit** the two helpers and the script. + +--- + +### Task 2: `_CxxThrowException` is stdcall on x86 + +**Files:** +- Create: `tslang/include/TypeScript/Pass/CxxThrowCallingConvPass.h`, `tslang/lib/TypeScriptExceptionPass/CxxThrowCallingConvPass.cpp` +- Modify: `tslang/lib/TypeScriptExceptionPass/CMakeLists.txt` (add the source), `tslang/tslang/transform.cpp` (~line 336-340, where `Win32ExceptionPass` is added), `tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp:695-702` (the commented-out block) +- Modify: `tslang/test/check-x86-eh.sh` + +**Interfaces:** +- Consumes: `compileOptions.targetInfo.stdcallDecoratesCxxThrow`, which is true exactly for `i686-*-windows-msvc`. +- Produces: `ts::CxxThrowCallingConvPass`, an `llvm::PassInfoMixin` module pass with `run(llvm::Module&, llvm::ModuleAnalysisManager&)`. + +`_CxxThrowException` is declared in two places: +- MLIR lowering: `include/TypeScript/LowerToLLVM/ThrowLogic.h:92-110`, `ch.getOrInsertFunction`, then `LLVM::CallOp`/`LLVM::InvokeOp`. +- `Win32ExceptionPass::getThrowFn`, for the rethrow in a synthesized catch-all. + +`Win32ExceptionPass` also rewrites calls into invokes (`ToInvoke`). So in LLVM IR, before optimization, the one place that sees every declaration and every call site is a module pass. The calling convention must be set on the callee **and** on every call site. A call whose calling convention differs from its callee's is undefined behaviour, and InstCombine replaces such calls with `unreachable`. That is why the pass must run before the optimization pipeline. + +- [ ] **Step 1: Write the failing check.** Append to `check-x86-eh.sh`, before `exit`: + +```bash +# --- Task 2: _CxxThrowException is __stdcall on x86 --------------------------------------- +READOBJ="$REPO/3rdParty/llvm/x64/release/bin/llvm-readobj.exe" +need "$READOBJ" +for triple in i686-pc-windows-msvc x86_64-pc-windows-msvc; do + obj="$TMP/throw.$triple.obj" + if "$TSLANG" --emit=obj --opt -mm=none --no-default-lib -mtriple="$triple" "$TESTS/00try_catch.ts" -o "$obj" 2>"$obj.err"; then + syms="$("$READOBJ" --symbols "$obj" | grep -o 'Name: .*CxxThrowException.*' | sort -u)" + case "$triple" in + i686*) [ "$syms" = "Name: __CxxThrowException@8" ] && ok "x86 obj: __CxxThrowException@8" \ + || bad "x86 obj: __CxxThrowException@8 (got: $syms)";; + x86_64*) [ "$syms" = "Name: _CxxThrowException" ] && ok "x64 obj: _CxxThrowException unchanged" \ + || bad "x64 obj: _CxxThrowException unchanged (got: $syms)";; + esac + else + bad "$triple obj: 00try_catch compiles"; cat "$obj.err" + fi +done +``` + +Run it: the x86 case FAILs today, getting `__CxxThrowException` without `@8`. Also check that `00try_catch` really contains a rethrow path, or add a second file that does (`00try_catch_rethrow`), so the call `Win32ExceptionPass` synthesizes is covered too. The check must fail if any single call site keeps the C calling convention. So also emit that file's IR after the pass (see Step 4) and grep that no `call void @_CxxThrowException` or `invoke void @_CxxThrowException` without `x86_stdcallcc` remains. + +- [ ] **Step 2: The pass.** `tslang/include/TypeScript/Pass/CxxThrowCallingConvPass.h`, following `Win32ExceptionPass.h` in the same directory: + +```cpp +#ifndef TYPESCRIPT_CXXTHROWCALLINGCONVPASS_H_ +#define TYPESCRIPT_CXXTHROWCALLINGCONVPASS_H_ + +#include "llvm/IR/PassManager.h" + +namespace ts +{ + // _CxxThrowException is __stdcall on 32-bit x86 MSVC; its symbol is then __CxxThrowException@8. + // Sets that convention on the declaration and on every call and invoke of it. A call whose + // convention differs from its callee's is undefined behaviour, which InstCombine turns into + // `unreachable`, so this runs before the optimization pipeline. + class CxxThrowCallingConvPass : public llvm::PassInfoMixin + { + public: + llvm::PreservedAnalyses run(llvm::Module &M, llvm::ModuleAnalysisManager &AM); + }; +} // namespace ts + +#endif // TYPESCRIPT_CXXTHROWCALLINGCONVPASS_H_ +``` + +`tslang/lib/TypeScriptExceptionPass/CxxThrowCallingConvPass.cpp`: + +```cpp +#include "TypeScript/Pass/CxxThrowCallingConvPass.h" + +#include "llvm/IR/InstrTypes.h" +#include "llvm/IR/Module.h" + +namespace ts +{ + llvm::PreservedAnalyses CxxThrowCallingConvPass::run(llvm::Module &M, llvm::ModuleAnalysisManager &AM) + { + auto *throwFn = M.getFunction("_CxxThrowException"); + if (!throwFn) + { + return llvm::PreservedAnalyses::all(); + } + + throwFn->setCallingConv(llvm::CallingConv::X86_StdCall); + for (auto *user : throwFn->users()) + { + if (auto *call = llvm::dyn_cast(user)) + { + if (call->getCalledOperand() == throwFn) + { + call->setCallingConv(llvm::CallingConv::X86_StdCall); + } + } + } + + return llvm::PreservedAnalyses::none(); + } +} // namespace ts +``` + +Match the include and namespace style of `Win32ExceptionPass.cpp`. If the function is used as a value rather than called directly, that is unexpected. Stop and report it: the pass would then be missing that use. + +- [ ] **Step 3: Wire it in.** + - In `transform.cpp`, directly after the `Win32ExceptionPass` block: + + ```cpp + if (compileOptions.targetInfo.stdcallDecoratesCxxThrow) + { + mpm.addPass(ts::CxxThrowCallingConvPass()); + } + ``` + + Add the `#include`. + - Add the source to `lib/TypeScriptExceptionPass/CMakeLists.txt`. + - In `Win32ExceptionPass.cpp`, replace the commented-out block at 695-702 with one line: + + ```cpp + // On 32-bit x86 this is __stdcall; CxxThrowCallingConvPass sets that on it and its call sites. + ``` + + - Check that `transform.cpp`'s pipeline runs for `--emit=obj`, `--emit=exe`, `--emit=dll` and `--emit=llvm`, with and without `--opt`. If some emit path lowers to an object without going through it, the pass is missing there: find that path and cover it. Say which paths you checked. + +- [ ] **Step 4: Verify.** + - `check-x86-eh.sh` → all ok. + - `--emit=llvm` for `00try_catch` and `00try_catch_rethrow` at i686 shows `declare x86_stdcallcc void @_CxxThrowException`, and every call or invoke of it carries `x86_stdcallcc`. + - `compare-ir.sh` x64 → noise only. + - `ctest -j 16 -C Release` → 2766/2766. + - Try linking `00try_catch` at i686 with `--emit=exe --opt -mm=none --no-default-lib`. Record the result without fixing anything past the link: link errors, or the run's exit code. That result is Task 3's input. + +- [ ] **Step 5: Commit.** + +--- + +### Task 3: 32-bit programs throw and catch correctly (Phase 3 gate) + +**Files:** +- Create: `tslang/test/x86/eh_order.ts` +- Modify: `tslang/test/check-x86-eh.sh` +- Modify, only if Step 3 finds a defect in scope: the file that defect is in. + +**Interfaces:** +- Consumes: Tasks 1 and 2, and Phase 2's x86 library layout. +- Produces: the Phase 3 gate result, and a recorded list of anything that fails for reasons outside Phase 3. + +- [ ] **Step 1: Unwinding-order program.** Create `tslang/test/x86/eh_order.ts`. It prints a trace and never reads a catch variable's value: + +```ts +function thrower(depth: number) { + try { + if (depth == 0) { + print("throw"); + throw 1; + } + thrower(depth - 1); + } finally { + print("finally " + depth); + } +} + +function nested() { + try { + try { + throw 1; + } catch { + print("inner catch"); + throw 2; // throw from a catch + } + } catch { + print("outer catch"); + } +} + +function rethrow() { + try { + try { + throw 1; + } catch (e) { + print("rethrow"); + throw e; + } + } catch { + print("caught rethrown"); + } +} + +try { + thrower(2); +} catch { + print("caught"); +} +nested(); +rethrow(); +print("done"); +``` + +Expected output: + +``` +throw +finally 0 +finally 1 +finally 2 +caught +inner catch +outer catch +rethrow +caught rethrown +done +``` + +First confirm the program compiles and prints exactly this at x64: `--opt -mm=gc --no-default-lib`, with the x64 libs. If the frontend rejects a construct, adapt it while keeping four things: finally blocks run innermost-first during unwinding, a throw from inside a catch is caught by the outer try, a rethrow is caught, and execution continues after each. Note every adaptation in a comment in the file. + +- [ ] **Step 2: Link-and-run cases.** Append to `check-x86-eh.sh`. For each memory model in `gc rc none`, build these programs at `-mtriple=i686-pc-windows-msvc --opt --no-default-lib` and run them: + - `tslang/test/x86/eh_order.ts`, which must match the expected output above exactly (strip `\r`); + - the corpus files `00try_catch`, `00nested_catch`, `00try_finally`, `00try_finally_return`, `00try_finally_break_continue`, `00try_catch_rethrow`, `00try_catch_mismatch_rethrow`, `00throw_in_catch`, `00try_catch_return`, `00alloc_in_catch` and `51exceptions`. Each must exit 0; they assert internally. + + For each run, also check that the exe's machine is `IMAGE_FILE_MACHINE_I386 (0x14C)`. Pass `--gc-lib-path="$REPO/3rdParty/gc/x64/release/lib"` for gc only, and `--tslang-lib-path="$REPO/__build/tslang-runtime/release"` always, as `check-x86-run.sh` does. Also check that the x86 prerequisites exist with `need`. Before adding a corpus file, confirm it runs with this flag set at x64. If one depends on the default library, or otherwise can't be built with `--no-default-lib`, leave it out and say which one and why. Use a 20-second timeout per run, so a hang is a FAIL and not a stuck script. + + Also, not gated: run `00catch_value` and `00catch_value_minimal` at x86 and record the results. They read catch values, so per the Global Constraints they don't gate this phase, but their results are useful for Phase 4. + +- [ ] **Step 3: Run, and fix what is in scope.** Run the script. Any failure gets a diagnosis before any fix: compare with x64, and find the first point of divergence. + - **In scope, fix here:** anything in the x86 EH path itself. That covers the throw call, the EH tables and their relocations, funclet or personality emission, the link (for example a `/SAFESEH` complaint from `LNK2026`), and the catch-type matching. A fix must not change x64 output; prove that with `compare-ir.sh`. + - **Out of scope, record only:** anything that fails the same way with no exception involved, or that belongs to Phase 4 (the default library, async). Report it with the diagnosis. If such a failure blocks a gate case, report BLOCKED and don't work around it. + +- [ ] **Step 4: Verify.** + - `check-x86-eh.sh` → all ok. + - `check-x86-run.sh` and `check-x86-libs.sh` still all ok. + - `compare-ir.sh` x64 → noise only. + - `ctest -j 16 -C Release` → 2766/2766. + +- [ ] **Step 5: Commit** the program and the script, plus any in-scope fix. An in-scope fix should be its own commit. + +--- + +## Phase 3 gate + +- [ ] `check-x86-eh.sh`: x86 EH tables hold absolute references, with no `__ImageBase`; the throw symbol is `__CxxThrowException@8` at x86 and `_CxxThrowException` at x64. +- [ ] `check-x86-eh.sh`: `eh_order.ts` prints the exact trace, and the listed exception corpus files exit 0, as 32-bit binaries under gc, rc and none. That covers throw/catch, nested catch, try/finally and rethrow. +- [ ] x64 `--emit=llvm` is unchanged from the plan's base commit, apart from the known noise. +- [ ] `ctest -j 16 -C Release` passes 2766/2766, and `MLIRGenTests` passes. +- [ ] `00catch_value*` results at x86 are recorded for Phase 4. + +## Rulings made while writing this plan + +- **The calling convention is set by one LLVM module pass**, not at each of the three places that create the declaration or a call. The MLIR lowering, the synthesized rethrow in `Win32ExceptionPass` and `ToInvoke` all create call sites. Only a module-level pass after all of them sees every one, and a single missed call site would be undefined behaviour that InstCombine turns into `unreachable`. +- **`__ImageBase` is not declared on x86.** Nothing references it there. +- **The helpers are not unified.** The LLVM-dialect and MLIR-level helpers each keep their own `ehReference`, because they build with different op sets. Merging them would be a refactor unrelated to 32-bit. +- **The corpus files are run directly by the x86 script, not through ctest.** Running the whole suite at 32 bits, through the test runner and with the default library, is Phase 4. From d505971bc50a612ee2e27785e6101183a373e1b8 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 21 Sep 2026 16:21:16 +0100 Subject: [PATCH 2/5] Make EH cross-references absolute on 32-bit x86 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 --- .../LowerToLLVM/LLVMRTTIHelperVCWin32.h | 73 ++++++++--------- .../MLIRLogic/MLIRRTTIHelperVCWin32.h | 80 +++++++++---------- tslang/test/check-x86-eh.sh | 66 +++++++++++++++ 3 files changed, 137 insertions(+), 82 deletions(-) create mode 100755 tslang/test/check-x86-eh.sh diff --git a/tslang/include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h b/tslang/include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h index f9f91085e..2be78603d 100644 --- a/tslang/include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h +++ b/tslang/include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h @@ -278,6 +278,13 @@ class LLVMRTTIHelperVCWin32 LogicalResult imageBase(mlir::Location loc) { + if (!compileOptions.targetInfo.usesImageBaseRelativeEH) + { + // absolute EH references (32-bit x86) never reference __ImageBase, and an unused + // external declaration would still pull in the linker-defined symbol + return success(); + } + auto name = imageBaseRef; if (parentModule.lookupSymbol(name)) { @@ -288,6 +295,30 @@ class LLVMRTTIHelperVCWin32 return success(); } + // A cross-reference inside ThrowInfo, CatchableType or CatchableTypeArray: an i32 field. + // 64-bit MSVC stores an image-base-relative RVA there, 32-bit x86 the absolute address, which + // is also 4 bytes - so the struct layouts coincide and only the value differs. + mlir::Value ehReference(mlir::Location loc, mlir::Value ptrValue) + { + if (!compileOptions.targetInfo.usesImageBaseRelativeEH) + { + if (compileOptions.sizeBits() != 32) + { + llvm::report_fatal_error("absolute EH references need 32-bit pointers"); + } + + return rewriter.create(loc, th.getI32Type(), ptrValue); + } + + auto intPtrTy = rewriter.getIntegerType(compileOptions.sizeBits()); + auto ptrInt = rewriter.create(loc, intPtrTy, ptrValue); + auto imageBasePtr = + rewriter.create(loc, th.getPtrType(), FlatSymbolRefAttr::get(rewriter.getContext(), imageBaseRef)); + auto imageBaseInt = rewriter.create(loc, intPtrTy, imageBasePtr); + auto rva = rewriter.create(loc, intPtrTy, ptrInt, imageBaseInt); + return rewriter.create(loc, th.getI32Type(), rva); + } + LogicalResult catchableTypes(mlir::Location loc) { for (auto type : types) @@ -328,19 +359,7 @@ class LLVMRTTIHelperVCWin32 auto rttiTypeDescriptor2PtrValue = rewriter.create(loc, LLVM::LLVMPointerType::get(rewriter.getContext()), FlatSymbolRefAttr::get(rewriter.getContext(), typeInfoRefName)); - auto rttiTypeDescriptor2IntValue = rewriter.create(loc, th.getI64Type(), rttiTypeDescriptor2PtrValue); - - auto imageBasePtrValue = - rewriter.create(loc, th.getPtrType(), FlatSymbolRefAttr::get(rewriter.getContext(), imageBaseRef)); - auto imageBaseIntValue = rewriter.create(loc, th.getI64Type(), imageBasePtrValue); - - // sub - auto subResValue = rewriter.create(loc, th.getI64Type(), rttiTypeDescriptor2IntValue, imageBaseIntValue); - - // trunc - auto subRes32Value = rewriter.create(loc, th.getI32Type(), subResValue); - - auto itemValue2 = subRes32Value; + auto itemValue2 = ehReference(loc, rttiTypeDescriptor2PtrValue); ch.setStructValue(loc, structVal, itemValue2, 1); auto itemValue3 = rewriter.create(loc, th.getI32Type(), rewriter.getI32IntegerAttr(0)); @@ -371,19 +390,7 @@ class LLVMRTTIHelperVCWin32 { auto rttiCatchableTypePtrValue = rewriter.create( loc, LLVM::LLVMPointerType::get(rewriter.getContext()), FlatSymbolRefAttr::get(rewriter.getContext(), catchableTypeRefName)); - auto rttiCatchableTypeIntValue = rewriter.create(loc, th.getI64Type(), rttiCatchableTypePtrValue); - - auto imageBasePtrValue = - rewriter.create(loc, th.getPtrType(), FlatSymbolRefAttr::get(rewriter.getContext(), imageBaseRef)); - auto imageBaseIntValue = rewriter.create(loc, th.getI64Type(), imageBasePtrValue); - - // sub - auto subResValue = rewriter.create(loc, th.getI64Type(), rttiCatchableTypeIntValue, imageBaseIntValue); - - // trunc - auto subRes32Value = rewriter.create(loc, th.getI32Type(), subResValue); - - return subRes32Value; + return ehReference(loc, rttiCatchableTypePtrValue); } LogicalResult catchableArrayType(mlir::Location loc) @@ -459,18 +466,8 @@ class LLVMRTTIHelperVCWin32 // value 3 auto rttiCatchableArrayTypePtrValue = rewriter.create( loc, LLVM::LLVMPointerType::get(rewriter.getContext()), FlatSymbolRefAttr::get(rewriter.getContext(), catchableTypeInfoArrayRef)); - auto rttiCatchableArrayTypeIntValue = rewriter.create(loc, th.getI64Type(), rttiCatchableArrayTypePtrValue); - - auto imageBasePtrValue = - rewriter.create(loc, th.getPtrType(), FlatSymbolRefAttr::get(rewriter.getContext(), imageBaseRef)); - auto imageBaseIntValue = rewriter.create(loc, th.getI64Type(), imageBasePtrValue); - - // sub - auto subResValue = rewriter.create(loc, th.getI64Type(), rttiCatchableArrayTypeIntValue, imageBaseIntValue); - - // trunc - auto subRes32Value = rewriter.create(loc, th.getI32Type(), subResValue); - ch.setStructValue(loc, structValue, subRes32Value, 3); + auto rttiCatchableArrayTypeRefValue = ehReference(loc, rttiCatchableArrayTypePtrValue); + ch.setStructValue(loc, structValue, rttiCatchableArrayTypeRefValue, 3); rewriter.create(loc, ValueRange{structValue}); diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h b/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h index beba95b0e..bcf964d68 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h @@ -398,6 +398,13 @@ class MLIRRTTIHelperVCWin32 mlir::LogicalResult imageBase(mlir::Location loc) { + if (!compileOptions.targetInfo.usesImageBaseRelativeEH) + { + // absolute EH references (32-bit x86) never reference __ImageBase, and an unused + // external declaration would still pull in the linker-defined symbol + return mlir::success(); + } + auto name = windows::imageBaseRef; if (parentModule.lookupSymbol(name)) { @@ -408,6 +415,31 @@ class MLIRRTTIHelperVCWin32 return mlir::success(); } + // A cross-reference inside ThrowInfo, CatchableType or CatchableTypeArray: an i32 field. + // 64-bit MSVC stores an image-base-relative RVA there, 32-bit x86 the absolute address, which + // is also 4 bytes - so the struct layouts coincide and only the value differs. + mlir::Value ehReference(mlir::Location loc, mlir::Value ptrValue) + { + if (!compileOptions.targetInfo.usesImageBaseRelativeEH) + { + if (compileOptions.sizeBits() != 32) + { + llvm::report_fatal_error("absolute EH references need 32-bit pointers"); + } + + return rewriter.create(loc, mth.getI32Type(), ptrValue); + } + + auto intPtrTy = mlir::IntegerType::get(rewriter.getContext(), compileOptions.sizeBits()); + auto ptrInt = rewriter.create(loc, intPtrTy, ptrValue); + auto imageBasePtr = rewriter.create(loc, mth.getOpaqueType(), + mlir::FlatSymbolRefAttr::get(rewriter.getContext(), windows::imageBaseRef)); + auto imageBaseInt = rewriter.create(loc, intPtrTy, imageBasePtr); + auto rva = rewriter.create( + loc, intPtrTy, rewriter.getI32IntegerAttr(static_cast(SyntaxKind::MinusToken)), ptrInt, imageBaseInt); + return rewriter.create(loc, mth.getI32Type(), rva); + } + mlir::LogicalResult catchableTypes(mlir::Location loc) { for (auto type : types) @@ -445,21 +477,7 @@ class MLIRRTTIHelperVCWin32 auto rttiTypeDescriptor2PtrValue = rewriter.create(loc, getRttiTypeDescriptor2PtrTy(StringRef(typeName).size()), mlir::FlatSymbolRefAttr::get(rewriter.getContext(), typeInfoRefName)); - auto rttiTypeDescriptor2IntValue = rewriter.create(loc, mth.getI64Type(), rttiTypeDescriptor2PtrValue); - - auto imageBasePtrValue = rewriter.create( - loc, mth.getOpaqueType(), mlir::FlatSymbolRefAttr::get(rewriter.getContext(), windows::imageBaseRef)); - auto imageBaseIntValue = rewriter.create(loc, mth.getI64Type(), imageBasePtrValue); - - // sub - auto subResValue = rewriter.create( - loc, mth.getI64Type(), rewriter.getI32IntegerAttr(static_cast(SyntaxKind::MinusToken)), - rttiTypeDescriptor2IntValue, imageBaseIntValue); - - // trunc - auto subRes32Value = rewriter.create(loc, mth.getI32Type(), subResValue); - - auto itemValue2 = subRes32Value; + auto itemValue2 = ehReference(loc, rttiTypeDescriptor2PtrValue); setStructValue(loc, structVal, itemValue2, 1); auto itemValue3 = rewriter.create(loc, mth.getI32Type(), rewriter.getI32IntegerAttr(0)); @@ -490,21 +508,7 @@ class MLIRRTTIHelperVCWin32 { auto rttiCatchableTypePtrValue = rewriter.create( loc, getCatchableTypePtrTy(), mlir::FlatSymbolRefAttr::get(rewriter.getContext(), catchableTypeRefName)); - auto rttiCatchableTypeIntValue = rewriter.create(loc, mth.getI64Type(), rttiCatchableTypePtrValue); - - auto imageBasePtrValue = rewriter.create(loc, mth.getOpaqueType(), - mlir::FlatSymbolRefAttr::get(rewriter.getContext(), windows::imageBaseRef)); - auto imageBaseIntValue = rewriter.create(loc, mth.getI64Type(), imageBasePtrValue); - - // sub - auto subResValue = rewriter.create( - loc, mth.getI64Type(), rewriter.getI32IntegerAttr(static_cast(SyntaxKind::MinusToken)), rttiCatchableTypeIntValue, - imageBaseIntValue); - - // trunc - auto subRes32Value = rewriter.create(loc, mth.getI32Type(), subResValue); - - return subRes32Value; + return ehReference(loc, rttiCatchableTypePtrValue); } mlir::LogicalResult catchableArrayType(mlir::Location loc) @@ -602,20 +606,8 @@ class MLIRRTTIHelperVCWin32 // value 3 auto rttiCatchableArrayTypePtrValue = rewriter.create( loc, getCatchableArrayTypePtrTy(arraySize), mlir::FlatSymbolRefAttr::get(rewriter.getContext(), catchableTypeInfoArrayRef)); - auto rttiCatchableArrayTypeIntValue = rewriter.create(loc, mth.getI64Type(), rttiCatchableArrayTypePtrValue); - - auto imageBasePtrValue = rewriter.create(loc, mth.getOpaqueType(), - mlir::FlatSymbolRefAttr::get(rewriter.getContext(), windows::imageBaseRef)); - auto imageBaseIntValue = rewriter.create(loc, mth.getI64Type(), imageBasePtrValue); - - // sub - auto subResValue = rewriter.create( - loc, mth.getI64Type(), rewriter.getI32IntegerAttr(static_cast(SyntaxKind::MinusToken)), rttiCatchableArrayTypeIntValue, - imageBaseIntValue); - - // trunc - auto subRes32Value = rewriter.create(loc, mth.getI32Type(), subResValue); - setStructValue(loc, structValue, subRes32Value, 3); + auto rttiCatchableArrayTypeRefValue = ehReference(loc, rttiCatchableArrayTypePtrValue); + setStructValue(loc, structValue, rttiCatchableArrayTypeRefValue, 3); rewriter.create(loc, mlir::ValueRange{structValue}); diff --git a/tslang/test/check-x86-eh.sh b/tslang/test/check-x86-eh.sh new file mode 100755 index 000000000..e51103484 --- /dev/null +++ b/tslang/test/check-x86-eh.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Checks for 32-bit Windows (i686-pc-windows-msvc) exception handling. Hand-run, like the other +# check-x86-*.sh scripts: tslang/test/check-x86-eh.sh +set -u +TSLANG="${1:?usage: check-x86-eh.sh }" +HERE="$(cd "$(dirname "$0")" && pwd)" +REPO="$(cd "$HERE/../.." && pwd)" +TESTS="$REPO/tslang/test/tester/tests" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +unset GC_LIB_PATH TSLANG_LIB_PATH GC_SHARED_LIB_PATH DEFAULT_LIB_PATH +fail=0 +ok() { echo "ok $1"; } +bad() { echo "FAIL $1"; fail=1; } +need() { [ -e "$1" ] || { echo "FAIL missing prerequisite: $1"; exit 1; }; } +need "$TSLANG" + +# emit_ir -> path of the .ll +emit_ir() { + local out="$TMP/$2.$1.ll" + "$TSLANG" --emit=llvm --opt -mm=none --no-default-lib -mtriple="$1" "$TESTS/$2.ts" -o "$out" 2>"$out.err" \ + || { echo "compile failed: $1 $2" >&2; cat "$out.err" >&2; return 1; } + echo "$out" +} + +# --- Task 1: EH cross-references --------------------------------------------------------- +# The i32 cross-references inside _TI (ThrowInfo), _CT (CatchableType) and _CTA +# (CatchableTypeArray) are global initializers, so they print as constant expressions: +# x64: i32 trunc (i64 sub (i64 ptrtoint (ptr @X to i64), i64 ptrtoint (ptr @__ImageBase to i64)) to i32) +# x86: i32 ptrtoint (ptr @X to i32) +x86ll="$(emit_ir i686-pc-windows-msvc 00try_catch)" || bad "x86 IR: 00try_catch compiles" +x64ll="$(emit_ir x86_64-pc-windows-msvc 00try_catch)" || bad "x64 IR: 00try_catch compiles" +# Names may be quoted (@"_CT??_R0H@84"), so each prefix allows an optional quote. +eh_globals=("_CT[?][?]" "_CTA" "_TI") +label() { case "$1" in _CTA) echo "CatchableTypeArray";; _TI) echo "ThrowInfo";; *) echo "CatchableType";; esac; } +if [ -n "${x86ll:-}" ]; then + grep -q "__ImageBase" "$x86ll" && bad "x86 IR: no __ImageBase" || ok "x86 IR: no __ImageBase" + for g in "${eh_globals[@]}"; do + grep -Eq "^@\"?$g[^ ]* = .* i32 ptrtoint \(ptr @[^ ]+ to i32\)" "$x86ll" \ + && ok "x86 IR: $(label "$g") holds absolute i32 references" || bad "x86 IR: $(label "$g") holds absolute i32 references" + done +fi +if [ -n "${x64ll:-}" ]; then + for g in "${eh_globals[@]}"; do + grep -Eq "^@\"?$g[^ ]* = .* i32 trunc \(i64 sub \(i64 ptrtoint \(ptr @[^ ]+ to i64\), i64 ptrtoint \(ptr @__ImageBase to i64\)\) to i32\)" "$x64ll" \ + && ok "x64 IR: $(label "$g") still image-base relative" || bad "x64 IR: $(label "$g") still image-base relative" + done +fi + +# In the x86 object the references must be absolute (IMAGE_REL_I386_DIR32), not image-relative +# (IMAGE_REL_I386_DIR32NB, which is what the RVA arithmetic lowered to). +READOBJ="$REPO/3rdParty/llvm/x64/release/bin/llvm-readobj.exe" +need "$READOBJ" +x86obj="$TMP/00try_catch.i686.obj" +if "$TSLANG" --emit=obj --opt -mm=none --no-default-lib -mtriple=i686-pc-windows-msvc "$TESTS/00try_catch.ts" \ + -o "$x86obj" 2>"$x86obj.err"; then + "$READOBJ" --relocations "$x86obj" > "$x86obj.rel" + grep -q "DIR32NB" "$x86obj.rel" && bad "x86 obj: no image-relative (DIR32NB) relocations" \ + || ok "x86 obj: no image-relative (DIR32NB) relocations" + grep -Eq "IMAGE_REL_I386_DIR32 __CTA1H " "$x86obj.rel" && ok "x86 obj: absolute (DIR32) EH relocations" \ + || bad "x86 obj: absolute (DIR32) EH relocations" +else + bad "x86 obj: 00try_catch compiles"; cat "$x86obj.err" +fi + +exit "$fail" From ff385e50f976f8479d2b799a2be4d3f3726c33f3 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 21 Sep 2026 16:39:19 +0100 Subject: [PATCH 3/5] Make _CxxThrowException stdcall on 32-bit x86 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 --- .../TypeScript/Pass/CxxThrowCallingConvPass.h | 25 ++++++++++++ .../TypeScriptExceptionPass/CMakeLists.txt | 1 + .../CxxThrowCallingConvPass.cpp | 38 ++++++++++++++++++ .../Win32ExceptionPass.cpp | 9 +---- tslang/test/check-x86-eh.sh | 40 +++++++++++++++++++ tslang/test/mask-ir.sh | 1 + tslang/tslang/transform.cpp | 8 ++++ 7 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 tslang/include/TypeScript/Pass/CxxThrowCallingConvPass.h create mode 100644 tslang/lib/TypeScriptExceptionPass/CxxThrowCallingConvPass.cpp diff --git a/tslang/include/TypeScript/Pass/CxxThrowCallingConvPass.h b/tslang/include/TypeScript/Pass/CxxThrowCallingConvPass.h new file mode 100644 index 000000000..609c4274e --- /dev/null +++ b/tslang/include/TypeScript/Pass/CxxThrowCallingConvPass.h @@ -0,0 +1,25 @@ +#ifndef CXXTHROWCALLINGCONVPASS__H +#define CXXTHROWCALLINGCONVPASS__H + +#include "llvm/Pass.h" +#include "llvm/IR/PassManager.h" + +namespace ts +{ + // _CxxThrowException is __stdcall on 32-bit x86 MSVC; its symbol is then __CxxThrowException@8. + // Sets that convention on the declaration and on every call and invoke of it. A call whose + // convention differs from its callee's is undefined behaviour, which InstCombine turns into + // `unreachable`, so this runs before the optimization pipeline. + class CxxThrowCallingConvPass : public llvm::PassInfoMixin + { + public: + llvm::PreservedAnalyses run(llvm::Module &M, llvm::ModuleAnalysisManager &AM); + + static bool isRequired() + { + return true; + } + }; +} + +#endif // CXXTHROWCALLINGCONVPASS__H diff --git a/tslang/lib/TypeScriptExceptionPass/CMakeLists.txt b/tslang/lib/TypeScriptExceptionPass/CMakeLists.txt index 585af93da..956901bf0 100644 --- a/tslang/lib/TypeScriptExceptionPass/CMakeLists.txt +++ b/tslang/lib/TypeScriptExceptionPass/CMakeLists.txt @@ -7,6 +7,7 @@ set(LLVM_LINK_COMPONENTS add_llvm_library(TypeScriptExceptionPass Win32ExceptionPass.cpp LandingPadFixPass.cpp + CxxThrowCallingConvPass.cpp ADDITIONAL_HEADER_DIRS diff --git a/tslang/lib/TypeScriptExceptionPass/CxxThrowCallingConvPass.cpp b/tslang/lib/TypeScriptExceptionPass/CxxThrowCallingConvPass.cpp new file mode 100644 index 000000000..80271d8a2 --- /dev/null +++ b/tslang/lib/TypeScriptExceptionPass/CxxThrowCallingConvPass.cpp @@ -0,0 +1,38 @@ +#include "TypeScript/Pass/CxxThrowCallingConvPass.h" + +#include "llvm/IR/InstrTypes.h" +#include "llvm/IR/Module.h" +#include "llvm/Support/ErrorHandling.h" + +using namespace llvm; + +#define DEBUG_TYPE "pass" + +namespace ts +{ + llvm::PreservedAnalyses CxxThrowCallingConvPass::run(llvm::Module &M, llvm::ModuleAnalysisManager &AM) + { + auto *throwFn = M.getFunction("_CxxThrowException"); + if (!throwFn) + { + return llvm::PreservedAnalyses::all(); + } + + throwFn->setCallingConv(CallingConv::X86_StdCall); + for (auto *user : throwFn->users()) + { + auto *call = dyn_cast(user); + if (!call || call->getCalledOperand() != throwFn) + { + // Only direct calls and invokes are expected. Any other use (the function taken as + // a value) could reach an indirect call this pass cannot give the convention to. + report_fatal_error("_CxxThrowException is used other than as a direct callee; " + "its x86 stdcall convention cannot be applied to that use"); + } + + call->setCallingConv(CallingConv::X86_StdCall); + } + + return llvm::PreservedAnalyses::none(); + } +} diff --git a/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp b/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp index 9588b698b..9cf5aaf00 100644 --- a/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp +++ b/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp @@ -692,14 +692,7 @@ struct Win32ExceptionPassCode llvm::Type *Args[] = {PointerType::get(Ctx, 0), PointerType::get(Ctx, 0)}; auto *FTy = llvm::FunctionType::get(Type::getVoidTy(Ctx), Args, /*isVarArg=*/false); auto Throw = Function::Create(FTy, llvm::GlobalValue::LinkageTypes::ExternalLinkage, "_CxxThrowException", module); - /* - // _CxxThrowException is stdcall on 32-bit x86 platforms. - if (CGM.getTarget().getTriple().getArch() == llvm::Triple::x86) - { - if (auto *Fn = dyn_cast(Throw.getCallee())) - Fn->setCallingConv(llvm::CallingConv::X86_StdCall); - } - */ + // On 32-bit x86 this is __stdcall; CxxThrowCallingConvPass sets that on it and its call sites. return Throw; } diff --git a/tslang/test/check-x86-eh.sh b/tslang/test/check-x86-eh.sh index e51103484..5800d350c 100755 --- a/tslang/test/check-x86-eh.sh +++ b/tslang/test/check-x86-eh.sh @@ -63,4 +63,44 @@ else bad "x86 obj: 00try_catch compiles"; cat "$x86obj.err" fi +# --- Task 2: _CxxThrowException is __stdcall on x86 --------------------------------------- +# The decorated symbol is what the linker sees. +for triple in i686-pc-windows-msvc x86_64-pc-windows-msvc; do + obj="$TMP/throw.$triple.obj" + if "$TSLANG" --emit=obj --opt -mm=none --no-default-lib -mtriple="$triple" "$TESTS/00try_catch.ts" -o "$obj" 2>"$obj.err"; then + syms="$("$READOBJ" --symbols "$obj" | grep -o 'Name: .*CxxThrowException.*' | sort -u)" + case "$triple" in + i686*) [ "$syms" = "Name: __CxxThrowException@8" ] && ok "x86 obj: __CxxThrowException@8" \ + || bad "x86 obj: __CxxThrowException@8 (got: $syms)";; + x86_64*) [ "$syms" = "Name: _CxxThrowException" ] && ok "x64 obj: _CxxThrowException unchanged" \ + || bad "x64 obj: _CxxThrowException unchanged (got: $syms)";; + esac + else + bad "$triple obj: 00try_catch compiles"; cat "$obj.err" + fi +done + +# Every call site must carry the convention too: one left at the C convention is UB, and +# InstCombine turns it into `unreachable`. 00try_catch throws from main (a call), 00try_catch_rethrow +# throws inside a try (an invoke), and 00try_finally gets the catch-all rethrow that +# Win32ExceptionPass synthesizes for a finally. Checked with and without --opt. +for name in 00try_catch 00try_catch_rethrow 00try_finally; do + for opt in --opt --opt=false; do + ll="$TMP/cc.$name$opt.ll" + if "$TSLANG" --emit=llvm $opt -mm=none --no-default-lib -mtriple=i686-pc-windows-msvc "$TESTS/$name.ts" -o "$ll" 2>"$ll.err"; then + decl="$(grep -c '^declare x86_stdcallcc void @_CxxThrowException(' "$ll")" + sites="$(grep -Ec '(call|invoke) (x86_stdcallcc )?void @_CxxThrowException\(' "$ll")" + cdecl="$(grep -E '(call|invoke) void @_CxxThrowException\(' "$ll")" + if [ "$decl" = 1 ] && [ "$sites" -gt 0 ] && [ -z "$cdecl" ]; then + ok "x86 IR $opt: $name declares and calls _CxxThrowException as x86_stdcallcc ($sites sites)" + else + bad "x86 IR $opt: $name declares and calls _CxxThrowException as x86_stdcallcc (decl=$decl sites=$sites)" + [ -n "$cdecl" ] && echo "$cdecl" + fi + else + bad "x86 IR $opt: $name compiles"; cat "$ll.err" + fi + done +done + exit "$fail" diff --git a/tslang/test/mask-ir.sh b/tslang/test/mask-ir.sh index c89f42e6c..2f03e7597 100755 --- a/tslang/test/mask-ir.sh +++ b/tslang/test/mask-ir.sh @@ -22,6 +22,7 @@ # real. Known noisy files in tslang/test/tester/tests: typeGuardOfFormTypeOfBoolean, # typeGuardFunction, 00funcs, 00union_bin_ops2, conditionalTypes2 (hash-ordered union choices), # 00array8_tuple_spread (an equal-size tie in findMaxSizeType), +# 00typeof_union_narrowing (differs between two runs of the same base-commit compiler), # and - before FH was masked - 00global_const_object_method and 00object_func. sed -E 's/([A-Za-z_$]\.?)[0-9]{6,}/\1N/g; s/(td|tsrel|tsret)_[0-9]+/\1_N/g; s/FH[0-9]+/FHN/g; s/\.[0-9]{6,}\.\.vtbl/.N..vtbl/g; s/(^|[^A-Za-z0-9_.$])-?[0-9]{12,}/\1N/g' "$1" \ | sed -E '/c"[^"]*(FHN|(td|tsrel|tsret)_N)/ s/\[[0-9]+ x i8\]/[K x i8]/g' diff --git a/tslang/tslang/transform.cpp b/tslang/tslang/transform.cpp index a2e009347..0c39a4a5d 100644 --- a/tslang/tslang/transform.cpp +++ b/tslang/tslang/transform.cpp @@ -13,6 +13,7 @@ #ifdef ENABLE_EXCEPTIONS #include "TypeScript/Pass/LandingPadFixPass.h" #include "TypeScript/Pass/Win32ExceptionPass.h" +#include "TypeScript/Pass/CxxThrowCallingConvPass.h" #endif #include "TypeScript/Pass/ExportFixPass.h" #ifdef ENABLE_DEBUGINFO_PATCH_INFO @@ -339,6 +340,13 @@ std::function makeCustomPassesWithOptimizingTransfo mpm.addPass(llvm::createModuleToFunctionPassAdaptor(ts::Win32ExceptionPass())); } + // After Win32ExceptionPass, which adds throw calls and turns calls into invokes, and + // before the optimization pipeline, which treats a calling-convention mismatch as UB. + if (compileOptions.targetInfo.stdcallDecoratesCxxThrow) + { + mpm.addPass(ts::CxxThrowCallingConvPass()); + } + llvm::Triple triple(m->getTargetTriple()); mpm.addPass(ts::ExportFixPass(triple.isWindowsMSVCEnvironment())); From 11e6b52924cab6c20ba5354a83555b551718d166 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 21 Sep 2026 16:49:31 +0100 Subject: [PATCH 4/5] Run exception programs as 32-bit exes under every model 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 --- tslang/test/check-x86-eh.sh | 68 +++++++++++++++++++++++++++++++++++++ tslang/test/x86/eh_order.ts | 53 +++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 tslang/test/x86/eh_order.ts diff --git a/tslang/test/check-x86-eh.sh b/tslang/test/check-x86-eh.sh index 5800d350c..97c569605 100755 --- a/tslang/test/check-x86-eh.sh +++ b/tslang/test/check-x86-eh.sh @@ -103,4 +103,72 @@ for name in 00try_catch 00try_catch_rethrow 00try_finally; do done done +# --- Task 3: 32-bit programs throw and catch correctly ------------------------------------ +# Each program is linked as a 32-bit exe under every memory model and run with a 20 s limit, so a +# hang is a FAIL. The x86 libraries sit in the x86 subdirectory of the directories x64 uses. +GC_DIR="$REPO/3rdParty/gc/x64/release/lib" +RT_DIR="$REPO/__build/tslang-runtime/release" +need "$GC_DIR/x86/gc.lib" +need "$RT_DIR/x86/TypeScriptAsyncRuntime.lib" +EH_ORDER="$REPO/tslang/test/x86/eh_order.ts" +need "$EH_ORDER" + +# run_x86