From 7ffdc7d2a28ed05c354ea1f4b7f8cb0f6159f35a Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 15 Jun 2026 08:54:56 +0300 Subject: [PATCH] =?UTF-8?q?refactor(ffi-linkage):=20Phase=209.1d=20?= =?UTF-8?q?=E2=80=94=20eliminate=20the=20foreign=5Fexpr=20AST=20node?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last linkage-family 'foreign' carrier. Migrated c_import.zig auto-synthesis (#import c {#include}) to build the extern shape (empty-block body + extern_export = .extern_) instead of a foreign_expr body — the Phase 5.0 fn-body flip applied to auto-synth. With nothing left building it, deleted the foreign_expr union variant + ForeignExpr struct (ast.zig) and every reader: the dead-arm switch cases (sema, resolver, generic, call, semantic_diagnostics, lsp), the coalescing reads in decl.zig (is_foreign local, cc/rename/dedup/variadic/visibility gates) + pack.zig, and checkForeignRefs (now reads extern_lib only). 9.1 LINKAGE PURGE COMPLETE — all that remains in src/ is the runtime-class family (9.2) + comments. Snapshot-neutral (the #import c examples 1215/1216/1217 + sqlite 1624 exercise the synth path); suite green (646 corpus / 444 unit, 0 failed). --- src/ast.zig | 6 --- src/c_import.zig | 35 ++++++------- src/ir/lower/call.zig | 2 +- src/ir/lower/decl.zig | 87 +++++++++++++-------------------- src/ir/lower/generic.zig | 2 +- src/ir/lower/pack.zig | 4 +- src/ir/resolver.zig | 2 +- src/ir/semantic_diagnostics.zig | 1 - src/lsp/server.zig | 2 +- src/sema.zig | 2 - 10 files changed, 55 insertions(+), 88 deletions(-) diff --git a/src/ast.zig b/src/ast.zig index 2e029a56..cbeee350 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -82,7 +82,6 @@ pub const Node = struct { inferred_type: void, builtin_expr: void, compiler_expr: void, - foreign_expr: ForeignExpr, library_decl: LibraryDecl, framework_decl: FrameworkDecl, function_type_expr: FunctionTypeExpr, @@ -743,11 +742,6 @@ pub const NamespaceDecl = struct { is_raw: bool = false, }; -pub const ForeignExpr = struct { - library_ref: ?[]const u8 = null, // identifier name of library constant - c_name: ?[]const u8 = null, // C symbol name override -}; - pub const LibraryDecl = struct { lib_name: []const u8, name: []const u8, // sx-side constant name diff --git a/src/c_import.zig b/src/c_import.zig index b1ec6868..3610b99c 100644 --- a/src/c_import.zig +++ b/src/c_import.zig @@ -259,11 +259,13 @@ pub fn processCImport( else try mapCTypeToSxNode(allocator, ret_str); - // Create foreign_expr body (no library_ref — symbols resolved at runtime) - const foreign_body = try allocator.create(Node); - foreign_body.* = .{ + // Extern-import body: an empty block + `extern_export = .extern_` (no + // LIB / csym — symbols resolve at runtime). Same shape the postfix + // `extern` keyword produces; lowering reads `extern_export`. + const extern_body = try allocator.create(Node); + extern_body.* = .{ .span = .{ .start = 0, .end = 0 }, - .data = .{ .foreign_expr = .{ .library_ref = null, .c_name = null } }, + .data = .{ .block = .{ .stmts = &.{}, .produces_value = false } }, }; const fn_node = try allocator.create(Node); @@ -273,9 +275,10 @@ pub fn processCImport( .name = name, .params = try params.toOwnedSlice(allocator), .return_type = ret_node, - .body = foreign_body, - // A foreign C function whose own NAME collides with a reserved - // type spelling (`int i2(int);`) is RAW — exempt from the + .body = extern_body, + .extern_export = .extern_, + // A C function whose own NAME collides with a reserved type + // spelling (`int i2(int);`) is RAW — exempt from the // reserved-type-name decl check so generated bindings import // without hand-edits. .is_raw = true, @@ -332,20 +335,12 @@ fn checkForeignRefs(valid: *const std.StringHashMap(void), decls: []const *Node, for (decls) |d| { switch (d.data) { .fn_decl => |fd| { - // A library reference rides on the legacy `#foreign` body - // (foreign_expr.library_ref) OR the new `extern LIB` keyword - // (extern_lib); both must name a declared #library / #import c - // unit. The diagnostic names the surface keyword the user wrote - // so the two spellings stay self-describing. - const kw: []const u8, const ref: []const u8 = switch (fd.body.data) { - .foreign_expr => |fe| .{ "#foreign", fe.library_ref orelse continue }, - else => if (fd.extern_export == .extern_) - .{ "extern", fd.extern_lib orelse continue } - else - continue, - }; + // A library reference rides on the `extern LIB` keyword + // (extern_lib); it must name a declared #library / #import c unit. + if (fd.extern_export != .extern_) continue; + const ref = fd.extern_lib orelse continue; if (!valid.contains(ref)) { - diags.addFmt(.err, d.span, "{s} library '{s}' is not declared; expected a #library constant or a named '#import c' unit", .{ kw, ref }); + diags.addFmt(.err, d.span, "extern library '{s}' is not declared; expected a #library constant or a named '#import c' unit", .{ref}); } }, .namespace_decl => |ns| checkForeignRefs(valid, ns.decls, diags), diff --git a/src/ir/lower/call.zig b/src/ir/lower/call.zig index 2a6e1f09..df48a964 100644 --- a/src/ir/lower/call.zig +++ b/src/ir/lower/call.zig @@ -726,7 +726,7 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref { // literal global symbol — the existing bare-name // machinery below resolves them. switch (fd.body.data) { - .foreign_expr, .builtin_expr, .compiler_expr => break :gate, + .builtin_expr, .compiler_expr => break :gate, else => {}, } if (hasComptimeParams(fd)) return self.lowerComptimeCall(fd, c); diff --git a/src/ir/lower/decl.zig b/src/ir/lower/decl.zig index e1f42253..1e1cc4de 100644 --- a/src/ir/lower/decl.zig +++ b/src/ir/lower/decl.zig @@ -391,7 +391,7 @@ pub fn funcWantsImplicitCtx(self: *const Lowering, fd: *const ast.FnDecl) bool { // C ABI, no sx context (Phase 2, gap iv). if (fd.extern_export != .none) return false; return switch (fd.body.data) { - .foreign_expr, .builtin_expr, .compiler_expr => false, + .builtin_expr, .compiler_expr => false, else => !isExportedEntryName(fd.name), }; } @@ -2084,21 +2084,18 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) // Foreign declarations with a trailing variadic param map to the C // calling convention's `...` tail. Drop the variadic param from the // IR signature (it has no C-level slot) and set is_variadic. - const is_foreign = fd.body.data == .foreign_expr; - // Bare `extern` import: an external C symbol declared via the new linkage - // surface (empty-block placeholder body, no `foreign_expr`). It shares - // `#foreign`'s C-ABI promotion + declareExtern routing below; the optional - // `extern LIB "csym"` lib/rename axis (extern_lib/extern_name) is consumed - // in Phase 1.2. (`export` defines take the beginFunction path, not here.) + // Bare `extern` import: an external C symbol declared via the `extern` + // linkage keyword (empty-block placeholder body). C-ABI promotion + + // declareExtern routing below; the optional `extern LIB "csym"` lib/rename + // axis is extern_lib/extern_name. (`export` defines take the beginFunction + // path, not here.) The `#import c` auto-synthesis also produces this shape. const is_extern_decl = fd.extern_export == .extern_; var is_variadic = false; var effective_params = fd.params; - // The C-variadic `...` tail applies to BOTH lib-less C-import spellings: - // the legacy `#foreign` (foreign_expr body) and the new `extern` keyword. - // A migrated variadic `extern` must drop the trailing slice param and set - // the flag exactly as its `#foreign` twin did (mirrored at the call site - // by `packVariadicCallArgs`). - if ((is_foreign or is_extern_decl) and fd.params.len > 0 and fd.params[fd.params.len - 1].is_variadic) { + // A lib-less C-import with a C-variadic `...` tail: drop the trailing slice + // param and set is_variadic (mirrored at the call site by + // `packVariadicCallArgs`). + if (is_extern_decl and fd.params.len > 0 and fd.params[fd.params.len - 1].is_variadic) { is_variadic = true; effective_params = fd.params[0 .. fd.params.len - 1]; } @@ -2119,23 +2116,19 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) }) catch unreachable; } - // `#foreign` declarations are external C symbols by definition — - // promote them to callconv(.c) when the user didn't write it - // explicitly. This keeps fn-ptr coercion type-safe: anything - // typed by name as `(args) -> ret` of a `#foreign` decl can be - // assigned to / passed as a `callconv(.c)` fn-pointer without a - // call-convention mismatch. - const cc: Function.CallingConvention = if (fd.call_conv == .c or is_foreign or is_extern_decl or fd.extern_export == .export_) .c else .default; + // `extern` declarations are external C symbols by definition — promote + // them to callconv(.c) when the user didn't write it explicitly. This keeps + // fn-ptr coercion type-safe: anything typed by name as `(args) -> ret` of an + // `extern` decl can be assigned to / passed as a `callconv(.c)` fn-pointer + // without a call-convention mismatch. + const cc: Function.CallingConvention = if (fd.call_conv == .c or is_extern_decl or fd.extern_export == .export_) .c else .default; - // Symbol-name override: `#foreign … "csym"` (foreign_expr.c_name) or the new - // `extern … "csym"` / `export … "csym"` (fd.extern_name). Declare under the C - // name and map the sx name → C name so call sites resolve to the real symbol. - // For `export` the stub is later promoted to a real definition (the body - // lowers into this C-named function via lazyLowerFunction — Phase 2.2). + // Symbol-name override: `extern … "csym"` / `export … "csym"` (fd.extern_name). + // Declare under the C name and map the sx name → C name so call sites resolve + // to the real symbol. For `export` the stub is later promoted to a real + // definition (the body lowers into this C-named function via lazyLowerFunction). const is_export_decl = fd.extern_export == .export_; - const rename_c_name: ?[]const u8 = if (is_foreign) - fd.body.data.foreign_expr.c_name - else if (is_extern_decl or is_export_decl) + const rename_c_name: ?[]const u8 = if (is_extern_decl or is_export_decl) fd.extern_name else null; @@ -2157,7 +2150,7 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) } const name_id = self.module.types.internString(name); - if ((is_foreign or is_extern_decl) and self.dedupeExternSymbol(fd, name_id, params.items, ret_ty)) return; + if (is_extern_decl and self.dedupeExternSymbol(fd, name_id, params.items, ret_ty)) return; const fid = self.builder.declareExtern(name_id, params.items, ret_ty); const func = self.module.getFunctionMut(fid); func.call_conv = cc; @@ -2204,7 +2197,7 @@ pub fn registerQualifiedFn(self: *Lowering, ns_name: []const u8, fd: *const ast. // Foreign / builtin / #compiler bodies keep their literal name; a // qualified alias has no distinct symbol to resolve to. switch (fd.body.data) { - .foreign_expr, .builtin_expr, .compiler_expr => return, + .builtin_expr, .compiler_expr => return, else => {}, } const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ns_name, short }) catch return; @@ -2251,28 +2244,16 @@ 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 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). + // Extern-C gate: only a lib-less C-import fn_decl is policed; a + // library-bound `extern LIB` decl (resolves via the named library, + // not a module edge) or a non-extern body is unconditionally + // visible. A lib-less `extern` decl routes to `visibleOverEdges` so + // a transitive reference gets the "C function not visible" + // diagnostic, not the generic top-level-name wording (example 1228). const fd = self.program_index.fn_ast_map.get(name) orelse return true; - 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); - }, - } + 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), } @@ -2325,7 +2306,7 @@ pub fn lazyLowerFunction(self: *Lowering, name: []const u8) void { // a fresh ct_module via `evalComptimeString`) emits `.call` against a // FuncId that doesn't exist locally; the interp can't find the // foreign target and silently no-ops instead of dispatching to libc. - if (fd.body.data == .foreign_expr or fd.extern_export == .extern_) { + if (fd.extern_export == .extern_) { if (self.resolveFuncByName(name) == null) { self.declareFunction(fd, name); self.lowered_functions.put(name, {}) catch {}; @@ -2532,7 +2513,7 @@ pub fn lowerFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, i // Check if the function body is a builtin or foreign declaration (no body // needed). `extern` imports are declare-only too (empty placeholder body). - if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr or fd.body.data == .compiler_expr or fd.extern_export == .extern_) { + if (fd.body.data == .builtin_expr or fd.body.data == .compiler_expr or fd.extern_export == .extern_) { // Already declared by scanDecls/declareFunction (which handles #foreign renames) return; } diff --git a/src/ir/lower/generic.zig b/src/ir/lower/generic.zig index cba26cc2..74db0e96 100644 --- a/src/ir/lower/generic.zig +++ b/src/ir/lower/generic.zig @@ -819,7 +819,7 @@ pub fn isPlainFreeFn(fd: *const ast.FnDecl) bool { // fn. `export` DEFINES a real body, so it stays plain-free. if (fd.extern_export == .extern_) return false; return switch (fd.body.data) { - .foreign_expr, .builtin_expr, .compiler_expr => false, + .builtin_expr, .compiler_expr => false, else => true, }; } diff --git a/src/ir/lower/pack.zig b/src/ir/lower/pack.zig index d08c3b3e..5660a633 100644 --- a/src/ir/lower/pack.zig +++ b/src/ir/lower/pack.zig @@ -296,12 +296,12 @@ pub fn lowerVariadicArgs(self: *Lowering, param_name: []const u8, call_args: []c /// Detects variadic params in the function decl, packs remaining args into a typed slice, /// and replaces the args list with [fixed_args..., slice_ref]. pub fn packVariadicCallArgs(self: *Lowering, fd: *const ast.FnDecl, c: *const ast.Call, args: *std.ArrayList(Ref)) void { - // A lib-less C-import variadic — `#foreign` (foreign_expr body) OR the new + // A lib-less C-import variadic via the `extern` keyword (or `#import c` // `extern` keyword — uses the C calling convention's `...` tail: extras are // passed through directly with default argument promotion (handled at the // call site), not packed into an sx slice. Mirrors the `is_variadic` drop // in `declareFunction`. - if ((fd.body.data == .foreign_expr or fd.extern_export == .extern_) and + if ((fd.extern_export == .extern_) and fd.params.len > 0 and fd.params[fd.params.len - 1].is_variadic) { return; diff --git a/src/ir/resolver.zig b/src/ir/resolver.zig index 25fcc2f7..f3f3a355 100644 --- a/src/ir/resolver.zig +++ b/src/ir/resolver.zig @@ -184,7 +184,7 @@ pub fn isPlainFreeFnDecl(fd: *const ast.FnDecl) bool { // body, so it stays plain-free. if (fd.extern_export == .extern_) return false; return switch (fd.body.data) { - .foreign_expr, .builtin_expr, .compiler_expr => false, + .builtin_expr, .compiler_expr => false, else => true, }; } diff --git a/src/ir/semantic_diagnostics.zig b/src/ir/semantic_diagnostics.zig index 2b354937..1c9d2236 100644 --- a/src/ir/semantic_diagnostics.zig +++ b/src/ir/semantic_diagnostics.zig @@ -351,7 +351,6 @@ pub const UnknownTypeChecker = struct { .inferred_type, .builtin_expr, .compiler_expr, - .foreign_expr, .framework_decl, .function_type_expr, .closure_type_expr, diff --git a/src/lsp/server.zig b/src/lsp/server.zig index 49776d53..8dd75fbb 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -1325,7 +1325,7 @@ pub const Server = struct { // Skip functions, types, structs, enums, unions, comptime, foreign, library switch (cd.value.data) { .fn_decl, .type_expr, .struct_decl, .enum_decl, .union_decl, - .comptime_expr, .foreign_expr, .library_decl, + .comptime_expr, .library_decl, => return, else => {}, } diff --git a/src/sema.zig b/src/sema.zig index e23ea8d1..d07e5d88 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -1295,7 +1295,6 @@ pub const Analyzer = struct { .inferred_type, .builtin_expr, .compiler_expr, - .foreign_expr, .library_decl, .framework_decl, .function_type_expr, @@ -1761,7 +1760,6 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node { .inferred_type, .builtin_expr, .compiler_expr, - .foreign_expr, .library_decl, .framework_decl, .function_type_expr,