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).
128 lines
5.5 KiB
Markdown
128 lines
5.5 KiB
Markdown
# RESOLVED — 0128: `[:0]u8` at FFI boundaries — conflicting symbol views, garbage string returns
|
|
|
|
> **RESOLVED** (2026-06-12). Investigation corrected the filing: the
|
|
> "silent `u8` return" and the "`?[:0]u8` unresolved panic" were BOTH
|
|
> artifacts of the reproducers binding the C symbol `getenv`, which
|
|
> std/process.sx already declares as `-> *u8` — the FIRST registration
|
|
> of a C symbol silently won and every call through the later
|
|
> declaration was typed by the older signature (`*u8`), cascading into
|
|
> the panic. `?[:0]u8` itself resolves correctly (it is `?string`).
|
|
> The two GENUINE defects, both fixed:
|
|
>
|
|
> 1. **Conflicting same-symbol redeclaration was silent.**
|
|
> `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 ("extern symbol '<s>' is
|
|
> already bound with a different signature").
|
|
> 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
|
|
> out-pointer landed in the C callee's first argument register.
|
|
> Now: such returns are classified by `cstrRetKind`
|
|
> (src/ir/emit_llvm.zig), declared as plain `ptr` returns (never
|
|
> sret), and the call site synthesizes the sx value via
|
|
> `cstrReturnToSx`: `{ptr, strlen(ptr)}` with the strlen call
|
|
> branch-guarded (NULL → `{null,0}`), wrapped in `{string, i1}`
|
|
> with `has = ptr != null` for the optional.
|
|
>
|
|
> Regression tests: `examples/1221-ffi-cstring-returns.sx` (plain +
|
|
> optional non-null via strerror/strsignal + optional NULL via
|
|
> 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`) 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)
|
|
|
|
`?[:0]u8` should lower to a `char *` at the FFI boundary — a single
|
|
nullable thin pointer — NOT a fat pointer. This is the natural sx type
|
|
for every libc/sqlite-style API that returns a nullable C string
|
|
(`getenv`, `sqlite3_errmsg`, `sqlite3_column_text`, ...).
|
|
|
|
## Current state (verified at master 1bc60d3)
|
|
|
|
`[: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
|
|
`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_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` extern RETURN silently resolves to `u8`
|
|
|
|
```sx
|
|
#import "modules/std.sx";
|
|
|
|
libc :: #library "c";
|
|
getenv_s :: (name: [:0]u8) -> [:0]u8 extern libc "getenv";
|
|
|
|
main :: () -> i32 {
|
|
v := getenv_s("PATH");
|
|
print("len={}\n", v.len); // error: field 'len' not found on type 'u8'
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
The declared return type `[:0]u8` resolves to plain `u8` — not the
|
|
`string` alias, not an error. Whatever the return-position type-name
|
|
path is, it disagrees with the table in types.zig and fails SILENTLY
|
|
into a wrong type (the silent-fallback-default class this repo's
|
|
CLAUDE.md forbids).
|
|
|
|
### Defect B — `?[:0]u8` does not resolve; panics LLVM emission
|
|
|
|
```sx
|
|
#import "modules/std.sx";
|
|
|
|
libc :: #library "c";
|
|
getenv_opt :: (name: [:0]u8) -> ?[:0]u8 extern libc "getenv";
|
|
|
|
main :: () -> i32 {
|
|
p := getenv_opt("PATH");
|
|
if p == null { return 1; }
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
Panics: "unresolved type reached LLVM emission"
|
|
(src/backend/llvm/types.zig:175). Meanwhile plain `?string` resolves
|
|
and works at the sx level — the `'?'` arm (types.zig:141) stores
|
|
`child_name = "[:0]u8"`, and whatever resolves the child later doesn't
|
|
go through the same table that maps the bracket spelling to
|
|
`string_type`. Minimum bar even without the feature: a diagnostic, not
|
|
a panic.
|
|
|
|
## Fix shape
|
|
|
|
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 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, extern string/optional-string RETURNS must be
|
|
rejected with a diagnostic naming the workaround (`?*u8`).
|
|
|
|
## Workaround in use
|
|
|
|
/Users/agra/projects/distribution/src/db/sqlite.sx declares every
|
|
nullable C-string return as `?*u8` (null-pointer niche verified working
|
|
in both JIT and AOT) and copies via a manual strlen helper; its header
|
|
comment cites this issue's gap. std's own `getenv :: -> *u8` +
|
|
cast-check is the same dodge. Both can migrate to `?[:0]u8` once the
|
|
contract lands.
|
|
|
|
## Verification
|
|
|
|
Both reproducers behave: A prints a real PATH length; B answers null /
|
|
non-null correctly for missing / present variables. `zig build test`,
|
|
`tests/run_examples.sh` green; pin both as examples.
|