feat(ffi-linkage)!: Phase 8.1 — parser hard-rejects #foreign (cutover)
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).
This commit is contained in:
126
src/parser.zig
126
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);
|
||||
|
||||
Reference in New Issue
Block a user