Two post-stream follow-ups flagged in CHECKPOINT-EXTERN-EXPORT.md, plus a reproducible vscode-extension packaging setup: - parser: drop the vestigial `RuntimeClassPrefix.is_extern` field and `parseRuntimeClassDecl`'s `is_extern` param. Always false since the `#foreign` token was deleted; the postfix `extern`/`export` keyword is the sole reference-vs-define decider. No behavior change (644 corpus / 442 unit). - vscode grammar: highlight `extern`/`export` as `storage.modifier.sx`. - vscode packaging: declare `@vscode/vsce` as a devDep + add `package` / `vscode:prepublish` scripts so the vsix rebuilds reproducibly (was an ambient tool). Add repository/homepage/bugs (Gitea), icon (swipelab logo, 256x256), galleryBanner, README with cover banner. Rebuilt the vsix.
64 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 9 COMPLETE — total foreign purge; 9.4 GATE PASSES. THE ENTIRE
FFI-LINKAGE STREAM (Parts A + B, Phases 0–9) IS DONE. Final commits: 9.0 token
delete (dfae690), 9.3 src/docs/example/library comment purge (811a280, e99383f,
dc51c4b, + the capital-Foreign sweep), 9.3 example filename renames + dedup
(b52d424), 9.3/9.4 issues/*.md purge + GATE (b9cfe25).
- 9.0: deleted the
hash_foreigntoken entirely (token/lexer/parser/lsp + the lex test);#foreignnow → a generic "expected ';'" parse error (accepted UX cost); deleted the obsolete 1176 rejection test. - 9.1/9.2: all internal identifiers renamed (linkage→
extern/is_extern, runtime-class→Runtime*/runtime_*per Decision 5,foreign_path→runtime_pathacross the build-hook boundary);foreign_exprnode eliminated. - 9.3: purged every
foreignCOMMENT (src caps + lowercase, examples, docs incl. the obsolete inline-asm Deviation 6, editors/vscode grammar) + renamed all 10*-foreign*example files (+ companions/expected/refs) to extern/runtime names (dedup'd 1218↔1229, removed orphan 1620 dir) + rewrote 20 issues/*.md writeups + renamed issues/0043. - 9.4 GATE:
grep -rniIE 'foreign' src/ library/ examples/ issues/ docs/ editors/ specs.md readme.md CLAUDE.md→ 0, excluding only the legitimate keeps:SQLITE_CONSTRAINT_FOREIGNKEY(SQLite API const) + vendoredlibrary/vendors/sqlite/ c/*(upstream third-party C). Noforeign-named files in the tree (node_modules + .sx-tmp are gitignored third-party/scratch). Suite green (644 corpus / 443 unit, 0 failed).
Prior: Phase 9.3 — text/comment purge (src + docs + example comments) (commits
e99383f docs, dc51c4b src, + examples purge STAGED pending a classifier outage —
commit message ready; git commit the staged examples/ changes when Bash is back).
foreign is now purged from: all src/ comments (reworded to extern/runtime-class;
fixed 2 user-facing diagnostics — the type-annotation parse error no longer lists
#foreign, and the Android no-#jni_main help shows #jni_class(…) extern), specs/
readme/CLAUDE ("Foreign Function Interface"→"C Interop", etc.), and all example .sx
comments (1219 stdout labels Foreign→Extern, snapshot regenerated). Suite green
(646/444) throughout; snapshot-neutral except the intentional 1219 regen.
What still contains foreign (the analyzed keep-list + the not-yet-done):
- KEEP (gate-exempt):
src/hash_foreigntoken + lexer entry +lex hash_foreigntest (#foreignx) + the 4 parser rejection messages ("#foreignhas been removed…");1176-diagnostics-foreign-removed.sx(its#foreigndecl + comments ARE the rejection test);SQLITE_CONSTRAINT_FOREIGNKEY+ vendoredlibrary/vendors/sqlite/c/*. - NOT YET DONE: example FILENAMES (
*-foreign*.sx+ the0729/1205/1218/1219/1306/1318/1216/1217families) and their#import/#include/#sourcepath refsexpected/files — needs a git-mv rename step; andissues/*.md(~20 writeups).
Prior: Phase 9.1 + 9.2 — internal IDENTIFIER purge COMPLETE (commits 9.1a b838f63,
9.1b b78e7dd, 9.1c cd14794, 9.1d 7ffdc7d, 9.2a 3354446, 9.2b 5c8af6e,
9.2b-fix a15a868, 9.2c d27be42, 9.2d 8cca3b9). Every foreign IDENTIFIER in
src/ is renamed — the only foreign left in src/ is COMMENTS + the kept token
(hash_foreign + its #foreignx lexer-boundary test) + the rejection-message string.
Suite green (646/444) at every commit.
- 9.1d eliminated the
foreign_exprAST node: migratedc_import.zigauto-synth to the extern shape, deleted the node +ForeignExpr+ all readers. - 9.2a/b/c/d ran the runtime-class family rename (Decision 5 →
Runtime*): typesForeignClassDecl→RuntimeClassDecletc.; fnsparseForeignClassDecl→parseRuntimeClassDecl,lowerForeignMethodCall→lowerRuntimeMethodCall, …; stateforeign_class_map→runtime_class_map,foreign_class_declvariant→runtime_class_decl; the extern-ref validators →Extern(linkage,checkExternRefs); the reference flag →is_extern(per user: reuse existing terminology, not a newis_reference); andforeign_path→runtime_pathCOUPLED across the hook boundary (build.sx accessorjni_main_runtime_path_at+ the registered hook string + bundle.sx + specs.md), with 37.irsnapshots regenerated for the renamed@BuildOptions.jni_main_runtime_path_atdeclare stub (symbol-name change only). - 9.1a/b/c (linkage): 5 collision-free renames (callExtern, …); "foreign symbol" diagnostic + panic → "extern symbol" (1172 regen); deleted dead VarDecl legacy fields.
Prior: Phase 9.1 (partial) — internal linkage-identifier purge (commits b838f63 9.1a,
b78e7dd 9.1b, cd14794 9.1c). PHASE 9 STARTED. Decision 6 = PURGE EVERYTHING,
scoped (user, 2026-06-15): purge foreign from all .sx files + all documentation +
all our Zig (src/), analyzing each grep hit — legitimate keeps stay
(SQLITE_CONSTRAINT_FOREIGNKEY + other SQLite API constant names, vendored
library/vendors/sqlite/c/*, 1176-diagnostics-foreign-removed.sx [the rejection test
MUST contain #foreign], the parser rejection-message string + hash_foreign token
[kept so #foreign keeps its friendly deprecation error]).
- 9.1a (
b838f63): 5 collision-free linkage renames —callForeign→callExtern,marshalForeignArg→marshalExternArg,dedupeForeignSymbol→dedupeExternSymbol,foreign_name_map→extern_name_map,is_foreign_c_api→is_extern_c_api. - 9.1b (
b78e7dd): the "foreign symbol already bound" diagnostic (decl.zig) + resolveFuncByName panic (call.zig) → "extern symbol". Intentional 1172 regen. - 9.1c (
cd14794): deleted the deadVarDecl.is_foreign/foreign_lib/foreign_namefields (the global#foreignpath rejects → write-dead; 3 coalescing readers in decl.zig simplified tovd.extern_name/vd.is_extern). All snapshot-neutral except the intentional 1172 regen; suite green (646/444).
COLLISION ANALYSIS (done — drives the rest of 9.1/9.2):
is_foreignlives on FnDecl?(no — flipped toextern_export), VarDecl (deleted in 9.1c), and ForeignClassDecl (ast.zig:903 — STILL LIVE, distinguishes runtime-class reference vs define; renamed in 9.2, not 9.1).is_extern/extern_lib/extern_namealready exist (VarDecl + IR insts) — so the oldforeign_*linkage names could NOT be blind-renamed onto them; 9.1c deleted the dead VarDecl trio instead of renaming.foreign_expr(25) is still BUILT byc_import.zigauto-synthesis (#import c {#include}synthesizes fn bodies asforeign_expr). To eliminate it: migrate that synth path to build the extern shape (empty-block body +extern_export = .extern_), exactly the Phase 5.0 fn-body flip but for auto-synth — THEN delete theforeign_exprnode + all readers. This is the last 9.1 item.
Prior: Phase 8 — CUTOVER: parser hard-rejects #foreign (feat! commit 3811311,
preceded by the 8.0 xfail 8180faf + 3 pre-cutover refactors 2cce6a3/720556b/
d132aab). PHASE 8 COMPLETE. The prefix #foreign linkage directive is removed:
all four parse sites (const-with-type 316, data global 425, fn body 2065, runtime-class
prefix via caller 260) reject it with the migration message "#foreign has been
removed; use the postfix extern (import) / export (define) linkage keyword
instead"; added a span-aware failAt for the runtime-class case (the lookahead
consumes the token before the reject decision). New example 1176
(diagnostics-foreign-removed) pins it. Pre-cutover migrations (all green,
behavior-preserving): the 7 identity ffi-foreign-* test DECLS (2cce6a3), the two
keyword-neutral diagnostic tests 1172 + 1228 with intentional snapshot regens
(720556b), and the 4 multi-file example companions Phase 7 missed (0729/a+b, 1617/c,
1623/mod — d132aab). Deleted obsolete tests 1174 (#foreign+postfix conflict, now
unreachable) + 1620 (#foreign nosuchunit, superseded by extern twin 1231), the GATE
A→B unit test + lowerSrcToIr helper (nothing left to compare), and converted the
in-source parse void function with foreign body parser test to postfix extern.
specs.md + readme.md document extern/export as the sole C-linkage surface. Suite
green (646 corpus / 444 unit, 0 failed).
Prior: Phase 7.4 — migrate straggler examples #foreign→extern (refactor commit
1a8991a). PHASE 7 MIGRATABLE WORK COMPLETE (7.1–7.4 done). Migrated 16 fn/global
examples across categories (0415/0602/0603/1024/1025/1605/1607-1609/1611/1616/1619/
1622/1628/1635/1636). Marker'd ones corpus-validated; the 3 unmarked uikit importers
(1607/1608/1616) verified byte-identical via sx ir probes. Empty snapshot diff; suite
green (647/444).
Phase 7 net result: every example that uses #foreign incidentally (FFI plumbing,
output-preserving) is now on extern/export. The 24 files still holding #foreign
are exactly the intended keep-list, all deferred to the Phase 8 cutover:
foreign-asserting diagnostics (migrating changes a snapshot): 1172, 1174, 1219 (stdout label), 1228 (equivalence test), 1620.- Identity
ffi-foreign-*feature tests (real decls; rename/dedup at cutover): 1205-global, 1205-global-helper, 1207, 1218, 1219, 1306, 1318. - Comment-only / provenance prose (decls=0;
#foreignonly in comments): 0716, 0729, 1216, 1223, 1224, 1225, 1229, 1230, 1231, 1332, 1348, 1349, 1426, + issues/0030. Lesson (7.3): the robust class-prefix transform is the GENERAL forms/#foreign\s+#(\w+)\((\"[^\"]*\")\)\s*\{/#$1($2) extern {/— 1417 also used#jni_interface/#objc_protocol/#swift_class/#swift_struct/#swift_protocol, and a#(objc|jni)_class-only regex leftexternin prefix position → parse error. All such directives accept the postfix modifier (probed). Bare defined#objc_class/#jni_classexamples (no#foreign) were left untouched — not a purge target (define→export is an optional consistency pass, deferrable).
Prior: Phase 7.1 — migrate incidental 12xx ffi examples (refactor commit 731fb8d).
Migrated 12 plain-C examples (1200/1206/1209-1215/1220/1221/1222); established the
keep-list policy above. Phase 7.2 (a68f7c2): 18 13xx obj-c examples (prefix→postfix
classes). Phase 7.3 (2888f6f): 13 14xx jni examples incl. 1417 multi-runtime.
Prior: Phase 6.5 — migrate gpu/ #foreign→extern; library/ now #foreign-free
(refactor commit 32a7628). PHASE 6 COMPLETE. Final batch: gpu/gles3.sx
(eglGetProcAddress + 1 comment) + gpu/metal.sx (MTLCreateSystemDefaultDevice), bare
fn markers → extern. Verified byte-identical sx ir on importers 1610 (gles3) +
1606 (metal). Zero #foreign remains anywhere under library/ — verified by
grep -rln '#foreign' library/ → no matches. Suite green (647 corpus / 444 unit, 0
failed).
Prior: Phase 6.4 — migrate ffi/ #foreign→extern (refactor commit 666a2e2).
objc/objc_block/raylib/sdl3/wasm (~51 sites): fn markers + objc.sx's 2 import runtime
classes (prefix→postfix extern). objc + objc_block validated by the 50 marked 13xx
corpus examples (incl. import classes 1300/1301 + defined classes 1339/1349);
raylib/ffi-sdl3/wasm verified by byte-identical sx ir probes pre/post. Empty snapshot
diff; suite green.
Prior: Phase 6.3 — migrate std/ #foreign→extern (refactor commit 59f90d2).
Pure source rename across 11 std modules (~60 sites):
cli/core/fmt/fs/log/net.kqueue/process/socket/thread/time/trace. All fn-decl markers
— bare #foreign;, #foreign libc;/#foreign tlib; (LIB ref), and
#foreign libc "csym"; (LIB+rename) → the same extern … tail (extern carries the
identical [LIB] ["csym"] axis); plus 2 stale comment mentions (fmt/fs). No class
forms in std. These modules ARE host-corpus-exercised → empty snapshot diff is direct
validation. Suite green (647 corpus / 444 unit, 0 failed). Remaining Phase 6 batches:
6.4 ffi (~50, has runtime classes), 6.5 remainder.
Prior: Phase 6.2 — migrate platform/ #foreign→extern/export (refactor commit
2cd5d7b). Pure source rename across uikit/android/android_jni/sdl3 (~64 sites):
30 fn … #foreign;→… extern;; 34 import runtime classes
#foreign #objc_class/#jni_class("X") {→#…_class("X") extern { (prefix→postfix);
4 defined Sx* obj-c classes #objc_class("X") {→… export {. Behavior-preserving;
empty snapshot diff. Verification (these modules are largely uncompiled by the
host corpus — bundle examples import bundle.sx, not the runtime modules; android.sx
only compiles under OS==.android): byte-identical sx ir on uikit importers 1610 +
1606 (which DO compile uikit incl. the 4 defined Sx* classes on host) and an sdl3
direct-import probe; android.sx verified by an identical 4-error dedup set (host
pthread clashes — the keyword-neutral "foreign symbol already bound" dedup message is
unchanged, and the probe parsed all migrated extern jni classes + EGL fns cleanly
before hitting them). Suite green (647 corpus / 444 unit, 0 failed). Remaining Phase 6
batches: 6.3 std (~60), 6.4 ffi (~50), 6.5 remainder.
Prior: Phase 6.1 — migrate vendors/sqlite #foreign→extern (refactor commit
410a52e). PART B PHASE 6 STARTED. Pure source rename: all 97
sqlite3_* … #foreign sqlib "csym"; fn decls → extern sqlib "csym"; (+ the one
stale header-comment reference, line 9). The extern_lib axis references the sqlib
#import c unit identically to #foreign sqlib, so IR/output is byte-identical —
empty snapshot diff (only sqlite.sx changed), and example 1624
(vendor-sqlite-module) stdout byte-unchanged. Suite green (647 corpus / 444 unit,
0 failed).
Prior: Phase 5.1 — annotate A→B gate post-flip + add fn-rename case (test commit
93e7b6f). PHASE 5 COMPLETE → PART B Phase 5 done. The A→B gate
(lower.test.zig) already asserted #foreign ≡ extern/export byte-identical IR
for fn / global / Obj-C class; post-Phase-5.0 the fn-decl + data-global paths build
the SAME extern-named AST, so cases 1/2 are now STRUCTURALLY identical (guaranteed by
construction, not coincidence). Annotated the gate header to record this and keep it
as a regression tripwire (catches a future reader re-diverging the spellings, or a
revert of the flip); case 3 (runtime class) stays behaviorally — not structurally —
equal via the single is_foreign_eff field. Added a fn-rename case (case 2b,
extern_name axis: c_abs → "abs") to broaden coverage beyond bare import
(verified IR-identical via sx ir probe before adding). Test-only, no snapshot churn.
Suite green (647 corpus / 444 unit, 0 failed).
Prior: Phase 5.0 — fn-decl #foreign body-marker FLIP (refactor commit 6b94bb6).
PHASE 5.0 PARSER ROUTING COMPLETE. The fn-body #foreign [LIB] ["csym"] marker
now builds the SAME extern AST postfix extern produces (extern_export = .extern_
extern_lib/extern_name+ empty-block body) instead of aforeign_exprbody. Behavior-preserving — all four prereqs (visibility, variadic, plain-free, lib-ref) ensure every downstream reader coalescesis_foreignwithextern_export, so IR + runtime are byte-identical (full corpus + A→B gate green). Decision 7 churn realised: example 1620's lib-ref error flips "#foreign library" → "extern library" (the only snapshot moved; hand-edited, not regen). Parser unit test updated to assert the extern shape. Spot-checked 1219/1218/0729 (foreign rename / cvariadic / same-name) end-to-end. All four#foreignparser paths now resolved: global (e5ddfbe) + fn-body (6b94bb6) flipped onto extern; const-with-type is dead (deferred); runtime-class is already coalesced (is_foreign_eff).c_import.zigauto-synthesis STILL emitsforeign_exprbodies (Phase 6+), so both shapes coexist — every reader stays dual. Suite green (647 corpus / 444 unit, 0 failed).
Prior: Phase 5.0 prereqs 3 & 4 — plain-free classification + extern lib-ref validation
(plain-free: xfail 2706521 → fix 3c94c14; lib-ref: xfail 38c3240 → fix
ad6aed3). Two MORE extern/#foreign divergences found while de-risking the fn-path
flip, both now closed. FOUR prereqs total done — the fn-decl flip fully de-risked.
- Prereq 3 (plain-free):
isPlainFreeFn/isPlainFreeFnDecl(resolver.zig:178, generic.zig:815) excluded a#foreignbody but classified an empty-blockexternfn as a plain free fn — so existing extern fns were wrongly counted in the bare-call ambiguity verdict (example: two same-nameextern libc "abs"authors errored ambiguous, while the#foreigntwin 0729 compiles). Both predicates now also excludeextern_export == .extern_;export(real body) stays plain-free. Example 1230. - Prereq 4 (lib-ref validation):
checkForeignRefs(c_import.zig) validated onlyforeign_expr.library_ref, so a bogusextern nosuchunit "abs"compiled silently while#foreign nosuchuniterrors (1620). Now reads the lib ref from EITHER spelling and names the surface keyword in the diagnostic (so 1620 stays byte-unchanged). Example 1231. - Two OTHER classifying sites probed and found BENIGN for extern (no flip prereq):
namespace/qualified dispatch (
registerQualifiedFndecl.zig:2208, namespace gate call.zig:729) — a namespacedexternfn resolves identically to its#foreigntwin (probe:cm.c_abs(-9)→ 9 both ways; the registered qualified alias resolves to the same extern symbol).
Prior: Phase 5.0 prereq — extern C-variadic tail (xfail 9a2c78d → fix 0fdc821) — the SECOND deferred fn-path prerequisite. BOTH original fn-path prereqs done. The C-variadic ... handling was keyed on the #foreign (foreign_expr)
body shape at two sites — the is_variadic drop in declareFunction
(decl.zig:2097) and the call-site early-out in packVariadicCallArgs
(pack.zig:302). A variadic extern therefore kept its trailing slice param and
slice-packed the extras → garbage at the C ABI (probe: sum_ints(3,10,20,30) →
53316585, not 60). Both gates now also fire for extern_export == .extern_, so a
variadic extern drops the ..args: []T, sets is_variadic, and passes extras
through the C ... slot with default argument promotion — byte-identical to its
#foreign twin. New example 1229 (1229-ffi-extern-cvariadic, JIT #source,
int-sum + double-avg). Suite green (645 corpus / 444 unit, 0 failed).
Prior: Phase 5.0 prereq — visibility-gate equivalence (xfail 717c35d → fix 7d8ba1a) — the first of the two deferred fn-path prerequisites.
The non-transitive C-import visibility gate (isVisible(.c_import_bare),
decl.zig:2249) used to recognise only the legacy #foreign body shape; a bare
extern fn (empty-block body + extern_export == .extern_) escaped the gate via
the body != foreign_expr → return true arm and was caught only by the general
isNameVisible gate — yielding the generic "not visible" wording instead of the
C-specific "C function not visible; add #import" one. Now BOTH lib-less spellings
route to visibleOverEdges, and a library-bound extern LIB (like #foreign LIB)
stays unconditionally visible — so a future fn-decl #foreign→extern migration
is byte-identical at this gate. New cross-module example 1228
(examples/1228-ffi-extern-c-non-transitive, main → b → c) pins the equivalence:
referencing c's lib-less #foreign AND extern twins transitively both produce
the identical C-specific diagnostic. Suite green (644 corpus / 444 unit, 0 failed).
Empirical finding (probe, not yet acted on): the bare-extern twin was NEVER a
silent visibility hole — the general isNameVisible gate already denied it; only
the diagnostic wording diverged. The fix aligns the wording + gate ownership.
Prior: Phase 5.0 (global path) (refactor lock, commit e5ddfbe) — PART B STARTED.
First of the four #foreign parser paths migrated onto the extern AST: the
data-global form name : T #foreign [lib] ["csym"]; now builds the same
extern-named VarDecl (is_extern/extern_lib/extern_name) that postfix
extern already produces, instead of is_foreign/foreign_lib/foreign_name.
Behavior-preserving — lowering coalesces both forms identically
(decl.zig:1119,1127,1141), so zero snapshot churn. The fn-decl, const-with-type,
and runtime-class #foreign paths still build the legacy AST.
Prior: Phase 4 (green) — PHASE 4 COMPLETE → PART A DONE; GATE A→B LOCKED. Four pieces:
(1) GATE A→B unit test (lower.test.zig, lowerSrcToIr helper + "GATE A→B" test) —
asserts #foreign and extern/export lower to byte-identical printed IR for a sample
fn, data global, and Obj-C runtime class. This is the hard gate: Part B may not start
migrating #foreign until it's green. Verified live (negative-probe: mutating one side
fails the assertion). (2) Diagnostic — #foreign + postfix conflict (1174): prefix
#foreign combined with postfix extern/export on an aggregate is now a clean parse
error (was a confusing internal "compiler bug" during class synthesis). (3) Diagnostic
— extern+export mutual exclusion (1175): both keywords on one fn decl is a clean
error (was bare "expected ';'"). (4) Docs: specs.md + readme.md document the three
extern/export axes (fns, globals, aggregates) alongside #foreign (which stays
documented until the Part B cutover). Suite green (643 corpus / 444 unit, 0 fail).
NOTE: extern+callconv redundancy needs no diagnostic — callconv(.c) extern is a
harmless dup (both .c), and any non-.c callconv already errors on its own.
Prior: Phase 3.1 (green) — PHASE 3 COMPLETE. Postfix extern/export on #objc_class/
#jni_class aggregates fully works. parseForeignClassDecl now parses an optional
extern/export modifier in the slot between the ("X") directive args and the {
body (parser.zig:~1409): extern→is_foreign_eff = true (reference an existing runtime
class, == legacy #foreign); export→is_foreign_eff = false (define + register a new sx
class, == bare #objc_class with no #foreign). The modifier maps straight onto the same
is_foreign decision the prefix #foreign already fed the node, so no objc_class.zig/
lowering change was needed — the new surface reuses the existing reference-vs-define path.
Examples: 1348 (objc extern import, dispatches NSObject.alloc().init() → green via
JIT), 1349 (objc export defined class, SxBar.alloc()/bump/get → counter: 2),
1426 (jni extern import, parse-only parse-only ok). Suite green (641 corpus / 443
unit, 0 fail).
Prior: 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 DONE (Phase 3): postfix extern/export on #objc_class/#jni_class
(reference vs define+register). Interplay/diagnostics/docs DONE (Phase 4) + the
A→B GATE IS LOCKED (#foreign ≡ extern/export IR for fn/global/class). PART A
COMPLETE. 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), 1348 (objc extern class), 1349 (objc export class), 1426
(jni extern class), 1174/1175 (interplay diagnostics).
Next step
NONE — the FFI-linkage stream is COMPLETE. extern/export fully replace
#foreign; the keyword is rejected; zero foreign remains in the gated tree (Parts
A + B, Phases 0–9 all done; the 9.4 gate passes). This stream can be archived.
Follow-ups (both DONE 2026-06-15, post-stream polish):
- ✅ Added
extern/exportto the editors/vscode tmLanguage keyword list as astorage.modifier.sxpattern (editors/vscode/syntaxes/sx.tmLanguage.json). - ✅ Dropped the vestigial
RuntimeClassPrefix.is_externfield +parseRuntimeClassDecl'sis_externparam (always-false dead path; the postfixextern/exportkeyword is the sole reference-vs-define decider). Suite green (644 corpus / 442 unit, 0 failed).
--- (historical: the finish-Phase-9 plan, now done) ---
PART B — finish Phase 9: example FILENAME renames + issues/*.md + 9.0/9.4.
(All src/ identifiers + AST node + all comments/docs/example-comments are DONE.)
- FIRST: commit the staged
examples/comment purge (a classifier outage blocked the commit; changes aregit added). Message: "refactor(ffi-linkage): Phase 9.3-examples — purge 'foreign' from example .sx comments". - Example filename rename (git-mv step, snapshot-careful): rename the
*-foreign*example files to extern/runtime names and update every#import/#include/#sourceref + theexpected/<name>.*companions. Families:0729-modules-flat-same-name-foreign(+/a.sx,/b.sxdir),1205-ffi-foreign-global(+-helper),1207-ffi-foreign-global-from-helper,1216-ffi-08-foreign-in-method(+.h/.c),1217-ffi-09-foreign-result-chain(+.h/.c),1218-ffi-foreign-cvariadic(+.c),1219-ffi-foreign,1306-ffi-objc-foreign-class-chained-dispatch,1318-ffi-objc-property-foreign. ⚠ A renamed file with an.ir/.stderrsnapshot that echoes its own path will need that snapshot regenerated (intentional). Pick new names that drop "foreign" (e.g.…-extern-global,…-extern-in-method,…-runtime-class-chained-dispatch). NOTE: keep1176-diagnostics-foreign-removed.sxname (it's the rejection test — fine to keep "foreign"). - issues/*.md (~20) — rewrite writeup prose
#foreign/foreign→extern/runtime-class. 2b.docs/*.md— ALSO in the gate scope (was missed; the gate areas are nowsrc/ library/ examples/ issues/ docs/ specs.md readme.md CLAUDE.md).docs/debugger.mdreferenced the renamedcallForeign(fixed →callExtern, UNCOMMITTED with the staged batch); sweep all ofdocs/for stale renamed-identifier refs +foreignprose. - 9.0 surface decision — RATIFIED (user, 2026-06-15): DELETE the
hash_foreigntoken. The user explicitly flagged token.zig:121 + lsp/server.zig:1693 "this also needs to go" — total purge, accept#foreign→generic error (no friendly migration hint). This is the LAST src change; it is load-bearing → needs a build + test + 1176 regen (do it when mutating Bash is back). Steps:- token.zig: remove
hash_foreignenum (121). - lexer.zig: remove the
.{ "#foreign", Tag.hash_foreign }map entry (91), drop#foreignfrom the directive-list comment (72), DELETE thelex hash_foreigntest (626-631, incl.#foreignx). - parser.zig: remove the 4
self.current.tag == .hash_foreignrejection sites (268 caller / 327 / 419 / 2024) + their messages, AND the 2 lookahead refs (hasFnBody…~3658 + ~3676). ⚠ Decide what#foreignlexes to with no keyword entry (likely an error/unknown-directive token) and confirm the parser surfaces a sane error. - lsp/server.zig: remove the
.hash_foreign,arm (1693). - 1176-diagnostics-foreign-removed: its expected stderr is the now-deleted
"
#foreignhas been removed…" message → it WILL change. Regen 1176's snapshot to whatever the generic post-deletion error is (intentional), OR delete 1176 entirely (its purpose — a friendly rejection — no longer exists). Recommend: keep 1176 as a "#foreignis no longer a directive" regression, regen its snapshot. NOTE: after this, 1176 may still contain#foreignin its SOURCE (the rejected token) — that's the only legitimately-remainingforeignin.sx, OR rename/rework it to avoid even that if the gate must be absolute.
- token.zig: remove
- 9.4 gate —
grep -rniIE 'foreign'over.sx+ docs +src/→ 0 (no keep-list left except possibly 1176's source token +SQLITE_CONSTRAINT_FOREIGNKEY+ vendored C).
--- (historical: the prose-purge plan, now mostly done) ---
PART B — finish Phase 9: the COMMENT / DOC / issues text purge (all src/
identifiers + the AST node are already done; remaining is prose). Lower-risk than the
renames (text only, mostly snapshot-neutral) but needs per-instance reading — NOT a
blind sed. Footprint: src/ ~205 (all comments now), examples/*.sx ~100 comments,
issues/*.md ~20 files, docs (specs/readme/CLAUDE).
Order:
- src/ comments (~200) — reword
foreign→extern/runtime-classto match the renamed identifiers. KEEP: the rejection-message string, thehash_foreigntoken + its#foreignx/lex hash_foreigntest, and any comment that legitimately explains the cutover (it must name#foreignto say it's removed). The ast.zig FnDecl comment still says "mirroring#foreign LIB "csym"(foreign_lib/foreign_name)" — reword. - examples/*.sx comments — the deferred provenance comments (full list in the prior
Next-step revision / git). ⚠ Many CONTRAST
#foreignvsextern— reword to stay coherent. ⚠1219-ffi-foreign.sxprints"foreign-rename: {}"/"=== 15. Foreign ==="to STDOUT — changing those regens its snapshot (intentional).1176/1216legitimately discuss#foreignremoval — keep minimal#foreignmentions where the test IS about it. - issues/*.md (~20) — rewrite writeup prose to
extern/export/runtime-class. - docs — specs.md (rename "Foreign Function Interface" heading → "C Interop"; the
#import c"foreign declarations" prose; the comptime "foreign function calls" line; §3344 "foreign code can't observe the error channel"), readme.md (211-212 the#import cexemption prose), CLAUDE.md (host_ffi#foreign("c")ref →extern; "foreign calls"). - 9.0 surface decision (recommend KEEP
hash_foreigntoken + rejection for a good deprecation — then it + the message + 1176 +#foreignxare permanent gate-exempt keeps). - 9.4 gate —
grep -rniIE 'foreign'over.sx+ docs +src/minus the keep-list → 0. KEEP-LIST (gate-exempt):SQLITE_CONSTRAINT_FOREIGNKEY+ SQLite API names, vendoredlibrary/vendors/sqlite/c/*, thehash_foreigntoken +#foreignxtest + rejection message string,1176-diagnostics-foreign-removed.sx(rejection test must contain it). Gate (scoped per user 2026-06-15):grep -rniIE 'foreign'→ 0 across.sxfiles, all docs, and oursrc/Zig — EXCLUDING the legitimate keeps listed in Last completed step (SQLite API names, vendored C, the rejection test/message +hash_foreigntoken).
Remaining, in suggested (dependency-safe) order:
- 9.1d — eliminate
foreign_expr(last linkage item): migratec_import.zigauto-synthesis to build the extern shape instead of aforeign_exprbody (the Phase 5.0 fn-body flip applied to auto-synth), then delete theforeign_exprAST node +ForeignExpr+ all readers (25). Snapshot-neutral; verify full corpus (the#import cexamples 1215/1216/1217 + sqlite 1624 exercise it). - 9.2 — runtime-class family rename →
Runtime*(Decision 5). The BIG one, do as small per-identifier commits withzig buildafter each (snapshot-neutral). Targets (counts):ForeignClassDecl(65)→RuntimeClassDecl·foreign_path(62)→runtime_path·foreign_class_map(44) ·current_foreign_class(34)/_method·ForeignMethodDecl(31) ·foreign_class_decl(30) ·foreign_expr-gone-by-now ·ForeignClassMember(20) ·ForeignFieldDecl(15) ·ForeignClassDecl.is_foreign(the live one)→e.g.is_reference·parse/tryParseForeignClass*·lowerForeign{Method,Static}Call·findForeign*InChain·resolveForeign*·register*ForeignClass*·*ForeignRefs·ForeignRuntime·current_foreign_class/_method. ⚠ COUPLED .sx↔.zig hook names:jni_main_foreign_path_at/jni_main_foreign_paths/hookJniMainForeignPathAt/foreignPathToJavaName/splitForeignPathspan build.sx + bundle.sx + compiler_hooks.zig- specs.md (2975/3049) — rename all four sites together.
- 9.x-src-comments — the ~200 bare-
foreigncomments insrc/(rename last, since many reference identifiers that 9.1d/9.2 rename; do AFTER those so the comment text matches the new names). - 9.3-examples comments — the deferred
.sxprovenance comments (0716, 0729, 1205/ 1207/1216/1218/1219/1220, 1223-1231, 1306/1308/1315/1318/1320/1321/1331/1332/1348/1349, 1412/1414/1417/1418/1419/1426, 0117/0415, 1140/1141/1125, issues/0030.sx). ⚠ Many CONTRAST#foreignvsextern("no#foreign, no#library") or reference renamed internals — rewrite each to stay coherent (NOT blind sed). ALSO:1219-ffi-foreign.sxprints"foreign-rename: {}"to STDOUT — changing it regens the snapshot (intentional). - 9.3-issues —
issues/*.mdwriteups (~20 files) → rewrite#foreign/foreigntoextern/export/runtime-classper the renames. - 9.3-docs — specs.md (12: rename "Foreign Function Interface" heading → "C Interop";
the
#import c"foreign declarations" prose; the jni_main_foreign_path_at refs with #2), readme.md (2), CLAUDE.md (2: host_ffi#foreign("c")ref + "foreign calls"). - 9.0 surface decision — keep
hash_foreigntoken + rejection (recommended: good deprecation) vs delete it. If kept, the token + the rejection-message string + 1176 are permanent legitimate keeps; the gate excludes them. - 9.4 gate —
grep -rniIE 'foreign'over the gated set minus the keep-list → 0.
- 6.2 verification note (carry forward): the
platform/runtime modules (uikit/android/android_jni) are NOT compiled by any marker'd host corpus test — verify future platform-adjacent migrations via directsx iron importers (1610/1606 compile uikit on host) or import probes, not the corpus alone. - Phases 6–7 (
refactorbatches, empty snapshot diff per batch): migrate the stdlib + examples from#foreignspelling toextern. Because the AST is already unified, this is a pure SOURCE rename (… #foreign LIB "sym";→… extern LIB "sym";for fns; the global/const forms similarly), and IR/output must be byte-identical per batch. NOTE:c_import.zigauto-synthesis (#import c {#include}) still BUILDSforeign_exprbodies internally — that's a compiler-internal path, migrated separately (likely Phase 8/9 area), not a source-spelling change. - Then Phase 8 (cutover: hard-reject the
#foreignkeyword) and Phase 9 (purge allforeignidentifiers — needs Decision 5 [done,Runtime*Class*] + Decision 6 [open, historical carve-out]).
Watch items carried forward:
c_import.zig:262auto-synthesis still emitsforeign_expr— both shapes coexist until that path is migrated; keep everybody.data == .foreign_exprreader dual (checked exhaustively this stream).- const-with-type
#foreignparser path (parser.zig:316) is still onforeign_exprbut DEAD (registers no const); migrate or delete it at the Phase 8 cutover. - The
decl.zig:2055"foreign symbol … already bound" dedupe message is keyword-neutral and fires for both forms — no churn, but reword to "extern" at cutover for consistency. Route the fn-decl#foreignpath so a#foreignfn builds the SAME extern AST that postfixexternalready produces, instead of aforeign_exprbody. This is the highest-value path (the bulk of#foreignusage). Key sub-questions to resolve before/while routing: - The
foreign_exprnode carrieslibrary_ref+c_name; theexternfn carriesextern_export = .extern_+extern_lib+extern_nameon the FnDecl with an empty-block body. Migration = the parser's fn-body#foreignarm (parser.zig:~2062) builds the extern shape (setextern_export, maplibrary_ref→extern_lib,c_name→extern_name) rather than aforeign_expr. - Lowering ALREADY coalesces the two at every fn site checked this stream
(
decl.zig2088/2124/2132/2156/2324/2531 readis_foreign OR extern_export), and the two prereq gates (visibilitydecl.zig:2249, variadicdecl.zig:2097+pack.zig:302) now do too — so the migration should be behavior-preserving with ZERO snapshot churn. VERIFY with the A→B gate test (lower.test.zig) + a fullzig build testafter routing; any churn means a site still readsforeign_exprstructurally and must be coalesced first. - ⚠ This ALSO migrates the const-with-type path implicitly IF it shares the same
foreign_expr→extern reshape (it buildsconst_decl{value=foreign_expr}). Decide: reshape the const path's value node alongside, or leave the dead const path onforeign_expruntil Phase 8 cutover. The const path is dead (see findings below), so leaving it is acceptable; but the parser arm is shared-ish — check whether the fn-body arm change touches it. - Cadence: because the migration is behavior-preserving (no churn), it's a single
refactor/lock commit (like the 5.0 global-path commite5ddfbe), NOT an xfail→fix pair.
Investigation findings (this session — reorder the remaining paths):
- const-with-type (
parser.zig:316,name :: type_expr #foreign …) is a DEAD path: it buildsconst_decl{value = foreign_expr}, butregisterTypedModuleConst(decl.zig:848-851) bails on aforeign_exprvalue (else => return), so it registers no const and emits no symbol — a probe (g_abs :: FP #foreign "abs";) returnsunresolved 'g_abs'at the use site, and the form is used NOWHERE inlibrary/examples/issues. Its migration target is ambiguous because theforeign_exprvalue node is SHARED with the fn-decl path, which isn't migrated yet. Decision (user, 2026-06-14): defer it — migrate it alongside the fn-decl path onceforeign_expr's extern shape is decided. The checkpoint's old "lowest-risk, route to the extern-named shape" note is wrong: the "confirm the value-node lowering path coalesces" gate can't be met (nothing lowers it). - runtime-class prefix (
parser.zig:~1351,#foreign #objc_class/#jni_class) is ALREADY coalesced: both prefix#foreignand postfixexternfeed the singleis_foreign_eff→is_foreignfield onforeign_class_decl(parser.zig:1421-1432), so there is NO Phase 5.0 AST change for it — only the Phase 9.2Runtime*Class*rename remains. Drop it from the Phase 5.0 path list.
So Phase 5.0's real remaining work collapses to: the fn-path variadic prereq, then
the fn-decl #foreign body-marker migration. const-with-type + runtime-class need
no standalone Phase 5.0 commit.
Then Phase 5.1 (lock): unit test that #foreign and extern produce identical IR (the
A→B gate already covers fn/global/class — extend or reuse lowerSrcToIr). Then Phases 6–7
migrate stdlib + examples (empty snapshot diff per batch), Phase 8 cutover (hard-reject
#foreign), Phase 9 total foreign purge.
⚠ CONFIRM BEFORE PART B (Open decisions 5 & 6): runtime-class rename target
(Runtime*Class* recommended vs Extern*Class*) and the historical carve-out (keep
issues/*.md provenance, gate live tree only — recommended). These decide Phase 9 renames;
the plan says confirm before Phase 9, but worth raising with the user before sinking Part B
effort. Also pick up the two Deferred items below at the start of Part B (the
visibility-gate equivalence in particular needs a cross-module example).
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 (carry into Part B): (a) docs — DONE in Phase 4 (specs.md/readme.md
document extern/export; #foreign stays until the Part B cutover); (b) visibility-gate
equivalence — DONE (717c35d/7d8ba1a): the c_import_bare gate now polices a
lib-less extern fn identically to its lib-less #foreign twin (same C-specific
diagnostic); a library-bound extern LIB stays unconditionally visible. Locked by the
cross-module example 1228. (Empirical: the bare-extern twin was never a silent hole — the
general isNameVisible gate already denied it; only the diagnostic wording diverged.)
Open decisions
Part A ratified (bare / postfix / ⇒ callconv(.c) / lib-separate). Part B:
- Decision 5 RATIFIED (user, 2026-06-14): runtime-class rename target =
Runtime*Class*(object-model axis, not linkage). Drives the Phase 9.2 identifier renames. - Decision 6 RATIFIED (user, 2026-06-15): PURGE EVERYTHING — the Phase 9.4 gate is
absolute, including
issues/*.mdwriteups (NOT the recommended keep-provenance default). Every#foreign/foreignreference in the gated tree (src/ library/ examples/ issues/ specs.md readme.md CLAUDE.md) is rewritten toextern/export; provenance lives in git history +(Regression issue NNNN)notes, not the keyword spelling. - Decision 7 RATIFIED (user, 2026-06-15): accept the churn —
#foreign-spelled decls produceextern-worded diagnostics; example 1620 regenerated (only snapshot moved). Aligns with Part B's extern-only end state; the interim oddity is cosmetic and removed at the Phase 8 cutover. Landed in the fn-body flip6b94bb6. (Original framing below.) — interim diagnostic wording for#foreign-spelled decls (gated the fn-body flip). Once the flip lands, a#foreign-spelled fn builds the extern AST, so any diagnostic that reads the unified AST can no longer tell the user wrote#foreignvsextern. Concretely, example 1620's lib-ref error flips "#foreign library…" → "extern library…". Options: (A, recommended) accept the narrow churn — regen 1620 as intentional; it aligns with Part B'sextern-only end state and the interim oddity (#foreignsource → "extern" message) is cosmetic and short-lived (Phase 8 cutover removes#foreign). (B) retain a one-bit surface marker onFnDecl(wrote_foreign) so interim diagnostics stay keyword-accurate (zero churn, small extra plumbing, marker deleted at cutover). Affects only diagnostic wording — IR/behavior identical either way.
Log
- (9.0 + 9.3 + 9.4) PHASE 9 COMPLETE — STREAM DONE; 9.4 GATE PASSES. Deleted the
hash_foreign token (9.0,
dfae690); purged allforeigncomments incl. capital-F (src/examples/docs/editors); renamed 10*-foreign*example files + dedup'd 1218 (b52d424); rewrote 20 issues/*.md + renamed 0043 (b9cfe25). Gate: zeroforeignin the gated tree exceptSQLITE_CONSTRAINT_FOREIGNKEY+ vendored sqlite c/. Suite green (644/443). User flagged several leftover areas mid-purge (docs/, editors/, capital-Foreign comments, the token) — all addressed. - (9.3 src capital-Foreign) Fixed the case-sensitivity gap — my earlier src verify grep
was case-sensitive, missing ~21 capital
Foreign/FOREIGNcomments (Foreign-class→ Runtime-class, Foreign path→Runtime path, Foreign decls→Extern decls, FOREIGN function→ extern function, etc.) across calls/inst/ffi_objc/jni_descriptor/emit_llvm/c_import/ lower.* /ops.zig. All reworded via Edit (comments only — no build impact). UNCOMMITTED (mutating Bash blocked by a classifier outage). After this, srcforeign= ONLY thehash_foreigntoken machinery + 4 rejection messages (the 9.0-delete targets). - (9.0 RATIFIED) User: DELETE the hash_foreign token (total purge). Pending build+regen.
- (9.3 text purge) Purged
foreignfrom allsrc/comments (dc51c4b), specs/readme/ CLAUDE (e99383f), and all example .sx comments (STAGED, commit pending a classifier outage). Fixed 2 user-facing diagnostics (type-annotation error, Android jni_main help). 1219 stdout labels Foreign→Extern (regen). Suite green (646/444). Remaining: example FILENAMES + issues/*.md + the 9.0 token decision + 9.4 gate. - (9.2a-d) RUNTIME-CLASS IDENTIFIER PURGE COMPLETE (Decision 5 →
Runtime*). 9.2a types (3354446), 9.2b fns+state+is_externflag (5c8af6e, fixeda15a868per user: reuseis_externnot newis_reference), 9.2c extern-ref validators →Extern(d27be42), 9.2dforeign_path→runtime_pathcoupled across the build-hook boundary + 37.irregens (8cca3b9).src/now has ZEROforeignidentifiers (only comments + the kept token/message remain). Suite green throughout. - (9.1d) Eliminated the
foreign_exprAST node — migratedc_import.zigauto-synth to the extern shape, deleted the node + all readers.refactor7ffdc7d. - (9.1c) Deleted dead
VarDecl.is_foreign/foreign_lib/foreign_name(global#foreignrejects → write-dead); 3 decl.zig readers simplified tovd.extern_name/vd.is_extern. Snapshot-neutral; suite green (646/444).refactorcd14794. - (9.1b) "foreign symbol already bound" diagnostic + resolveFuncByName panic → "extern
symbol"; intentional 1172 regen. Suite green.
refactorb78e7dd. - (9.1a) PHASE 9 STARTED. 5 collision-free linkage renames (callForeign→callExtern,
marshalForeignArg, dedupeForeignSymbol, foreign_name_map→extern_name_map,
is_foreign_c_api). Snapshot-neutral; suite green.
refactorb838f63. Decision 6 scoped by user: purge.sx+ docs + oursrc/Zig, keep legitimate hits (SQLite API names, vendored C, the rejection test/message + hash_foreign token). - (8.1 cutover) PHASE 8 COMPLETE. Parser hard-rejects
#foreignat all 4 sites (const/global/fn-body viaself.fail; runtime-class viaself.failAtat the caller, new helper); greens xfail 1176. Deleted obsolete 1174 + 1620, the GATE A→B test +lowerSrcToIrhelper; converted the in-source parser test to postfixextern;extern_export→const. specs.md + readme.md drop#foreign. Suite green (646/444).feat!3811311. - (8.0 xfail) Added
1176-diagnostics-foreign-removed.sxpinning the desired rejection. RED (still accepted).test/xfail8180faf. - (8 pre-cutover) Migrated the 4 multi-file example companions Phase 7 missed
(0729/a+b, 1617/c, 1623/mod).
refactord132aab. - (8 pre-cutover) Migrated keyword-neutral diagnostics 1172 (decl→extern, message stays
internal "foreign symbol") + 1228 (→ two foreign-free extern symbols c_abs_one/_two),
intentional snapshot regens reviewed.
refactor720556b. - (8 pre-cutover) Migrated the 7 identity
ffi-foreign-*test decls to extern/export (decls only; comments left for Phase 9.3).refactor2cce6a3. - (7.4 stragglers) PHASE 7 MIGRATABLE WORK COMPLETE. Migrated 16 fn/global examples
(0415/0602/0603/1024/1025/1605/1607-1609/1611/1616/1619/1622/1628/1635/1636)
#foreign→extern; 1607/1608/1616 (unmarked) verified bysx irprobes. 24-file keep-list remains by design (deferred to Phase 8). Suite green (647/444).refactor1a8991a. - (7.3 14xx) Migrated 13 jni examples (1410-1419/1423/1424/1425). 1417 (all-runtimes) hit
a parse-error trap: a
#(objc|jni)_class-only regex leftexternin PREFIX position on#jni_interface/#objc_protocol/#swift_*lines → fixed with the GENERAL#foreign #(\w+)("X") {→#$1("X") extern {rewrite (all such directives accept the postfix modifier, probed). Kept 1426 (comment-only). Suite green.refactor2888f6f. - (7.2 13xx) Migrated 18 obj-c examples (1308/1311-1321/1341-1347): prefix→postfix import
classes + fn markers. Kept identity 1306/1318, comment-only 1332/1348/1349. No 13xx
snapshot asserts on foreign. Suite green.
refactora68f7c2. - (7.1 12xx) PHASE 7 STARTED. Migrated 12 incidental plain-C examples
(1200/1206/1209-1215/1220/1221/1222)
#foreign→extern; output byte-identical, empty snapshot diff, corpus-validated. Established the keep-list policy (see Last completed step): kept 1172/1174/1620/1228 + ffi-foreign-* (1205/1207/1216/1218/1219)- comment-only 1223/1229/1230/1231 for Phase 8. Suite green (647/444).
refactor731fb8d.
- comment-only 1223/1229/1230/1231 for Phase 8. Suite green (647/444).
- (6.5 gpu) PHASE 6 COMPLETE. Migrated
gpu/gles3.sx+gpu/metal.sx(3 sites);library/now#foreign-free (grep -rln '#foreign' library/→ 0). Verified byte-identicalsx iron importers 1610/1606. Suite green (647/444).refactor32a7628. - (6.4 ffi) Migrated
ffi/objc/objc_block/raylib/sdl3/wasm (~51 sites): fn markers + objc.sx's 2 import classes (prefix→postfixextern). objc/objc_block validated by 50 marked 13xx examples; raylib/ffi-sdl3/wasm bysx irprobes pre/post. Empty snapshot diff; suite green (647/444).refactor666a2e2. - (6.3 std) Migrated 11
std/modules (~60 sites): cli/core/fmt/fs/log/net.kqueue/ process/socket/thread/time/trace. All fn-decl markers (bare /libc|tlibLIB ref /libc "csym"rename) →extern …+ 2 comment mentions; no class forms. Host-corpus- exercised → empty snapshot diff validates. Suite green (647/444).refactor59f90d2. - (6.2 platform) Migrated
platform/(uikit/android/android_jni/sdl3, ~64 sites): 30 fn#foreign;→extern;, 34 import classes prefix#foreign #objc/jni_class→ postfix… extern {, 4 definedSx*objc classes →… export {. Behavior- preserving, empty snapshot diff. Verified byte-identicalsx iron uikit importers 1610/1606 + sdl3 probe; android via identical 4-error dedup set (host-only module). Suite green (647/444).refactor2cd5d7b. NOTE: these runtime modules aren't in the marker'd host corpus — verified out-of-band. - (6.1 sqlite) PHASE 6 STARTED. Migrated
vendors/sqlite/sqlite.sx: 97… #foreign sqlib "csym";fn decls →extern sqlib "csym";(+ line-9 comment).extern_libreferences thesqlib#import cunit like#foreign sqlib; IR byte-identical, empty snapshot diff, example 1624 stdout unchanged. Suite green (647/444).refactor410a52e. - (5.1 gate annotate) PHASE 5 COMPLETE. Annotated the A→B gate header
(
lower.test.zig) to record that post-Phase-5.0 the fn/global#foreignpaths build the same extern-named AST → cases 1/2 are structurally (not coincidentally) identical; the gate stays as a regression tripwire. Added fn-rename case 2b (c_abs→"abs",extern_nameaxis), IR-identical per asx irprobe. Test-only, no snapshot churn. Suite green (647/444).test93e7b6f. - (5.0 fn-body flip) PHASE 5.0 PARSER ROUTING COMPLETE. Flipped the fn-body
#foreignparser arm (parser.zig:~2062) onto the extern AST (empty-block body +extern_export = .extern_+ extern_lib/extern_name);extern_exportmadevarso the body arm can route onto it. Updated the parser unit test to assert the extern shape. Behavior-preserving via the four prereqs; only example 1620's lib-ref message churned ("#foreign library"→"extern library", Decision 7, hand-edited). Suite green (647 corpus / 444 unit).refactor6b94bb6. - (5.0 prereq plain-free xfail) Added
1230-ffi-extern-same-name-authors(two flat authors ofabsvalviaextern libc "abs"; theexterntwin of#foreign0729). RED — extern authors wrongly counted as ambiguous (646/1 fail).test/xfail2706521. - (5.0 prereq plain-free fix)
isPlainFreeFn/isPlainFreeFnDeclnow also excludeextern_export == .extern_(external C symbol, no sx body; name-keyed first-wins like#foreign);exportstays plain-free. 1230 green (absval = 7). Suite green (646/444).fix/green3c94c14. - (5.0 prereq lib-ref xfail) Added
1231-ffi-extern-undeclared-lib(extern nosuchunit "abs"— bogus lib ref). RED — compiles silently (extern lib ref unvalidated).test/xfail38c3240. - (5.0 prereq lib-ref fix)
checkForeignRefs(c_import.zig) now reads the lib ref from either spelling (foreign_expr.library_ref OR extern_lib) and names the surface keyword, so 1620 (#foreign) is byte-unchanged and 1231 (extern) gets "extern library … not declared". 1231 green. Suite green (647/444).fix/greenad6aed3. ALL FOUR fn-path prereqs DONE → fn-body flip de-risked; awaiting Decision 7 (interim wording). - (5.0 prereq variadic xfail) Added
1229-ffi-extern-cvariadic(JIT#source, int-sum + double-avg,externC-variadic). Expected snapshot pins the DESIRED correct output. RED (variadicexternslice-packs extras → garbage:sum_ints(3,10,20,30)→ 53316585; doubles → 0.0).test/xfail9a2c78d. - (5.0 prereq variadic fix) Extended the two C-variadic gates — the
is_variadicdrop indeclareFunction(decl.zig:2097) and the early-out inpackVariadicCallArgs(pack.zig:302) — to fire forextern_export == .extern_as well as aforeign_exprbody. 1229 green (60/2.000000). Suite green (645 corpus / 444 unit, 0 failed).fix/green0fdc821. BOTH fn-path prereqs DONE → fn-decl#foreignbody-marker migration unblocked. - (5.0 prereq vis xfail) Added cross-module example
1228-ffi-extern-c-non-transitive(main → b → c). Main references c's lib-less#foreign+externtwins transitively; expected snapshot pins the DESIRED equivalent C-specific diagnostic for both. RED (extern twin gets the generic "not visible" wording — 443/444).test/xfail commit717c35d; the fix greens it. - (5.0 prereq vis fix) Extended
isVisible(.c_import_bare)(decl.zig:2249) to switch on the body: aforeign_exprbody OR anextern_export == .extern_decl with no lib both route tovisibleOverEdges; a library-bound decl stays unconditionally visible. 1228 green — both twins emit "C function not visible". Suite green (644 corpus / 444 unit, 0 failed).fix/green commit7d8ba1a. Deferred prereq (b) CLOSED. Investigation this session also found const-with-type is a DEAD parser path (defer per user) and the runtime-class prefix is already coalesced (no Phase 5.0 change) — see Next step. - (5.0 global) PART B STARTED. Routed the
#foreigndata-global parser path (parser.zig:425) onto the extern-namedVarDecl(is_extern/extern_lib/extern_name) — the same AST postfixexternbuilds. Behavior-preserving (lowering coalesces both atdecl.zig:1119,1127,1141); zero snapshot churn. Suite green (444/444 unit, 643 corpus).refactorlock, commite5ddfbe. Remaining Phase 5.0 paths: const-with-type (316), fn-body (2059, needs visibility+variadic prereqs), runtime-class prefix (1305). - (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. - (3.0) Added
examples/1348-ffi-objc-extern-class.sx(postfixexternon#objc_class, new spelling of#foreign #objc_class). RED (parser:expected '{'after the directive). Hand-authored green snapshots.xfailcommit; 3.1 greens it. - (3.1a) Wired the postfix
extern/exportaggregate slot inparseForeignClassDecl(optional modifier between("X")and{;var is_foreign_effoverrides the passedis_foreign, threaded into theforeign_class_declnode). No lowering change — reuses the existingis_foreignreference-vs-define path. 1348 green. Suite green (639/443).greencommit. PHASE 3 COMPLETE. - (3.1b) Behavior-lock: added
examples/1426-ffi-jni-extern-class.sx(jniextern, parse-only) +examples/1349-ffi-objc-export-class.sx(objcexportdefined class,counter: 2). Both pass against the 3.1a parser change (locked in their own commit per the cadence rule). Suite green (641/443).lockcommit. (Note:-Dupdate-goldensnewline-normalizes empty stderr → reverted unrelated 1226/1227 churn, kept new stderr 0-byte per repo convention; runner normalizes both.) - (4.gate) GATE A→B — added
lowerSrcToIrhelper + "GATE A→B" test tolower.test.zig:#foreign≡extern/exportbyte-identical printed IR for fn / global / Obj-C class. Verified live via negative-probe (mutate one side → assertion fails). Behavior-lock; the equivalence was prototyped first withsx ir(LLVM IR byte-identical for all three). Suite green (641/444).testcommit. - (4.diag1) Added
examples/1174-diagnostics-foreign-postfix-conflict.sx— prefix#foreign- postfix
exporton an aggregate previously surfaced a confusing internal "emitObjcDefinedClassAllocImp … compiler bug".xfail(golden = clean message) →green:parseForeignClassDeclrejects the combo at the postfix keyword (failFmt). Suite green.
- postfix
- (4.docs)
specs.md(new "extern/exportlinkage keywords" subsection after the#foreignFFI docs) +readme.md(C Interop section) document the three axes.docscommit. - (4.diag2) Added
examples/1175-diagnostics-extern-export-conflict.sx—extern exporton one fn decl previously gave bare "expected ';'".xfail(golden = clean message) →green:parseFnDeclrejects a second linkage keyword afterparseOptionalExternExport. Suite green (643/444). PHASE 4 COMPLETE → PART A DONE. - (golden-fix)
-Dupdate-goldenschurn RESOLVED. Root cause was NOT a code bug:writeGoldenalways writescontent + "\n"(empty → canonical 1-byte\n, used by 484 of 489 empty goldens). The 5 churning stderr files [1226/1227/1348/1349/1426] were 0-byte outliers (verify trims trailing\nso both forms passed, but regen always rewrote them to 1-byte). Conformed all 5 to the 1-byte form →-Dupdate-goldensis now idempotent, no more churn. (Separately: a flaky0712-sha256-streaming>10s timeout appears only under concurrentzig buildload — not a real failure; re-run serially.)
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.