From 10c483bd0dc8940230c79a986ece5f66c6610caa Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 20 Sep 2026 22:42:48 +0100 Subject: [PATCH 01/20] Add design for 32-bit compilation Four-phase plan for compiling to 32-bit targets, with i686-pc-windows-msvc as the proof target and width-correctness that generalizes to any 32-bit triple. Probing the current build first moved most of the scope. #333 removing the LLVMSupport dependency turns out to have been the gate: -mm=none already produces a working 32-bit .exe. What remains is a 32-bit Boehm and runtime, the __stdcall decoration on _CxxThrowException, and the image-base subtraction in the Win32 RTTI helpers, which x86 must not do. Two assumptions the probing disproved are recorded in the spec, since the emitted IR reads the other way: the async runtime's i64 parameters are int64_t rather than size_t and so are already correct on x86, and the ThrowInfo/CatchableType layouts coincide between the two ABIs because an x86 absolute pointer is the same width as an x64 RVA. Co-Authored-By: Claude Opus 5 --- .../2026-09-20-32-bit-compilation-design.md | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-20-32-bit-compilation-design.md 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 new file mode 100644 index 000000000..6390d14ad --- /dev/null +++ b/docs/superpowers/specs/2026-09-20-32-bit-compilation-design.md @@ -0,0 +1,214 @@ +# 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, not yet implemented. + +## 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` alongside the parsed `Triple` +itself. It holds two kinds of fact: + +- **Widths** — `pointerBits`, `indexBits`. +- **Policy** — `usesImageBaseRelativeEH`, `stdcallDecoratesCxxThrow`, + `supportsInProcessJit`. + +`TypeHelper` gains `getSizeType()` and `getPointerIntType()` backed by it. +`TypeHelper` currently takes only an `MLIRContext*`, so it grows a +`CompileOptions` parameter. That is a wide but mechanical diff and the single +riskiest mechanical change in this plan. + +Keeping the parsed `llvm::Triple` on `CompileOptions` as well means one-off +queries do not each require a new `TargetInfo` field. + +### 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 has already rotted: `loongarch64` is on it, +`riscv32` appears nowhere. 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 `CompileOptions`. The arch list is +replaced. + +There are 38 `getI64Type()` call sites across `lib/` and `include/`. Eighteen +of them 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. Phase 1 audits the +remaining 20, 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. + +Most are expected to be genuinely 64-bit. The audit's value is not the count of +changes but that the classification becomes explicit and survives future edits. + +`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, and no 32-bit target +emits 64-bit-shaped size or pointer arithmetic. Verifiable with no native +builds. + +## Phase 2 — 32-bit native build matrix + +- `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. + +**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`. + +## 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 38 call sites 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. | From 78a1e2340d0ea97e71d925f75f16e24e5740bba8 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 20 Sep 2026 23:09:42 +0100 Subject: [PATCH 02/20] Add Phase 1 implementation plan for 32-bit support Six TDD tasks: TargetInfo and its derivation from the triple, carrying it on CompileOptions and deleting the rotted arch list, target-width accessors on TypeHelper, the data layout on the emitted module, the i64 audit, and the --emit=jit guard. Deviates from the spec on one point. The spec calls threading CompileOptions into TypeHelper "a wide but mechanical diff"; counting found 152 construction sites, nearly all of which only ask for width-independent types. The options are optional instead, and getSizeType() asserts rather than guessing, so only the ~20 sites that need a width are touched. Every LLVM and MLIR API the plan calls was checked against the headers in 3rdParty/llvm/x64/release rather than written from memory. Co-Authored-By: Claude Opus 5 --- .../2026-09-20-32-bit-phase-1-target-width.md | 855 ++++++++++++++++++ 1 file changed, 855 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md diff --git a/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md new file mode 100644 index 000000000..dfb80e50e --- /dev/null +++ b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md @@ -0,0 +1,855 @@ +# 32-bit Phase 1: Target Width Correctness — 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:** Make the compiler derive pointer/size width and target policy from the target triple rather than from a hand-maintained architecture list, so that any 32-bit triple is handled correctly. + +**Architecture:** A new `TargetInfo` struct is derived once from `llvm::Triple` in `prepareOptions()` and carried on `CompileOptions` alongside the parsed `Triple`. It holds widths (`pointerBits`) and policy predicates named for the decision they answer (`usesImageBaseRelativeEH`, `stdcallDecoratesCxxThrow`, `supportsInProcessJit`). `TypeHelper` gains an optional `CompileOptions` pointer and target-width accessors; only call sites that need a width pass it. + +**Tech Stack:** C++17, MLIR/LLVM (prebuilt in `3rdParty/llvm/x64`), CMake + MSVC (Visual Studio 18 2026), GoogleTest via `add_mlir_unittest`. + +**Spec:** `docs/superpowers/specs/2026-09-20-32-bit-compilation-design.md` + +## Global Constraints + +- Proof target is `i686-pc-windows-msvc`. Width correctness must generalize to any 32-bit triple; only this one gets a native build matrix (later phases). +- The compiler itself stays x64 and cross-compiles. Never build `tslang.exe` as 32-bit. +- Policy predicates are named for what they decide, never for an architecture. `usesImageBaseRelativeEH`, never `isX64`. +- Arch naming, where it appears, is `x86` / `x64`. +- Missing per-arch resources are hard errors, never a fallback to another arch. +- This phase changes **no** behavior for existing 64-bit targets. Any diff in x64 output is a bug in this phase. +- The 18 `getI64Type()` sites in `LLVMRTTIHelperVCWin32.h` and `MLIRRTTIHelperVCWin32.h` belong to Phase 3. Do not touch them here. + +## Deviation from the spec + +The spec says `TypeHelper` "grows a `CompileOptions` parameter" and calls it "a wide but mechanical diff". Counting found 152 `TypeHelper` construction sites across 22 files, nearly all of which only use width-independent helpers (`getI8Type`, `getPtrType`, `getVoidType`). This plan instead gives `TypeHelper` an **optional** `const CompileOptions *`, defaulting to `nullptr`, and migrates only the sites that call a target-width accessor. `getSizeType()` asserts the pointer is set, so a site that needs a width and did not pass options fails loudly in debug rather than silently picking a wrong one. Net effect: same design, ~20 sites touched instead of 152. + +## File Structure + +| File | Responsibility | +| --- | --- | +| `include/TypeScript/TargetInfo.h` (create) | The `TargetInfo` struct and its derivation from a target/host `llvm::Triple` pair. Header-only; pure, no MLIR dependency, so it unit-tests without a compilation context. | +| `include/TypeScript/DataStructs.h` (modify) | `CompileOptions` gains `TargetInfo targetInfo`. `sizeBits` becomes a deprecated accessor over `targetInfo.pointerBits` so existing readers keep working during migration. | +| `tslang/opts.cpp` (modify) | `prepareOptions()` builds `TargetInfo` from the target and host triples. The 23-entry 64-bit arch list is deleted. | +| `include/TypeScript/LowerToLLVM/TypeHelper.h` (modify) | Optional `const CompileOptions *`; adds `getSizeType()` and `getPointerIntType()`. | +| `lib/TypeScript/MLIRGenModule.cpp` (modify) | Emits `target datalayout` on the module, not just the triple. | +| `unittests/MLIRGen/TargetInfo.cpp` (create) | GoogleTest coverage of `TargetInfo::fromTriple` across triples. | +| `unittests/MLIRGen/CMakeLists.txt` (modify) | Registers the new test file. | + +## Build and test commands + +Configure and build (once, if not already done): + +```bash +cmake --preset windows-msbuild-2026-debug +cmake --build --preset build-windows-msbuild-2026-debug --target MLIRTypeScriptUnitTests +``` + +Run the unit tests: + +```bash +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-debug/bin/MLIRGenTests.exe --gtest_filter='TargetInfoTest.*' +``` + +--- + +### Task 1: `TargetInfo` and its derivation from a triple + +**Files:** +- Create: `i:/TypeScriptCompiler/tslang/include/TypeScript/TargetInfo.h` +- Create: `i:/TypeScriptCompiler/tslang/unittests/MLIRGen/TargetInfo.cpp` +- Modify: `i:/TypeScriptCompiler/tslang/unittests/MLIRGen/CMakeLists.txt` + +**Interfaces:** +- Consumes: `llvm::Triple` from `llvm/TargetParser/Triple.h`. +- Produces: `struct TargetInfo` with fields `unsigned pointerBits`, `bool usesImageBaseRelativeEH`, `bool stdcallDecoratesCxxThrow`, `bool supportsInProcessJit`, and `static TargetInfo fromTriple(const llvm::Triple &target, const llvm::Triple &host)`. Tasks 2-5 and Phase 3 depend on exactly these names. + +- [ ] **Step 1: Write the failing test** + +Create `unittests/MLIRGen/TargetInfo.cpp`: + +```cpp +#include "TypeScript/TargetInfo.h" + +#include "llvm/TargetParser/Triple.h" + +#include "gmock/gmock.h" + +// TargetInfo replaces a hand-maintained list of 64-bit architectures in opts.cpp. That list had +// already rotted - loongarch64 was on it, riscv32 appeared nowhere - so these tests deliberately +// cover arches nobody has built for, to prove the answer comes from the triple rather than from +// somebody remembering to add a line. +namespace +{ + +llvm::Triple t(const char *str) +{ + return llvm::Triple(llvm::Triple::normalize(str)); +} + +const llvm::Triple &hostX64() +{ + static llvm::Triple host = t("x86_64-pc-windows-msvc"); + return host; +} + +TargetInfo infoFor(const char *target) +{ + return TargetInfo::fromTriple(t(target), hostX64()); +} + +TEST(TargetInfoTest, PointerWidthComesFromTheTriple) +{ + EXPECT_EQ(infoFor("x86_64-pc-windows-msvc").pointerBits, 64u); + EXPECT_EQ(infoFor("i686-pc-windows-msvc").pointerBits, 32u); + EXPECT_EQ(infoFor("wasm32-unknown-unknown").pointerBits, 32u); + EXPECT_EQ(infoFor("aarch64-unknown-linux-gnu").pointerBits, 64u); +} + +// The arches the old list got wrong, in both directions. +TEST(TargetInfoTest, PointerWidthIsRightForArchesTheOldListMissed) +{ + EXPECT_EQ(infoFor("riscv32-unknown-elf").pointerBits, 32u); + EXPECT_EQ(infoFor("armv7-unknown-linux-gnueabihf").pointerBits, 32u); + EXPECT_EQ(infoFor("loongarch64-unknown-linux-gnu").pointerBits, 64u); +} + +// An unknown arch has no width of its own; falling back to 0 would make every size computation +// degenerate, so it takes the host's. +TEST(TargetInfoTest, UnknownArchFallsBackToHostWidth) +{ + EXPECT_EQ(infoFor("unknown-unknown-unknown").pointerBits, 64u); +} + +// MSVC C++ EH cross-references are image-base-relative RVAs on 64-bit Windows and absolute +// pointers on 32-bit x86. Non-Windows targets use the Itanium scheme and neither applies. +TEST(TargetInfoTest, ImageBaseRelativeEHIsWin64Only) +{ + EXPECT_TRUE(infoFor("x86_64-pc-windows-msvc").usesImageBaseRelativeEH); + EXPECT_FALSE(infoFor("i686-pc-windows-msvc").usesImageBaseRelativeEH); + EXPECT_FALSE(infoFor("x86_64-unknown-linux-gnu").usesImageBaseRelativeEH); +} + +// _CxxThrowException is __stdcall on 32-bit x86 only, which is what puts the @8 on the symbol. +TEST(TargetInfoTest, StdcallThrowIsWin32X86Only) +{ + EXPECT_TRUE(infoFor("i686-pc-windows-msvc").stdcallDecoratesCxxThrow); + EXPECT_FALSE(infoFor("x86_64-pc-windows-msvc").stdcallDecoratesCxxThrow); + EXPECT_FALSE(infoFor("i686-unknown-linux-gnu").stdcallDecoratesCxxThrow); +} + +// tslang.exe is an x64 process and cannot execute i386 or wasm in-process. +TEST(TargetInfoTest, InProcessJitNeedsAMatchingHost) +{ + EXPECT_TRUE(infoFor("x86_64-pc-windows-msvc").supportsInProcessJit); + EXPECT_FALSE(infoFor("i686-pc-windows-msvc").supportsInProcessJit); + EXPECT_FALSE(infoFor("wasm32-unknown-unknown").supportsInProcessJit); + EXPECT_FALSE(infoFor("x86_64-unknown-linux-gnu").supportsInProcessJit); +} + +} // namespace +``` + +Register it by adding `TargetInfo.cpp` to `unittests/MLIRGen/CMakeLists.txt`: + +```cmake +add_mlir_unittest(MLIRGenTests + TypeToString.cpp + DeclarationPrinter.cpp + ExportFilter.cpp + TypeHelper.cpp + TargetInfo.cpp +) +``` + +- [ ] **Step 2: Run the test to verify it fails** + +```bash +cmake --build --preset build-windows-msbuild-2026-debug --target MLIRTypeScriptUnitTests +``` + +Expected: compile error, `Cannot open include file: 'TypeScript/TargetInfo.h'`. + +- [ ] **Step 3: Write the implementation** + +Create `include/TypeScript/TargetInfo.h`: + +```cpp +#ifndef TYPESCRIPT_TARGETINFO_H_ +#define TYPESCRIPT_TARGETINFO_H_ + +#include "llvm/TargetParser/Triple.h" + +// Everything the compiler needs to know about the target beyond the triple string itself, +// derived once in prepareOptions() and carried on CompileOptions. +// +// Predicates are named for the decision they answer, never for the architecture that currently +// needs them - `usesImageBaseRelativeEH`, not `isX64`. A new target then answers the questions, +// instead of every call site growing another arch comparison. This replaces a hand-maintained +// list of 64-bit arches in opts.cpp that had already rotted. +struct TargetInfo +{ + // Width of a pointer, and of the integer type used for sizes and indices. + unsigned pointerBits = 64; + + // MSVC C++ EH stores the cross-references inside ThrowInfo and CatchableType as + // image-base-relative RVAs on 64-bit Windows, and as absolute pointers on 32-bit x86. Both + // are 4 bytes, so the struct layouts coincide and only the stored value differs: x86 must + // not subtract the image base. See Phase 3. + bool usesImageBaseRelativeEH = true; + + // _CxxThrowException is __stdcall on 32-bit x86, so the emitted symbol carries the @8 + // suffix that the linker asks for. + bool stdcallDecoratesCxxThrow = false; + + // Whether tslang.exe - an x64 process - can execute this target's code in-process. + bool supportsInProcessJit = true; + + static TargetInfo fromTriple(const llvm::Triple &target, const llvm::Triple &host) + { + TargetInfo info; + + info.pointerBits = target.getArchPointerBitWidth(); + if (info.pointerBits == 0) + { + // An unknown arch has no width of its own. Zero would make every size computation + // degenerate, so take the host's - the same assumption the compiler already makes + // when no triple is given at all. + info.pointerBits = host.getArchPointerBitWidth(); + } + + const bool msvc = target.isKnownWindowsMSVCEnvironment(); + const bool x86_32 = target.getArch() == llvm::Triple::x86; + + info.usesImageBaseRelativeEH = msvc && !x86_32; + info.stdcallDecoratesCxxThrow = msvc && x86_32; + info.supportsInProcessJit = + target.getArch() == host.getArch() && target.getOS() == host.getOS(); + + return info; + } +}; + +#endif // TYPESCRIPT_TARGETINFO_H_ +``` + +- [ ] **Step 4: Run the test to verify it passes** + +```bash +cmake --build --preset build-windows-msbuild-2026-debug --target MLIRTypeScriptUnitTests +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-debug/bin/MLIRGenTests.exe --gtest_filter='TargetInfoTest.*' +``` + +Expected: 6 tests, all PASS. + +- [ ] **Step 5: Commit** + +```bash +git add tslang/include/TypeScript/TargetInfo.h tslang/unittests/MLIRGen/TargetInfo.cpp tslang/unittests/MLIRGen/CMakeLists.txt +git commit -m "Derive target width and policy from the triple + +TargetInfo answers what the compiler needs to know about a target: +pointer width, and the policy questions Win32 EH and the JIT ask. Named +for the decisions rather than the arches, so a new target answers them +instead of each call site growing an arch comparison. + +Co-Authored-By: Claude Opus 5 " +``` + +--- + +### Task 2: Carry `TargetInfo` on `CompileOptions` and delete the arch list + +**Files:** +- Modify: `i:/TypeScriptCompiler/tslang/include/TypeScript/DataStructs.h:9-24` +- Modify: `i:/TypeScriptCompiler/tslang/tslang/opts.cpp:36-91` + +**Interfaces:** +- Consumes: `TargetInfo::fromTriple` from Task 1. +- Produces: `CompileOptions::targetInfo` (a `TargetInfo`) and `CompileOptions::sizeBits` retained as an accessor returning `targetInfo.pointerBits`. Every existing reader of `compileOptions.sizeBits` keeps compiling unchanged — that is the point of keeping it. + +- [ ] **Step 1: Write the failing test** + +Append to `unittests/MLIRGen/TargetInfo.cpp`, inside the anonymous namespace: + +```cpp +// sizeBits is what ~20 existing call sites read. It must keep answering, and it must now answer +// from TargetInfo rather than from a separate field that could drift away from it. +TEST(TargetInfoTest, CompileOptionsSizeBitsFollowsTargetInfo) +{ + CompileOptions options; + + options.targetInfo = infoFor("i686-pc-windows-msvc"); + EXPECT_EQ(options.sizeBits(), 32); + + options.targetInfo = infoFor("x86_64-pc-windows-msvc"); + EXPECT_EQ(options.sizeBits(), 64); +} +``` + +Add the include at the top of the file, below the `TargetInfo.h` include: + +```cpp +#include "TypeScript/DataStructs.h" +``` + +- [ ] **Step 2: Run the test to verify it fails** + +```bash +cmake --build --preset build-windows-msbuild-2026-debug --target MLIRTypeScriptUnitTests +``` + +Expected: compile error — `CompileOptions` has no member `targetInfo`, and `sizeBits` is an `int` data member, not callable. + +- [ ] **Step 3: Write the implementation** + +In `include/TypeScript/DataStructs.h`, add the include at the top: + +```cpp +#include "TypeScript/TargetInfo.h" +``` + +Then replace the `int sizeBits;` member (line 20) with: + +```cpp + 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(targetInfo.pointerBits); + } +``` + +In `tslang/opts.cpp`, add the include: + +```cpp +#include "TypeScript/TargetInfo.h" +``` + +Replace `compileOptions.sizeBits = 32;` (line 57) with: + +```cpp + compileOptions.targetInfo = TargetInfo::fromTriple( + TheTriple, llvm::Triple(llvm::sys::getDefaultTargetTriple())); +``` + +Delete the entire `if (TheTriple.getArch() == llvm::Triple::UnknownArch || ...) { compileOptions.sizeBits = 64; }` block (lines 67-91). It is replaced by `getArchPointerBitWidth()` inside `TargetInfo::fromTriple`. + +Then update every reader of `compileOptions.sizeBits` to call it. The sites, from the spec's audit: + +- `include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h:279` +- `include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h:118` +- `include/TypeScript/LowerToLLVM/ThrowLogic.h:149` +- `include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h:143` +- `include/TypeScript/MLIRLogic/MLIRTypeHelper.h:1646, 1651, 1866, 1915, 1920` +- `lib/TypeScript/LowerToLLVM.cpp:5250, 5293, 7257` +- `lib/TypeScript/MLIRGenModule.cpp:263` +- `tslang/transform.cpp:347, 348` +- `unittests/MLIRGen/TypeHelper.cpp:36` + +Each is a mechanical `compileOptions.sizeBits` → `compileOptions.sizeBits()`. The one exception is `unittests/MLIRGen/TypeHelper.cpp:36`, which *assigns* it; change that line from `compileOptions.sizeBits = 64;` to: + +```cpp + compileOptions.targetInfo = TargetInfo::fromTriple( + llvm::Triple("x86_64-pc-windows-msvc"), llvm::Triple("x86_64-pc-windows-msvc")); +``` + +- [ ] **Step 4: Run the tests to verify they pass** + +```bash +cmake --build --preset build-windows-msbuild-2026-debug --target MLIRTypeScriptUnitTests +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-debug/bin/MLIRGenTests.exe +``` + +Expected: all tests PASS, including the pre-existing `MLIRTypeHelperTest` suite. If any `MLIRTypeHelperTest` test now fails, the width default changed for x64 — that is a regression in this task, not an acceptable diff. + +- [ ] **Step 5: Verify x64 output is byte-identical** + +This phase must not change 64-bit behavior. Build the compiler and diff its IR against the current one: + +```bash +cmake --build --preset build-windows-msbuild-2026-release +cd "$TMPDIR" && printf 'print("x");\n' > w.ts +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe \ + --emit=llvm -mm=gc --no-default-lib w.ts && mv w.ll after.ll +git stash && cmake --build --preset build-windows-msbuild-2026-release +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe \ + --emit=llvm -mm=gc --no-default-lib w.ts && mv w.ll before.ll +git stash pop +diff before.ll after.ll +``` + +Expected: no differences. + +- [ ] **Step 6: Commit** + +```bash +git add tslang/include/TypeScript/DataStructs.h tslang/tslang/opts.cpp tslang/unittests/MLIRGen/TargetInfo.cpp tslang/unittests/MLIRGen/TypeHelper.cpp +git commit -m "Take pointer width from the triple, not from an arch list + +opts.cpp listed 23 architectures it considered 64-bit. The list had +already rotted: loongarch64 was on it, riscv32 appeared nowhere. Triple +already knows, so ask it. + +sizeBits stays, as an accessor over TargetInfo, so it cannot drift from +the one place the width is decided. + +Co-Authored-By: Claude Opus 5 " +``` + +--- + +### Task 3: Target-width accessors on `TypeHelper` + +**Files:** +- Modify: `i:/TypeScriptCompiler/tslang/include/TypeScript/LowerToLLVM/TypeHelper.h:19-30,62-66` +- Modify: `i:/TypeScriptCompiler/tslang/unittests/MLIRGen/TargetInfo.cpp` + +**Interfaces:** +- Consumes: `CompileOptions::targetInfo` from Task 2. +- Produces: `TypeHelper::getSizeType()` and `TypeHelper::getPointerIntType()`, both returning `mlir::Type`; and constructors `TypeHelper(OpBuilder &, const CompileOptions &)` and `TypeHelper(MLIRContext *, const CompileOptions &)`. The existing two constructors stay, so the 152 existing sites are untouched. + +- [ ] **Step 1: Write the failing test** + +Append to `unittests/MLIRGen/TargetInfo.cpp`, inside the anonymous namespace: + +```cpp +// TypeHelper is constructed at 152 sites, nearly all of which only ask for width-independent +// types. Rather than thread CompileOptions through all of them, the options are optional and +// only the sites that need a width pass them - so getSizeType() has to answer from the target, +// and has to refuse rather than guess when nobody supplied one. +TEST(TargetInfoTest, TypeHelperSizeTypeFollowsTheTarget) +{ + mlir::MLIRContext context; + + CompileOptions options32; + options32.targetInfo = infoFor("i686-pc-windows-msvc"); + EXPECT_EQ(typescript::TypeHelper(&context, options32).getSizeType(), + mlir::IntegerType::get(&context, 32)); + + CompileOptions options64; + options64.targetInfo = infoFor("x86_64-pc-windows-msvc"); + EXPECT_EQ(typescript::TypeHelper(&context, options64).getSizeType(), + mlir::IntegerType::get(&context, 64)); +} +``` + +Add these includes at the top of the file: + +```cpp +#include "TypeScript/LowerToLLVM/TypeHelper.h" + +#include "mlir/IR/MLIRContext.h" +``` + +- [ ] **Step 2: Run the test to verify it fails** + +```bash +cmake --build --preset build-windows-msbuild-2026-debug --target MLIRTypeScriptUnitTests +``` + +Expected: compile error — no `TypeHelper` constructor taking `CompileOptions`, and no member `getSizeType`. + +- [ ] **Step 3: Write the implementation** + +In `include/TypeScript/LowerToLLVM/TypeHelper.h`, add the include: + +```cpp +#include "TypeScript/DataStructs.h" +``` + +Replace the class's member and constructor block (lines 19-30) with: + +```cpp +class TypeHelper +{ + MLIRContext *context; + + // Optional on purpose. TypeHelper is constructed at ~152 sites, nearly all of which only ask + // for width-independent types (getI8Type, getPtrType, getVoidType). Threading CompileOptions + // through all of them would be a large diff for no benefit, so only the sites that ask for a + // target width pass it - and getSizeType() asserts rather than guessing when they have not. + const CompileOptions *compileOptions = nullptr; + + public: + TypeHelper(OpBuilder &rewriter) : context(rewriter.getContext()) + { + } + + TypeHelper(MLIRContext *context) : context(context) + { + } + + TypeHelper(OpBuilder &rewriter, const CompileOptions &compileOptions) + : context(rewriter.getContext()), compileOptions(&compileOptions) + { + } + + TypeHelper(MLIRContext *context, const CompileOptions &compileOptions) + : context(context), compileOptions(&compileOptions) + { + } +``` + +Then add the accessors immediately after `getI64Type()` (line 66): + +```cpp + // An integer as wide as the target's pointer: object sizes, byte counts, GEP indices. + // Distinct from getI64Type(), which is for values that are 64-bit whatever the target is - + // the language's own 64-bit integers and runtime ABI parameters declared int64_t. + mlir::Type getSizeType() + { + assert(compileOptions && "getSizeType() needs the target: construct TypeHelper with CompileOptions"); + return mlir::IntegerType::get(context, compileOptions->targetInfo.pointerBits); + } + + // The integer a pointer converts to under ptrtoint on this target. + mlir::Type getPointerIntType() + { + return getSizeType(); + } +``` + +- [ ] **Step 4: Run the test to verify it passes** + +```bash +cmake --build --preset build-windows-msbuild-2026-debug --target MLIRTypeScriptUnitTests +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-debug/bin/MLIRGenTests.exe --gtest_filter='TargetInfoTest.*' +``` + +Expected: 8 tests, all PASS. + +- [ ] **Step 5: Commit** + +```bash +git add tslang/include/TypeScript/LowerToLLVM/TypeHelper.h tslang/unittests/MLIRGen/TargetInfo.cpp +git commit -m "Give TypeHelper target-width accessors + +getSizeType() is for things that are as wide as a pointer; getI64Type() +stays for things that are 64-bit whatever the target is. Keeping both +means the audit in the next commit can record which each site meant. + +The options are optional because 152 sites construct TypeHelper and +almost none need a width. getSizeType() asserts rather than guessing. + +Co-Authored-By: Claude Opus 5 " +``` + +--- + +### Task 4: Emit `target datalayout` on the module + +**Files:** +- Modify: `i:/TypeScriptCompiler/tslang/lib/TypeScript/MLIRGenModule.cpp:254-264` + +**Interfaces:** +- Consumes: `CompileOptions::sizeBits()` from Task 2. +- Produces: no new API. The emitted LLVM module gains a `target datalayout` line. + +Today the module carries only `llvm.target_triple`, and the data layout is set much later, in `tslang/obj.cpp:248`, from the `TargetMachine`. So `--emit=llvm` output is not self-describing: it names a 32-bit target while carrying LLVM's default layout. Anything reasoning about size between MLIRGen and `obj.cpp` is guessing. + +- [ ] **Step 1: Write the failing test** + +This is an end-to-end property of emitted IR, and the repo has no lit/FileCheck harness — the test is a shell check. Create `i:/TypeScriptCompiler/tslang/test/check-datalayout.sh`: + +```bash +#!/usr/bin/env bash +# Phase 1 gate: emitted IR must name a data layout that matches its triple, so that --emit=llvm +# output is self-describing rather than carrying LLVM's default layout under a 32-bit triple. +set -u +TSLANG="${1:?usage: check-datalayout.sh }" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +printf 'print("x");\n' > "$work/dl.ts" + +fail=0 +check() { + local triple="$1" expect="$2" + "$TSLANG" --emit=llvm -mm=none --no-default-lib -mtriple="$triple" "$work/dl.ts" \ + -o "$work/dl.ll" >/dev/null 2>&1 + local got + got="$(grep -m1 '^target datalayout' "$work/dl.ll" || true)" + if [ -z "$got" ]; then + echo "FAIL $triple: no 'target datalayout' in emitted IR" + fail=1 + elif ! printf '%s' "$got" | grep -q -- "$expect"; then + echo "FAIL $triple: expected a layout containing '$expect', got: $got" + fail=1 + else + echo "ok $triple" + fi +} + +# p:32: and p:64: are the pointer-size entries; they are what this phase is about. +check "i686-pc-windows-msvc" "p:32:" +check "wasm32-unknown-unknown" "p:32:" +check "x86_64-pc-windows-msvc" "-p270:" # x86-64 layouts carry the AS270/271/272 entries + +exit "$fail" +``` + +- [ ] **Step 2: Run it to verify it fails** + +```bash +chmod +x i:/TypeScriptCompiler/tslang/test/check-datalayout.sh +i:/TypeScriptCompiler/tslang/test/check-datalayout.sh \ + i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe +``` + +Expected: all three FAIL with "no 'target datalayout' in emitted IR". + +- [ ] **Step 3: Write the implementation** + +In `lib/TypeScript/MLIRGenModule.cpp`, inside the `if (!compileOptions.moduleTargetTriple.empty())` block, after the `dlti.dl_spec` assignment, add: + +```cpp + // The data layout is otherwise only set in obj.cpp, from the TargetMachine, which is + // after --emit=llvm has already printed the module. Setting it here makes the emitted + // IR self-describing: a 32-bit triple no longer prints alongside LLVM's default + // 64-bit layout. Derived from the triple via LLVM's own target registry so it matches + // exactly what obj.cpp will later set, rather than being a second hand-built string. + std::string errorMessage; + if (auto *target = llvm::TargetRegistry::lookupTarget( + compileOptions.moduleTargetTriple, errorMessage)) + { + std::unique_ptr machine(target->createTargetMachine( + llvm::Triple(compileOptions.moduleTargetTriple), "generic", "", + llvm::TargetOptions(), std::nullopt)); + if (machine) + { + theModule->setAttr( + mlir::LLVM::LLVMDialect::getDataLayoutAttrName(), + builder.getStringAttr(machine->createDataLayout().getStringRepresentation())); + } + } +``` + +Add the includes at the top of `MLIRGenModule.cpp`: + +```cpp +#include "llvm/MC/TargetRegistry.h" +#include "llvm/Target/TargetMachine.h" +``` + +- [ ] **Step 4: Run it to verify it passes** + +```bash +cmake --build --preset build-windows-msbuild-2026-release +i:/TypeScriptCompiler/tslang/test/check-datalayout.sh \ + i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe +``` + +Expected: three `ok` lines, exit 0. + +Target registration is already handled and needs no work here: `tslang.cpp:337` calls `llvm::InitializeAllTargets()` in `main`, before any compilation, and that function calls `InitializeAllTargetInfos()` internally (see `llvm/Support/TargetSelect.h`). So every configured backend, wasm included, is in the registry by the time `MLIRGenModule` runs. If `lookupTarget` nevertheless fails for a triple, that backend is not in this LLVM build's `Targets.def` — check before assuming a registration bug. + +- [ ] **Step 5: Commit** + +```bash +git add tslang/lib/TypeScript/MLIRGenModule.cpp tslang/test/check-datalayout.sh +git commit -m "Set the data layout on the emitted module + +The layout was only set in obj.cpp, from the TargetMachine, which is +after --emit=llvm has printed. So IR emitted for a 32-bit triple carried +LLVM's default 64-bit layout, and anything reading sizes between MLIRGen +and obj.cpp was guessing. + +Co-Authored-By: Claude Opus 5 " +``` + +--- + +### Task 5: Audit and classify the 20 non-EH `getI64Type()` sites + +**Files:** +- Modify (audit, some will change): `lib/TypeScript/GCPass.cpp` (1), `lib/TypeScript/LowerToLLVM.cpp` (2), `lib/TypeScript/MLIRGenAccessCall.cpp` (1), `lib/TypeScript/MLIRGenClasses.cpp` (4), `lib/TypeScript/MLIRGenExpressions.cpp` (1), `lib/TypeScript/MLIRGenImpl.h` (1), `lib/TypeScript/MLIRGenInterfaces.cpp` (2), `include/TypeScript/LowerToLLVM/CastLogicHelper.h` (2), `include/TypeScript/LowerToLLVM/CodeLogicHelper.h` (1), `include/TypeScript/LowerToLLVM/ConvertLogic.h` (1), `include/TypeScript/LowerToLLVM/ThrowLogic.h` (1), `include/TypeScript/LowerToLLVM/TypeHelper.h` (1, the definition), `include/TypeScript/MLIRLogic/MLIRTypeHelper.h` (2) + +**Interfaces:** +- Consumes: `TypeHelper::getSizeType()` from Task 3. +- Produces: no new API. Each audited site carries a one-line comment recording its classification. + +**Do not touch** `include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h` (9 sites) or `include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h` (9 sites). Those are Phase 3's; it rewrites that arithmetic anyway, and changing them here would collide. + +- [ ] **Step 1: Enumerate the sites** + +```bash +cd i:/TypeScriptCompiler/tslang +grep -rn 'getI64Type()' lib include \ + | grep -v 'RTTIHelperVCWin32.h' \ + > /tmp/i64-audit.txt +wc -l /tmp/i64-audit.txt +``` + +Expected: 20 lines. + +- [ ] **Step 2: Classify each site** + +For each line, read the surrounding code and decide which of two things the `i64` means: + +- **genuinely 64-bit** — the value is 64 bits whatever the target is. The language's own 64-bit integer types; runtime ABI parameters declared `int64_t` in `lib/AsyncRuntimeCommon.inc` (`mlirAsyncRuntimeAddRef`, `mlirAsyncRuntimeCreateValue`, `mlirAsyncRuntimeAddTokenToGroup`); anything whose width is fixed by a contract outside the target. **Leave the code as is** and add a comment saying why. +- **target-width** — a pointer converted with `ptrtoint`, an object size, a byte count, a GEP index. **Change to `getSizeType()`.** + +Add a one-line comment at each site recording the decision, in the form: + +```cpp +// 64-bit whatever the target: mlirAsyncRuntimeAddRef takes int64_t, not size_t. +``` + +or + +```cpp +// target-width: this is a byte count for the allocation below. +``` + +The comment is the deliverable here as much as any code change. The point of the audit is that the classification becomes explicit and survives future edits — a bare `getI64Type()` gives a later reader nothing to check against. + +- [ ] **Step 3: Run the unit tests** + +```bash +cmake --build --preset build-windows-msbuild-2026-debug --target MLIRTypeScriptUnitTests +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-debug/bin/MLIRGenTests.exe +``` + +Expected: all PASS. + +- [ ] **Step 4: Verify x64 output is unchanged** + +Any site correctly classified as target-width emits `i64` on x64 either way, so x64 IR must be byte-identical. A diff here means a site was misclassified. + +```bash +cmake --build --preset build-windows-msbuild-2026-release +cd "$TMPDIR" && printf 'class C { x: number; f() { return this.x; } }\nconst c = new C();\nc.x = 1;\nprint(c.f());\n' > a.ts +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe \ + --emit=llvm -mm=gc --no-default-lib a.ts && mv a.ll after.ll +git stash && cmake --build --preset build-windows-msbuild-2026-release +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe \ + --emit=llvm -mm=gc --no-default-lib a.ts && mv a.ll before.ll +git stash pop +diff before.ll after.ll +``` + +Expected: no differences. + +- [ ] **Step 5: Run the full test suite** + +The suite is the real check that no 64-bit behavior moved. Run it exactly as for a normal change, Release: + +```bash +cmake --build --preset build-windows-msbuild-2026-release +cd i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release && ctest -j 16 +``` + +Expected: the same result as `main` — 2765/2765 as of 2026-09-16. Any new failure is this task's, not a pre-existing one; confirm by re-running that test on `main` before concluding otherwise. + +- [ ] **Step 6: Commit** + +```bash +git add -A tslang/lib tslang/include +git commit -m "Classify every i64 in lowering as fixed-width or target-width + +Each of the 20 non-EH getI64Type() sites now says which it meant. Most +are genuinely 64-bit - the async runtime's parameters are int64_t, not +size_t, so i64 is right on x86 too. The rest are pointer and size +arithmetic and now ask the target. + +x64 IR is unchanged, which is the check that the classification is +right: a target-width site emits i64 on x64 either way. + +Co-Authored-By: Claude Opus 5 " +``` + +--- + +### Task 6: Refuse `--emit=jit` for a non-host target + +**Files:** +- Modify: `i:/TypeScriptCompiler/tslang/tslang/jit.cpp:554-566` + +**Interfaces:** +- Consumes: `CompileOptions::targetInfo.supportsInProcessJit` from Task 1. +- Produces: no new API. + +`tslang.exe` is an x64 process and cannot execute i386 or wasm code in-process. The spec requires this be refused up front with a message that says so. + +- [ ] **Step 1: Find out what it does today** + +```bash +cd "$TMPDIR" && printf 'print("x");\n' > j.ts +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe \ + --emit=jit -mm=none --no-default-lib -mtriple=i686-pc-windows-msvc j.ts; echo "exit=$?" +``` + +Record the observed behavior in the commit message. If it already refuses with a clear message, close this task as unnecessary and say so rather than adding a second check. + +- [ ] **Step 2: Write the guard** + +In `tslang/jit.cpp`, immediately after `TheTriple = llvm::Triple(targetTriple);` (line 561): + +```cpp + // tslang.exe is an x64 process: it cannot execute i386 or wasm code in-process. Refused + // here rather than left to LLJIT, whose failure for this is a relocation or "symbol not + // found" error deep in the session that says nothing about the triple being the cause. + if (!compileOptions.targetInfo.supportsInProcessJit) + { + llvm::WithColor::error(llvm::errs(), "tslang") + << "--emit=jit runs the code in this process, which is " + << llvm::sys::getDefaultTargetTriple() << ", so it cannot run code built for " + << targetTriple << ". Build it instead with --emit=exe or --emit=obj.\n"; + return -1; + } +``` + +- [ ] **Step 3: Verify the guard fires** + +```bash +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe \ + --emit=jit -mm=none --no-default-lib -mtriple=i686-pc-windows-msvc j.ts; echo "exit=$?" +``` + +Expected: the new error message, non-zero exit. + +- [ ] **Step 4: Verify the host case still works** + +```bash +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe \ + --emit=jit -mm=gc j.ts; echo "exit=$?" +``` + +Expected: prints `x`, exit 0. + +- [ ] **Step 5: Run the full suite** + +```bash +cd i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release && ctest -j 16 +``` + +Expected: unchanged from Task 5. + +- [ ] **Step 6: Commit** + +```bash +git add tslang/tslang/jit.cpp +git commit -m "Refuse --emit=jit for a target this process cannot run + +tslang.exe is x64 and cannot execute i386 or wasm in-process. Left to +LLJIT this surfaces as a relocation failure deep in the session that +never mentions the triple. + +Co-Authored-By: Claude Opus 5 " +``` + +--- + +## Phase 1 gate + +All of the following, before Phase 2 is planned: + +- [ ] `MLIRGenTests.exe` passes, including the new `TargetInfoTest` suite. +- [ ] `test/check-datalayout.sh` passes for `i686-pc-windows-msvc`, `wasm32-unknown-unknown` and `x86_64-pc-windows-msvc`. +- [ ] `ctest` matches `main` — 2765/2765. +- [ ] x64 `--emit=llvm` output is byte-identical to `main` for a class-and-method sample. +- [ ] `--emit=jit -mtriple=i686-pc-windows-msvc` refuses with a message naming both triples. +- [ ] Every one of the 20 non-EH `getI64Type()` sites carries a classification comment. +- [ ] The 18 EH sites are untouched, and `git diff --stat` on the two RTTI helpers is empty. From d5e2b7d9f92685f90f6e40b7374789ec7bc0df04 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 20 Sep 2026 23:15:34 +0100 Subject: [PATCH 03/20] Fix three defects the pre-flight scan found in the Phase 1 plan The JIT guard cited a line inside the dumpToObjectFile path, reached only after the engine is built - it would never have fired on the ordinary JIT path. Moved to the top of runJit. The gate demanded the two RTTI helpers show an empty diff, which Task 2 necessarily violates: it has to change sizeBits to sizeBits() in both. Narrowed to what the spec actually reserves for Phase 3, the getI64Type arithmetic. The audit's count of 20 included the definition of getI64Type itself. Nineteen are call sites. Co-Authored-By: Claude Opus 5 --- .../2026-09-20-32-bit-phase-1-target-width.md | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md index dfb80e50e..762ed7e1f 100644 --- a/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md +++ b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md @@ -18,7 +18,7 @@ - Arch naming, where it appears, is `x86` / `x64`. - Missing per-arch resources are hard errors, never a fallback to another arch. - This phase changes **no** behavior for existing 64-bit targets. Any diff in x64 output is a bug in this phase. -- The 18 `getI64Type()` sites in `LLVMRTTIHelperVCWin32.h` and `MLIRRTTIHelperVCWin32.h` belong to Phase 3. Do not touch them here. +- The 18 `getI64Type()` sites in `LLVMRTTIHelperVCWin32.h` and `MLIRRTTIHelperVCWin32.h` belong to Phase 3. Do not touch those call sites here. The files themselves are not frozen: Task 2 changes `compileOptions.sizeBits` to `sizeBits()` in both, which is required. ## Deviation from the spec @@ -661,7 +661,7 @@ Co-Authored-By: Claude Opus 5 " --- -### Task 5: Audit and classify the 20 non-EH `getI64Type()` sites +### Task 5: Audit and classify the 19 non-EH `getI64Type()` sites **Files:** - Modify (audit, some will change): `lib/TypeScript/GCPass.cpp` (1), `lib/TypeScript/LowerToLLVM.cpp` (2), `lib/TypeScript/MLIRGenAccessCall.cpp` (1), `lib/TypeScript/MLIRGenClasses.cpp` (4), `lib/TypeScript/MLIRGenExpressions.cpp` (1), `lib/TypeScript/MLIRGenImpl.h` (1), `lib/TypeScript/MLIRGenInterfaces.cpp` (2), `include/TypeScript/LowerToLLVM/CastLogicHelper.h` (2), `include/TypeScript/LowerToLLVM/CodeLogicHelper.h` (1), `include/TypeScript/LowerToLLVM/ConvertLogic.h` (1), `include/TypeScript/LowerToLLVM/ThrowLogic.h` (1), `include/TypeScript/LowerToLLVM/TypeHelper.h` (1, the definition), `include/TypeScript/MLIRLogic/MLIRTypeHelper.h` (2) @@ -682,7 +682,7 @@ grep -rn 'getI64Type()' lib include \ wc -l /tmp/i64-audit.txt ``` -Expected: 20 lines. +Expected: 20 lines. **One of them is not a call site:** `include/TypeScript/LowerToLLVM/TypeHelper.h:62` is the *definition* of `getI64Type()` itself. Leave it alone. That leaves **19 call sites** to classify. - [ ] **Step 2: Classify each site** @@ -749,7 +749,7 @@ Expected: the same result as `main` — 2765/2765 as of 2026-09-16. Any new fail git add -A tslang/lib tslang/include git commit -m "Classify every i64 in lowering as fixed-width or target-width -Each of the 20 non-EH getI64Type() sites now says which it meant. Most +Each of the 19 non-EH getI64Type() call sites now says which it meant. Most are genuinely 64-bit - the async runtime's parameters are int64_t, not size_t, so i64 is right on x86 too. The rest are pointer and size arithmetic and now ask the target. @@ -765,7 +765,7 @@ Co-Authored-By: Claude Opus 5 " ### Task 6: Refuse `--emit=jit` for a non-host target **Files:** -- Modify: `i:/TypeScriptCompiler/tslang/tslang/jit.cpp:554-566` +- Modify: `i:/TypeScriptCompiler/tslang/tslang/jit.cpp:357` (the first statement of `runJit`) **Interfaces:** - Consumes: `CompileOptions::targetInfo.supportsInProcessJit` from Task 1. @@ -785,20 +785,28 @@ Record the observed behavior in the commit message. If it already refuses with a - [ ] **Step 2: Write the guard** -In `tslang/jit.cpp`, immediately after `TheTriple = llvm::Triple(targetTriple);` (line 561): +The guard goes at the **top of `runJit`**, as the first statement of the function body at `tslang/jit.cpp:359` — before `PrintStackTraceOnErrorSignal`, `registerMLIRDialects` and `InitializeNativeTarget`. Nothing should be initialized for a run that cannot happen. `compileOptions` is a parameter of `runJit` (line 357), so it is in scope. + +Note there is a *second* `llvm::Triple TheTriple` block further down at line 554; that one is inside the `dumpToObjectFile` path and is **not** where this guard belongs. ```cpp - // tslang.exe is an x64 process: it cannot execute i386 or wasm code in-process. Refused - // here rather than left to LLJIT, whose failure for this is a relocation or "symbol not - // found" error deep in the session that says nothing about the triple being the cause. - if (!compileOptions.targetInfo.supportsInProcessJit) - { - llvm::WithColor::error(llvm::errs(), "tslang") - << "--emit=jit runs the code in this process, which is " - << llvm::sys::getDefaultTargetTriple() << ", so it cannot run code built for " - << targetTriple << ". Build it instead with --emit=exe or --emit=obj.\n"; - return -1; - } +int runJit(int argc, char **argv, mlir::ModuleOp module, CompileOptions &compileOptions) +{ + // tslang.exe is an x64 process: it cannot execute i386 or wasm code in-process. Refused up + // front rather than left to LLJIT, whose failure for this is a relocation or "symbol not + // found" error deep in the session that never mentions the triple as the cause. + if (!compileOptions.targetInfo.supportsInProcessJit) + { + llvm::WithColor::error(llvm::errs(), "tslang") + << "--emit=jit runs the code in this process, which is " + << llvm::sys::getDefaultTargetTriple() << ", so it cannot run code built for " + << compileOptions.moduleTargetTriple + << ". Build it instead with --emit=exe or --emit=obj.\n"; + return -1; + } + + // to avoid false positive memory leak reports in release builds + // ... existing body continues unchanged ... ``` - [ ] **Step 3: Verify the guard fires** @@ -851,5 +859,5 @@ All of the following, before Phase 2 is planned: - [ ] `ctest` matches `main` — 2765/2765. - [ ] x64 `--emit=llvm` output is byte-identical to `main` for a class-and-method sample. - [ ] `--emit=jit -mtriple=i686-pc-windows-msvc` refuses with a message naming both triples. -- [ ] Every one of the 20 non-EH `getI64Type()` sites carries a classification comment. -- [ ] The 18 EH sites are untouched, and `git diff --stat` on the two RTTI helpers is empty. +- [ ] Every one of the 19 non-EH `getI64Type()` call sites carries a classification comment (the 20th grep hit is the definition in `TypeHelper.h:62`). +- [ ] The 18 EH `getI64Type()` sites are untouched. The two RTTI helper files are *not* otherwise frozen: Task 2 legitimately changes `compileOptions.sizeBits` to `sizeBits()` at `LLVMRTTIHelperVCWin32.h:118` and `MLIRRTTIHelperVCWin32.h:143`. Verify with `git diff -- | grep getI64Type`, which must be empty — not with `git diff --stat`, which will legitimately be non-empty. From a7c2cf31a63ffdb1c2d1fcb52c05a9f856e12671 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 20 Sep 2026 23:18:16 +0100 Subject: [PATCH 04/20] Derive target width and policy from the triple TargetInfo answers what the compiler needs to know about a target: pointer width, and the policy questions Win32 EH and the JIT ask. Named for the decisions rather than the arches, so a new target answers them instead of each call site growing an arch comparison. Co-Authored-By: Claude Sonnet 5 --- tslang/include/TypeScript/TargetInfo.h | 56 ++++++++++++++++++ tslang/unittests/MLIRGen/CMakeLists.txt | 1 + tslang/unittests/MLIRGen/TargetInfo.cpp | 79 +++++++++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 tslang/include/TypeScript/TargetInfo.h create mode 100644 tslang/unittests/MLIRGen/TargetInfo.cpp diff --git a/tslang/include/TypeScript/TargetInfo.h b/tslang/include/TypeScript/TargetInfo.h new file mode 100644 index 000000000..62a96c1f6 --- /dev/null +++ b/tslang/include/TypeScript/TargetInfo.h @@ -0,0 +1,56 @@ +#ifndef TYPESCRIPT_TARGETINFO_H_ +#define TYPESCRIPT_TARGETINFO_H_ + +#include "llvm/TargetParser/Triple.h" + +// Everything the compiler needs to know about the target beyond the triple string itself, +// derived once in prepareOptions() and carried on CompileOptions. +// +// Predicates are named for the decision they answer, never for the architecture that currently +// needs them - `usesImageBaseRelativeEH`, not `isX64`. A new target then answers the questions, +// instead of every call site growing another arch comparison. This replaces a hand-maintained +// list of 64-bit arches in opts.cpp that had already rotted. +struct TargetInfo +{ + // Width of a pointer, and of the integer type used for sizes and indices. + unsigned pointerBits = 64; + + // MSVC C++ EH stores the cross-references inside ThrowInfo and CatchableType as + // image-base-relative RVAs on 64-bit Windows, and as absolute pointers on 32-bit x86. Both + // are 4 bytes, so the struct layouts coincide and only the stored value differs: x86 must + // not subtract the image base. See Phase 3. + bool usesImageBaseRelativeEH = true; + + // _CxxThrowException is __stdcall on 32-bit x86, so the emitted symbol carries the @8 + // suffix that the linker asks for. + bool stdcallDecoratesCxxThrow = false; + + // Whether tslang.exe - an x64 process - can execute this target's code in-process. + bool supportsInProcessJit = true; + + static TargetInfo fromTriple(const llvm::Triple &target, const llvm::Triple &host) + { + TargetInfo info; + + info.pointerBits = target.getArchPointerBitWidth(); + if (info.pointerBits == 0) + { + // An unknown arch has no width of its own. Zero would make every size computation + // degenerate, so take the host's - the same assumption the compiler already makes + // when no triple is given at all. + info.pointerBits = host.getArchPointerBitWidth(); + } + + const bool msvc = target.isKnownWindowsMSVCEnvironment(); + const bool x86_32 = target.getArch() == llvm::Triple::x86; + + info.usesImageBaseRelativeEH = msvc && !x86_32; + info.stdcallDecoratesCxxThrow = msvc && x86_32; + info.supportsInProcessJit = + target.getArch() == host.getArch() && target.getOS() == host.getOS(); + + return info; + } +}; + +#endif // TYPESCRIPT_TARGETINFO_H_ diff --git a/tslang/unittests/MLIRGen/CMakeLists.txt b/tslang/unittests/MLIRGen/CMakeLists.txt index 7e5e98ef4..3d41b35a3 100644 --- a/tslang/unittests/MLIRGen/CMakeLists.txt +++ b/tslang/unittests/MLIRGen/CMakeLists.txt @@ -3,6 +3,7 @@ add_mlir_unittest(MLIRGenTests DeclarationPrinter.cpp ExportFilter.cpp TypeHelper.cpp + TargetInfo.cpp ) get_property(dialect_libs GLOBAL PROPERTY MLIR_DIALECT_LIBS) diff --git a/tslang/unittests/MLIRGen/TargetInfo.cpp b/tslang/unittests/MLIRGen/TargetInfo.cpp new file mode 100644 index 000000000..3b3190d26 --- /dev/null +++ b/tslang/unittests/MLIRGen/TargetInfo.cpp @@ -0,0 +1,79 @@ +#include "TypeScript/TargetInfo.h" + +#include "llvm/TargetParser/Triple.h" + +#include "gmock/gmock.h" + +// TargetInfo replaces a hand-maintained list of 64-bit architectures in opts.cpp. That list had +// already rotted - loongarch64 was on it, riscv32 appeared nowhere - so these tests deliberately +// cover arches nobody has built for, to prove the answer comes from the triple rather than from +// somebody remembering to add a line. +namespace +{ + +llvm::Triple t(const char *str) +{ + return llvm::Triple(llvm::Triple::normalize(str)); +} + +const llvm::Triple &hostX64() +{ + static llvm::Triple host = t("x86_64-pc-windows-msvc"); + return host; +} + +TargetInfo infoFor(const char *target) +{ + return TargetInfo::fromTriple(t(target), hostX64()); +} + +TEST(TargetInfoTest, PointerWidthComesFromTheTriple) +{ + EXPECT_EQ(infoFor("x86_64-pc-windows-msvc").pointerBits, 64u); + EXPECT_EQ(infoFor("i686-pc-windows-msvc").pointerBits, 32u); + EXPECT_EQ(infoFor("wasm32-unknown-unknown").pointerBits, 32u); + EXPECT_EQ(infoFor("aarch64-unknown-linux-gnu").pointerBits, 64u); +} + +// The arches the old list got wrong, in both directions. +TEST(TargetInfoTest, PointerWidthIsRightForArchesTheOldListMissed) +{ + EXPECT_EQ(infoFor("riscv32-unknown-elf").pointerBits, 32u); + EXPECT_EQ(infoFor("armv7-unknown-linux-gnueabihf").pointerBits, 32u); + EXPECT_EQ(infoFor("loongarch64-unknown-linux-gnu").pointerBits, 64u); +} + +// An unknown arch has no width of its own; falling back to 0 would make every size computation +// degenerate, so it takes the host's. +TEST(TargetInfoTest, UnknownArchFallsBackToHostWidth) +{ + EXPECT_EQ(infoFor("unknown-unknown-unknown").pointerBits, 64u); +} + +// MSVC C++ EH cross-references are image-base-relative RVAs on 64-bit Windows and absolute +// pointers on 32-bit x86. Non-Windows targets use the Itanium scheme and neither applies. +TEST(TargetInfoTest, ImageBaseRelativeEHIsWin64Only) +{ + EXPECT_TRUE(infoFor("x86_64-pc-windows-msvc").usesImageBaseRelativeEH); + EXPECT_FALSE(infoFor("i686-pc-windows-msvc").usesImageBaseRelativeEH); + EXPECT_FALSE(infoFor("x86_64-unknown-linux-gnu").usesImageBaseRelativeEH); +} + +// _CxxThrowException is __stdcall on 32-bit x86 only, which is what puts the @8 on the symbol. +TEST(TargetInfoTest, StdcallThrowIsWin32X86Only) +{ + EXPECT_TRUE(infoFor("i686-pc-windows-msvc").stdcallDecoratesCxxThrow); + EXPECT_FALSE(infoFor("x86_64-pc-windows-msvc").stdcallDecoratesCxxThrow); + EXPECT_FALSE(infoFor("i686-unknown-linux-gnu").stdcallDecoratesCxxThrow); +} + +// tslang.exe is an x64 process and cannot execute i386 or wasm in-process. +TEST(TargetInfoTest, InProcessJitNeedsAMatchingHost) +{ + EXPECT_TRUE(infoFor("x86_64-pc-windows-msvc").supportsInProcessJit); + EXPECT_FALSE(infoFor("i686-pc-windows-msvc").supportsInProcessJit); + EXPECT_FALSE(infoFor("wasm32-unknown-unknown").supportsInProcessJit); + EXPECT_FALSE(infoFor("x86_64-unknown-linux-gnu").supportsInProcessJit); +} + +} // namespace From ad8060efbdd2af796d57f99db028fd7b3709427e Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 20 Sep 2026 23:20:07 +0100 Subject: [PATCH 05/20] Correct the unittest binary path in the Phase 1 plan MLIRGenTests.exe lands under unittests/MLIRGen/Debug/, not bin/. bin/ exists and holds tslang.exe, which is why the wrong path looked plausible. Found by the Task 1 implementer; fixed here so Tasks 2, 3 and 5 do not each rediscover it. Co-Authored-By: Claude Opus 5 --- .../plans/2026-09-20-32-bit-phase-1-target-width.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md index 762ed7e1f..9ad3ace5d 100644 --- a/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md +++ b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md @@ -48,7 +48,7 @@ cmake --build --preset build-windows-msbuild-2026-debug --target MLIRTypeScriptU Run the unit tests: ```bash -i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-debug/bin/MLIRGenTests.exe --gtest_filter='TargetInfoTest.*' +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-debug/unittests/MLIRGen/Debug/MLIRGenTests.exe --gtest_filter='TargetInfoTest.*' ``` --- @@ -237,7 +237,7 @@ struct TargetInfo ```bash cmake --build --preset build-windows-msbuild-2026-debug --target MLIRTypeScriptUnitTests -i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-debug/bin/MLIRGenTests.exe --gtest_filter='TargetInfoTest.*' +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-debug/unittests/MLIRGen/Debug/MLIRGenTests.exe --gtest_filter='TargetInfoTest.*' ``` Expected: 6 tests, all PASS. @@ -360,7 +360,7 @@ Each is a mechanical `compileOptions.sizeBits` → `compileOptions.sizeBits()`. ```bash cmake --build --preset build-windows-msbuild-2026-debug --target MLIRTypeScriptUnitTests -i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-debug/bin/MLIRGenTests.exe +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-debug/unittests/MLIRGen/Debug/MLIRGenTests.exe ``` Expected: all tests PASS, including the pre-existing `MLIRTypeHelperTest` suite. If any `MLIRTypeHelperTest` test now fails, the width default changed for x64 — that is a regression in this task, not an acceptable diff. @@ -516,7 +516,7 @@ Then add the accessors immediately after `getI64Type()` (line 66): ```bash cmake --build --preset build-windows-msbuild-2026-debug --target MLIRTypeScriptUnitTests -i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-debug/bin/MLIRGenTests.exe --gtest_filter='TargetInfoTest.*' +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-debug/unittests/MLIRGen/Debug/MLIRGenTests.exe --gtest_filter='TargetInfoTest.*' ``` Expected: 8 tests, all PASS. @@ -709,7 +709,7 @@ The comment is the deliverable here as much as any code change. The point of the ```bash cmake --build --preset build-windows-msbuild-2026-debug --target MLIRTypeScriptUnitTests -i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-debug/bin/MLIRGenTests.exe +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-debug/unittests/MLIRGen/Debug/MLIRGenTests.exe ``` Expected: all PASS. From fb540d9fb94d1a4feb53f7740a9a636197fb89aa Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 20 Sep 2026 23:25:19 +0100 Subject: [PATCH 06/20] Diff x64 IR against a stored baseline, not a stashed rebuild Tasks 2 and 5 verified no-x64-change by stashing the work and rebuilding Release to regenerate a 'before' - a second full LLVM link each time, with uncommitted work on the stash while it ran. The release binary predating Phase 1 was still on disk, so the baseline is captured once from it and stored in the plan's workspace. Same check, one rebuild instead of two, and nothing goes on the stash. Co-Authored-By: Claude Opus 5 --- .../2026-09-20-32-bit-phase-1-target-width.md | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md index 9ad3ace5d..c14deb40d 100644 --- a/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md +++ b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md @@ -367,18 +367,19 @@ Expected: all tests PASS, including the pre-existing `MLIRTypeHelperTest` suite. - [ ] **Step 5: Verify x64 output is byte-identical** -This phase must not change 64-bit behavior. Build the compiler and diff its IR against the current one: +This phase must not change 64-bit behavior. + +A pre-change baseline is already stored at `i:/TypeScriptCompiler/.superpowers/sdd/2026-09-20-32-bit-phase-1-target-width/baseline/`, generated with the release +binary as it stood before any Phase 1 change. Diff against it. Do **not** `git stash` and +rebuild to regenerate a baseline - that costs a second full Release link (many minutes) and +risks losing uncommitted work. ```bash +BASE=i:/TypeScriptCompiler/.superpowers/sdd/2026-09-20-32-bit-phase-1-target-width/baseline cmake --build --preset build-windows-msbuild-2026-release -cd "$TMPDIR" && printf 'print("x");\n' > w.ts -i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe \ - --emit=llvm -mm=gc --no-default-lib w.ts && mv w.ll after.ll -git stash && cmake --build --preset build-windows-msbuild-2026-release -i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe \ - --emit=llvm -mm=gc --no-default-lib w.ts && mv w.ll before.ll -git stash pop -diff before.ll after.ll +cd "$BASE" +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe \n --emit=llvm -mm=gc --no-default-lib simple.ts -o after-simple.ll +diff simple.ll after-simple.ll && echo "x64 IR unchanged" ``` Expected: no differences. @@ -718,16 +719,17 @@ Expected: all PASS. Any site correctly classified as target-width emits `i64` on x64 either way, so x64 IR must be byte-identical. A diff here means a site was misclassified. +A pre-change baseline is already stored at `i:/TypeScriptCompiler/.superpowers/sdd/2026-09-20-32-bit-phase-1-target-width/baseline/`, generated with the release +binary as it stood before any Phase 1 change. Diff against it. Do **not** `git stash` and +rebuild to regenerate a baseline - that costs a second full Release link (many minutes) and +risks losing uncommitted work. + ```bash +BASE=i:/TypeScriptCompiler/.superpowers/sdd/2026-09-20-32-bit-phase-1-target-width/baseline cmake --build --preset build-windows-msbuild-2026-release -cd "$TMPDIR" && printf 'class C { x: number; f() { return this.x; } }\nconst c = new C();\nc.x = 1;\nprint(c.f());\n' > a.ts -i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe \ - --emit=llvm -mm=gc --no-default-lib a.ts && mv a.ll after.ll -git stash && cmake --build --preset build-windows-msbuild-2026-release -i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe \ - --emit=llvm -mm=gc --no-default-lib a.ts && mv a.ll before.ll -git stash pop -diff before.ll after.ll +cd "$BASE" +i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe \n --emit=llvm -mm=gc --no-default-lib classes.ts -o after-classes.ll +diff classes.ll after-classes.ll && echo "x64 IR unchanged" ``` Expected: no differences. From c1384ba5ccfa2f4c2a156dae8848a14d63beacdf Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 20 Sep 2026 23:31:00 +0100 Subject: [PATCH 07/20] Take pointer width from the triple, not from an arch list opts.cpp listed 23 architectures it considered 64-bit. The list had already rotted: loongarch64 was on it, riscv32 appeared nowhere. Triple already knows, so ask it. sizeBits stays, as an accessor over TargetInfo, so it cannot drift from the one place the width is decided. Updated every reader of compileOptions.sizeBits to call it (grep found the same ~20 call sites the spec's audit listed, no more). Co-Authored-By: Claude Sonnet 5 --- tslang/include/TypeScript/DataStructs.h | 10 ++++++- .../LowerToLLVM/LLVMCodeHelperBase.h | 2 +- .../LowerToLLVM/LLVMRTTIHelperVCWin32.h | 2 +- .../TypeScript/LowerToLLVM/ThrowLogic.h | 2 +- .../MLIRLogic/MLIRRTTIHelperVCWin32.h | 2 +- .../TypeScript/MLIRLogic/MLIRTypeHelper.h | 10 +++---- tslang/lib/TypeScript/LowerToLLVM.cpp | 6 ++-- tslang/lib/TypeScript/MLIRGenModule.cpp | 2 +- tslang/tslang/opts.cpp | 28 ++----------------- tslang/tslang/transform.cpp | 4 +-- tslang/unittests/MLIRGen/TargetInfo.cpp | 14 ++++++++++ tslang/unittests/MLIRGen/TypeHelper.cpp | 5 +++- 12 files changed, 45 insertions(+), 42 deletions(-) diff --git a/tslang/include/TypeScript/DataStructs.h b/tslang/include/TypeScript/DataStructs.h index 4e558aa14..c029221f8 100644 --- a/tslang/include/TypeScript/DataStructs.h +++ b/tslang/include/TypeScript/DataStructs.h @@ -2,6 +2,7 @@ #define TYPESCRIPT_DATASTRUCT_H_ #include "TypeScript/TypeScriptCompiler/Defines.h" +#include "TypeScript/TargetInfo.h" #include #include @@ -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(targetInfo.pointerBits); + } bool isWasm; bool isWindows; bool isExecutable; diff --git a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h index dbdb0eeb3..b3ecab79a 100644 --- a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h +++ b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h @@ -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) diff --git a/tslang/include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h b/tslang/include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h index f958475e9..f9f91085e 100644 --- a/tslang/include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h +++ b/tslang/include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h @@ -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) diff --git a/tslang/include/TypeScript/LowerToLLVM/ThrowLogic.h b/tslang/include/TypeScript/LowerToLLVM/ThrowLogic.h index 37877d50d..623382396 100644 --- a/tslang/include/TypeScript/LowerToLLVM/ThrowLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/ThrowLogic.h @@ -146,7 +146,7 @@ class ThrowLogic auto allocExceptFuncName = "__cxa_allocate_exception"; - auto sizeType = compileOptions.sizeBits == 32 ? th.getI32Type() : th.getI64Type(); + auto sizeType = compileOptions.sizeBits() == 32 ? th.getI32Type() : th.getI64Type(); auto cxxAllocException = ch.getOrInsertFunction(allocExceptFuncName, th.getFunctionType(i8PtrTy, {sizeType})); diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h b/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h index dbf083480..beba95b0e 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h @@ -140,7 +140,7 @@ class MLIRRTTIHelperVCWin32 // 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) diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h index 1f71c5035..e52163117 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h @@ -1643,12 +1643,12 @@ class MLIRTypeHelper if (typeLeft.isIndex()) { - intTypeLeft = mlir::IntegerType::get(typeLeft.getContext(), compileOptions.sizeBits); + intTypeLeft = mlir::IntegerType::get(typeLeft.getContext(), compileOptions.sizeBits()); } if (typeRight.isIndex()) { - intTypeRight = mlir::IntegerType::get(typeRight.getContext(), compileOptions.sizeBits); + intTypeRight = mlir::IntegerType::get(typeRight.getContext(), compileOptions.sizeBits()); } if (intTypeLeft && intTypeRight) @@ -1863,7 +1863,7 @@ class MLIRTypeHelper auto litType = literalType.getElementType(); if (auto litIntType = dyn_cast(litType)) { - if (isa(dstType) && litIntType.getIntOrFloatBitWidth() <= (unsigned int)compileOptions.sizeBits) + if (isa(dstType) && litIntType.getIntOrFloatBitWidth() <= (unsigned int)compileOptions.sizeBits()) { return true; } @@ -1912,12 +1912,12 @@ class MLIRTypeHelper if (dstType.isIndex()) { - destIntType = mlir::IntegerType::get(dstType.getContext(), compileOptions.sizeBits); + destIntType = mlir::IntegerType::get(dstType.getContext(), compileOptions.sizeBits()); } if (srcType.isIndex()) { - srcIntType = mlir::IntegerType::get(srcType.getContext(), compileOptions.sizeBits); + srcIntType = mlir::IntegerType::get(srcType.getContext(), compileOptions.sizeBits()); } if (destIntType && srcIntType) diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index ec984e0f3..3b3841a0a 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -5247,7 +5247,7 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(getTypeConverter())); auto intPtrType = llvmtch.getIntPtrType(0); - auto negative1 = tsLlvmContext->compileOptions.sizeBits == 32 + auto negative1 = tsLlvmContext->compileOptions.sizeBits() == 32 ? clh.createI32ConstantOf(-1) : clh.createI64ConstantOf(-1); auto methodOrFieldIntPtrValue = rewriter.create(loc, intPtrType, methodOrFieldPtr); @@ -5290,7 +5290,7 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(getTypeConverter())); - auto negative1 = tsLlvmContext->compileOptions.sizeBits == 32 + auto negative1 = tsLlvmContext->compileOptions.sizeBits() == 32 ? clh.createI32ConstantOf(-1) : clh.createI64ConstantOf(-1); auto intPtrType = llvmtch.getIntPtrType(0); @@ -7254,7 +7254,7 @@ void TypeScriptToLLVMLoweringPass::runOnOperation() // doing more complicated lowerings, involving loop region arguments. mlir::DataLayout dl(m); LowerToLLVMOptions options(&getContext(), dl); - if (tsContext.compileOptions.isWasm && tsContext.compileOptions.sizeBits == 32) + if (tsContext.compileOptions.isWasm && tsContext.compileOptions.sizeBits() == 32) { options.dataLayout = llvm::DataLayout("e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-f128:64-n32:64-S128-ni:1:10:20"); diff --git a/tslang/lib/TypeScript/MLIRGenModule.cpp b/tslang/lib/TypeScript/MLIRGenModule.cpp index d5558bb73..776386155 100644 --- a/tslang/lib/TypeScript/MLIRGenModule.cpp +++ b/tslang/lib/TypeScript/MLIRGenModule.cpp @@ -260,7 +260,7 @@ namespace mlirgen // DataLayout for IndexType // TODO: seems u need to do it on LLVM level, as LLVMTypeHelper knows size of index - auto indexSize = mlir::DataLayoutEntryAttr::get(builder.getIndexType(), builder.getI32IntegerAttr(compileOptions.sizeBits)); + auto indexSize = mlir::DataLayoutEntryAttr::get(builder.getIndexType(), builder.getI32IntegerAttr(compileOptions.sizeBits())); theModule->setAttr("dlti.dl_spec", mlir::DataLayoutSpecAttr::get(builder.getContext(), {indexSize})); } diff --git a/tslang/tslang/opts.cpp b/tslang/tslang/opts.cpp index 983fb225c..0023d2601 100644 --- a/tslang/tslang/opts.cpp +++ b/tslang/tslang/opts.cpp @@ -8,6 +8,7 @@ #include "TypeScript/TypeScriptCompiler/Defines.h" #include "TypeScript/DataStructs.h" +#include "TypeScript/TargetInfo.h" #define DEBUG_TYPE "tslang" @@ -54,7 +55,8 @@ CompileOptions prepareOptions() compileOptions.moduleTargetTriple = moduleTargetTriple; compileOptions.isWindows = TheTriple.isKnownWindowsMSVCEnvironment(); compileOptions.isWasm = TheTriple.getArch() == llvm::Triple::wasm64 || TheTriple.getArch() == llvm::Triple::wasm32; - compileOptions.sizeBits = 32; + compileOptions.targetInfo = TargetInfo::fromTriple( + TheTriple, llvm::Triple(llvm::sys::getDefaultTargetTriple())); compileOptions.isExecutable = emitAction == Action::BuildExe; compileOptions.isDLL = emitAction == Action::BuildDll; // A DLL never gets one, whatever was asked for: its root initialization runs from the @@ -64,30 +66,6 @@ CompileOptions prepareOptions() compileOptions.appendGCtorsToMethod = appendGCtorsToMethod.getValue(); compileOptions.strictNullChecks = strictNullChecks.getValue(); compileOptions.enableFastMath = enableFastMath.getValue(); - if ( - TheTriple.getArch() == llvm::Triple::UnknownArch - || TheTriple.getArch() == llvm::Triple::aarch64 // AArch64 (little endian): aarch64 - || TheTriple.getArch() == llvm::Triple::aarch64_be // AArch64 (big endian): aarch64_be - || TheTriple.getArch() == llvm::Triple::aarch64_32 // AArch64 (little endian) ILP32: aarch64_32 - || TheTriple.getArch() == llvm::Triple::bpfel // eBPF or extended BPF or 64-bit BPF (little endian) - || TheTriple.getArch() == llvm::Triple::bpfeb // eBPF or extended BPF or 64-bit BPF (big endian) - || TheTriple.getArch() == llvm::Triple::loongarch64 // LoongArch (64-bit): loongarch64 - || TheTriple.getArch() == llvm::Triple::mips64 // MIPS64: mips64, mips64r6, mipsn32, mipsn32r6 - || TheTriple.getArch() == llvm::Triple::mips64el // MIPS64EL: mips64el, mips64r6el, mipsn32el, mipsn32r6el - || TheTriple.getArch() == llvm::Triple::ppc64 // PPC64: powerpc64, ppu - || TheTriple.getArch() == llvm::Triple::ppc64le // PPC64LE: powerpc64le - || TheTriple.getArch() == llvm::Triple::riscv64 // RISC-V (64-bit): riscv64 - || TheTriple.getArch() == llvm::Triple::x86_64 // X86-64: amd64, x86_64 - || TheTriple.getArch() == llvm::Triple::nvptx64 // NVPTX: 64-bit - || TheTriple.getArch() == llvm::Triple::amdil64 // AMDIL with 64-bit pointers - || TheTriple.getArch() == llvm::Triple::hsail64 // AMD HSAIL with 64-bit pointers - || TheTriple.getArch() == llvm::Triple::spir64 // SPIR: standard portable IR for OpenCL 64-bit version - || TheTriple.getArch() == llvm::Triple::spirv64 // SPIR-V with 64-bit pointers - || TheTriple.getArch() == llvm::Triple::wasm64 // WebAssembly with 64-bit pointers - || TheTriple.getArch() == llvm::Triple::renderscript64 // 64-bit RenderScript - ) { - compileOptions.sizeBits = 64; - } if (!outputFilename.empty()) { diff --git a/tslang/tslang/transform.cpp b/tslang/tslang/transform.cpp index 50640d166..a2e009347 100644 --- a/tslang/tslang/transform.cpp +++ b/tslang/tslang/transform.cpp @@ -344,8 +344,8 @@ std::function makeCustomPassesWithOptimizingTransfo if (compileOptions.isWasm) { - mpm.addPass(ts::MemAllocFixPass(compileOptions.sizeBits)); - mpm.addPass(ts::AliasPass(true, compileOptions.sizeBits)); + mpm.addPass(ts::MemAllocFixPass(compileOptions.sizeBits())); + mpm.addPass(ts::AliasPass(true, compileOptions.sizeBits())); } if (*ol == llvm::OptimizationLevel::O0) diff --git a/tslang/unittests/MLIRGen/TargetInfo.cpp b/tslang/unittests/MLIRGen/TargetInfo.cpp index 3b3190d26..3a34c50b0 100644 --- a/tslang/unittests/MLIRGen/TargetInfo.cpp +++ b/tslang/unittests/MLIRGen/TargetInfo.cpp @@ -1,4 +1,5 @@ #include "TypeScript/TargetInfo.h" +#include "TypeScript/DataStructs.h" #include "llvm/TargetParser/Triple.h" @@ -76,4 +77,17 @@ TEST(TargetInfoTest, InProcessJitNeedsAMatchingHost) EXPECT_FALSE(infoFor("x86_64-unknown-linux-gnu").supportsInProcessJit); } +// sizeBits is what ~20 existing call sites read. It must keep answering, and it must now answer +// from TargetInfo rather than from a separate field that could drift away from it. +TEST(TargetInfoTest, CompileOptionsSizeBitsFollowsTargetInfo) +{ + CompileOptions options; + + options.targetInfo = infoFor("i686-pc-windows-msvc"); + EXPECT_EQ(options.sizeBits(), 32); + + options.targetInfo = infoFor("x86_64-pc-windows-msvc"); + EXPECT_EQ(options.sizeBits(), 64); +} + } // namespace diff --git a/tslang/unittests/MLIRGen/TypeHelper.cpp b/tslang/unittests/MLIRGen/TypeHelper.cpp index 06da12124..f2ff7cce2 100644 --- a/tslang/unittests/MLIRGen/TypeHelper.cpp +++ b/tslang/unittests/MLIRGen/TypeHelper.cpp @@ -11,6 +11,8 @@ #include "mlir/IR/MLIRContext.h" +#include "llvm/TargetParser/Triple.h" + #include "gmock/gmock.h" #define DEBUG_TYPE "test" @@ -33,7 +35,8 @@ struct MLIRTypeHelperTest : public testing::Test void SetUp() override { context.getOrLoadDialect(); - compileOptions.sizeBits = 64; + compileOptions.targetInfo = TargetInfo::fromTriple( + llvm::Triple("x86_64-pc-windows-msvc"), llvm::Triple("x86_64-pc-windows-msvc")); } mlir::MLIRContext *getContext() From 15729821607ec4541e2c86e2b97d4f41c766dd9c Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 20 Sep 2026 23:36:18 +0100 Subject: [PATCH 08/20] Correct why the arch list was wrong I wrote that the list had rotted because loongarch64 was on it and riscv32 was absent. Both are correct: the list marks arches as 64-bit and defaults to 32, so loongarch64 belongs on it and riscv32 belongs off it. Neither was evidence of anything. The real defect, found by enumerating all 20 entries against LLVM's width table: aarch64_32 was marked 64-bit. ARM64_32 is ILP32 and its pointers are 32 bits. Replacing the list with getArchPointerBitWidth() fixes that. Co-Authored-By: Claude Opus 5 --- .../plans/2026-09-20-32-bit-phase-1-target-width.md | 8 ++++---- .../specs/2026-09-20-32-bit-compilation-design.md | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md index c14deb40d..7479a1458 100644 --- a/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md +++ b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md @@ -76,9 +76,9 @@ Create `unittests/MLIRGen/TargetInfo.cpp`: #include "gmock/gmock.h" // TargetInfo replaces a hand-maintained list of 64-bit architectures in opts.cpp. That list had -// already rotted - loongarch64 was on it, riscv32 appeared nowhere - so these tests deliberately -// cover arches nobody has built for, to prove the answer comes from the triple rather than from -// somebody remembering to add a line. +// already gone wrong: it marks aarch64_32 as 64-bit, but ARM64_32 is an ILP32 target whose +// pointers are 32 bits. These tests deliberately cover arches nobody has built for, to prove +// the answer comes from the triple rather than from somebody remembering to add a line. namespace { @@ -391,7 +391,7 @@ git add tslang/include/TypeScript/DataStructs.h tslang/tslang/opts.cpp tslang/un git commit -m "Take pointer width from the triple, not from an arch list opts.cpp listed 23 architectures it considered 64-bit. The list had -already rotted: loongarch64 was on it, riscv32 appeared nowhere. Triple +wrong: it marked aarch64_32 as 64-bit, but ARM64_32 has 32-bit pointers. Triple already knows, so ask it. sizeBits stays, as an accessor over TargetInfo, so it cannot drift from 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 6390d14ad..19ea47643 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 @@ -77,8 +77,8 @@ it. ### Replacing the arch list `tslang/opts.cpp` currently derives `sizeBits` from a hand-maintained list of -64-bit architectures. The list has already rotted: `loongarch64` is on it, -`riscv32` appears nowhere. It is replaced by +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". From 9f931eaf5a0e576f21f918150f6445ba2ce2904e Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 20 Sep 2026 23:39:30 +0100 Subject: [PATCH 09/20] Give TypeHelper target-width accessors getSizeType() is for things that are as wide as a pointer; getI64Type() stays for things that are 64-bit whatever the target is. Keeping both means the audit in the next commit can record which each site meant. The options are optional because 152 sites construct TypeHelper and almost none need a width. getSizeType() asserts rather than guessing. Co-Authored-By: Claude Sonnet 5 --- .../TypeScript/LowerToLLVM/TypeHelper.h | 32 +++++++++++++++++++ tslang/unittests/MLIRGen/TargetInfo.cpp | 30 +++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/tslang/include/TypeScript/LowerToLLVM/TypeHelper.h b/tslang/include/TypeScript/LowerToLLVM/TypeHelper.h index 437c41e51..f705152e6 100644 --- a/tslang/include/TypeScript/LowerToLLVM/TypeHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/TypeHelper.h @@ -2,6 +2,7 @@ #define MLIR_TYPESCRIPT_LOWERTOLLVMLOGIC_TYPEHELPER_H_ #include "TypeScript/Config.h" +#include "TypeScript/DataStructs.h" #include "TypeScript/Defines.h" #include "TypeScript/Passes.h" #include "TypeScript/TypeScriptDialect.h" @@ -20,6 +21,12 @@ class TypeHelper { MLIRContext *context; + // Optional on purpose. TypeHelper is constructed at ~152 sites, nearly all of which only ask + // for width-independent types (getI8Type, getPtrType, getVoidType). Threading CompileOptions + // through all of them would be a large diff for no benefit, so only the sites that ask for a + // target width pass it - and getSizeType() asserts rather than guessing when they have not. + const CompileOptions *compileOptions = nullptr; + public: TypeHelper(OpBuilder &rewriter) : context(rewriter.getContext()) { @@ -29,6 +36,16 @@ class TypeHelper { } + TypeHelper(OpBuilder &rewriter, const CompileOptions &compileOptions) + : context(rewriter.getContext()), compileOptions(&compileOptions) + { + } + + TypeHelper(MLIRContext *context, const CompileOptions &compileOptions) + : context(context), compileOptions(&compileOptions) + { + } + mlir::Type getBooleanType() { return mlir_ts::BooleanType::get(context); @@ -64,6 +81,21 @@ class TypeHelper return mlir::IntegerType::get(context, 64); } + // An integer as wide as the target's pointer: object sizes, byte counts, GEP indices. + // Distinct from getI64Type(), which is for values that are 64-bit whatever the target is - + // the language's own 64-bit integers and runtime ABI parameters declared int64_t. + mlir::Type getSizeType() + { + assert(compileOptions && "getSizeType() needs the target: construct TypeHelper with CompileOptions"); + return mlir::IntegerType::get(context, compileOptions->targetInfo.pointerBits); + } + + // The integer a pointer converts to under ptrtoint on this target. + mlir::Type getPointerIntType() + { + return getSizeType(); + } + mlir::Type getF32Type() { return Float32Type::get(context); diff --git a/tslang/unittests/MLIRGen/TargetInfo.cpp b/tslang/unittests/MLIRGen/TargetInfo.cpp index 3a34c50b0..f96eae68c 100644 --- a/tslang/unittests/MLIRGen/TargetInfo.cpp +++ b/tslang/unittests/MLIRGen/TargetInfo.cpp @@ -1,8 +1,11 @@ #include "TypeScript/TargetInfo.h" #include "TypeScript/DataStructs.h" +#include "TypeScript/LowerToLLVM/TypeHelper.h" #include "llvm/TargetParser/Triple.h" +#include "mlir/IR/MLIRContext.h" + #include "gmock/gmock.h" // TargetInfo replaces a hand-maintained list of 64-bit architectures in opts.cpp. That list had @@ -90,4 +93,31 @@ TEST(TargetInfoTest, CompileOptionsSizeBitsFollowsTargetInfo) EXPECT_EQ(options.sizeBits(), 64); } +// TypeHelper is constructed at 152 sites, nearly all of which only ask for width-independent +// types. Rather than thread CompileOptions through all of them, the options are optional and +// only the sites that need a width pass them - so getSizeType() has to answer from the target, +// and has to refuse rather than guess when nobody supplied one. +TEST(TargetInfoTest, TypeHelperSizeTypeFollowsTheTarget) +{ + mlir::MLIRContext context; + + CompileOptions options32; + options32.targetInfo = infoFor("i686-pc-windows-msvc"); + EXPECT_EQ(::typescript::TypeHelper(&context, options32).getSizeType(), + mlir::IntegerType::get(&context, 32)); + + CompileOptions options64; + options64.targetInfo = infoFor("x86_64-pc-windows-msvc"); + EXPECT_EQ(::typescript::TypeHelper(&context, options64).getSizeType(), + mlir::IntegerType::get(&context, 64)); +} + +// ARM64_32 (watchOS) is the arch the old hand-written list in opts.cpp got wrong: it listed +// aarch64_32 among the 64-bit arches, but ILP32 means 32-bit pointers. Deriving the width from +// the triple fixes that, and this test is what keeps it fixed. +TEST(TargetInfoTest, Arm64_32IsAnIlp32Target) +{ + EXPECT_EQ(infoFor("arm64_32-apple-watchos").pointerBits, 32u); +} + } // namespace From 866d15be1c294f7764d1514aa06f80ff77f3d81a Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 20 Sep 2026 23:49:25 +0100 Subject: [PATCH 10/20] Fail identifiably when getSizeType() has no target in Release assert() alone is a no-op under NDEBUG, and this project's CI builds Release, so a missing CompileOptions was guessing a width via a null dereference with no message naming the function or the cause. Keep the assert for fast local diagnosis, add report_fatal_error so the failure is identifiable in every build configuration. Also record in getPointerIntType()'s comment why it stays a separate name from getSizeType() despite currently just delegating to it: the call-site audit needs to tell the two concepts apart. Co-Authored-By: Claude Sonnet 5 --- .../include/TypeScript/LowerToLLVM/TypeHelper.h | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tslang/include/TypeScript/LowerToLLVM/TypeHelper.h b/tslang/include/TypeScript/LowerToLLVM/TypeHelper.h index f705152e6..3c643f672 100644 --- a/tslang/include/TypeScript/LowerToLLVM/TypeHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/TypeHelper.h @@ -11,6 +11,8 @@ #include "mlir/IR/PatternMatch.h" #include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "llvm/Support/ErrorHandling.h" + using namespace mlir; namespace mlir_ts = mlir::typescript; @@ -87,10 +89,22 @@ class TypeHelper mlir::Type getSizeType() { assert(compileOptions && "getSizeType() needs the target: construct TypeHelper with CompileOptions"); + if (!compileOptions) + { + // assert() alone is a no-op under NDEBUG (this project's CI builds Release), which + // would otherwise turn a missing target into an unattributed null-deref crash. Fail + // identifiably in every build configuration instead of guessing a width. + llvm::report_fatal_error("getSizeType() needs the target: construct TypeHelper with CompileOptions"); + } + return mlir::IntegerType::get(context, compileOptions->targetInfo.pointerBits); } - // The integer a pointer converts to under ptrtoint on this target. + // The integer a pointer converts to under ptrtoint on this target. Kept as a separate name + // from getSizeType(), even though it currently just delegates, so call sites record which + // concept they actually meant - the audit of existing TypeHelper call sites can tell "this + // is a size/byte-count" from "this is a pointer-to-int conversion" instead of both collapsing + // into one ambiguously-named accessor. mlir::Type getPointerIntType() { return getSizeType(); From 9ba4cf373b23bc901c710036b2dd1eaecc1b3033 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 20 Sep 2026 23:57:41 +0100 Subject: [PATCH 11/20] Set the data layout on the emitted module The layout was only set in obj.cpp, from the TargetMachine, which is after --emit=llvm has printed. So IR emitted for a 32-bit triple carried LLVM's default 64-bit layout, and anything reading sizes between MLIRGen and obj.cpp was guessing. Co-Authored-By: Claude Sonnet 5 --- tslang/lib/TypeScript/MLIRGenModule.cpp | 28 +++++++++++++++++++++ tslang/test/check-datalayout.sh | 33 +++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 tslang/test/check-datalayout.sh diff --git a/tslang/lib/TypeScript/MLIRGenModule.cpp b/tslang/lib/TypeScript/MLIRGenModule.cpp index 776386155..485704cd1 100644 --- a/tslang/lib/TypeScript/MLIRGenModule.cpp +++ b/tslang/lib/TypeScript/MLIRGenModule.cpp @@ -1,5 +1,13 @@ // Module, discovery, include/import driver methods of MLIRGenImpl (see MLIRGenImpl.h). +// Included first, before MLIRGenImpl.h: MLIRGenImpl.h transitively pulls in ts-new-parser's +// config.h and MLIRGenContextDefines.h, which #define single-letter macros (S(x), V(x), ...) +// for terse AST/MLIR construction. llvm/Support/CommandLine.h and FormattedStream.h, +// transitively included by the two headers below, use those same letters as ordinary +// identifiers (parameter and member names), so those macros corrupt them if already active. +#include "llvm/MC/TargetRegistry.h" +#include "llvm/Target/TargetMachine.h" + #include "TypeScript/ObjDumper.h" #include "MLIRGenImpl.h" @@ -262,6 +270,26 @@ namespace mlirgen // TODO: seems u need to do it on LLVM level, as LLVMTypeHelper knows size of index auto indexSize = mlir::DataLayoutEntryAttr::get(builder.getIndexType(), builder.getI32IntegerAttr(compileOptions.sizeBits())); theModule->setAttr("dlti.dl_spec", mlir::DataLayoutSpecAttr::get(builder.getContext(), {indexSize})); + + // The data layout is otherwise only set in obj.cpp, from the TargetMachine, which is + // after --emit=llvm has already printed the module. Setting it here makes the emitted + // IR self-describing: a 32-bit triple no longer prints alongside LLVM's default + // 64-bit layout. Derived from the triple via LLVM's own target registry so it matches + // exactly what obj.cpp will later set, rather than being a second hand-built string. + std::string errorMessage; + if (auto *target = llvm::TargetRegistry::lookupTarget( + compileOptions.moduleTargetTriple, errorMessage)) + { + std::unique_ptr machine(target->createTargetMachine( + llvm::Triple(compileOptions.moduleTargetTriple), "generic", "", + llvm::TargetOptions(), std::nullopt)); + if (machine) + { + theModule->setAttr( + mlir::LLVM::LLVMDialect::getDataLayoutAttrName(), + builder.getStringAttr(machine->createDataLayout().getStringRepresentation())); + } + } } builder.setInsertionPointToStart(theModule.getBody()); diff --git a/tslang/test/check-datalayout.sh b/tslang/test/check-datalayout.sh new file mode 100644 index 000000000..4874acb37 --- /dev/null +++ b/tslang/test/check-datalayout.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Phase 1 gate: emitted IR must name a data layout that matches its triple, so that --emit=llvm +# output is self-describing rather than carrying LLVM's default layout under a 32-bit triple. +set -u +TSLANG="${1:?usage: check-datalayout.sh }" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +printf 'print("x");\n' > "$work/dl.ts" + +fail=0 +check() { + local triple="$1" expect="$2" + "$TSLANG" --emit=llvm -mm=none --no-default-lib -mtriple="$triple" "$work/dl.ts" \ + -o "$work/dl.ll" >/dev/null 2>&1 + local got + got="$(grep -m1 '^target datalayout' "$work/dl.ll" || true)" + if [ -z "$got" ]; then + echo "FAIL $triple: no 'target datalayout' in emitted IR" + fail=1 + elif ! printf '%s' "$got" | grep -q -- "$expect"; then + echo "FAIL $triple: expected a layout containing '$expect', got: $got" + fail=1 + else + echo "ok $triple" + fi +} + +# p:32: and p:64: are the pointer-size entries; they are what this phase is about. +check "i686-pc-windows-msvc" "p:32:" +check "wasm32-unknown-unknown" "p:32:" +check "x86_64-pc-windows-msvc" "-p270:" # x86-64 layouts carry the AS270/271/272 entries + +exit "$fail" From 97adcbf1b32289fbace9a04564adff8d506bb3ab Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 21 Sep 2026 00:08:32 +0100 Subject: [PATCH 12/20] Make unresolvable-triple datalayout lookup fatal, not silent Task 4 review, round 1. If TargetRegistry::lookupTarget or createTargetMachine failed, the module was emitted anyway with a target triple but no datalayout -- exactly the bug this phase exists to fix, and reachable only from --emit=llvm since dump.cpp never consults the target registry itself. Both failure modes now emit a diagnostic naming the triple and fail the codegen, matching how obj.cpp treats the same lookup. Also: use the non-deprecated lookupTarget(const Triple&, std::string&) overload instead of the StringRef one slated for removal after LLVM 22 branches, and qualify the comment's claim that the derived layout matches obj.cpp's -- true for x86, not guaranteed once obj.cpp threads a -target-abi into TargetOptions for ABI-sensitive backends. Added a fourth case to check-datalayout.sh: a garbage triple must now fail with a non-zero exit and an error naming the triple. Co-Authored-By: Claude Sonnet 5 --- tslang/lib/TypeScript/MLIRGenModule.cpp | 43 +++++++++++++++++-------- tslang/test/check-datalayout.sh | 22 +++++++++++++ 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/tslang/lib/TypeScript/MLIRGenModule.cpp b/tslang/lib/TypeScript/MLIRGenModule.cpp index 485704cd1..31f87ddbe 100644 --- a/tslang/lib/TypeScript/MLIRGenModule.cpp +++ b/tslang/lib/TypeScript/MLIRGenModule.cpp @@ -274,22 +274,39 @@ namespace mlirgen // The data layout is otherwise only set in obj.cpp, from the TargetMachine, which is // after --emit=llvm has already printed the module. Setting it here makes the emitted // IR self-describing: a 32-bit triple no longer prints alongside LLVM's default - // 64-bit layout. Derived from the triple via LLVM's own target registry so it matches - // exactly what obj.cpp will later set, rather than being a second hand-built string. + // 64-bit layout. Derived from the triple via LLVM's own target registry, using a + // default TargetOptions, so it matches what obj.cpp will later set for the x86 + // targets this phase cares about; on an ABI-name-sensitive backend (ARM, Mips, + // PowerPC, RISCV) obj.cpp additionally threads a `-target-abi` into TargetOptions, + // which could change the layout there but not here. + // + // --emit=llvm (dump.cpp:dumpLLVMIR) never calls lookupTarget itself, so this is the + // only place on that path that validates the triple. A silently-swallowed failure + // here would reproduce the exact bug this task fixes: IR naming a triple but carrying + // the wrong (default) layout. So treat both failure modes as fatal, consistent with + // obj.cpp's handling of the same lookup. std::string errorMessage; - if (auto *target = llvm::TargetRegistry::lookupTarget( - compileOptions.moduleTargetTriple, errorMessage)) + llvm::Triple targetTriple(compileOptions.moduleTargetTriple); + auto *target = llvm::TargetRegistry::lookupTarget(targetTriple, errorMessage); + if (!target) { - std::unique_ptr machine(target->createTargetMachine( - llvm::Triple(compileOptions.moduleTargetTriple), "generic", "", - llvm::TargetOptions(), std::nullopt)); - if (machine) - { - theModule->setAttr( - mlir::LLVM::LLVMDialect::getDataLayoutAttrName(), - builder.getStringAttr(machine->createDataLayout().getStringRepresentation())); - } + emitError(location, "unable to find target for triple '") + << compileOptions.moduleTargetTriple << "': " << errorMessage; + return mlir::failure(); + } + + std::unique_ptr machine(target->createTargetMachine( + targetTriple, "generic", "", llvm::TargetOptions(), std::nullopt)); + if (!machine) + { + emitError(location, "unable to create target machine for triple '") + << compileOptions.moduleTargetTriple << "'"; + return mlir::failure(); } + + theModule->setAttr( + mlir::LLVM::LLVMDialect::getDataLayoutAttrName(), + builder.getStringAttr(machine->createDataLayout().getStringRepresentation())); } builder.setInsertionPointToStart(theModule.getBody()); diff --git a/tslang/test/check-datalayout.sh b/tslang/test/check-datalayout.sh index 4874acb37..d668553d5 100644 --- a/tslang/test/check-datalayout.sh +++ b/tslang/test/check-datalayout.sh @@ -30,4 +30,26 @@ check "i686-pc-windows-msvc" "p:32:" check "wasm32-unknown-unknown" "p:32:" check "x86_64-pc-windows-msvc" "-p270:" # x86-64 layouts carry the AS270/271/272 entries +# A triple the target registry cannot resolve must be a hard, diagnosed failure rather than +# silently emitted IR that names the triple but carries no (or the wrong default) datalayout -- +# that's exactly the bug this phase exists to fix. +check_bad_triple() { + local triple="$1" + local out err status + err="$("$TSLANG" --emit=llvm -mm=none --no-default-lib -mtriple="$triple" "$work/dl.ts" \ + -o "$work/bad.ll" 2>&1 >/dev/null)" + status=$? + if [ "$status" -eq 0 ]; then + echo "FAIL $triple: expected a non-zero exit for an unresolvable triple, got 0" + fail=1 + elif ! printf '%s' "$err" | grep -q -- "$triple"; then + echo "FAIL $triple: expected the error to name the triple, got: $err" + fail=1 + else + echo "ok $triple (rejected as expected)" + fi +} + +check_bad_triple "not-a-real-triple" + exit "$fail" From 32754e4399bfce17aa40a4d43bf844aabc142739 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 21 Sep 2026 00:14:15 +0100 Subject: [PATCH 13/20] Pin the audit to a verified list of 18 call sites A grep for getI64Type returns 21 hits outside the reserved RTTI helpers, but three are not call sites: two definitions and one comment mentioning the name. The plan's count drifted twice for this reason. The list is now generated once, with exclusions applied, and the task reads it instead of re-deriving it. Co-Authored-By: Claude Opus 5 --- .../plans/2026-09-20-32-bit-phase-1-target-width.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md index 7479a1458..90ffdee1c 100644 --- a/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md +++ b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md @@ -662,7 +662,7 @@ Co-Authored-By: Claude Opus 5 " --- -### Task 5: Audit and classify the 19 non-EH `getI64Type()` sites +### Task 5: Audit and classify the 18 non-EH `getI64Type()` call sites **Files:** - Modify (audit, some will change): `lib/TypeScript/GCPass.cpp` (1), `lib/TypeScript/LowerToLLVM.cpp` (2), `lib/TypeScript/MLIRGenAccessCall.cpp` (1), `lib/TypeScript/MLIRGenClasses.cpp` (4), `lib/TypeScript/MLIRGenExpressions.cpp` (1), `lib/TypeScript/MLIRGenImpl.h` (1), `lib/TypeScript/MLIRGenInterfaces.cpp` (2), `include/TypeScript/LowerToLLVM/CastLogicHelper.h` (2), `include/TypeScript/LowerToLLVM/CodeLogicHelper.h` (1), `include/TypeScript/LowerToLLVM/ConvertLogic.h` (1), `include/TypeScript/LowerToLLVM/ThrowLogic.h` (1), `include/TypeScript/LowerToLLVM/TypeHelper.h` (1, the definition), `include/TypeScript/MLIRLogic/MLIRTypeHelper.h` (2) @@ -683,7 +683,11 @@ grep -rn 'getI64Type()' lib include \ wc -l /tmp/i64-audit.txt ``` -Expected: 20 lines. **One of them is not a call site:** `include/TypeScript/LowerToLLVM/TypeHelper.h:62` is the *definition* of `getI64Type()` itself. Leave it alone. That leaves **19 call sites** to classify. +**Do not derive the list yourself — it is already written for you** at +`i:/TypeScriptCompiler/.superpowers/sdd/2026-09-20-32-bit-phase-1-target-width/task-5-sites.md`, +with the exclusions already applied. A raw grep returns 21 hits, of which three are not +call sites at all (two definitions of `getI64Type` and one comment mentioning it), so a +count taken from grep will mislead you. **There are 18 call sites.** - [ ] **Step 2: Classify each site** @@ -751,7 +755,7 @@ Expected: the same result as `main` — 2765/2765 as of 2026-09-16. Any new fail git add -A tslang/lib tslang/include git commit -m "Classify every i64 in lowering as fixed-width or target-width -Each of the 19 non-EH getI64Type() call sites now says which it meant. Most +Each of the 18 non-EH getI64Type() call sites now says which it meant. Most are genuinely 64-bit - the async runtime's parameters are int64_t, not size_t, so i64 is right on x86 too. The rest are pointer and size arithmetic and now ask the target. @@ -861,5 +865,5 @@ All of the following, before Phase 2 is planned: - [ ] `ctest` matches `main` — 2765/2765. - [ ] x64 `--emit=llvm` output is byte-identical to `main` for a class-and-method sample. - [ ] `--emit=jit -mtriple=i686-pc-windows-msvc` refuses with a message naming both triples. -- [ ] Every one of the 19 non-EH `getI64Type()` call sites carries a classification comment (the 20th grep hit is the definition in `TypeHelper.h:62`). +- [ ] Every one of the 18 non-EH `getI64Type()` call sites listed in `task-5-sites.md` carries a classification comment. - [ ] The 18 EH `getI64Type()` sites are untouched. The two RTTI helper files are *not* otherwise frozen: Task 2 legitimately changes `compileOptions.sizeBits` to `sizeBits()` at `LLVMRTTIHelperVCWin32.h:118` and `MLIRRTTIHelperVCWin32.h:143`. Verify with `git diff -- | grep getI64Type`, which must be empty — not with `git diff --stat`, which will legitimately be non-empty. From 6e1073c026ac1680d96190d322bfe4ddd0c35fb0 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 21 Sep 2026 00:26:32 +0100 Subject: [PATCH 14/20] Classify every i64 in lowering as fixed-width or target-width Each of the 18 non-EH getI64Type() call sites now says which it meant. Thirteen are pointer and size arithmetic and now ask the target; five are genuinely 64-bit and keep i64 with a comment saying why. The one that needed looking up rather than guessing was Boehm's GC_descr: it is a typedef of GC_word, which gc.h defines as the integer the size of void *, not a fixed 64 bits. So the whole typed-GC descriptor group moves together - the global, the bitmap method's return type, the GCMakeDescriptorOp result and the two extern declarations - or the op and its callee would disagree on width. The interface -1 slot sentinels move for the same reason: the lowering that tests them already builds its -1 at compileOptions.sizeBits(). Left alone: the bigint literal type and the numeric promotion ladder (the language's own 64-bit integer), _i64toa's first parameter (long long, not size_t), and the createI64ConstantOf/getI64AttrValue helpers, whose width is their contract. x64 IR is unchanged, which is the check that the classification is right: a target-width site emits i64 on x64 either way. Co-Authored-By: Claude Opus 5 --- .../TypeScript/LowerToLLVM/CastLogicHelper.h | 13 ++++++++---- .../TypeScript/LowerToLLVM/CodeLogicHelper.h | 3 +++ .../TypeScript/LowerToLLVM/ConvertLogic.h | 3 +++ .../TypeScript/LowerToLLVM/ThrowLogic.h | 6 ++++-- .../TypeScript/MLIRLogic/MLIRTypeHelper.h | 3 +++ tslang/lib/TypeScript/GCPass.cpp | 5 +++-- tslang/lib/TypeScript/LowerToLLVM.cpp | 13 ++++++++---- tslang/lib/TypeScript/MLIRGenAccessCall.cpp | 4 +++- tslang/lib/TypeScript/MLIRGenClasses.cpp | 21 ++++++++++++++----- tslang/lib/TypeScript/MLIRGenExpressions.cpp | 2 ++ tslang/lib/TypeScript/MLIRGenImpl.h | 2 ++ tslang/lib/TypeScript/MLIRGenInterfaces.cpp | 16 ++++++++++---- 12 files changed, 69 insertions(+), 22 deletions(-) diff --git a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h index 029c95a4d..5c2c98633 100644 --- a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h @@ -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) { } @@ -712,8 +712,11 @@ class CastLogicHelper if (isa(inLLVMType) && isBool(resLLVMType)) { - auto intVal = rewriter.create(loc, th.getI64Type(), in); - return rewriter.create(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(loc, ptrIntType, in); + return rewriter.create(loc, arith::CmpIPredicate::ne, intVal, clh.createIConstantOf(ptrIntType.getIntOrFloatBitWidth(), 0)); } if (isa(inLLVMType) && isInt(resLLVMType)) @@ -723,7 +726,9 @@ class CastLogicHelper if (isa(inLLVMType) && isFloat(resLLVMType)) { - auto intVal = rewriter.create(loc, th.getI64Type(), in); + // target-width: a ptrtoint of a real pointer; the integer handed to sitofp is as wide as + // the target's pointer. + auto intVal = rewriter.create(loc, th.getPointerIntType(), in); return rewriter.create(loc, resLLVMType, intVal); } diff --git a/tslang/include/TypeScript/LowerToLLVM/CodeLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/CodeLogicHelper.h index e6ed35354..cc83a9a04 100644 --- a/tslang/include/TypeScript/LowerToLLVM/CodeLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/CodeLogicHelper.h @@ -72,6 +72,9 @@ class CodeLogicHelper return rewriter.create(loc, rewriter.getIntegerType(32), rewriter.getIntegerAttr(rewriter.getI32Type(), value)); } + // 64-bit whatever the target: this helper's whole contract is "give me an i64 constant" - the + // width is in its name. Call sites that want a pointer-wide constant use createIConstantOf() + // with TypeHelper::getSizeType()'s width instead. mlir::Value createI64ConstantOf(int64_t value) { return rewriter.create(loc, rewriter.getIntegerType(64), rewriter.getIntegerAttr(rewriter.getI64Type(), value)); diff --git a/tslang/include/TypeScript/LowerToLLVM/ConvertLogic.h b/tslang/include/TypeScript/LowerToLLVM/ConvertLogic.h index f6b39e9bb..5d84adc21 100644 --- a/tslang/include/TypeScript/LowerToLLVM/ConvertLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/ConvertLogic.h @@ -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{rewriter.getI64Type(), th.getPtrType(), rewriter.getI32Type()}, true)); diff --git a/tslang/include/TypeScript/LowerToLLVM/ThrowLogic.h b/tslang/include/TypeScript/LowerToLLVM/ThrowLogic.h index 623382396..9e4538ec2 100644 --- a/tslang/include/TypeScript/LowerToLLVM/ThrowLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/ThrowLogic.h @@ -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) { } @@ -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})); diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h index e52163117..d45e1a8bb 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h @@ -151,6 +151,9 @@ class MLIRTypeHelper return mlir::IntegerAttr::get(getI32Type(), mlir::APInt(32, value, true)); } + // 64-bit whatever the target: like getI64Type() above, this helper's contract is its name - an + // attribute of the 64-bit integer type. Sites needing a pointer-wide constant build the type from + // compileOptions.sizeBits() and use builder.getIntegerAttr() instead. mlir::IntegerAttr getI64AttrValue(int64_t value) { return mlir::IntegerAttr::get(getI64Type(), mlir::APInt(64, value, true)); diff --git a/tslang/lib/TypeScript/GCPass.cpp b/tslang/lib/TypeScript/GCPass.cpp index 01b8691e8..11db0c0d3 100644 --- a/tslang/lib/TypeScript/GCPass.cpp +++ b/tslang/lib/TypeScript/GCPass.cpp @@ -262,10 +262,11 @@ class GCPass : public mlir::PassWrapper { PatternRewriter rewriter(memSetCallOp.getContext()); - TypeHelper th(memSetCallOp.getContext()); + TypeHelper th(memSetCallOp.getContext(), tsContext.compileOptions); LLVMCodeHelper ch(memSetCallOp, rewriter, nullptr, tsContext.compileOptions); auto i8PtrTy = th.getPtrType(); - auto gcInitFuncOp = ch.getOrInsertFunction("GC_malloc_atomic", th.getFunctionType(th.getPtrType(), mlir::ArrayRef{th.getI64Type()})); + // target-width: GC_malloc_atomic's parameter is `size_t size_in_bytes` (3rdParty/gc-8.2.12/include/gc.h:545). + auto gcInitFuncOp = ch.getOrInsertFunction("GC_malloc_atomic", th.getFunctionType(th.getPtrType(), mlir::ArrayRef{th.getSizeType()})); markAsAllocatorIfNeeded("GC_malloc_atomic", gcInitFuncOp); } diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 3b3841a0a..222981a8b 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -6222,12 +6222,15 @@ class GCMakeDescriptorOpLowering : public TsLlvmPatterncompileOptions); LLVMCodeHelper ch(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); auto i64PtrTy = th.getPtrType(); - auto gcMakeDescriptorFunc = ch.getOrInsertFunction("GC_make_descriptor", th.getFunctionType(rewriter.getI64Type(), {i64PtrTy, rewriter.getI64Type()})); + // target-width: GC_make_descriptor returns GC_descr, a typedef of GC_word, which gc.h defines + // as the unsigned integer the size of `void *`; its second parameter is a `size_t` bit count + // (3rdParty/gc-8.2.12/include/gc_typed.h:52). Neither is a fixed 64 bits. + auto gcMakeDescriptorFunc = ch.getOrInsertFunction("GC_make_descriptor", th.getFunctionType(th.getSizeType(), {i64PtrTy, th.getSizeType()})); rewriter.replaceOpWithNewOp(op, gcMakeDescriptorFunc, ValueRange{transformed.getTypeBitmap(), transformed.getSizeOfBitmapInElements()}); return success(); @@ -6268,7 +6271,7 @@ class GCNewExplicitlyTypedOpLowering : public TsLlvmPatterncompileOptions); CodeLogicHelper clh(op, rewriter); TypeConverterHelper tch(getTypeConverter()); - TypeHelper th(rewriter); + TypeHelper th(rewriter, tsLlvmContext->compileOptions); auto llvmIndexType = tch.convertType(th.getIndexType()); @@ -6291,7 +6294,9 @@ class GCNewExplicitlyTypedOpLowering : public TsLlvmPattern(location, typeDescrType, typeDescRef); diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index 41266f2c3..3fffecc2b 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -1309,8 +1309,11 @@ genContext); location, fullClassStaticFieldName, true, newClassPtr->isDeclaration ? VariableType::External : VariableType::Var, [&](mlir::Location location, const GenContext &genContext) { + // target-width: the global caches a Boehm GC_descr (= GC_word, the integer the size + // of `void *`), so it is pointer-wide rather than a fixed 64 bits. + auto typeDescrType = builder.getIntegerType(compileOptions.sizeBits()); auto init = - builder.create(location, builder.getI64Type(), mth.getI64AttrValue(0)); + builder.create(location, typeDescrType, builder.getIntegerAttr(typeDescrType, 0)); return std::make_tuple(init.getType(), init, TypeProvided::Yes); }, genContext); @@ -1334,7 +1337,9 @@ genContext); auto name = TYPE_BITMAP_NAME; auto fullClassStaticFieldName = getTypeBitmapMethodName(newClassPtr); - auto funcType = getFunctionType({}, builder.getI64Type(), false); + // target-width: the bitmap method returns the Boehm GC_descr built below (= GC_word, the + // integer the size of `void *`), and must agree with the descriptor global's type. + auto funcType = getFunctionType({}, builder.getIntegerType(compileOptions.sizeBits()), false); mlirGenFunctionBody( location, name, fullClassStaticFieldName, funcType, @@ -1439,7 +1444,9 @@ genContext); auto saveToElement = builder.create(location, valWithBit, elemRef); } - auto typeDescr = builder.create(location, builder.getI64Type(), arrayValue, + // target-width: GC_make_descriptor's result is a GC_descr (= GC_word, the integer the + // size of `void *`) - see the lowering in LowerToLLVM.cpp GCMakeDescriptorOpLowering. + auto typeDescr = builder.create(location, builder.getIntegerType(compileOptions.sizeBits()), arrayValue, sizeOfTypeInBitmapTypes); auto retVarInfo = symbolTable.lookup(RETURN_VARIABLE_NAME); @@ -1721,8 +1728,12 @@ genContext); // detected and handled up front rather than falling into the "present" // branches - use the same -1 sentinel placeholder, cast to the slot's // ref/func-pointer type. - auto negative1 = builder.create(location, builder.getI64Type(), - mth.getI64AttrValue(-1)); + // target-width: a -1 pointer sentinel, cast straight to a ref/func-pointer slot + // below. The lowering that tests it (InterfaceSymbolRefOpLowering in + // LowerToLLVM.cpp) builds its -1 at compileOptions.sizeBits(), so this must match. + auto ptrIntType = builder.getIntegerType(compileOptions.sizeBits()); + auto negative1 = builder.create(location, ptrIntType, + builder.getIntegerAttr(ptrIntType, -1)); auto slotType = methodOrField.isField ? methodOrField.fieldInfo.type : methodOrField.methodInfo.funcType; auto castedPtr = cast(location, mlir_ts::RefType::get(slotType), negative1, genContext); vtableValue = builder.create( diff --git a/tslang/lib/TypeScript/MLIRGenExpressions.cpp b/tslang/lib/TypeScript/MLIRGenExpressions.cpp index 7be28fc83..ab5f8d86f 100644 --- a/tslang/lib/TypeScript/MLIRGenExpressions.cpp +++ b/tslang/lib/TypeScript/MLIRGenExpressions.cpp @@ -1373,6 +1373,8 @@ namespace mlirgen *(bigIntLiteral->text.end() - 1) == S('n') ? bigIntLiteral->text.substr(0, bigIntLiteral->text.length() - 1) : bigIntLiteral->text.c_str())); + // 64-bit whatever the target: a `bigint` literal is modeled as the language's own 64-bit + // integer type; its width is fixed by the language, not by the pointer width. auto type = builder.getI64Type(); auto attrVal = mlir::IntegerAttr::get(type, newVal.getExtValue()); auto literalType = mlir_ts::LiteralType::get(attrVal, type); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 3ebd90705..8b9bd59a1 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -5611,6 +5611,8 @@ class MLIRGenImpl { return { builder.getF128Type(), + // 64-bit whatever the target: these are the language's own fixed-width numeric types, the + // rungs of the promotion ladder - i64/SInt(64) mean "the 64-bit integer type", not "a size". getNumberType(), builder.getF64Type(), builder.getI64Type(), SInt(64), builder.getIndexType(), builder.getF32Type(), SInt(32), builder.getI32Type(), builder.getF16Type(), SInt(16), builder.getI16Type(), diff --git a/tslang/lib/TypeScript/MLIRGenInterfaces.cpp b/tslang/lib/TypeScript/MLIRGenInterfaces.cpp index 9e0c8ef78..be1eb78e5 100644 --- a/tslang/lib/TypeScript/MLIRGenInterfaces.cpp +++ b/tslang/lib/TypeScript/MLIRGenInterfaces.cpp @@ -440,8 +440,12 @@ namespace mlirgen { // null value, as missing field/method // auto nullObj = builder.create(location, getNullType()); - auto negative1 = builder.create(location, builder.getI64Type(), - mth.getI64AttrValue(-1)); + // target-width: a -1 pointer sentinel, cast straight to the field's ref type + // below. InterfaceSymbolRefOpLowering (LowerToLLVM.cpp) compares against a -1 + // built at compileOptions.sizeBits(), so this must be the same width. + auto ptrIntType = builder.getIntegerType(compileOptions.sizeBits()); + auto negative1 = builder.create(location, ptrIntType, + builder.getIntegerAttr(ptrIntType, -1)); auto castedPtr = cast(location, mlir_ts::RefType::get(methodOrField.fieldInfo.type), negative1, genContext); vtableValue = builder.create( @@ -459,8 +463,12 @@ namespace mlirgen // a required method is always compile-time-resolvable through // lookupObjectLiteralMethodSymbol/findMethod above and never reaches // isMissing here; only a genuinely-absent optional method does. - auto negative1 = builder.create(location, builder.getI64Type(), - mth.getI64AttrValue(-1)); + // target-width: a -1 pointer sentinel, cast straight to the method's + // func-pointer ref type below - same convention and same width as the field + // case above and as InterfaceSymbolRefOpLowering's compileOptions.sizeBits() -1. + auto ptrIntType = builder.getIntegerType(compileOptions.sizeBits()); + auto negative1 = builder.create(location, ptrIntType, + builder.getIntegerAttr(ptrIntType, -1)); auto castedPtr = cast(location, mlir_ts::RefType::get(methodOrField.methodInfo.funcType), negative1, genContext); vtableValue = builder.create( From 185125025f63769d240debae8dc993b0f50f94be Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 21 Sep 2026 00:28:32 +0100 Subject: [PATCH 15/20] Give ctest its -C Release, without which nothing runs This is a multi-config MSBuild generator, so a bare 'ctest -j 16' reports all 2765 tests as (Not Run) and exits with errors. That reads as a catastrophic regression and is nothing of the sort. Found by the Task 5 implementer; confirmed here by running both forms. Co-Authored-By: Claude Opus 5 --- .../plans/2026-09-20-32-bit-phase-1-target-width.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md index 90ffdee1c..78155aaee 100644 --- a/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md +++ b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md @@ -744,7 +744,7 @@ The suite is the real check that no 64-bit behavior moved. Run it exactly as for ```bash cmake --build --preset build-windows-msbuild-2026-release -cd i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release && ctest -j 16 +cd i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release && ctest -j 16 -C Release ``` Expected: the same result as `main` — 2765/2765 as of 2026-09-16. Any new failure is this task's, not a pre-existing one; confirm by re-running that test on `main` before concluding otherwise. @@ -836,7 +836,7 @@ Expected: prints `x`, exit 0. - [ ] **Step 5: Run the full suite** ```bash -cd i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release && ctest -j 16 +cd i:/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release && ctest -j 16 -C Release ``` Expected: unchanged from Task 5. @@ -862,7 +862,7 @@ All of the following, before Phase 2 is planned: - [ ] `MLIRGenTests.exe` passes, including the new `TargetInfoTest` suite. - [ ] `test/check-datalayout.sh` passes for `i686-pc-windows-msvc`, `wasm32-unknown-unknown` and `x86_64-pc-windows-msvc`. -- [ ] `ctest` matches `main` — 2765/2765. +- [ ] `ctest -j 16 -C Release` matches `main` — 2765/2765. Without `-C Release` on this multi-config MSBuild generator every test reports "(Not Run)", which looks like catastrophic failure and is not. - [ ] x64 `--emit=llvm` output is byte-identical to `main` for a class-and-method sample. - [ ] `--emit=jit -mtriple=i686-pc-windows-msvc` refuses with a message naming both triples. - [ ] Every one of the 18 non-EH `getI64Type()` call sites listed in `task-5-sites.md` carries a classification comment. From 9be0623fe7ce737e58cf5475646e1b1da94353b9 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 21 Sep 2026 00:42:58 +0100 Subject: [PATCH 16/20] Make the pointer->float cast unsigned and widen the GC descriptor op contract Review fallout from classifying the i64 sites, not from the calls themselves. The pointer->float cast now uses uitofp. While ptrtoint produced a fixed i64 the zero-extension made the signedness moot; now that the integer is only as wide as the pointer, any address at or above 0x80000000 - reachable in a /LARGEADDRESSAWARE i686 process or a large wasm32 memory - would have read back as a negative float. An address is unsigned, which is also what the ZExtOp two branches down already assumes. GCMakeDescriptorOp's result and GCNewExplicitlyTypedOp's operand were still constrained to I64 in the ODS. The generated build() takes the result type verbatim so construction would have worked, but verification would have rejected the i32 descriptor those ops now carry on a 32-bit target. They are the only two I64 operand/result constraints in the file. Three comments corrected: createI64ConstantOf's claimed an invariant the codebase does not hold (LowerToLLVM.cpp passes it a byte count), the promotion-ladder one called getIndexType() fixed-width when it is the one thing there that is not, and getTypeBitmapValueType() now records that it is the same GC_word contract and the one remaining width gap in that path, deliberately left for a later phase. x64 IR is still byte-identical for both baseline samples; suite 2765/2765. Co-Authored-By: Claude Opus 5 --- .../include/TypeScript/LowerToLLVM/CastLogicHelper.h | 12 +++++++++--- .../include/TypeScript/LowerToLLVM/CodeLogicHelper.h | 9 ++++++--- tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h | 5 +++++ tslang/include/TypeScript/TypeScriptOps.td | 7 +++++-- tslang/lib/TypeScript/MLIRGenImpl.h | 6 ++++-- 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h index 5c2c98633..a89628af4 100644 --- a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h @@ -726,10 +726,16 @@ class CastLogicHelper if (isa(inLLVMType) && isFloat(resLLVMType)) { - // target-width: a ptrtoint of a real pointer; the integer handed to sitofp is as wide as - // the target's pointer. + // 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(loc, th.getPointerIntType(), in); - return rewriter.create(loc, resLLVMType, intVal); + return rewriter.create(loc, resLLVMType, intVal); } if (isInt(inLLVMType) && isa(resLLVMType)) diff --git a/tslang/include/TypeScript/LowerToLLVM/CodeLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/CodeLogicHelper.h index cc83a9a04..b4f5f0339 100644 --- a/tslang/include/TypeScript/LowerToLLVM/CodeLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/CodeLogicHelper.h @@ -72,9 +72,12 @@ class CodeLogicHelper return rewriter.create(loc, rewriter.getIntegerType(32), rewriter.getIntegerAttr(rewriter.getI32Type(), value)); } - // 64-bit whatever the target: this helper's whole contract is "give me an i64 constant" - the - // width is in its name. Call sites that want a pointer-wide constant use createIConstantOf() - // with TypeHelper::getSizeType()'s width instead. + // 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(loc, rewriter.getIntegerType(64), rewriter.getIntegerAttr(rewriter.getI64Type(), value)); diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h index d45e1a8bb..aa59fce11 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h @@ -185,6 +185,11 @@ class MLIRTypeHelper } #ifdef ENABLE_TYPED_GC + // NOT YET target-width, deliberately: this models GC_word, the element type of the bitmap array + // handed to GC_make_descriptor(const GC_word *, size_t), so by the same contract as the + // descriptor itself it is pointer-wide rather than a fixed 64 bits. Left at u64 for a later + // phase - it is the one remaining width gap in the typed-GC path, which is inert anyway while + // Config.h has ENABLE_TYPED_GC false. mlir::IntegerType getTypeBitmapValueType() { return getU64Type(); diff --git a/tslang/include/TypeScript/TypeScriptOps.td b/tslang/include/TypeScript/TypeScriptOps.td index 73b8f0634..1133f6c04 100644 --- a/tslang/include/TypeScript/TypeScriptOps.td +++ b/tslang/include/TypeScript/TypeScriptOps.td @@ -2429,14 +2429,17 @@ def TypeScript_NoOp : TypeScript_Op<"noop", [Pure]> { def TypeScript_GCMakeDescriptorOp : TypeScript_Op<"GCMakeDescriptor"> { let summary = "GC make descriptor operation"; + // The descriptor is a Boehm GC_descr, a typedef of GC_word - the integer the size of `void *` - + // so it is pointer-wide, i32 on a 32-bit target. An I64 constraint would reject it there. let arguments = (ins TypeScript_Ref:$typeBitmap, Index:$sizeOfBitmapInElements); - let results = (outs I64:$descr); + let results = (outs AnySignlessInteger:$descr); } def TypeScript_GCNewExplicitlyTypedOp : TypeScript_Op<"GCNewExplicitlyTyped"> { let summary = "GC new exmplicitly typed operation"; - let arguments = (ins I64:$typeDescr); + // Same GC_descr contract as GCMakeDescriptorOp above: pointer-wide, not a fixed 64 bits. + let arguments = (ins AnySignlessInteger:$typeDescr); let results = (outs Res:$instance); } diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 8b9bd59a1..545b5ac31 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -5611,8 +5611,10 @@ class MLIRGenImpl { return { builder.getF128Type(), - // 64-bit whatever the target: these are the language's own fixed-width numeric types, the - // rungs of the promotion ladder - i64/SInt(64) mean "the 64-bit integer type", not "a size". + // 64-bit whatever the target (getI64Type/SInt(64) only): these name the language's own + // 64-bit integer types - a rung of the promotion ladder - not a size or a pointer. The + // getIndexType() beside them is a different thing entirely and IS target-dependent; it + // sits at this rung because that is where it promotes, not because it is 64 bits wide. getNumberType(), builder.getF64Type(), builder.getI64Type(), SInt(64), builder.getIndexType(), builder.getF32Type(), SInt(32), builder.getI32Type(), builder.getF16Type(), SInt(16), builder.getI16Type(), From 52d278d62aacf15078f0b125c53963f110293485 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 21 Sep 2026 08:39:32 +0100 Subject: [PATCH 17/20] Refuse --emit=jit for a target this process cannot run Verified before this change: with the release binary, both `--emit=jit -mm=none --no-default-lib -mtriple=i686-pc-windows-msvc` and the same with `-mtriple=wasm32-unknown-unknown` printed "Hello 32" and exited 0. tslang.exe is an x64 process; jit.cpp:634 overwrites the module's target triple with the host TargetMachine's, so it silently ran x64 code while the user asked for i386 or wasm. Added a guard as the first statement of runJit(), before any LLVM target initialization, that checks CompileOptions::targetInfo.supportsInProcessJit (true only when the requested target's arch and OS match the host's) and refuses with a message naming both the host triple and the requested one. Left to LLJIT, the same mismatch surfaces later as a relocation or "symbol not found" failure deep in the session that never mentions the triple as the cause. Co-Authored-By: Claude Sonnet 5 --- tslang/tslang/jit.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tslang/tslang/jit.cpp b/tslang/tslang/jit.cpp index e3f7185f5..f9faee537 100644 --- a/tslang/tslang/jit.cpp +++ b/tslang/tslang/jit.cpp @@ -356,6 +356,19 @@ extern "C" EXCEPTION_DISPOSITION __CxxFrameHandler3(struct _EXCEPTION_RECORD *, int runJit(int argc, char **argv, mlir::ModuleOp module, CompileOptions &compileOptions) { + // tslang.exe is an x64 process: it cannot execute i386 or wasm code in-process. Refused up + // front rather than left to LLJIT, whose failure for this is a relocation or "symbol not + // found" error deep in the session that never mentions the triple as the cause. + if (!compileOptions.targetInfo.supportsInProcessJit) + { + llvm::WithColor::error(llvm::errs(), "tslang") + << "--emit=jit runs the code in this process, which is " + << llvm::sys::getDefaultTargetTriple() << ", so it cannot run code built for " + << compileOptions.moduleTargetTriple + << ". Build it instead with --emit=exe or --emit=obj.\n"; + return -1; + } + // to avoid false positive memory leak reports in release builds // Print a stack trace if we signal out. llvm::sys::PrintStackTraceOnErrorSignal(argv[0]); From d926e5e40950556e6dba5332ef19918dc5e29746 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 21 Sep 2026 08:58:07 +0100 Subject: [PATCH 18/20] Compare the optional-member sentinel at target width on both sides InterfaceSymbolRefOpLowering built its ptrtoint at llvmtch.getIntPtrType(0), which comes from the type converter's data layout - LLVM's default, 64-bit pointers, for every non-wasm target - while the -1 it compared against was built at compileOptions.sizeBits(). On i686 that was an i64 against an i32 and verification failed ("'llvm.icmp' op requires all operands to have the same type") for any optional interface member. Both sides now take the width from compileOptions. The ptrtoint has to be at 32 bits on i686: at i64 it would zero-extend the 0xFFFFFFFF sentinel and never compare equal. The constant is built directly rather than through createIConstantOf, whose unsigned value parameter would give 0xFFFFFFFF, not -1, at 64 bits. x64 IR is unchanged for both baseline samples and 00interface_optional.ts. Co-Authored-By: Claude Opus 5 --- tslang/lib/TypeScript/LowerToLLVM.cpp | 17 ++++++----------- tslang/test/check-datalayout.sh | 0 2 files changed, 6 insertions(+), 11 deletions(-) mode change 100644 => 100755 tslang/test/check-datalayout.sh diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 222981a8b..a716bcf62 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -5245,11 +5245,9 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(getTypeConverter())); - auto intPtrType = llvmtch.getIntPtrType(0); - auto negative1 = tsLlvmContext->compileOptions.sizeBits() == 32 - ? clh.createI32ConstantOf(-1) - : clh.createI64ConstantOf(-1); + // Both sides take the target width from compileOptions, not from the type converter's data layout, which is not yet target-derived. + auto intPtrType = rewriter.getIntegerType(tsLlvmContext->compileOptions.sizeBits()); + auto negative1 = rewriter.create(loc, intPtrType, rewriter.getIntegerAttr(intPtrType, -1)); auto methodOrFieldIntPtrValue = rewriter.create(loc, intPtrType, methodOrFieldPtr); auto isMissing = rewriter.create(loc, LLVM::ICmpPredicate::eq, methodOrFieldIntPtrValue, negative1); @@ -5288,12 +5286,9 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(getTypeConverter())); - - auto negative1 = tsLlvmContext->compileOptions.sizeBits() == 32 - ? clh.createI32ConstantOf(-1) - : clh.createI64ConstantOf(-1); - auto intPtrType = llvmtch.getIntPtrType(0); + // Both sides take the target width from compileOptions, not from the type converter's data layout, which is not yet target-derived. + auto intPtrType = rewriter.getIntegerType(tsLlvmContext->compileOptions.sizeBits()); + auto negative1 = rewriter.create(loc, intPtrType, rewriter.getIntegerAttr(intPtrType, -1)); auto methodOrFieldIntPtrValue = rewriter.create(loc, intPtrType, methodOrFieldPtr); auto condVal = rewriter.create(loc, LLVM::ICmpPredicate::eq, methodOrFieldIntPtrValue, negative1); diff --git a/tslang/test/check-datalayout.sh b/tslang/test/check-datalayout.sh old mode 100644 new mode 100755 From e9c1312d46c5c4b09cc83aba128193b0cf541a66 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 21 Sep 2026 08:58:20 +0100 Subject: [PATCH 19/20] Make the old-arch-list test cover what the list actually got wrong The comment said the old opts.cpp list had rotted because loongarch64 was on it and riscv32 appeared nowhere. Neither is a fault: the list named 64-bit arches and defaulted to 32, so loongarch64, riscv32 and armv7 all came out right. What it got wrong was aarch64_32 listed as 64-bit (already covered by Arm64_32IsAnIlp32Target) and 64-bit arches it did not list, which it gave 32: systemz, sparcv9, amdgcn, ve, and also spirv and riscv64be. PointerWidthIsRightForArchesTheOldListMissed now asserts 64 for s390x, sparcv9, amdgcn and ve, each a spelling llvm::Triple's parseArch maps to the intended arch, so it fails against the old list. Also marks test/check-datalayout.sh executable; it was committed 100644. Co-Authored-By: Claude Opus 5 --- tslang/unittests/MLIRGen/TargetInfo.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tslang/unittests/MLIRGen/TargetInfo.cpp b/tslang/unittests/MLIRGen/TargetInfo.cpp index f96eae68c..6ed6bbc25 100644 --- a/tslang/unittests/MLIRGen/TargetInfo.cpp +++ b/tslang/unittests/MLIRGen/TargetInfo.cpp @@ -8,10 +8,11 @@ #include "gmock/gmock.h" -// TargetInfo replaces a hand-maintained list of 64-bit architectures in opts.cpp. That list had -// already rotted - loongarch64 was on it, riscv32 appeared nowhere - so these tests deliberately -// cover arches nobody has built for, to prove the answer comes from the triple rather than from -// somebody remembering to add a line. +// TargetInfo replaces a hand-maintained list of 64-bit architectures in opts.cpp; any arch not on +// it got 32. The list was wrong in two ways: it put aarch64_32 among the 64-bit arches (it is ILP32, +// see Arm64_32IsAnIlp32Target), and it missed 64-bit arches - systemz, sparcv9, amdgcn, ve, spirv, +// riscv64be - which therefore got 32. These tests cover arches nobody has built for, to prove the +// answer comes from the triple rather than from somebody remembering to add a line. namespace { @@ -39,12 +40,14 @@ TEST(TargetInfoTest, PointerWidthComesFromTheTriple) EXPECT_EQ(infoFor("aarch64-unknown-linux-gnu").pointerBits, 64u); } -// The arches the old list got wrong, in both directions. +// 64-bit arches missing from the old list, which gave them 32. Each spelling is one that +// llvm::Triple's parseArch maps to the arch named in the comment. TEST(TargetInfoTest, PointerWidthIsRightForArchesTheOldListMissed) { - EXPECT_EQ(infoFor("riscv32-unknown-elf").pointerBits, 32u); - EXPECT_EQ(infoFor("armv7-unknown-linux-gnueabihf").pointerBits, 32u); - EXPECT_EQ(infoFor("loongarch64-unknown-linux-gnu").pointerBits, 64u); + EXPECT_EQ(infoFor("s390x-unknown-linux-gnu").pointerBits, 64u); // systemz + EXPECT_EQ(infoFor("sparcv9-sun-solaris").pointerBits, 64u); // sparcv9 + EXPECT_EQ(infoFor("amdgcn-amd-amdhsa").pointerBits, 64u); // amdgcn + EXPECT_EQ(infoFor("ve-unknown-linux-gnu").pointerBits, 64u); // ve } // An unknown arch has no width of its own; falling back to 0 would make every size computation From 93dca01d6493791c5211e4322cf6dbdbee43c359 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 21 Sep 2026 08:58:23 +0100 Subject: [PATCH 20/20] Bring the 32-bit spec and Phase 1 plan in line with what was built Spec: Phase 1 is marked implemented. Drops the parsed llvm::Triple on CompileOptions and indexBits, neither of which exists. Describes TypeHelper's optional CompileOptions pointer and getSizeType()'s report_fatal_error in place of the "wide but mechanical" parameter. Corrects the audit count to 18 call sites, records its result (13 target-width, 5 genuinely 64-bit) and the two latent bugs it found. Re-scopes the Phase 1 gate: the lowering's type converter still uses LLVM's default data layout for non-wasm targets, so 64-bit-shaped layout on i686 is not yet gone; deriving that layout from the module becomes Phase 2's first task, with its own x64 diff. Records the ABI-by-environment width disagreement as an open issue and the i686 for-await failures as a Phase 4 finding. Plan: same Triple and getSizeType() corrections, 20-entry arch list, the gate allows the one added target datalayout line, the 18-site list is inlined, and the baseline directory is marked local-only scratch. Co-Authored-By: Claude Opus 5 --- .../2026-09-20-32-bit-phase-1-target-width.md | 79 ++++++++++++----- .../2026-09-20-32-bit-compilation-design.md | 84 ++++++++++++++----- 2 files changed, 118 insertions(+), 45 deletions(-) diff --git a/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md index 78155aaee..558054cd2 100644 --- a/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md +++ b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md @@ -4,7 +4,7 @@ **Goal:** Make the compiler derive pointer/size width and target policy from the target triple rather than from a hand-maintained architecture list, so that any 32-bit triple is handled correctly. -**Architecture:** A new `TargetInfo` struct is derived once from `llvm::Triple` in `prepareOptions()` and carried on `CompileOptions` alongside the parsed `Triple`. It holds widths (`pointerBits`) and policy predicates named for the decision they answer (`usesImageBaseRelativeEH`, `stdcallDecoratesCxxThrow`, `supportsInProcessJit`). `TypeHelper` gains an optional `CompileOptions` pointer and target-width accessors; only call sites that need a width pass it. +**Architecture:** A new `TargetInfo` struct is derived once from `llvm::Triple` in `prepareOptions()` and carried on `CompileOptions`, next to the existing `moduleTargetTriple` string. It holds widths (`pointerBits`) and policy predicates named for the decision they answer (`usesImageBaseRelativeEH`, `stdcallDecoratesCxxThrow`, `supportsInProcessJit`). `TypeHelper` gains an optional `CompileOptions` pointer and target-width accessors; only call sites that need a width pass it. **Tech Stack:** C++17, MLIR/LLVM (prebuilt in `3rdParty/llvm/x64`), CMake + MSVC (Visual Studio 18 2026), GoogleTest via `add_mlir_unittest`. @@ -22,7 +22,7 @@ ## Deviation from the spec -The spec says `TypeHelper` "grows a `CompileOptions` parameter" and calls it "a wide but mechanical diff". Counting found 152 `TypeHelper` construction sites across 22 files, nearly all of which only use width-independent helpers (`getI8Type`, `getPtrType`, `getVoidType`). This plan instead gives `TypeHelper` an **optional** `const CompileOptions *`, defaulting to `nullptr`, and migrates only the sites that call a target-width accessor. `getSizeType()` asserts the pointer is set, so a site that needs a width and did not pass options fails loudly in debug rather than silently picking a wrong one. Net effect: same design, ~20 sites touched instead of 152. +The spec says `TypeHelper` "grows a `CompileOptions` parameter" and calls it "a wide but mechanical diff". Counting found 152 `TypeHelper` construction sites across 22 files, nearly all of which only use width-independent helpers (`getI8Type`, `getPtrType`, `getVoidType`). This plan instead gives `TypeHelper` an **optional** `const CompileOptions *`, defaulting to `nullptr`, and migrates only the sites that call a target-width accessor. `getSizeType()` checks the pointer is set and calls `report_fatal_error` when it is not, in every build configuration, so a site that needs a width and did not pass options fails loudly rather than silently picking a wrong one. Net effect: same design, ~20 sites touched instead of 152. ## File Structure @@ -30,7 +30,7 @@ The spec says `TypeHelper` "grows a `CompileOptions` parameter" and calls it "a | --- | --- | | `include/TypeScript/TargetInfo.h` (create) | The `TargetInfo` struct and its derivation from a target/host `llvm::Triple` pair. Header-only; pure, no MLIR dependency, so it unit-tests without a compilation context. | | `include/TypeScript/DataStructs.h` (modify) | `CompileOptions` gains `TargetInfo targetInfo`. `sizeBits` becomes a deprecated accessor over `targetInfo.pointerBits` so existing readers keep working during migration. | -| `tslang/opts.cpp` (modify) | `prepareOptions()` builds `TargetInfo` from the target and host triples. The 23-entry 64-bit arch list is deleted. | +| `tslang/opts.cpp` (modify) | `prepareOptions()` builds `TargetInfo` from the target and host triples. The 20-entry 64-bit arch list is deleted. | | `include/TypeScript/LowerToLLVM/TypeHelper.h` (modify) | Optional `const CompileOptions *`; adds `getSizeType()` and `getPointerIntType()`. | | `lib/TypeScript/MLIRGenModule.cpp` (modify) | Emits `target datalayout` on the module, not just the triple. | | `unittests/MLIRGen/TargetInfo.cpp` (create) | GoogleTest coverage of `TargetInfo::fromTriple` across triples. | @@ -75,10 +75,11 @@ Create `unittests/MLIRGen/TargetInfo.cpp`: #include "gmock/gmock.h" -// TargetInfo replaces a hand-maintained list of 64-bit architectures in opts.cpp. That list had -// already gone wrong: it marks aarch64_32 as 64-bit, but ARM64_32 is an ILP32 target whose -// pointers are 32 bits. These tests deliberately cover arches nobody has built for, to prove -// the answer comes from the triple rather than from somebody remembering to add a line. +// TargetInfo replaces a hand-maintained list of 64-bit architectures in opts.cpp; any arch not on +// it got 32. The list was wrong in two ways: it put aarch64_32 among the 64-bit arches (it is ILP32, +// see Arm64_32IsAnIlp32Target), and it missed 64-bit arches - systemz, sparcv9, amdgcn, ve, spirv, +// riscv64be - which therefore got 32. These tests cover arches nobody has built for, to prove the +// answer comes from the triple rather than from somebody remembering to add a line. namespace { @@ -106,12 +107,14 @@ TEST(TargetInfoTest, PointerWidthComesFromTheTriple) EXPECT_EQ(infoFor("aarch64-unknown-linux-gnu").pointerBits, 64u); } -// The arches the old list got wrong, in both directions. +// 64-bit arches missing from the old list, which gave them 32. Each spelling is one that +// llvm::Triple's parseArch maps to the arch named in the comment. TEST(TargetInfoTest, PointerWidthIsRightForArchesTheOldListMissed) { - EXPECT_EQ(infoFor("riscv32-unknown-elf").pointerBits, 32u); - EXPECT_EQ(infoFor("armv7-unknown-linux-gnueabihf").pointerBits, 32u); - EXPECT_EQ(infoFor("loongarch64-unknown-linux-gnu").pointerBits, 64u); + EXPECT_EQ(infoFor("s390x-unknown-linux-gnu").pointerBits, 64u); // systemz + EXPECT_EQ(infoFor("sparcv9-sun-solaris").pointerBits, 64u); // sparcv9 + EXPECT_EQ(infoFor("amdgcn-amd-amdhsa").pointerBits, 64u); // amdgcn + EXPECT_EQ(infoFor("ve-unknown-linux-gnu").pointerBits, 64u); // ve } // An unknown arch has no width of its own; falling back to 0 would make every size computation @@ -369,8 +372,10 @@ Expected: all tests PASS, including the pre-existing `MLIRTypeHelperTest` suite. This phase must not change 64-bit behavior. -A pre-change baseline is already stored at `i:/TypeScriptCompiler/.superpowers/sdd/2026-09-20-32-bit-phase-1-target-width/baseline/`, generated with the release -binary as it stood before any Phase 1 change. Diff against it. Do **not** `git stash` and +A pre-change baseline is stored at `i:/TypeScriptCompiler/.superpowers/sdd/2026-09-20-32-bit-phase-1-target-width/baseline/`, generated with the release +binary as it stood before any Phase 1 change. That directory is local-only scratch (`.superpowers/` is +gitignored); a reader without it regenerates it from `main`'s Release binary, emitting +`--emit=llvm -mm=gc --no-default-lib` for the same samples. Diff against it. Do **not** `git stash` and rebuild to regenerate a baseline - that costs a second full Release link (many minutes) and risks losing uncommitted work. @@ -390,7 +395,7 @@ Expected: no differences. git add tslang/include/TypeScript/DataStructs.h tslang/tslang/opts.cpp tslang/unittests/MLIRGen/TargetInfo.cpp tslang/unittests/MLIRGen/TypeHelper.cpp git commit -m "Take pointer width from the triple, not from an arch list -opts.cpp listed 23 architectures it considered 64-bit. The list had +opts.cpp listed 20 architectures it considered 64-bit. The list had wrong: it marked aarch64_32 as 64-bit, but ARM64_32 has 32-bit pointers. Triple already knows, so ask it. @@ -683,11 +688,37 @@ grep -rn 'getI64Type()' lib include \ wc -l /tmp/i64-audit.txt ``` -**Do not derive the list yourself — it is already written for you** at -`i:/TypeScriptCompiler/.superpowers/sdd/2026-09-20-32-bit-phase-1-target-width/task-5-sites.md`, -with the exclusions already applied. A raw grep returns 21 hits, of which three are not -call sites at all (two definitions of `getI64Type` and one comment mentioning it), so a -count taken from grep will mislead you. **There are 18 call sites.** +**Do not derive the list yourself — it is below**, with the exclusions already applied. +A raw grep returns 21 hits, of which three are not call sites at all (two definitions of +`getI64Type` and one comment mentioning it), so a count taken from grep will mislead you. +**There are 18 call sites.** + +Generated at commit 97adcbf1. Line numbers shift as you edit; file plus surrounding code is +authoritative. Excluded, and not to be touched or annotated: the two Win32 RTTI helpers (18 +sites, Phase 3's); `TypeHelper.h:81`, the definition of `TypeHelper::getI64Type`; +`TypeHelper.h:87`, a comment mentioning it; `MLIRTypeHelper.h:71`, the definition of +`MLIRTypeHelper::getI64Type`. + +``` + 1 lib/TypeScript/GCPass.cpp:268: auto gcInitFuncOp = ch.getOrInsertFunction("GC_malloc_atomic", th.getFunctionType(th.getPtrType(), mlir::ArrayRef{th.getI64Type()})); + 2 lib/TypeScript/LowerToLLVM.cpp:6230: auto gcMakeDescriptorFunc = ch.getOrInsertFunction("GC_make_descriptor", th.getFunctionType(rewriter.getI64Type(), {i64PtrTy, rewriter.getI64Type()})); + 3 lib/TypeScript/LowerToLLVM.cpp:6294: auto gcMallocExplicitlyTypedFunc = ch.getOrInsertFunction("GC_malloc_explicitly_typed", th.getFunctionType(i8PtrTy, {rewriter.getI64Type(), rewriter.getI64Type()})); + 4 lib/TypeScript/MLIRGenAccessCall.cpp:1848: auto typeDescrType = builder.getI64Type(); + 5 lib/TypeScript/MLIRGenClasses.cpp:1313: builder.create(location, builder.getI64Type(), mth.getI64AttrValue(0)); + 6 lib/TypeScript/MLIRGenClasses.cpp:1337: auto funcType = getFunctionType({}, builder.getI64Type(), false); + 7 lib/TypeScript/MLIRGenClasses.cpp:1442: auto typeDescr = builder.create(location, builder.getI64Type(), arrayValue, + 8 lib/TypeScript/MLIRGenClasses.cpp:1724: auto negative1 = builder.create(location, builder.getI64Type(), + 9 lib/TypeScript/MLIRGenExpressions.cpp:1376: auto type = builder.getI64Type(); +10 lib/TypeScript/MLIRGenImpl.h:5614: getNumberType(), builder.getF64Type(), builder.getI64Type(), SInt(64), builder.getIndexType(), +11 lib/TypeScript/MLIRGenInterfaces.cpp:443: auto negative1 = builder.create(location, builder.getI64Type(), +12 lib/TypeScript/MLIRGenInterfaces.cpp:462: auto negative1 = builder.create(location, builder.getI64Type(), +13 include/TypeScript/LowerToLLVM/CastLogicHelper.h:715: auto intVal = rewriter.create(loc, th.getI64Type(), in); +14 include/TypeScript/LowerToLLVM/CastLogicHelper.h:726: auto intVal = rewriter.create(loc, th.getI64Type(), in); +15 include/TypeScript/LowerToLLVM/CodeLogicHelper.h:77: return rewriter.create(loc, rewriter.getIntegerType(64), rewriter.getIntegerAttr(rewriter.getI64Type(), value)); +16 include/TypeScript/LowerToLLVM/ConvertLogic.h:60: ArrayRef{rewriter.getI64Type(), th.getPtrType(), rewriter.getI32Type()}, true)); +17 include/TypeScript/LowerToLLVM/ThrowLogic.h:149: auto sizeType = compileOptions.sizeBits() == 32 ? th.getI32Type() : th.getI64Type(); +18 include/TypeScript/MLIRLogic/MLIRTypeHelper.h:156: return mlir::IntegerAttr::get(getI64Type(), mlir::APInt(64, value, true)); +``` - [ ] **Step 2: Classify each site** @@ -723,8 +754,10 @@ Expected: all PASS. Any site correctly classified as target-width emits `i64` on x64 either way, so x64 IR must be byte-identical. A diff here means a site was misclassified. -A pre-change baseline is already stored at `i:/TypeScriptCompiler/.superpowers/sdd/2026-09-20-32-bit-phase-1-target-width/baseline/`, generated with the release -binary as it stood before any Phase 1 change. Diff against it. Do **not** `git stash` and +A pre-change baseline is stored at `i:/TypeScriptCompiler/.superpowers/sdd/2026-09-20-32-bit-phase-1-target-width/baseline/`, generated with the release +binary as it stood before any Phase 1 change. That directory is local-only scratch (`.superpowers/` is +gitignored); a reader without it regenerates it from `main`'s Release binary, emitting +`--emit=llvm -mm=gc --no-default-lib` for the same samples. Diff against it. Do **not** `git stash` and rebuild to regenerate a baseline - that costs a second full Release link (many minutes) and risks losing uncommitted work. @@ -863,7 +896,7 @@ All of the following, before Phase 2 is planned: - [ ] `MLIRGenTests.exe` passes, including the new `TargetInfoTest` suite. - [ ] `test/check-datalayout.sh` passes for `i686-pc-windows-msvc`, `wasm32-unknown-unknown` and `x86_64-pc-windows-msvc`. - [ ] `ctest -j 16 -C Release` matches `main` — 2765/2765. Without `-C Release` on this multi-config MSBuild generator every test reports "(Not Run)", which looks like catastrophic failure and is not. -- [ ] x64 `--emit=llvm` output is byte-identical to `main` for a class-and-method sample. +- [ ] x64 `--emit=llvm` output is identical to `main` for a class-and-method sample, except the one added `target datalayout` line. - [ ] `--emit=jit -mtriple=i686-pc-windows-msvc` refuses with a message naming both triples. -- [ ] Every one of the 18 non-EH `getI64Type()` call sites listed in `task-5-sites.md` carries a classification comment. +- [ ] Every one of the 18 non-EH `getI64Type()` call sites listed in Task 5, Step 1 carries a classification comment. - [ ] The 18 EH `getI64Type()` sites are untouched. The two RTTI helper files are *not* otherwise frozen: Task 2 legitimately changes `compileOptions.sizeBits` to `sizeBits()` at `LLVMRTTIHelperVCWin32.h:118` and `MLIRRTTIHelperVCWin32.h:143`. Verify with `git diff -- | grep getI64Type`, which must be empty — not with `git diff --stat`, which will legitimately be non-empty. 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 19ea47643..beaf86e44 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, not yet implemented. +Status: approved design. Phase 1 implemented; phases 2-4 not. ## Why now @@ -51,20 +51,22 @@ 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` alongside the parsed `Triple` -itself. It holds two kinds of fact: +`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`, `indexBits`. +- **Widths** — `pointerBits`, used for pointers, sizes and indices alike. - **Policy** — `usesImageBaseRelativeEH`, `stdcallDecoratesCxxThrow`, `supportsInProcessJit`. `TypeHelper` gains `getSizeType()` and `getPointerIntType()` backed by it. -`TypeHelper` currently takes only an `MLIRContext*`, so it grows a -`CompileOptions` parameter. That is a wide but mechanical diff and the single -riskiest mechanical change in this plan. - -Keeping the parsed `llvm::Triple` on `CompileOptions` as well means one-off -queries do not each require a new `TargetInfo` field. +`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 @@ -98,34 +100,61 @@ memory, which is far harder to diagnose than a directory that is not there. ## Phase 1 — Target width correctness -`TargetInfo` lands. `TypeHelper` takes `CompileOptions`. The arch list is -replaced. +`TargetInfo` lands. `TypeHelper` takes an optional `CompileOptions`. The arch +list is replaced. -There are 38 `getI64Type()` call sites across `lib/` and `include/`. Eighteen -of them are in the two Win32 RTTI helpers +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. Phase 1 audits the -remaining 20, and each is classified in a comment as one of: +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. -Most are expected to be genuinely 64-bit. The audit's value is not the count of -changes but that the classification becomes explicit and survives future edits. +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, and no 32-bit target -emits 64-bit-shaped size or pointer arithmetic. Verifiable with no native -builds. +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. @@ -175,6 +204,9 @@ 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. @@ -194,6 +226,14 @@ confusion: 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 @@ -207,7 +247,7 @@ confusion: | 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 38 call sites 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. | +| 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. |