Skip to content

Take pointer width from the target triple (32-bit support, phase 1) - #334

Merged
ASDAlexander77 merged 20 commits into
mainfrom
spec-32-bit-compilation
Sep 21, 2026
Merged

ASDAlexander77 merged 20 commits into
mainfrom
spec-32-bit-compilation

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Summary

The compiler now takes pointer width from the target triple instead of a hand-written list of 64-bit architectures. This is phase 1 of 32-bit support; the design for all four phases is in docs/superpowers/specs/2026-09-20-32-bit-compilation-design.md.

  • TargetInfo (include/TypeScript/TargetInfo.h) is built once from the triple in prepareOptions() and stored on CompileOptions. It holds the pointer width, from Triple::getArchPointerBitWidth(), and three policy flags for later phases. sizeBits is now an accessor over it.
  • The old list was wrong. It marked aarch64_32 as 64-bit (ARM64_32 is ILP32), and it left out systemz, sparcv9, amdgcn and ve, which therefore got 32.
  • TypeHelper::getSizeType() / getPointerIntType(). TypeHelper takes an optional CompileOptions; only the sites that need a width pass it. Without it, getSizeType() calls report_fatal_error in every build configuration, not just Debug.
  • getI64Type() audit. Each of the 18 call sites outside the Win32 RTTI helpers is now marked either target-width (13, changed) or 64-bit whatever the target (5, unchanged). The audit fixed two 32-bit bugs:
    • The -1 sentinel for a missing optional interface member was built at i64 but compared at a different width. At i686 that was invalid IR ('llvm.icmp' op requires all operands to have the same type), which broke 12 test files.
    • A pointer converted to a float went through sitofp, so a 32-bit address at or above 0x80000000 became negative. It is now uitofp.
  • target datalayout is set when the module is generated, so --emit=llvm output names its layout, not only its triple. A triple with no registered target is now an error at this point instead of silently emitting IR without a layout.

Behavior change

--emit=jit -mtriple=<a non-host target> used to run x64 code and exit 0, because the JIT replaced the module's triple with the host's. It now refuses:

tslang: error: --emit=jit runs the code in this process, which is x86_64-pc-windows-msvc, so it cannot run code built for i686-pc-windows-msvc. Build it instead with --emit=exe or --emit=obj.

Nothing under test/ uses --emit=jit with -mtriple.

x64 output

Unchanged, apart from the added target datalayout line.

Not in this PR

The lowering's LLVMTypeConverter is still built with LLVM's default data layout for every non-wasm target, so at i686 struct layout, union sizing and getIntPtrType still assume 8-byte pointers. Fixing that changes x64 output as well: the default layout aligns i64 at 32 bits, x64's real one at 64. So it is left for the first task of phase 2, which blocks any native 32-bit test. The spec records this, together with the wasm32 layout hardcoded in the same place, which is also wrong.

Test plan

  • Windows Release suite: ctest -j 16 -C Release passes 2765/2765 at the head commit.
  • MLIRGenTests: 142/142, including the new TargetInfoTest cases (widths across triples, arches the old list got wrong, EH and JIT policy, TypeHelper::getSizeType).
  • x64 --emit=llvm output diffed against the pre-change compiler: only the added target datalayout line differs.
  • test/check-datalayout.sh (run by hand, not wired into ctest): correct layout for i686-pc-windows-msvc, wasm32-unknown-unknown and x86_64-pc-windows-msvc; an unknown triple fails and names it.
  • 00interface_optional.ts now compiles at i686 (icmp eq i32 …, -1).
  • --emit=jit refuses i686 and wasm32; still runs with no -mtriple and with -mtriple=x86_64-pc-windows-msvc.
  • Linux CI. Not run locally.

🤖 Generated with Claude Code

ASDAlexander77 and others added 20 commits September 20, 2026 22:42
Four-phase plan for compiling to 32-bit targets, with i686-pc-windows-msvc as
the proof target and width-correctness that generalizes to any 32-bit triple.

Probing the current build first moved most of the scope. #333 removing the
LLVMSupport dependency turns out to have been the gate: -mm=none already
produces a working 32-bit .exe. What remains is a 32-bit Boehm and runtime,
the __stdcall decoration on _CxxThrowException, and the image-base subtraction
in the Win32 RTTI helpers, which x86 must not do.

Two assumptions the probing disproved are recorded in the spec, since the
emitted IR reads the other way: the async runtime's i64 parameters are
int64_t rather than size_t and so are already correct on x86, and the
ThrowInfo/CatchableType layouts coincide between the two ABIs because an x86
absolute pointer is the same width as an x64 RVA.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six TDD tasks: TargetInfo and its derivation from the triple, carrying it
on CompileOptions and deleting the rotted arch list, target-width
accessors on TypeHelper, the data layout on the emitted module, the
i64 audit, and the --emit=jit guard.

Deviates from the spec on one point. The spec calls threading
CompileOptions into TypeHelper "a wide but mechanical diff"; counting
found 152 construction sites, nearly all of which only ask for
width-independent types. The options are optional instead, and
getSizeType() asserts rather than guessing, so only the ~20 sites that
need a width are touched.

Every LLVM and MLIR API the plan calls was checked against the headers
in 3rdParty/llvm/x64/release rather than written from memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The JIT guard cited a line inside the dumpToObjectFile path, reached only
after the engine is built - it would never have fired on the ordinary JIT
path. Moved to the top of runJit.

The gate demanded the two RTTI helpers show an empty diff, which Task 2
necessarily violates: it has to change sizeBits to sizeBits() in both.
Narrowed to what the spec actually reserves for Phase 3, the getI64Type
arithmetic.

The audit's count of 20 included the definition of getI64Type itself.
Nineteen are call sites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TargetInfo answers what the compiler needs to know about a target:
pointer width, and the policy questions Win32 EH and the JIT ask. Named
for the decisions rather than the arches, so a new target answers them
instead of each call site growing an arch comparison.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MLIRGenTests.exe lands under unittests/MLIRGen/Debug/, not bin/. bin/
exists and holds tslang.exe, which is why the wrong path looked
plausible. Found by the Task 1 implementer; fixed here so Tasks 2, 3
and 5 do not each rediscover it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tasks 2 and 5 verified no-x64-change by stashing the work and rebuilding
Release to regenerate a 'before' - a second full LLVM link each time,
with uncommitted work on the stash while it ran.

The release binary predating Phase 1 was still on disk, so the baseline
is captured once from it and stored in the plan's workspace. Same check,
one rebuild instead of two, and nothing goes on the stash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
opts.cpp listed 23 architectures it considered 64-bit. The list had
already rotted: loongarch64 was on it, riscv32 appeared nowhere. Triple
already knows, so ask it.

sizeBits stays, as an accessor over TargetInfo, so it cannot drift from
the one place the width is decided. Updated every reader of
compileOptions.sizeBits to call it (grep found the same ~20 call sites
the spec's audit listed, no more).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
I wrote that the list had rotted because loongarch64 was on it and
riscv32 was absent. Both are correct: the list marks arches as 64-bit
and defaults to 32, so loongarch64 belongs on it and riscv32 belongs
off it. Neither was evidence of anything.

The real defect, found by enumerating all 20 entries against LLVM's
width table: aarch64_32 was marked 64-bit. ARM64_32 is ILP32 and its
pointers are 32 bits. Replacing the list with getArchPointerBitWidth()
fixes that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
getSizeType() is for things that are as wide as a pointer; getI64Type()
stays for things that are 64-bit whatever the target is. Keeping both
means the audit in the next commit can record which each site meant.

The options are optional because 152 sites construct TypeHelper and
almost none need a width. getSizeType() asserts rather than guessing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
assert() alone is a no-op under NDEBUG, and this project's CI builds
Release, so a missing CompileOptions was guessing a width via a null
dereference with no message naming the function or the cause. Keep the
assert for fast local diagnosis, add report_fatal_error so the failure
is identifiable in every build configuration.

Also record in getPointerIntType()'s comment why it stays a separate
name from getSizeType() despite currently just delegating to it: the
call-site audit needs to tell the two concepts apart.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The layout was only set in obj.cpp, from the TargetMachine, which is
after --emit=llvm has printed. So IR emitted for a 32-bit triple carried
LLVM's default 64-bit layout, and anything reading sizes between MLIRGen
and obj.cpp was guessing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Task 4 review, round 1. If TargetRegistry::lookupTarget or
createTargetMachine failed, the module was emitted anyway with a
target triple but no datalayout -- exactly the bug this phase exists
to fix, and reachable only from --emit=llvm since dump.cpp never
consults the target registry itself. Both failure modes now emit a
diagnostic naming the triple and fail the codegen, matching how
obj.cpp treats the same lookup.

Also: use the non-deprecated lookupTarget(const Triple&, std::string&)
overload instead of the StringRef one slated for removal after LLVM 22
branches, and qualify the comment's claim that the derived layout
matches obj.cpp's -- true for x86, not guaranteed once obj.cpp threads
a -target-abi into TargetOptions for ABI-sensitive backends.

Added a fourth case to check-datalayout.sh: a garbage triple must now
fail with a non-zero exit and an error naming the triple.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A grep for getI64Type returns 21 hits outside the reserved RTTI
helpers, but three are not call sites: two definitions and one comment
mentioning the name. The plan's count drifted twice for this reason.

The list is now generated once, with exclusions applied, and the task
reads it instead of re-deriving it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each of the 18 non-EH getI64Type() call sites now says which it meant.
Thirteen are pointer and size arithmetic and now ask the target; five are
genuinely 64-bit and keep i64 with a comment saying why.

The one that needed looking up rather than guessing was Boehm's GC_descr:
it is a typedef of GC_word, which gc.h defines as the integer the size of
void *, not a fixed 64 bits. So the whole typed-GC descriptor group moves
together - the global, the bitmap method's return type, the
GCMakeDescriptorOp result and the two extern declarations - or the op and
its callee would disagree on width. The interface -1 slot sentinels move
for the same reason: the lowering that tests them already builds its -1 at
compileOptions.sizeBits().

Left alone: the bigint literal type and the numeric promotion ladder (the
language's own 64-bit integer), _i64toa's first parameter (long long, not
size_t), and the createI64ConstantOf/getI64AttrValue helpers, whose width
is their contract.

x64 IR is unchanged, which is the check that the classification is
right: a target-width site emits i64 on x64 either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This is a multi-config MSBuild generator, so a bare 'ctest -j 16'
reports all 2765 tests as (Not Run) and exits with errors. That reads
as a catastrophic regression and is nothing of the sort.

Found by the Task 5 implementer; confirmed here by running both forms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…contract

Review fallout from classifying the i64 sites, not from the calls themselves.

The pointer->float cast now uses uitofp. While ptrtoint produced a fixed i64
the zero-extension made the signedness moot; now that the integer is only as
wide as the pointer, any address at or above 0x80000000 - reachable in a
/LARGEADDRESSAWARE i686 process or a large wasm32 memory - would have read
back as a negative float. An address is unsigned, which is also what the
ZExtOp two branches down already assumes.

GCMakeDescriptorOp's result and GCNewExplicitlyTypedOp's operand were still
constrained to I64 in the ODS. The generated build() takes the result type
verbatim so construction would have worked, but verification would have
rejected the i32 descriptor those ops now carry on a 32-bit target. They are
the only two I64 operand/result constraints in the file.

Three comments corrected: createI64ConstantOf's claimed an invariant the
codebase does not hold (LowerToLLVM.cpp passes it a byte count), the
promotion-ladder one called getIndexType() fixed-width when it is the one
thing there that is not, and getTypeBitmapValueType() now records that it is
the same GC_word contract and the one remaining width gap in that path,
deliberately left for a later phase.

x64 IR is still byte-identical for both baseline samples; suite 2765/2765.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verified before this change: with the release binary, both
`--emit=jit -mm=none --no-default-lib -mtriple=i686-pc-windows-msvc`
and the same with `-mtriple=wasm32-unknown-unknown` printed "Hello 32"
and exited 0. tslang.exe is an x64 process; jit.cpp:634 overwrites the
module's target triple with the host TargetMachine's, so it silently
ran x64 code while the user asked for i386 or wasm.

Added a guard as the first statement of runJit(), before any LLVM
target initialization, that checks
CompileOptions::targetInfo.supportsInProcessJit (true only when the
requested target's arch and OS match the host's) and refuses with a
message naming both the host triple and the requested one. Left to
LLJIT, the same mismatch surfaces later as a relocation or "symbol not
found" failure deep in the session that never mentions the triple as
the cause.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
InterfaceSymbolRefOpLowering built its ptrtoint at llvmtch.getIntPtrType(0),
which comes from the type converter's data layout - LLVM's default, 64-bit
pointers, for every non-wasm target - while the -1 it compared against was
built at compileOptions.sizeBits(). On i686 that was an i64 against an i32 and
verification failed ("'llvm.icmp' op requires all operands to have the same
type") for any optional interface member.

Both sides now take the width from compileOptions. The ptrtoint has to be at
32 bits on i686: at i64 it would zero-extend the 0xFFFFFFFF sentinel and never
compare equal. The constant is built directly rather than through
createIConstantOf, whose unsigned value parameter would give 0xFFFFFFFF, not
-1, at 64 bits.

x64 IR is unchanged for both baseline samples and 00interface_optional.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment said the old opts.cpp list had rotted because loongarch64 was on
it and riscv32 appeared nowhere. Neither is a fault: the list named 64-bit
arches and defaulted to 32, so loongarch64, riscv32 and armv7 all came out
right. What it got wrong was aarch64_32 listed as 64-bit (already covered by
Arm64_32IsAnIlp32Target) and 64-bit arches it did not list, which it gave 32:
systemz, sparcv9, amdgcn, ve, and also spirv and riscv64be.

PointerWidthIsRightForArchesTheOldListMissed now asserts 64 for s390x,
sparcv9, amdgcn and ve, each a spelling llvm::Triple's parseArch maps to the
intended arch, so it fails against the old list.

Also marks test/check-datalayout.sh executable; it was committed 100644.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Spec: Phase 1 is marked implemented. Drops the parsed llvm::Triple on
CompileOptions and indexBits, neither of which exists. Describes TypeHelper's
optional CompileOptions pointer and getSizeType()'s report_fatal_error in
place of the "wide but mechanical" parameter. Corrects the audit count to 18
call sites, records its result (13 target-width, 5 genuinely 64-bit) and the
two latent bugs it found. Re-scopes the Phase 1 gate: the lowering's type
converter still uses LLVM's default data layout for non-wasm targets, so
64-bit-shaped layout on i686 is not yet gone; deriving that layout from the
module becomes Phase 2's first task, with its own x64 diff. Records the
ABI-by-environment width disagreement as an open issue and the i686
for-await failures as a Phase 4 finding.

Plan: same Triple and getSizeType() corrections, 20-entry arch list, the gate
allows the one added target datalayout line, the 18-site list is inlined, and
the baseline directory is marked local-only scratch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 6eac25f into main Sep 21, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the spec-32-bit-compilation branch September 21, 2026 09:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant