10 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 2.2 (green) — PHASE 2 COMPLETE. export (define + expose) fully works:
external linkage + C ABI + no sx ctx + force-lowered root + optional "csym" rename.
All four export-gap conditions filled in decl.zig: (i) .external linkage for
extern_export == .export_ on both define paths (lowerFunctionBodyInto,
lowerFunction); (ii) C-ABI promotion on the define paths + declareFunction stub cc;
(iv) funcWantsImplicitCtx returns false for any non-.none modifier; force-lower:
export fns are lowering roots in lowerMainAndComptime (else an uncalled export fn
stays a bodiless declare); (iii) export … "csym" declares the stub under the C name
lazyLowerFunctionpromotes the body into it viaforeign_name_map. Examples 1226 (bare export, C callssx_square→ 37/82) + 1227 (export "triple_c", C callstriple_c→ 22) green via the new AOT corpus mode. Suite green (638 corpus / 443 unit, 0 fail).
AOT corpus mode + run_examples.sh retired. C→sx-by-name can't link under the
corpus's sx run JIT mode (a JIT-resident symbol is invisible to a dlopen'd C dylib's
flat-namespace lookup), so an expected/<name>.aot marker switches an example to a
sx build + execute flow. The standalone tests/run_examples.sh was deleted —
zig build test is now the sole corpus runner (verify-step.sh + CLAUDE.md updated).
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 (PHASE 1) + export (PHASE 2) FULLY WORKING.
extern: functions — bare (f :: (…) -> R extern;) AND renamed (extern [LIB] "csym");
data globals — bare + renamed. export: functions — bare (f :: (…) -> R export {…})
AND renamed (export "csym"); external linkage, C ABI, no ctx, force-lowered as a root.
All behavior-equivalent to the matching #foreign form. extern_lib is parsed + stored
but is a reference only — actual linking stays the #library/build-flag axis.
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 (extern bare fn), 1224 (extern fn rename), 1225 (extern bare
global), 1226 (export bare fn, AOT), 1227 (export fn rename, AOT).
Next step
Phase 3 — aggregates (objc / jni runtime classes): #objc_class("X") extern { … }
(import) + … export { … } (define) parse alongside legacy #foreign #objc_class
(parser.zig tryParseForeignClassPrefix/parseForeignClassDecl); map postfix
extern→reference, export→define+register (objc_class.zig); per-runtime tests
(objc, jni). Then Phase 4 (interplay/diagnostics/docs + the A→B gate: unit test that
#foreign and extern/export lower to identical IR) before Part B migration.
FUTURE MILESTONE — C→sx-by-name in JIT (sx run). Investigated this session
(user-requested spike, RESOLVED feasible-but-blocked). Adding the C #source objects
directly into the ORC JITDylib (LLVMOrcLLJITAddObjectFile) instead of dlopen'ing a
dylib makes C↔sx cross-references resolve both ways in one link domain — proven: a
~20-line spike ran 1226 via sx run (37/82) and all 13 existing #source FFI examples
still passed. BLOCKER: C objects using _Thread_local (the return-trace runtime
sx_trace.c) SIGABRT under JITLink — MachO thread-local-variable handling needs the ORC
MachOPlatform set up (the bare LLVMOrcCreateLLJIT default doesn't), and C
constructors/__mod_init_func won't run without ORC initializer support. 42 errors-*
examples crashed in the spike. A real impl needs a C++ shim in llvm_shim.c
(LLJITBuilder().setObjectLinkingLayerCreator(...) + MachOPlatform::Create) — its own
milestone, NOT Phase 2/3 scope. The AOT .aot-marker corpus mode is the pragmatic test
path and works today. Spike fully reverted (target.zig/main.zig at HEAD).
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. - (JIT spike) User-requested feasibility investigation of C→sx-by-name in
sx run(JIT). Verdict: feasible viaLLVMOrcLLJITAddObjectFile(C objects into the ORC JITDylib) — proven by a throwaway spike — but blocked by JITLink MachO TLV handling (sx_trace.c's_Thread_localSIGABRTs without the ORCMachOPlatform). Own future milestone (see Next step). Spike reverted; no commit. - (2.0) Added the AOT corpus mode (
expected/<name>.aot→sx build+ execute) tocorpus_run.test.zig+ retiredtests/run_examples.sh(verify-step.sh/CLAUDE.md updated) +examples/1226-ffi-export-fn.{sx,c,h}(C callssx_squareback). RED (AOT link fails:_sx_squareundefined — export not lowered).xfail; 2.1 greens it. - (2.1) Filled export gaps i/ii/iv in
decl.zig(.externallinkage +.ccc on both define paths;funcWantsImplicitCtxfalse for any non-.nonemodifier) + force-lower export fns as roots inlowerMainAndComptime. 1226 green via AOT (37/82). Suite green (637/443).greencommit. - (2.2a) Added
examples/1227-ffi-export-fn-rename.sx(export "triple_c", C callstriple_c). RED (define path emits@sx_triple, ignoresextern_name→ C ref undefined).xfail; 2.2b greens it. - (2.2b)
declareFunctionrename branch fires forexport(stub under C name + sx→C inforeign_name_map);lazyLowerFunctionresolves the stub by that C name so the body promotes into the C-named function (define @triple_c). sx-side call sites resolve via the same map (probe: 5*5→25). 1227 green (22); 1226 unregressed. Suite green (638/443).greencommit. PHASE 2 COMPLETE —exportfully works.
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.