diff --git a/src/ir/lower/decl.zig b/src/ir/lower/decl.zig index bc963f5..8457fb3 100644 --- a/src/ir/lower/decl.zig +++ b/src/ir/lower/decl.zig @@ -2247,13 +2247,28 @@ pub fn isVisible(self: *Lowering, name: []const u8, vis: resolver_mod.Visibility // this predicate is single-hop only. .impl_transitive => @panic("isVisible: transitive visibility is owned by findVisibleImpls"), .c_import_bare => { - // Foreign-C gate: only C-import fn_decls without a library_ref - // are policed; a non-foreign body or a library-bound foreign - // decl is unconditionally visible. + // Foreign-C gate: only a lib-less C-import fn_decl is policed; a + // library-bound decl (resolves via the named library, not a + // module edge) or a non-C body is unconditionally visible. The + // legacy `#foreign` form (a `foreign_expr` body) and the new + // `extern` keyword (`extern_export == .extern_`, empty-block body) + // are two spellings of the same lib-less C-symbol import, so BOTH + // route to `visibleOverEdges` here — a migrated `extern` decl must + // get the identical "C function not visible" diagnostic its + // `#foreign` twin did, not the generic top-level-name wording + // (FFI-linkage Part B; example 1228). const fd = self.program_index.fn_ast_map.get(name) orelse return true; - if (fd.body.data != .foreign_expr) return true; - if (fd.body.data.foreign_expr.library_ref != null) return true; - return self.visibleOverEdges(name); + switch (fd.body.data) { + .foreign_expr => |fe| { + if (fe.library_ref != null) return true; + return self.visibleOverEdges(name); + }, + else => { + if (fd.extern_export != .extern_) return true; + if (fd.extern_lib != null) return true; + return self.visibleOverEdges(name); + }, + } }, .user_bare_flat => return self.visibleOverEdges(name), }