From bf6ef8370f28b147132178c9c9874378378b667b Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 14 Jun 2026 12:40:35 +0300 Subject: [PATCH] feat(ffi-linkage): add kw_extern/kw_export tokens (Phase 0.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lex 'extern' and 'export' as keywords beside 'callconv': new token.Tag variants + keywords StaticStringMap entries + LSP semantic-token keyword classification. Adds a 'lex linkage keywords' unit test. Tokens only — parser/AST plumbing and lowering land in later phases. Corpus sweep confirmed no .sx identifier collides with the new reserved words. lock commit per the cadence rule. --- current/CHECKPOINT-EXTERN-EXPORT.md | 23 +++++++++++++++++------ src/lexer.zig | 9 +++++++++ src/lsp/server.zig | 2 ++ src/token.zig | 4 ++++ 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/current/CHECKPOINT-EXTERN-EXPORT.md b/current/CHECKPOINT-EXTERN-EXPORT.md index 3b85886..56d3391 100644 --- a/current/CHECKPOINT-EXTERN-EXPORT.md +++ b/current/CHECKPOINT-EXTERN-EXPORT.md @@ -5,8 +5,13 @@ 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 -None — plan authored, not yet started. (Merged the former EXTERN-EXPORT + -FOREIGN-MIGRATION plans into this single plan.) +**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). ## Current state Syntax decided + ratified: bare `extern`/`export`, postfix in the `callconv(.c)` @@ -15,12 +20,15 @@ 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). No code written. +the live tree (Phase 9.4 gate). Tokens exist (0.0); parser/AST not yet plumbed. ## Next step -**Phase 0.0** — add `kw_extern`/`kw_export` tokens + keyword-map entries (beside -`kw_callconv`) + unit lex test. Then 0.1 (parser plumbing, unconsumed), then Phase 1 -(`extern` working). See the plan's **"Kickoff prompt"** section. +**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. ## Open decisions Part A ratified (bare / postfix / `⇒ callconv(.c)` / lib-separate). Part B (confirm @@ -30,6 +38,9 @@ historical carve-out — keep `issues/*.md` provenance, gate the live tree only. ## Log - (init) Plan written; FFI-linkage stream opened. - (merge) Folded FOREIGN-MIGRATION in as Part B; deleted the split plan + checkpoint. +- (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. ## Known issues None yet. diff --git a/src/lexer.zig b/src/lexer.zig index 006905f..30bdaad 100644 --- a/src/lexer.zig +++ b/src/lexer.zig @@ -537,6 +537,15 @@ test "lex keywords" { } } +test "lex linkage keywords" { + // extern / export are keywords (FFI-linkage stream), lexed beside callconv. + var lex = Lexer.init("callconv extern export"); + const expected = [_]Tag{ .kw_callconv, .kw_extern, .kw_export }; + for (expected) |exp| { + try std.testing.expectEqual(exp, lex.next().tag); + } +} + test "lex type-like identifiers" { // i32, u8, bool, string are identifiers, not keywords var lex = Lexer.init("i32 u8 bool string"); diff --git a/src/lsp/server.zig b/src/lsp/server.zig index b949486..49776d5 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -1683,6 +1683,8 @@ pub const Server = struct { .kw_impl, .kw_inline, .kw_callconv, + .kw_extern, + .kw_export, .hash_run, .hash_import, .hash_insert, diff --git a/src/token.zig b/src/token.zig index c1c2d1b..0e053f3 100644 --- a/src/token.zig +++ b/src/token.zig @@ -43,6 +43,8 @@ pub const Tag = enum { kw_Self, // Self (in protocol declarations) kw_inline, // inline (compile-time if/for/while) kw_callconv, // callconv (calling convention annotation) + kw_extern, // extern (import: external linkage, C ABI, no body) + kw_export, // export (define + expose: external linkage, C ABI) // Symbols colon, // : @@ -280,6 +282,8 @@ pub const keywords = std.StaticStringMap(Tag).initComptime(.{ .{ "Self", .kw_Self }, .{ "inline", .kw_inline }, .{ "callconv", .kw_callconv }, + .{ "extern", .kw_extern }, + .{ "export", .kw_export }, }); pub fn getKeyword(bytes: []const u8) ?Tag {