issues: file 0128 — [:0]u8 FFI returns silently u8; ?[:0]u8 unresolved panic
[:0]u8 aliases string (fat) and params already ABI-thin to char*, but a foreign -> [:0]u8 return silently resolves to plain u8, and ?[:0]u8 never resolves at all (LLVM emission panic) even though ?string works. Design contract recorded: ?[:0]u8 lowers to a nullable char* at the boundary, length synthesized on the sx side; until then such returns must be diagnosed, not mis-typed.
This commit is contained in:
90
issues/0128-cstring-ffi-boundary-returns-and-optional.md
Normal file
90
issues/0128-cstring-ffi-boundary-returns-and-optional.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# 0128: `[:0]u8` at FFI boundaries — silent `u8` returns, unresolvable optional
|
||||
|
||||
## 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
|
||||
`#foreign` 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
|
||||
`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`
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
libc :: #library "c";
|
||||
getenv_s :: (name: [:0]u8) -> [:0]u8 #foreign 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 #foreign 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 foreign
|
||||
`-> [: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
|
||||
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.
|
||||
Reference in New Issue
Block a user