Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
10c483b
Add design for 32-bit compilation
ASDAlexander77 Sep 20, 2026
78a1e23
Add Phase 1 implementation plan for 32-bit support
ASDAlexander77 Sep 20, 2026
d5e2b7d
Fix three defects the pre-flight scan found in the Phase 1 plan
ASDAlexander77 Sep 20, 2026
a7c2cf3
Derive target width and policy from the triple
ASDAlexander77 Sep 20, 2026
ad8060e
Correct the unittest binary path in the Phase 1 plan
ASDAlexander77 Sep 20, 2026
fb540d9
Diff x64 IR against a stored baseline, not a stashed rebuild
ASDAlexander77 Sep 20, 2026
c1384ba
Take pointer width from the triple, not from an arch list
ASDAlexander77 Sep 20, 2026
1572982
Correct why the arch list was wrong
ASDAlexander77 Sep 20, 2026
9f931ea
Give TypeHelper target-width accessors
ASDAlexander77 Sep 20, 2026
866d15b
Fail identifiably when getSizeType() has no target in Release
ASDAlexander77 Sep 20, 2026
9ba4cf3
Set the data layout on the emitted module
ASDAlexander77 Sep 20, 2026
97adcbf
Make unresolvable-triple datalayout lookup fatal, not silent
ASDAlexander77 Sep 20, 2026
32754e4
Pin the audit to a verified list of 18 call sites
ASDAlexander77 Sep 20, 2026
6e1073c
Classify every i64 in lowering as fixed-width or target-width
ASDAlexander77 Sep 20, 2026
1851250
Give ctest its -C Release, without which nothing runs
ASDAlexander77 Sep 20, 2026
9be0623
Make the pointer->float cast unsigned and widen the GC descriptor op …
ASDAlexander77 Sep 20, 2026
52d278d
Refuse --emit=jit for a target this process cannot run
ASDAlexander77 Sep 21, 2026
d926e5e
Compare the optional-member sentinel at target width on both sides
ASDAlexander77 Sep 21, 2026
e9c1312
Make the old-arch-list test cover what the list actually got wrong
ASDAlexander77 Sep 21, 2026
93dca01
Bring the 32-bit spec and Phase 1 plan in line with what was built
ASDAlexander77 Sep 21, 2026
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
902 changes: 902 additions & 0 deletions docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md

Large diffs are not rendered by default.

254 changes: 254 additions & 0 deletions docs/superpowers/specs/2026-09-20-32-bit-compilation-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
# 32-bit compilation

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.

## Why now

Until #333, every compiled program linked `LLVMSupport.lib` and
`LLVMDemangle.lib`. Those come from the prebuilt LLVM in `3rdParty/llvm/x64`,
which exists only as x64 — so a 32-bit link was blocked by a library we could
not rebuild cheaply. #333 removed that dependency. Every remaining x64 blocker
is a library this repository builds from source.

## Measured baseline

Probed with the Sep-18 build (`__build/tslang/windows-msbuild-2026-release`),
target `i686-pc-windows-msvc`. These are measurements, not estimates.

| Case | Result |
| --- | --- |
| `--emit=obj -mm=none --no-default-lib` | Works. `hello.obj` machine type `0x014c` (`IMAGE_FILE_MACHINE_I386`). |
| `--emit=exe -mm=none --no-default-lib` | Works. `hello.exe` PE machine `0x014c`; runs and prints. |
| `LLVMSupport.lib` in the link | Gone, as of #333. (The older `__build/tslang/msbuild/x64/release` build still drags it in.) |
| Exceptions | `unresolved external symbol __CxxThrowException`; linker hints `__CxxThrowException@8`. |
| `-mm=gc` | `gc.lib : warning LNK4272: library machine type 'x64' conflicts with target machine type 'x86'`, then unresolved `_GC_init`, `_GC_enable_threads`. |
| `TypeScriptAsyncRuntime.lib` | Same x64/x86 machine-type conflict. |

clang's driver locates the x86 CRT and Windows SDK import libraries on its own.
No toolchain-discovery work is needed.

### Two findings that reduced scope

Both were assumptions that probing disproved. They are recorded because the
obvious reading of the emitted IR suggests the opposite.

**The async runtime ABI is not a width bug.** Emitted IR declares
`mlirAsyncRuntimeAddRef(ptr, i64)`, `mlirAsyncRuntimeCreateValue(i64)` and
similar regardless of triple, which looks like a 64-bit-shaped ABI. It is not:
`lib/AsyncRuntimeCommon.inc` declares these parameters as `int64_t`, not
`size_t`. `i64` is correct on both arches. Nothing to change.

**The Win32 EH struct layouts are already correct for x86.** `ThrowInfo` is
emitted as `{i32 x 4}` and `CatchableType` as `{i32 x 7}`
(`include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h`). On x64 those i32
fields hold image-base-relative RVAs. On 32-bit MSVC the corresponding fields
hold absolute pointers — which are also 4 bytes. The layouts coincide. What
differs is only the *value* stored: x86 must not subtract the image base.

## Architecture

One new type, `TargetInfo`, constructed once in `prepareOptions()` from
`llvm::Triple` and carried on `CompileOptions`. The triple itself stays on
`CompileOptions` only as the existing `moduleTargetTriple` string. `TargetInfo`
holds two kinds of fact:

- **Widths** — `pointerBits`, used for pointers, sizes and indices alike.
- **Policy** — `usesImageBaseRelativeEH`, `stdcallDecoratesCxxThrow`,
`supportsInProcessJit`.

`TypeHelper` gains `getSizeType()` and `getPointerIntType()` backed by it.
`TypeHelper` took only an `MLIRContext*`; it now takes an optional
`const CompileOptions *`, defaulting to null. Most of its ~150 construction
sites use only width-independent helpers (`getI8Type`, `getPtrType`,
`getVoidType`) and are unchanged; only the sites that need a target width pass
the options. `getSizeType()` called on a `TypeHelper` built without them fails
via `report_fatal_error` in every build configuration, rather than guessing a
width.

### Naming policy predicates

Policy predicates are named for what they decide, not for the architecture that
currently needs them: `usesImageBaseRelativeEH`, never `isX64`. This is what
keeps phase 3 from smearing `arch == x86_64` across three RTTI helpers, and it
is what makes the answer reviewable in one place next to the comment explaining
it.

### Replacing the arch list

`tslang/opts.cpp` currently derives `sizeBits` from a hand-maintained list of
64-bit architectures. The list is wrong: it marks `aarch64_32` as 64-bit, but
ARM64_32 is an ILP32 target whose pointers are 32 bits. It is replaced by
`llvm::Triple::getArchPointerBitWidth()`, which is what makes "any 32-bit
triple" true rather than "the 32-bit triples someone remembered".

### Arch in the library layout

`x86` and `x64`, matching the existing `3rdParty/gc/x64`. Arch is the outermost
dimension under each component, so a whole arch tree ships or is deleted as one
unit and existing x64 paths keep their shape:

- `3rdParty/gc/x86/release/`, `3rdParty/gcdll/x86/release/`
- `defaultlib/{lib,dll}/x86/{debug,release}/{gc,rc,none}/`

Strict, no fallback — the rule the default-lib layout already follows. A
missing arch tree is an error that names the arch and the script that builds
it. It never falls back to another arch: that link succeeds and then corrupts
memory, which is far harder to diagnose than a directory that is not there.

## Phase 1 — Target width correctness

`TargetInfo` lands. `TypeHelper` takes an optional `CompileOptions`. The arch
list is replaced.

Eighteen `getI64Type()` call sites are in the two Win32 RTTI helpers
(`include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h` and
`include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h`, nine each); those are
phase 3's, since phase 3 rewrites that arithmetic anyway. Outside them there are
18 call sites across `lib/` and `include/`. A grep for `getI64Type()` returns
21 lines there, but three are not call sites: the two definitions of
`getI64Type` (`TypeHelper` and `MLIRTypeHelper`) and a comment mentioning it.
Phase 1 audits the 18, and each is classified in a comment as one of:

- **genuinely 64-bit** — the language's `number` is f64; runtime ABI
parameters declared `int64_t`; anything whose width is fixed by a contract
outside the target.
- **target-width** — pointer arithmetic, object sizes, GEP indices.

Result: 13 are target-width and 5 genuinely 64-bit. The audit's value is not
the count of changes but that the classification becomes explicit and survives
future edits. It also found two latent 32-bit bugs, both fixed:

- The `-1` sentinel for an optional interface member the object does not
provide was produced at `i64` in MLIRGen while the lowering that tests for it
compared at a different width. Producer and consumer now both use the target
width.
- A pointer converted to a float went through `sitofp`. With the intermediate
integer now as wide as the pointer, an address at or above `0x80000000` on a
32-bit target would read back as a negative float. It is now `uitofp`.

`target datalayout` is set at IR emission rather than only later in
`tslang/obj.cpp`, so `--emit=llvm` output is self-describing.

**Gate.** `--emit=llvm` for `i686-pc-windows-msvc`, `wasm32-unknown-unknown`
and `x86_64-pc-windows-msvc` emits the correct datalayout. Verifiable with no
native builds.

The gate originally also required that no 32-bit target emit 64-bit-shaped size
or pointer arithmetic. Phase 1 does not meet that, and the requirement moves to
phase 2. The lowering's `LLVMTypeConverter` is built with LLVM's default data
layout for every non-wasm target (`TypeScriptToLLVMLoweringPass::runOnOperation`
in `lib/TypeScript/LowerToLLVM.cpp`, ~7255-7268), so on i686 `getIntPtrType`,
struct layout, union sizing and alignment still use 8-byte pointers. Phase 1
fixes the widths MLIRGen and the lowering choose explicitly; it does not change
what the type converter derives.

## Phase 2 — 32-bit native build matrix

The first task, and a gate on any native 32-bit test, is to build the type
converter's data layout from the module's `llvm.data_layout` instead of LLVM's
default. This changes x64 output — the default layout aligns `i64` at 32 bits,
x64's real layout at 64 — so it needs its own x64 diff and review rather than
riding along with the build-matrix work. It also removes the hardcoded wasm32
layout string, which has `f128:64` and lacks `i128:128` and so matches neither
of LLVM's derivations.

- `3rdParty/gc/x86` and `3rdParty/gcdll/x86`: Boehm built for x86, static and
DLL, debug and release.
- `TypeScriptRuntime` and `TypeScriptAsyncRuntime` cross-built for x86.
- `tslang/exe.cpp` resolves `GC_LIB_PATH`, `GC_SHARED_LIB_PATH` and
`TSLANG_LIB_PATH` per target arch rather than per host.

The compiler itself stays x64 and cross-compiles. Building `tslang.exe` as a
32-bit binary is out of scope.

`checkGCLibPath` and `checkTslangLibPath` grow a COFF machine-type check, so an
arch mismatch is reported against the path that is wrong rather than surfacing
as LNK4272 plus a list of unresolved symbols.

**Gate.** `-mm=gc` and `-mm=rc` link and run a 32-bit hello world.

## Phase 3 — Win32 x86 exception handling

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`.

**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.

**Gate.** throw/catch, nested catch, try/finally and rethrow behave correctly
as 32-bit binaries.

## Phase 4 — Default library and the suite

`getDefaultLibSubDir` in `include/TypeScript/Defines.h` gains an arch segment,
giving `defaultlib/{lib,dll}/x86/{debug,release}/{gc,rc,none}/`. The
default-lib build scripts and the test runner gain an arch flag.

Async/await and generators are measured here rather than in an earlier phase.
They depend on phases 2 and 3 and cannot run before both are in place. Nothing
in the probing suggests they are separately broken; if they are, that is a
finding of this phase.

Already found: at i686, `00for_await` and `00for_await_yield` fail at
`--emit=llvm` with an `unrealized_conversion_cast` left in the IR; x64 passes.

**Gate.** The same suite that runs at 64 bits runs at 32 bits, green. Genuine
failures are triaged and recorded the way the Debug-suite failures are, not
silently excluded.

## Error handling

Three failures get explicit, actionable messages instead of downstream
confusion:

1. **Missing per-arch library tree** — names the arch and the script that
builds it. Not a fallback to another arch.
2. **Arch mismatch in a library that is present** — reported against the
offending path at resolution time, via the machine-type check, rather than
as a linker warning buried among unresolved symbols.
3. **`--emit=jit` with a non-host arch** — `tslang.exe` is an x64 process and
cannot execute i386 code in-process. Refused up front with a message saying
so. What the combination does today must be confirmed before this guard is
written; the guard is `supportsInProcessJit`.

## Open issues

- **ABI-by-environment triples.** For `x86_64-…-gnux32`, mips64 `gnuabin32` and
aarch64 `ilp32`, `getArchPointerBitWidth()` says 64 while the TargetMachine's
data layout says `p:32`. Nothing asserts the two agree. Proposed fix: a check
in `lib/TypeScript/MLIRGenModule.cpp` comparing
`createDataLayout().getPointerSizeInBits(0)` with `compileOptions.sizeBits()`.

## Out of scope

- `-m32` shorthand. `-mtriple=i686-pc-windows-msvc` already works and is the
mechanism; a second spelling earns nothing.
- Building `tslang.exe` itself as a 32-bit binary.
- 32-bit targets other than `i686-pc-windows-msvc` as *proof* targets. Phase 1
makes width-correctness general, and `wasm32` continues to be exercised, but
only `i686-pc-windows-msvc` gets a full native build matrix and suite run.

## Decisions taken

| Decision | Choice | Alternative rejected because |
| --- | --- | --- |
| Where target facts live | `TargetInfo` on `CompileOptions`, derived once from `llvm::Triple` | Querying the `Triple` at each site scatters policy across every width-dependent call site and relocates the arch-list rot rather than fixing it. Querying MLIR's `DataLayout` cannot express EH policy at all, and much width-dependent code runs in MLIRGen where no `DataLayout` is in scope. |
| Arch naming | `x86` / `x64` | Matches `3rdParty/gc/x64`, which already exists. |
| Arch position in paths | Outermost under each component | A whole arch tree ships or is deleted as one unit; x64 paths keep their shape. |
| Missing arch tree | Hard error | A fallback links and then corrupts memory. |
| Suite scope at 32 bits | The same suite as 64 bits | A declared subset hides exactly the failures this work exists to find. |
10 changes: 9 additions & 1 deletion tslang/include/TypeScript/DataStructs.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define TYPESCRIPT_DATASTRUCT_H_

#include "TypeScript/TypeScriptCompiler/Defines.h"
#include "TypeScript/TargetInfo.h"

#include <string>
#include <vector>
Expand All @@ -17,7 +18,14 @@ struct CompileOptions
bool generateDebugInfo;
bool lldbDebugInfo;
std::string moduleTargetTriple;
int sizeBits;
TargetInfo targetInfo;

// Pointer/size width in bits. Kept as an accessor rather than a field so it cannot drift
// away from targetInfo, which is the single place the target's width is decided.
int sizeBits() const
{
return static_cast<int>(targetInfo.pointerBits);
}
bool isWasm;
bool isWindows;
bool isExecutable;
Expand Down
21 changes: 16 additions & 5 deletions tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class CastLogicHelper

public:
CastLogicHelper(Operation *op, PatternRewriter &rewriter, TypeConverterHelper &tch, CompileOptions &compileOptions)
: op(op), rewriter(rewriter), tch(tch), th(rewriter), ch(op, rewriter, tch.typeConverter, compileOptions), clh(op, rewriter), loc(op->getLoc()),
: op(op), rewriter(rewriter), tch(tch), th(rewriter, compileOptions), ch(op, rewriter, tch.typeConverter, compileOptions), clh(op, rewriter), loc(op->getLoc()),
compileOptions(compileOptions), external(false)
{
}
Expand Down Expand Up @@ -712,8 +712,11 @@ class CastLogicHelper

if (isa<LLVM::LLVMPointerType>(inLLVMType) && isBool(resLLVMType))
{
auto intVal = rewriter.create<LLVM::PtrToIntOp>(loc, th.getI64Type(), in);
return rewriter.create<mlir::arith::CmpIOp>(loc, arith::CmpIPredicate::ne, intVal, clh.createI64ConstantOf(0));
// target-width: a ptrtoint of a real pointer - the integer it produces is as wide as the
// target's pointer, and the zero it is compared against has to be the same width.
auto ptrIntType = th.getPointerIntType();
auto intVal = rewriter.create<LLVM::PtrToIntOp>(loc, ptrIntType, in);
return rewriter.create<mlir::arith::CmpIOp>(loc, arith::CmpIPredicate::ne, intVal, clh.createIConstantOf(ptrIntType.getIntOrFloatBitWidth(), 0));
}

if (isa<LLVM::LLVMPointerType>(inLLVMType) && isInt(resLLVMType))
Expand All @@ -723,8 +726,16 @@ class CastLogicHelper

if (isa<LLVM::LLVMPointerType>(inLLVMType) && isFloat(resLLVMType))
{
auto intVal = rewriter.create<LLVM::PtrToIntOp>(loc, th.getI64Type(), in);
return rewriter.create<mlir::arith::SIToFPOp>(loc, resLLVMType, intVal);
// target-width: a ptrtoint of a real pointer; the integer handed to the fp conversion is
// as wide as the target's pointer.
// Unsigned, not signed: an address is an unsigned quantity, and once the integer is only
// as wide as the pointer its top bit is reachable - any address >= 0x80000000 in a
// /LARGEADDRESSAWARE i686 process or a large wasm32 memory would read back as a negative
// float under sitofp. (Same reasoning as the ZExtOp used for int widening two branches
// down.) While this produced a 64-bit integer from a 32-bit pointer the zero-extension
// hid the question; it no longer does.
auto intVal = rewriter.create<LLVM::PtrToIntOp>(loc, th.getPointerIntType(), in);
return rewriter.create<mlir::arith::UIToFPOp>(loc, resLLVMType, intVal);
}

if (isInt(inLLVMType) && isa<LLVM::LLVMPointerType>(resLLVMType))
Expand Down
6 changes: 6 additions & 0 deletions tslang/include/TypeScript/LowerToLLVM/CodeLogicHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ class CodeLogicHelper
return rewriter.create<LLVM::ConstantOp>(loc, rewriter.getIntegerType(32), rewriter.getIntegerAttr(rewriter.getI32Type(), value));
}

// 64-bit whatever the target: this helper builds a constant of the fixed 64-bit integer type,
// which is the whole of its contract - it has no target to ask. That is a statement about this
// function, not about its callers: some of them do pass byte counts through it (e.g.
// LowerToLLVM.cpp's MemoryAlloc(clh.createI64ConstantOf(2))), which is a width question of their
// own and not audited here. A caller that wants a pointer-wide constant should build it with
// createIConstantOf() at TypeHelper::getSizeType()'s width.
mlir::Value createI64ConstantOf(int64_t value)
{
return rewriter.create<LLVM::ConstantOp>(loc, rewriter.getIntegerType(64), rewriter.getIntegerAttr(rewriter.getI64Type(), value));
Expand Down
3 changes: 3 additions & 0 deletions tslang/include/TypeScript/LowerToLLVM/ConvertLogic.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ class ConvertLogic
{
auto i8PtrTy = th.getPtrType();

// 64-bit whatever the target: _i64toa's first parameter is `long long`
// (char *_i64toa(long long value, char *str, int radix)) - a fixed 64-bit integer on every
// target this compiler emits for, not a size_t.
auto _i64toaFuncOp = ch.getOrInsertFunction(
"_i64toa", th.getFunctionType(th.getPtrType(),
ArrayRef<mlir::Type>{rewriter.getI64Type(), th.getPtrType(), rewriter.getI32Type()}, true));
Expand Down
2 changes: 1 addition & 1 deletion tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ class LLVMCodeHelperBase
// moving - sits behind ENABLE_TYPED_GC and was retired in §9.2, before the header existed.
unsigned getHeapBlockHeaderSize()
{
return compileOptions.sizeBits / 8;
return compileOptions.sizeBits() / 8;
}

mlir::Value createHeapBlockHeaderSizeConstant(mlir::Location loc, mlir::Type llvmIndexType, bool negated = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class LLVMRTTIHelperVCWin32
// wide as the target's pointer, unlike `int` and `double`, which are fixed.
int pointerSize()
{
return compileOptions.sizeBits / 8;
return compileOptions.sizeBits() / 8;
}

std::string join(StringRef name, const char *prefix, const char *suffix)
Expand Down
6 changes: 4 additions & 2 deletions tslang/include/TypeScript/LowerToLLVM/ThrowLogic.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class ThrowLogic

public:
ThrowLogic(Operation *op, PatternRewriter &rewriter, TypeConverterHelper &tch, Location loc, CompileOptions &compileOptions)
: op(op), rewriter(rewriter), th(rewriter), ch(op, rewriter, tch.typeConverter, compileOptions), clh(op, rewriter), loc(loc),
: op(op), rewriter(rewriter), th(rewriter, compileOptions), ch(op, rewriter, tch.typeConverter, compileOptions), clh(op, rewriter), loc(loc),
typeConverter(tch.typeConverter), compileOptions(compileOptions), isWasm(compileOptions.isWasm), isWindows(compileOptions.isWindows)
{
}
Expand Down Expand Up @@ -146,7 +146,9 @@ class ThrowLogic

auto allocExceptFuncName = "__cxa_allocate_exception";

auto sizeType = compileOptions.sizeBits == 32 ? th.getI32Type() : th.getI64Type();
// target-width: __cxa_allocate_exception takes a `size_t` byte count. This site already asked
// the target by hand; getSizeType() is the same answer, spelled once.
auto sizeType = th.getSizeType();

auto cxxAllocException = ch.getOrInsertFunction(allocExceptFuncName, th.getFunctionType(i8PtrTy, {sizeType}));

Expand Down
Loading
Loading