diff --git a/examples/0134-types-global-init-from-module-const.sx b/examples/0134-types-global-init-from-module-const.sx new file mode 100644 index 0000000..9a542ef --- /dev/null +++ b/examples/0134-types-global-init-from-module-const.sx @@ -0,0 +1,18 @@ +// Top-level global initialized from a module constant copies the constant's +// value (not a silent zero). `K : A : 42; g : A = K;` resolves the forward +// alias `A` to `s32` and materializes `g`'s static initializer from `K`. +// Regression (issue 0071): `registerTopLevelGlobal`'s init_val switch only +// handled literals/array/struct literals; an identifier initializer fell +// through to a null payload and the global silently zero-initialized. +#import "modules/std.sx"; + +A :: B; +B :: s32; + +K : A : 42; +g : A = K; + +main :: () -> s32 { + print("g={}\n", g); + return g; +} diff --git a/examples/expected/0134-types-global-init-from-module-const.exit b/examples/expected/0134-types-global-init-from-module-const.exit new file mode 100644 index 0000000..d81cc07 --- /dev/null +++ b/examples/expected/0134-types-global-init-from-module-const.exit @@ -0,0 +1 @@ +42 diff --git a/examples/expected/0134-types-global-init-from-module-const.stderr b/examples/expected/0134-types-global-init-from-module-const.stderr new file mode 100644 index 0000000..e69de29 diff --git a/examples/expected/0134-types-global-init-from-module-const.stdout b/examples/expected/0134-types-global-init-from-module-const.stdout new file mode 100644 index 0000000..9ba36fc --- /dev/null +++ b/examples/expected/0134-types-global-init-from-module-const.stdout @@ -0,0 +1 @@ +g=42 diff --git a/issues/0071-global-initializer-module-const-silent-zero.md b/issues/0071-global-initializer-module-const-silent-zero.md new file mode 100644 index 0000000..add2e31 --- /dev/null +++ b/issues/0071-global-initializer-module-const-silent-zero.md @@ -0,0 +1,117 @@ +# 0071 — global initialized from module const silently zero-initializes + +> **RESOLVED.** Root cause: `Lowering.registerTopLevelGlobal`'s init_val switch +> serialized only literal / array-literal / struct-literal initializers; an +> identifier initializer (`g : A = K;`) fell through to `else => null`, so the +> global was emitted with no payload and silently zero-initialized. +> Fix: extracted the initializer serialization into `Lowering.globalInitValue` +> and added an `.identifier` arm that materializes the global's static value from +> `ProgramIndex.module_const_map` (typed module consts are registered in the same +> pass-2 just before, via `registerTypedModuleConst`). An identifier that names no +> usable constant now emits a diagnostic instead of silently zeroing. Other +> initializer shapes (enum-literal shorthand, etc.) keep their established +> static-lowering behavior — this pass only closes the identifier/module-const +> hole. Regression: `examples/0134-types-global-init-from-module-const.sx` +> (`g=42` / exit 42). + +## Symptom + +A top-level global initialized from a module constant compiles but is +zero-initialized instead of receiving the constant's value. + +Observed: + +```text +g=0 +``` + +Expected: `g` should be initialized to `42`, or the compiler should reject the +initializer loudly if identifier/module-const global initializers are not +supported. + +## Reproduction + +```sx +#import "modules/std.sx"; + +A :: B; +B :: s32; + +K : A : 42; +g : A = K; + +main :: () -> s32 { + print("g={}\n", g); + return g; +} +``` + +Run: + +```sh +./zig-out/bin/sx run .sx-tmp/probe-0070-global-init-from-const.sx +``` + +The repro is standalone; the inline source above is sufficient to recreate the +scratch file under `.sx-tmp/`. + +## Investigation prompt + +Fix issue 0071: a top-level global initialized from a module constant must not +silently become zero. + +Context: +- This surfaced during Codex re-review of `932cdfa`, the issue-0070 fix. +- `932cdfa` correctly defers top-level global and typed-module-const annotation + resolution until after the forward-alias fixpoint. +- The remaining bug is in the global initializer path, not the annotation path: + `K : A : 42; g : A = K;` resolves `A` correctly, registers `K` in + `ProgramIndex.module_const_map`, but `g` is emitted as zero. + +Suspected area: +- `src/ir/lower.zig`, `Lowering.registerTopLevelGlobal`. +- Its `init_val` switch serializes literal / array / struct-literal initializers, + but an identifier initializer falls through to `else => null`, and the global + is emitted with no initializer payload. That silently becomes zero-initialized. +- Related facts: `ProgramIndex.module_const_map` already records typed module + constants via `registerTypedModuleConst`, now in pass 2 after alias convergence. + +Likely fix: +- Add explicit handling for identifier initializers that name a module constant, + converting the recorded constant value into the global's `ConstantValue` with + the global's declared type. +- If some initializer shape cannot be represented as a global constant yet, emit + a diagnostic instead of returning `null` / zero-initializing. +- Do not regress issue 0070: `A :: B; B :: s32; g : A = 7;` and + `K : A : 35;` must still resolve through the converged alias map. +- Preserve literal, array literal, struct literal, and foreign-global behavior. + +Verification: +- Add a focused regression, likely in the `01xx` types block: + +```sx +#import "modules/std.sx"; +A :: B; +B :: s32; +K : A : 42; +g : A = K; +main :: () -> s32 { print("g={}\n", g); return g; } +``` + +- Keep these green: + - `examples/0133-types-forward-alias-global.sx` + - `examples/0132-types-forward-type-alias.sx` + - `examples/0116-types-type-alias-size-align.sx` + - `examples/0201-generics-generic-struct.sx` + - `examples/1117-diagnostics-value-const-as-type-rejected.sx` +- Run: + +```sh +zig build +zig build test +bash tests/run_examples.sh +``` + +Expected result: the new regression prints `g=42` and exits `42`; unsupported +global initializer shapes no longer silently zero-initialize; the full suite +passes. diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 35f0e20..772d56b 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1271,16 +1271,7 @@ pub const Lowering = struct { // name is the optional override or the sx name itself. const sym_name = vd.foreign_name orelse vd.name; const name_id = self.module.types.internString(sym_name); - const init_val: ?inst_mod.ConstantValue = if (vd.is_foreign) null else if (vd.value) |v| switch (v.data) { - .undef_literal => .zeroinit, - .int_literal => |il| .{ .int = il.value }, - .bool_literal => |bl| .{ .boolean = bl.value }, - .float_literal => |fl| .{ .float = fl.value }, - .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, - .array_literal => |al| self.constArrayLiteral(al.elements), - .struct_literal => |sl| self.constStructLiteral(&sl, var_ty), - else => null, - } else null; + const init_val = self.globalInitValue(vd, var_ty); const gid = self.module.addGlobal(.{ .name = name_id, .ty = var_ty, @@ -1291,6 +1282,44 @@ pub const Lowering = struct { self.program_index.global_names.put(vd.name, .{ .id = gid, .ty = var_ty }) catch {}; } + /// Serialize a top-level global's initializer into a static `ConstantValue`. + /// Foreign globals (extern symbol) and value-less declarations carry no + /// payload — they default to zero/extern at link, which is correct. An + /// identifier initializer that names a module constant is materialized from + /// the recorded constant (`K : A : 42; g : A = K;` → 42, issue 0071); a + /// global initialized from an identifier that resolves to no usable constant + /// is rejected with a diagnostic rather than silently zero-initialized — a + /// global has no run site for a dynamic initializer. + fn globalInitValue(self: *Lowering, vd: *const ast.VarDecl, var_ty: TypeId) ?inst_mod.ConstantValue { + if (vd.is_foreign) return null; + const v = vd.value orelse return null; + return switch (v.data) { + .undef_literal => .zeroinit, + .int_literal => |il| .{ .int = il.value }, + .bool_literal => |bl| .{ .boolean = bl.value }, + .float_literal => |fl| .{ .float = fl.value }, + .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, + .array_literal => |al| self.constArrayLiteral(al.elements), + .struct_literal => |sl| self.constStructLiteral(&sl, var_ty), + .identifier => |id| blk: { + // A global initialized from a module constant copies the + // constant's recorded value (typed module consts land in + // `module_const_map` via `registerTypedModuleConst`, run in the + // same pass-2 before this). + if (self.program_index.module_const_map.get(id.name)) |ci| { + if (self.constExprValue(ci.value)) |cv| break :blk cv; + } + if (self.diagnostics) |d| + d.addFmt(.err, v.span, "global '{s}' must be initialized by a compile-time constant; '{s}' is not a usable constant here", .{ vd.name, id.name }); + break :blk null; + }, + // Other initializer shapes (enum-literal shorthand, etc.) keep their + // established static-lowering behavior; this pass only closes the + // identifier/module-const hole (issue 0071). + else => null, + }; + } + /// Resolve identifier-RHS type aliases whose target is declared LATER in the /// file. The forward scan above only registers an alias (`A :: B`) when `B` /// is already in `type_alias_map` / the `TypeTable`; a forward target isn't