fix(ir): resolve forward alias in top-level global annotations (issue 0070)

Issue 0069's resolveForwardIdentifierAliases fixpoint runs at the END of
scanDecls, but top-level var_decl globals and typed module constants had
their annotations resolved via resolveType(ta) inside the SAME scan loop,
before the fixpoint. So a forward identifier alias (`A :: B; B :: s32;`)
used as a global's type (`g : A = 7;`) was still absent from
type_alias_map: resolveType fabricated an empty-struct stub, and the global
got a type mismatching its initializer at LLVM verification (the typed-const
path `K : A : 42;` silently mistyped the constant instead).

Split scanDecls into two passes: pass 1 registers function/type/alias facts,
then resolveForwardIdentifierAliases converges the aliases, then pass 2
registers var_decl globals (registerTopLevelGlobal) and typed module
constants (registerTypedModuleConst) against the converged alias map.
Globals/typed-consts can't be named in a type position, so deferring them
past type/alias registration is order-safe; the untyped module-const branch
(no annotation to resolve) stays in pass 1.

One incidental IR snapshot reorder (examples/1309: user globals now emit
after foreign-class globals — semantically identical, program still exits 0).

Regression: examples/0133-types-forward-alias-global.sx (forward-alias global
+ typed const). Gate: zig build, zig build test, run_examples.sh -> 354/0.
This commit is contained in:
agra
2026-06-02 17:20:31 +03:00
parent 49a383df6d
commit 932cdfa2ec
7 changed files with 225 additions and 52 deletions

View File

@@ -0,0 +1,22 @@
// Forward identifier type alias as a TOP-LEVEL annotation — a global var
// and a typed module constant whose annotation is a forward alias
// (`A :: B; B :: s32;`) resolve to the alias target, the same as the
// ordered form, instead of a fabricated stub.
// Regression (issue 0070): top-level global / typed-const annotations were
// resolved inside the scan loop BEFORE the forward-alias fixpoint ran, so
// `g : A` got a stub type that mismatched its initializer at LLVM
// verification. Global/const annotation resolution now runs in scan pass 2,
// after the fixpoint.
#import "modules/std.sx";
A :: B;
B :: s32;
g : A = 7;
K : A : 35;
main :: () -> s32 {
print("global g: {}\n", g);
print("const K: {}\n", K);
return g + K;
}

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1,2 @@
global g: 7
const K: 35

View File

@@ -1,9 +1,9 @@
@__SxFoo_state_ivar = internal global ptr null
@__SxFoo_class = internal global ptr null
@OS = internal global i64 0 @OS = internal global i64 0
@ARCH = internal global i64 0 @ARCH = internal global i64 0
@POINTER_SIZE = internal global i64 8 @POINTER_SIZE = internal global i64 8
@__SxFoo_state_ivar = internal global ptr null
@__SxFoo_class = internal global ptr null
@__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null } @__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null }
@__sx_objc_cstr_dealloc = internal global [8 x i8] c"dealloc\00" @__sx_objc_cstr_dealloc = internal global [8 x i8] c"dealloc\00"
@str = private unnamed_addr constant [2 x i8] c"0\00", align 1 @str = private unnamed_addr constant [2 x i8] c"0\00", align 1

View File

@@ -0,0 +1,113 @@
# 0070 — forward alias in top-level global annotation reaches LLVM verifier
> **RESOLVED.** Root cause: issue 0069's `resolveForwardIdentifierAliases`
> fixpoint runs at the END of `Lowering.scanDecls`, but the same scan loop
> resolved top-level `var_decl` global annotations (and typed module-constant
> annotations) via `self.resolveType(ta)` BEFORE that fixpoint ran — so a forward
> alias (`A :: B; B :: s32; g : A = 7;`) was still absent from
> `type_alias_map`, `resolveType` fabricated an empty-struct stub, and the global
> got a type mismatching its initializer at LLVM verification (the typed-const
> path silently mistyped the constant instead).
> Fix: split `scanDecls` into two passes. Pass 1 registers function/type/alias
> facts; then `resolveForwardIdentifierAliases` converges the aliases; then pass 2
> registers top-level `var_decl` globals (`registerTopLevelGlobal`) and typed
> module constants (`registerTypedModuleConst`), so their annotations resolve
> against the converged alias map. Globals/typed-consts can't be named in a type
> position, so deferring them past type/alias registration is order-safe; the
> untyped module-const branch (no annotation to resolve) stays in pass 1.
> Regression: `examples/0133-types-forward-alias-global.sx`.
## Symptom
A forward identifier type alias used as a top-level global's type annotation
does not resolve before the global is registered, producing an LLVM verifier
failure instead of compiling as the alias target type.
Observed:
```text
LLVM verification failed: Global variable initializer type does not match global variable type!
ptr @g
```
Expected: `A :: B; B :: s32; g : A = 7;` should type `g` as `s32` and compile/run
the same way as the ordered alias form.
## Reproduction
```sx
A :: B;
B :: s32;
g : A = 7;
main :: () -> s32 {
return g;
}
```
Run:
```sh
./zig-out/bin/sx run .sx-tmp/probe-0069-forward-alias-global.sx
```
The repro is standalone; the inline source above is sufficient to recreate the
scratch file under `.sx-tmp/`.
## Investigation prompt
Fix issue 0070: a forward identifier type alias used in a top-level global
annotation must resolve before that global's type is registered.
Context:
- Issue 0069 (`49a383d`) added `Lowering.resolveForwardIdentifierAliases`, a
fixpoint post-pass at the end of `scanDecls`, to resolve top-level
identifier-RHS aliases like `A :: B; B :: s32;`.
- That works for aliases used later in function bodies because the A2.4
unknown-type pass and body lowering run after `scanDecls`.
- But top-level `var_decl` annotations are resolved inside the same `scanDecls`
loop before `resolveForwardIdentifierAliases(decls)` is called. So
`g : A = 7;` can be typed while `A` is still absent from
`ProgramIndex.type_alias_map`.
- Suspected area: `src/ir/lower.zig`, `Lowering.scanDecls`, especially the
ordering between `.const_decl` alias collection, the new
`resolveForwardIdentifierAliases`, and the `.var_decl` branch that calls
`self.resolveType(ta)`.
Likely fix:
- Split the scan ordering so all top-level type declarations and identifier
aliases converge before any top-level global annotation is resolved.
- One possible shape: first scan/register function/type/alias facts, run the
forward-alias fixpoint, then handle top-level `var_decl` global registration
and literal module constants that require resolved annotation types.
- Do not reintroduce issue 0068: `NotAType :: 123; v: NotAType` must still emit
`unknown type 'NotAType'`.
- Do not fabricate stubs while trying to resolve the forward alias. The alias
facts should still come from `ProgramIndex.type_alias_map` and real
`TypeTable.findByName` hits.
Verification:
- Add a focused regression, likely in the `01xx` types block:
```sx
A :: B;
B :: s32;
g : A = 7;
main :: () -> s32 { return g; }
```
- Keep `examples/0132-types-forward-type-alias.sx`,
`examples/0116-types-type-alias-size-align.sx`,
`examples/0201-generics-generic-struct.sx`, and
`examples/1117-diagnostics-value-const-as-type-rejected.sx` green.
- Run:
```sh
zig build
zig build test
bash tests/run_examples.sh
```
Expected result: the forward-alias global program exits 7, issue 0068 remains
rejected with a diagnostic, and the full suite passes.

View File

@@ -1153,16 +1153,13 @@ pub const Lowering = struct {
} }
// comptime_expr handled in Pass 2 // comptime_expr handled in Pass 2
// Simple value constants with type annotation (e.g. AF_INET :s32: 2) // Typed value constants (`AF_INET :s32: 2`) are registered in
if (cd.type_annotation) |ta| { // pass 2 below — after the forward-alias fixpoint — so a
switch (cd.value.data) { // forward identifier alias in the annotation resolves to its
.int_literal, .float_literal, .bool_literal, .string_literal, .undef_literal, .null_literal => { // target instead of a fabricated stub (issue 0070). Untyped
const ty = self.resolveType(ta); // literal constants carry no annotation to resolve, so they
self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = ty }) catch {}; // stay here (their type comes from the literal / inference).
}, if (cd.type_annotation == null) {
else => {},
}
} else {
// Untyped literal constants (e.g. UI_VERT_SRC :: #string GLSL...GLSL;) // Untyped literal constants (e.g. UI_VERT_SRC :: #string GLSL...GLSL;)
switch (cd.value.data) { switch (cd.value.data) {
.string_literal => self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .string }) catch {}, .string_literal => self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .string }) catch {},
@@ -1210,50 +1207,88 @@ pub const Lowering = struct {
.ufcs_alias => |ua| { .ufcs_alias => |ua| {
self.program_index.ufcs_alias_map.put(ua.name, ua.target) catch {}; self.program_index.ufcs_alias_map.put(ua.name, ua.target) catch {};
}, },
.var_decl => |vd| { // Top-level globals are registered in a second pass (below),
// Top-level mutable global (e.g., `context : Context = ---;`) // after the forward-alias fixpoint, so a forward identifier
// Use self.resolveType so type aliases like `Handle :: u32;` resolve // alias used as a global's type annotation resolves (issue 0070).
// to their target type (not a synthetic empty struct). When the .var_decl => {},
// user omitted the annotation, infer from the initializer
// expression; foreign globals with no annotation are diagnosed
// because their type can't be inferred without an initializer.
const var_ty: TypeId = if (vd.type_annotation) |ta|
self.resolveType(ta)
else if (vd.value) |val|
self.inferExprType(val)
else blk: {
if (self.diagnostics) |d|
d.addFmt(.err, null, "top-level var '{s}' has no type annotation and no initializer to infer from", .{vd.name});
break :blk .void;
};
// Foreign globals reference a symbol defined in libSystem etc.
// (`_NSConcreteStackBlock : *void #foreign;`). The C symbol
// 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 gid = self.module.addGlobal(.{
.name = name_id,
.ty = var_ty,
.init_val = init_val,
.is_const = false,
.is_extern = vd.is_foreign,
});
self.program_index.global_names.put(vd.name, .{ .id = gid, .ty = var_ty }) catch {};
},
else => {}, else => {},
} }
} }
self.resolveForwardIdentifierAliases(decls); self.resolveForwardIdentifierAliases(decls);
// Pass 2: registrations that resolve a top-level type annotation run
// after the alias fixpoint, so a forward identifier alias used as the
// annotation resolves to its target (issue 0070).
for (decls) |decl| {
self.setCurrentSourceFile(decl.source_file);
switch (decl.data) {
.var_decl => self.registerTopLevelGlobal(&decl.data.var_decl),
.const_decl => |cd| self.registerTypedModuleConst(&cd),
else => {},
}
}
}
/// Register a typed module-level value constant (`AF_INET :s32: 2`). Run in
/// scanDecls pass 2 (after `resolveForwardIdentifierAliases`) so a forward
/// identifier alias in the annotation (`A :: B; B :: s32; K : A : 42;`)
/// resolves to its target rather than a fabricated empty-struct stub, which
/// would otherwise mistype the constant (issue 0070).
fn registerTypedModuleConst(self: *Lowering, cd: *const ast.ConstDecl) void {
const ta = cd.type_annotation orelse return;
switch (cd.value.data) {
.int_literal, .float_literal, .bool_literal, .string_literal, .undef_literal, .null_literal => {
const ty = self.resolveType(ta);
self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = ty }) catch {};
},
else => {},
}
}
/// Register a top-level mutable global (e.g., `context : Context = ---;`).
/// Run AFTER `resolveForwardIdentifierAliases` so a forward identifier alias
/// in the type annotation (`A :: B; B :: s32; g : A = 7;`) resolves to its
/// target instead of a fabricated empty-struct stub, which would otherwise
/// give the global a type that mismatches its initializer at LLVM
/// verification (issue 0070). Globals can't be named in a type position, so
/// deferring them past type/alias registration introduces no ordering hazard.
fn registerTopLevelGlobal(self: *Lowering, vd: *const ast.VarDecl) void {
// Use self.resolveType so type aliases like `Handle :: u32;` resolve
// to their target type (not a synthetic empty struct). When the
// user omitted the annotation, infer from the initializer
// expression; foreign globals with no annotation are diagnosed
// because their type can't be inferred without an initializer.
const var_ty: TypeId = if (vd.type_annotation) |ta|
self.resolveType(ta)
else if (vd.value) |val|
self.inferExprType(val)
else blk: {
if (self.diagnostics) |d|
d.addFmt(.err, null, "top-level var '{s}' has no type annotation and no initializer to infer from", .{vd.name});
break :blk .void;
};
// Foreign globals reference a symbol defined in libSystem etc.
// (`_NSConcreteStackBlock : *void #foreign;`). The C symbol
// 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 gid = self.module.addGlobal(.{
.name = name_id,
.ty = var_ty,
.init_val = init_val,
.is_const = false,
.is_extern = vd.is_foreign,
});
self.program_index.global_names.put(vd.name, .{ .id = gid, .ty = var_ty }) catch {};
} }
/// Resolve identifier-RHS type aliases whose target is declared LATER in the /// Resolve identifier-RHS type aliases whose target is declared LATER in the