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, so the empty
snapshot diff is direct validation. Suite green (647 corpus / 444 unit, 0
failed).
Pure source rename across uikit/android/android_jni/sdl3 (~64 #foreign sites):
- 30 fn decls '… #foreign;' → '… extern;'
- 34 import runtime classes '#foreign #objc_class/#jni_class("X") {' →
'#objc_class/#jni_class("X") extern {' (prefix → postfix modifier)
- 4 defined Sx* obj-c classes '#objc_class("X") {' → '… export {'
Behavior-preserving (AST already unified post-Phase-5.0). Verified byte-identical
IR via 'sx ir' on the uikit importers 1610 + 1606 (which compile uikit incl. the
4 defined Sx* classes on host) and an sdl3 probe; android.sx (host-incompatible,
only compiles under OS==.android) verified by an identical 4-error dedup set (the
keyword-neutral 'foreign symbol already bound' message is unchanged). Empty
snapshot diff; suite green (647 corpus / 444 unit, 0 failed).
Pure source rename: all 97 'sqlite3_* ... #foreign sqlib "csym";' fn decls
→ 'extern sqlib "csym";' (+ the one stale header-comment reference). The
extern_lib axis references the 'sqlib' #import c unit identically to #foreign
sqlib, so IR/output is byte-identical. Empty snapshot diff; example 1624
(vendor-sqlite-module) stdout byte-unchanged. Suite green (647 corpus / 444
unit, 0 failed).
Phase 5.0 flipped the fn-decl and data-global #foreign parser paths onto the
same extern-named AST that postfix extern produces, so the A→B gate's fn/global
cases are now STRUCTURALLY identical (guaranteed by construction, not empirically
equal). Annotate the gate header to record this and keep it as a regression
tripwire against a future reader re-diverging the two spellings or a revert of
the flip. Add a fn-rename case (extern_name axis: c_abs -> "abs") to broaden
coverage beyond bare import. Test-only; suite green (647 corpus / 444 unit, 0
failed). PHASE 5.1 COMPLETE → PART B Phase 5 done; next Phase 6 (migrate stdlib).
The fn-body `#foreign [LIB] ["csym"]` marker now builds the SAME shape postfix
`extern` produces — extern_export = .extern_ + extern_lib/extern_name + an
empty-block body — instead of a `foreign_expr` body. With all four prereqs
landed (visibility, variadic, plain-free classification, lib-ref validation),
every downstream reader coalesces is_foreign with extern_export, so the IR and
runtime behavior are byte-identical (full corpus + the A->B gate stay green).
The surface keyword is no longer on the AST, so a `#foreign`-spelled decl now
yields `extern`-worded diagnostics — the single accepted churn (Decision 7):
example 1620's lib-ref error flips '#foreign library' -> 'extern library'.
Parser-surface diagnostics (conflict/expected-token) fire on the literal keyword
and are unaffected. c_import auto-synthesis still emits foreign_expr bodies (not
this step), so both shapes still coexist. Parser unit test updated to assert the
extern shape.
647 corpus / 444 unit, 0 failed. The const-with-type (dead) + runtime-class
(already coalesced) paths need no flip — Phase 5.0 parser routing is complete.
Plain-free classification + extern lib-ref validation closed (the 3rd and
4th extern/#foreign divergences). All four fn-path prereqs now done. The
fn-decl #foreign->extern flip is scoped: IR zero-churn, only example 1620's
lib-ref wording churns. Records Decision 7 (interim diagnostic wording) as
the one gate before executing the flip.
checkForeignRefs now reads a library reference from either spelling — the
legacy #foreign body (foreign_expr.library_ref) or the new extern keyword
(extern_lib) — and validates both against the declared #library / #import c
units. The diagnostic names the surface keyword the user wrote (#foreign vs
extern), so example 1620 (#foreign) is byte-unchanged and example 1231
(extern) gets the parallel 'extern library ... not declared'. Greens 1231.
647 corpus / 444 unit, 0 failed.
An `extern LIB "csym"` ref must name a declared #library / #import c unit,
like its `#foreign LIB` twin (example 1620). Today checkForeignRefs reads
only foreign_expr.library_ref and skips the extern keyword's extern_lib, so
a bogus `extern nosuchunit "abs"` compiles silently (the symbol resolves
via the default image and runs). Expected pins the DESIRED compile-time
diagnostic; the next commit extends checkForeignRefs to green it. Fourth
extern/#foreign divergence and a prerequisite for the fn-decl migration.
647 corpus (1231 xfail), 444 unit.
isPlainFreeFn / isPlainFreeFnDecl excluded a #foreign body but classified
an empty-block extern fn as a plain free function, so existing extern fns
were wrongly counted in the bare-call ambiguity verdict (and eligible for
the out-of-line-slot / shadow-author pass). Both predicates now also
exclude extern_export == .extern_ (an external C symbol with no
sx-lowerable body, name-keyed first-wins dispatch like #foreign); export
keeps a real body and stays plain-free. Greens example 1230 — same-name
extern authors compile like their #foreign twins (0729).
646 corpus / 444 unit, 0 failed.
Two flat imports each declare `absval` via `extern libc "abs"` (the
`extern` twin of example 0729's `#foreign` form). Like its #foreign twin,
this must compile + run (prints 7), not error as an ambiguous bare-call
collision.
Today `isPlainFreeFn` / `isPlainFreeFnDecl` exclude a `#foreign` body but
classify an empty-block `extern` fn as a plain free function, so the two
extern authors ARE counted in the bare-call ambiguity verdict and the call
errors. A third extern/#foreign divergence (after visibility + variadic)
and a prerequisite for migrating the fn-decl `#foreign` path onto `extern`.
646 corpus (1230 xfail), 444 unit.
Next step is the fn-decl #foreign body-marker migration onto extern
(behavior-preserving single refactor commit; lowering + both prereq
gates already coalesce is_foreign with extern_export).
Two gates were keyed on the `#foreign` (foreign_expr) body shape only:
- declareFunction: the is_variadic drop (decl.zig) — a variadic extern
kept its trailing slice param in the IR signature.
- packVariadicCallArgs: the call-site early-out (pack.zig) — extras were
slice-packed instead of passed through the C `...` slot.
Both now also fire for `extern_export == .extern_`, so a variadic
`extern` drops the trailing `..args: []T`, sets is_variadic, and passes
extras through the C ABI with default argument promotion — byte-identical
to its `#foreign` twin. Greens example 1229.
645 corpus / 444 unit, 0 failed.
A trailing `..args: []T` on an `extern` fn must map to the C `...` tail
like its `#foreign` twin (example 1218). Today the variadic handling in
both declareFunction (is_variadic drop) and packVariadicCallArgs
(call-site early-out) is gated on `#foreign` only, so a variadic
`extern` keeps the trailing slice param and slice-packs the extras —
garbage at the C ABI (probe: sum_ints(3,10,20,30) → 53316585, not 60).
Example 1229 pins the DESIRED correct output; the next commit extends
both gates to cover extern and greens it. Prerequisite for migrating the
fn-decl `#foreign` path onto `extern`.
645 corpus (1229 xfail), 444 unit.
- Mark deferred prereq (b) visibility-gate equivalence CLOSED (1228).
- Record const-with-type as a dead path (deferred per user) and the
runtime-class prefix as already-coalesced (no Phase 5.0 change).
- Next step is the fn-path variadic prerequisite.
The non-transitive C-import visibility gate (`isVisible(.c_import_bare)`)
only recognised the legacy `#foreign` body shape; a bare `extern` fn
(empty-block body + extern_export == .extern_) escaped 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 a library-bound `#foreign LIB`) stays
unconditionally visible. This makes a future fn-decl `#foreign`->`extern`
migration byte-identical at this gate. Greens example 1228.
644 corpus / 444 unit, 0 failed.
Cross-module example (main → b → c) referencing c's lib-less C imports
transitively. The non-transitive C-import gate (lower/decl.zig
c_import_bare) must police the legacy `#foreign` form and the new
`extern` keyword IDENTICALLY — same 'C function not visible' diagnostic,
not the generic top-level-name wording. Today the extern twin escapes the
c_import_bare gate (body is an empty block, not foreign_expr) and is only
caught by the general isNameVisible gate, yielding the generic message.
Expected snapshot pins the DESIRED equivalent wording; the next commit
aligns the gate to green it. Prerequisite for migrating the fn-decl
`#foreign` path onto `extern`.
443/444 corpus (1228 xfail), 444 unit.
Part B begins: `#foreign` becomes an alias for `extern`. First of the four
`#foreign` parser paths to migrate — the data-global form
(`name : T #foreign [lib] ["csym"];`). It now builds the SAME extern-named
VarDecl (`is_extern`/`extern_lib`/`extern_name`) that the postfix `extern`
global path already produces, instead of `is_foreign`/`foreign_lib`/`foreign_name`.
Behavior-preserving: lowering coalesces the two forms identically — the symbol
name is `extern_name orelse foreign_name orelse name` (decl.zig:1119), and both
`is_foreign` and `is_extern` feed the same `.is_extern` IR flag + early-return
(decl.zig:1127,1141). The A->B gate already proved fn/global/class lower to
byte-identical IR, so the corpus locks this with zero snapshot churn.
Suite green: 10/10 steps, 444/444 unit, 643 corpus, 0 failed.
The fn-decl, const-with-type, and runtime-class `#foreign` paths still build the
legacy AST; they migrate next (the fn path needs the deferred visibility-gate +
variadic alignment first).
Eliminates the recurring -Dupdate-goldens churn: these 5 were 0-byte
outliers while 484 other empty goldens use the writeGolden-produced
1-byte "\n" form. The corpus runner trims trailing newlines on both
sides during verify, so both forms passed — but regen always rewrote
them to 1-byte. Conforming them makes -Dupdate-goldens idempotent.
The define path now honors the optional `export … "csym"` symbol-name
override (gap iii). declareFunction's rename branch fires for `export` too:
the extern stub is declared under the C name and the sx→C mapping recorded
in foreign_name_map. lazyLowerFunction then resolves the stub by that C
name (via foreign_name_map) so the body promotes into the C-named function
— emitting `define @triple_c` instead of `@sx_triple`. sx-side call sites
to the sx name resolve through the same map (verified: 5*5 prints 25).
example/1227 greens: the companion C calls `triple_c` and prints
call_triple(7) = 22. Bare export (1226) is unaffected (no rename → sx
name). Suite green (638 corpus / 443 unit). Phase 2 (`export`) complete.
example/1227 exposes the sx fn `sx_triple` to C under the symbol `triple_c`
via `export "triple_c"`; the companion C calls `triple_c` by that name.
RED: the define path emits the fn under its sx name (`sx_triple`) and
ignores the parsed `extern_name`, so the C reference to `triple_c` is
undefined at AOT link. The next commit consumes the rename on the define
path (gap iii) and greens it.
`export` (define + expose) now lowers to a defined C-ABI symbol with
external linkage and no implicit sx context — the four export-gap
conditions in src/ir/lower/decl.zig:
- (i) linkage: force `.external` for `extern_export == .export_` on both
define paths (lowerFunctionBodyInto, lowerFunction), beside the
OS-called entry points.
- (ii) C ABI: promote call_conv to `.c` on the define paths and in the
declareFunction extern-stub cc.
- (iv) no ctx: funcWantsImplicitCtx returns false for any non-`.none`
modifier (extern AND export), so no `__sx_ctx` slot is prepended.
- force-lower: an `export` fn is a lowering root (like `main`) in
lowerMainAndComptime — its purpose is external consumption, so it must
emit a body even when no sx code calls it; otherwise lazy lowering
leaves it a bodiless `declare`.
example/1226 now builds + runs via the AOT corpus mode: the companion C
calls `sx_square` by name and prints 37 / 82. Suite green (637 corpus /
443 unit). The optional `export "csym"` rename (gap iii) is Phase 2.2.
Phase 2 of the extern/export stream verifies `export` (define + expose a
C-ABI sx symbol) end-to-end. C->sx-by-name linkage cannot work 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 this lands a new AOT
execution mode for the corpus: an `expected/<name>.aot` marker switches an
example from JIT `sx run` to a `sx build` + execute flow, linking the sx
object with its C `#source` companions into a native binary.
example/1226 defines `sx_square :: (n: i32) -> i32 export { ... }` and a
companion .c that declares `extern int sx_square(int)` and calls it back.
RED: with `export` not yet lowered, the AOT link fails with an undefined
`_sx_square` (the define path still emits it `internal` + with an implicit
ctx slot, and lazy lowering leaves an uncalled export fn as a bodiless
declare). Phase 2.1 greens it.
Also retires the standalone `tests/run_examples.sh` runner — `zig build
test` (src/corpus_run.test.zig) is now the sole corpus runner, and the
shell mirror would have needed its own AOT-mode port to stay in lockstep.
verify-step.sh drops its redundant step (zig build test already runs the
corpus); CLAUDE.md documents the `.aot` mode.
Parser: a 'kw_extern' branch in the var-decl-with-type-annotation path
(beside #foreign) parses 'name : type extern [LIB] ["csym"];' into
VarDecl.is_extern/extern_lib/extern_name; the trailing diagnostic now
lists 'extern'. Lowering: registerTopLevelGlobal uses
extern_name orelse foreign_name orelse name for the C symbol and sets
is_extern = is_foreign or is_extern; globalInitValue returns null (no
initializer) for extern globals too.
examples/1225 green: '__stdinp : *void extern;' lowers to
'@__stdinp = external global ptr'; @__stdinp reads non-null. Suite
green (636 corpus / 443 unit).
Phase 1 done: extern functions (bare + rename) and data globals (bare +
rename) all work, behavior-equivalent to the matching #foreign form.
export (Phase 2), aggregates (Phase 3), docs + A->B gate (Phase 4)
remain. green commit.
Add examples/1225-ffi-extern-global.sx — '__stdinp : *void extern;'
references libSystem's stdin pointer via the bare 'extern' modifier on
a typed var decl (the extern-named counterpart of the #foreign global
in examples/1205). Hand-authored snapshot expects the success output.
RED: 1225 is the sole corpus failure (636 ran, 1 failed) — parse error,
'extern' after a type annotation is not yet accepted in the var-decl
path. Phase 1.2d parses it and lowers the extern global.
xfail commit per the cadence rule.
parseFnDecl parses the optional [LIB] ["csym"] tail after the
extern/export keyword into FnDecl.extern_lib/extern_name (mirrors
'#foreign LIB "csym"'). declareFunction unifies the symbol-name
override: rename_c_name = foreign_expr.c_name (for #foreign) OR
fd.extern_name (for extern) -> declare under the C name and map sx->C
in foreign_name_map; the dedupe guard now covers extern too.
examples/1224 green: 'c_abs :: (n) -> i32 extern "abs";' resolves
c_abs to libc abs -> c_abs(-42) = 42. 1223 (bare extern) unregressed.
Suite green (635 corpus / 443 unit).
extern_lib is parsed + stored but not a linking driver — like
'#foreign libc', it references a lib; the #library decl + build flags
remain the separate linking axis (decision 4). green commit.
Add examples/1224-ffi-extern-fn-rename.sx — 'c_abs :: (n) -> i32
extern "abs";' binds C's abs via the optional symbol-name override.
Hand-authored expected captures the success output (c_abs(-42) = 42).
RED: 1224 is the sole corpus failure (635 ran, 1 failed) — parse error,
the '"abs"' string after 'extern' is not yet accepted. Phase 1.2b
parses the optional [LIB] ["csym"] tail and consumes the rename.
xfail commit per the cadence rule.
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).
Add examples/1223-ffi-extern-fn.sx — binds libc 'abs' via bare 'extern'
(sx name = C symbol, no rename). Hand-authored expected/ captures the
SUCCESS output (abs(-7)=7 / abs(42)=42, exit 0).
RED: 1223 is the sole corpus failure (634 ran, 1 failed) — it parses
then errors at sema ('body produces no value') because lowering does
not yet route extern fns through declareExtern. Phase 1.1 wires the
lowering and turns this green.
xfail commit per the cadence rule (no commit both adds a test and makes
it pass).
parseFnDecl now calls parseOptionalExternExport() after the callconv
slot and stores the modifier on FnDecl.extern_export. For 'extern' the
body is ';' (an empty-block placeholder — the modifier carries the
linkage, no *_expr node, per the naming constraint). Both fn-decl
lookahead predicates (isFunctionDef, hasFnBodyAfterArrow) now treat
kw_extern/kw_export as fn-body markers beside kw_callconv, so
'(...) -> R extern;' is recognized as a fn def rather than a fn-type
const.
Per user feedback, decision 4 ("library separate") is REVISED: extern
carries an optional LIB + "csym" axis mirroring '#foreign LIB "csym"',
so it is a true #foreign superset (Gate A->B requirement — the Part B
migration of 466 #foreign uses across 6 libs must preserve each
symbol's library). Added FnDecl.extern_lib/extern_name and
VarDecl.extern_lib (beside is_extern/extern_name).
All unconsumed by lowering: extern parses, but a fn still errors at
sema (body produces no value). Suite green (443 unit / 633 corpus).
lock commit.
Add ast.ExternExportModifier { none, extern_, export_ } beside
CallingConvention; FnDecl.extern_export and VarDecl.is_extern/extern_name
fields (all defaulting to absent); and Parser.parseOptionalExternExport()
mirroring parseOptionalCallConv.
None of this is consumed by a decl path yet — no user-facing behavior
change, corpus diff empty. Two inline parser unit tests pin the helper's
keyword mapping and the field defaults. Phase 1.0 wires the helper into
the fn-decl path. lock commit.