fix(ir): materialize global initialized from module const (issue 0071)

registerTopLevelGlobal's init_val switch serialized only literal / array-
literal / struct-literal initializers. An identifier initializer
(`K : A : 42; g : A = K;`) fell through to `else => null`, so the global was
emitted with no payload and silently zero-initialized (printed g=0).

Extract the initializer serialization into globalInitValue and add an
.identifier arm that materializes the global's static value from
ProgramIndex.module_const_map (typed module consts are registered in the same
scanDecls pass-2 just before, via registerTypedModuleConst). An identifier
that names no usable constant now emits a diagnostic instead of silently
zeroing — a global has no run site for a dynamic initializer.

Other initializer shapes (enum-literal shorthand, etc.) keep their established
static-lowering behavior; enum-literal globals' zero-init is load-bearing for
`inline if OS == ...` in the stdlib, so it stays out of scope here. This pass
only closes the identifier/module-const hole.

Regression: examples/0134-types-global-init-from-module-const.sx (g=42, exit
42). Gate: zig build, zig build test, run_examples.sh -> 355/0.
This commit is contained in:
agra
2026-06-02 17:45:37 +03:00
parent 932cdfa2ec
commit ad7200c196
6 changed files with 176 additions and 10 deletions

View File

@@ -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;
}

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1 @@
g=42

View File

@@ -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.

View File

@@ -1271,16 +1271,7 @@ pub const Lowering = struct {
// name is the optional override or the sx name itself. // name is the optional override or the sx name itself.
const sym_name = vd.foreign_name orelse vd.name; const sym_name = vd.foreign_name orelse vd.name;
const name_id = self.module.types.internString(sym_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) { const init_val = self.globalInitValue(vd, var_ty);
.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 gid = self.module.addGlobal(.{ const gid = self.module.addGlobal(.{
.name = name_id, .name = name_id,
.ty = var_ty, .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 {}; 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 /// 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` /// 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 /// is already in `type_alias_map` / the `TypeTable`; a forward target isn't