lang: introduce cstring — the C-boundary string (Odin model)

cstring is ONE pointer to a null-terminated u8 buffer, C's char*: thin
(8 bytes, no length; cstring_len walks to the terminator), crossing
#foreign boundaries verbatim in both directions, with ?cstring as the
nullable case lowering to the same bare pointer (null = absent).

Conversion discipline mirrors Odin: a string LITERAL coerces implicitly
(its bytes are terminated constants); any other string is rejected with
a diagnostic naming to_cstring (it may be an unterminated view); and
cstring never coerces to string implicitly — from_cstring(c) is the
explicit zero-copy view, pricing the strlen.

Plumbing: TypeId/TypeInfo builtin slot 18 (first_user 19), name
classifiers, size/align/name tables, LLVM ptr lowering, the ?T pointer
niche, the xx pointer ladder, the literal-gated coercion plan
(isConstString + data_ptr), and the reserved-spelling set. std gains
cstring_len/from_cstring/to_cstring (fmt.sx, re-exported); the old
cstring(size) allocator helper is renamed alloc_string everywhere;
getenv migrates to (name: cstring) -> ?cstring as the canonical user
and env() drops its manual strlen/memcpy.

Pinned: examples/1222 (FFI both directions, literal coercion,
?cstring null paths, round trip) and examples/1173 (both coercion
diagnostics); FAIL pre-feature. The alloc_string rename + getenv
signature shift the .ir snapshots — regenerated. zig build test
426/426; run_examples 604/604.

Spec: reserved spelling + cstring section + C-interop rows.
This commit is contained in:
agra
2026-06-12 14:50:53 +03:00
parent d88bdd7242
commit 1d17b0abcf
58 changed files with 26437 additions and 25257 deletions

View File

@@ -204,7 +204,7 @@ bundle_main :: () -> bool {
// buffer for libc / `[:0]u8` callees. Allocated from
// `context.allocator` like the rest of the bundling stage.
str_to_cstr :: (s: string) -> [:0]u8 {
buf := cstring(s.len);
buf := alloc_string(s.len);
memcpy(buf.ptr, s.ptr, s.len);
buf
}
@@ -927,7 +927,7 @@ MANIFEST, pkg_esc, lib_esc, lib_esc, lib_esc)
// `co/swipelab/sxchess/SxApp` → `co.swipelab.sxchess.SxApp`.
slash_to_dot :: (path: string) -> string {
buf := cstring(path.len);
buf := alloc_string(path.len);
i := 0;
while i < path.len {
c := path[i];

View File

@@ -69,7 +69,10 @@ optional_to_string :: fmt.optional_to_string;
concat :: fmt.concat;
substr :: fmt.substr;
path_join :: fmt.path_join;
cstring :: fmt.cstring;
alloc_string :: fmt.alloc_string;
cstring_len :: fmt.cstring_len;
from_cstring :: fmt.from_cstring;
to_cstring :: fmt.to_cstring;
alloc_slice :: fmt.alloc_slice;
// fmt internals, re-exported only because they were always part of the

View File

@@ -6,7 +6,7 @@
// --- Slice & string allocation ---
cstring :: (size: i64) -> string {
alloc_string :: (size: i64) -> string {
raw := context.allocator.alloc_bytes(size + 1);
memset(raw, 0, size + 1);
s : string = ---;
@@ -31,7 +31,7 @@ int_to_string :: (n: i64) -> string {
// overflows for i64::MIN (its magnitude is unrepresentable as a
// positive i64). sx `%` truncates toward zero, so `n % 10` keeps n's
// sign; take each remainder's absolute value for the digit.
tmp := cstring(20);
tmp := alloc_string(20);
i := 19;
v := n;
while v != 0 {
@@ -56,7 +56,7 @@ uint_to_string :: (n: i64) -> string {
// next limb; the per-step accumulator stays well within i64
// (max 9*65536 + 65535), so signed `/` and `%` are exact.
g := decompose_u16x4(n);
tmp := cstring(20);
tmp := alloc_string(20);
i := 19;
while g[0] != 0 or g[1] != 0 or g[2] != 0 or g[3] != 0 {
rem := 0;
@@ -89,7 +89,7 @@ float_to_string :: (f: f64) -> string {
fl := fstr.len;
prefix := if neg then 1 else 0;
total := prefix + il + 1 + 6;
buf := cstring(total);
buf := alloc_string(total);
pos := 0;
if neg { buf[0] = 45; pos = 1; }
memcpy(@buf[pos], istr.ptr, il);
@@ -144,7 +144,7 @@ int_to_hex_string :: (n: i64) -> string {
if n == 0 { return "0"; }
g := decompose_u16x4(n);
buf := cstring(16);
buf := alloc_string(16);
hex_group(buf, 0, g[0]);
hex_group(buf, 4, g[1]);
hex_group(buf, 8, g[2]);
@@ -162,18 +162,48 @@ int_to_hex_string :: (n: i64) -> string {
concat :: (a: string, b: string) -> string {
al := a.len;
bl := b.len;
buf := cstring(al + bl);
buf := alloc_string(al + bl);
memcpy(buf.ptr, a.ptr, al);
memcpy(@buf[al], b.ptr, bl);
buf
}
substr :: (s: string, start: i64, len: i64) -> string {
buf := cstring(len);
buf := alloc_string(len);
memcpy(buf.ptr, @s[start], len);
buf
}
// ── cstring: the C-boundary string ────────────────────────────────────
// `cstring` is ONE pointer to a null-terminated u8 buffer — C's `char *`.
// It carries no length (`cstring_len` walks to the terminator) and
// crosses `#foreign` boundaries verbatim in both directions; `?cstring`
// is the nullable case (null pointer = absent). String LITERALS coerce
// to `cstring` implicitly — their bytes are terminated constants; every
// other `string` must materialize through `to_cstring`.
// Byte length of `c` (strlen — O(n), walks to the terminator).
cstring_len :: (c: cstring) -> i64 {
p : [*]u8 = xx c;
n := 0;
while p[n] != 0 { n += 1; }
n
}
// A zero-copy string VIEW over `c`'s bytes ({ptr, strlen}). The view
// shares C's buffer — `substr` it if it must outlive the source.
from_cstring :: (c: cstring) -> string {
p : [*]u8 = xx c;
string.{ ptr = p, len = cstring_len(c) }
}
// An owned, terminated copy of `s` as a `cstring`.
to_cstring :: (s: string) -> cstring {
z := alloc_string(s.len);
memcpy(z.ptr, s.ptr, s.len);
xx z.ptr
}
// Join path components with the POSIX separator ('/'). Skips empty
// components and collapses duplicate separators at component
// boundaries. Used for bundle paths where Apple .app and Android APK

View File

@@ -130,7 +130,7 @@ read_file :: (path: [:0]u8) -> ?string {
size := lseek(fd, 0, SEEK_END);
if size < 0 { close(fd); return null; }
lseek(fd, 0, SEEK_SET);
buf := cstring(size);
buf := alloc_string(size);
n := read(fd, buf.ptr, xx size);
close(fd);
if cast(i64) n != size { return null; }
@@ -191,7 +191,7 @@ create_dir_all :: (path: [:0]u8) -> bool {
last -= 1;
}
if last > 0 {
parent := cstring(last);
parent := alloc_string(last);
memcpy(parent.ptr, path.ptr, last);
if !create_dir_all(parent) { return false; }
}
@@ -211,7 +211,7 @@ copy_file :: (src: [:0]u8, dst: [:0]u8) -> bool {
return false;
}
ok := true;
buf := cstring(65536);
buf := alloc_string(65536);
loop := true;
while loop {
n := read(src_fd, buf.ptr, 65536);

View File

@@ -24,8 +24,7 @@ popen :: (cmd: [:0]u8, mode: [:0]u8) -> *void #foreign libc;
pclose :: (stream: *void) -> i32 #foreign libc;
fread :: (ptr: [*]u8, size: usize, nmemb: usize, stream: *void) -> usize #foreign libc;
feof :: (stream: *void) -> i32 #foreign libc;
getenv :: (name: [:0]u8) -> *u8 #foreign libc;
strlen :: (s: *u8) -> usize #foreign libc;
getenv :: (name: cstring) -> ?cstring #foreign libc;
system :: (cmd: [:0]u8) -> i32 #foreign libc;
// ── Public types ─────────────────────────────────────────────────────
@@ -53,7 +52,7 @@ run :: (cmd: [:0]u8) -> ?ProcessResult {
if cast(i64) f == 0 { return null; }
out := "";
buf := cstring(4096);
buf := alloc_string(4096);
loop := true;
while loop {
n := fread(buf.ptr, 1, 4096, f);
@@ -84,14 +83,11 @@ run :: (cmd: [:0]u8) -> ?ProcessResult {
// Read an environment variable. Returns null if unset; an empty
// string if set to "".
env :: (name: [:0]u8) -> ?string {
p := getenv(name);
addr : i64 = xx p;
if addr == 0 { return null; }
n := strlen(p);
if n == 0 { return ""; }
buf := cstring(cast(i64) n);
memcpy(buf.ptr, xx p, cast(i64) n);
buf
p := getenv(to_cstring(name));
if p == null { return null; }
v := from_cstring(p!);
if v.len == 0 { return ""; }
substr(v, 0, v.len)
}
// Locate an executable by walking `$PATH`. Returns the absolute path
@@ -103,7 +99,7 @@ find_executable :: (name: [:0]u8) -> ?string {
// (executable names like `codesign`, `javac`, `aapt2`).
cmd := concat("command -v ", name);
// Need null-terminated for popen.
cmd_z := cstring(cmd.len);
cmd_z := alloc_string(cmd.len);
memcpy(cmd_z.ptr, cmd.ptr, cmd.len);
if r := run(cmd_z) {
if r.exit_code != 0 { return null; }