docs(ffi-linkage): checkpoint — Phase 4 complete, Part A done, A→B gate locked

This commit is contained in:
agra
2026-06-14 16:08:01 +03:00
parent 422c6577cf
commit aafcbf6d78

View File

@@ -5,7 +5,22 @@ Companion to `current/PLAN-EXTERN-EXPORT.md` — one merged plan: **Part A** add
every commit, one step at a time per the cadence rule.
## Last completed step
**Phase 3.1** (green) — **PHASE 3 COMPLETE.** Postfix `extern`/`export` on `#objc_class`/
**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
@@ -47,22 +62,30 @@ data globals — bare + renamed. export: functions — bare (`f :: (…) -> R ex
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 NOT started (Phase 3). 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).
**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
**Phase 4 — interplay, diagnostics, docs (+ the A→B gate).** (a) Diagnostics for combining
the surfaces: reject `extern`+`export` together; decide/handle prefix `#foreign` **and**
postfix `extern`/`export` on the same aggregate (today the postfix silently overrides
`is_foreign` — Phase 4 should reject the redundant/contradictory combo, mirroring the fn
path). (b) `extern`+`callconv` stacking/redundancy on fns. (c) docs — `specs.md`/`readme.md`
document the three `extern`/`export` axes (fns, globals, aggregates); `#foreign` stays
documented until the Part B cutover. (d) **GATE A→B (hard):** unit test asserting `#foreign`
and `extern`/`export` lower to identical IR for a sample fn / global / **class** — lock
before any Part B migration. Also pick up the two **Deferred** items below at this gate. (interplay/diagnostics/docs + the A→B gate: unit test that
`#foreign` and `extern`/`export` lower to identical IR) before Part B migration.
**PART B — Phase 5 (`#foreign` becomes an alias for `extern`).** Part A is complete and
the A→B gate is locked, so migration can begin. Phase 5.0 (`lock`): route the four
`#foreign` parser paths (`parser.zig:316,425,1305,1970`) to build the **same** extern-named
AST as `extern`/`export` — suite green, all snapshots unchanged. Phase 5.1 (`lock`): unit
test that `#foreign` and `extern` produce identical IR (the gate already covers this — extend
or reuse `lowerSrcToIr`). Then Phases 67 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
@@ -78,9 +101,9 @@ examples crashed in the spike. A real impl needs a C++ shim in `llvm_shim.c`
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 (do in Phase 4):** (a) docs`specs.md`/`readme.md` document
`extern`/`export` (the plan defers docs to Phase 4; `#foreign` stays documented until
the Part B cutover); (b) visibility-gate equivalence — bare `extern` (no `extern_lib`)
**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 — bare `extern` (no `extern_lib`)
is currently unconditionally visible via the `c_import_bare` gate
(`decl.zig:~2241`, `fd.body.data != .foreign_expr → return true`), whereas a lib-less
`#foreign` is policed by `visibleOverEdges`. Single-file examples don't exercise this;
@@ -162,6 +185,24 @@ historical carve-out — keep `issues/*.md` provenance, gate the live tree only.
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.** (Workflow note: `-Dupdate-goldens`
keeps newline-normalizing the 5 empty-stderr files [1226/1227/1348/1349/1426] — revert
that churn after each regen; a flaky `0712-sha256-streaming` timeout appears only under
concurrent `zig build` load, not a real failure.)
## Known issues
- **Workflow hazard (1.2):** an editor format-on-save (or `zig fmt`) clobbered the