From 28d38f2f2f9f50236064ead8c874180130c4143f Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 14 Jun 2026 20:58:31 +0300 Subject: [PATCH] =?UTF-8?q?docs(ffi-linkage):=20checkpoint=20=E2=80=94=20P?= =?UTF-8?q?hase=205.0=20visibility-gate=20prereq=20DONE;=20const-with-type?= =?UTF-8?q?/runtime-class=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mark deferred prereq (b) visibility-gate equivalence CLOSED (1228). - Record const-with-type as a dead path (deferred per user) and the runtime-class prefix as already-coalesced (no Phase 5.0 change). - Next step is the fn-path variadic prerequisite. --- current/CHECKPOINT-EXTERN-EXPORT.md | 99 +++++++++++++++++++++-------- 1 file changed, 71 insertions(+), 28 deletions(-) diff --git a/current/CHECKPOINT-EXTERN-EXPORT.md b/current/CHECKPOINT-EXTERN-EXPORT.md index d08b24e..96d1bf2 100644 --- a/current/CHECKPOINT-EXTERN-EXPORT.md +++ b/current/CHECKPOINT-EXTERN-EXPORT.md @@ -5,15 +5,32 @@ 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 (global path)** (`refactor` lock, commit `e5ddfbe`) — **PART B STARTED.** +**Phase 5.0 prereq — visibility-gate equivalence** (xfail `717c35d` → fix +`7d8ba1a`) — the first of the two deferred fn-path prerequisites is now LOCKED. +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. Suite green (10/10 steps, -444/444 unit, 643 corpus, 0 failed). The fn-decl, const-with-type, and -runtime-class `#foreign` paths still build the legacy AST. +(`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) — @@ -82,23 +99,37 @@ 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, remaining three `#foreign` parser paths.** The global path -(`parser.zig:425`) is done (commit `e5ddfbe`). Remaining: -- **const-with-type** (`parser.zig:316`, `name :: type_expr #foreign …`) — builds a - `const_decl` whose value is a `foreign_expr` node. Lowest-risk of the three; route - to the extern-named shape used by the fn path. Confirm the value-node lowering path - coalesces with extern before committing. -- **fn-decl body marker** (`parser.zig:2059`) — **needs prerequisites first**: (a) the - deferred **visibility-gate** alignment (`decl.zig:2254` — a lib-less `extern` must be - policed like a lib-less `#foreign`; currently bare `extern` is unconditionally visible), - which needs a **cross-module example** to lock; (b) **variadic** handling — the - `is_variadic` drop at `decl.zig:2097` is gated on `is_foreign` only, so a migrated - variadic `#foreign` (e.g. `printf`) would lose its `...` tail unless extended to - `is_extern_decl`. Do these (xfail/lock) BEFORE routing the fn path. -- **runtime-class prefix** (`parser.zig:1305`, `#foreign #objc_class/#jni_class`) — route - the prefix to the same `is_foreign_eff` the postfix `extern` already feeds - (`parseForeignClassDecl`); the class node keeps its `is_foreign` field (renamed to a - `runtime*` axis only in Phase 9.2, not `extern`). +**PART B — Phase 5.0, fn-path variadic prerequisite** (the SECOND of the two +deferred fn-path prereqs; visibility-gate equivalence is now DONE). The +`is_variadic` drop in `declareFunction` is gated on `is_foreign` only, so a +migrated variadic `extern` (e.g. `printf`) would lose its `...` tail. Extend the +gate to cover `extern_export == .extern_`. Cadence: xfail an `extern` variadic fn +losing its `...` (locks the gap), then the next commit fixes the gate. Find the +exact site via `grep -n "is_variadic" src/ir/lower/decl.zig` (the checkpoint's old +`decl.zig:2097` line ref may have drifted). AFTER both prereqs land, the fn-decl +`#foreign` body-marker path (`parser.zig:~2062`) can migrate onto `extern`. + +**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 @@ -127,13 +158,12 @@ milestone, NOT Phase 2/3 scope. The AOT `.aot`-marker corpus mode is the pragmat 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 — 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; -verify/align it at the A→B gate (a bare-extern lib-less fn should be policed like its -`#foreign` twin). Adding it now would be untested — needs a cross-module example. +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: @@ -144,6 +174,19 @@ Part A ratified (bare / postfix / `⇒ callconv(.c)` / lib-separate). Part B: NOT confirm this at the Part A milestone; confirm before Phase 9. ## Log +- (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