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.
This commit is contained in:
@@ -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`
|
||||
|
||||
14
examples/0637-comptime-extern-slice-arg.sx
Normal file
14
examples/0637-comptime-extern-slice-arg.sx
Normal file
@@ -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;
|
||||
}
|
||||
1
examples/expected/0637-comptime-extern-slice-arg.exit
Normal file
1
examples/expected/0637-comptime-extern-slice-arg.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
examples/expected/0637-comptime-extern-slice-arg.stderr
Normal file
1
examples/expected/0637-comptime-extern-slice-arg.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/expected/0637-comptime-extern-slice-arg.stdout
Normal file
1
examples/expected/0637-comptime-extern-slice-arg.stdout
Normal file
@@ -0,0 +1 @@
|
||||
len=12
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user