comptime VM: support optional-of-word extern returns + string/any struct_init

Two host-FFI gaps surfaced by the sx Android bundler running on the VM
(default_pipeline calls env() -> getenv() -> ?cstring, and from_cstring builds
a string literal):

- callHostExtern: an extern returning an OPTIONAL whose child is a single
  register word (e.g. getenv() -> ?cstring) now wraps the bare C payload word
  into the {payload@0, has@sizeof(child)} optional aggregate (present iff
  non-null), mirroring emit_llvm's char*->?cstring handling. Previously bailed
  'non-word return'. The non-word bail now names the symbol + return type.
- struct_init: the builtin two-word aggregates string ({ptr,len}) and any
  ({tag,value}) can now be struct_init'd (e.g. string.{ ptr=, len= } in
  from_cstring). Previously bailed 'struct_init at a builtin result type'.

These let the full Android .apk bundling pipeline (javac/d8/aapt2/zipalign/
apksigner) run on the comptime VM. 709/0 corpus + 476/476 unit.
This commit is contained in:
agra
2026-06-19 22:14:57 +03:00
parent 3014c61236
commit d8fb42501d

View File

@@ -673,6 +673,18 @@ pub const Vm = struct {
.struct_init => |agg| {
const table = try self.requireTable();
const sty = ins.ty;
// `string`/`any` are builtin TWO-WORD aggregates (`{ptr@0, len@8}` /
// `{tag@0, value@8}`) — a literal like `string.{ ptr = p, len = n }`
// (e.g. `from_cstring`) struct_inits one. Lay each operand as an
// 8-byte word; the other builtins have no aggregate literal form.
if (sty == .string or sty == .any) {
const a = self.machine.allocBytes(16, 8);
for (agg.fields, 0..) |fr, i| {
if (i >= 2) break;
try self.machine.writeWord(a + @as(Addr, @intCast(i)) * 8, 8, frame.get(fr.index()));
}
return .{ .value = a };
}
if (sty.isBuiltin()) return self.failMsg("comptime VM: struct_init at a builtin result type");
const addr = self.machine.allocBytes(table.typeSizeBytes(sty), table.typeAlignBytes(sty));
// `struct_init` is the generic aggregate-literal op — its result
@@ -1330,12 +1342,26 @@ pub const Vm = struct {
host_ffi.callVoidRet(symbol, argv) catch return self.failMsg("comptime extern call failed (void)");
return @as(Reg, 0);
}
if (kindOf(table, ret) != .word)
return self.failMsg("comptime extern call: non-word (aggregate/string/float) return not yet supported on the VM");
// The C function returns a single register word. For a plain word return
// that word IS the result. For an OPTIONAL whose child is itself a single
// word (e.g. `getenv() -> ?cstring`, a `char*` the sx side treats as a
// nullable handle), the C returns the bare payload and we wrap it into the
// `{payload@0, has@sizeof(child)}` aggregate below (present iff non-null) —
// mirroring emit_llvm's wrapping of an extern `char*`→`?cstring` return.
const opt_child: ?TypeId = if (!ret.isBuiltin() and table.get(ret) == .optional) blk: {
const ch = table.get(ret).optional.child;
// An optional with a SENTINEL (pointer) child is itself a word and is
// handled by the plain-word path; only the `{payload, has}` aggregate
// form (kindOf == .aggregate) needs wrapping here.
break :blk if (kindOf(table, ret) == .aggregate and kindOf(table, ch) == .word and !isFloat(ch)) ch else null;
} else null;
const word_ty: TypeId = opt_child orelse ret;
if (kindOf(table, word_ty) != .word or isFloat(word_ty))
return self.failFmt("comptime extern call '{s}': non-word (aggregate/string/float) return ({s}) not yet supported on the VM", .{ name, table.typeName(ret) });
// A pointer-ish return goes through callPtrRet (void* ABI); an integer-ish
// return through callIntRet (i64 ABI). Either way the result is a single
// word — a returned pointer is already a valid absolute `Addr`.
const r: u64 = if (isPointerish(table, ret)) blk: {
const r: u64 = if (isPointerish(table, word_ty)) blk: {
break :blk if (variadic)
host_ffi.callPtrRetVar(symbol, fixed, argv) catch return self.failMsg("comptime extern call failed (ptr)")
else
@@ -1347,6 +1373,13 @@ pub const Vm = struct {
host_ffi.callIntRet(symbol, argv) catch return self.failMsg("comptime extern call failed (int)");
break :blk @bitCast(v);
};
if (opt_child) |child| {
// Wrap the bare payload word into the `{payload, has}` optional aggregate.
const addr = self.machine.allocBytes(table.typeSizeBytes(ret), table.typeAlignBytes(ret));
try self.writeField(table, addr, child, r);
try self.machine.writeWord(addr + table.typeSizeBytes(child), 1, @intFromBool(r != 0));
return @as(Reg, addr);
}
return @as(Reg, r);
}