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).
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:
extern LIB "csym"rename for fns — extendparseOptionalExternExport()(orparseFnDecl) to parse the optionalLIBident +"csym"string after the keyword intoFnDecl.extern_lib/extern_name; consume them indeclareFunction(mirror the#foreignc_name block atdecl.zig:~2119: declare under the C name, map sx→C inforeign_name_map/dedupe). New example renaming a libc symbol (e.g.c_abs :: (n: i32) -> i32 extern "abs";).- extern-global
g : T extern [LIB] ["csym"];— parse path atparser.zig:425(the var-decl with type annotation): accept postfixextern→ setVarDecl.is_extern/extern_lib/extern_name; lower like the#foreignglobal (decl.zig:~1115-1137,.is_extern = vd.is_foreign). New example mirroringexamples/1205-ffi-foreign-globalwithextern.
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_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.
Known issues
None yet.