From 6b94bb6bba857dc09cfb7a5bc7b425e7f0626655 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 15 Jun 2026 04:03:51 +0300 Subject: [PATCH] =?UTF-8?q?refactor(ffi-linkage):=20Phase=205.0=20?= =?UTF-8?q?=E2=80=94=20flip=20fn-decl=20#foreign=20body=20marker=20onto=20?= =?UTF-8?q?the=20extern=20AST?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...620-cimport-foreign-ref-unvalidated.stderr | 2 +- src/parser.zig | 39 ++++++++++++------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/examples/expected/1620-cimport-foreign-ref-unvalidated.stderr b/examples/expected/1620-cimport-foreign-ref-unvalidated.stderr index 485cb2e..9d33d57 100644 --- a/examples/expected/1620-cimport-foreign-ref-unvalidated.stderr +++ b/examples/expected/1620-cimport-foreign-ref-unvalidated.stderr @@ -1,4 +1,4 @@ -error: #foreign library 'nosuchunit' is not declared; expected a #library constant or a named '#import c' unit +error: extern library 'nosuchunit' is not declared; expected a #library constant or a named '#import c' unit --> examples/1620-cimport-foreign-ref-unvalidated.sx:12:1 | 12 | ref_answer :: () -> i32 #foreign nosuchunit "ref_answer"; diff --git a/src/parser.zig b/src/parser.zig index 52b66bd..88bfdb6 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -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); }