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

Large diffs are not rendered by default.

31 changes: 30 additions & 1 deletion docs/superpowers/specs/2026-09-20-32-bit-compilation-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Design for compiling TypeScript to 32-bit targets, with `i686-pc-windows-msvc`
as the proof target and width-correctness that generalizes to any 32-bit triple.

Status: approved design. Phases 1-3 implemented; phase 4 not.
Status: approved design. Phases 1-3 and 4a implemented; 4b and 4c not.

## Why now

Expand Down Expand Up @@ -261,6 +261,29 @@ Already found: at i686, `00for_await` and `00for_await_yield` fail at
failures are triaged and recorded the way the Debug-suite failures are, not
silently excluded.

**Split.** A corpus probe (`tslang/test/probe-corpus.sh`: every corpus file as
a single-file exe, x64 and i686 × gc/rc/none) found 15 files failing at i686
only, so phase 4 is three PRs:

- **4a — union storage as bytes (done).** A tagged union stored its value as
its largest member's struct; bytes in that struct's padding were not carried
by a value copy, so another member's field there was lost — at x64 for some
layouts, at i686 for common ones (a pointer followed by an 8-aligned `f64`).
The value field is now a packed `<{[N/P x iP], [N%P x i8]}>` (P the pointer
size), with the unused tail zeroed when a smaller member is stored. Also
fixed: `castLLVMTypes` copied small aggregates as one pointer (4 bytes at
i686), and a module-level union initialized with a constant now gets a
global constructor instead of failing to compile. 29 probe rows newly pass,
none regress; x64 `.text` is unchanged within 0.03%.
- **4b — async at i686.**
- **4c — the suite at i686:** test-runner arch flag, x86 default library,
reading `__decls` from a DLL file, and the remaining failures.

Remaining i686-only probe failures after 4a: the six async files (4b);
`internals` (an `inline_asm<i64>` with an `=r` constraint, which no single
i686 register can satisfy); `02funcs_vararg` (unresolved `_printf`; it is
commented out of the suite).

## Error handling

Three failures get explicit, actionable messages instead of downstream
Expand Down Expand Up @@ -312,6 +335,12 @@ confusion:
process exits 127 with its earlier output unflushed. `throw <any>x` and class
objects are caught. Pre-existing; `eh_order.ts` uses `catch (e: TypeOf<1>)`.

- **A global initialized from a DLL-exported global reads garbage (not 32-bit;
x64 too).** Found in phase 4a review: in an importer, `const copiedN: number
= cN` where `cN` is exported by a `-shared` module prints a denormal (e.g.
3.04e-312) with both the old and new compiler; a union copy crashes silently.
Likely the importer's global constructor runs before the import is bound.

## Out of scope

- `-m32` shorthand. `-mtriple=i686-pc-windows-msvc` already works and is the
Expand Down
59 changes: 28 additions & 31 deletions tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -560,23 +560,6 @@ class CastLogicHelper
}
}

/*
// TODO: we do not need as struct can cast to struct
if (auto inUnionType = dyn_cast<mlir_ts::UnionType>(inType))
{
if (auto resUnionType = dyn_cast<mlir_ts::UnionType>(resType))
{
LLVMTypeConverterHelper ltch((LLVMTypeConverter &)tch.typeConverter);
auto maxStoreType = ltch.findMaxSizeType(inUnionType);
auto value = rewriter.create<mlir_ts::GetValueFromUnionOp>(loc, maxStoreType, in);
auto typeOfValue =
rewriter.create<mlir_ts::GetTypeInfoFromUnionOp>(loc, mlir_ts::StringType::get(rewriter.getContext()), in);
auto unionValue = rewriter.create<mlir_ts::CreateUnionInstanceOp>(loc, resType, value, typeOfValue);
return unionValue;
}
}
*/

if (auto resUnionType = dyn_cast<mlir_ts::UnionType>(resType))
{
// TODO: do I need to test income types?
Expand Down Expand Up @@ -619,9 +602,11 @@ class CastLogicHelper
bool needTagRes = mth.isUnionTypeNeedsTag(loc, resUnionType, baseTypeRes);
if (needTagRes)
{
// carry the source's value field as it is stored - its bytes, whichever
// member they hold - not as one member's type, whose padding a value copy drops
LLVMTypeConverterHelper ltch((const LLVMTypeConverter *)tch.typeConverter);
auto maxStoreType = ltch.findMaxSizeType(inUnionType);
auto value = rewriter.create<mlir_ts::GetValueFromUnionOp>(loc, maxStoreType, in);
auto storageType = ltch.getUnionStorageType(inUnionType);
auto value = rewriter.create<mlir_ts::GetValueFromUnionOp>(loc, storageType, in);
auto typeOfValue = rewriter.create<mlir_ts::GetTypeInfoFromUnionOp>(loc, mlir_ts::StringType::get(rewriter.getContext()), in);
auto unionValue = rewriter.create<mlir_ts::CreateUnionInstanceOp>(loc, resType, value, typeOfValue);
return unionValue;
Expand Down Expand Up @@ -772,7 +757,11 @@ class CastLogicHelper
return mlir::Value();
}

mlir::Value castLLVMTypes(mlir::Value in, mlir::Type inLLVMType, mlir::Type resType, mlir::Type resLLVMType)
// `sizesMayDiffer`: the caller reinterprets on purpose between aggregates of different sizes
// - a union member going into or out of a tagged union's storage - so a size mismatch is not
// worth a warning.
mlir::Value castLLVMTypes(mlir::Value in, mlir::Type inLLVMType, mlir::Type resType, mlir::Type resLLVMType,
bool sizesMayDiffer = false)
{
if (inLLVMType == resLLVMType)
{
Expand All @@ -795,7 +784,7 @@ class CastLogicHelper
auto srcSize = llvmtch.getTypeAllocSizeInBytes(inLLVMType);
auto dstSize = llvmtch.getTypeAllocSizeInBytes(resLLVMType);

if (srcSize != dstSize)
if (srcSize != dstSize && !sizesMayDiffer)
{
op->emitWarning("types have different sizes:\n ")
<< inLLVMType << " size of #" << srcSize << ",\n " << resLLVMType << " size of #" << dstSize;
Expand All @@ -805,15 +794,21 @@ class CastLogicHelper
loc, mlir_ts::RefType::get(inType), in, rewriter.getBoolAttr(false), rewriter.getIndexAttr(0));
auto dstAddr = rewriter.create<mlir_ts::VariableOp>(loc, mlir_ts::RefType::get(resType),
mlir::Value(), rewriter.getBoolAttr(false), rewriter.getIndexAttr(0));
if (srcSize <= 8 && dstSize <= 8)
{
rewriter.create<mlir_ts::LoadSaveOp>(loc, dstAddr, srcAddr);
}
else

if (dstSize > srcSize)
{
rewriter.create<mlir_ts::CopyStructOp>(loc, dstAddr, srcAddr);
// the copy fills only the first srcSize bytes: zero the rest rather than leave
// it undefined - a union's storage is compared bytewise once boxed into `any`,
// and stale bytes could look like pointers to a conservative collector
auto dstPtr = rewriter.create<mlir_ts::DialectCastOp>(loc, tch.convertType(dstAddr.getType()), dstAddr);
auto zero = rewriter.create<LLVM::ZeroOp>(loc, resLLVMType);
rewriter.create<LLVM::StoreOp>(loc, zero, dstPtr);
}

// copies min(srcSize, dstSize) bytes; LLVM turns a constant-size copy into plain
// loads and stores
rewriter.create<mlir_ts::CopyStructOp>(loc, dstAddr, srcAddr);

auto val = rewriter.create<mlir_ts::LoadOp>(loc, resType, dstAddr);
return val;
}
Expand Down Expand Up @@ -1117,10 +1112,12 @@ class CastLogicHelper
typeOfValue = toh.typeOfLogic(loc, valueForBoxing, unionType, compileOptions);

LLVMTypeConverterHelper llvmtch((const LLVMTypeConverter *)tch.typeConverter);
// so we need to get biggest value from Union
auto maxUnionType = llvmtch.findMaxSizeType(unionType);
LLVM_DEBUG(llvm::dbgs() << "\n!! max size union type: " << maxUnionType << "\n";);
valueForBoxing = rewriter.create<mlir_ts::GetValueFromUnionOp>(loc, maxUnionType, in);
// box the union's value field whole, as its bytes: unboxing reads it back as
// whichever member the tag names, and a member type's padding would drop bytes
// another member keeps there
auto storageType = llvmtch.getUnionStorageType(unionType);
LLVM_DEBUG(llvm::dbgs() << "\n!! union storage type: " << storageType << "\n";);
valueForBoxing = rewriter.create<mlir_ts::GetValueFromUnionOp>(loc, storageType, in);
}
else
{
Expand Down
38 changes: 38 additions & 0 deletions tslang/include/TypeScript/LowerToLLVM/LLVMTypeConverterHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,44 @@ class LLVMTypeConverterHelper
return selectedType;
}

// Bytes a union's value field must hold: the largest alloc size among its members.
unsigned getUnionStorageSize(mlir_ts::UnionType unionType)
{
unsigned size = 0;
for (auto subType : unionType.getTypes())
{
size = std::max(size, (unsigned)getTypeAllocSizeInBytes(typeConverter->convertType(subType)));
}

return size;
}

// The LLVM type of a tagged union's value field: N bytes, N being the largest member's alloc
// size, as a packed <{ [N / P x iP], [N % P x i8] }> with P the pointer size in bytes (an
// empty part is left out). Being packed, it has no padding, so every byte of every member is
// carried by a value copy (see the UnionType conversion in LowerToLLVM.cpp). Pointer-sized
// words rather than one [N x i8]: LLVM passes an array argument element by element, and a
// byte array would become N separate arguments.
mlir::Type getUnionStorageType(mlir_ts::UnionType unionType)
{
auto *context = &typeConverter->getContext();
auto size = getUnionStorageSize(unionType);
auto wordBytes = getPointerBitwidth(0) / 8;

SmallVector<mlir::Type> parts;
if (size / wordBytes > 0)
{
parts.push_back(LLVM::LLVMArrayType::get(mlir::IntegerType::get(context, wordBytes * 8), size / wordBytes));
}

if (size % wordBytes > 0)
{
parts.push_back(LLVM::LLVMArrayType::get(mlir::IntegerType::get(context, 8), size % wordBytes));
}

return LLVM::LLVMStructType::getLiteral(context, parts, /*isPacked=*/true);
}

const LLVMTypeConverter *typeConverter;
};
} // namespace typescript
Expand Down
41 changes: 34 additions & 7 deletions tslang/lib/TypeScript/LowerToLLVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2166,7 +2166,7 @@ struct CreateUnionInstanceOpLowering : public TsLlvmPattern<mlir_ts::CreateUnion
rewriter.create<LLVM::InsertValueOp>(loc, udefVal, transformed.getTypeInfo(), MLIRHelper::getStructIndex(rewriter, UNION_TAG_INDEX));
auto val1 = rewriter.create<LLVM::InsertValueOp>(loc, val0, in, MLIRHelper::getStructIndex(rewriter, UNION_VALUE_INDEX));

auto casted = castLogic.castLLVMTypes(val1, unionPartialType, op.getType(), resType);
auto casted = castLogic.castLLVMTypes(val1, unionPartialType, op.getType(), resType, /*sizesMayDiffer=*/true);
if (!casted)
{
return mlir::failure();
Expand Down Expand Up @@ -2211,7 +2211,7 @@ struct GetValueFromUnionOpLowering : public TsLlvmPattern<mlir_ts::GetValueFromU
// TODO: should not cast anything
CastLogicHelper castLogic(op, rewriter, tch, tsLlvmContext->compileOptions);
auto casted = castLogic.castLLVMTypes(transformed.getIn(), transformed.getIn().getType(), unionPartialType,
unionPartialType);
unionPartialType, /*sizesMayDiffer=*/true);
if (!casted)
{
return mlir::failure();
Expand Down Expand Up @@ -4025,7 +4025,10 @@ struct GlobalOpLowering : public TsLlvmPattern<mlir_ts::GlobalOp>
isa<mlir_ts::ArrayPushOp>(op) || isa<mlir_ts::ArrayUnshiftOp>(op) || isa<mlir_ts::ArraySpliceOp>(op) ||
isa<mlir_ts::ArrayPopOp>(op) || isa<mlir_ts::ArrayShiftOp>(op) || isa<mlir_ts::DeleteOp>(op) ||
isa<mlir_ts::SetLengthOfOp>(op) || isa<mlir_ts::SetStringLengthOp>(op) ||
isa<mlir_ts::StringConcatOp>(op) || isa<mlir_ts::CharToStringOp>(op))
isa<mlir_ts::StringConcatOp>(op) || isa<mlir_ts::CharToStringOp>(op) ||
// a tagged union's value goes in and out through memory (an alloca and a copy),
// which a global's initializer cannot hold
isa<mlir_ts::CreateUnionInstanceOp>(op) || isa<mlir_ts::GetValueFromUnionOp>(op))
{
createAsGlobalConstructor = true;
}
Expand All @@ -4036,6 +4039,22 @@ struct GlobalOpLowering : public TsLlvmPattern<mlir_ts::GlobalOp>
{
createAsGlobalConstructor = true;
}
else
{
// see CreateUnionInstanceOp above: a cast to or from a tagged union lowers to
// one of those ops
MLIRTypeHelper mth(rewriter.getContext(), tsLlvmContext->compileOptions);
for (auto type : {castType, castOp.getIn().getType()})
{
if (auto unionType = dyn_cast<mlir_ts::UnionType>(type))
{
if (mth.isUnionTypeNeedsTag(castOp->getLoc(), unionType))
{
createAsGlobalConstructor = true;
}
}
}
}
}

// TODO: error in try-catch
Expand Down Expand Up @@ -6691,10 +6710,18 @@ static void populateTypeScriptConversionPatterns(LLVMTypeConverter &converter, m
LLVMTypeConverterHelper ltch(&converter);
MLIRTypeHelper mth(m.getContext(), compileOptions);

mlir::Type selectedType = ltch.findMaxSizeType(type);
bool needTag = mth.isUnionTypeNeedsTag(mlir::UnknownLoc::get(type.getContext()), type);

LLVM_DEBUG(llvm::dbgs() << "\n!! max size type in union: " << selectedType
// With a tag, the value field is padding-free storage as large as the largest member
// (packed words plus a byte tail, see getUnionStorageType), not that member's struct
// type. A value copy of a struct does not carry its padding, and another member's field
// can sit in that padding: at 32-bit a member's f64 lands in the storage member's padding
// after a 4-byte pointer, and it read back as garbage once the union was copied. Without
// a tag there is no real union - the members share one base type - and that type is kept
// as it is.
mlir::Type selectedType = needTag ? ltch.getUnionStorageType(type) : ltch.findMaxSizeType(type);

LLVM_DEBUG(llvm::dbgs() << "\n!! storage type in union: " << selectedType
<< "\n size: " << ltch.getTypeAllocSizeInBytes(selectedType) << "\n Tag: " << (needTag ? "yes" : "no")
<< "\n union type: " << type << "\n";);

Expand Down Expand Up @@ -7222,8 +7249,8 @@ void TypeScriptToLLVMLoweringPass::runOnOperation()
// 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
// null, 1, which LLVM folds with the module's layout), but union storage sizing
// (getUnionStorageSize), 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)
Expand Down
Loading
Loading