docs(ffi-linkage): checkpoint — prereqs 3 & 4 done (4/4); fn-body flip de-risked, Decision 7 open

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.
This commit is contained in:
agra
2026-06-15 03:55:09 +03:00
parent ad6aed3d7a
commit 4dca38881e

View File

@@ -5,9 +5,30 @@ 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 5.0 prereq — extern C-variadic tail** (xfail `9a2c78d` → fix `0fdc821`) —
the SECOND (and last) deferred fn-path prerequisite. **BOTH fn-path prereqs now
done.** The C-variadic `...` handling was keyed on the `#foreign` (`foreign_expr`)
**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 now done — the fn-decl `#foreign``extern`
flip is 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
@@ -111,8 +132,29 @@ AOT), 1227 (export fn rename, AOT), 1348 (objc extern class), 1349 (objc export
(jni extern class), 1174/1175 (interplay diagnostics).
## Next step
**PART B — Phase 5.0, fn-decl `#foreign` body-marker migration** (BOTH prereqs —
visibility-gate + variadic — are now DONE). Route the fn-decl `#foreign` path so a
**PART B — Phase 5.0, fn-decl `#foreign` body-marker flip** — fully de-risked (ALL
FOUR prereqs done: visibility, variadic, plain-free classification, lib-ref
validation). The flip itself is now small. **AWAITING ONE DESIGN DECISION** (see
"Open decisions / Decision 7" below) before executing — it determines interim
diagnostic wording + whether one snapshot churns.
Mechanics of the flip (when greenlit):
1. `parser.zig:~2081` fn-body `#foreign` arm: build the extern shape
(`extern_export = .extern_`, `library_ref→extern_lib`, `c_name→extern_name`,
empty-block body) instead of a `foreign_expr` body.
2. Update parser unit test `parser.zig:4266-4267` (asserts the fn-body `#foreign`
builds a `foreign_expr` body with `library_ref "rl"`) to assert the extern shape.
3. Run the A→B gate (`lower.test.zig`) + full `zig build test`.
**Verified churn surface (NARROW):** IR is zero-churn (all lowering sites coalesce
`is_foreign`/`extern_export` — confirmed across this stream). The ONLY corpus churn
is example **1620**'s lib-ref message: a `#foreign`-spelled decl becomes the extern
AST, so `checkForeignRefs` would emit "extern library 'X'…" instead of "#foreign
library 'X'…". Parser-surface diagnostics (`parser.zig:484/1429`) and runtime-class
messages are unaffected (they fire on the literal keyword pre-AST). `c_import.zig`
auto-synthesis STILL builds `foreign_expr` bodies (not migrated this step), so
`foreign_expr` does not disappear — both shapes coexist, which is why every reader
had to coalesce. 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:
@@ -201,8 +243,33 @@ Part A ratified (bare / postfix / `⇒ callconv(.c)` / lib-separate). Part B:
- **Decision 6 STILL OPEN**: historical carve-out — keep `issues/*.md` (+ design-doc prose)
as provenance & gate only the live tree (recommended) vs purge everything. The user did
NOT confirm this at the Part A milestone; confirm before Phase 9.
- **Decision 7 OPEN — interim diagnostic wording for `#foreign`-spelled decls** (gates 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
- (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: