From 932cdfa2ec7dfe9d2939d839cee7ef2472a4c05d Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 2 Jun 2026 17:20:31 +0300 Subject: [PATCH] fix(ir): resolve forward alias in top-level global annotations (issue 0070) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- examples/0133-types-forward-alias-global.sx | 22 +++ .../0133-types-forward-alias-global.exit | 1 + .../0133-types-forward-alias-global.stderr | 0 .../0133-types-forward-alias-global.stdout | 2 + .../1309-ffi-objc-class-method-lowering.ir | 4 +- ...alias-global-annotation-before-fixpoint.md | 113 +++++++++++++++ src/ir/lower.zig | 135 +++++++++++------- 7 files changed, 225 insertions(+), 52 deletions(-) create mode 100644 examples/0133-types-forward-alias-global.sx create mode 100644 examples/expected/0133-types-forward-alias-global.exit create mode 100644 examples/expected/0133-types-forward-alias-global.stderr create mode 100644 examples/expected/0133-types-forward-alias-global.stdout create mode 100644 issues/0070-forward-alias-global-annotation-before-fixpoint.md diff --git a/examples/0133-types-forward-alias-global.sx b/examples/0133-types-forward-alias-global.sx new file mode 100644 index 0000000..572cb88 --- /dev/null +++ b/examples/0133-types-forward-alias-global.sx @@ -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; +} diff --git a/examples/expected/0133-types-forward-alias-global.exit b/examples/expected/0133-types-forward-alias-global.exit new file mode 100644 index 0000000..d81cc07 --- /dev/null +++ b/examples/expected/0133-types-forward-alias-global.exit @@ -0,0 +1 @@ +42 diff --git a/examples/expected/0133-types-forward-alias-global.stderr b/examples/expected/0133-types-forward-alias-global.stderr new file mode 100644 index 0000000..e69de29 diff --git a/examples/expected/0133-types-forward-alias-global.stdout b/examples/expected/0133-types-forward-alias-global.stdout new file mode 100644 index 0000000..628ca79 --- /dev/null +++ b/examples/expected/0133-types-forward-alias-global.stdout @@ -0,0 +1,2 @@ +global g: 7 +const K: 35 diff --git a/examples/expected/1309-ffi-objc-class-method-lowering.ir b/examples/expected/1309-ffi-objc-class-method-lowering.ir index 6e713f0..fbc26d3 100644 --- a/examples/expected/1309-ffi-objc-class-method-lowering.ir +++ b/examples/expected/1309-ffi-objc-class-method-lowering.ir @@ -1,9 +1,9 @@ +@__SxFoo_state_ivar = internal global ptr null +@__SxFoo_class = internal global ptr null @OS = internal global i64 0 @ARCH = internal global i64 0 @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_objc_cstr_dealloc = internal global [8 x i8] c"dealloc\00" @str = private unnamed_addr constant [2 x i8] c"0\00", align 1 diff --git a/issues/0070-forward-alias-global-annotation-before-fixpoint.md b/issues/0070-forward-alias-global-annotation-before-fixpoint.md new file mode 100644 index 0000000..f9650cc --- /dev/null +++ b/issues/0070-forward-alias-global-annotation-before-fixpoint.md @@ -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. diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 932b134..35f0e20 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1153,16 +1153,13 @@ pub const Lowering = struct { } // comptime_expr handled in Pass 2 - // Simple value constants with type annotation (e.g. AF_INET :s32: 2) - if (cd.type_annotation) |ta| { - 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 => {}, - } - } else { + // Typed value constants (`AF_INET :s32: 2`) are registered in + // pass 2 below — after the forward-alias fixpoint — so a + // forward identifier alias in the annotation resolves to its + // target instead of a fabricated stub (issue 0070). Untyped + // literal constants carry no annotation to resolve, so they + // stay here (their type comes from the literal / inference). + if (cd.type_annotation == null) { // Untyped literal constants (e.g. UI_VERT_SRC :: #string GLSL...GLSL;) switch (cd.value.data) { .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| { self.program_index.ufcs_alias_map.put(ua.name, ua.target) catch {}; }, - .var_decl => |vd| { - // Top-level mutable global (e.g., `context : Context = ---;`) - // 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 {}; - }, + // Top-level globals are registered in a second pass (below), + // after the forward-alias fixpoint, so a forward identifier + // alias used as a global's type annotation resolves (issue 0070). + .var_decl => {}, else => {}, } } 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