refactor(ffi-linkage): Phase 9.3/9.4 — purge 'foreign' from issues/*.md; GATE PASS

Rewrote 20 issue writeups to the extern/runtime-class vocabulary (#foreign→extern,
foreign_class_map→runtime_class_map, parseForeignClassDecl→parseRuntimeClassDecl,
findForeignMethodInChain→findRuntimeMethodInChain, dedupeForeignSymbol→
dedupeExternSymbol, is_foreign_c_api→is_extern_c_api, stale filename refs to the
renamed examples, foreign-class→runtime-class, bare foreign→extern). Renamed
issues/0043-…-foreign-class-…→…-runtime-class-….

PHASE 9 COMPLETE — 9.4 GATE PASSES: zero 'foreign' across src/library/examples/
issues/docs/editors/specs/readme/CLAUDE, excluding only the SQLite API constant
SQLITE_CONSTRAINT_FOREIGNKEY + vendored sqlite3.c/.h (upstream third-party).
Suite green (644 corpus / 443 unit, 0 failed).
This commit is contained in:
agra
2026-06-15 11:18:35 +03:00
parent b52d424369
commit b9cfe2554f
20 changed files with 77 additions and 77 deletions

View File

@@ -2,13 +2,13 @@
> **Status: superseded — kept for reference.** Relocated from the old > **Status: superseded — kept for reference.** Relocated from the old
> `examples/issue-0019/` fixture during the test-layout migration. The behavior > `examples/issue-0019/` fixture during the test-layout migration. The behavior
> it probed (A imports B and C; C must NOT see B's `#foreign` C functions just > it probed (A imports B and C; C must NOT see B's `extern` C functions just
> because A imported B) is now covered by the passing test > because A imported B) is now covered by the passing test
> `examples/0706-modules-import-non-transitive.sx`. > `examples/0706-modules-import-non-transitive.sx`.
## What it probed ## What it probed
`main` imports both `c_wrapper.sx` (which declares C `#foreign` functions) and `main` imports both `c_wrapper.sx` (which declares C `extern` functions) and
`other.sx`. `other.sx` should *not* gain access to `c_wrapper`'s C functions `other.sx`. `other.sx` should *not* gain access to `c_wrapper`'s C functions
transitively — using one should produce the "not visible; #import the module that transitively — using one should produce the "not visible; #import the module that
declares it" diagnostic. declares it" diagnostic.

View File

@@ -9,7 +9,7 @@
Support an `extern G : T;` top-level form so a global **defined** in one sx Support an `extern G : T;` top-level form so a global **defined** in one sx
source file can be **referenced** from another without threading it through source file can be **referenced** from another without threading it through
parameters — mirroring how `#foreign` function declarations work (declared in one parameters — mirroring how `extern` function declarations work (declared in one
place, defined elsewhere, resolved at link time). place, defined elsewhere, resolved at link time).
```sx ```sx
@@ -28,8 +28,8 @@ load :: (self: *ChessPieces, path: [:0]u8) {
Today `pieces.load` takes `has_gpu: bool, gpu: GPU` params and `main.sx` threads Today `pieces.load` takes `has_gpu: bool, gpu: GPU` params and `main.sx` threads
them through; cross-file `extern` globals would drop that ceremony. Distinct from them through; cross-file `extern` globals would drop that ceremony. Distinct from
the existing `name : T #foreign;` form (an *external C* data symbol from the existing `name : T extern;` form (an *external C* data symbol from
libsystem etc. — see `examples/1205-ffi-foreign-global.sx`); this request is for libsystem etc. — see `examples/1205-ffi-extern-global.sx`); this request is for
sx-defined globals shared across sx modules. sx-defined globals shared across sx modules.
## Reproduction ## Reproduction

View File

@@ -1,4 +1,4 @@
# issue-0043: lazy-lowered function bodies don't resolve foreign-class method dispatch # issue-0043: lazy-lowered function bodies don't resolve runtime-class method dispatch
**FIXED.** The original repro **FIXED.** The original repro
(`sx build --target ios-sim issue-0043.sx` with a (`sx build --target ios-sim issue-0043.sx` with a
@@ -6,7 +6,7 @@
`inline if OS == .ios { ... }`-gated function called transitively `inline if OS == .ios { ... }`-gated function called transitively
from `caller :: (...) callconv(.c)`) compiles cleanly today; chess from `caller :: (...) callconv(.c)`) compiles cleanly today; chess
on iOS-sim runs end-to-end through the same dispatch shape. The on iOS-sim runs end-to-end through the same dispatch shape. The
fix lives in tree as part of broader foreign-class / fix lives in tree as part of broader runtime-class /
lazyLowerFunction work — no specific commit isolates it. lazyLowerFunction work — no specific commit isolates it.
Below preserved as a record of the original problem. Below preserved as a record of the original problem.
@@ -53,7 +53,7 @@ lowerExpr
So when the *outer* `lowerCall` decides to lazy-lower So when the *outer* `lowerCall` decides to lazy-lower
`uikit_scene_will_connect_ios`, the *inner* body's method-dispatch `uikit_scene_will_connect_ios`, the *inner* body's method-dispatch
on `*UIWindow` etc. fails to find the foreign-class declaration — on `*UIWindow` etc. fails to find the runtime-class declaration —
even though the declaration is at module scope at the top of even though the declaration is at module scope at the top of
uikit.sx and resolves fine for non-lazy lowering paths (`sx build` uikit.sx and resolves fine for non-lazy lowering paths (`sx build`
for macOS target compiles the same source cleanly). for macOS target compiles the same source cleanly).
@@ -64,12 +64,12 @@ for macOS target compiles the same source cleanly).
#import "modules/std.sx"; #import "modules/std.sx";
#import "modules/compiler.sx"; #import "modules/compiler.sx";
UIWindow :: #foreign #objc_class("UIWindow") { UIWindow :: #objc_class("UIWindow") extern {
alloc :: () -> *UIWindow; alloc :: () -> *UIWindow;
initWithWindowScene :: (self: *Self, scene: *void) -> *UIWindow; initWithWindowScene :: (self: *Self, scene: *void) -> *UIWindow;
} }
// Function B: uses foreign-class method dispatch. // Function B: uses runtime-class method dispatch.
do_work :: (scene: *void) { do_work :: (scene: *void) {
win := UIWindow.alloc().initWithWindowScene(scene); win := UIWindow.alloc().initWithWindowScene(scene);
_ = win; _ = win;
@@ -104,30 +104,30 @@ Suspected area: `lazyLowerFunction` at
[src/ir/lower.zig:1057](../src/ir/lower.zig#L1057) and the field- [src/ir/lower.zig:1057](../src/ir/lower.zig#L1057) and the field-
access method dispatch at access method dispatch at
[src/ir/lower.zig:5290](../src/ir/lower.zig#L5290) (around the [src/ir/lower.zig:5290](../src/ir/lower.zig#L5290) (around the
`foreign_class_map.get(sname_for_foreign)` lookup). `runtime_class_map.get(sname_for_runtime)` lookup).
Hypotheses: Hypotheses:
1. `lazyLowerFunction` swaps some piece of lowering state on entry 1. `lazyLowerFunction` swaps some piece of lowering state on entry
(e.g., `saved_source_file`, `current_ctx_ref`) but doesn't preserve (e.g., `saved_source_file`, `current_ctx_ref`) but doesn't preserve
access to `foreign_class_map`. Check whether the map is access to `runtime_class_map`. Check whether the map is
instance-state vs. shared. instance-state vs. shared.
2. The receiver type for `win` (`*UIWindow`) isn't being resolved to 2. The receiver type for `win` (`*UIWindow`) isn't being resolved to
its `getStructTypeName` correctly during lazy lowering — possibly its `getStructTypeName` correctly during lazy lowering — possibly
`inferExprType` for the lazy-lowered context resolves to an `inferExprType` for the lazy-lowered context resolves to an
anonymous type instead of `UIWindow`. anonymous type instead of `UIWindow`.
3. The foreign-class declarations are added to `foreign_class_map` 3. The runtime-class declarations are added to `runtime_class_map`
during a pre-scan pass that runs BEFORE the outer function `A`'s during a pre-scan pass that runs BEFORE the outer function `A`'s
body is lowered, but lazy lowering of `B` from within `A` might body is lowered, but lazy lowering of `B` from within `A` might
be observing the map at a pre-scan state where uikit.sx's be observing the map at a pre-scan state where uikit.sx's
declarations haven't been seen yet (cross-module ordering). declarations haven't been seen yet (cross-module ordering).
What the fix likely needs to do: What the fix likely needs to do:
- Confirm `foreign_class_map` contains `"UIWindow"` etc. at the - Confirm `runtime_class_map` contains `"UIWindow"` etc. at the
point of the lazy lowering call (add a debug print at the failing point of the lazy lowering call (add a debug print at the failing
dispatch). dispatch).
- If the map IS populated, trace why the field-access dispatch falls - If the map IS populated, trace why the field-access dispatch falls
through to line 5640 instead of taking the `foreign_class_map.get` through to line 5640 instead of taking the `runtime_class_map.get`
branch at line 5306. branch at line 5306.
- If the map is NOT populated, find the seeding path and fix the - If the map is NOT populated, find the seeding path and fix the
ordering — likely a missing pre-scan step before lazy lowering. ordering — likely a missing pre-scan step before lazy lowering.
@@ -142,7 +142,7 @@ away.
The migration of `library/modules/platform/uikit.sx` to declarative The migration of `library/modules/platform/uikit.sx` to declarative
`#objc_class` dispatch (Phase 3.2 plan part C, clusters C4 + C5) `#objc_class` dispatch (Phase 3.2 plan part C, clusters C4 + C5)
necessarily places foreign-class method calls inside iOS-only helper necessarily places runtime-class method calls inside iOS-only helper
functions that get lazily lowered when chess's iOS scene-lifecycle functions that get lazily lowered when chess's iOS scene-lifecycle
hooks fire them. Without this fix, the migration produces compile hooks fire them. Without this fix, the migration produces compile
errors on iOS-sim that don't appear on macOS. C1+C2+C3 happened to errors on iOS-sim that don't appear on macOS. C1+C2+C3 happened to

View File

@@ -31,8 +31,8 @@ method declaration — by position, not by hardcoded name.
#import "modules/std.sx"; #import "modules/std.sx";
#import "modules/std/objc.sx"; #import "modules/std/objc.sx";
// Foreign declaration so we can dispatch. // Extern declaration so we can dispatch.
NSObject :: #foreign #objc_class("NSObject") { NSObject :: #objc_class("NSObject") extern {
class :: () -> *void; class :: () -> *void;
description :: (self: *Self) -> *void; description :: (self: *Self) -> *void;
} }
@@ -86,7 +86,7 @@ $ grep -n '"self"' src/ir/lower.zig
``` ```
The methods that synthesize the IMP trampoline (M1.2 A.2) and the The methods that synthesize the IMP trampoline (M1.2 A.2) and the
ones that wire `*Self` → opaque foreign-class stub (M1.2 A.3) appear ones that wire `*Self` → opaque runtime-class stub (M1.2 A.3) appear
to either: to either:
(a) emit the trampoline assuming the slot name in the body is "self", (a) emit the trampoline assuming the slot name in the body is "self",
@@ -139,12 +139,12 @@ went wrong:
1. uikit.sx renamed the AppDelegate's IMP method first param to `this`, 1. uikit.sx renamed the AppDelegate's IMP method first param to `this`,
so `xx this` appeared inside the body of a `-> BOOL` method. so `xx this` appeared inside the body of a `-> BOOL` method.
2. The body called `center.addObserver_selector_name_object(xx this, ..., null)` 2. The body called `center.addObserver_selector_name_object(xx this, ..., null)`
on a `*NSNotificationCenter` foreign-class receiver. on a `*NSNotificationCenter` runtime-class receiver.
3. `lowerCall` sets a per-arg `self.target_type` from 3. `lowerCall` sets a per-arg `self.target_type` from
`resolveCallParamTypes(c)`. For UFCS dispatch on a foreign-class `resolveCallParamTypes(c)`. For UFCS dispatch on a runtime-class
alias, that function had no path covering `foreign_class_map` alias, that function had no path covering `runtime_class_map`
it tried `resolveFuncByName(qualified)` and `fn_ast_map.get(qualified)`, it tried `resolveFuncByName(qualified)` and `fn_ast_map.get(qualified)`,
both of which miss for `#foreign #objc_class` methods. both of which miss for `#objc_class(…) extern` methods.
4. With `param_types` empty, the per-arg `target_type` assignment was 4. With `param_types` empty, the per-arg `target_type` assignment was
skipped, so `self.target_type` retained its previous value: the skipped, so `self.target_type` retained its previous value: the
enclosing fn's return type, **BOOL → i8**. enclosing fn's return type, **BOOL → i8**.
@@ -160,9 +160,9 @@ exercised in the body in the original tests, OR the encoding
happened to land somewhere benign (e.g. the AOT-with-iOS-sim path happened to land somewhere benign (e.g. the AOT-with-iOS-sim path
plus UIKit's specific validation order). plus UIKit's specific validation order).
**Fix:** add a `foreign_class_map.get(sname)` **Fix:** add a `runtime_class_map.get(sname)`
`findForeignMethodInChain` path to `resolveCallParamTypes`. When the `findRuntimeMethodInChain` path to `resolveCallParamTypes`. When the
UFCS receiver is a foreign-class alias, walk the `#extends` chain to UFCS receiver is a runtime-class alias, walk the `#extends` chain to
find the method, then resolve its declared param types (skipping the find the method, then resolve its declared param types (skipping the
implicit `*Self` for instance methods). With the fix, `param_types` implicit `*Self` for instance methods). With the fix, `param_types`
returns `[*void, *void, *void, *void]` for the addObserver: call, returns `[*void, *void, *void, *void]` for the addObserver: call,

View File

@@ -88,7 +88,7 @@ edit in `library/modules/std.sx`.
The FFI plan migrates all stdlib variadic decls from the legacy The FFI plan migrates all stdlib variadic decls from the legacy
form to the new `..name: []Type` form (`path_join`, `format`, form to the new `..name: []Type` form (`path_join`, `format`,
`print`, plus the foreign `open` decl, plus the example fixtures). `print`, plus the extern `open` decl, plus the example fixtures).
Per the FFI cadence rule the migration is supposed to be a Per the FFI cadence rule the migration is supposed to be a
mechanical textual change with identical semantics. This bug mechanical textual change with identical semantics. This bug
blocks that. blocks that.

View File

@@ -66,7 +66,7 @@ The macOS SDL startup never reorients CWD (or the asset root) to the bundle.
calls `SDL_Init(SDL_INIT_VIDEO)` and creates the window but does no `chdir`, calls `SDL_Init(SDL_INIT_VIDEO)` and creates the window but does no `chdir`,
so `read_file_bytes` so `read_file_bytes`
([library/modules/ui/glyph_cache.sx:202](../library/modules/ui/glyph_cache.sx#L202), ([library/modules/ui/glyph_cache.sx:202](../library/modules/ui/glyph_cache.sx#L202),
and the chess `#foreign read_file_bytes`) opens paths relative to whatever CWD and the chess `extern read_file_bytes`) opens paths relative to whatever CWD
the launcher set — `/` under Finder/`open`. the launcher set — `/` under Finder/`open`.
The other platforms already handle this: The other platforms already handle this:
@@ -97,8 +97,8 @@ Recommended approach — `SDL_GetBasePath()`:
to it at the top of `init` when `BuildOptions.is_macos` (gate so `sx run` to it at the top of `init` when `BuildOptions.is_macos` (gate so `sx run`
during development isn't affected — or gate on "the base path differs from during development isn't affected — or gate on "the base path differs from
CWD and contains an `assets/` dir"). CWD and contains an `assets/` dir").
- Add the `#foreign` decl for `SDL_GetBasePath` (returns `*u8`, SDL-owned) and - Add the `extern` decl for `SDL_GetBasePath` (returns `*u8`, SDL-owned) and
call `chdir` (already used by uikit.sx — reuse the same `#foreign`). call `chdir` (already used by uikit.sx — reuse the same `extern`).
Alternative (no SDL dependency): `_NSGetExecutablePath` + `dirname`, same as a Alternative (no SDL dependency): `_NSGetExecutablePath` + `dirname`, same as a
plain macOS resolve. SDL_GetBasePath is simpler and already links SDL3. plain macOS resolve. SDL_GetBasePath is simpler and already links SDL3.

View File

@@ -30,7 +30,7 @@ This bit the moment `modules/std/objc.sx` (imported by `main.sx`,
## Root cause ## Root cause
`mergeFlat`/`addOwnDecl` dedup the global flat decl list by `mergeFlat`/`addOwnDecl` dedup the global flat decl list by
`decl.data.declName()`. Named decls (structs, fns, foreign classes) dedup `decl.data.declName()`. Named decls (structs, fns, runtime classes) dedup
fine across diamonds. But `impl_block` is anonymous — `declName()` returns fine across diamonds. But `impl_block` is anonymous — `declName()` returns
`null` (see [src/ast.zig](../src/ast.zig) `Data.declName`) — so the dedup `null` (see [src/ast.zig](../src/ast.zig) `Data.declName`) — so the dedup
guard was skipped and the **same cached** impl node (modules are cached in guard was skipped and the **same cached** impl node (modules are cached in

View File

@@ -8,7 +8,7 @@
> function signature and non-generic struct field type and rejects any leaf name > function signature and non-generic struct field type and rejects any leaf name
> that is not a primitive, an in-scope generic param (`$T` / `type_params`), a > that is not a primitive, an in-scope generic param (`$T` / `type_params`), a
> declared type, or a real (non-stub) registered type. The load-bearing > declared type, or a real (non-stub) registered type. The load-bearing
> empty-struct stub is left intact (forward references + foreign-class opaque > empty-struct stub is left intact (forward references + runtime-class opaque
> types still rely on it during the scan); the pass runs after scanning and before > types still rely on it during the scan); the pass runs after scanning and before
> body lowering, so `core.zig`'s `hasErrors()` halts the build before any stub > body lowering, so `core.zig`'s `hasErrors()` halts the build before any stub
> reaches codegen. A value param used as a type gets the tailored hint > reaches codegen. A value param used as a type gets the tailored hint

View File

@@ -74,7 +74,7 @@ Suspected area:
Likely fix: Likely fix:
- Change `collectDeclaredTypeNames` / `harvestScopeDecls` so only declarations - Change `collectDeclaredTypeNames` / `harvestScopeDecls` so only declarations
that actually introduce type-position names are added: struct / enum / union / that actually introduce type-position names are added: struct / enum / union /
error declarations, type aliases, generic templates, protocols, foreign error declarations, type aliases, generic templates, protocols, extern
classes, and local type declarations. classes, and local type declarations.
- Do not add arbitrary value const names to the type-name set. - Do not add arbitrary value const names to the type-name set.
- Preserve valid type alias behavior such as `Alias :: u32;` and local - Preserve valid type alias behavior such as `Alias :: u32;` and local

View File

@@ -84,7 +84,7 @@ Likely fix:
a diagnostic instead of returning `null` / zero-initializing. a diagnostic instead of returning `null` / zero-initializing.
- Do not regress issue 0070: `A :: B; B :: i32; g : A = 7;` and - Do not regress issue 0070: `A :: B; B :: i32; g : A = 7;` and
`K : A : 35;` must still resolve through the converged alias map. `K : A : 35;` must still resolve through the converged alias map.
- Preserve literal, array literal, struct literal, and foreign-global behavior. - Preserve literal, array literal, struct literal, and extern-global behavior.
Verification: Verification:
- Add a focused regression, likely in the `01xx` types block: - Add a focused regression, likely in the `01xx` types block:

View File

@@ -3,7 +3,7 @@
> **✅ RESOLVED.** Root cause: four FFI call-arg lowering loops resolved an > **✅ RESOLVED.** Root cause: four FFI call-arg lowering loops resolved an
> argument's IR type via `getRefIRType(arg_ref) orelse .void` — a silent fallback > argument's IR type via `getRefIRType(arg_ref) orelse .void` — a silent fallback
> to the load-bearing real type `.void`, which downstream `toLLVMType` → > to the load-bearing real type `.void`, which downstream `toLLVMType` →
> `abiCoerceParamType` → `coerceArg` treat as a legitimate (void-typed) foreign > `abiCoerceParamType` → `coerceArg` treat as a legitimate (void-typed) extern
> argument, corrupting the call ABI with no diagnostic. Fix: one shared resolver > argument, corrupting the call ABI with no diagnostic. Fix: one shared resolver
> `LLVMEmitter.argIRTypeOrFail` ([src/ir/emit_llvm.zig]) returns the dedicated > `LLVMEmitter.argIRTypeOrFail` ([src/ir/emit_llvm.zig]) returns the dedicated
> `.unresolved` sentinel on a failed lookup — never `.void`/`.i64` — so the failure > `.unresolved` sentinel on a failed lookup — never `.void`/`.i64` — so the failure
@@ -20,7 +20,7 @@
**One-line:** Four FFI call-arg lowering sites silently default a failed **One-line:** Four FFI call-arg lowering sites silently default a failed
argument-type lookup to `.void` — the forbidden silent-type-fallback anti-pattern argument-type lookup to `.void` — the forbidden silent-type-fallback anti-pattern
(`.void` as a failed-type-lookup sentinel), which would produce a void-typed (`.void` as a failed-type-lookup sentinel), which would produce a void-typed
foreign-call argument (wrong LLVM param type → silent ABI corruption) with no extern-call argument (wrong LLVM param type → silent ABI corruption) with no
diagnostic if the lookup ever fails. diagnostic if the lookup ever fails.
**Observed:** `self.getRefIRType(arg_ref) orelse .void` at: **Observed:** `self.getRefIRType(arg_ref) orelse .void` at:
@@ -30,7 +30,7 @@ diagnostic if the lookup ever fails.
- `src/backend/llvm/ops.zig:761` (JNI `Call<Type>Method` arg loop) - `src/backend/llvm/ops.zig:761` (JNI `Call<Type>Method` arg loop)
Each then does `toLLVMType(raw_ty)``abiCoerceParamType``coerceArg`, so a Each then does `toLLVMType(raw_ty)``abiCoerceParamType``coerceArg`, so a
`.void` fallback silently mis-types the foreign-call argument. `.void` fallback silently mis-types the extern-call argument.
**Expected:** `getRefIRType` returning null for a real call argument is a **Expected:** `getRefIRType` returning null for a real call argument is a
"must-succeed lookup" failure (every arg is a valid param/instruction ref). Per "must-succeed lookup" failure (every arg is a valid param/instruction ref). Per
@@ -49,7 +49,7 @@ discovered bug — file an issue, do not just delete the default in place"*).
## Reproduction ## Reproduction
This is a **latent / static** finding: there is no known sx program that drives This is a **latent / static** finding: there is no known sx program that drives
`getRefIRType` to `null` for a valid foreign-call argument (well-formed IR always `getRefIRType` to `null` for a valid extern-call argument (well-formed IR always
has a type for every arg ref), so it cannot currently be triggered at runtime — which has a type for every arg ref), so it cannot currently be triggered at runtime — which
is exactly why it is dangerous (a future IR change that breaks the invariant would is exactly why it is dangerous (a future IR change that breaks the invariant would
corrupt FFI ABI silently). The code paths are exercised (and must stay green after corrupt FFI ABI silently). The code paths are exercised (and must stay green after
@@ -81,4 +81,4 @@ loud-failure path, see below.)
> green (the happy path is unchanged); (2) add a `*.test.zig` unit test that > green (the happy path is unchanged); (2) add a `*.test.zig` unit test that
> constructs an FFI call op with a bogus arg ref and asserts the loud failure fires > constructs an FFI call op with a bogus arg ref and asserts the loud failure fires
> (not a `.void` silent default). Expected new behavior: an unresolved FFI arg type > (not a `.void` silent default). Expected new behavior: an unresolved FFI arg type
> produces a clear compiler error / panic, never a void-typed foreign argument. > produces a clear compiler error / panic, never a void-typed extern argument.

View File

@@ -38,7 +38,7 @@
> name span in the AST (`VarDecl.name_span`, `DestructureDecl.name_spans`, > name span in the AST (`VarDecl.name_span`, `DestructureDecl.name_spans`,
> `IfExpr`/`WhileExpr.binding_span`, `ForExpr.capture_span`/`index_span`, > `IfExpr`/`WhileExpr.binding_span`, `ForExpr.capture_span`/`index_span`,
> `MatchArm.capture_span`, `CatchExpr`/`OnFailStmt.binding_span`, > `MatchArm.capture_span`, `CatchExpr`/`OnFailStmt.binding_span`,
> `Protocol`/`ForeignMethodDecl.param_name_spans`), populated by the parser at > `Protocol`/`RuntimeMethodDecl.param_name_spans`), populated by the parser at
> each binding site. `checkBindingNames` passes that span to the diagnostic, so > each binding site. `checkBindingNames` passes that span to the diagnostic, so
> the caret underlines the offending identifier itself instead of the enclosing > the caret underlines the offending identifier itself instead of the enclosing
> statement / `if` / `match` / `protocol` / `#objc_class` block. No call site > statement / `if` / `match` / `protocol` / `#objc_class` block. No call site

View File

@@ -1,4 +1,4 @@
# 0089 — backtick raw-identifier escape + `#import c` foreign-name exemption from the reserved-type-name rule # 0089 — backtick raw-identifier escape + `#import c` extern-name exemption from the reserved-type-name rule
> **✅ RESOLVED** (foundation step F0.6). Two mechanisms, per Agra's design > **✅ RESOLVED** (foundation step F0.6). Two mechanisms, per Agra's design
> ruling; the final shape is the **universal raw identifier** (attempt 4): > ruling; the final shape is the **universal raw identifier** (attempt 4):
@@ -13,9 +13,9 @@
> declaration node ([src/ast.zig]): `VarDecl` / `ConstDecl` / `Param` / `FnDecl` > declaration node ([src/ast.zig]): `VarDecl` / `ConstDecl` / `Param` / `FnDecl`
> plus `IfExpr` / `WhileExpr` optional bindings, `ForExpr` capture + index, > plus `IfExpr` / `WhileExpr` optional bindings, `ForExpr` capture + index,
> `MatchArm` capture, `CatchExpr` / `OnFailStmt` tag bindings, `DestructureDecl` > `MatchArm` capture, `CatchExpr` / `OnFailStmt` tag bindings, `DestructureDecl`
> per-name, protocol-default / foreign-class method params, AND every > per-name, protocol-default / runtime-class method params, AND every
> type-introducing decl — `StructDecl` / `EnumDecl` / `UnionDecl` / > type-introducing decl — `StructDecl` / `EnumDecl` / `UnionDecl` /
> `ErrorSetDecl` / `ProtocolDecl` / `ForeignClassDecl` / `UfcsAlias` / > `ErrorSetDecl` / `ProtocolDecl` / `RuntimeClassDecl` / `UfcsAlias` /
> `NamespaceDecl` / `ImportDecl` / `CImportDecl` / `LibraryDecl`. > `NamespaceDecl` / `ImportDecl` / `CImportDecl` / `LibraryDecl`.
> >
> - **Value position.** The parser skips `Type.fromName` for a raw identifier > - **Value position.** The parser skips `Type.fromName` for a raw identifier
@@ -81,15 +81,15 @@
> reserved-spelled impl method needs the backtick (`` `i2 :: (self) ``), no > reserved-spelled impl method needs the backtick (`` `i2 :: (self) ``), no
> more exempt than a free function (cf. `examples/1122`). Pinned by > more exempt than a free function (cf. `examples/1122`). Pinned by
> `examples/0158-types-reserved-name-member-exempt.sx`. > `examples/0158-types-reserved-name-member-exempt.sx`.
> 2. **`#import c` foreign-name exemption.** `c_import.zig` synthesizes foreign > 2. **`#import c` extern-name exemption.** `c_import.zig` synthesizes extern
> `#foreign` decls with `Param.is_raw = true` (and the synthesized `FnDecl` > `extern` decls with `Param.is_raw = true` (and the synthesized `FnDecl`
> `is_raw = true`), so generated C names that collide with reserved type names > `is_raw = true`), so generated C names that collide with reserved type names
> (`i1`, `i2`) import unedited and a reserved-name foreign fn is bare-callable. > (`i1`, `i2`) import unedited and a reserved-name extern fn is bare-callable.
> >
> **Bare-callable foreign / backtick fn.** `lowerCall` rewrites a `.type_expr` > **Bare-callable extern / backtick fn.** `lowerCall` rewrites a `.type_expr`
> callee to an identifier when a function **of RAW provenance** of that name is in > callee to an identifier when a function **of RAW provenance** of that name is in
> scope ([src/ir/lower.zig]) — scoped to the callee `FnDecl`'s `is_raw` flag, so it > scope ([src/ir/lower.zig]) — scoped to the callee `FnDecl`'s `is_raw` flag, so it
> only ever fires for a backtick / `#import c` foreign fn (the decl check guarantees > only ever fires for a backtick / `#import c` extern fn (the decl check guarantees
> no bare reserved-name fn exists). `i2(4)` resolves to the function (`TypeName(val)` > no bare reserved-name fn exists). `i2(4)` resolves to the function (`TypeName(val)`
> is not a cast). > is not a cast).
> >
@@ -107,8 +107,8 @@
> fields / union tag / protocol method signature — read & written bare and via > fields / union tag / protocol method signature — read & written bare and via
> backtick; impl method definition takes the backtick), > backtick; impl method definition takes the backtick),
> `examples/1054-errors-backtick-reserved-binding.sx` (`catch`/`onfail` tag > `examples/1054-errors-backtick-reserved-binding.sx` (`catch`/`onfail` tag
> bindings), `examples/1220-ffi-c-import-reserved-name-params.{sx,h,c}` (foreign > bindings), `examples/1220-ffi-c-import-reserved-name-params.{sx,h,c}` (extern
> param + fn-name exemption, bare-callable foreign fn); negatives > param + fn-name exemption, bare-callable extern fn); negatives
> `examples/1119`/`1121`/`1123` (bare reserved binding across forms), > `examples/1119`/`1121`/`1123` (bare reserved binding across forms),
> `examples/1140-diagnostics-reserved-name-const-fn-decl.sx` (bare const + fn decl), > `examples/1140-diagnostics-reserved-name-const-fn-decl.sx` (bare const + fn decl),
> `examples/1141-diagnostics-reserved-name-type-decl.sx` (bare struct / enum / union > `examples/1141-diagnostics-reserved-name-type-decl.sx` (bare struct / enum / union
@@ -156,8 +156,8 @@ declarations — which is what now fires on the C-imported `i1`/`i2`.
External / imported source does NOT need to conform to sx naming standards. Two External / imported source does NOT need to conform to sx naming standards. Two
mechanisms: mechanisms:
1. **Auto-exempt imports.** `#import c` (and other foreign) declarations are 1. **Auto-exempt imports.** `#import c` (and other extern) declarations are
treated as RAW identifiers: foreign names are never type-classified and never treated as RAW identifiers: extern names are never type-classified and never
reserved-checked, so generated bindings "just work" with zero user edits. reserved-checked, so generated bindings "just work" with zero user edits.
2. **Backtick raw-identifier for sx code.** A leading backtick makes the following 2. **Backtick raw-identifier for sx code.** A leading backtick makes the following
identifier raw — an identifier that is NEVER type-classified, so it bypasses the identifier raw — an identifier that is NEVER type-classified, so it bypasses the

View File

@@ -21,7 +21,7 @@ the same colliding entry.
`ns.fn` — they just had nothing to find. `NamespaceDecl` carries the `ns.fn` — they just had nothing to find. `NamespaceDecl` carries the
module's `own_decls` (populated in `imports.addNamespace`) so the module's `own_decls` (populated in `imports.addNamespace`) so the
registration covers authored decls, not transitive flat imports. Generic registration covers authored decls, not transitive flat imports. Generic
/ comptime / pack / foreign functions are excluded — they dispatch by / comptime / pack / extern functions are excluded — they dispatch by
monomorphization off the bare template name, not the plain monomorphization off the bare template name, not the plain
`resolveFuncByName` path, so a qualified alias would strand their `resolveFuncByName` path, so a qualified alias would strand their
per-call type bindings. The qualified function is declared + lowered on per-call type bindings. The qualified function is declared + lowered on
@@ -106,7 +106,7 @@ class can't diverge again. The fields the defer now restores on all paths:
- `builder.current_block` - `builder.current_block`
- `builder.inst_counter` - `builder.inst_counter`
(The `current_foreign_class`, `jni_env_stack_base`, and pack-mono / (The `current_runtime_class`, `jni_env_stack_base`, and pack-mono /
`inline_return_target` fields already had their own `defer`s and apply on all `inline_return_target` fields already had their own `defer`s and apply on all
paths; they are unchanged.) paths; they are unchanged.)

View File

@@ -43,7 +43,7 @@ collision:
`resolveBareCallee(name, caller_file) -> .func(ResolvedAuthor) | .ambiguous | `resolveBareCallee(name, caller_file) -> .func(ResolvedAuthor) | .ambiguous |
.none` (`src/ir/lower.zig`). It returns `.none` whenever the outcome would .none` (`src/ir/lower.zig`). It returns `.none` whenever the outcome would
equal first-wins (single author, or own-author == winner), so every equal first-wins (single author, or own-author == winner), so every
single-author / local / parameter / std / qualified / foreign / generic / single-author / local / parameter / std / qualified / extern / generic /
builtin name resolves byte-for-byte as before. Only a genuine flat collision builtin name resolves byte-for-byte as before. Only a genuine flat collision
reroutes: own-author wins; else the caller's flat-reachable authors — `≥2` reroutes: own-author wins; else the caller's flat-reachable authors — `≥2`
distinct → `.ambiguous` (loud "qualify the call" diagnostic), exactly one distinct → `.ambiguous` (loud "qualify the call" diagnostic), exactly one
@@ -85,7 +85,7 @@ fails on pre-fix code and passes after):
- `0726-modules-flat-same-name-variadic` — per-source variadic packing. - `0726-modules-flat-same-name-variadic` — per-source variadic packing.
- `0728-modules-flat-same-name-paramtype` — per-source parameter target typing - `0728-modules-flat-same-name-paramtype` — per-source parameter target typing
(value vs pointer param). (value vs pointer param).
- `0729-modules-flat-same-name-foreign` — same-name `#foreign` authors are NOT - `0729-modules-flat-same-name-extern` — same-name `extern` authors are NOT
rerouted (non-plain authors keep first-wins). rerouted (non-plain authors keep first-wins).
- `0730-modules-flat-same-name-default-arg` — per-source default-arg expansion. - `0730-modules-flat-same-name-default-arg` — per-source default-arg expansion.
- `0731-modules-flat-same-name-closure` — per-source `closure(fn)` + bare - `0731-modules-flat-same-name-closure` — per-source `closure(fn)` + bare

View File

@@ -8,7 +8,7 @@
> `namespaceAliasVerdict` — visible targets dispatch the member fd pinned > `namespaceAliasVerdict` — visible targets dispatch the member fd pinned
> to the TARGET module (`namespaceFnMember` + fd-keyed `bareAuthorFuncId`), > to the TARGET module (`namespaceFnMember` + fd-keyed `bareAuthorFuncId`),
> ambiguous carries diagnose loudly, and an alias that exists only beyond > ambiguous carries diagnose loudly, and an alias that exists only beyond
> one flat hop errors "namespace 'X' is not visible". Foreign/builtin/ > one flat hop errors "namespace 'X' is not visible". Extern/builtin/
> #compiler members keep the literal-symbol path. Regression tests: > #compiler members keep the literal-symbol path. Regression tests:
> `examples/0832-modules-namespace-alias-two-hop-not-visible.sx`, > `examples/0832-modules-namespace-alias-two-hop-not-visible.sx`,
> `examples/0833-modules-namespace-alias-carried-collision-ambiguous.sx`, > `examples/0833-modules-namespace-alias-carried-collision-ambiguous.sx`,

View File

@@ -108,5 +108,5 @@ Context: BLOCKS the std.sx-as-pure-re-exports restructure — `print` /
unblocked by 0120; this is the remaining known gap. Still unprobed unblocked by 0120; this is the remaining known gap. Still unprobed
for the restructure (next session, after this fix): protocol aliases for the restructure (next session, after this fix): protocol aliases
(`Allocator`, parameterized `Into`), `#builtin` decl aliases (`Allocator`, parameterized `Into`), `#builtin` decl aliases
(`size_of`, `out`, `string :: []u8`), `#foreign` decl aliases (`size_of`, `out`, `string :: []u8`), `extern` decl aliases
(`memcpy`). (`memcpy`).

View File

@@ -54,7 +54,7 @@ Legitimate flexible shapes must keep working: slice variadics
(`..xs: []T` — no upper bound), comptime/protocol packs (`..$args` / (`..xs: []T` — no upper bound), comptime/protocol packs (`..$args` /
`..xs: P` — own dispatch), default-valued params (incl. `..xs: P` — own dispatch), default-valued params (incl.
`loc: Source_Location = #caller_location`), generic `$T` fns `loc: Source_Location = #caller_location`), generic `$T` fns
(explicit vs inferred type args make the count flexible), `#foreign` (explicit vs inferred type args make the count flexible), `extern`
C variadics, `#compiler` / `#builtin` bodies. C variadics, `#compiler` / `#builtin` bodies.
## Reproduction ## Reproduction

View File

@@ -10,11 +10,11 @@
> The two GENUINE defects, both fixed: > The two GENUINE defects, both fixed:
> >
> 1. **Conflicting same-symbol redeclaration was silent.** > 1. **Conflicting same-symbol redeclaration was silent.**
> `dedupeForeignSymbol` (src/ir/lower/decl.zig) now runs at foreign > `dedupeExternSymbol` (src/ir/lower/decl.zig) now runs at extern
> registration: an EQUAL signature shares the first registration's > registration: an EQUAL signature shares the first registration's
> FuncId; a CONFLICTING one is diagnosed ("foreign symbol '<s>' is > FuncId; a CONFLICTING one is diagnosed ("extern symbol '<s>' is
> already bound with a different signature"). > already bound with a different signature").
> 2. **Foreign `-> string` / `-> ?string` returns read garbage.** The > 2. **Extern `-> string` / `-> ?string` returns read garbage.** The
> C side returns ONE `char *`; the LLVM signature declared the fat > C side returns ONE `char *`; the LLVM signature declared the fat
> `{ptr,i64}` (len = register garbage; bus error on use), and > `{ptr,i64}` (len = register garbage; bus error on use), and
> `?string` (24 B struct) was mis-declared SRET — the hidden > `?string` (24 B struct) was mis-declared SRET — the hidden
@@ -28,13 +28,13 @@
> >
> Regression tests: `examples/1221-ffi-cstring-returns.sx` (plain + > Regression tests: `examples/1221-ffi-cstring-returns.sx` (plain +
> optional non-null via strerror/strsignal + optional NULL via > optional non-null via strerror/strsignal + optional NULL via
> dlerror) and `examples/1172-diagnostics-foreign-symbol-conflict.sx` > dlerror) and `examples/1172-diagnostics-extern-symbol-conflict.sx`
> (the getenv conflict); both FAIL on pre-fix master. The extern > (the getenv conflict); both FAIL on pre-fix master. The extern
> dedupe changes IR snapshots (duplicate libc decls collapse), so the > dedupe changes IR snapshots (duplicate libc decls collapse), so the
> affected `.ir` files were regenerated. Gates: zig build test > affected `.ir` files were regenerated. Gates: zig build test
> 426/426, tests/run_examples.sh 602/602, distribution repo 21/21. > 426/426, tests/run_examples.sh 602/602, distribution repo 21/21.
> Boundary: comptime-interp (`#run`) foreign calls are untouched, and > Boundary: comptime-interp (`#run`) extern calls are untouched, and
> indirect (fn-pointer) foreign calls don't synthesize — both can > indirect (fn-pointer) extern calls don't synthesize — both can
> follow if ever needed. > follow if ever needed.
## Design contract (Agra, 2026-06-12) ## Design contract (Agra, 2026-06-12)
@@ -48,20 +48,20 @@ for every libc/sqlite-style API that returns a nullable C string
`[:0]u8` is an ALIAS for `string` (src/types.zig:145 — the `'['` arm `[:0]u8` is an ALIAS for `string` (src/types.zig:145 — the `'['` arm
returns `.string_type`), i.e. a fat ptr+len value at the sx level. At a returns `.string_type`), i.e. a fat ptr+len value at the sx level. At a
`#foreign` PARAM position the C-ABI lowering already thins it: sx `extern` PARAM position the C-ABI lowering already thins it: sx
`string`/slices coerce to a single pointer and the length is dropped `string`/slices coerce to a single pointer and the length is dropped
(src/backend/llvm/abi.zig, the `is_foreign_c_api` knob) — so (src/backend/llvm/abi.zig, the `is_extern_c_api` knob) — so
`popen :: (cmd: [:0]u8, ...)` works and matches the design contract. `popen :: (cmd: [:0]u8, ...)` works and matches the design contract.
The other two boundary positions are broken: The other two boundary positions are broken:
### Defect A — `-> [:0]u8` foreign RETURN silently resolves to `u8` ### Defect A — `-> [:0]u8` extern RETURN silently resolves to `u8`
```sx ```sx
#import "modules/std.sx"; #import "modules/std.sx";
libc :: #library "c"; libc :: #library "c";
getenv_s :: (name: [:0]u8) -> [:0]u8 #foreign libc "getenv"; getenv_s :: (name: [:0]u8) -> [:0]u8 extern libc "getenv";
main :: () -> i32 { main :: () -> i32 {
v := getenv_s("PATH"); v := getenv_s("PATH");
@@ -82,7 +82,7 @@ CLAUDE.md forbids).
#import "modules/std.sx"; #import "modules/std.sx";
libc :: #library "c"; libc :: #library "c";
getenv_opt :: (name: [:0]u8) -> ?[:0]u8 #foreign libc "getenv"; getenv_opt :: (name: [:0]u8) -> ?[:0]u8 extern libc "getenv";
main :: () -> i32 { main :: () -> i32 {
p := getenv_opt("PATH"); p := getenv_opt("PATH");
@@ -104,11 +104,11 @@ a panic.
1. Make the bracket spelling resolve identically everywhere — return 1. Make the bracket spelling resolve identically everywhere — return
position and optional child position must hit the same alias table position and optional child position must hit the same alias table
that param position does (Defect A and the resolution half of B). that param position does (Defect A and the resolution half of B).
2. Implement the boundary contract for returns: a foreign 2. Implement the boundary contract for returns: a extern
`-> [:0]u8` / `-> ?[:0]u8` receives ONE pointer from C; the sx-side `-> [:0]u8` / `-> ?[:0]u8` receives ONE pointer from C; the sx-side
`string` is built by synthesizing the length (strlen) at the `string` is built by synthesizing the length (strlen) at the
boundary, and for the optional a NULL pointer maps to `null`. boundary, and for the optional a NULL pointer maps to `null`.
If (2) is deferred, foreign string/optional-string RETURNS must be If (2) is deferred, extern string/optional-string RETURNS must be
rejected with a diagnostic naming the workaround (`?*u8`). rejected with a diagnostic naming the workaround (`?*u8`).
## Workaround in use ## Workaround in use

View File

@@ -23,9 +23,9 @@
A `#library` declaration in a module that is reached through TWO (or A `#library` declaration in a module that is reached through TWO (or
more) levels of aliased `#import` never makes it into the build's more) levels of aliased `#import` never makes it into the build's
library list: `sx build` emits no `-l<name>` on the link line (link library list: `sx build` emits no `-l<name>` on the link line (link
fails with `Undefined symbols` for every `#foreign` fn of that fails with `Undefined symbols` for every `extern` fn of that
library), and `sx run` skips the dlopen of that library (the JIT then library), and `sx run` skips the dlopen of that library (the JIT then
resolves the foreign symbols only if some already-loaded image happens resolves the extern symbol only if some already-loaded image happens
to export them). to export them).
- Observed: `main → b :: #import "b.sx" → c :: #import "c.sx"` where - Observed: `main → b :: #import "b.sx" → c :: #import "c.sx"` where
@@ -44,11 +44,11 @@ to export them).
Three files in one directory; build `a.sx`. Three files in one directory; build `a.sx`.
```sx ```sx
// c.sx — declares the library + a foreign fn // c.sx — declares the library + a extern fn
#import "modules/std.sx"; #import "modules/std.sx";
zlib :: #library "z"; zlib :: #library "z";
zlibVersion :: () -> ?cstring #foreign zlib "zlibVersion"; zlibVersion :: () -> ?cstring extern zlib "zlibVersion";
zver :: () -> string { zver :: () -> string {
p := zlibVersion(); p := zlibVersion();
@@ -91,7 +91,7 @@ Found in the distribution repo the first time a product chain nested
the SQLite bindings two aliases deep: `dist.sx → ops :: #import the SQLite bindings two aliases deep: `dist.sx → ops :: #import
"release/ops.sx" → db :: #import "../repo/db.sx" → #import "release/ops.sx" → db :: #import "../repo/db.sx" → #import
"../db/sqlite.sx"` loses `-lsqlite3` even though the bindings compile "../db/sqlite.sx"` loses `-lsqlite3` even though the bindings compile
fine (the foreign wrappers ARE in main.o; only the link flag is gone). fine (the extern wrappers ARE in main.o; only the link flag is gone).
## Investigation prompt ## Investigation prompt