# sx `extern`/`export` + `#foreign` retirement — Checkpoint (FFI-linkage stream) Companion to `current/PLAN-EXTERN-EXPORT.md` — one merged plan: **Part A** adds `extern`/`export`, **Part B** migrates `#foreign` and purges `foreign`. Update after every commit, one step at a time per the cadence rule. ## Last completed step **Phase 1.1** (green) — wired extern fn lowering in `decl.zig`; example **1223 now green**, full suite green (634 corpus / 443 unit, 0 fail). A bare `extern` fn lowers exactly like a lib-less `#foreign` import: `declare i32 @abs(i32) #0` — external linkage, C ABI, NO `__sx_ctx` param; calls emit `call i32 @abs(i32 -7)` and resolve against the default-linked libc. Six edits, all routing `extern` declare-only (mirroring the `foreign_expr` guards): (1) `funcWantsImplicitCtx` suppresses ctx for `.extern_`; (2) `declareFunction` adds `is_extern_decl` and (3) includes it in the C-ABI promotion; (4) `lazyLowerFunction` routes `.extern_`→declare-only; (5) `lowerFunction` declare-only guard; (6) `lowerFunctionBodyInto` never promotes/lowers an extern stub. Hand-authored 1.0b snapshot matched byte-exact — no regen needed. No `.ir` snapshot added (the trivial `declare i32 @abs(i32)` doesn't warrant a 1000-line full-prelude dump; behavioral `.stdout` already catches ctx/linkage regressions). ## 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` FUNCTIONS WORK** (import; bare form, no rename) — parse + lower complete, behavior-equivalent to a lib-less `#foreign` fn. Still TODO in Phase 1: the `extern LIB "csym"` lib/rename axis (fields exist, unconsumed) and the extern-global form. `export` not started (Phase 2). Part B `foreign` footprint to purge: 643 lines / ~57 identifiers in `src/` + 28 doc lines. End-state invariant: **zero `foreign`** (Phase 9.4 gate). **Done**: 0.0 tokens, 0.1 AST/parser plumbing, 1.0a fn-path parsing + lib/name fields, 1.0b xfail example, 1.1 fn lowering (green). ## Next step **Phase 1.2** (green) — two parts, each its own xfail→green or behavior-lock: 1. **`extern LIB "csym"` rename for fns** — extend `parseOptionalExternExport()` (or `parseFnDecl`) to parse the optional `LIB` ident + `"csym"` string after the keyword into `FnDecl.extern_lib`/`extern_name`; consume them in `declareFunction` (mirror the `#foreign` c_name block at `decl.zig:~2119`: declare under the C name, map sx→C in `foreign_name_map`/dedupe). New example renaming a libc symbol (e.g. `c_abs :: (n: i32) -> i32 extern "abs";`). 2. **extern-global `g : T extern [LIB] ["csym"];`** — parse path at `parser.zig:425` (the var-decl with type annotation): accept postfix `extern` → set `VarDecl.is_extern`/`extern_lib`/`extern_name`; lower like the `#foreign` global (`decl.zig:~1115-1137`, `.is_extern = vd.is_foreign`). New example mirroring `examples/1205-ffi-foreign-global` with `extern`. Stop at end of Phase 1 (do NOT start Phase 2 `export` or Part B migration). Then the A→B gate: a unit test that `#foreign` and `extern` lower to identical IR. ## Open decisions Part A ratified (bare / postfix / `⇒ callconv(.c)` / lib-separate). Part B (confirm before Phase 9): runtime-class rename target — `Runtime*Class*` (recommended); historical carve-out — keep `issues/*.md` provenance, gate the live tree only. ## Log - (init) Plan written; FFI-linkage stream opened. - (merge) Folded FOREIGN-MIGRATION in as Part B; deleted the split plan + checkpoint. - (0.0) Added `kw_extern`/`kw_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). ## 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.**