From 3811311e127924cb79303cb9c943a6562a203643 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 15 Jun 2026 08:06:05 +0300 Subject: [PATCH] =?UTF-8?q?feat(ffi-linkage)!:=20Phase=208.1=20=E2=80=94?= =?UTF-8?q?=20parser=20hard-rejects=20#foreign=20(cutover)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prefix #foreign linkage directive is removed. All four parse sites (const-with-type, data global, fn body, runtime-class prefix) now reject it with a migration message ('#foreign has been removed; use the postfix extern (import) / export (define) linkage keyword instead'); added a span-aware failAt for the runtime-class case (the lookahead consumes the token before the reject decision). Greens the Phase 8.0 xfail 1176. - Deleted obsolete tests: 1174 (#foreign+postfix conflict — unreachable now that #foreign alone is rejected) and 1620 (#foreign nosuchunit lib-ref — superseded by the extern twin 1231). Their assertions tested #foreign-specific behavior. - Removed the GATE A→B unit test + lowerSrcToIr helper (lower.test.zig): it locked #foreign ≡ extern through the migration; with #foreign gone there is nothing to compare. Converted the in-source 'parse void function with foreign body' parser test to the surviving postfix 'extern' spelling (identical resulting AST). - specs.md + readme.md drop #foreign; document extern/export as the sole C-linkage surface. extern_export in parseFnDecl is now const (the fn-body arm that mutated it is gone). Suite green (646 corpus / 444 unit, 0 failed). NOTE: comment-only #foreign in examples + issues/*.md prose + internal foreign_* identifiers remain for Phase 9 (now unblocked: Decision 6 = purge everything). --- ...74-diagnostics-foreign-postfix-conflict.sx | 15 -- .../1620-cimport-foreign-ref-unvalidated.sx | 17 -- ...-diagnostics-foreign-postfix-conflict.exit | 1 - ...iagnostics-foreign-postfix-conflict.stderr | 5 - ...iagnostics-foreign-postfix-conflict.stdout | 1 - .../1620-cimport-foreign-ref-unvalidated.exit | 1 - ...620-cimport-foreign-ref-unvalidated.stderr | 5 - ...620-cimport-foreign-ref-unvalidated.stdout | 1 - readme.md | 14 +- specs.md | 59 +++---- src/ir/lower.test.zig | 164 ------------------ src/parser.zig | 126 +++++--------- 12 files changed, 69 insertions(+), 340 deletions(-) delete mode 100644 examples/1174-diagnostics-foreign-postfix-conflict.sx delete mode 100644 examples/1620-cimport-foreign-ref-unvalidated.sx delete mode 100644 examples/expected/1174-diagnostics-foreign-postfix-conflict.exit delete mode 100644 examples/expected/1174-diagnostics-foreign-postfix-conflict.stderr delete mode 100644 examples/expected/1174-diagnostics-foreign-postfix-conflict.stdout delete mode 100644 examples/expected/1620-cimport-foreign-ref-unvalidated.exit delete mode 100644 examples/expected/1620-cimport-foreign-ref-unvalidated.stderr delete mode 100644 examples/expected/1620-cimport-foreign-ref-unvalidated.stdout diff --git a/examples/1174-diagnostics-foreign-postfix-conflict.sx b/examples/1174-diagnostics-foreign-postfix-conflict.sx deleted file mode 100644 index 41513f6..0000000 --- a/examples/1174-diagnostics-foreign-postfix-conflict.sx +++ /dev/null @@ -1,15 +0,0 @@ -// Phase 4 (FFI-linkage) interplay diagnostic: the prefix `#foreign` linkage -// modifier and the postfix `extern`/`export` keyword are two spellings of the -// same axis — combining them on one aggregate is contradictory (`#foreign` -// means import, `export` means define) or redundant (`#foreign … extern`). -// The parser rejects the combo at the postfix keyword instead of silently -// letting the postfix override `#foreign` (which previously surfaced as a -// confusing internal "compiler bug" diagnostic during class synthesis). -// -// Expected: one error caret on the `export` keyword; exit 1. - -C :: #foreign #objc_class("NSObject") export { - alloc :: () -> *C; -} - -main :: () -> i32 { 0 } diff --git a/examples/1620-cimport-foreign-ref-unvalidated.sx b/examples/1620-cimport-foreign-ref-unvalidated.sx deleted file mode 100644 index d275c6e..0000000 --- a/examples/1620-cimport-foreign-ref-unvalidated.sx +++ /dev/null @@ -1,17 +0,0 @@ -// A `#foreign` ref must name something real (PLAN-C C3.1): `nosuchunit` -// names neither a #library constant nor a named `#import c` unit, so -// this is a compile-time diagnostic — a typo'd ref previously compiled -// and resolved silently through whatever image carried the symbol. -// Regression (PLAN-C C0.2b xfail, flipped by C3.1). -#import "modules/std.sx"; - -refs :: #import c { - #source "1620-cimport-foreign-ref-unvalidated/ref.c"; -}; - -ref_answer :: () -> i32 #foreign nosuchunit "ref_answer"; - -main :: () -> i32 { - print("ref = {}\n", ref_answer()); - 0 -} diff --git a/examples/expected/1174-diagnostics-foreign-postfix-conflict.exit b/examples/expected/1174-diagnostics-foreign-postfix-conflict.exit deleted file mode 100644 index d00491f..0000000 --- a/examples/expected/1174-diagnostics-foreign-postfix-conflict.exit +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/examples/expected/1174-diagnostics-foreign-postfix-conflict.stderr b/examples/expected/1174-diagnostics-foreign-postfix-conflict.stderr deleted file mode 100644 index 2171764..0000000 --- a/examples/expected/1174-diagnostics-foreign-postfix-conflict.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: conflicting linkage: prefix '#foreign' cannot be combined with postfix 'export'; use either '#foreign' or postfix 'extern'/'export', not both - --> examples/1174-diagnostics-foreign-postfix-conflict.sx:11:39 - | -11 | C :: #foreign #objc_class("NSObject") export { - | ^^^^^^ diff --git a/examples/expected/1174-diagnostics-foreign-postfix-conflict.stdout b/examples/expected/1174-diagnostics-foreign-postfix-conflict.stdout deleted file mode 100644 index 8b13789..0000000 --- a/examples/expected/1174-diagnostics-foreign-postfix-conflict.stdout +++ /dev/null @@ -1 +0,0 @@ - diff --git a/examples/expected/1620-cimport-foreign-ref-unvalidated.exit b/examples/expected/1620-cimport-foreign-ref-unvalidated.exit deleted file mode 100644 index d00491f..0000000 --- a/examples/expected/1620-cimport-foreign-ref-unvalidated.exit +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/examples/expected/1620-cimport-foreign-ref-unvalidated.stderr b/examples/expected/1620-cimport-foreign-ref-unvalidated.stderr deleted file mode 100644 index 9d33d57..0000000 --- a/examples/expected/1620-cimport-foreign-ref-unvalidated.stderr +++ /dev/null @@ -1,5 +0,0 @@ -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/examples/expected/1620-cimport-foreign-ref-unvalidated.stdout b/examples/expected/1620-cimport-foreign-ref-unvalidated.stdout deleted file mode 100644 index 8b13789..0000000 --- a/examples/expected/1620-cimport-foreign-ref-unvalidated.stdout +++ /dev/null @@ -1 +0,0 @@ - diff --git a/readme.md b/readme.md index 4220f6e..4286632 100644 --- a/readme.md +++ b/readme.md @@ -29,7 +29,7 @@ main :: () { - First-class closures with value capture - Protocol-based polymorphism (traits) - Pattern matching on enums, optionals, and type categories -- C interop via `#foreign` and `#import c` +- C interop via `extern` / `export` and `#import c` - Targets: macOS (ARM64, x86_64), Linux (x86_64, ARM64), Windows (x86_64), WebAssembly ## Building @@ -402,20 +402,20 @@ FIBONACCI_10 :: #run fib(10); ### C Interop -Foreign functions: +C linkage: ```sx libc :: #library "c"; -printf :: (fmt: [:0]u8, args: ..Any) -> i32 #foreign libc; -write_fd :: (fd: i32, buf: [*]u8, count: u64) -> i64 #foreign libc "write"; +printf :: (fmt: [:0]u8, args: ..Any) -> i32 extern libc; +write_fd :: (fd: i32, buf: [*]u8, count: u64) -> i64 extern libc "write"; ``` -`extern` / `export` are the keyword surface for C linkage. `extern` is the modern -spelling of `#foreign` (import); `export` is its dual — define a function in sx and +`extern` / `export` are the keyword surface for C linkage. `extern` imports a +symbol defined elsewhere; `export` is its dual — define a function in sx and expose it under the C ABI so C can call back in. Both imply `callconv(.c)` and take the same optional `[LIB] ["csym"]` rename tail; they also apply to data globals and to Obj-C / JNI runtime-class aggregates (postfix after the `#objc_class(…)` directive). ```sx -abs :: (x: i32) -> i32 extern; // import (== `#foreign`) +abs :: (x: i32) -> i32 extern; // import an external C symbol sx_square :: (x: i32) -> i32 export { x * x } // define + expose to C __stdinp : *void extern; // extern data global NSObject :: #objc_class("NSObject") extern { alloc :: () -> *NSObject; } // reference a runtime class diff --git a/specs.md b/specs.md index 359b8a1..51adf0f 100644 --- a/specs.md +++ b/specs.md @@ -1055,7 +1055,7 @@ the chain leaves the checked zone. `cstring` is the C-boundary string: ONE pointer to a null-terminated u8 buffer — exactly C's `char *`. It is thin (8 bytes, no length field; -`cstring_len` walks to the terminator, O(n)) and crosses `#foreign` +`cstring_len` walks to the terminator, O(n)) and crosses `extern` boundaries verbatim in BOTH directions. `?cstring` is the nullable case and lowers to the same bare pointer (null = absent) — the natural type for `getenv`-style returns and optional `char *` parameters. @@ -1190,58 +1190,45 @@ Optionals work in `#run` blocks — `??`, `!`, `if val :=`, null checks all supp ### Foreign Function Interface (C Interop) -To call C functions, declare a library constant with `#library` and bind functions with `#foreign`: +C linkage is expressed with the postfix `extern` (import) and `export` (define + +expose) keywords. `extern` declares a symbol defined elsewhere — a C function or +data global resolved at link time; `export` is its dual — **define** a symbol in +sx and expose it under the C ABI so C (or asm, or another language) can call it. +Both imply `callconv(.c)`, carry external linkage, and suppress the implicit sx +context parameter. They are postfix modifiers, written where `callconv` would go. ```sx // Declare a named library constant libc :: #library "c"; sdl :: #library "SDL3"; -// Bind foreign functions — library ref is required -socket :: (domain: i32, type: i32, protocol: i32) -> i32 #foreign libc; -SDL_Init :: (flags: u32) -> bool #foreign sdl; - -// Symbol renaming — optional second argument gives the C symbol name -write_fd :: (fd: i32, buf: [*]u8, count: u64) -> i64 #foreign libc "write"; -``` - -- `#library "name"` must be assigned to a named constant. The library is passed to the linker (`-lname` on Unix, `name.lib` on Windows). -- `#foreign lib_ref` declares a function as external C. The library reference is optional: when present it is passed to the linker (`-lname` on Unix); when omitted (`name :: (…) -> T #foreign;`), the symbol must resolve at link time from a framework or an already-linked / auto-detected library. -- `#foreign lib_ref "c_symbol"` renames the binding: the sx function name differs from the C symbol. This avoids name collisions (e.g. POSIX `write` vs an sx builtin). - -#### `extern` / `export` linkage keywords - -`extern` and `export` are the keyword surface for C linkage. `extern` is the -modern spelling of `#foreign` (import a symbol defined elsewhere); `export` is -its dual — **define** a symbol in sx and expose it under the C ABI so C (or asm, -or another language) can call it. Both imply `callconv(.c)`, carry external -linkage, and suppress the implicit sx context parameter. They are postfix -modifiers, written where `callconv` would go. - -```sx // Functions — `extern` imports, `export` defines + exposes -abs :: (x: i32) -> i32 extern; // import (== `#foreign`) -write_fd :: (fd: i32, buf: [*]u8, n: u64) -> i64 extern libc "write"; // [LIB] ["csym"] +socket :: (domain: i32, type: i32, protocol: i32) -> i32 extern libc; +SDL_Init :: (flags: u32) -> bool extern sdl; +abs :: (x: i32) -> i32 extern; // no LIB: resolves from a framework / auto-linked lib +write_fd :: (fd: i32, buf: [*]u8, n: u64) -> i64 extern libc "write"; // [LIB] ["csym"] rename sx_square :: (x: i32) -> i32 export { x * x } // define; C can call `sx_square` triple_c :: (x: i32) -> i32 export "triple_c" { x * 3 } // export under a C name // Data globals — `extern` imports an external global -__stdinp : *void extern; // (== ` : #foreign;`) +__stdinp : *void extern; // Aggregates (Obj-C / JNI runtime classes) — postfix after the directive NSObject :: #objc_class("NSObject") extern { alloc :: () -> *NSObject; } // reference SxFoo :: #objc_class("SxFoo") export { counter: i32; bump :: (self: *Self) { … } } // define ``` -- `extern` takes the same optional `[LIB] ["csym"]` tail as `#foreign` - (`extern libc "write"`): a `#library` alias reference then a C symbol rename. - The `#library` declaration + build-flag linking mechanism is a separate axis — - `extern` *references* a library, it does not declare one. -- `export "csym"` renames the exported symbol the same way (the C-visible name - differs from the sx name). -- On an aggregate, the prefix `#foreign` modifier and a postfix `extern`/`export` - keyword are the same axis and cannot be combined: `#objc_class("X") extern` is - exactly `#foreign #objc_class("X")`; writing both is a compile error. +- `#library "name"` must be assigned to a named constant. The library is passed + to the linker (`-lname` on Unix, `name.lib` on Windows). +- `extern lib_ref` declares a function (or ` : extern;` a data + global) as an external C symbol. The library reference is optional: when present + it is passed to the linker (`-lname` on Unix); when omitted, the symbol must + resolve at link time from a framework or an already-linked / auto-detected + library. The `#library` declaration + build-flag linking mechanism is a separate + axis — `extern` *references* a library, it does not declare one. +- `extern lib_ref "c_symbol"` (and `export "c_symbol"`) renames the binding: the + sx name differs from the C symbol. This avoids name collisions (e.g. POSIX + `write` vs an sx builtin) and gives an export a stable C-visible name. ### C Interop Type Mapping diff --git a/src/ir/lower.test.zig b/src/ir/lower.test.zig index 220f8c4..b2dacfe 100644 --- a/src/ir/lower.test.zig +++ b/src/ir/lower.test.zig @@ -1575,167 +1575,3 @@ test "lower: scan populates source-keyed caches per declaring source (E0)" { try std.testing.expect(global_k.value == k_a.value or global_k.value == k_b.value); } -// ── GATE A→B: `extern`/`export` are a behavior-equivalent superset of `#foreign` ── -// -// The FFI-linkage stream replaces the prefix `#foreign` linkage modifier with the -// postfix `extern` (import) / `export` (define) keywords. Part B may not begin -// migrating `#foreign` call sites until this is locked: a sample fn / global / -// runtime-class written with `#foreign` must lower to byte-identical IR as the -// same decl written with `extern`. This test is the gate — keep it green. -// -// Phase 5.0 POST-FLIP NOTE: the fn-decl and data-global `#foreign` parser paths now -// build the *same* extern-named AST that postfix `extern` produces (commits e5ddfbe -// global, 6b94bb6 fn-body), so cases 1/2/4 below are STRUCTURALLY identical at the -// AST level — equivalence is guaranteed by construction, not coincidence. The test -// stays as a regression tripwire: if a future change re-diverges the two spellings -// (a reader that branches on `foreign_expr` structurally, or a revert of the flip), -// this gate catches it. Case 3 (runtime class) was always coalesced onto the single -// `is_foreign_eff` field, so it is behaviorally — not structurally — equal. - -/// Lower a single self-contained `.sx` source (no stdlib imports) all the way to -/// the printed module IR text. Mirrors the full pipeline in the fix-0102b tests -/// above (parse → resolveImports → lowerRoot → printModule). Arena-allocated; -/// the caller's arena owns everything. -fn lowerSrcToIr(alloc: std.mem.Allocator, io: std.Io, src: []const u8) ![]const u8 { - var tmp = std.testing.tmpDir(.{}); - defer tmp.cleanup(); - - try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = src }); - - var dirbuf: [4096]u8 = undefined; - const dirlen = try tmp.dir.realPath(io, &dirbuf); - const absdir = dirbuf[0..dirlen]; - - const main_path = try std.fmt.allocPrint(alloc, "{s}/main.sx", .{absdir}); - const main_source = try alloc.dupeZ(u8, src); - var p = parser.Parser.init(alloc, main_source); - const root = p.parse() catch return error.ParseFailed; - - var chain = std.StringHashMap(void).init(alloc); - var cache = imports.ModuleCache.init(alloc); - var import_graph = std.StringHashMap(std.StringHashMap(void)).init(alloc); - var flat_import_graph = std.StringHashMap(std.StringHashMap(void)).init(alloc); - const stdlib_paths = [_][]const u8{}; - - const mod = try imports.resolveImports( - alloc, - io, - root, - absdir, - main_path, - &chain, - &cache, - null, - null, - &stdlib_paths, - &import_graph, - &flat_import_graph, - .{}, - ); - - var module_scopes = std.StringHashMap(std.StringHashMap(void)).init(alloc); - try module_scopes.put(main_path, mod.scope); - var cache_it = cache.iterator(); - while (cache_it.next()) |entry| { - try module_scopes.put(entry.key_ptr.*, entry.value_ptr.scope); - } - var facts = try imports.buildImportFacts(alloc, main_path, mod, &cache); - - const resolved_root = try alloc.create(Node); - resolved_root.* = .{ .span = root.span, .data = .{ .root = .{ .decls = mod.decls } } }; - - var module = ir_mod.Module.init(alloc); - // NOTE: no `defer module.deinit()` — the printed IR is duped into the arena - // before return, and the arena owns the rest; deiniting here is unnecessary. - var diagnostics = errors.DiagnosticList.init(alloc, main_source, main_path); - var lowering = Lowering.init(&module); - lowering.main_file = main_path; - lowering.resolved_root = resolved_root; - lowering.diagnostics = &diagnostics; - lowering.program_index.module_scopes = &module_scopes; - lowering.program_index.import_graph = &import_graph; - lowering.program_index.flat_import_graph = &flat_import_graph; - lowering.program_index.module_decls = &facts.decls; - - lowering.lowerRoot(resolved_root); - if (diagnostics.hasErrors()) return error.LoweringFailed; - - const print_mod = @import("print.zig"); - var aw = std.Io.Writer.Allocating.init(alloc); - try print_mod.printModule(&module, &aw.writer); - const result = aw.writer.toArrayList(); - return try alloc.dupe(u8, result.items); -} - -test "lower: GATE A→B — #foreign and extern/export lower to identical IR" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const alloc = arena.allocator(); - const io = lowerTestIo(); - - // 1. FUNCTION — bare libc-style import. - { - const foreign_ir = try lowerSrcToIr(alloc, io, - \\f :: (a: i32) -> i32 #foreign; - \\main :: () -> i32 { f(7) } - \\ - ); - const extern_ir = try lowerSrcToIr(alloc, io, - \\f :: (a: i32) -> i32 extern; - \\main :: () -> i32 { f(7) } - \\ - ); - try std.testing.expectEqualStrings(foreign_ir, extern_ir); - } - - // 2. DATA GLOBAL — extern global. - { - const foreign_ir = try lowerSrcToIr(alloc, io, - \\g : *void #foreign; - \\main :: () -> i64 { xx g } - \\ - ); - const extern_ir = try lowerSrcToIr(alloc, io, - \\g : *void extern; - \\main :: () -> i64 { xx g } - \\ - ); - try std.testing.expectEqualStrings(foreign_ir, extern_ir); - } - - // 2b. FUNCTION RENAME — sx name bound to a different C symbol (`extern_name` axis). - { - const foreign_ir = try lowerSrcToIr(alloc, io, - \\c_abs :: (a: i32) -> i32 #foreign "abs"; - \\main :: () -> i32 { c_abs(-7) } - \\ - ); - const extern_ir = try lowerSrcToIr(alloc, io, - \\c_abs :: (a: i32) -> i32 extern "abs"; - \\main :: () -> i32 { c_abs(-7) } - \\ - ); - try std.testing.expectEqualStrings(foreign_ir, extern_ir); - } - - // 3. RUNTIME CLASS — Obj-C reference (import) class with dispatch. - { - const foreign_ir = try lowerSrcToIr(alloc, io, - \\C :: #foreign #objc_class("NSObject") { - \\ alloc :: () -> *C; - \\ init :: (self: *Self) -> *Self; - \\} - \\main :: () -> i32 { a := C.alloc().init(); if a != null { return 1; } 0 } - \\ - ); - const extern_ir = try lowerSrcToIr(alloc, io, - \\C :: #objc_class("NSObject") extern { - \\ alloc :: () -> *C; - \\ init :: (self: *Self) -> *Self; - \\} - \\main :: () -> i32 { a := C.alloc().init(); if a != null { return 1; } 0 } - \\ - ); - try std.testing.expectEqualStrings(foreign_ir, extern_ir); - } -} diff --git a/src/parser.zig b/src/parser.zig index 88bfdb6..b2e158b 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -257,7 +257,15 @@ pub const Parser = struct { // Define-by-default: bare `#jni_class("...")` declares a new class (sx-defined). // `#foreign` flips that to "reference an existing class on the foreign side." // `#jni_main` flags the class as the launchable entry (Android Activity). + const prefix_loc = self.current.loc; if (self.tryParseForeignClassPrefix()) |prefix| { + // Phase 8 cutover: the prefix `#foreign` on a runtime-class directive is + // removed — reference an existing class via the postfix `extern` modifier + // (`X :: #objc_class("…") extern { … }`) instead. `prefix_loc` pins the + // diagnostic to the `#foreign` token (already consumed by the lookahead). + if (prefix.is_foreign) { + return self.failAt(prefix_loc, "`#foreign` has been removed; use the postfix `extern` (import) / `export` (define) linkage keyword instead"); + } return self.parseForeignClassDecl(name, start_pos, prefix.runtime, prefix.is_foreign, prefix.is_main, name_is_raw); } @@ -312,30 +320,11 @@ pub const Parser = struct { return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = value, .value = bi, .name_span = name_span, .is_raw = name_is_raw } }); } - // name :: type_expr #foreign [lib] ["c_name"]; — foreign with type annotation + // Phase 8 cutover: the prefix `#foreign` linkage directive has been + // removed — reject it with a migration message (was: `name :: type + // #foreign [lib] ["c_name"];`, now `name :: type extern [lib] ["c_name"];`). if (self.current.tag == .hash_foreign) { - const fi_start = self.current.loc.start; - self.advance(); - // Optional: library reference (identifier). Omitted when the symbol - // resolves at link time from a framework or auto-detected library. - var lib_ref: ?[]const u8 = null; - if (self.current.tag == .identifier) { - lib_ref = self.tokenSlice(self.current); - self.advance(); - } - // Optional: C symbol name (string literal) - var c_name: ?[]const u8 = null; - if (self.current.tag == .string_literal) { - const raw = self.tokenSlice(self.current); - c_name = raw[1 .. raw.len - 1]; - self.advance(); - } - try self.expect(.semicolon); - const fi = try self.createNode(fi_start, .{ .foreign_expr = .{ - .library_ref = lib_ref, - .c_name = c_name, - } }); - return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = value, .value = fi, .name_span = name_span, .is_raw = name_is_raw } }); + return self.fail("`#foreign` has been removed; use the postfix `extern` (import) / `export` (define) linkage keyword instead"); } try self.expect(.semicolon); @@ -422,35 +411,11 @@ pub const Parser = struct { return try self.createNode(start_pos, .{ .var_decl = .{ .name = name, .name_span = name_span, .type_annotation = type_node, .value = null, .is_raw = name_is_raw } }); } + // Phase 8 cutover: prefix `#foreign` on a data global is removed — + // reject it (was `name : type #foreign [lib] ["c_name"];`, now + // `name : type extern [lib] ["c_name"];`, handled by the `kw_extern` arm). if (self.current.tag == .hash_foreign) { - // name : type #foreign [lib] ["c_name"]; (extern global from libsystem etc.) - // Phase 5.0: `#foreign` is an alias for `extern`. Build the SAME - // extern-named AST the postfix `extern` global path builds below — - // lowering coalesces is_foreign/is_extern identically (decl.zig:1127, - // 1141), so this is behavior-preserving (snapshots unchanged). - self.advance(); - var lib_ref: ?[]const u8 = null; - if (self.current.tag == .identifier) { - lib_ref = self.tokenSlice(self.current); - self.advance(); - } - var c_name: ?[]const u8 = null; - if (self.current.tag == .string_literal) { - const raw = self.tokenSlice(self.current); - c_name = raw[1 .. raw.len - 1]; - self.advance(); - } - try self.expect(.semicolon); - return try self.createNode(start_pos, .{ .var_decl = .{ - .name = name, - .name_span = name_span, - .type_annotation = type_node, - .value = null, - .is_extern = true, - .extern_lib = lib_ref, - .extern_name = c_name, - .is_raw = name_is_raw, - } }); + return self.fail("`#foreign` has been removed; use the postfix `extern` (import) / `export` (define) linkage keyword instead"); } if (self.current.tag == .kw_extern) { @@ -2004,9 +1969,7 @@ pub const Parser = struct { const call_conv = try self.parseOptionalCallConv(); // Optional postfix linkage modifier: `extern` (import) / `export` (define). - // `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(); + const 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 @@ -2062,34 +2025,11 @@ pub const Parser = struct { const ci_start = self.current.loc.start; 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(); - // Optional: library reference (identifier). - if (self.current.tag == .identifier) { - extern_lib = self.tokenSlice(self.current); - self.advance(); - } - // Optional: C symbol name (string literal). - if (self.current.tag == .string_literal) { - const raw = self.tokenSlice(self.current); - extern_name = raw[1 .. raw.len - 1]; - self.advance(); - } - try self.expect(.semicolon); - 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 == .hash_foreign) { + // Phase 8 cutover: the fn-body `#foreign` marker is removed — reject it. + // The import form is now the postfix `extern` keyword handled above + // (`f :: (…) -> R extern [LIB] ["csym"];`). + return self.fail("`#foreign` has been removed; use the postfix `extern` (import) / `export` (define) linkage keyword instead"); } else if (self.current.tag == .fat_arrow) blk: { is_arrow = true; self.advance(); @@ -4083,6 +4023,18 @@ pub const Parser = struct { } return error.ParseError; } + + /// Like `fail`, but pins the diagnostic to an explicit source span rather + /// than `self.current` — used when the offending token has already been + /// consumed (e.g. a lookahead committed past it before the reject decision). + fn failAt(self: *Parser, loc: anytype, msg: []const u8) error{ParseError} { + self.err_msg = msg; + self.err_offset = loc.start; + if (self.diagnostics) |diags| { + diags.add(.err, msg, .{ .start = loc.start, .end = loc.end }); + } + return error.ParseError; + } }; test "parse minimal main" { @@ -4261,11 +4213,11 @@ test "parse void function with builtin body" { try std.testing.expect(decl.data.fn_decl.body.data == .builtin_expr); } -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;"; +test "parse void function with extern import" { + // A postfix `extern LIB` fn import builds an empty-block body + + // extern_export = .extern_ + extern_lib. (Phase 8 removed the legacy + // prefix `#foreign` spelling that used to produce this same shape.) + const source = "InitWindow :: (width: i32, height: i32, title: *u8) -> void extern rl;"; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); var parser = Parser.init(arena.allocator(), source);