refactor(ffi-linkage): Phase 5.0 — flip fn-decl #foreign body marker onto the extern AST

The fn-body `#foreign [LIB] ["csym"]` marker now builds the SAME shape postfix
`extern` produces — extern_export = .extern_ + extern_lib/extern_name + an
empty-block body — instead of a `foreign_expr` body. With all four prereqs
landed (visibility, variadic, plain-free classification, lib-ref validation),
every downstream reader coalesces is_foreign with extern_export, so the IR and
runtime behavior are byte-identical (full corpus + the A->B gate stay green).

The surface keyword is no longer on the AST, so a `#foreign`-spelled decl now
yields `extern`-worded diagnostics — the single accepted churn (Decision 7):
example 1620's lib-ref error flips '#foreign library' -> 'extern library'.
Parser-surface diagnostics (conflict/expected-token) fire on the literal keyword
and are unaffected. c_import auto-synthesis still emits foreign_expr bodies (not
this step), so both shapes still coexist. Parser unit test updated to assert the
extern shape.

647 corpus / 444 unit, 0 failed. The const-with-type (dead) + runtime-class
(already coalesced) paths need no flip — Phase 5.0 parser routing is complete.
This commit is contained in:
agra
2026-06-15 04:03:51 +03:00
parent 4dca38881e
commit 6b94bb6bba
2 changed files with 27 additions and 14 deletions

View File

@@ -2004,7 +2004,9 @@ pub const Parser = struct {
const call_conv = try self.parseOptionalCallConv();
// Optional postfix linkage modifier: `extern` (import) / `export` (define).
const extern_export = self.parseOptionalExternExport();
// `var` because the fn-body `#foreign` marker (an alias for postfix
// `extern`) routes onto `.extern_` from inside the body chain below.
var extern_export = self.parseOptionalExternExport();
// `extern` and `export` are mutually exclusive — one declaration is either
// an import or a definition, never both. Reject the redundant second keyword
@@ -2061,27 +2063,33 @@ pub const Parser = struct {
self.advance();
break :blk try self.createNode(ci_start, .{ .compiler_expr = {} });
} else if (self.current.tag == .hash_foreign) blk: {
// Phase 5.0 (Part B): the fn-body `#foreign` marker is an alias for
// postfix `extern` — build the SAME extern shape (empty-block body +
// `extern_export = .extern_` carrying the optional `[LIB] ["csym"]`
// on extern_lib/extern_name) the `extern` keyword produces above, so
// every downstream reader sees ONE form. Lowering + all gates
// coalesce `is_foreign` with `extern_export` (decl.zig declareFunction
// / visibility, pack.zig variadic, resolver/generic plain-free,
// c_import lib-ref). The surface keyword is gone from the AST here, so
// a `#foreign`-spelled decl now yields `extern`-worded diagnostics
// (Decision 7 — accepted interim churn until the Phase 8 cutover).
const fi_start = self.current.loc.start;
self.advance();
// Required: library reference (identifier)
// Optional: library reference (identifier).
var lib_ref: ?[]const u8 = null;
if (self.current.tag == .identifier) {
lib_ref = self.tokenSlice(self.current);
extern_lib = self.tokenSlice(self.current);
self.advance();
}
// Optional: C symbol name (string literal)
var c_name: ?[]const u8 = null;
// Optional: C symbol name (string literal).
if (self.current.tag == .string_literal) {
const raw = self.tokenSlice(self.current);
c_name = raw[1 .. raw.len - 1];
extern_name = raw[1 .. raw.len - 1];
self.advance();
}
try self.expect(.semicolon);
break :blk try self.createNode(fi_start, .{ .foreign_expr = .{
.library_ref = lib_ref,
.c_name = c_name,
} });
extern_export = .extern_;
const stmts = try self.allocator.alloc(*Node, 0);
break :blk try self.createNode(fi_start, .{ .block = .{ .stmts = stmts, .produces_value = false } });
} else if (self.current.tag == .fat_arrow) blk: {
is_arrow = true;
self.advance();
@@ -4254,6 +4262,9 @@ test "parse void function with builtin body" {
}
test "parse void function with foreign body" {
// Phase 5.0 (Part B): the fn-body `#foreign` marker now builds the unified
// `extern` shape (extern_export = .extern_ + extern_lib, empty-block body),
// NOT a `foreign_expr` body — an alias for postfix `extern LIB`.
const source = "InitWindow :: (width: i32, height: i32, title: *u8) -> void #foreign rl;";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
@@ -4263,8 +4274,10 @@ test "parse void function with foreign body" {
const decl = root.data.root.decls[0];
try std.testing.expect(decl.data == .fn_decl);
try std.testing.expectEqualStrings("InitWindow", decl.data.fn_decl.name);
try std.testing.expect(decl.data.fn_decl.body.data == .foreign_expr);
try std.testing.expectEqualStrings("rl", decl.data.fn_decl.body.data.foreign_expr.library_ref.?);
try std.testing.expectEqual(ast.ExternExportModifier.extern_, decl.data.fn_decl.extern_export);
try std.testing.expectEqualStrings("rl", decl.data.fn_decl.extern_lib.?);
try std.testing.expect(decl.data.fn_decl.body.data == .block);
try std.testing.expectEqual(@as(usize, 0), decl.data.fn_decl.body.data.block.stmts.len);
try std.testing.expectEqual(@as(usize, 3), decl.data.fn_decl.params.len);
}