Two post-stream follow-ups flagged in CHECKPOINT-EXTERN-EXPORT.md, plus a reproducible vscode-extension packaging setup: - parser: drop the vestigial `RuntimeClassPrefix.is_extern` field and `parseRuntimeClassDecl`'s `is_extern` param. Always false since the `#foreign` token was deleted; the postfix `extern`/`export` keyword is the sole reference-vs-define decider. No behavior change (644 corpus / 442 unit). - vscode grammar: highlight `extern`/`export` as `storage.modifier.sx`. - vscode packaging: declare `@vscode/vsce` as a devDep + add `package` / `vscode:prepublish` scripts so the vsix rebuilds reproducibly (was an ambient tool). Add repository/homepage/bugs (Gitea), icon (swipelab logo, 256x256), galleryBanner, README with cover banner. Rebuilt the vsix.
862 lines
64 KiB
Markdown
862 lines
64 KiB
Markdown
# 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/<name>.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/<name>.*` 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/<name>.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.**
|