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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
477 changes: 477 additions & 0 deletions docs/superpowers/plans/2026-09-21-32-bit-phase-2-native-libs.md

Large diffs are not rendered by default.

56 changes: 47 additions & 9 deletions docs/superpowers/specs/2026-09-20-32-bit-compilation-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ 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}/`

Those are the build trees. The compiler finds the x86 libraries in the `x86`
subdirectory of the lib path it is given, so the gc scripts also copy their
output under the x64 lib directories (see phase 2, "As built").

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
Expand Down Expand Up @@ -155,18 +159,42 @@ 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.
**As built.** The lookup rule is one path for both machines: for an x86
target, the compiler takes each library from the `x86` subdirectory of the lib
path it is given (`--gc-lib-path`, `--gc-shared-lib-path`, `--tslang-lib-path`
or their environment variables). The x64 layout stays flat and unchanged.

| Library | Built by | Installed to | Copied to (what the compiler reads) | Flag value, for x64 and x86 alike |
| --- | --- | --- | --- | --- |
| static Boehm `gc.lib` | `scripts\build_gc_<cfg>_vs_x86.bat` | `3rdParty/gc/x86/<cfg>/` | `3rdParty/gc/x64/<cfg>/lib/x86/gc.lib` | `--gc-lib-path=3rdParty/gc/x64/<cfg>/lib` |
| Boehm DLL: `gc.lib` + `gc.dll` | `scripts\build_gc_<cfg>_shared_vs_x86.bat` | `3rdParty/gcdll/x86/<cfg>/` (`lib/`, `bin/`) | `3rdParty/gcdll/x64/<cfg>/lib/x86/` (both files) | `--gc-shared-lib-path=3rdParty/gcdll/x64/<cfg>/lib` |
| `TypeScriptAsyncRuntime.lib` | `scripts\build_tslang_runtime_<cfg>_x86.bat` | `__build/tslang-runtime/<cfg>/x86/` | (installed in place) | `--tslang-lib-path=__build/tslang-runtime/<cfg>` |

`<cfg>` is `release` for `--opt` and `debug` otherwise, the same choice that
picks the CRT. Each gc script installs into its own x86 tree, then copies into
the x86 subdirectory of the matching x64 lib directory. It adds only that
subdirectory; the x64 files are not touched. The shared build's `gc.dll` goes
beside `gc.lib` because the compiler looks for `gc.dll` next to the import
library first and in `../bin` second. From `lib/x86`, `../bin` would be
`lib/bin`, which does not exist. So the value users already pass for x64
(the `lib` directory, as the test suite does) works for x86 unchanged.
`prepare_3rdParty.bat <cfg> x86` checks the copied location, so an x86 build
made before the copy existed is re-run and gets the copy.

There is no x86 `TypeScriptRuntime` (the JIT DLL): `--emit=jit` refuses a
non-host arch, so nothing would load it.

`tslang/exe.cpp` resolves the three paths for the target, not the host. For
Windows x86 and x64 it reads the COFF machine of the library the link will use
(`Dump::coffMachine`), and of the `gc.dll` it picked. A mismatch is a hard error
naming the file, its machine and the target's, rather than LNK4272 plus a list
of unresolved symbols. A missing x86 build is a hard error naming the script
and the directory to pass. `tslang/test/check-x86-libs.sh` covers the error
paths. `tslang/test/check-x86-run.sh` links and runs 32-bit programs from the
real layout under `-mm=gc`, `rc` and `none`.

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

Expand Down Expand Up @@ -233,6 +261,16 @@ confusion:
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()`.
- **Importing an x86 DLL (phase 4).** An x86 `--emit=dll` builds, but compiling
a program that imports it fails at compile time with
`./lib.dll: Can't open: Unknown error (0xC1)`.
`MLIRGenImpl::mlirGenImportSharedLib` calls `getPermanentLibrary`, which loads
the x86 DLL into the x64 compiler process to read its `__decls`. The fix is to
read `__decls` from the file rather than by loading it.
- **`00for_await` at i686 (phase 4).** A likely cause of the leftover
`unrealized_conversion_cast`: upstream `ConvertAsyncToLLVMPass`
(`AsyncToLLVM.cpp:1026`) builds `LowerToLLVMOptions(ctx)` with LLVM's default
layout and a 64-bit index, so its types disagree with the i686 type converter's.

## Out of scope

Expand Down
23 changes: 23 additions & 0 deletions prepare_3rdParty.bat
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ if not "%1"=="" (
set BUILD=%1
)

set ARCH=x64
if not "%2"=="" set ARCH=%2

set GC_VER=8.2.12
set LIBATOMIC_OPS_VER=7.10.0

Expand Down Expand Up @@ -50,4 +53,24 @@ IF EXIST ".\3rdParty\gcdll\x64\%BUILD%\lib\gc.lib" (
) ELSE (
cd %p%
@call scripts\build_gc_%BUILD%_shared_%TOOL%.bat
)

rem x86 (Win32) Boehm, for compiling 32-bit programs (-mtriple=i686-pc-windows-msvc). Opt-in:
rem prepare_3rdParty.bat release x86
rem Checked where the compiler looks: each x86 script also copies its output into the x86
rem subdirectory of the x64 lib directory (one --gc-lib-path / --gc-shared-lib-path serves both
rem machines). A build from before that copy existed is re-run, which redoes the copy.
IF "%ARCH%"=="x86" (
IF EXIST ".\3rdParty\gc\x64\%BUILD%\lib\x86\gc.lib" (
echo "No need to build x86 GC (%BUILD%)"
) ELSE (
cd %p%
@call scripts\build_gc_%BUILD%_%TOOL%_x86.bat
)
IF EXIST ".\3rdParty\gcdll\x64\%BUILD%\lib\x86\gc.dll" (
echo "No need to build x86 shared GC (%BUILD%)"
) ELSE (
cd %p%
@call scripts\build_gc_%BUILD%_shared_%TOOL%_x86.bat
)
)
23 changes: 23 additions & 0 deletions scripts/build_gc_debug_shared_vs_x86.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@rem Debug counterpart of build_gc_release_shared_vs_x86.bat - x86 (Win32) Boehm built as a DLL,
@rem for debug (no --opt) x86 -shared links and programs that import a shared library.
@rem See that script for why the shared collector exists at all (item 5ao: two statically
@rem linked collectors free each other's objects).
@rem
@rem Installs into 3rdParty\gcdll\x86\debug, then copies the import library gc.lib and gc.dll into
@rem the x86 subdirectory of the x64 lib directory, 3rdParty\gcdll\x64\debug\lib\x86, so one flag
@rem serves both machines:
@rem --gc-shared-lib-path=3rdParty\gcdll\x64\debug\lib
@rem Only that x86 subdirectory is added; the x64 files are not touched.
@rem
@rem MultiThreadedDebug (/MTd) to match the debug linker, which links libcmtd/libvcruntimed/
@rem libucrtd - mixing static and dynamic, or debug and release, CRTs crashes at startup.
pushd
mkdir __build\gcdll\msbuild\x86\debug
cd __build\gcdll\msbuild\x86\debug
cmake ../../../../../3rdParty/gc-8.2.12 -G "Visual Studio 18 2026" -A Win32 %EXTRA_PARAM% -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=ON -Wno-dev -DCMAKE_INSTALL_PREFIX=../../../../../3rdParty/gcdll/x86/debug -Denable_threads=ON -Denable_cplusplus=OFF -DCMAKE_POLICY_DEFAULT_CMP0091=NEW -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebug
cmake --build . --config Debug -j 8
cmake --install . --config Debug
popd
if not exist "%~dp0..\3rdParty\gcdll\x64\debug\lib\x86" mkdir "%~dp0..\3rdParty\gcdll\x64\debug\lib\x86"
copy /Y "%~dp0..\3rdParty\gcdll\x86\debug\lib\gc.lib" "%~dp0..\3rdParty\gcdll\x64\debug\lib\x86\gc.lib"
copy /Y "%~dp0..\3rdParty\gcdll\x86\debug\bin\gc.dll" "%~dp0..\3rdParty\gcdll\x64\debug\lib\x86\gc.dll"
18 changes: 18 additions & 0 deletions scripts/build_gc_debug_vs_x86.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@rem x86 (Win32) Boehm, static, for debug (no --opt) 32-bit programs (-mtriple=i686-pc-windows-msvc).
@rem
@rem Installs into 3rdParty\gc\x86\debug, then copies gc.lib into the x86 subdirectory of the
@rem x64 lib directory, 3rdParty\gc\x64\debug\lib\x86. For an x86 target the compiler looks in
@rem the x86 subdirectory of the lib path it is given, so one flag serves both machines:
@rem --gc-lib-path=3rdParty\gc\x64\debug\lib
@rem Only that x86 subdirectory is added; the x64 files are not touched.
@rem
@rem MultiThreadedDebug (/MTd) to match the debug linker, which links libcmtd/libvcruntimed/libucrtd.
pushd
mkdir __build\gc\msbuild\x86\debug
cd __build\gc\msbuild\x86\debug
cmake ../../../../../3rdParty/gc-8.2.12 -G "Visual Studio 18 2026" -A Win32 %EXTRA_PARAM% -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=OFF -Wno-dev -DCMAKE_INSTALL_PREFIX=../../../../../3rdParty/gc/x86/debug -Denable_threads=ON -Denable_cplusplus=OFF -DCMAKE_POLICY_DEFAULT_CMP0091=NEW -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebug
cmake --build . --config Debug -j 20
cmake --install . --config Debug
popd
if not exist "%~dp0..\3rdParty\gc\x64\debug\lib\x86" mkdir "%~dp0..\3rdParty\gc\x64\debug\lib\x86"
copy /Y "%~dp0..\3rdParty\gc\x86\debug\lib\gc.lib" "%~dp0..\3rdParty\gc\x64\debug\lib\x86\gc.lib"
28 changes: 28 additions & 0 deletions scripts/build_gc_release_shared_vs_x86.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@rem x86 (Win32) Boehm built as a DLL, for release (--opt) 32-bit -shared links and programs that
@rem import a shared library (-mtriple=i686-pc-windows-msvc).
@rem
@rem A program that loads a tslang shared library ends up with TWO collectors when both the
@rem executable and the library link gc.lib statically: each has its own heap and its own idea
@rem of what the roots are. Objects the library allocates are then invisible to the executable's
@rem roots, so the collector frees strings the executable is still holding. See item 5ao in
@rem tslang/docs/reference-counting-evaluation.md.
@rem
@rem Static linking stays the default and is correct on its own - one binary, one collector.
@rem This build exists for the shared case, where one collector has to be shared too.
@rem
@rem Installs into 3rdParty\gcdll\x86\release, then copies the import library gc.lib and gc.dll
@rem into the x86 subdirectory of the x64 lib directory, 3rdParty\gcdll\x64\release\lib\x86. For an
@rem x86 target the compiler looks in the x86 subdirectory of the lib path it is given and finds
@rem gc.dll beside gc.lib there, so one flag serves both machines:
@rem --gc-shared-lib-path=3rdParty\gcdll\x64\release\lib
@rem Only that x86 subdirectory is added; the x64 files are not touched.
pushd
mkdir __build\gcdll\msbuild\x86\release
cd __build\gcdll\msbuild\x86\release
cmake ../../../../../3rdParty/gc-8.2.12 -G "Visual Studio 18 2026" -A Win32 %EXTRA_PARAM% -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -Wno-dev -DCMAKE_INSTALL_PREFIX=../../../../../3rdParty/gcdll/x86/release -Denable_threads=ON -Denable_cplusplus=OFF -DCMAKE_POLICY_DEFAULT_CMP0091=NEW -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded
cmake --build . --config Release -j 8
cmake --install . --config Release
popd
if not exist "%~dp0..\3rdParty\gcdll\x64\release\lib\x86" mkdir "%~dp0..\3rdParty\gcdll\x64\release\lib\x86"
copy /Y "%~dp0..\3rdParty\gcdll\x86\release\lib\gc.lib" "%~dp0..\3rdParty\gcdll\x64\release\lib\x86\gc.lib"
copy /Y "%~dp0..\3rdParty\gcdll\x86\release\bin\gc.dll" "%~dp0..\3rdParty\gcdll\x64\release\lib\x86\gc.dll"
16 changes: 16 additions & 0 deletions scripts/build_gc_release_vs_x86.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@rem x86 (Win32) Boehm, static, for release (--opt) 32-bit programs (-mtriple=i686-pc-windows-msvc).
@rem
@rem Installs into 3rdParty\gc\x86\release, then copies gc.lib into the x86 subdirectory of the
@rem x64 lib directory, 3rdParty\gc\x64\release\lib\x86. For an x86 target the compiler looks in
@rem the x86 subdirectory of the lib path it is given, so one flag serves both machines:
@rem --gc-lib-path=3rdParty\gc\x64\release\lib
@rem Only that x86 subdirectory is added; the x64 files are not touched.
pushd
mkdir __build\gc\msbuild\x86\release
cd __build\gc\msbuild\x86\release
cmake ../../../../../3rdParty/gc-8.2.12 -G "Visual Studio 18 2026" -A Win32 %EXTRA_PARAM% -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -Wno-dev -DCMAKE_INSTALL_PREFIX=../../../../../3rdParty/gc/x86/release -Denable_threads=ON -Denable_cplusplus=OFF -DCMAKE_POLICY_DEFAULT_CMP0091=NEW -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded
cmake --build . --config Release -j 8
cmake --install . --config Release
popd
if not exist "%~dp0..\3rdParty\gc\x64\release\lib\x86" mkdir "%~dp0..\3rdParty\gc\x64\release\lib\x86"
copy /Y "%~dp0..\3rdParty\gc\x86\release\lib\gc.lib" "%~dp0..\3rdParty\gc\x64\release\lib\x86\gc.lib"
9 changes: 9 additions & 0 deletions scripts/build_tslang_runtime_debug_x86.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@rem TypeScriptAsyncRuntime for x86, linked into 32-bit programs (-mtriple=i686-pc-windows-msvc).
@rem Needs 3rdParty\gc\x86\debug (scripts\build_gc_debug_vs_x86.bat). Installs into
@rem __build\tslang-runtime\debug\x86; pass --tslang-lib-path=__build\tslang-runtime\debug
@rem (the parent: for an x86 target the compiler looks in its x86 subdirectory).
pushd %~dp0..
cmake -S tslang\runtime -B __build\tslang-runtime\msbuild\x86\debug -G "Visual Studio 18 2026" -A Win32 -Wno-dev -DTSLANG_GC_INCLUDE=%cd%\3rdParty\gc\x86\debug\include -DTSLANG_MLIR_INCLUDE=%cd%\3rdParty\llvm\x64\debug\include -DCMAKE_INSTALL_PREFIX=%cd%\__build\tslang-runtime\debug\x86
cmake --build __build\tslang-runtime\msbuild\x86\debug --config Debug -j 8
cmake --install __build\tslang-runtime\msbuild\x86\debug --config Debug
popd
9 changes: 9 additions & 0 deletions scripts/build_tslang_runtime_release_x86.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@rem TypeScriptAsyncRuntime for x86, linked into 32-bit programs (-mtriple=i686-pc-windows-msvc).
@rem Needs 3rdParty\gc\x86\release (scripts\build_gc_release_vs_x86.bat). Installs into
@rem __build\tslang-runtime\release\x86; pass --tslang-lib-path=__build\tslang-runtime\release
@rem (the parent: for an x86 target the compiler looks in its x86 subdirectory).
pushd %~dp0..
cmake -S tslang\runtime -B __build\tslang-runtime\msbuild\x86\release -G "Visual Studio 18 2026" -A Win32 -Wno-dev -DTSLANG_GC_INCLUDE=%cd%\3rdParty\gc\x86\release\include -DTSLANG_MLIR_INCLUDE=%cd%\3rdParty\llvm\x64\release\include -DCMAKE_INSTALL_PREFIX=%cd%\__build\tslang-runtime\release\x86
cmake --build __build\tslang-runtime\msbuild\x86\release --config Release -j 8
cmake --install __build\tslang-runtime\msbuild\x86\release --config Release
popd
5 changes: 5 additions & 0 deletions tslang/include/TypeScript/ObjDumper.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ namespace Dump
// rather than importing one from gc.dll / its host or having none. Read from the binary itself,
// so it answers for libraries linked by hand or by an older tslang as well.
bool containsGarbageCollector(llvm::StringRef);

// The COFF machine (IMAGE_FILE_MACHINE_*) of an object file, a static library or an import
// library - for a library, that of its first member that names one - or 0 when there is none
// to be read. A query: an unreadable or non-COFF file is 0, not an error.
uint16_t coffMachine(llvm::StringRef);
}

std::unique_ptr<Dumper> createCOFFDumper(const COFFObjectFile &);
Expand Down
36 changes: 28 additions & 8 deletions tslang/lib/TypeScript/LowerToLLVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7217,6 +7217,33 @@ void TypeScriptToLLVMLoweringPass::runOnOperation()
{
auto m = getOperation();

// The type converter sizes pointers, structs and unions and computes alignment with this layout,
// so it has to be the target's. MLIRGenModule has already put the TargetMachine's layout on the
// module. LLVM's default layout - what this used before - has 8-byte pointers and a 4-byte-aligned
// i64, wrong for i686 in the first and for x64 in the second, so a module without the attribute is
// an error rather than a fallback. SizeOfOp does not read this layout (it emits getelementptr
// null, 1, which LLVM folds with the module's layout), but union storage selection
// (findMaxSizeType), getIntPtrType and debug-info offsets do. Checked before anything else, so
// the pass fails before it has changed the module (the @dllname renames below).
auto dataLayoutAttr = m->getAttrOfType<mlir::StringAttr>(mlir::LLVM::LLVMDialect::getDataLayoutAttrName());
if (!dataLayoutAttr)
{
m.emitError() << "module has no '" << mlir::LLVM::LLVMDialect::getDataLayoutAttrName()
<< "' attribute; MLIRGenModule sets it from the target triple's TargetMachine, and lowering "
"will not guess a layout";
signalPassFailure();
return;
}

auto parsedDataLayout = llvm::DataLayout::parse(dataLayoutAttr.getValue());
if (!parsedDataLayout)
{
m.emitError() << "invalid data layout '" << dataLayoutAttr.getValue()
<< "': " << llvm::toString(parsedDataLayout.takeError());
signalPassFailure();
return;
}

// @dllname on a global: unlike a function, an LLVM global carries no string attribute
// ExportFixPass could rename it by, so rename the symbol and its uses here instead
SmallVector<mlir_ts::GlobalOp> renamedGlobals;
Expand Down Expand Up @@ -7254,14 +7281,7 @@ void TypeScriptToLLVMLoweringPass::runOnOperation()
// doing more complicated lowerings, involving loop region arguments.
mlir::DataLayout dl(m);
LowerToLLVMOptions options(&getContext(), dl);
if (tsContext.compileOptions.isWasm && tsContext.compileOptions.sizeBits() == 32)
{
options.dataLayout = llvm::DataLayout("e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-f128:64-n32:64-S128-ni:1:10:20");

m->setAttr(
mlir::LLVM::LLVMDialect::getDataLayoutAttrName(),
mlir::StringAttr::get(&getContext(), "e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-f128:64-n32:64-S128-ni:1:10:20"));
}
options.dataLayout = *parsedDataLayout;

options.allocLowering = LowerToLLVMOptions::AllocLowering::AlignedAlloc;
DataLayoutAnalysis analysis(m);
Expand Down
Loading
Loading