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
> `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
> `examples/0706-modules-import-non-transitive.sx`.
## 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
transitively — using one should produce the "not visible; #import the module that
declares it" diagnostic.

View File

@@ -9,7 +9,7 @@
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
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).
```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
them through; cross-file `extern` globals would drop that ceremony. Distinct from
the existing `name : T #foreign;` form (an *external C* data symbol from
libsystem etc. — see `examples/1205-ffi-foreign-global.sx`); this request is for
the existing `name : T extern;` form (an *external C* data symbol from
libsystem etc. — see `examples/1205-ffi-extern-global.sx`); this request is for
sx-defined globals shared across sx modules.
## 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
(`sx build --target ios-sim issue-0043.sx` with a
@@ -6,7 +6,7 @@
`inline if OS == .ios { ... }`-gated function called transitively
from `caller :: (...) callconv(.c)`) compiles cleanly today; chess
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.
Below preserved as a record of the original problem.
@@ -53,7 +53,7 @@ lowerExpr
So when the *outer* `lowerCall` decides to lazy-lower
`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
uikit.sx and resolves fine for non-lazy lowering paths (`sx build`
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/compiler.sx";
UIWindow :: #foreign #objc_class("UIWindow") {
UIWindow :: #objc_class("UIWindow") extern {
alloc :: () -> *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) {
win := UIWindow.alloc().initWithWindowScene(scene);
_ = win;
@@ -104,30 +104,30 @@ Suspected area: `lazyLowerFunction` at
[src/ir/lower.zig:1057](../src/ir/lower.zig#L1057) and the field-
access method dispatch at
[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:
1. `lazyLowerFunction` swaps some piece of lowering state on entry
(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.
2. The receiver type for `win` (`*UIWindow`) isn't being resolved to
its `getStructTypeName` correctly during lazy lowering — possibly
`inferExprType` for the lazy-lowered context resolves to an
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
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
declarations haven't been seen yet (cross-module ordering).
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
dispatch).
- 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.
- If the map is NOT populated, find the seeding path and fix the
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
`#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
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

View File

@@ -31,8 +31,8 @@ method declaration — by position, not by hardcoded name.
#import "modules/std.sx";
#import "modules/std/objc.sx";
// Foreign declaration so we can dispatch.
NSObject :: #foreign #objc_class("NSObject") {
// Extern declaration so we can dispatch.
NSObject :: #objc_class("NSObject") extern {
class :: () -> *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
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:
(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`,
so `xx this` appeared inside the body of a `-> BOOL` method.
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
`resolveCallParamTypes(c)`. For UFCS dispatch on a foreign-class
alias, that function had no path covering `foreign_class_map`
`resolveCallParamTypes(c)`. For UFCS dispatch on a runtime-class
alias, that function had no path covering `runtime_class_map`
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
skipped, so `self.target_type` retained its previous value: the
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
plus UIKit's specific validation order).
**Fix:** add a `foreign_class_map.get(sname)`
`findForeignMethodInChain` path to `resolveCallParamTypes`. When the
UFCS receiver is a foreign-class alias, walk the `#extends` chain to
**Fix:** add a `runtime_class_map.get(sname)`
`findRuntimeMethodInChain` path to `resolveCallParamTypes`. When the
UFCS receiver is a runtime-class alias, walk the `#extends` chain to
find the method, then resolve its declared param types (skipping the
implicit `*Self` for instance methods). With the fix, `param_types`
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
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
mechanical textual change with identical semantics. This bug
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`,
so `read_file_bytes`
([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 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`
during development isn't affected — or gate on "the base path differs from
CWD and contains an `assets/` dir").
- Add the `#foreign` decl for `SDL_GetBasePath` (returns `*u8`, SDL-owned) and
call `chdir` (already used by uikit.sx — reuse the same `#foreign`).
- Add the `extern` decl for `SDL_GetBasePath` (returns `*u8`, SDL-owned) and
call `chdir` (already used by uikit.sx — reuse the same `extern`).
Alternative (no SDL dependency): `_NSGetExecutablePath` + `dirname`, same as a
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
`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
`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

View File

@@ -8,7 +8,7 @@
> 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
> 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
> 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

View File

@@ -74,7 +74,7 @@ Suspected area:
Likely fix:
- Change `collectDeclaredTypeNames` / `harvestScopeDecls` so only declarations
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.
- Do not add arbitrary value const names to the type-name set.
- 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.
- Do not regress issue 0070: `A :: B; B :: i32; g : A = 7;` and
`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:
- 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
> argument's IR type via `getRefIRType(arg_ref) orelse .void` — a silent fallback
> 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
> `LLVMEmitter.argIRTypeOrFail` ([src/ir/emit_llvm.zig]) returns the dedicated
> `.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
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
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.
**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)
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
"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
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
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
@@ -81,4 +81,4 @@ loud-failure path, see below.)
> 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
> (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`,
> `IfExpr`/`WhileExpr.binding_span`, `ForExpr.capture_span`/`index_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
> the caret underlines the offending identifier itself instead of the enclosing
> 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
> ruling; the final shape is the **universal raw identifier** (attempt 4):
@@ -13,9 +13,9 @@
> declaration node ([src/ast.zig]): `VarDecl` / `ConstDecl` / `Param` / `FnDecl`
> plus `IfExpr` / `WhileExpr` optional bindings, `ForExpr` capture + index,
> `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` /
> `ErrorSetDecl` / `ProtocolDecl` / `ForeignClassDecl` / `UfcsAlias` /
> `ErrorSetDecl` / `ProtocolDecl` / `RuntimeClassDecl` / `UfcsAlias` /
> `NamespaceDecl` / `ImportDecl` / `CImportDecl` / `LibraryDecl`.
>
> - **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
> more exempt than a free function (cf. `examples/1122`). Pinned by
> `examples/0158-types-reserved-name-member-exempt.sx`.
> 2. **`#import c` foreign-name exemption.** `c_import.zig` synthesizes foreign
> `#foreign` decls with `Param.is_raw = true` (and the synthesized `FnDecl`
> 2. **`#import c` extern-name exemption.** `c_import.zig` synthesizes extern
> `extern` decls with `Param.is_raw = true` (and the synthesized `FnDecl`
> `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
> 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)`
> is not a cast).
>
@@ -107,8 +107,8 @@
> fields / union tag / protocol method signature — read & written bare and via
> backtick; impl method definition takes the backtick),
> `examples/1054-errors-backtick-reserved-binding.sx` (`catch`/`onfail` tag
> bindings), `examples/1220-ffi-c-import-reserved-name-params.{sx,h,c}` (foreign
> param + fn-name exemption, bare-callable foreign fn); negatives
> bindings), `examples/1220-ffi-c-import-reserved-name-params.{sx,h,c}` (extern
> param + fn-name exemption, bare-callable extern fn); negatives
> `examples/1119`/`1121`/`1123` (bare reserved binding across forms),
> `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
@@ -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
mechanisms:
1. **Auto-exempt imports.** `#import c` (and other foreign) declarations are
treated as RAW identifiers: foreign names are never type-classified and never
1. **Auto-exempt imports.** `#import c` (and other extern) declarations are
treated as RAW identifiers: extern names are never type-classified and never
reserved-checked, so generated bindings "just work" with zero user edits.
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

View File

@@ -21,7 +21,7 @@ the same colliding entry.
`ns.fn` — they just had nothing to find. `NamespaceDecl` carries the
module's `own_decls` (populated in `imports.addNamespace`) so the
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
`resolveFuncByName` path, so a qualified alias would strand their
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.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
paths; they are unchanged.)

View File

@@ -43,7 +43,7 @@ collision:
`resolveBareCallee(name, caller_file) -> .func(ResolvedAuthor) | .ambiguous |
.none` (`src/ir/lower.zig`). It returns `.none` whenever the outcome would
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
reroutes: own-author wins; else the caller's flat-reachable authors — `≥2`
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.
- `0728-modules-flat-same-name-paramtype` — per-source parameter target typing
(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).
- `0730-modules-flat-same-name-default-arg` — per-source default-arg expansion.
- `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
> to the TARGET module (`namespaceFnMember` + fd-keyed `bareAuthorFuncId`),
> 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:
> `examples/0832-modules-namespace-alias-two-hop-not-visible.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
for the restructure (next session, after this fix): protocol aliases
(`Allocator`, parameterized `Into`), `#builtin` decl aliases
(`size_of`, `out`, `string :: []u8`), `#foreign` decl aliases
(`size_of`, `out`, `string :: []u8`), `extern` decl aliases
(`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: P` — own dispatch), default-valued params (incl.
`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.
## Reproduction

View File

@@ -10,11 +10,11 @@
> The two GENUINE defects, both fixed:
>
> 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
> 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").
> 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
> `{ptr,i64}` (len = register garbage; bus error on use), and
> `?string` (24 B struct) was mis-declared SRET — the hidden
@@ -28,13 +28,13 @@
>
> Regression tests: `examples/1221-ffi-cstring-returns.sx` (plain +
> 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
> dedupe changes IR snapshots (duplicate libc decls collapse), so the
> affected `.ir` files were regenerated. Gates: zig build test
> 426/426, tests/run_examples.sh 602/602, distribution repo 21/21.
> Boundary: comptime-interp (`#run`) foreign calls are untouched, and
> indirect (fn-pointer) foreign calls don't synthesize — both can
> Boundary: comptime-interp (`#run`) extern calls are untouched, and
> indirect (fn-pointer) extern calls don't synthesize — both can
> follow if ever needed.
## 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
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
(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.
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
#import "modules/std.sx";
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 {
v := getenv_s("PATH");
@@ -82,7 +82,7 @@ CLAUDE.md forbids).
#import "modules/std.sx";
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 {
p := getenv_opt("PATH");
@@ -104,11 +104,11 @@ a panic.
1. Make the bracket spelling resolve identically everywhere — return
position and optional child position must hit the same alias table
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
`string` is built by synthesizing the length (strlen) at the
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`).
## Workaround in use

View File

@@ -23,9 +23,9 @@
A `#library` declaration in a module that is reached through TWO (or
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
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
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).
- 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`.
```sx
// c.sx — declares the library + a foreign fn
// c.sx — declares the library + a extern fn
#import "modules/std.sx";
zlib :: #library "z";
zlibVersion :: () -> ?cstring #foreign zlib "zlibVersion";
zlibVersion :: () -> ?cstring extern zlib "zlibVersion";
zver :: () -> string {
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
"release/ops.sx" → db :: #import "../repo/db.sx" → #import
"../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