Files
sx/current/CHECKPOINT-EXTERN-EXPORT.md
agra 18c43984e1 feat(ffi-linkage): lower extern fns as C imports (Phase 1.1)
Route a bare 'extern' fn declare-only, exactly like a lib-less #foreign
import. Six edits in decl.zig, each mirroring an existing foreign_expr
guard so the empty-block placeholder body is never lowered:

  1. funcWantsImplicitCtx: suppress the implicit __sx_ctx for .extern_
  2. declareFunction: add is_extern_decl
  3. ...and include it in the C-ABI calling-convention promotion
  4. lazyLowerFunction: .extern_ -> declareFunction (declare-only)
  5. lowerFunction: .extern_ in the declare-only guard
  6. lowerFunctionBodyInto: never promote/lower an extern stub

examples/1223 now green: 'extern' abs lowers to 'declare i32 @abs(i32)'
(external linkage, C ABI, no ctx param) and the call resolves against
the default-linked libc -> abs(-7)=7, abs(42)=42. The 1.0b hand-authored
snapshot matched byte-exact (no regen). Suite green (634 corpus / 443
unit). green commit (makes the 1.0b xfail pass; adds no new test).
2026-06-14 13:17:00 +03:00

4.8 KiB

sx extern/export + #foreign retirement — Checkpoint (FFI-linkage stream)

Companion to current/PLAN-EXTERN-EXPORT.md — one merged plan: Part A adds extern/export, Part B migrates #foreign and purges foreign. Update after every commit, one step at a time per the cadence rule.

Last completed step

Phase 1.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.

Known issues

None yet.