feat(ffi-linkage): extern/export parser+AST plumbing, unconsumed (Phase 0.1)
Add ast.ExternExportModifier { none, extern_, export_ } beside
CallingConvention; FnDecl.extern_export and VarDecl.is_extern/extern_name
fields (all defaulting to absent); and Parser.parseOptionalExternExport()
mirroring parseOptionalCallConv.
None of this is consumed by a decl path yet — no user-facing behavior
change, corpus diff empty. Two inline parser unit tests pin the helper's
keyword mapping and the field defaults. Phase 1.0 wires the helper into
the fn-decl path. lock commit.
This commit is contained in:
@@ -5,13 +5,14 @@ Companion to `current/PLAN-EXTERN-EXPORT.md` — one merged plan: **Part A** add
|
|||||||
every commit, one step at a time per the cadence rule.
|
every commit, one step at a time per the cadence rule.
|
||||||
|
|
||||||
## Last completed step
|
## Last completed step
|
||||||
**Phase 0.0** (lock) — added `kw_extern`/`kw_export` to `token.Tag` + the
|
**Phase 0.1** (lock) — added `ast.ExternExportModifier = enum { none, extern_,
|
||||||
`keywords` `StaticStringMap` (beside `kw_callconv`, `token.zig:45,282`); classified
|
export_ }` (beside `CallingConvention`), `FnDecl.extern_export` (default `.none`),
|
||||||
both as `ST.keyword` in the LSP `classifyToken` exhaustive switch
|
`VarDecl.is_extern`/`extern_name` (defaults absent), and
|
||||||
(`lsp/server.zig`); added `test "lex linkage keywords"` in `lexer.zig`. Suite green
|
`parser.parseOptionalExternExport()` (mirrors `parseOptionalCallConv`, just below it
|
||||||
(441 unit / 633 corpus, 0 fail). `extern`/`export` are now reserved words; corpus
|
at `parser.zig:~3683`). **Helper + fields defined but NOT consumed by any decl path**
|
||||||
sweep confirmed no `.sx` identifier used either (only comments + the unpinned
|
— no user-facing behavior change, corpus diff empty. Two inline parser unit tests
|
||||||
`issues/0030-*` repro, which doesn't run in the suite).
|
(`parseOptionalExternExport recognizes linkage keywords`, `extern/export AST fields
|
||||||
|
default to absent`). Suite green (443 unit [+2] / 633 corpus, 0 fail).
|
||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
Syntax decided + ratified: bare `extern`/`export`, postfix in the `callconv(.c)`
|
Syntax decided + ratified: bare `extern`/`export`, postfix in the `callconv(.c)`
|
||||||
@@ -20,15 +21,17 @@ slot, `extern ⇒ callconv(.c)`, library separate. Touch-points mapped — token
|
|||||||
`decl.zig:1123,387,2110,2382,2514`; IR/emit already capable (no codegen change).
|
`decl.zig:1123,387,2110,2382,2514`; IR/emit already capable (no codegen change).
|
||||||
Export gap = 4 lowering conditions. Part B `foreign` footprint to purge: 643 lines /
|
Export gap = 4 lowering conditions. Part B `foreign` footprint to purge: 643 lines /
|
||||||
~57 identifiers in `src/` + 28 doc lines. End-state invariant: **zero `foreign`** in
|
~57 identifiers in `src/` + 28 doc lines. End-state invariant: **zero `foreign`** in
|
||||||
the live tree (Phase 9.4 gate). Tokens exist (0.0); parser/AST not yet plumbed.
|
the live tree (Phase 9.4 gate). **Phase 0 done**: tokens (0.0) + AST/parser plumbing
|
||||||
|
(0.1) exist, unconsumed. Phase 1 wires `extern` into the fn/global decl paths.
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
**Phase 0.1** (lock) — `parseOptionalExternExport()` (mirror `parseOptionalCallConv`,
|
**Phase 1.0** (xfail) — accept postfix `extern` after the callconv slot in the
|
||||||
`parser.zig:3669`) + `ast.ExternExportModifier` enum + `FnDecl.extern_export` +
|
fn-decl path (`parser.zig:1950`: call `parseOptionalExternExport()`, store on
|
||||||
`VarDecl.is_extern`/`extern_name` fields; **parsed but not yet consumed**; unit AST
|
`FnDecl.extern_export`); add `examples/12xx-ffi-extern-fn.sx` that extern-binds a libc
|
||||||
test. Then Phase 1 (`extern` working: 1.0 xfail accept postfix, 1.1 green lower via
|
symbol — **red** (parses, but lowering not wired yet). Then 1.1 green (lower via
|
||||||
`declareExtern`, 1.2 green rename + extern-global). See the plan's **"Kickoff
|
`declareExtern`: `is_extern`, `.external`, `callconv(.c)`, no ctx — anchors
|
||||||
prompt"**. Stop at end of Phase 1.
|
`decl.zig:1123,387,2110,2113`), 1.2 green (`extern "csym"` rename + extern-global
|
||||||
|
`g : T extern;`, `parser.zig:425`). Stop at end of Phase 1.
|
||||||
|
|
||||||
## Open decisions
|
## Open decisions
|
||||||
Part A ratified (bare / postfix / `⇒ callconv(.c)` / lib-separate). Part B (confirm
|
Part A ratified (bare / postfix / `⇒ callconv(.c)` / lib-separate). Part B (confirm
|
||||||
@@ -41,6 +44,9 @@ historical carve-out — keep `issues/*.md` provenance, gate the live tree only.
|
|||||||
- (0.0) Added `kw_extern`/`kw_export` tokens + keyword-map entries + LSP keyword
|
- (0.0) Added `kw_extern`/`kw_export` tokens + keyword-map entries + LSP keyword
|
||||||
classification + `lex linkage keywords` test. Suite green; no identifier collisions
|
classification + `lex linkage keywords` test. Suite green; no identifier collisions
|
||||||
in the corpus. `lock` commit.
|
in the corpus. `lock` commit.
|
||||||
|
- (0.1) Added `ast.ExternExportModifier` + `FnDecl.extern_export` +
|
||||||
|
`VarDecl.is_extern`/`extern_name` + `parseOptionalExternExport()` (unconsumed) + 2
|
||||||
|
parser unit tests. Suite green (443/633). `lock` commit.
|
||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
None yet.
|
None yet.
|
||||||
|
|||||||
19
src/ast.zig
19
src/ast.zig
@@ -123,6 +123,14 @@ pub const Root = struct {
|
|||||||
|
|
||||||
pub const CallingConvention = enum { default, c };
|
pub const CallingConvention = enum { default, c };
|
||||||
|
|
||||||
|
/// Linkage modifier written in the postfix slot after `callconv(...)`:
|
||||||
|
/// `name :: (sig) -> Ret [callconv(.x)] [extern | export] [;|{…}];`
|
||||||
|
/// `extern` = import (external linkage, C ABI, no sx ctx — `#foreign`'s role);
|
||||||
|
/// `export` = define + expose (body + external linkage + C ABI + no ctx).
|
||||||
|
/// Both imply `callconv(.c)`. Variants carry a trailing `_` to dodge the Zig
|
||||||
|
/// keywords. `.none` = no linkage modifier (the ordinary sx-internal decl).
|
||||||
|
pub const ExternExportModifier = enum { none, extern_, export_ };
|
||||||
|
|
||||||
pub const FnDecl = struct {
|
pub const FnDecl = struct {
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
params: []const Param,
|
params: []const Param,
|
||||||
@@ -131,6 +139,10 @@ pub const FnDecl = struct {
|
|||||||
type_params: []const StructTypeParam = &.{},
|
type_params: []const StructTypeParam = &.{},
|
||||||
is_arrow: bool = false,
|
is_arrow: bool = false,
|
||||||
call_conv: CallingConvention = .default,
|
call_conv: CallingConvention = .default,
|
||||||
|
/// Postfix linkage modifier (`extern`/`export`) written after the
|
||||||
|
/// `callconv(...)` slot. `.none` for an ordinary sx-internal function.
|
||||||
|
/// Parsed in Phase 0.1; not consumed by the fn-decl path until Phase 1.
|
||||||
|
extern_export: ExternExportModifier = .none,
|
||||||
/// Span of the function's name token, for the reserved-type-name decl
|
/// Span of the function's name token, for the reserved-type-name decl
|
||||||
/// diagnostic. Synthesized decls (e.g. `#import c` foreign
|
/// diagnostic. Synthesized decls (e.g. `#import c` foreign
|
||||||
/// functions, lowering-time objc/protocol method synthesis) leave it zero.
|
/// functions, lowering-time objc/protocol method synthesis) leave it zero.
|
||||||
@@ -343,6 +355,13 @@ pub const VarDecl = struct {
|
|||||||
is_foreign: bool = false,
|
is_foreign: bool = false,
|
||||||
foreign_lib: ?[]const u8 = null,
|
foreign_lib: ?[]const u8 = null,
|
||||||
foreign_name: ?[]const u8 = null,
|
foreign_name: ?[]const u8 = null,
|
||||||
|
/// `extern`-global form `g : T extern ["csym"];` — a reference to a global
|
||||||
|
/// defined elsewhere (external linkage, resolved at link time). The new
|
||||||
|
/// extern-named surface; distinct from the legacy `#foreign` path above.
|
||||||
|
/// `extern_name` is the optional symbol-name override. Parsed in Phase 0.1;
|
||||||
|
/// not consumed by the var-decl path until Phase 1.2.
|
||||||
|
is_extern: bool = false,
|
||||||
|
extern_name: ?[]const u8 = null,
|
||||||
/// True when the binding name was written as a backtick raw identifier
|
/// True when the binding name was written as a backtick raw identifier
|
||||||
/// (`` `i2 := … ``). A raw name is exempt from the reserved-type-name
|
/// (`` `i2 := … ``). A raw name is exempt from the reserved-type-name
|
||||||
/// binding check.
|
/// binding check.
|
||||||
|
|||||||
@@ -3680,6 +3680,25 @@ pub const Parser = struct {
|
|||||||
return cc;
|
return cc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Postfix linkage modifier in the slot after `callconv(...)`:
|
||||||
|
/// `extern` (import) or `export` (define + expose), or `.none` if neither.
|
||||||
|
/// Mirrors `parseOptionalCallConv`. Bare-keyword today; the optional
|
||||||
|
/// `"csym"` symbol-name override lands in Phase 1.2/2.2. Defined here in
|
||||||
|
/// Phase 0.1 but NOT yet called from any decl path (wired in Phase 1.0).
|
||||||
|
fn parseOptionalExternExport(self: *Parser) ast.ExternExportModifier {
|
||||||
|
switch (self.current.tag) {
|
||||||
|
.kw_extern => {
|
||||||
|
self.advance();
|
||||||
|
return .extern_;
|
||||||
|
},
|
||||||
|
.kw_export => {
|
||||||
|
self.advance();
|
||||||
|
return .export_;
|
||||||
|
},
|
||||||
|
else => return .none,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn isAssignOp(self: *const Parser) bool {
|
fn isAssignOp(self: *const Parser) bool {
|
||||||
return switch (self.current.tag) {
|
return switch (self.current.tag) {
|
||||||
.equal, .plus_equal, .minus_equal, .star_equal, .slash_equal, .percent_equal,
|
.equal, .plus_equal, .minus_equal, .star_equal, .slash_equal, .percent_equal,
|
||||||
@@ -3975,6 +3994,48 @@ test "parse minimal main" {
|
|||||||
try std.testing.expectEqual(@as(i64, 42), body.data.block.stmts[0].data.int_literal.value);
|
try std.testing.expectEqual(@as(i64, 42), body.data.block.stmts[0].data.int_literal.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "parseOptionalExternExport recognizes linkage keywords (unconsumed)" {
|
||||||
|
// Phase 0.1 plumbing: the helper exists and maps the keywords, but no
|
||||||
|
// decl path calls it yet (wired in Phase 1.0). Drive it directly.
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
{
|
||||||
|
var parser = Parser.init(arena.allocator(), "extern");
|
||||||
|
try std.testing.expectEqual(ast.ExternExportModifier.extern_, parser.parseOptionalExternExport());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
var parser = Parser.init(arena.allocator(), "export");
|
||||||
|
try std.testing.expectEqual(ast.ExternExportModifier.export_, parser.parseOptionalExternExport());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
var parser = Parser.init(arena.allocator(), "foo");
|
||||||
|
try std.testing.expectEqual(ast.ExternExportModifier.none, parser.parseOptionalExternExport());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "extern/export AST fields default to absent (unconsumed)" {
|
||||||
|
// FnDecl.extern_export defaults to .none on a normally-parsed function;
|
||||||
|
// the fn-decl path does not consume the modifier until Phase 1.0.
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
var parser = Parser.init(arena.allocator(), "f :: () {}");
|
||||||
|
const root = try parser.parse();
|
||||||
|
const fd = root.data.root.decls[0].data.fn_decl;
|
||||||
|
try std.testing.expectEqual(ast.ExternExportModifier.none, fd.extern_export);
|
||||||
|
|
||||||
|
// VarDecl.is_extern / extern_name default to absent (no var-decl path
|
||||||
|
// consumes them until Phase 1.2). A struct literal locks field presence +
|
||||||
|
// defaults without depending on a top-level var form.
|
||||||
|
const vd: ast.VarDecl = .{
|
||||||
|
.name = "g",
|
||||||
|
.name_span = .{ .start = 0, .end = 0 },
|
||||||
|
.type_annotation = null,
|
||||||
|
.value = null,
|
||||||
|
};
|
||||||
|
try std.testing.expect(!vd.is_extern);
|
||||||
|
try std.testing.expect(vd.extern_name == null);
|
||||||
|
}
|
||||||
|
|
||||||
test "block value: trailing expr without `;` produces a value" {
|
test "block value: trailing expr without `;` produces a value" {
|
||||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
defer arena.deinit();
|
defer arena.deinit();
|
||||||
|
|||||||
Reference in New Issue
Block a user