From 6a7f6902b86c77fa67e00955ab1b068c5f97b6d4 Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 18 Jun 2026 18:09:46 +0300 Subject: [PATCH] comptime VM: extern slice/string args (-> NUL-term char*) + float guards (Phase 4D.2) Extract marshalExternArg: a scalar/pointer word passes verbatim (a cstring arg already works as a pointer word via 4D.1); a string/slice {ptr,len} fat pointer is copied into a NUL-terminated arena buffer and its char* passed -- mirrors the legacy marshalExternArg, and is what the bundler's popen(cmd: [:0]u8, ...) needs. Add float guards on args AND returns: floats are kindOf == .word but the host_ffi trampolines have no float variant, so bail loudly rather than miscall through an integer register (the legacy interp doesn't support float FFI either -> parity). New example 0637-comptime-extern-slice-arg (#run strlen("hello, world") with a [:0]u8 param -> 12) runs HANDLED on the VM, byte-matching legacy. 699/0 both gates. The FFI escape now covers scalar/pointer/cstring/slice args + scalar/pointer returns. --- current/CHECKPOINT-COMPILER-API.md | 12 +++++++ examples/0637-comptime-extern-slice-arg.sx | 14 ++++++++ .../0637-comptime-extern-slice-arg.exit | 1 + .../0637-comptime-extern-slice-arg.stderr | 1 + .../0637-comptime-extern-slice-arg.stdout | 1 + src/ir/comptime_vm.zig | 35 ++++++++++++++++--- 6 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 examples/0637-comptime-extern-slice-arg.sx create mode 100644 examples/expected/0637-comptime-extern-slice-arg.exit create mode 100644 examples/expected/0637-comptime-extern-slice-arg.stderr create mode 100644 examples/expected/0637-comptime-extern-slice-arg.stdout diff --git a/current/CHECKPOINT-COMPILER-API.md b/current/CHECKPOINT-COMPILER-API.md index 37757205..ee820f26 100644 --- a/current/CHECKPOINT-COMPILER-API.md +++ b/current/CHECKPOINT-COMPILER-API.md @@ -352,6 +352,18 @@ when reached (sentinels or accessor fns; see the design doc Risks). `List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.) ## Log +- **Phase 4D.2 (VM plan) — extern SLICE/string args (→ NUL-terminated `char*`) + float guards (2026-06-18).** + Extracted `marshalExternArg`: a scalar/pointer WORD passes verbatim (a `cstring` arg already works + as a pointer word via 4D.1); a `string`/slice `{ptr,len}` fat pointer is copied into a + NUL-terminated arena buffer and its `char*` passed (mirrors legacy `marshalExternArg` — what the + bundler's `popen(cmd: [:0]u8, …)` needs). Added FLOAT guards on args AND returns: floats are + `kindOf == .word` but the host_ffi trampolines have no float variant, so they bail loudly rather + than miscall through an integer register (the legacy interp doesn't support float FFI either, so + parity holds — no corpus float-FFI example exists). New example `0637-comptime-extern-slice-arg` + (`#run strlen("hello, world")` with a `[:0]u8` param → 12) runs **HANDLED on the VM**, byte-matching + legacy. **699/0 BOTH gates.** On `reify`. The FFI escape is now complete for scalar/pointer/cstring/ + slice args + scalar/pointer returns — enough for the bundler's libc surface. **Next (4D.3):** + `compiler_call` (#compiler hooks — 0602/0603), the last legacy-only role besides #insert/bundler. - **Phase 4D.1 (VM plan) — general host-FFI escape: the VM calls any extern libc fn via dlsym + host_ffi (2026-06-18).** Replaced the "extern not ported → bail" stub in `Vm.invoke` with `callHostExtern`: resolve the symbol via `host_ffi.lookupSymbol` (dlsym RTLD_DEFAULT) and dispatch through the `host_ffi` diff --git a/examples/0637-comptime-extern-slice-arg.sx b/examples/0637-comptime-extern-slice-arg.sx new file mode 100644 index 00000000..b4b57de3 --- /dev/null +++ b/examples/0637-comptime-extern-slice-arg.sx @@ -0,0 +1,14 @@ +// Comptime host-FFI with a SLICE argument: a `#run` calling a libc function whose +// parameter is a `[:0]u8` (a `{ptr,len}` fat pointer), not a bare `cstring`. The VM +// marshals the fat pointer to a NUL-terminated `char*` before the call (Phase 4D.2), +// mirroring the legacy interpreter. (A bare `cstring` arg already passes as a word.) +#import "modules/std.sx"; + +strlen :: (s: [:0]u8) -> usize extern libc; + +LEN :: #run strlen("hello, world"); + +main :: () -> i32 { + print("len={}\n", LEN); + return 0; +} diff --git a/examples/expected/0637-comptime-extern-slice-arg.exit b/examples/expected/0637-comptime-extern-slice-arg.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0637-comptime-extern-slice-arg.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0637-comptime-extern-slice-arg.stderr b/examples/expected/0637-comptime-extern-slice-arg.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0637-comptime-extern-slice-arg.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0637-comptime-extern-slice-arg.stdout b/examples/expected/0637-comptime-extern-slice-arg.stdout new file mode 100644 index 00000000..4a2587d8 --- /dev/null +++ b/examples/expected/0637-comptime-extern-slice-arg.stdout @@ -0,0 +1 @@ +len=12 diff --git a/src/ir/comptime_vm.zig b/src/ir/comptime_vm.zig index f99f2bb1..1241749a 100644 --- a/src/ir/comptime_vm.zig +++ b/src/ir/comptime_vm.zig @@ -1059,16 +1059,15 @@ pub const Vm = struct { var packed_args: [8]usize = undefined; for (args, 0..) |a, i| { - const aty = try self.refTy(ref_types, a); - if (kindOf(table, aty) != .word) - return self.failMsg("comptime extern call: non-word (aggregate/string/float) arg not yet marshaled on the VM"); - packed_args[i] = @intCast(frame.get(a.index())); // scalar bits OR host pointer + packed_args[i] = try self.marshalExternArg(table, try self.refTy(ref_types, a), frame.get(a.index())); } const argv = packed_args[0..args.len]; const fixed = callee.params.len; const variadic = callee.is_variadic and args.len > fixed; const ret = callee.ret; + if (isFloat(ret)) + return self.failMsg("comptime extern call: float return not supported (host_ffi has no float trampoline)"); if (ret == .void or ret == .noreturn) { if (variadic) host_ffi.callVoidRetVar(symbol, fixed, argv) catch return self.failMsg("comptime extern call failed (void)") @@ -1096,6 +1095,34 @@ pub const Vm = struct { return @as(Reg, r); } + /// Marshal one extern arg (of IR type `aty`, register value `reg`) to the `usize` + /// the host_ffi trampolines expect. A scalar/pointer WORD passes verbatim (a + /// pointer Reg is already a host pointer). A string/slice fat-pointer is copied + /// into a NUL-terminated buffer and its `char*` passed (mirrors the legacy + /// `marshalExternArg`). Floats (no float trampoline) and non-fat-pointer + /// aggregates bail loudly — never a silent miscall. + fn marshalExternArg(self: *Vm, table: *const types.TypeTable, aty: TypeId, reg: Reg) Error!usize { + switch (kindOf(table, aty)) { + .word => { + if (isFloat(aty)) + return self.failMsg("comptime extern call: float arg not supported (host_ffi has no float trampoline)"); + return @intCast(reg); // scalar bits OR host pointer + }, + .aggregate => { + // Only a string/slice `{ptr, len}` fat pointer marshals (→ a + // NUL-terminated `char*`); any other aggregate bails. + if (aty != .string and (aty.isBuiltin() or table.get(aty) != .slice)) + return self.failMsg("comptime extern call: non-string/slice aggregate arg not marshaled on the VM"); + const n: usize = @intCast(try self.sliceLen(reg)); + const data = try self.sliceData(table, reg); + const buf = self.machine.allocBytes(n + 1, 1); // zeroed → NUL at [n] + if (n > 0) @memcpy(try self.machine.bytes(buf, n), try self.machine.bytes(data, n)); + return @intCast(buf); + }, + .unsupported => return self.failMsg("comptime extern call: unsupported arg type"), + } + } + /// Largest single comptime allocation the VM will service natively. A bogus / /// pathological comptime `malloc` above this bails to the legacy path (which /// calls real libc) rather than OOM-panicking the compiler via `allocBytes`.