refactor(ffi-linkage): Phase 9.1d — eliminate the foreign_expr AST node

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).
This commit is contained in:
agra
2026-06-15 08:54:56 +03:00
parent 98264b8640
commit 7ffdc7d2a2
10 changed files with 55 additions and 88 deletions

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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,
};
}

View File

@@ -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;

View File

@@ -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,
};
}

View File

@@ -351,7 +351,6 @@ pub const UnknownTypeChecker = struct {
.inferred_type,
.builtin_expr,
.compiler_expr,
.foreign_expr,
.framework_decl,
.function_type_expr,
.closure_type_expr,