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

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

View File

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

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,

View File

@@ -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 => {},
}

View File

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