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..558054cd2 --- /dev/null +++ b/docs/superpowers/plans/2026-09-20-32-bit-phase-1-target-width.md @@ -0,0 +1,902 @@ +# 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`, 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`. + +**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 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 + +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 + +| 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 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. | +| `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/unittests/MLIRGen/Debug/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; 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 +{ + +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); +} + +// 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("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 +// 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/unittests/MLIRGen/Debug/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/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. + +- [ ] **Step 5: Verify x64 output is byte-identical** + +This phase must not change 64-bit behavior. + +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. + +```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 "$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. + +- [ ] **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 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. + +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/unittests/MLIRGen/Debug/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 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) + +**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 +``` + +**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** + +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/unittests/MLIRGen/Debug/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. + +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. + +```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 "$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. + +- [ ] **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 -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. + +- [ ] **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 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. + +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:357` (the first statement of `runJit`) + +**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** + +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 +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** + +```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 -C Release +``` + +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 -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 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, 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 new file mode 100644 index 000000000..beaf86e44 --- /dev/null +++ b/docs/superpowers/specs/2026-09-20-32-bit-compilation-design.md @@ -0,0 +1,254 @@ +# 32-bit compilation + +Design for compiling TypeScript to 32-bit targets, with `i686-pc-windows-msvc` +as the proof target and width-correctness that generalizes to any 32-bit triple. + +Status: approved design. Phase 1 implemented; phases 2-4 not. + +## Why now + +Until #333, every compiled program linked `LLVMSupport.lib` and +`LLVMDemangle.lib`. Those come from the prebuilt LLVM in `3rdParty/llvm/x64`, +which exists only as x64 — so a 32-bit link was blocked by a library we could +not rebuild cheaply. #333 removed that dependency. Every remaining x64 blocker +is a library this repository builds from source. + +## Measured baseline + +Probed with the Sep-18 build (`__build/tslang/windows-msbuild-2026-release`), +target `i686-pc-windows-msvc`. These are measurements, not estimates. + +| Case | Result | +| --- | --- | +| `--emit=obj -mm=none --no-default-lib` | Works. `hello.obj` machine type `0x014c` (`IMAGE_FILE_MACHINE_I386`). | +| `--emit=exe -mm=none --no-default-lib` | Works. `hello.exe` PE machine `0x014c`; runs and prints. | +| `LLVMSupport.lib` in the link | Gone, as of #333. (The older `__build/tslang/msbuild/x64/release` build still drags it in.) | +| Exceptions | `unresolved external symbol __CxxThrowException`; linker hints `__CxxThrowException@8`. | +| `-mm=gc` | `gc.lib : warning LNK4272: library machine type 'x64' conflicts with target machine type 'x86'`, then unresolved `_GC_init`, `_GC_enable_threads`. | +| `TypeScriptAsyncRuntime.lib` | Same x64/x86 machine-type conflict. | + +clang's driver locates the x86 CRT and Windows SDK import libraries on its own. +No toolchain-discovery work is needed. + +### Two findings that reduced scope + +Both were assumptions that probing disproved. They are recorded because the +obvious reading of the emitted IR suggests the opposite. + +**The async runtime ABI is not a width bug.** Emitted IR declares +`mlirAsyncRuntimeAddRef(ptr, i64)`, `mlirAsyncRuntimeCreateValue(i64)` and +similar regardless of triple, which looks like a 64-bit-shaped ABI. It is not: +`lib/AsyncRuntimeCommon.inc` declares these parameters as `int64_t`, not +`size_t`. `i64` is correct on both arches. Nothing to change. + +**The Win32 EH struct layouts are already correct for x86.** `ThrowInfo` is +emitted as `{i32 x 4}` and `CatchableType` as `{i32 x 7}` +(`include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h`). On x64 those i32 +fields hold image-base-relative RVAs. On 32-bit MSVC the corresponding fields +hold absolute pointers — which are also 4 bytes. The layouts coincide. What +differs is only the *value* stored: x86 must not subtract the image base. + +## Architecture + +One new type, `TargetInfo`, constructed once in `prepareOptions()` from +`llvm::Triple` and carried on `CompileOptions`. The triple itself stays on +`CompileOptions` only as the existing `moduleTargetTriple` string. `TargetInfo` +holds two kinds of fact: + +- **Widths** — `pointerBits`, used for pointers, sizes and indices alike. +- **Policy** — `usesImageBaseRelativeEH`, `stdcallDecoratesCxxThrow`, + `supportsInProcessJit`. + +`TypeHelper` gains `getSizeType()` and `getPointerIntType()` backed by it. +`TypeHelper` took only an `MLIRContext*`; it now takes an optional +`const CompileOptions *`, defaulting to null. Most of its ~150 construction +sites use only width-independent helpers (`getI8Type`, `getPtrType`, +`getVoidType`) and are unchanged; only the sites that need a target width pass +the options. `getSizeType()` called on a `TypeHelper` built without them fails +via `report_fatal_error` in every build configuration, rather than guessing a +width. + +### Naming policy predicates + +Policy predicates are named for what they decide, not for the architecture that +currently needs them: `usesImageBaseRelativeEH`, never `isX64`. This is what +keeps phase 3 from smearing `arch == x86_64` across three RTTI helpers, and it +is what makes the answer reviewable in one place next to the comment explaining +it. + +### Replacing the arch list + +`tslang/opts.cpp` currently derives `sizeBits` from a hand-maintained list of +64-bit architectures. The list is wrong: it marks `aarch64_32` as 64-bit, but +ARM64_32 is an ILP32 target whose pointers are 32 bits. It is replaced by +`llvm::Triple::getArchPointerBitWidth()`, which is what makes "any 32-bit +triple" true rather than "the 32-bit triples someone remembered". + +### Arch in the library layout + +`x86` and `x64`, matching the existing `3rdParty/gc/x64`. Arch is the outermost +dimension under each component, so a whole arch tree ships or is deleted as one +unit and existing x64 paths keep their shape: + +- `3rdParty/gc/x86/release/`, `3rdParty/gcdll/x86/release/` +- `defaultlib/{lib,dll}/x86/{debug,release}/{gc,rc,none}/` + +Strict, no fallback — the rule the default-lib layout already follows. A +missing arch tree is an error that names the arch and the script that builds +it. It never falls back to another arch: that link succeeds and then corrupts +memory, which is far harder to diagnose than a directory that is not there. + +## Phase 1 — Target width correctness + +`TargetInfo` lands. `TypeHelper` takes an optional `CompileOptions`. The arch +list is replaced. + +Eighteen `getI64Type()` call sites are in the two Win32 RTTI helpers +(`include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCWin32.h` and +`include/TypeScript/MLIRLogic/MLIRRTTIHelperVCWin32.h`, nine each); those are +phase 3's, since phase 3 rewrites that arithmetic anyway. Outside them there are +18 call sites across `lib/` and `include/`. A grep for `getI64Type()` returns +21 lines there, but three are not call sites: the two definitions of +`getI64Type` (`TypeHelper` and `MLIRTypeHelper`) and a comment mentioning it. +Phase 1 audits the 18, and each is classified in a comment as one of: + +- **genuinely 64-bit** — the language's `number` is f64; runtime ABI + parameters declared `int64_t`; anything whose width is fixed by a contract + outside the target. +- **target-width** — pointer arithmetic, object sizes, GEP indices. + +Result: 13 are target-width and 5 genuinely 64-bit. The audit's value is not +the count of changes but that the classification becomes explicit and survives +future edits. It also found two latent 32-bit bugs, both fixed: + +- The `-1` sentinel for an optional interface member the object does not + provide was produced at `i64` in MLIRGen while the lowering that tests for it + compared at a different width. Producer and consumer now both use the target + width. +- A pointer converted to a float went through `sitofp`. With the intermediate + integer now as wide as the pointer, an address at or above `0x80000000` on a + 32-bit target would read back as a negative float. It is now `uitofp`. + +`target datalayout` is set at IR emission rather than only later in +`tslang/obj.cpp`, so `--emit=llvm` output is self-describing. + +**Gate.** `--emit=llvm` for `i686-pc-windows-msvc`, `wasm32-unknown-unknown` +and `x86_64-pc-windows-msvc` emits the correct datalayout. Verifiable with no +native builds. + +The gate originally also required that no 32-bit target emit 64-bit-shaped size +or pointer arithmetic. Phase 1 does not meet that, and the requirement moves to +phase 2. The lowering's `LLVMTypeConverter` is built with LLVM's default data +layout for every non-wasm target (`TypeScriptToLLVMLoweringPass::runOnOperation` +in `lib/TypeScript/LowerToLLVM.cpp`, ~7255-7268), so on i686 `getIntPtrType`, +struct layout, union sizing and alignment still use 8-byte pointers. Phase 1 +fixes the widths MLIRGen and the lowering choose explicitly; it does not change +what the type converter derives. + +## Phase 2 — 32-bit native build matrix + +The first task, and a gate on any native 32-bit test, is to build the type +converter's data layout from the module's `llvm.data_layout` instead of LLVM's +default. This changes x64 output — the default layout aligns `i64` at 32 bits, +x64's real layout at 64 — so it needs its own x64 diff and review rather than +riding along with the build-matrix work. It also removes the hardcoded wasm32 +layout string, which has `f128:64` and lacks `i128:128` and so matches neither +of LLVM's derivations. + +- `3rdParty/gc/x86` and `3rdParty/gcdll/x86`: Boehm built for x86, static and + DLL, debug and release. +- `TypeScriptRuntime` and `TypeScriptAsyncRuntime` cross-built for x86. +- `tslang/exe.cpp` resolves `GC_LIB_PATH`, `GC_SHARED_LIB_PATH` and + `TSLANG_LIB_PATH` per target arch rather than per host. + +The compiler itself stays x64 and cross-compiles. Building `tslang.exe` as a +32-bit binary is out of scope. + +`checkGCLibPath` and `checkTslangLibPath` grow a COFF machine-type check, so an +arch mismatch is reported against the path that is wrong rather than surfacing +as LNK4272 plus a list of unresolved symbols. + +**Gate.** `-mm=gc` and `-mm=rc` link and run a 32-bit hello world. + +## Phase 3 — Win32 x86 exception handling + +Two changes, both narrow because of the finding above. + +1. `_CxxThrowException` is `__stdcall` on x86. The block already present but + commented out at `lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp:696` + sets `CallingConv::X86_StdCall`, which makes LLVM emit + `__CxxThrowException@8`. Gated on `stdcallDecoratesCxxThrow`. +2. The image-base subtraction in `LLVMRTTIHelperVCWin32.h` (three sites) and + the corresponding logic in `MLIRRTTIHelperVCWin32.h` is gated on + `usesImageBaseRelativeEH`. On x86 the field takes the `ptrtoint` result + directly. The `ptrtoint` and `sub` widths come from `TargetInfo` rather than + being hardcoded `i64`. + +**Testing.** Tests assert that control flow reaches the catch, and on +unwinding order. They do not assert on the catch variable's contents: reading a +catch variable's value is a known-unreliable area independent of this work, and +a test written on it would fail for reasons that have nothing to do with 32-bit +support. + +**Gate.** throw/catch, nested catch, try/finally and rethrow behave correctly +as 32-bit binaries. + +## Phase 4 — Default library and the suite + +`getDefaultLibSubDir` in `include/TypeScript/Defines.h` gains an arch segment, +giving `defaultlib/{lib,dll}/x86/{debug,release}/{gc,rc,none}/`. The +default-lib build scripts and the test runner gain an arch flag. + +Async/await and generators are measured here rather than in an earlier phase. +They depend on phases 2 and 3 and cannot run before both are in place. Nothing +in the probing suggests they are separately broken; if they are, that is a +finding of this phase. + +Already found: at i686, `00for_await` and `00for_await_yield` fail at +`--emit=llvm` with an `unrealized_conversion_cast` left in the IR; x64 passes. + +**Gate.** The same suite that runs at 64 bits runs at 32 bits, green. Genuine +failures are triaged and recorded the way the Debug-suite failures are, not +silently excluded. + +## Error handling + +Three failures get explicit, actionable messages instead of downstream +confusion: + +1. **Missing per-arch library tree** — names the arch and the script that + builds it. Not a fallback to another arch. +2. **Arch mismatch in a library that is present** — reported against the + offending path at resolution time, via the machine-type check, rather than + as a linker warning buried among unresolved symbols. +3. **`--emit=jit` with a non-host arch** — `tslang.exe` is an x64 process and + cannot execute i386 code in-process. Refused up front with a message saying + so. What the combination does today must be confirmed before this guard is + written; the guard is `supportsInProcessJit`. + +## Open issues + +- **ABI-by-environment triples.** For `x86_64-…-gnux32`, mips64 `gnuabin32` and + aarch64 `ilp32`, `getArchPointerBitWidth()` says 64 while the TargetMachine's + data layout says `p:32`. Nothing asserts the two agree. Proposed fix: a check + in `lib/TypeScript/MLIRGenModule.cpp` comparing + `createDataLayout().getPointerSizeInBits(0)` with `compileOptions.sizeBits()`. + +## Out of scope + +- `-m32` shorthand. `-mtriple=i686-pc-windows-msvc` already works and is the + mechanism; a second spelling earns nothing. +- Building `tslang.exe` itself as a 32-bit binary. +- 32-bit targets other than `i686-pc-windows-msvc` as *proof* targets. Phase 1 + makes width-correctness general, and `wasm32` continues to be exercised, but + only `i686-pc-windows-msvc` gets a full native build matrix and suite run. + +## Decisions taken + +| Decision | Choice | Alternative rejected because | +| --- | --- | --- | +| Where target facts live | `TargetInfo` on `CompileOptions`, derived once from `llvm::Triple` | Querying the `Triple` at each site scatters policy across every width-dependent call site and relocates the arch-list rot rather than fixing it. Querying MLIR's `DataLayout` cannot express EH policy at all, and much width-dependent code runs in MLIRGen where no `DataLayout` is in scope. | +| Arch naming | `x86` / `x64` | Matches `3rdParty/gc/x64`, which already exists. | +| Arch position in paths | Outermost under each component | A whole arch tree ships or is deleted as one unit; x64 paths keep their shape. | +| Missing arch tree | Hard error | A fallback links and then corrupts memory. | +| Suite scope at 32 bits | The same suite as 64 bits | A declared subset hides exactly the failures this work exists to find. | 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/CastLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h index 029c95a4d..a89628af4 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,8 +726,16 @@ class CastLogicHelper if (isa(inLLVMType) && isFloat(resLLVMType)) { - auto intVal = rewriter.create(loc, th.getI64Type(), in); - return rewriter.create(loc, resLLVMType, intVal); + // target-width: a ptrtoint of a real pointer; the integer handed to the fp conversion is + // as wide as the target's pointer. + // Unsigned, not signed: an address is an unsigned quantity, and once the integer is only + // as wide as the pointer its top bit is reachable - any address >= 0x80000000 in a + // /LARGEADDRESSAWARE i686 process or a large wasm32 memory would read back as a negative + // float under sitofp. (Same reasoning as the ZExtOp used for int widening two branches + // down.) While this produced a 64-bit integer from a 32-bit pointer the zero-extension + // hid the question; it no longer does. + auto intVal = rewriter.create(loc, th.getPointerIntType(), in); + 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 e6ed35354..b4f5f0339 100644 --- a/tslang/include/TypeScript/LowerToLLVM/CodeLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/CodeLogicHelper.h @@ -72,6 +72,12 @@ class CodeLogicHelper return rewriter.create(loc, rewriter.getIntegerType(32), rewriter.getIntegerAttr(rewriter.getI32Type(), value)); } + // 64-bit whatever the target: this helper builds a constant of the fixed 64-bit integer type, + // which is the whole of its contract - it has no target to ask. That is a statement about this + // function, not about its callers: some of them do pass byte counts through it (e.g. + // LowerToLLVM.cpp's MemoryAlloc(clh.createI64ConstantOf(2))), which is a width question of their + // own and not audited here. A caller that wants a pointer-wide constant should build it with + // createIConstantOf() at TypeHelper::getSizeType()'s width. mlir::Value createI64ConstantOf(int64_t value) { return rewriter.create(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/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..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/LowerToLLVM/TypeHelper.h b/tslang/include/TypeScript/LowerToLLVM/TypeHelper.h index 437c41e51..3c643f672 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" @@ -10,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; @@ -20,6 +23,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 +38,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 +83,33 @@ 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"); + 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. 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(); + } + mlir::Type getF32Type() { return Float32Type::get(context); 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..aa59fce11 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)); @@ -182,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(); @@ -1643,12 +1651,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 +1871,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 +1920,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/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/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/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 ec984e0f3..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); @@ -6222,12 +6217,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 +6266,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 +6289,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..545b5ac31 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -5611,6 +5611,10 @@ class MLIRGenImpl { return { builder.getF128Type(), + // 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(), 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( diff --git a/tslang/lib/TypeScript/MLIRGenModule.cpp b/tslang/lib/TypeScript/MLIRGenModule.cpp index d5558bb73..31f87ddbe 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" @@ -260,8 +268,45 @@ 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})); + + // 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, 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; + llvm::Triple targetTriple(compileOptions.moduleTargetTriple); + auto *target = llvm::TargetRegistry::lookupTarget(targetTriple, errorMessage); + if (!target) + { + 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 new file mode 100755 index 000000000..d668553d5 --- /dev/null +++ b/tslang/test/check-datalayout.sh @@ -0,0 +1,55 @@ +#!/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 + +# 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" 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]); 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/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..6ed6bbc25 --- /dev/null +++ b/tslang/unittests/MLIRGen/TargetInfo.cpp @@ -0,0 +1,126 @@ +#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; 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 +{ + +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); +} + +// 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("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 +// 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); +} + +// 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); +} + +// 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 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()