diff --git a/examples/0835-modules-same-name-global-vs-const-own.sx b/examples/0835-modules-same-name-global-vs-const-own.sx new file mode 100644 index 0000000..f27ab93 --- /dev/null +++ b/examples/0835-modules-same-name-global-vs-const-own.sx @@ -0,0 +1,15 @@ +// A module's own scalar const `K` and another module's same-named ARRAY +// GLOBAL (`K : [4]s64 = .[...]`) coexist: each module's bare `K` binds its +// OWN author. The global registry is last-wins across modules, so without +// source-aware selection a.sx's `K` read the array global's address. +// +// Regression (issue 0115): a.sx printed the array's address; h.sx's reads +// stayed correct only by registration order. + +#import "modules/std.sx"; +#import "0835-modules-same-name-global-vs-const-own/a.sx"; +h :: #import "0835-modules-same-name-global-vs-const-own/h.sx"; + +main :: () { + print("a_k={} use_k={}\n", a_k(), h.use_k()); +} diff --git a/examples/0835-modules-same-name-global-vs-const-own/a.sx b/examples/0835-modules-same-name-global-vs-const-own/a.sx new file mode 100644 index 0000000..8dba014 --- /dev/null +++ b/examples/0835-modules-same-name-global-vs-const-own/a.sx @@ -0,0 +1,2 @@ +K :: 1; +a_k :: () -> s64 { K } diff --git a/examples/0835-modules-same-name-global-vs-const-own/h.sx b/examples/0835-modules-same-name-global-vs-const-own/h.sx new file mode 100644 index 0000000..17525a4 --- /dev/null +++ b/examples/0835-modules-same-name-global-vs-const-own/h.sx @@ -0,0 +1,2 @@ +K : [4]s64 = .[11, 22, 33, 44]; +use_k :: () -> s64 { K[2] } diff --git a/examples/0836-modules-own-const-vs-ns-array-global.sx b/examples/0836-modules-own-const-vs-ns-array-global.sx new file mode 100644 index 0000000..6a852b6 --- /dev/null +++ b/examples/0836-modules-own-const-vs-ns-array-global.sx @@ -0,0 +1,15 @@ +// The MAIN file's own typed scalar const `K : s64 : 4` vs a namespaced +// module's same-named array global: the main file's bare `K` is its own +// scalar — type inference must not borrow the array global's type either +// (the print pack used to format the whole 4-element array). +// +// Regression (issue 0115). + +#import "modules/std.sx"; +h :: #import "0836-modules-own-const-vs-ns-array-global/h.sx"; + +K : s64 : 4; + +main :: () { + print("K={} use_k={}\n", K, h.use_k()); +} diff --git a/examples/0836-modules-own-const-vs-ns-array-global/h.sx b/examples/0836-modules-own-const-vs-ns-array-global/h.sx new file mode 100644 index 0000000..17525a4 --- /dev/null +++ b/examples/0836-modules-own-const-vs-ns-array-global/h.sx @@ -0,0 +1,2 @@ +K : [4]s64 = .[11, 22, 33, 44]; +use_k :: () -> s64 { K[2] } diff --git a/examples/0837-modules-array-const-no-cross-borrow.sx b/examples/0837-modules-array-const-no-cross-borrow.sx new file mode 100644 index 0000000..a93882c --- /dev/null +++ b/examples/0837-modules-array-const-no-cross-borrow.sx @@ -0,0 +1,17 @@ +// An ARRAY-typed `::` const (`K : [4]s64 : .[...]`) is not a supported +// module-const shape — its own module's read diagnoses cleanly as +// unresolved. The own author OWNS the name: the read must never borrow +// another module's same-named scalar const (which used to type `K[2]` +// against the scalar and panic at LLVM emission). +// +// Regression (issue 0115). When array `::` consts land, repoint this +// example at the new behavior. + +#import "modules/std.sx"; +h :: #import "0837-modules-array-const-no-cross-borrow/h.sx"; + +K : s64 : 4; + +main :: () { + print("{}\n", h.use_k()); +} diff --git a/examples/0837-modules-array-const-no-cross-borrow/h.sx b/examples/0837-modules-array-const-no-cross-borrow/h.sx new file mode 100644 index 0000000..f3dfc32 --- /dev/null +++ b/examples/0837-modules-array-const-no-cross-borrow/h.sx @@ -0,0 +1,2 @@ +K : [4]s64 : .[11, 22, 33, 44]; +use_k :: () -> s64 { K[2] } diff --git a/examples/expected/0835-modules-same-name-global-vs-const-own.exit b/examples/expected/0835-modules-same-name-global-vs-const-own.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0835-modules-same-name-global-vs-const-own.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0835-modules-same-name-global-vs-const-own.stderr b/examples/expected/0835-modules-same-name-global-vs-const-own.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0835-modules-same-name-global-vs-const-own.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0835-modules-same-name-global-vs-const-own.stdout b/examples/expected/0835-modules-same-name-global-vs-const-own.stdout new file mode 100644 index 0000000..5d092d9 --- /dev/null +++ b/examples/expected/0835-modules-same-name-global-vs-const-own.stdout @@ -0,0 +1 @@ +a_k=1 use_k=33 diff --git a/examples/expected/0836-modules-own-const-vs-ns-array-global.exit b/examples/expected/0836-modules-own-const-vs-ns-array-global.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0836-modules-own-const-vs-ns-array-global.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0836-modules-own-const-vs-ns-array-global.stderr b/examples/expected/0836-modules-own-const-vs-ns-array-global.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0836-modules-own-const-vs-ns-array-global.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0836-modules-own-const-vs-ns-array-global.stdout b/examples/expected/0836-modules-own-const-vs-ns-array-global.stdout new file mode 100644 index 0000000..fb369c4 --- /dev/null +++ b/examples/expected/0836-modules-own-const-vs-ns-array-global.stdout @@ -0,0 +1 @@ +K=4 use_k=33 diff --git a/examples/expected/0837-modules-array-const-no-cross-borrow.exit b/examples/expected/0837-modules-array-const-no-cross-borrow.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0837-modules-array-const-no-cross-borrow.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0837-modules-array-const-no-cross-borrow.stderr b/examples/expected/0837-modules-array-const-no-cross-borrow.stderr new file mode 100644 index 0000000..a99ce1b --- /dev/null +++ b/examples/expected/0837-modules-array-const-no-cross-borrow.stderr @@ -0,0 +1,5 @@ +error: unresolved 'K' (in examples/0837-modules-array-const-no-cross-borrow/h.sx fn use_k) + --> examples/0837-modules-array-const-no-cross-borrow/h.sx:2:22 + | + 2 | use_k :: () -> s64 { K[2] } + | ^ diff --git a/examples/expected/0837-modules-array-const-no-cross-borrow.stdout b/examples/expected/0837-modules-array-const-no-cross-borrow.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0837-modules-array-const-no-cross-borrow.stdout @@ -0,0 +1 @@ + diff --git a/issues/0115-same-name-const-scalar-array-collision.md b/issues/0115-same-name-const-scalar-array-collision.md index 8143f94..99e03ec 100644 --- a/issues/0115-same-name-const-scalar-array-collision.md +++ b/issues/0115-same-name-const-scalar-array-collision.md @@ -1,5 +1,23 @@ # 0115 — same-name consts of different shapes collide across modules (panic / silent clobber) +> **RESOLVED** (2026-06-11). Root cause: the globals registry +> (`global_names`) is last-wins across modules and every read/write/addr/ +> call site consulted it with no source-awareness; `var_decl` was not even +> a selectable raw author. Fix: `var_decl` joins `RawDeclRef`; +> `selectGlobalAuthor` (the globals analogue of F2's `selectModuleConst`) +> selects the author own-wins / one-flat-visible / ambiguous-loudly and +> serves the AUTHOR's per-source global; all bare-identifier global sites +> (read, addr-of, assignment, fn-ptr call, type inference) route through +> it. `selectModuleConst` gained `.own_opaque` so an own const author with +> no materialized value (unsupported shape, e.g. an array `::` const) +> blocks borrowing another module's same-named const instead of panicking. +> The fn-as-VALUE arm admits raw-facts-only authors (an own fn dropped from +> the global decl list by a flat-merge collision — the 0601 `test` case). +> Regression tests: examples/0835, 0836, 0837. The co-blockers also +> landed: dead-global elimination at emit (unreferenced plain-data globals +> are not emitted) and 1055/1056 no longer pin global error ordinals — +> the full std namespace tail is enabled on top. + **Symptom.** When two modules in one program declare a same-named module const with DIFFERENT shapes (scalar `K : s64 : 4` vs array `K : [4]s64 : .[...]`), resolution conflates them instead of selecting diff --git a/src/imports.zig b/src/imports.zig index d1c84bf..c9215c0 100644 --- a/src/imports.zig +++ b/src/imports.zig @@ -457,6 +457,7 @@ pub const ModuleCache = std.StringHashMap(ResolvedModule); pub const RawDeclRef = union(enum) { fn_decl: *const ast.FnDecl, const_decl: *const ast.ConstDecl, + var_decl: *const ast.VarDecl, struct_decl: *const ast.StructDecl, enum_decl: *const ast.EnumDecl, union_decl: *const ast.UnionDecl, @@ -499,13 +500,14 @@ pub const NamespaceTarget = struct { pub const NamespaceEdges = std.StringHashMap(std.StringHashMap(NamespaceTarget)); /// The `RawDeclRef` a top-level node carries, or null when the node is not a -/// selectable named declaration (e.g. `impl_block`, `var_decl`, `ufcs_alias`, -/// a flat `c_import_decl`). Public so the unified resolver's namespace collector +/// selectable named declaration (e.g. `impl_block`, `ufcs_alias`, a flat +/// `c_import_decl`). Public so the unified resolver's namespace collector /// can classify a `NamespaceTarget.own_decls` node without re-deriving the map. pub fn rawDeclRefOf(decl: *const Node) ?RawDeclRef { return switch (decl.data) { .fn_decl => |*d| .{ .fn_decl = d }, .const_decl => |*d| .{ .const_decl = d }, + .var_decl => |*d| .{ .var_decl = d }, .struct_decl => |*d| .{ .struct_decl = d }, .enum_decl => |*d| .{ .enum_decl = d }, .union_decl => |*d| .{ .union_decl = d }, @@ -584,6 +586,7 @@ pub fn buildImportFacts( pub const DeclKind = enum { function, constant, + global, @"struct", @"enum", @"union", @@ -597,6 +600,7 @@ fn declKindOf(ref: RawDeclRef) DeclKind { return switch (ref) { .fn_decl => .function, .const_decl => .constant, + .var_decl => .global, .struct_decl => .@"struct", .enum_decl => .@"enum", .union_decl => .@"union", diff --git a/src/ir/expr_typer.zig b/src/ir/expr_typer.zig index 555a47b..25bff61 100644 --- a/src/ir/expr_typer.zig +++ b/src/ir/expr_typer.zig @@ -273,9 +273,17 @@ pub const ExprTyper = struct { if (self.l.implicit_ctx_enabled and std.mem.eql(u8, id.name, "context")) { if (self.l.module.types.findByName(self.l.module.types.internString("Context"))) |ty| return ty; } - // Check global variables (e.g., `context : Context`) + // Check global variables (e.g., `context : Context`) — + // source-aware (issue 0115): infer the AUTHOR's global type, + // never an unrelated module's same-named one. `.not_a_global` + // falls through to the const / fn arms below. if (self.l.program_index.global_names.get(id.name)) |gi| { - return gi.ty; + switch (self.l.selectGlobalAuthor(id.name)) { + .resolved => |g| return g.ty, + .untracked => return gi.ty, + .ambiguous, .not_visible => return .unresolved, + .not_a_global => {}, + } } // Check module-level value constants (e.g., WIDTH :f32: 800). // F4: a same-name VALUE const must infer the SOURCE-AWARE author's @@ -290,7 +298,7 @@ pub const ExprTyper = struct { return switch (self.l.selectModuleConst(id.name)) { .resolved => |sel| sel.info.ty, .none => ci_global.ty, - .ambiguous => .unresolved, + .own_opaque, .ambiguous => .unresolved, }; } // A bare type name (alias like `Vec4`, struct name, or diff --git a/src/ir/lower.zig b/src/ir/lower.zig index d9dbfd0..e721e75 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1508,6 +1508,9 @@ pub const Lowering = struct { pub const sourceConstIsFloatTyped = lower_comptime.sourceConstIsFloatTyped; pub const comptimeIntNamed = lower_comptime.comptimeIntNamed; pub const selectModuleConst = lower_comptime.selectModuleConst; + pub const GlobalAuthor = lower_comptime.GlobalAuthor; + pub const selectGlobalAuthor = lower_comptime.selectGlobalAuthor; + pub const resolveGlobalRef = lower_comptime.resolveGlobalRef; pub const sourceModuleConst = lower_comptime.sourceModuleConst; pub const pinConstAuthorSource = lower_comptime.pinConstAuthorSource; pub const foldComptimeFloatInit = lower_comptime.foldComptimeFloatInit; diff --git a/src/ir/lower/call.zig b/src/ir/lower/call.zig index 8152725..3153e4d 100644 --- a/src/ir/lower/call.zig +++ b/src/ir/lower/call.zig @@ -9,6 +9,7 @@ const unescape = @import("../../unescape.zig"); const errors = @import("../../errors.zig"); const program_index_mod = @import("../program_index.zig"); const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo; +const GlobalInfo = program_index_mod.GlobalInfo; const CallResolver = @import("../calls.zig").CallResolver; const TypeId = types.TypeId; @@ -542,7 +543,7 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref { } } // May be a global variable holding a function pointer - if (self.program_index.global_names.get(id.name)) |gi| { + if (self.resolveGlobalRef(id.name, c.callee.span)) |gi| { if (!gi.ty.isBuiltin()) { const gti = self.module.types.get(gi.ty); if (gti == .function) { @@ -2176,12 +2177,20 @@ pub fn resolveCallParamTypes(self: *Lowering, c: *const ast.Call, sel_author: ?* return types_list.items; } - // Check global function pointer variables - if (self.program_index.global_names.get(bare_name)) |gi| { - if (!gi.ty.isBuiltin()) { - const ti = self.module.types.get(gi.ty); - if (ti == .function) { - return ti.function.params; + // Check global function pointer variables (quiet author-aware lookup — + // param typing only; the call site diagnoses ambiguity / visibility) + if (self.program_index.global_names.get(bare_name)) |gi_global| { + const gi: ?GlobalInfo = switch (self.selectGlobalAuthor(bare_name)) { + .resolved => |g| g, + .untracked => gi_global, + else => null, + }; + if (gi) |g| { + if (!g.ty.isBuiltin()) { + const ti = self.module.types.get(g.ty); + if (ti == .function) { + return ti.function.params; + } } } } diff --git a/src/ir/lower/comptime.zig b/src/ir/lower/comptime.zig index 07438d4..80e2c1c 100644 --- a/src/ir/lower/comptime.zig +++ b/src/ir/lower/comptime.zig @@ -8,6 +8,7 @@ const unescape = @import("../../unescape.zig"); const parser_mod = @import("../../parser.zig"); const interp_mod = @import("../interp.zig"); const program_index_mod = @import("../program_index.zig"); +const resolver_mod = @import("../resolver.zig"); const ModuleConstInfo = program_index_mod.ModuleConstInfo; const TypeId = types.TypeId; @@ -788,7 +789,7 @@ pub fn foldSourceConstInt(self: *Lowering, name: []const u8, frame: ?*const Cons defer restore.unpin(); return program_index_mod.evalConstIntExpr(sel.info.value, SourceConstCtx{ .lowering = self, .frame = &f }); }, - .ambiguous, .none => null, + .own_opaque, .ambiguous, .none => null, }; } @@ -803,7 +804,7 @@ pub fn foldSourceConstFloat(self: *Lowering, name: []const u8, frame: ?*const Co defer restore.unpin(); return program_index_mod.evalConstFloatExpr(sel.info.value, SourceConstCtx{ .lowering = self, .frame = &f }); }, - .ambiguous, .none => null, + .own_opaque, .ambiguous, .none => null, }; } @@ -819,7 +820,7 @@ pub fn sourceConstIsFloatTyped(self: *Lowering, name: []const u8, frame: ?*const defer restore.unpin(); return program_index_mod.isFloatValuedExpr(sel.info.value, SourceConstCtx{ .lowering = self, .frame = &f }); }, - .ambiguous, .none => false, + .own_opaque, .ambiguous, .none => false, }; } @@ -836,6 +837,11 @@ pub const SelectedConst = struct { const ConstAuthor = union(enum) { resolved: SelectedConst, + /// The reader's OWN module authors `name` as a const, but no per-source + /// value registered — an unsupported const shape (e.g. an array-literal + /// const). The own author owns the name: the read must NOT borrow another + /// module's same-named const, so callers treat it as unresolvable. + own_opaque, ambiguous, none, }; @@ -864,7 +870,14 @@ pub fn selectModuleConst(self: *Lowering, name: []const u8) ConstAuthor { var res = self.resolver(); const set = res.collectVisibleAuthors(name, from, .user_bare_flat); defer if (set.flat.len > 0) self.alloc.free(set.flat); - if (set.own) |o| if (self.sourceModuleConst(o.source, name)) |ci| return .{ .resolved = .{ .info = ci, .source = o.source } }; + if (set.own) |o| { + if (self.sourceModuleConst(o.source, name)) |ci| return .{ .resolved = .{ .info = ci, .source = o.source } }; + // The reader's own module authors `name` as a const that never + // materialized a per-source value (unsupported shape). Owning the + // name blocks borrowing a flat import's / the global registration's + // same-named const (issue 0115). + if (o.raw == .const_decl) return .own_opaque; + } var the_one: ?SelectedConst = null; var count: usize = 0; for (set.flat) |fa| { @@ -884,6 +897,89 @@ pub fn sourceModuleConst(self: *Lowering, source: []const u8, name: []const u8) return inner.get(name); } +pub const GlobalAuthor = union(enum) { + /// The visible author's OWN global (per-source partition) — emit it. + resolved: program_index_mod.GlobalInfo, + /// The visible author declares `name` but NOT as a global (a const, fn, + /// type, ...) — skip the global arm and let the later arms decide. + not_a_global, + /// ≥2 distinct flat-visible authors and none is the reader's own. + ambiguous, + /// `name` is authored somewhere, but no author is visible from the + /// reading module (namespaced-only / beyond one flat hop). + not_visible, + /// No raw author anywhere — a compiler-synthesized global (FFI metadata, + /// trace machinery, ...). Emit the global registration directly. + untracked, +}; + +/// The source-aware GLOBAL author of `name` from the querying module — the +/// global-variable analogue of `selectModuleConst`. The global registry +/// (`global_names`) is last-wins across modules; this selects the AUTHOR +/// first (own wins, then the single direct flat author) and reads the +/// global from the SELECTED author's per-source partition, so a module +/// whose own `K` is a const (or whose flat import authors `K`) never has a +/// bare `K` hijacked by an unrelated module's same-named global. +pub fn selectGlobalAuthor(self: *Lowering, name: []const u8) GlobalAuthor { + const from = self.current_source_file orelse self.main_file orelse return .untracked; + var res = self.resolver(); + const set = res.collectVisibleAuthors(name, from, .user_bare_flat); + defer if (set.flat.len > 0) self.alloc.free(set.flat); + if (set.own) |o| return globalAuthorAt(self, o, name); + if (set.flat.len >= 2) return .ambiguous; + if (set.flat.len == 1) return globalAuthorAt(self, set.flat[0], name); + return if (anyRawAuthor(self, name)) .not_visible else .untracked; +} + +fn globalAuthorAt(self: *Lowering, author: resolver_mod.RawAuthor, name: []const u8) GlobalAuthor { + if (self.program_index.globals_by_source.get(author.source)) |inner| { + if (inner.get(name)) |g| return .{ .resolved = g }; + } + // A var_decl author with no per-source registration: the decl was deduped + // at flat-merge (two modules declaring the same extern symbol), so the + // global registered under the surviving author's source. The author IS a + // global — serve the symbol's registration. + if (author.raw == .var_decl) { + if (self.program_index.global_names.get(name)) |g| return .{ .resolved = g }; + } + return .not_a_global; +} + +/// The source-aware global for a bare reference to `name`, or null when the +/// reference does not (visibly) resolve to a global — the ambiguous / +/// not-visible outcomes diagnose here and return null so the caller's +/// fall-through path runs. Sites needing custom placeholder emission use +/// `selectGlobalAuthor` directly. +pub fn resolveGlobalRef(self: *Lowering, name: []const u8, span: ?ast.Span) ?program_index_mod.GlobalInfo { + const gi = self.program_index.global_names.get(name) orelse return null; + switch (self.selectGlobalAuthor(name)) { + .resolved => |g| return g, + .not_a_global => return null, + .ambiguous => { + if (self.diagnostics) |d| + d.addFmt(.err, span, "'{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{name}); + return null; + }, + .not_visible => { + if (self.diagnostics) |d| + d.addFmt(.err, span, "'{s}' is not visible; #import the module that declares it", .{name}); + return null; + }, + .untracked => return gi, + } +} + +/// True when ANY module's raw-decl index carries `name` — distinguishes a +/// user-authored-but-not-visible name from a compiler-synthesized one. +fn anyRawAuthor(self: *Lowering, name: []const u8) bool { + const decls = self.program_index.module_decls orelse return false; + var it = decls.valueIterator(); + while (it.next()) |mod| { + if (mod.names.contains(name)) return true; + } + return false; +} + /// Saved `current_source_file` for a const-author pin; `unpin()` restores it. const ConstSourcePin = struct { lowering: *Lowering, diff --git a/src/ir/lower/decl.zig b/src/ir/lower/decl.zig index e8fefc5..c843c80 100644 --- a/src/ir/lower/decl.zig +++ b/src/ir/lower/decl.zig @@ -1024,6 +1024,11 @@ pub fn globalInitValue(self: *Lowering, vd: *const ast.VarDecl, var_ty: TypeId) const sel: SelectedConst = switch (self.selectModuleConst(id.name)) { .resolved => |s| s, .none => .{ .info = ci_global, .source = null }, + .own_opaque => { + 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; + }, .ambiguous => { if (self.diagnostics) |d| d.addFmt(.err, v.span, "'{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{id.name}); @@ -1570,7 +1575,7 @@ pub fn selectNominalLeaf(self: *Lowering, name: []const u8, from: []const u8, ra pub fn isNamedTypeKind(raw: resolver_mod.RawDeclRef) bool { return switch (raw) { .struct_decl, .enum_decl, .union_decl, .error_set_decl, .protocol_decl, .foreign_class_decl => true, - .fn_decl, .const_decl, .namespace_decl => false, + .fn_decl, .const_decl, .var_decl, .namespace_decl => false, }; } @@ -1598,7 +1603,7 @@ pub fn namedRefTid(self: *Lowering, ref: resolver_mod.RawDeclRef, name: []const .enum_decl => |d| (table.type_decl_tids.get(@ptrCast(d)) orelse table.findByName(table.internString(name))), .union_decl => |d| (table.type_decl_tids.get(@ptrCast(d)) orelse table.findByName(table.internString(name))), .error_set_decl, .protocol_decl, .foreign_class_decl => table.findByName(table.internString(name)), - .fn_decl, .const_decl, .namespace_decl => null, + .fn_decl, .const_decl, .var_decl, .namespace_decl => null, }; } diff --git a/src/ir/lower/expr.zig b/src/ir/lower/expr.zig index ce5316f..1e27054 100644 --- a/src/ir/lower/expr.zig +++ b/src/ir/lower/expr.zig @@ -1622,9 +1622,26 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref { }; break :blk self.builder.load(self.current_ctx_ref, ctx_ty); } - // Check globals (#run constants) + // Check globals (#run constants) — source-aware (issue 0115): + // the global registry is last-wins across modules, so select the + // AUTHOR first and emit ITS global, never an unrelated module's + // same-named one. if (self.program_index.global_names.get(id.name)) |gi| { - break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty); + switch (self.selectGlobalAuthor(id.name)) { + .resolved => |g| break :blk self.builder.emit(.{ .global_get = g.id }, g.ty), + .not_a_global => {}, + .ambiguous => { + if (self.diagnostics) |d| + d.addFmt(.err, node.span, "'{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{id.name}); + break :blk self.emitPlaceholder(id.name); + }, + .not_visible => { + if (self.diagnostics) |d| + d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{id.name}); + break :blk self.emitError(id.name, node.span); + }, + .untracked => break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty), + } } // Check module-level value constants (e.g. AF_INET :s32: 2) if (self.program_index.module_const_map.get(id.name)) |ci_global| { @@ -1640,6 +1657,10 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref { // author (no per-source partition) — emit its global value. switch (self.selectModuleConst(id.name)) { .resolved => |sel| break :blk self.emitModuleConst(sel.info, sel.source), + // Own const author with no materialized value (unsupported + // shape, e.g. an array const) — fall through; the tail of + // identifier lowering diagnoses it as unresolved. + .own_opaque => {}, .ambiguous => { if (self.diagnostics) |d| d.addFmt(.err, node.span, "'{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{id.name}); @@ -1651,7 +1672,16 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref { // Check if it's a function name — produce function pointer reference // Resolve mangled name for block-local functions const eff_fn_name = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name; - if (self.program_index.fn_ast_map.contains(eff_fn_name)) { + // An own fn whose name a flat-merge collision dropped from the + // global decl list (first-wins) has no `fn_ast_map` entry but IS + // a raw-facts author — the author selection inside this arm + // serves it, so admit it through the gate. + const fn_author_only = !self.program_index.fn_ast_map.contains(eff_fn_name) and + std.mem.eql(u8, eff_fn_name, id.name) and + (if (self.scope) |scope| scope.lookup(id.name) == null else true) and + self.current_source_file != null and + self.selectPlainCallableAuthor(id.name, self.current_source_file.?) == .func; + if (self.program_index.fn_ast_map.contains(eff_fn_name) or fn_author_only) { // Visibility check only for user-typed bare names (id.name // == eff_fn_name) without a UFCS alias. Mangled local- // scope names and UFCS rewrites are compiler indirections @@ -1666,11 +1696,18 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref { } // Type-as-value: if target is Any (Type variable), produce a type name string if (self.target_type == .any) { - const fd = self.program_index.fn_ast_map.get(eff_fn_name).?; - const fn_type_str = self.formatFnTypeString(fd); - const sid = self.module.types.internString(fn_type_str); - const str = self.builder.constString(sid); - break :blk self.builder.boxAny(str, .string); + const fd_any: ?*const ast.FnDecl = self.program_index.fn_ast_map.get(eff_fn_name) orelse fd_blk: { + switch (self.selectPlainCallableAuthor(id.name, self.current_source_file.?)) { + .func => |sf| break :fd_blk sf.decl, + else => break :fd_blk null, + } + }; + if (fd_any) |fd| { + const fn_type_str = self.formatFnTypeString(fd); + const sid = self.module.types.internString(fn_type_str); + const str = self.builder.constString(sid); + break :blk self.builder.boxAny(str, .string); + } } // taking a bare same-name fn as a VALUE // (func_ref, fn-ptr / closure coercion) must capture the @@ -1835,7 +1872,7 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref { } } // address_of(global) → emit global_addr (pointer to global, not load) - if (self.program_index.global_names.get(id_name)) |gi| { + if (self.resolveGlobalRef(id_name, node.span)) |gi| { const ptr_ty = self.module.types.ptrTo(gi.ty); break :blk self.builder.emit(.{ .global_addr = gi.id }, ptr_ty); } diff --git a/src/ir/lower/nominal.zig b/src/ir/lower/nominal.zig index 92db488..cd35db9 100644 --- a/src/ir/lower/nominal.zig +++ b/src/ir/lower/nominal.zig @@ -297,7 +297,7 @@ pub fn rawNamedTypePtr(ref: resolver_mod.RawDeclRef) ?*const anyopaque { .error_set_decl => |d| @ptrCast(d), .protocol_decl => |d| @ptrCast(d), .foreign_class_decl => |d| @ptrCast(d), - .fn_decl, .const_decl, .namespace_decl => null, + .fn_decl, .const_decl, .var_decl, .namespace_decl => null, }; } diff --git a/src/ir/lower/stmt.zig b/src/ir/lower/stmt.zig index e0beb78..531021f 100644 --- a/src/ir/lower/stmt.zig +++ b/src/ir/lower/stmt.zig @@ -552,8 +552,14 @@ pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void { } } if (!found_local) { + // Quiet author-aware lookup (type inference only; the store + // site diagnoses ambiguity / visibility). if (self.program_index.global_names.get(asgn.target.data.identifier.name)) |gi| { - self.target_type = gi.ty; + switch (self.selectGlobalAuthor(asgn.target.data.identifier.name)) { + .resolved => |g| self.target_type = g.ty, + .untracked => self.target_type = gi.ty, + else => {}, + } } } } else if (asgn.target.data == .index_expr) { @@ -617,9 +623,11 @@ pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void { } } } - // Fallback: global variable assignment + // Fallback: global variable assignment — source-aware (issue + // 0115): write the AUTHOR's global, never an unrelated module's + // same-named one. if (!handled) { - if (self.program_index.global_names.get(id.name)) |gi| { + if (self.resolveGlobalRef(id.name, asgn.target.span)) |gi| { if (asgn.op == .assign) { const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != gi.ty and val_ty != .void and gi.ty != .void) @@ -890,7 +898,7 @@ pub fn lowerExprAsPtr(self: *Lowering, node: *const Node) Ref { } return binding.ref; } - } else if (self.program_index.global_names.get(id.name)) |gi| { + } else if (self.resolveGlobalRef(id.name, null)) |gi| { // Module-global lvalue: address into the global's live storage // so a downstream GEP/store targets the global itself, not a // loaded copy. A pointer-typed global is loaded first to get