fix(llvm): emit union constants for non-constantable unions

`constant: U : value` produced a constant of the wrong type:
lb_const_value took its union-lowering branch only when
`is_type_union_constantable(type)` held, so for a union with a variant
that whitelist rejects (`union{bool, ^int}`) the value fell through to
the scalar branch and reached `LLVMConstInt(lb_type(m, union_type),
...)`.  The constant emitted there does not match the union's type:
bytes that change between runs for `true`, a compiler crash for `1.0`,
an `is_type_string` assertion for a string literal.

The checker already resolves which variant a constant represents and
records it in `ExactValue::variant_type` - the variant type, tag index,
payload and padding the union branch needs - so that branch is now also
taken when the value pins a variant this backend can build as a
constant.  The type-level whitelist was re-guessing information the
value already carries; variant types this backend cannot build as
constants (`any`) stay excluded.

lb_build_static_variables had the same defect in another place: it
lowered an initializer with the expression's type instead of the
declared type, so `@(static) s: U` emitted a payload-only constant and
LLVM rejected the module with “Global variable initializer type does not
match global variable type!”.  It now passes the declared type and gives
the global the constant's layout-compatible type, the way the file-scope
path already does.

Blast radius: every union-typed constant, i.e. typed constants, global
static initializers, @(static)/@(thread_local)/@(rodata), default
parameters and constant aggregate elements.  Unions whose variants are
all constantable keep the previous path.
This commit is contained in:
alyekypo committed 2026-09-28 19:32:04 +00:00
1 parent 395920130e
commit b9c4067f78
3 files changed
+218 -2

No files matched your search

+5 -1
View File
@@ -830,7 +830,11 @@ gb_internal lbValue lb_const_value(lbModule *m, Type *type, ExactValue value, lb
bool is_local = cc.allow_local && m->curr_procedure != nullptr;
if (is_type_union(type) && is_type_union_constantable(type)) {
// A union constant needs a payload this backend can build, and the checker pins
// `variant_type` even for variants it cannot (`any` needs a backing global and a typeid),
// so ask the same question the aggregate constant paths ask.
if (is_type_union(type) && (is_type_union_constantable(type) ||
(value.variant_type != nullptr && elem_type_can_be_constant(value.variant_type)))) {
Type *bt = base_type(type);
GB_ASSERT(bt->kind == Type_Union);
if (bt->Union.variants.count == 0) {
+17 -1
View File
@@ -2434,7 +2434,7 @@ gb_internal void lb_build_static_variables(lbProcedure *p, AstValueDecl *vd) {
if (e->Variable.is_rodata) {
cc.is_rodata = true;
}
value = lb_const_value(p->module, ast_value->tav.type, ast_value->tav.value, cc);
value = lb_const_value(p->module, is_type_any(e->type) ? ast_value->tav.type : e->type, ast_value->tav.value, cc);
}
String mangled_name = {};
@@ -2488,6 +2488,22 @@ gb_internal void lb_build_static_variables(lbProcedure *p, AstValueDecl *vd) {
LLVMValueRef init = llvm_const_named_struct(p->module, e->type, vals.data, vals.count);
LLVMSetInitializer(global, init);
} else {
LLVMTypeRef expected_type = lb_type(p->module, e->type);
LLVMTypeRef actual_type = LLVMTypeOf(value.value);
GB_ASSERT_MSG(lb_sizeof(actual_type) == lb_sizeof(expected_type),
"size mismatch for @(static) initializer of %.*s",
LIT(name));
if (actual_type != expected_type) {
LLVMDeleteGlobal(global);
global = LLVMAddGlobal(p->module->mod, actual_type, c_name);
LLVMSetAlignment(global, cast(u32)type_align_of(e->type));
if (e->Variable.is_rodata) {
LLVMSetGlobalConstant(global, true);
}
if (!lb_apply_thread_local_model(global, e->Variable.thread_local_model)) {
LLVMSetLinkage(global, LLVMInternalLinkage);
}
}
LLVMSetInitializer(global, value.value);
}
}
@@ -0,0 +1,196 @@
package test_internal
import "core:testing"
// A union constant must be byte-identical to the same value built at run time,
// whatever the variant. These unions are not `is_type_union_constantable`, so
// lb_const_value used to skip its union branch for them and lower the value as
// a scalar of the union's LLVM type.
V :: union { bool, f64, ^int }
SP :: struct { p: ^int, n: int }
VI :: union { SP, int }
VE :: enum { A, B }
VES :: union { VE, ^int }
VA :: union { bool, [4]^int }
VAOK :: struct { a: int, b: int }
VC :: union { VAOK, int }
@(private="file")
expect_same_bytes :: proc(t: ^testing.T, constant, variable: $T, what: string) {
testing.expectf(t,
transmute([size_of(T)]u8)constant == transmute([size_of(T)]u8)variable,
"%s: constant %v vs variable %v", what,
transmute([size_of(T)]u8)constant, transmute([size_of(T)]u8)variable)
}
// typed constant in a procedure body (the report's program)
@(test)
union_typed_constant_local :: proc(t: ^testing.T) {
{
c: V : true
v: V = true
expect_same_bytes(t, c, v, "bool")
}
{
c: V : f64(1.5)
v: V = f64(1.5)
expect_same_bytes(t, c, v, "f64")
}
{
c: V : f64(2)
v: V = f64(2)
expect_same_bytes(t, c, v, "f64 from integer literal")
}
}
// file-scope typed constants and global variables with constant initializers
@(private="file") FC_BOOL: V = true
@(private="file") FC_F64: V = f64(2.718)
@(private="file") FC_INT: VI = 7
@(test)
union_typed_constant_file_scope :: proc(t: ^testing.T) {
c0: V = true
c1: V = f64(2.718)
c2: VI = 7
expect_same_bytes(t, FC_BOOL, c0, "file-scope bool constant")
expect_same_bytes(t, FC_F64, c1, "file-scope f64 constant")
expect_same_bytes(t, FC_INT, c2, "file-scope int constant")
}
@(test)
union_global_initializer :: proc(t: ^testing.T) {
c0: V = true
c1: V = f64(1.25)
expect_same_bytes(t, GB, c0, "global bool")
expect_same_bytes(t, GF, c1, "global f64")
}
@(private="file") GB: V = true
@(private="file") GF: V = f64(1.25)
// every variant kind, including the ones the constant whitelist rejects
@(test)
union_variant_kinds :: proc(t: ^testing.T) {
{
c: VE : .B
v: VE = .B
expect_same_bytes(t, c, v, "enum-variant union")
}
{
c: VI : SP{n=7}
v: VI = SP{n=7}
expect_same_bytes(t, c, v, "struct-with-pointer variant")
}
{
c: VI : 42
v: VI = 42
expect_same_bytes(t, c, v, "other variant of the same union")
}
{
c: VA : true
v: VA = true
expect_same_bytes(t, c, v, "large-payload variant")
}
{
c: VES : VE.A
v: VES = VE.A
expect_same_bytes(t, c, v, "enum payload with pointer variant present")
}
}
// a union with only constantable variants keeps working
@(test)
union_constantable_control :: proc(t: ^testing.T) {
c: VC : VAOK{1, 2}
v: VC = VAOK{1, 2}
expect_same_bytes(t, c, v, "constantable union")
}
// default parameter values are materialized through the same constant path
union_default :: proc(x: V = true) -> V { return x }
@(test)
union_default_parameter :: proc(t: ^testing.T) {
v: V = true
expect_same_bytes(t, union_default(), v, "default parameter")
}
// @(static) / @(thread_local) / @(rodata): the initializer used to be lowered
// with the expression's type instead of the declared type, so the global was
// sized to the payload while every access used the union layout.
VS :: union { int, bool }
VSS :: union { string, int }
@(test)
union_static_and_rodata :: proc(t: ^testing.T) {
@(static) s_int: VS = 1
@(static) s_str: VSS = "xy"
@(static) s_bool: V = true
@(static) s_f64: V = f64(1.5)
@(thread_local) th: V = true
@(static) @(rodata) ro: V = true
i: VS = 1
s: VSS = "xy"
b: V = true
f: V = f64(1.5)
expect_same_bytes(t, s_int, i, "@(static) int")
expect_same_bytes(t, s_str, s, "@(static) string")
expect_same_bytes(t, s_bool, b, "@(static) bool")
expect_same_bytes(t, s_f64, f, "@(static) f64")
expect_same_bytes(t, th, b, "@(thread_local)")
expect_same_bytes(t, ro, b, "@(rodata) static")
expect_same_bytes(t, RO_GLOBAL, b, "@(rodata) global")
// the tag must be in the right place, not past the end of the global
if v, ok := s_int.(int); ok {
testing.expect_value(t, v, 1)
} else {
testing.expectf(t, false, "@(static) int variant not resolved")
}
if v, ok := s_str.(string); ok {
testing.expect_value(t, v, "xy")
} else {
testing.expectf(t, false, "@(static) string variant not resolved")
}
if v, ok := s_bool.(bool); ok {
testing.expect(t, v, "@(static) bool variant not resolved")
} else {
testing.expectf(t, false, "@(static) bool variant not resolved")
}
if v, ok := s_f64.(f64); ok {
testing.expect_value(t, v, 1.5)
} else {
testing.expectf(t, false, "@(static) f64 variant not resolved")
}
if v, ok := th.(bool); ok {
testing.expect(t, v, "@(thread_local) bool variant not resolved")
} else {
testing.expectf(t, false, "@(thread_local) bool variant not resolved")
}
if v, ok := ro.(bool); ok {
testing.expect(t, v, "@(rodata) bool variant not resolved")
} else {
testing.expectf(t, false, "@(rodata) bool variant not resolved")
}
}
@(rodata)
RO_GLOBAL: V = true
// statics of non-union types must be unaffected
@(test)
static_non_union_unaffected :: proc(t: ^testing.T) {
@(static) i: int = 42
@(static) f: f64 = 2.5
@(static) s: string = "hey"
@(static) a: [3]int = {1, 2, 3}
@(static) e: VE = .B
testing.expect_value(t, i, 42)
testing.expect_value(t, f, 2.5)
testing.expect_value(t, s, "hey")
testing.expect_value(t, a, [3]int{1, 2, 3})
testing.expect_value(t, e, VE.B)
}