From b9c4067f7874507a2d05eae774d5c2ced7140c12 Mon Sep 17 00:00:00 2001 From: alyekypo Date: Mon, 28 Sep 2026 19:32:04 +0000 Subject: [PATCH] fix(llvm): emit union constants for non-constantable unions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- src/llvm_backend_const.cpp | 6 +- src/llvm_backend_stmt.cpp | 18 +- .../test_union_const_nonconstantable.odin | 196 ++++++++++++++++++ 3 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 tests/internal/test_union_const_nonconstantable.odin diff --git a/src/llvm_backend_const.cpp b/src/llvm_backend_const.cpp index 31d1e16fb..01e049544 100644 --- a/src/llvm_backend_const.cpp +++ b/src/llvm_backend_const.cpp @@ -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) { diff --git a/src/llvm_backend_stmt.cpp b/src/llvm_backend_stmt.cpp index a0f0f48a3..b888d86bd 100644 --- a/src/llvm_backend_stmt.cpp +++ b/src/llvm_backend_stmt.cpp @@ -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); } } diff --git a/tests/internal/test_union_const_nonconstantable.odin b/tests/internal/test_union_const_nonconstantable.odin new file mode 100644 index 000000000..c6ca58b78 --- /dev/null +++ b/tests/internal/test_union_const_nonconstantable.odin @@ -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) +} \ No newline at end of file