Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
415 changes: 415 additions & 0 deletions docs/superpowers/plans/2026-09-22-32-bit-phase-3-x86-eh.md

Large diffs are not rendered by default.

62 changes: 51 additions & 11 deletions docs/superpowers/specs/2026-09-20-32-bit-compilation-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -202,24 +202,46 @@ 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
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

Expand Down Expand Up @@ -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 <any>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
Expand Down
73 changes: 35 additions & 38 deletions tslang/include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<LLVM::GlobalOp>(name))
{
Expand All @@ -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<LLVM::PtrToIntOp>(loc, th.getI32Type(), ptrValue);
}

auto intPtrTy = rewriter.getIntegerType(compileOptions.sizeBits());
auto ptrInt = rewriter.create<LLVM::PtrToIntOp>(loc, intPtrTy, ptrValue);
auto imageBasePtr =
rewriter.create<LLVM::AddressOfOp>(loc, th.getPtrType(), FlatSymbolRefAttr::get(rewriter.getContext(), imageBaseRef));
auto imageBaseInt = rewriter.create<LLVM::PtrToIntOp>(loc, intPtrTy, imageBasePtr);
auto rva = rewriter.create<LLVM::SubOp>(loc, intPtrTy, ptrInt, imageBaseInt);
return rewriter.create<LLVM::TruncOp>(loc, th.getI32Type(), rva);
}

LogicalResult catchableTypes(mlir::Location loc)
{
for (auto type : types)
Expand Down Expand Up @@ -328,19 +359,7 @@ class LLVMRTTIHelperVCWin32
auto rttiTypeDescriptor2PtrValue =
rewriter.create<LLVM::AddressOfOp>(loc, LLVM::LLVMPointerType::get(rewriter.getContext()),
FlatSymbolRefAttr::get(rewriter.getContext(), typeInfoRefName));
auto rttiTypeDescriptor2IntValue = rewriter.create<LLVM::PtrToIntOp>(loc, th.getI64Type(), rttiTypeDescriptor2PtrValue);

auto imageBasePtrValue =
rewriter.create<LLVM::AddressOfOp>(loc, th.getPtrType(), FlatSymbolRefAttr::get(rewriter.getContext(), imageBaseRef));
auto imageBaseIntValue = rewriter.create<LLVM::PtrToIntOp>(loc, th.getI64Type(), imageBasePtrValue);

// sub
auto subResValue = rewriter.create<LLVM::SubOp>(loc, th.getI64Type(), rttiTypeDescriptor2IntValue, imageBaseIntValue);

// trunc
auto subRes32Value = rewriter.create<LLVM::TruncOp>(loc, th.getI32Type(), subResValue);

auto itemValue2 = subRes32Value;
auto itemValue2 = ehReference(loc, rttiTypeDescriptor2PtrValue);
ch.setStructValue(loc, structVal, itemValue2, 1);

auto itemValue3 = rewriter.create<LLVM::ConstantOp>(loc, th.getI32Type(), rewriter.getI32IntegerAttr(0));
Expand Down Expand Up @@ -371,19 +390,7 @@ class LLVMRTTIHelperVCWin32
{
auto rttiCatchableTypePtrValue = rewriter.create<LLVM::AddressOfOp>(
loc, LLVM::LLVMPointerType::get(rewriter.getContext()), FlatSymbolRefAttr::get(rewriter.getContext(), catchableTypeRefName));
auto rttiCatchableTypeIntValue = rewriter.create<LLVM::PtrToIntOp>(loc, th.getI64Type(), rttiCatchableTypePtrValue);

auto imageBasePtrValue =
rewriter.create<LLVM::AddressOfOp>(loc, th.getPtrType(), FlatSymbolRefAttr::get(rewriter.getContext(), imageBaseRef));
auto imageBaseIntValue = rewriter.create<LLVM::PtrToIntOp>(loc, th.getI64Type(), imageBasePtrValue);

// sub
auto subResValue = rewriter.create<LLVM::SubOp>(loc, th.getI64Type(), rttiCatchableTypeIntValue, imageBaseIntValue);

// trunc
auto subRes32Value = rewriter.create<LLVM::TruncOp>(loc, th.getI32Type(), subResValue);

return subRes32Value;
return ehReference(loc, rttiCatchableTypePtrValue);
}

LogicalResult catchableArrayType(mlir::Location loc)
Expand Down Expand Up @@ -459,18 +466,8 @@ class LLVMRTTIHelperVCWin32
// value 3
auto rttiCatchableArrayTypePtrValue = rewriter.create<LLVM::AddressOfOp>(
loc, LLVM::LLVMPointerType::get(rewriter.getContext()), FlatSymbolRefAttr::get(rewriter.getContext(), catchableTypeInfoArrayRef));
auto rttiCatchableArrayTypeIntValue = rewriter.create<LLVM::PtrToIntOp>(loc, th.getI64Type(), rttiCatchableArrayTypePtrValue);

auto imageBasePtrValue =
rewriter.create<LLVM::AddressOfOp>(loc, th.getPtrType(), FlatSymbolRefAttr::get(rewriter.getContext(), imageBaseRef));
auto imageBaseIntValue = rewriter.create<LLVM::PtrToIntOp>(loc, th.getI64Type(), imageBasePtrValue);

// sub
auto subResValue = rewriter.create<LLVM::SubOp>(loc, th.getI64Type(), rttiCatchableArrayTypeIntValue, imageBaseIntValue);

// trunc
auto subRes32Value = rewriter.create<LLVM::TruncOp>(loc, th.getI32Type(), subResValue);
ch.setStructValue(loc, structValue, subRes32Value, 3);
auto rttiCatchableArrayTypeRefValue = ehReference(loc, rttiCatchableArrayTypePtrValue);
ch.setStructValue(loc, structValue, rttiCatchableArrayTypeRefValue, 3);

rewriter.create<LLVM::ReturnOp>(loc, ValueRange{structValue});

Expand Down
80 changes: 36 additions & 44 deletions tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<mlir_ts::GlobalOp>(name))
{
Expand All @@ -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<mlir_ts::CastOp>(loc, mth.getI32Type(), ptrValue);
}

auto intPtrTy = mlir::IntegerType::get(rewriter.getContext(), compileOptions.sizeBits());
auto ptrInt = rewriter.create<mlir_ts::CastOp>(loc, intPtrTy, ptrValue);
auto imageBasePtr = rewriter.create<mlir_ts::ConstantOp>(loc, mth.getOpaqueType(),
mlir::FlatSymbolRefAttr::get(rewriter.getContext(), windows::imageBaseRef));
auto imageBaseInt = rewriter.create<mlir_ts::CastOp>(loc, intPtrTy, imageBasePtr);
auto rva = rewriter.create<mlir_ts::ArithmeticBinaryOp>(
loc, intPtrTy, rewriter.getI32IntegerAttr(static_cast<int32_t>(SyntaxKind::MinusToken)), ptrInt, imageBaseInt);
return rewriter.create<mlir_ts::CastOp>(loc, mth.getI32Type(), rva);
}

mlir::LogicalResult catchableTypes(mlir::Location loc)
{
for (auto type : types)
Expand Down Expand Up @@ -445,21 +477,7 @@ class MLIRRTTIHelperVCWin32
auto rttiTypeDescriptor2PtrValue =
rewriter.create<mlir_ts::ConstantOp>(loc, getRttiTypeDescriptor2PtrTy(StringRef(typeName).size()),
mlir::FlatSymbolRefAttr::get(rewriter.getContext(), typeInfoRefName));
auto rttiTypeDescriptor2IntValue = rewriter.create<mlir_ts::CastOp>(loc, mth.getI64Type(), rttiTypeDescriptor2PtrValue);

auto imageBasePtrValue = rewriter.create<mlir_ts::ConstantOp>(
loc, mth.getOpaqueType(), mlir::FlatSymbolRefAttr::get(rewriter.getContext(), windows::imageBaseRef));
auto imageBaseIntValue = rewriter.create<mlir_ts::CastOp>(loc, mth.getI64Type(), imageBasePtrValue);

// sub
auto subResValue = rewriter.create<mlir_ts::ArithmeticBinaryOp>(
loc, mth.getI64Type(), rewriter.getI32IntegerAttr(static_cast<int32_t>(SyntaxKind::MinusToken)),
rttiTypeDescriptor2IntValue, imageBaseIntValue);

// trunc
auto subRes32Value = rewriter.create<mlir_ts::CastOp>(loc, mth.getI32Type(), subResValue);

auto itemValue2 = subRes32Value;
auto itemValue2 = ehReference(loc, rttiTypeDescriptor2PtrValue);
setStructValue(loc, structVal, itemValue2, 1);

auto itemValue3 = rewriter.create<mlir_ts::ConstantOp>(loc, mth.getI32Type(), rewriter.getI32IntegerAttr(0));
Expand Down Expand Up @@ -490,21 +508,7 @@ class MLIRRTTIHelperVCWin32
{
auto rttiCatchableTypePtrValue = rewriter.create<mlir_ts::ConstantOp>(
loc, getCatchableTypePtrTy(), mlir::FlatSymbolRefAttr::get(rewriter.getContext(), catchableTypeRefName));
auto rttiCatchableTypeIntValue = rewriter.create<mlir_ts::CastOp>(loc, mth.getI64Type(), rttiCatchableTypePtrValue);

auto imageBasePtrValue = rewriter.create<mlir_ts::ConstantOp>(loc, mth.getOpaqueType(),
mlir::FlatSymbolRefAttr::get(rewriter.getContext(), windows::imageBaseRef));
auto imageBaseIntValue = rewriter.create<mlir_ts::CastOp>(loc, mth.getI64Type(), imageBasePtrValue);

// sub
auto subResValue = rewriter.create<mlir_ts::ArithmeticBinaryOp>(
loc, mth.getI64Type(), rewriter.getI32IntegerAttr(static_cast<int32_t>(SyntaxKind::MinusToken)), rttiCatchableTypeIntValue,
imageBaseIntValue);

// trunc
auto subRes32Value = rewriter.create<mlir_ts::CastOp>(loc, mth.getI32Type(), subResValue);

return subRes32Value;
return ehReference(loc, rttiCatchableTypePtrValue);
}

mlir::LogicalResult catchableArrayType(mlir::Location loc)
Expand Down Expand Up @@ -602,20 +606,8 @@ class MLIRRTTIHelperVCWin32
// value 3
auto rttiCatchableArrayTypePtrValue = rewriter.create<mlir_ts::ConstantOp>(
loc, getCatchableArrayTypePtrTy(arraySize), mlir::FlatSymbolRefAttr::get(rewriter.getContext(), catchableTypeInfoArrayRef));
auto rttiCatchableArrayTypeIntValue = rewriter.create<mlir_ts::CastOp>(loc, mth.getI64Type(), rttiCatchableArrayTypePtrValue);

auto imageBasePtrValue = rewriter.create<mlir_ts::ConstantOp>(loc, mth.getOpaqueType(),
mlir::FlatSymbolRefAttr::get(rewriter.getContext(), windows::imageBaseRef));
auto imageBaseIntValue = rewriter.create<mlir_ts::CastOp>(loc, mth.getI64Type(), imageBasePtrValue);

// sub
auto subResValue = rewriter.create<mlir_ts::ArithmeticBinaryOp>(
loc, mth.getI64Type(), rewriter.getI32IntegerAttr(static_cast<int32_t>(SyntaxKind::MinusToken)), rttiCatchableArrayTypeIntValue,
imageBaseIntValue);

// trunc
auto subRes32Value = rewriter.create<mlir_ts::CastOp>(loc, mth.getI32Type(), subResValue);
setStructValue(loc, structValue, subRes32Value, 3);
auto rttiCatchableArrayTypeRefValue = ehReference(loc, rttiCatchableArrayTypePtrValue);
setStructValue(loc, structValue, rttiCatchableArrayTypeRefValue, 3);

rewriter.create<mlir_ts::GlobalResultOp>(loc, mlir::ValueRange{structValue});

Expand Down
25 changes: 25 additions & 0 deletions tslang/include/TypeScript/Pass/CxxThrowCallingConvPass.h
Original file line number Diff line number Diff line change
@@ -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<CxxThrowCallingConvPass>
{
public:
llvm::PreservedAnalyses run(llvm::Module &M, llvm::ModuleAnalysisManager &AM);

static bool isRequired()
{
return true;
}
};
}

#endif // CXXTHROWCALLINGCONVPASS__H
4 changes: 3 additions & 1 deletion tslang/include/TypeScript/TargetInfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions tslang/lib/TypeScriptExceptionPass/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ set(LLVM_LINK_COMPONENTS
add_llvm_library(TypeScriptExceptionPass
Win32ExceptionPass.cpp
LandingPadFixPass.cpp
CxxThrowCallingConvPass.cpp

ADDITIONAL_HEADER_DIRS

Expand Down
38 changes: 38 additions & 0 deletions tslang/lib/TypeScriptExceptionPass/CxxThrowCallingConvPass.cpp
Original file line number Diff line number Diff line change
@@ -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<CallBase>(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();
}
}
Loading
Loading