Repository navigation
Store union values without padding (32-bit support, phase 4a) - #337
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add tslang/test/probe-corpus.sh, which compiles and runs every file in the tester corpus as a single-file exe for x64 and i686 under gc, rc and none, and writes a TSV. Its --compare mode lists every row that passed in a baseline and does not pass now. Add 00union_member_padding, whose union members have fields that sit in another member's padding (at x64 and at i686). Re-arm the `a` assert in 00bigint_struct_layout, which fails for the same reason. The suite is red on these two tests until union storage becomes a byte array (Phase 4a, Task 2). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A tagged union lowered to a packed { tag, S }, where S was the LLVM type
of its largest member. A value copy of a struct does not carry its
padding, so a smaller member's field that sat in S's padding was lost
whenever the union was copied as a value. At x64 this hit only some
layouts (00bigint_struct_layout could not assert its field `a`); at i686,
where a pointer is 4 bytes and an f64 after it is 8-aligned, it broke
ordinary code: 02union_type printed "Error 9.11037e-305 downloading".
S is now [N x i8], N being the largest member's alloc size, so the union
keeps its size and every byte is carried. Members still go in and out
through memory. Readers audited and fixed:
- union -> union casts and union -> any boxing extracted the value as
the largest member's type, dropping the same padding; they now carry
the source's byte storage.
- castLLVMTypes' through-memory aggregate path now accepts arrays as
well as structs.
- that path used LoadSaveOp, which moves one pointer-sized value, for
aggregates up to 8 bytes; at i686 that dropped the value of an 8-byte
{ tag, [4 x i8] } union. The limit is now the pointer size (unchanged
at x64).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Global initializers: a tagged union's value goes in and out through
memory, which an llvm.mlir.global initializer cannot hold, so a
module-level or class-static union initialized with a constant failed
with "ops with side effects not allowed in global initializers". A
CreateUnionInstance, a GetValueFromUnion, or a cast to or from a tagged
union now makes the global use a global constructor. New test
00union_global_init.
- Storage is packed <{ [N / P x iP], [N % P x i8] }> (P = pointer size)
instead of [N x i8]: LLVM passes an array argument element by element,
so a byte array turned a union argument into N byte arguments.
- castLLVMTypes always copies aggregates with CopyStructOp. LoadSaveOp
moved exactly one pointer's worth of bytes, wrong for anything smaller.
- When the destination is larger than the source, its tail is zeroed
before the copy. Boxed into `any`, a union's storage is compared
bytewise, and stale bytes made equal numbers compare unequal (i686 -O0).
- The array case added to castLLVMTypes is reverted; nothing reaches it.
- The "types have different sizes" warning is silenced for a member going
into or out of a union, where the sizes differ by design.
- Stale findMaxSizeType references removed from a comment and dead code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 4 is split into 4a (union byte storage, done), 4b (async) and 4c (the suite at i686). Records the remaining i686-only probe failures and a pre-existing DLL-global init bug found in review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 4a of 32-bit support: a tagged union now reads back every member's fields exactly, on x64 and on i686. This fixes a miscompile that has been in x64 all along. The plan is
docs/superpowers/plans/2026-09-22-32-bit-phase-4a-union-byte-storage.md.The defect
A tagged union lowered to a packed
{ptr tag, S}, whereSwas the LLVM struct of its largest member. Members go in and out through memory, but any bytes that fall inS's padding are not part of the value once it is loaded asS. So a smaller member's field stored there was lost the next time the union was copied.{flag; a: s32; b: s32} | {flag; v: number},aread back as 0 (gc) or garbage (rc, none).00bigint_struct_layoutcould not assert it.f64hits this in common shapes.02union_typeprintedError 9.11037e-305 downloadinginstead ofError 1 downloading.The change
<{[N/P x iP], [N%P x i8]}>: pointer-sized words plus a byte tail, where P is the pointer size in bytes. N is the old alloc size, so no union changes size. Words rather than bytes, because LLVM flattens a first-class[N x i8]argument into N separate byte arguments.LLVMTypeConverterHelper::getUnionStorageSize/getUnionStorageType.castToAnyof a union: both used the largest member's type as the value type;castLLVMTypes: aggregates of 8 bytes or less went through aLoadSaveOpthat moves exactly one pointer, which is 4 bytes at i686. That shortcut is removed; the constant-size memcpy is folded anyway.any === anyof two identical values boxed from a union could be false, and the conservative GC saw garbage words.ops with side effects not allowed in global initializers; nowlet g: number | string = "x"compiles as well. Only tagged-union globals move to a constructor. Non-tagged unions (string | undefined, literal unions) are untouched.types have different sizeswarning no longer fires for member-into-union conversions, which differ in size by design.x64
x64 output changes for every tagged union, by design. x64
.textover the corpus (gc,--opt_level=3) changes by −0.03% (340240 → 340129 bytes). Exported functions that take or return unions change ABI: default-lib or DLL binaries built by the previous compiler must be rebuilt.New:
tslang/test/probe-corpus.shCompiles and runs every corpus file as a single-file exe with the suite's flags, for x64 and i686 × gc/rc/none, and writes a TSV.
--compare before.tsv after.tsvlists every row that passed before and does not pass after. It is hand-run, like the othercheck-*.shscripts. About 100 files per model fail at x64 too; those are multi-file tests that need the test runner.The file count goes from 520 to 522, because of the two new tests.
--comparefinds 0 regressions and 29 newly passing rows. At i686 the newly passing files are02union_type,03union_type,03union_type_case_order,00typeof_static_fold_conditions,parser, andarrayLiteralsunder rc.Still failing at i686 only:
internals, whoseinline_asm<i64>with=rfits no single i686 register;02funcs_vararg, which is out of the suite.Found, not fixed
const copiedN: number = cNprints a denormal, and a union copy crashes silently. The old compiler does the same, and it happens at x64 too. It is recorded in the spec's open issues.00array8_tuple_spreadandarrayLiterals2ES5at i686 under rc flipped between two probe runs of the same compiler before this change.Test plan
ctest -j 16 -C Releasepasses 2769/2769. That includes the new00union_member_paddingand00union_global_init(compile and jit), and00bigint_struct_layoutwithaasserted again.00union_global_initalso passes with--di --opt_level=0and with--emit=jit.probe-corpus.sh --compare: 0 regressions against the base-commit baseline at either arch.--verify-ownership -mm=rcgives output identical to the base compiler on 62 union corpus files at both arches. The one exception is00union_global_init, which the base compiler cannot compile. The verifier runs before LowerToLLVM.🤖 Generated with Claude Code