# 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_foreign` token entirely (token/lexer/parser/lsp + the lex test); `#foreign` now → 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_path` across the build-hook boundary); `foreign_expr` node eliminated. - **9.3:** purged every `foreign` COMMENT (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) + vendored `library/vendors/sqlite/ c/*` (upstream third-party C). No `foreign`-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_foreign` token + lexer entry + `lex hash_foreign` test (`#foreignx`) + the 4 parser rejection messages ("`#foreign` has been removed…"); `1176-diagnostics-foreign-removed.sx` (its `#foreign` decl + comments ARE the rejection test); `SQLITE_CONSTRAINT_FOREIGNKEY` + vendored `library/vendors/sqlite/c/*`. - **NOT YET DONE:** example FILENAMES (`*-foreign*.sx` + the `0729`/`1205`/`1218`/`1219`/ `1306`/`1318`/`1216`/`1217` families) and their `#import`/`#include`/`#source` path refs + `expected/` files — needs a git-mv rename step; and **`issues/*.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_expr` AST node: migrated `c_import.zig` auto-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*`): types `ForeignClassDecl`→`RuntimeClassDecl` etc.; fns `parseForeignClassDecl`→ `parseRuntimeClassDecl`, `lowerForeignMethodCall`→`lowerRuntimeMethodCall`, …; state `foreign_class_map`→`runtime_class_map`, `foreign_class_decl` variant→ `runtime_class_decl`; the extern-ref validators → `Extern` (linkage, `checkExternRefs`); the reference flag → **`is_extern`** (per user: reuse existing terminology, not a new `is_reference`); and `foreign_path`→`runtime_path` COUPLED across the hook boundary (build.sx accessor `jni_main_runtime_path_at` + the registered hook string + bundle.sx + specs.md), with 37 `.ir` snapshots regenerated for the renamed `@BuildOptions.jni_main_runtime_path_at` declare 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 dead `VarDecl.is_foreign`/`foreign_lib`/ `foreign_name` fields (the global `#foreign` path rejects → write-dead; 3 coalescing readers in decl.zig simplified to `vd.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_foreign` lives on FnDecl?(no — flipped to `extern_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_name` already exist (VarDecl + IR insts) — so the old `foreign_*` 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 by `c_import.zig` auto-synthesis** (`#import c {#include}` synthesizes fn bodies as `foreign_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 the `foreign_expr` node + 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 `refactor`s `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; `#foreign` only 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 form `s/#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 left `extern` in *prefix* position → parse error. All such directives accept the postfix modifier (probed). Bare defined `#objc_class`/`#jni_class` examples (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 a `foreign_expr` body. Behavior-preserving — all four prereqs (visibility, variadic, plain-free, lib-ref) ensure every downstream reader coalesces `is_foreign` with `extern_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 `#foreign` parser 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.zig` auto-synthesis STILL emits `foreign_expr` bodies (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 `#foreign` body but classified an empty-block `extern` fn as a plain free fn — so existing extern fns were wrongly counted in the bare-call ambiguity verdict (example: two same-name `extern libc "abs"` authors errored ambiguous, while the `#foreign` twin 0729 compiles). Both predicates now also exclude `extern_export == .extern_`; `export` (real body) stays plain-free. Example **1230**. - **Prereq 4 (lib-ref validation):** `checkForeignRefs` (c_import.zig) validated only `foreign_expr.library_ref`, so a bogus `extern nosuchunit "abs"` compiled silently while `#foreign nosuchunit` errors (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 (`registerQualifiedFn` decl.zig:2208, namespace gate call.zig:729) — a namespaced `extern` fn resolves identically to its `#foreign` twin (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 + `lazyLowerFunction` promotes the body into it via `foreign_name_map`. Examples **1226** (bare export, C calls `sx_square` → 37/82) + **1227** (`export "triple_c"`, C calls `triple_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/.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`/`export` to the editors/vscode tmLanguage keyword list as a `storage.modifier.sx` pattern (`editors/vscode/syntaxes/sx.tmLanguage.json`). - ✅ Dropped the vestigial `RuntimeClassPrefix.is_extern` field + `parseRuntimeClassDecl`'s `is_extern` param (always-false dead path; the postfix `extern`/`export` keyword 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.) 0. **FIRST: commit the staged `examples/` comment purge** (a classifier outage blocked the commit; changes are `git add`ed). Message: "refactor(ffi-linkage): Phase 9.3-examples — purge 'foreign' from example .sx comments". 1. **Example filename rename** (git-mv step, snapshot-careful): rename the `*-foreign*` example files to extern/runtime names and update every `#import`/`#include`/`#source` ref + the `expected/.*` companions. Families: `0729-modules-flat-same-name-foreign` (+ `/a.sx`,`/b.sx` dir), `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`/`.stderr` snapshot 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: keep `1176-diagnostics-foreign-removed.sx` name (it's the rejection test — fine to keep "foreign"). 2. **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 now `src/ library/ examples/ issues/ docs/ specs.md readme.md CLAUDE.md`). `docs/debugger.md` referenced the renamed `callForeign` (fixed → `callExtern`, UNCOMMITTED with the staged batch); sweep all of `docs/` for stale renamed-identifier refs + `foreign` prose. 3. **9.0 surface decision — RATIFIED (user, 2026-06-15): DELETE the `hash_foreign` token.** 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_foreign` enum (121). - lexer.zig: remove the `.{ "#foreign", Tag.hash_foreign }` map entry (91), drop `#foreign` from the directive-list comment (72), DELETE the `lex hash_foreign` test (626-631, incl. `#foreignx`). - parser.zig: remove the 4 `self.current.tag == .hash_foreign` rejection sites (268 caller / 327 / 419 / 2024) + their messages, AND the 2 lookahead refs (`hasFnBody…` ~3658 + ~3676). ⚠ Decide what `#foreign` lexes 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 "`#foreign` has 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 "`#foreign` is no longer a directive" regression, regen its snapshot. NOTE: after this, 1176 may still contain `#foreign` in its SOURCE (the rejected token) — that's the only legitimately-remaining `foreign` in `.sx`, OR rename/rework it to avoid even that if the gate must be absolute. 4. **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: 1. **src/ comments** (~200) — reword `foreign`→`extern`/`runtime-class` to match the renamed identifiers. KEEP: the rejection-message string, the `hash_foreign` token + its `#foreignx`/`lex hash_foreign` test, and any comment that legitimately explains the cutover (it must name `#foreign` to say it's removed). The ast.zig FnDecl comment still says "mirroring `#foreign LIB "csym"` (foreign_lib/foreign_name)" — reword. 2. **examples/*.sx comments** — the deferred provenance comments (full list in the prior Next-step revision / git). ⚠ Many CONTRAST `#foreign` vs `extern` — reword to stay coherent. ⚠ `1219-ffi-foreign.sx` prints `"foreign-rename: {}"`/`"=== 15. Foreign ==="` to STDOUT — changing those regens its snapshot (intentional). `1176`/`1216` legitimately discuss `#foreign` removal — keep minimal `#foreign` mentions where the test IS about it. 3. **issues/*.md** (~20) — rewrite writeup prose to `extern`/`export`/`runtime-class`. 4. **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 c` exemption prose), CLAUDE.md (host_ffi `#foreign("c")` ref → `extern`; "foreign calls"). 5. **9.0 surface decision** (recommend KEEP `hash_foreign` token + rejection for a good deprecation — then it + the message + 1176 + `#foreignx` are permanent gate-exempt keeps). 6. **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, vendored `library/vendors/sqlite/c/*`, the `hash_foreign` token + `#foreignx` test + 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 `.sx` files, all docs, and our `src/` Zig — EXCLUDING the legitimate keeps listed in Last completed step (SQLite API names, vendored C, the rejection test/message + `hash_foreign` token). Remaining, in suggested (dependency-safe) order: 1. **9.1d — eliminate `foreign_expr`** (last linkage item): migrate `c_import.zig` auto-synthesis to build the extern shape instead of a `foreign_expr` body (the Phase 5.0 fn-body flip applied to auto-synth), then delete the `foreign_expr` AST node + `ForeignExpr` + all readers (25). Snapshot-neutral; verify full corpus (the `#import c` examples 1215/1216/1217 + sqlite 1624 exercise it). 2. **9.2 — runtime-class family rename → `Runtime*` (Decision 5).** The BIG one, do as small per-identifier commits with `zig build` after 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`/`splitForeignPath` span build.sx + bundle.sx + compiler_hooks.zig + specs.md (2975/3049) — rename all four sites together. 3. **9.x-src-comments** — the ~200 bare-`foreign` comments in `src/` (rename last, since many reference identifiers that 9.1d/9.2 rename; do AFTER those so the comment text matches the new names). 4. **9.3-examples comments** — the deferred `.sx` provenance 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 `#foreign` vs `extern` ("no `#foreign`, no `#library`") or reference renamed internals — rewrite each to stay coherent (NOT blind sed). ALSO: `1219-ffi-foreign.sx` prints `"foreign-rename: {}"` to STDOUT — changing it regens the snapshot (intentional). 5. **9.3-issues** — `issues/*.md` writeups (~20 files) → rewrite `#foreign`/`foreign` to `extern`/`export`/`runtime-class` per the renames. 6. **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"). 7. **9.0 surface decision** — keep `hash_foreign` token + rejection (recommended: good deprecation) vs delete it. If kept, the token + the rejection-message string + 1176 are permanent legitimate keeps; the gate excludes them. 8. **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 direct `sx ir` on importers (1610/1606 compile uikit on host) or import probes, not the corpus alone. - **Phases 6–7** (`refactor` batches, empty snapshot diff per batch): migrate the stdlib + examples from `#foreign` spelling to `extern`. 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.zig` auto-synthesis (`#import c {#include}`) still BUILDS `foreign_expr` bodies 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 `#foreign` keyword) and **Phase 9** (purge all `foreign` identifiers — needs Decision 5 [done, `Runtime*Class*`] + Decision 6 [open, historical carve-out]). **Watch items carried forward:** - `c_import.zig:262` auto-synthesis still emits `foreign_expr` — both shapes coexist until that path is migrated; keep every `body.data == .foreign_expr` reader dual (checked exhaustively this stream). - const-with-type `#foreign` parser path (`parser.zig:316`) is still on `foreign_expr` but 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 `#foreign` path so a `#foreign` fn builds the SAME extern AST that postfix `extern` already produces, instead of a `foreign_expr` body. This is the highest-value path (the bulk of `#foreign` usage). Key sub-questions to resolve before/while routing: - The `foreign_expr` node carries `library_ref` + `c_name`; the `extern` fn carries `extern_export = .extern_` + `extern_lib` + `extern_name` on the FnDecl with an empty-block body. Migration = the parser's fn-body `#foreign` arm (`parser.zig:~2062`) builds the extern shape (set `extern_export`, map `library_ref→extern_lib`, `c_name→extern_name`) rather than a `foreign_expr`. - Lowering ALREADY coalesces the two at every fn site checked this stream (`decl.zig` 2088/2124/2132/2156/2324/2531 read `is_foreign OR extern_export`), and the two prereq gates (visibility `decl.zig:2249`, variadic `decl.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 full `zig build test` after routing; any churn means a site still reads `foreign_expr` structurally and must be coalesced first. - ⚠ This ALSO migrates the **const-with-type** path implicitly IF it shares the same `foreign_expr`→extern reshape (it builds `const_decl{value=foreign_expr}`). Decide: reshape the const path's value node alongside, or leave the dead const path on `foreign_expr` until 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 commit `e5ddfbe`), 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 builds `const_decl{value = foreign_expr}`, but `registerTypedModuleConst` (`decl.zig:848-851`) bails on a `foreign_expr` value (`else => return`), so it registers no const and emits no symbol — a probe (`g_abs :: FP #foreign "abs";`) returns `unresolved 'g_abs'` at the use site, and the form is used NOWHERE in `library`/`examples`/`issues`. Its migration target is ambiguous because the `foreign_expr` value 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 once `foreign_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 `#foreign` and postfix `extern` feed the single `is_foreign_eff`→`is_foreign` field on `foreign_class_decl` (`parser.zig:1421-1432`), so there is NO Phase 5.0 AST change for it — only the Phase 9.2 `Runtime*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/*.md` writeups (NOT the recommended keep-provenance default). Every `#foreign`/`foreign` reference in the gated tree (`src/ library/ examples/ issues/ specs.md readme.md CLAUDE.md`) is rewritten to `extern`/`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 produce `extern`-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 flip `6b94bb6`. (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 `#foreign` vs `extern`. 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's `extern`-only end state and the interim oddity (`#foreign` source → "extern" message) is cosmetic and short-lived (Phase 8 cutover removes `#foreign`). **(B)** retain a one-bit surface marker on `FnDecl` (`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 all `foreign` comments incl. capital-F (src/examples/docs/editors); renamed 10 `*-foreign*` example files + dedup'd 1218 (`b52d424`); rewrote 20 issues/*.md + renamed 0043 (`b9cfe25`). Gate: zero `foreign` in the gated tree except `SQLITE_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`/`FOREIGN` comments (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, src `foreign` = ONLY the `hash_foreign` token 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 `foreign` from all `src/` 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_extern` flag (`5c8af6e`, fixed `a15a868` per user: reuse `is_extern` not new `is_reference`), 9.2c extern-ref validators → `Extern` (`d27be42`), 9.2d `foreign_path`→`runtime_path` coupled across the build-hook boundary + 37 `.ir` regens (`8cca3b9`). `src/` now has ZERO `foreign` identifiers (only comments + the kept token/message remain). Suite green throughout. - (9.1d) Eliminated the `foreign_expr` AST node — migrated `c_import.zig` auto-synth to the extern shape, deleted the node + all readers. `refactor` `7ffdc7d`. - (9.1c) Deleted dead `VarDecl.is_foreign`/`foreign_lib`/`foreign_name` (global `#foreign` rejects → write-dead); 3 decl.zig readers simplified to `vd.extern_name`/`vd.is_extern`. Snapshot-neutral; suite green (646/444). `refactor` `cd14794`. - (9.1b) "foreign symbol already bound" diagnostic + resolveFuncByName panic → "extern symbol"; intentional 1172 regen. Suite green. `refactor` `b78e7dd`. - (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. `refactor` `b838f63`. Decision 6 scoped by user: purge `.sx` + docs + our `src/` 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 `#foreign` at all 4 sites (const/global/fn-body via `self.fail`; runtime-class via `self.failAt` at the caller, new helper); greens xfail 1176. Deleted obsolete 1174 + 1620, the GATE A→B test + `lowerSrcToIr` helper; converted the in-source parser test to postfix `extern`; `extern_export` → `const`. specs.md + readme.md drop `#foreign`. Suite green (646/444). `feat!` `3811311`. - (8.0 xfail) Added `1176-diagnostics-foreign-removed.sx` pinning the desired rejection. RED (still accepted). `test`/xfail `8180faf`. - (8 pre-cutover) Migrated the 4 multi-file example companions Phase 7 missed (0729/a+b, 1617/c, 1623/mod). `refactor` `d132aab`. - (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. `refactor` `720556b`. - (8 pre-cutover) Migrated the 7 identity `ffi-foreign-*` test decls to extern/export (decls only; comments left for Phase 9.3). `refactor` `2cce6a3`. - (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 by `sx ir` probes. 24-file keep-list remains by design (deferred to Phase 8). Suite green (647/444). `refactor` `1a8991a`. - (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 left `extern` in 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. `refactor` `2888f6f`. - (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. `refactor` `a68f7c2`. - (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). `refactor` `731fb8d`. - (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-identical `sx ir` on importers 1610/1606. Suite green (647/444). `refactor` `32a7628`. - (6.4 ffi) Migrated `ffi/` objc/objc_block/raylib/sdl3/wasm (~51 sites): fn markers + objc.sx's 2 import classes (prefix→postfix `extern`). objc/objc_block validated by 50 marked 13xx examples; raylib/ffi-sdl3/wasm by `sx ir` probes pre/post. Empty snapshot diff; suite green (647/444). `refactor` `666a2e2`. - (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`|`tlib` LIB ref / `libc "csym"` rename) → `extern …` + 2 comment mentions; no class forms. Host-corpus- exercised → empty snapshot diff validates. Suite green (647/444). `refactor` `59f90d2`. - (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 defined `Sx*` objc classes → `… export {`. Behavior- preserving, empty snapshot diff. Verified byte-identical `sx ir` on uikit importers 1610/1606 + sdl3 probe; android via identical 4-error dedup set (host-only module). Suite green (647/444). `refactor` `2cd5d7b`. 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_lib` references the `sqlib` `#import c` unit like `#foreign sqlib`; IR byte-identical, empty snapshot diff, example 1624 stdout unchanged. Suite green (647/444). `refactor` `410a52e`. - (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 `#foreign` paths 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_name` axis), IR-identical per a `sx ir` probe. Test-only, no snapshot churn. Suite green (647/444). `test` `93e7b6f`. - (5.0 fn-body flip) **PHASE 5.0 PARSER ROUTING COMPLETE.** Flipped the fn-body `#foreign` parser arm (`parser.zig:~2062`) onto the extern AST (empty-block body + `extern_export = .extern_` + extern_lib/extern_name); `extern_export` made `var` so 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). `refactor` `6b94bb6`. - (5.0 prereq plain-free xfail) Added `1230-ffi-extern-same-name-authors` (two flat authors of `absval` via `extern libc "abs"`; the `extern` twin of `#foreign` 0729). RED — extern authors wrongly counted as ambiguous (646/1 fail). `test`/xfail `2706521`. - (5.0 prereq plain-free fix) `isPlainFreeFn`/`isPlainFreeFnDecl` now also exclude `extern_export == .extern_` (external C symbol, no sx body; name-keyed first-wins like `#foreign`); `export` stays plain-free. 1230 green (`absval = 7`). Suite green (646/444). `fix`/green `3c94c14`. - (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`/xfail `38c3240`. - (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`/green `ad6aed3`. **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, `extern` C-variadic). Expected snapshot pins the DESIRED correct output. RED (variadic `extern` slice-packs extras → garbage: `sum_ints(3,10,20,30)` → 53316585; doubles → 0.0). `test`/xfail `9a2c78d`. - (5.0 prereq variadic fix) Extended the two C-variadic gates — the `is_variadic` drop in `declareFunction` (`decl.zig:2097`) and the early-out in `packVariadicCallArgs` (`pack.zig:302`) — to fire for `extern_export == .extern_` as well as a `foreign_expr` body. 1229 green (`60` / `2.000000`). Suite green (645 corpus / 444 unit, 0 failed). `fix`/green `0fdc821`. **BOTH fn-path prereqs DONE → fn-decl `#foreign` body-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` + `extern` twins 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 commit `717c35d`; the fix greens it. - (5.0 prereq vis fix) Extended `isVisible(.c_import_bare)` (`decl.zig:2249`) to switch on the body: a `foreign_expr` body OR an `extern_export == .extern_` decl with no lib both route to `visibleOverEdges`; 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 commit `7d8ba1a`. **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 `#foreign` data-global parser path (`parser.zig:425`) onto the extern-named `VarDecl` (`is_extern`/`extern_lib`/ `extern_name`) — the same AST postfix `extern` builds. Behavior-preserving (lowering coalesces both at `decl.zig:1119,1127,1141`); zero snapshot churn. Suite green (444/444 unit, 643 corpus). `refactor` lock, commit `e5ddfbe`. 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_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. - (1.0a) Wired fn-path extern parsing (`parseFnDecl` + both lookahead predicates) + added `FnDecl.extern_lib`/`extern_name` + `VarDecl.extern_lib` per user feedback (decision 4 revised: extern carries an optional lib axis). Unconsumed by lowering. Suite green (443/633). `lock` commit. - (1.0b) Added `examples/1223-ffi-extern-fn.sx` + hand-authored success snapshots. RED (634 ran, 1 failed — sema `body produces no value`). `xfail` commit; 1.1 greens it. - (1.1) Wired extern fn lowering (6 edits in `decl.zig`, all declare-only routing mirroring `foreign_expr`): `funcWantsImplicitCtx` + `declareFunction` cc + `lazyLowerFunction`/`lowerFunction`/`lowerFunctionBodyInto` guards. 1223 green; `declare i32 @abs(i32)` (C ABI, no ctx). Suite green (634/443). `green` commit. - (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"` after `extern` not yet accepted). `xfail`; 1.2b greens it. (Also recovered a formatter-clobbered `parser.zig` — see Known issues.) - (1.2b) `parseFnDecl` parses the optional `[LIB] ["csym"]` tail into `extern_lib`/`extern_name`; `declareFunction` unifies 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). `green` commit. extern_lib parsed+stored (lib linking stays the `#library` axis). - (1.2c) Added `examples/1225-ffi-extern-global.sx` (`__stdinp : *void extern;`, mirrors `#foreign` global 1205) + success snapshot. RED (636 ran, 1 failed — parse error: var-decl `extern` not accepted). `xfail`; 1.2d greens it. - (1.2d) Parser `kw_extern` branch in the var-decl path (`[LIB] ["csym"]` → `is_extern`/`extern_lib`/`extern_name`) + `registerTopLevelGlobal`/`globalInitValue` consume `is_extern`. 1225 green (`@__stdinp = external global ptr`). Suite green (636/443). `green` commit. **PHASE 1 COMPLETE** — `extern` fns + globals fully work. - (JIT spike) User-requested feasibility investigation of C→sx-by-name in `sx run` (JIT). Verdict: feasible via `LLVMOrcLLJITAddObjectFile` (C objects into the ORC JITDylib) — proven by a throwaway spike — but blocked by JITLink MachO TLV handling (`sx_trace.c`'s `_Thread_local` SIGABRTs without the ORC `MachOPlatform`). Own future milestone (see Next step). Spike reverted; no commit. - (2.0) Added the **AOT corpus mode** (`expected/.aot` → `sx build` + execute) to `corpus_run.test.zig` + retired `tests/run_examples.sh` (verify-step.sh/CLAUDE.md updated) + `examples/1226-ffi-export-fn.{sx,c,h}` (C calls `sx_square` back). RED (AOT link fails: `_sx_square` undefined — export not lowered). `xfail`; 2.1 greens it. - (2.1) Filled export gaps i/ii/iv in `decl.zig` (`.external` linkage + `.c` cc on both define paths; `funcWantsImplicitCtx` false for any non-`.none` modifier) + force-lower export fns as roots in `lowerMainAndComptime`. 1226 green via AOT (37/82). Suite green (637/443). `green` commit. - (2.2a) Added `examples/1227-ffi-export-fn-rename.sx` (`export "triple_c"`, C calls `triple_c`). RED (define path emits `@sx_triple`, ignores `extern_name` → C ref undefined). `xfail`; 2.2b greens it. - (2.2b) `declareFunction` rename branch fires for `export` (stub under C name + sx→C in `foreign_name_map`); `lazyLowerFunction` resolves 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). `green` commit. **PHASE 2 COMPLETE** — `export` fully works. - (3.0) Added `examples/1348-ffi-objc-extern-class.sx` (postfix `extern` on `#objc_class`, new spelling of `#foreign #objc_class`). RED (parser: `expected '{'` after the directive). Hand-authored green snapshots. `xfail` commit; 3.1 greens it. - (3.1a) Wired the postfix `extern`/`export` aggregate slot in `parseForeignClassDecl` (optional modifier between `("X")` and `{`; `var is_foreign_eff` overrides the passed `is_foreign`, threaded into the `foreign_class_decl` node). No lowering change — reuses the existing `is_foreign` reference-vs-define path. 1348 green. Suite green (639/443). `green` commit. **PHASE 3 COMPLETE.** - (3.1b) Behavior-lock: added `examples/1426-ffi-jni-extern-class.sx` (jni `extern`, parse-only) + `examples/1349-ffi-objc-export-class.sx` (objc `export` defined class, `counter: 2`). Both pass against the 3.1a parser change (locked in their own commit per the cadence rule). Suite green (641/443). `lock` commit. (Note: `-Dupdate-goldens` newline-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 `lowerSrcToIr` helper + "GATE A→B" test to `lower.test.zig`: `#foreign` ≡ `extern`/`export` byte-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 with `sx ir` (LLVM IR byte-identical for all three). Suite green (641/444). `test` commit. - (4.diag1) Added `examples/1174-diagnostics-foreign-postfix-conflict.sx` — prefix `#foreign` + postfix `export` on an aggregate previously surfaced a confusing internal "emitObjcDefinedClassAllocImp … compiler bug". `xfail` (golden = clean message) → `green`: `parseForeignClassDecl` rejects the combo at the postfix keyword (`failFmt`). Suite green. - (4.docs) `specs.md` (new "`extern`/`export` linkage keywords" subsection after the `#foreign` FFI docs) + `readme.md` (C Interop section) document the three axes. `docs` commit. - (4.diag2) Added `examples/1175-diagnostics-extern-export-conflict.sx` — `extern export` on one fn decl previously gave bare "expected ';'". `xfail` (golden = clean message) → `green`: `parseFnDecl` rejects a second linkage keyword after `parseOptionalExternExport`. Suite green (643/444). **PHASE 4 COMPLETE → PART A DONE.** - (golden-fix) **`-Dupdate-goldens` churn RESOLVED.** Root cause was NOT a code bug: `writeGolden` always writes `content + "\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 `\n` so both forms passed, but regen always rewrote them to 1-byte). Conformed all 5 to the 1-byte form → `-Dupdate-goldens` is now idempotent, no more churn. (Separately: a flaky `0712-sha256-streaming` >10s timeout appears only under concurrent `zig build` load — not a real failure; re-run serially.) ## Known issues - **Workflow hazard (1.2):** an editor format-on-save (or `zig fmt`) clobbered the working-tree `src/parser.zig` between commits — it reformatted one-liners AND silently dropped my `hasFnBodyAfterArrow` extern edit, reverting 1223 to a parse error. Recovered with `git 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.**