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. diff --git a/docs/superpowers/specs/2026-09-20-32-bit-compilation-design.md b/docs/superpowers/specs/2026-09-20-32-bit-compilation-design.md index 4072299e6..914034d73 100644 --- a/docs/superpowers/specs/2026-09-20-32-bit-compilation-design.md +++ b/docs/superpowers/specs/2026-09-20-32-bit-compilation-design.md @@ -3,7 +3,7 @@ Design for compiling TypeScript to 32-bit targets, with `i686-pc-windows-msvc` as the proof target and width-correctness that generalizes to any 32-bit triple. -Status: approved design. Phase 1 implemented; phases 2-4 not. +Status: approved design. Phases 1-3 implemented; phase 4 not. ## Why now @@ -202,15 +202,30 @@ real layout under `-mm=gc`, `rc` and `none`. Two changes, both narrow because of the finding above. -1. `_CxxThrowException` is `__stdcall` on x86. The block already present but - commented out at `lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp:696` - sets `CallingConv::X86_StdCall`, which makes LLVM emit - `__CxxThrowException@8`. Gated on `stdcallDecoratesCxxThrow`. -2. The image-base subtraction in `LLVMRTTIHelperVCWin32.h` (three sites) and - the corresponding logic in `MLIRRTTIHelperVCWin32.h` is gated on - `usesImageBaseRelativeEH`. On x86 the field takes the `ptrtoint` result - directly. The `ptrtoint` and `sub` widths come from `TargetInfo` rather than - being hardcoded `i64`. +1. `_CxxThrowException` is `__stdcall` on x86, so its symbol is + `__CxxThrowException@8`. **As built:** a module pass, + `CxxThrowCallingConvPass` (`lib/TypeScriptExceptionPass`), sets + `CallingConv::X86_StdCall` on the declaration and on every call and invoke + of it. `transform.cpp` adds it after `Win32ExceptionPass` and before the + optimization pipeline, gated on `stdcallDecoratesCxxThrow`. It is one pass + rather than a fix at each declaration because three places create calls: + the MLIR lowering (`ThrowLogic.h`), the rethrow `Win32ExceptionPass` + synthesizes, and its `ToInvoke`. A call whose convention differs from its + callee's is undefined behaviour, which InstCombine turns into `unreachable`. + Any use other than as a direct callee is a hard error. The commented-out + block that used to sit in `Win32ExceptionPass::getThrowFn` is gone. +2. The cross-references in `ThrowInfo`, `CatchableType` and + `CatchableTypeArray` are built by one `ehReference` function in each Win32 + RTTI helper. When `usesImageBaseRelativeEH` holds it emits + `trunc(ptrtoint - ptrtoint __ImageBase)` at `sizeBits()` width, which is + exactly the old x64 output; otherwise it emits `ptrtoint` to i32, and + `__ImageBase` is not declared. 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, and were changed only so neither helper can compute RVAs + on x86. `usesImageBaseRelativeEH` is decided by pointer width, as clang's + `MicrosoftCXXABI::isImageRelative()` does, so 32-bit ARM Windows is absolute + as well. **Testing.** Tests assert that control flow reaches the catch, and on unwinding order. They do not assert on the catch variable's contents: reading a @@ -218,8 +233,15 @@ catch variable's value is a known-unreliable area independent of this work, and a test written on it would fail for reasons that have nothing to do with 32-bit support. +`tslang/test/check-x86-eh.sh` (hand-run, needs the phase 2 x86 libraries, +release only) checks the IR and object form of both changes, then links and +runs `tslang/test/x86/eh_order.ts` (an exact unwinding trace) and eleven +exception corpus files as 32-bit exes under gc, rc and none. Debug x86 builds, +the `using`/dispose corpus files and a throw crossing an x86 `--emit=dll` +boundary into a C++ `catch (int)` were verified by hand. + **Gate.** throw/catch, nested catch, try/finally and rethrow behave correctly -as 32-bit binaries. +as 32-bit binaries. Met: no compiler change beyond the two above was needed. ## Phase 4 — Default library and the suite @@ -272,6 +294,24 @@ confusion: (`AsyncToLLVM.cpp:1026`) builds `LowerToLLVMOptions(ctx)` with LLVM's default layout and a 64-bit index, so its types disagree with the i686 type converter's. +- **Async programs crash at i686 (phase 4).** Found in phase 3: every async + program, including `00async_await` and one with no `try`, exits 139 as a + 32-bit exe. Not EH-related. +- **x86 EH type names use the 64-bit mangling (phase 4, interop only).** The + hardcoded names in `LLVMRTTIHelperVCWin32Const.h` (`??_R0PEAD@8`, + `_CT??_R0PEAD@88`) carry the `__ptr64` qualifier and 64-bit sizes. tslang's + throw and catch share them, so tslang programs are unaffected, and a C++ + `catch (int)` across a DLL boundary works; catching a *pointer* type thrown + by MSVC-compiled x86 code would not match. +- **`00catch_value*` at i686.** Both exit 0 under gc, rc and none, release and + debug, as of phase 3. +- **Untyped `catch (e)` does not catch a thrown number (not 32-bit; x64 too).** + The `ThrowInfo` follows the thrown value's static type + (`MLIRGenStatements.cpp` ~1049-1067) while an untyped catch variable gets a + `void*` handler (`??_R0PEAX@8`), so `throw 1` / `throw "s"` escape it and the + process exits 127 with its earlier output unflushed. `throw x` and class + objects are caught. Pre-existing; `eh_order.ts` uses `catch (e: TypeOf<1>)`. + ## Out of scope - `-m32` shorthand. `-mtriple=i686-pc-windows-msvc` already works and is the 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/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/include/TypeScript/TargetInfo.h b/tslang/include/TypeScript/TargetInfo.h index 62a96c1f6..12121550c 100644 --- a/tslang/include/TypeScript/TargetInfo.h +++ b/tslang/include/TypeScript/TargetInfo.h @@ -44,7 +44,9 @@ struct TargetInfo const bool msvc = target.isKnownWindowsMSVCEnvironment(); const bool x86_32 = target.getArch() == llvm::Triple::x86; - info.usesImageBaseRelativeEH = msvc && !x86_32; + // By pointer width, as clang's MicrosoftCXXABI::isImageRelative() decides it: 32-bit ARM + // Windows stores absolute pointers too, not just x86. + info.usesImageBaseRelativeEH = msvc && info.pointerBits == 64; info.stdcallDecoratesCxxThrow = msvc && x86_32; info.supportsInProcessJit = target.getArch() == host.getArch() && target.getOS() == host.getOS(); 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 new file mode 100755 index 000000000..b5ffdbac8 --- /dev/null +++ b/tslang/test/check-x86-eh.sh @@ -0,0 +1,176 @@ +#!/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" +# Without timeout every run would report exit 127, which looks like a real failure mode. +command -v timeout >/dev/null || { echo "FAIL missing prerequisite: timeout"; exit 1; } + +# 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 + +# --- 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 + +# --- 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