docs(ffi-linkage): checkpoint — Phase 5.0 parser routing COMPLETE (fn-body flip landed)

fn-decl #foreign body marker now builds the unified extern AST. All four
#foreign parser paths resolved (global + fn-body flipped; const-with-type
dead; runtime-class already coalesced). Decision 7 ratified (accept churn).
Next: Phase 5.1 (confirm A->B gate covers post-flip) then Phases 6-7
(source #foreign->extern rename in stdlib + examples).
This commit is contained in:
agra
2026-06-15 04:05:04 +03:00
parent 6b94bb6bba
commit bde284ee21

View File

@@ -5,11 +5,26 @@ 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 prereqs 3 & 4 — plain-free classification + extern lib-ref validation**
**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 now done — the fn-decl `#foreign``extern`
flip is fully de-risked.**
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
@@ -132,29 +147,34 @@ 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 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.
**PART B — Phase 5.1: lock `#foreign` `extern` IR, then Phases 67 (migrate
stdlib + examples).** Phase 5.0 parser routing is COMPLETE.
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`.
- **Phase 5.1** (`lock`/`test`): the A→B gate (`lower.test.zig`) already asserts
`#foreign``extern`/`export` byte-identical IR for fn / global / Obj-C class, and
it's now TRIVIALLY true for the fn path (both build the same AST). Decide whether 5.1
needs anything beyond the existing gate — likely just confirm/annotate it covers the
post-flip world (the fn path no longer has two distinct shapes to compare). Could fold
into a doc note rather than a new test.
- **Phases 67** (`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]).
**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
**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:
@@ -243,8 +263,11 @@ 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
- **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
@@ -255,6 +278,13 @@ Part A ratified (bare / postfix / `⇒ callconv(.c)` / lib-separate). Part B:
deleted at cutover). Affects only diagnostic wording — IR/behavior identical either way.
## Log
- (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`.