diff --git a/current/CHECKPOINT-EXTERN-EXPORT.md b/current/CHECKPOINT-EXTERN-EXPORT.md index 56d3391..532432f 100644 --- a/current/CHECKPOINT-EXTERN-EXPORT.md +++ b/current/CHECKPOINT-EXTERN-EXPORT.md @@ -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. ## Last completed step -**Phase 0.0** (lock) — added `kw_extern`/`kw_export` to `token.Tag` + the -`keywords` `StaticStringMap` (beside `kw_callconv`, `token.zig:45,282`); classified -both as `ST.keyword` in the LSP `classifyToken` exhaustive switch -(`lsp/server.zig`); added `test "lex linkage keywords"` in `lexer.zig`. Suite green -(441 unit / 633 corpus, 0 fail). `extern`/`export` are now reserved words; corpus -sweep confirmed no `.sx` identifier used either (only comments + the unpinned -`issues/0030-*` repro, which doesn't run in the suite). +**Phase 0.1** (lock) — added `ast.ExternExportModifier = enum { none, extern_, +export_ }` (beside `CallingConvention`), `FnDecl.extern_export` (default `.none`), +`VarDecl.is_extern`/`extern_name` (defaults absent), and +`parser.parseOptionalExternExport()` (mirrors `parseOptionalCallConv`, just below it +at `parser.zig:~3683`). **Helper + fields defined but NOT consumed by any decl path** +— no user-facing behavior change, corpus diff empty. Two inline parser unit tests +(`parseOptionalExternExport recognizes linkage keywords`, `extern/export AST fields +default to absent`). Suite green (443 unit [+2] / 633 corpus, 0 fail). ## Current state 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). 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 -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 -**Phase 0.1** (lock) — `parseOptionalExternExport()` (mirror `parseOptionalCallConv`, -`parser.zig:3669`) + `ast.ExternExportModifier` enum + `FnDecl.extern_export` + -`VarDecl.is_extern`/`extern_name` fields; **parsed but not yet consumed**; unit AST -test. Then Phase 1 (`extern` working: 1.0 xfail accept postfix, 1.1 green lower via -`declareExtern`, 1.2 green rename + extern-global). See the plan's **"Kickoff -prompt"**. Stop at end of Phase 1. +**Phase 1.0** (xfail) — accept postfix `extern` after the callconv slot in the +fn-decl path (`parser.zig:1950`: call `parseOptionalExternExport()`, store on +`FnDecl.extern_export`); add `examples/12xx-ffi-extern-fn.sx` that extern-binds a libc +symbol — **red** (parses, but lowering not wired yet). Then 1.1 green (lower via +`declareExtern`: `is_extern`, `.external`, `callconv(.c)`, no ctx — anchors +`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 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 classification + `lex linkage keywords` test. Suite green; no identifier collisions 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 None yet. diff --git a/src/ast.zig b/src/ast.zig index 5c38dcb..f539253 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -123,6 +123,14 @@ pub const Root = struct { 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 { name: []const u8, params: []const Param, @@ -131,6 +139,10 @@ pub const FnDecl = struct { type_params: []const StructTypeParam = &.{}, is_arrow: bool = false, 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 /// diagnostic. Synthesized decls (e.g. `#import c` foreign /// functions, lowering-time objc/protocol method synthesis) leave it zero. @@ -343,6 +355,13 @@ pub const VarDecl = struct { is_foreign: bool = false, foreign_lib: ?[]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 /// (`` `i2 := … ``). A raw name is exempt from the reserved-type-name /// binding check. diff --git a/src/parser.zig b/src/parser.zig index ace1d1c..f03b572 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -3680,6 +3680,25 @@ pub const Parser = struct { 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 { return switch (self.current.tag) { .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); } +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" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit();