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:
18
examples/0134-types-global-init-from-module-const.sx
Normal file
18
examples/0134-types-global-init-from-module-const.sx
Normal 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
42
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
g=42
|
||||||
117
issues/0071-global-initializer-module-const-silent-zero.md
Normal file
117
issues/0071-global-initializer-module-const-silent-zero.md
Normal 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.
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user