[RFC][ClangIR] Widen bool / _BitInt / ext-vector-bool to their in-memory types in lowering

Some types differ between register and memory. bool is i1 in a register and i8 in memory; _BitInt(N) is iN but is stored in a wider padded integer; an ext_vector_type of bool is <N x i1> but is stored packed in an iP (P = max(N, 8)). CIR keeps the source type literal in CIRGen and widens to the memory form during lowering. bool already works this way; this proposes the same, consistently, for _BitInt and ext-vector-bool, and states the rule that makes it unambiguous.

_BitInt is wrong today: we emit the constant at its exact width, which leaves the storage padding undefined, so signed _BitInt(6) = -1 writes 0x3f where classic and GCC write 0xff. The fix is to widen and sign/zero-extend to the storage integer in lowering, the same place bool becomes i8. ext-vector-bool is the same idea with a bitcast+shuffle instead of an integer extension, and a bool4 global has to be i8 to match classic. The _BitInt side is in review as #205605; the ext-vector-bool side isn’t posted yet — it’s what surfaced the question this RFC is settling, so I’m holding it until the rule is agreed.

/local/home/adams/cir/https:/github.com/llvm/llvm-project/pull/205605

The rule: cir.bool, cir.vector<cir.bool>, and cir.int<N, bitint> are source types, and they take their memory form wherever they are stored — scalars, globals, allocas, and aggregate members alike. That matches clang, where struct { bool b; } is { i8 } uniformly and the i1/i8 conversion happens at member access, not by re-typing the struct. A raw i1/iN that exists only as an intrinsic or ABI value is a different thing and is spelled with the integer type, never cir.bool. CIR already follows this almost everywhere: the overflow builtins build { iN, i1 } literally, AMDGPU’s div.scale uses an !u1i struct member, and the X86 mask builtins (fpclass, kadd, the compares) and SVE predicates use i1-typed vectors. The one exception is vp2intersect, which builds its result as a struct of cir.bool vectors for an intrinsic whose ABI is <16 x i1>; that is a modeling bug, and it gets fixed to use i1 vectors like its siblings.

With that rule there is nothing to defer and no record-context machinery to add. Members widen because the member type says so, and value or intrinsic aggregates stay literal because they were never spelled with the source type in the first place. The only errorNYI is the _BitInt byte-array split widths (129, 257, …) where the storage integer’s alloc size exceeds its store size — an unbuilt storage form, not a correctness gap.

Realization stays in LowerToLLVM, where bool already lives: convertTypeForMemory for the leaf and member types, emitToMemory/emitFromMemory at load/store, and the global-init attr path. The work lands as the member widening plus the vp2intersect fix, then the _BitInt and ext-vector-bool constant and load/store paths on top.

@andykaylor @erichkeane

1 Like

Direction sounds good to me.

There’s a case to be made that these should be done in a pass before LowerToLLVM (perhaps even as part of ABI lowering?), one advantage is keeping it 1-1 as much as possible when translating to LLVM. However, given that there are no use cases that would require this prior to LLVM, I wouldn’t worry about it at this time (and avoid introducing compile time slowdown without a good reason).

Keeping realization in LowerToLLVM for now. Folding it into a pre-lowering pass or ABI lowering would be the right home if something ever needs the memory form before LLVM, but nothing does today, so I’ll keep it where bool already lives and skip the extra pass.

1 Like