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:
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
1
|
||||
@@ -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 {
|
||||
| ^^^^^^
|
||||
@@ -1 +0,0 @@
|
||||
1
|
||||
@@ -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";
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
14
readme.md
14
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
|
||||
|
||||
59
specs.md
59
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; // (== `<name> : <type> #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 `<name> : <type> 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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
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