Parser: a 'kw_extern' branch in the var-decl-with-type-annotation path (beside #foreign) parses 'name : type extern [LIB] ["csym"];' into VarDecl.is_extern/extern_lib/extern_name; the trailing diagnostic now lists 'extern'. Lowering: registerTopLevelGlobal uses extern_name orelse foreign_name orelse name for the C symbol and sets is_extern = is_foreign or is_extern; globalInitValue returns null (no initializer) for extern globals too. examples/1225 green: '__stdinp : *void extern;' lowers to '@__stdinp = external global ptr'; @__stdinp reads non-null. Suite green (636 corpus / 443 unit). Phase 1 done: extern functions (bare + rename) and data globals (bare + rename) all work, behavior-equivalent to the matching #foreign form. export (Phase 2), aggregates (Phase 3), docs + A->B gate (Phase 4) remain. green commit.
7.0 KiB
sx extern/export + #foreign retirement — Checkpoint (FFI-linkage stream)
Companion to current/PLAN-EXTERN-EXPORT.md — one merged plan: Part A adds
extern/export, Part B migrates #foreign and purges foreign. Update after
every commit, one step at a time per the cadence rule.
Last completed step
Phase 1.2d (green) — PHASE 1 COMPLETE. extern data globals now parse + lower:
added a kw_extern branch in the var-decl-with-type-annotation path (parser.zig:~451,
beside #foreign) parsing [LIB] ["csym"] into is_extern/extern_lib/extern_name
(+ updated the trailing diagnostic to list extern); registerTopLevelGlobal now uses
extern_name orelse foreign_name orelse name for the C symbol and sets is_extern = is_foreign or is_extern; globalInitValue returns null (no init) for extern too.
Example 1225 green — __stdinp : *void extern; lowers to @__stdinp = external global ptr and @__stdinp reads non-null. Full suite green (636 corpus / 443 unit, 0
fail). Kickoff exit criteria met: suite green; extern libc fn + global bindings run;
#foreign unregressed (all its examples pass → snapshots unchanged).
Current state
Syntax: bare extern/export, postfix after callconv(.c), extern ⇒ callconv(.c).
Decision 4 revised (user 2026-06-14): extern carries an optional LIB+"csym"
axis (extern_lib/extern_name) like #foreign; the #library decl + build-flag
linking stays separate. extern IS FULLY WORKING (PHASE 1 DONE): functions —
bare (f :: (…) -> R extern;) AND renamed (extern [LIB] "csym"); data globals —
bare (g : T extern;) AND renamed. All behavior-equivalent to the matching #foreign
form (external linkage, C ABI, no sx ctx). extern_lib is parsed + stored but is a
reference only — actual linking stays the #library/build-flag axis (same as
#foreign's lib ref). export NOT started (Phase 2). Aggregates NOT started (Phase 3).
Part B foreign footprint to purge: 643 lines / ~57 identifiers in src/ + 28 doc
lines. End-state invariant: zero foreign (Phase 9.4 gate). Examples: 1223 (bare
fn), 1224 (fn rename), 1225 (bare global).
Next step
SESSION STOP — kickoff scope was Phases 0–1 only; Phase 1 is complete. Do NOT
start Phase 2 here. Next session picks up Phase 2 — export (define + expose):
fills the four export-gap conditions in decl.zig (all on the define path, not
declareExtern): (i) force .external linkage when extern_export == .export_
(:2382/:2514); (ii) promote to C ABI (:2110/the lowerFunction cc at :2522);
(iii) symbol-name override via export "csym" (already parsed into extern_name —
just consume on the define path); (iv) ctx already suppressed for .export_? NO —
funcWantsImplicitCtx currently suppresses only .extern_; broaden to != .none
(or add .export_). Start with an xfail multi-file test: an export fn called from a
companion .c caller. Then Phase 3 (aggregates), Phase 4 (interplay/diagnostics/docs
- the A→B gate: unit test that
#foreignandexternlower to identical IR).
Deferred (do in Phase 4): (a) docs — specs.md/readme.md document
extern/export (the plan defers docs to Phase 4; #foreign stays documented until
the Part B cutover); (b) visibility-gate equivalence — bare extern (no extern_lib)
is currently unconditionally visible via the c_import_bare gate
(decl.zig:~2241, fd.body.data != .foreign_expr → return true), whereas a lib-less
#foreign is policed by visibleOverEdges. Single-file examples don't exercise this;
verify/align it at the A→B gate (a bare-extern lib-less fn should be policed like its
#foreign twin). Adding it now would be untested — needs a cross-module example.
Open decisions
Part A ratified (bare / postfix / ⇒ callconv(.c) / lib-separate). Part B (confirm
before Phase 9): runtime-class rename target — Runtime*Class* (recommended);
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_exporttokens + keyword-map entries + LSP keyword classification +lex linkage keywordstest. Suite green; no identifier collisions in the corpus.lockcommit. - (0.1) Added
ast.ExternExportModifier+FnDecl.extern_export+VarDecl.is_extern/extern_name+parseOptionalExternExport()(unconsumed) + 2 parser unit tests. Suite green (443/633).lockcommit. - (1.0a) Wired fn-path extern parsing (
parseFnDecl+ both lookahead predicates) + addedFnDecl.extern_lib/extern_name+VarDecl.extern_libper user feedback (decision 4 revised: extern carries an optional lib axis). Unconsumed by lowering. Suite green (443/633).lockcommit. - (1.0b) Added
examples/1223-ffi-extern-fn.sx+ hand-authored success snapshots. RED (634 ran, 1 failed — semabody produces no value).xfailcommit; 1.1 greens it. - (1.1) Wired extern fn lowering (6 edits in
decl.zig, all declare-only routing mirroringforeign_expr):funcWantsImplicitCtx+declareFunctioncc +lazyLowerFunction/lowerFunction/lowerFunctionBodyIntoguards. 1223 green;declare i32 @abs(i32)(C ABI, no ctx). Suite green (634/443).greencommit. - (1.2a) Added
examples/1224-ffi-extern-fn-rename.sx(c_abs :: … extern "abs";) + hand-authored success snapshot (c_abs(-42) = 42). RED (635 ran, 1 failed — parse error:"abs"afterexternnot yet accepted).xfail; 1.2b greens it. (Also recovered a formatter-clobberedparser.zig— see Known issues.) - (1.2b)
parseFnDeclparses the optional[LIB] ["csym"]tail intoextern_lib/extern_name;declareFunctionunifies the rename (foreign c_name OR extern_name → declare under C name, map sx→C) and extends the dedupe guard to extern. 1224 green (c_abs→abs); 1223 unregressed. Suite green (635/443).greencommit. extern_lib parsed+stored (lib linking stays the#libraryaxis). - (1.2c) Added
examples/1225-ffi-extern-global.sx(__stdinp : *void extern;, mirrors#foreignglobal 1205) + success snapshot. RED (636 ran, 1 failed — parse error: var-declexternnot accepted).xfail; 1.2d greens it. - (1.2d) Parser
kw_externbranch in the var-decl path ([LIB] ["csym"]→is_extern/extern_lib/extern_name) +registerTopLevelGlobal/globalInitValueconsumeis_extern. 1225 green (@__stdinp = external global ptr). Suite green (636/443).greencommit. PHASE 1 COMPLETE —externfns + globals fully work.
Known issues
- Workflow hazard (1.2): an editor format-on-save (or
zig fmt) clobbered the working-treesrc/parser.zigbetween commits — it reformatted one-liners AND silently dropped myhasFnBodyAfterArrowextern edit, reverting 1223 to a parse error. Recovered withgit checkout src/parser.zig(HEAD had the correct, committed version). After any Edit-tool change to a file the IDE may have open, rebuild + run the affected example before trusting the edit.