comptime VM arc: abi(.compiler) ABI, out as sx fn, VM-native diagnostics, BuildConfig threaded

Lands the full VM/compiler-API arc on branch reify (701/0 both gates):
- abi(.compiler) ABI replaces abi(.zig) extern compiler + the fake
  #library "compiler"; bodiless decl = compiler-API surface, bodied =
  user compiler-domain fn (lowered for VM eval, emit-skipped).
- out is a plain sx fn (libc write) — the out builtin deleted; the VM
  handles it via host-FFI. trace_resolve + interp_print_frames ported.
- 4B VM-native diagnostics: 1179/1180 render proper comptime type
  construction failed: under strict.
- S5a: build_options/set_post_link_callback on abi(.compiler) with
  BuildConfig threaded into the VM (green intermediate).
- 0522 fixed (describe(args: []Type)); regression 0638.

Strict deletion-gate down to 4 compiler_call bails (1609/1614/1615/1616)
+ 1654 (legitimate unresolvable-symbol diagnostic).
This commit is contained in:
agra
2026-06-19 07:04:10 +03:00
parent fdc4ee2331
commit 2060373c16
80 changed files with 12684 additions and 11922 deletions

View File

@@ -3,8 +3,11 @@
//! The comptime evaluator is being rebuilt around a flat, byte-addressable memory
//! so comptime values are NATIVE BYTES (like runtime), instead of the tagged
//! `Value` union the legacy interpreter (`interp.zig`) uses. This module is the
//! machine substrate: a linear byte memory with a bump/stack allocator, plus a
//! per-call `Frame` holding a register file.
//! machine substrate: byte-addressable memory backed by an ARENA of stable host
//! allocations (each `allocBytes` never moves; freed wholesale on `deinit`), plus
//! a per-call `Frame` holding a register file. `Addr` is the allocation's real
//! host pointer, so a flat-memory pointer and an FFI-returned host pointer are the
//! same kind of value.
//!
//! Value model (grows over later sub-steps): a register (`Reg`) is a raw 64-bit
//! word that is EITHER an immediate scalar (its bits) OR an `Addr` into flat
@@ -17,9 +20,10 @@
//! bytes. Layout (sizes/offsets/pointer width) is supplied by the type table when
//! the executor lays a value out, so cross-compilation stays correct.
//!
//! Sub-step 1 (this file): `Machine` (memory + bump/stack alloc + scalar word
//! read/write + byte views) and `Frame` (register file + stack reclamation). No
//! op execution yet — the executor + op handlers arrive in the next sub-step. The
//! `Machine` (arena-backed memory + scalar word read/write + byte views) holds the
//! comptime stack + heap; `Frame` is the per-call register file. A `Frame` does NOT
//! reclaim the machine's memory on exit — a callee can return an aggregate whose
//! register holds an `Addr` into flat memory, and reclaiming would dangle it. The
//! legacy interpreter remains the live evaluator until the VM reaches parity.
const std = @import("std");
@@ -28,6 +32,7 @@ const types = @import("types.zig");
const mod_mod = @import("module.zig");
const interp_mod = @import("interp.zig");
const host_ffi = @import("host_ffi.zig");
const errors_mod = @import("../errors.zig");
const Value = interp_mod.Value;
const Inst = inst_mod.Inst;
const Ref = inst_mod.Ref;
@@ -186,7 +191,7 @@ pub var last_bail_reason: ?[]const u8 = null;
/// hardened to return `error.OutOfBounds` (not a debug panic) on a null/out-of-
/// range/oversized access, so a malformed run bails to `null` (→ legacy fallback)
/// rather than crashing the compiler. On a bail, `last_bail_reason` names the cause.
pub fn tryEval(gpa: std.mem.Allocator, module: *const Module, func_id: inst_mod.FuncId) ?Value {
pub fn tryEval(gpa: std.mem.Allocator, module: *const Module, func_id: inst_mod.FuncId, build_config: ?*interp_mod.BuildConfig, source_map: ?*const std.StringHashMap([:0]const u8)) ?Value {
last_bail_reason = null;
const func = module.getFunction(func_id);
if (func.is_extern or func.blocks.items.len == 0) {
@@ -197,6 +202,8 @@ pub fn tryEval(gpa: std.mem.Allocator, module: *const Module, func_id: inst_mod.
defer vm.deinit();
vm.table = &module.types;
vm.module = module;
vm.build_config = build_config;
vm.source_map = source_map;
// `runEntry` materializes the implicit `*Context` (a comptime const-init /
// `#run` wrapper is nullary in user args, so the implicit ctx is its sole
@@ -276,6 +283,16 @@ pub const Vm = struct {
/// The module — resolves a `call`'s callee `FuncId` to its `Function`. Optional
/// so leaf functions (no calls) need none; a `call` bails loudly if it is absent.
module: ?*const Module = null,
/// The mutable build configuration (`BuildOptions` accumulator) — the SAME
/// `BuildConfig` `EmitLLVM` owns and `main.zig` reads post-link. Threaded in at
/// the `#run`/const-init eval sites so an `abi(.compiler)` `BuildOptions` function
/// (e.g. `set_post_link_callback`) records into it directly. Null at lowering-time
/// type-fn evals (no build config exists yet); such a function bails loudly.
build_config: ?*interp_mod.BuildConfig = null,
/// File → source text (the diagnostics' `import_sources`), threaded from the host
/// so `trace_resolve` can turn a packed `(func_id, span.start)` comptime frame into
/// `file:line:col` + the source line. Null → line/col degrade to 1 / "".
source_map: ?*const std.StringHashMap([:0]const u8) = null,
/// Current call-recursion depth, guarded against host stack overflow on deep /
/// infinite comptime recursion (mirrors the legacy interp's `call_depth`).
depth: u32 = 0,
@@ -819,6 +836,76 @@ pub const Vm = struct {
const fid: u64 = if (self.call_stack.items.len > 0) self.call_stack.items[self.call_stack.items.len - 1].index() else 0;
return .{ .value = (fid << 32) | @as(u64, ins.span.start) };
},
// Dump the comptime call-frame chain (`trace.print_interpreter_frames`) —
// the VM-native mirror of the legacy `printInterpFrames`. Walks the active
// `call_stack` (skipping the last frame, the `print_interpreter_frames`
// fn itself, like the legacy) and writes ` at <name>` lines straight to
// fd 1 (consistent with `out`'s now-direct libc `write`).
.interp_print_frames => {
const module = self.module orelse return self.failMsg("comptime interp_print_frames: no module");
const n = self.call_stack.items.len;
if (n <= 1) return .{ .value = null_addr };
var buf = std.ArrayList(u8).empty;
defer buf.deinit(self.gpa);
buf.appendSlice(self.gpa, "comptime call frames (most recent call last):\n") catch return self.failMsg("comptime interp_print_frames: out of memory");
var i: usize = 0;
while (i < n - 1) : (i += 1) {
const fname = module.types.getString(module.getFunction(self.call_stack.items[i]).name);
buf.appendSlice(self.gpa, " at ") catch return self.failMsg("comptime interp_print_frames: out of memory");
buf.appendSlice(self.gpa, fname) catch return self.failMsg("comptime interp_print_frames: out of memory");
buf.append(self.gpa, '\n') catch return self.failMsg("comptime interp_print_frames: out of memory");
}
_ = std.c.write(1, buf.items.ptr, buf.items.len);
return .{ .value = null_addr };
},
// Unpack a comptime frame `(func_id << 32 | span.start)` and build a
// `Frame { file, line, col, func, line_text }` aggregate in flat memory —
// the VM-native mirror of the legacy interp's `.trace_resolve`. `ins.ty`
// is the `Frame` struct, so each field's type/offset comes from the table.
.trace_resolve => |u| {
const table = try self.requireTable();
const module = self.module orelse return self.failMsg("comptime trace_resolve: no module");
const raw = frame.get(u.operand.index());
const fid: u32 = @intCast(raw >> 32);
const offset: u32 = @truncate(raw);
if (fid >= module.functions.items.len) return self.failMsg("comptime trace_resolve: func id out of range");
const func = module.getFunction(inst_mod.FuncId.fromIndex(fid));
const func_name = module.types.getString(func.name);
const file_full = func.source_file orelse "";
const file = std.fs.path.basename(file_full);
var line: i64 = 1;
var col: i64 = 1;
var line_text: []const u8 = "";
if (self.source_map) |sm| {
if (sm.get(file_full)) |src| {
const loc = errors_mod.SourceLoc.compute(src, offset);
line = @intCast(loc.line);
col = @intCast(loc.col);
line_text = errors_mod.lineAt(src, offset);
}
}
const fty = ins.ty;
if (fty.isBuiltin() or table.get(fty) != .@"struct")
return self.failMsg("comptime trace_resolve: result type is not a Frame struct");
const sfields = table.get(fty).@"struct".fields;
if (sfields.len != 5) return self.failMsg("comptime trace_resolve: Frame struct is not 5 fields");
const addr = self.machine.allocBytes(table.typeSizeBytes(fty), table.typeAlignBytes(fty));
// { file, line, col, func, line_text } — positional, matching the legacy build.
try self.writeField(table, addr + fieldOffset(table, fty, 0), sfields[0].ty, try self.makeStringValue(table, file));
try self.writeField(table, addr + fieldOffset(table, fty, 1), sfields[1].ty, @bitCast(line));
try self.writeField(table, addr + fieldOffset(table, fty, 2), sfields[2].ty, @bitCast(col));
try self.writeField(table, addr + fieldOffset(table, fty, 3), sfields[3].ty, try self.makeStringValue(table, func_name));
try self.writeField(table, addr + fieldOffset(table, fty, 4), sfields[4].ty, try self.makeStringValue(table, line_text));
return .{ .value = addr };
},
// `error_tag_name(e)` — the runtime tag id (a word) → its name string via
// the always-linked tag-name table. Pure: builds a `{ptr,len}` string in
// flat memory. Mirrors the legacy interp's `error_tag_name_get`.
.error_tag_name_get => |u| {
const table = try self.requireTable();
const id: u32 = @intCast(frame.get(u.operand.index()));
return .{ .value = try self.makeStringValue(table, table.getTagName(id)) };
},
// ── Calls ───────────────────────────────────────────
// Direct call: resolve the static callee `FuncId` and dispatch.
@@ -840,6 +927,21 @@ pub const Vm = struct {
// `comptime_func` run on this same VM, or a scalar static value),
// memoized. Mirrors the legacy interp's `getGlobal`.
.global_get => |gid| return .{ .value = try self.evalGlobal(gid) },
// `&global` — only `&__sx_default_context` is materialised at comptime
// (its address sees runtime use via the implicit-ctx plumbing). Return
// the context's flat-memory address — an aggregate value IS its address,
// so a later `load`/field read sees the materialised Context. Mirrors the
// legacy interp's `global_addr` (the sole supported global); any other
// global bails to legacy fallback.
.global_addr => |gid| {
const module = self.module orelse return self.failMsg("comptime VM: global_addr needs a module");
if (gid.index() < module.globals.items.len and
std.mem.eql(u8, module.types.getString(module.globals.items[gid.index()].name), "__sx_default_context"))
{
return .{ .value = try self.materializeDefaultContext(module) };
}
return self.failMsg("comptime global_addr: only `&__sx_default_context` is materialised at comptime");
},
// A function value is its encoded func-ref word (see `funcRefWord`).
.func_ref => |fid| return .{ .value = funcRefWord(fid) },
@@ -1005,6 +1107,14 @@ pub const Vm = struct {
return error.Unsupported;
}
/// Like `failMsg` but for a runtime-formatted reason (e.g. naming the offending
/// variant). Allocated in `gpa` so it survives to the host's diagnostic render;
/// the build fails on this path, so the small leak is moot.
fn failFmt(self: *Vm, comptime fmt: []const u8, args: anytype) error{Unsupported} {
self.detail = std.fmt.allocPrint(self.gpa, fmt, args) catch "comptime VM: out of memory formatting diagnostic";
return error.Unsupported;
}
fn badRef(self: *Vm) error{Unsupported} {
self.detail = "comptime VM: malformed IR — operand ref out of range (unresolved name?)";
return error.Unsupported;
@@ -1322,6 +1432,26 @@ pub const Vm = struct {
if (std.mem.eql(u8, name, "register_type")) {
return self.registerTypeVm(args, frame, ref_types);
}
// ── BuildOptions (migrated off `#compiler` onto `abi(.compiler)`) ───────
// `build_options()` hands back an opaque, zero-field `BuildOptions` handle;
// the real state lives on the threaded `BuildConfig`. Return the null
// sentinel word (the handle is never dereferenced — every operation takes it
// as an ignored `self`). Mirrors the legacy `hookBuildOptions` (`.void_val`).
if (std.mem.eql(u8, name, "build_options")) {
return @as(Reg, null_addr);
}
// `set_post_link_callback(self, cb)` — record the callback `FuncId` on the
// build config so `main.zig` re-enters the evaluator post-link. The cb arg is
// a `func_ref` word. Mirrors the legacy `hookSetPostLinkCallback`.
if (std.mem.eql(u8, name, "set_post_link_callback")) {
if (args.len != 2) return self.failMsg("comptime set_post_link_callback: expected (self, cb)");
const bc = self.build_config orelse
return self.failMsg("comptime set_post_link_callback: no build config threaded into the VM");
const fid = funcRefToId(frame.get(args[1].index())) orelse
return self.failMsg("comptime set_post_link_callback: cb arg is not a function value");
bc.post_link_callback_fn = fid;
return @as(Reg, null_addr);
}
return null; // not a known compiler function → caller bails to legacy
}
@@ -1428,7 +1558,7 @@ pub const Vm = struct {
/// `Type` elements with no name) from flat memory into `TypeId`s.
fn decodeTypeSlice(self: *Vm, table: *const types.TypeTable, slice_word: Reg, slice_ty: TypeId, out: *std.ArrayList(TypeId)) Error!void {
if (slice_ty.isBuiltin() or table.get(slice_ty) != .slice)
return self.failMsg("comptime define: tuple elements arg is not a slice");
return self.failMsg("comptime define(): tuple elements arg is not a slice");
const elem_ty = table.get(slice_ty).slice.element; // Type (.type_value)
const len = try self.sliceLen(slice_word);
const base = try self.sliceData(table, slice_word);
@@ -1436,10 +1566,38 @@ pub const Vm = struct {
for (0..@intCast(len)) |i| {
const e = base + @as(Addr, @intCast(i)) * stride;
const t: TypeId = @enumFromInt(@as(u32, @intCast(try self.readField(table, e, .type_value))));
out.append(self.gpa, t) catch return self.failMsg("comptime define: out of memory");
out.append(self.gpa, t) catch return self.failMsg("comptime define(): out of memory");
}
}
/// Resolve the `TypeId` a reflection builtin (`type_name` / `type_is_unsigned`)
/// queries, given the arg's IR type `aty` and its register word `w`. A
/// `.type_value` word IS a `TypeId`; an Any box `{ tag@0, value@8 }` yields its
/// tag (the boxed value's runtime type), unless tag == `type_value` — a boxed
/// Type (the `type_of(x)` shape) whose real id sits in the value slot. The
/// VM-native mirror of the legacy `Value.reflectTypeId`.
fn reflectArgTypeId(self: *Vm, aty: TypeId, w: Reg) Error!TypeId {
// A `TypeId` index is a u32; a word that doesn't fit is a garbage/mis-read
// value (e.g. a wrong slice stride yielding an `Any` element at the wrong
// offset — see 0522). Bail loudly instead of letting `@intCast` abort: the
// VM must never crash.
if (aty == .type_value) return TypeId.fromIndex(try self.typeIdxOf(w));
if (aty == .any) {
const tag = try self.machine.readWord(w, 8);
if (tag == @as(u64, TypeId.type_value.index()))
return TypeId.fromIndex(try self.typeIdxOf(try self.machine.readWord(w + 8, 8)));
return TypeId.fromIndex(try self.typeIdxOf(tag));
}
return self.failMsg("comptime reflection builtin: arg is not a Type value or an Any box");
}
/// Narrow a 64-bit word to a `u32` `TypeId` index, bailing (never crashing) when
/// it doesn't fit — the tripwire for a mis-read reflection arg.
fn typeIdxOf(self: *Vm, w: u64) Error!u32 {
return std.math.cast(u32, w) orelse
self.failMsg("comptime reflection builtin: type word out of TypeId range (mis-read arg?)");
}
/// Service a comptime metatype `#builtin` (`meta.sx`'s `declare`/`define`)
/// natively on flat memory, the VM-native mirror of the legacy
/// `interp.execBuiltinInner` arms. Returns the result word, or `null` for a
@@ -1458,27 +1616,27 @@ pub const Vm = struct {
// define(handle, info) → complete the declared slot from a TypeInfo VALUE.
.define => {
const table = try self.requireTable();
if (bi.args.len != 2) return self.failMsg("comptime define: expected (handle, info)");
if (bi.args.len != 2) return self.failMsg("comptime define(): expected (handle, info)");
const handle = try self.argTypeId(bi.args, frame, 0);
// `info`: a TypeInfo tagged-union value `{ tag@0, payload@tag_size }`.
const info_ty = try self.refTy(ref_types, bi.args[1]);
if (info_ty.isBuiltin() or table.get(info_ty) != .tagged_union)
return self.failMsg("comptime define: info arg is not a TypeInfo tagged union");
return self.failMsg("comptime define(): info arg is not a TypeInfo tagged union");
const tu = table.get(info_ty).tagged_union;
// The `{ tag@0, payload@tag_size }` read below assumes a tag-headed
// layout (true for `TypeInfo`); a `backing_type` union is laid out
// differently, so bail rather than read the tag from the wrong bytes.
if (tu.backing_type != null)
return self.failMsg("comptime define: info is a backing_type tagged union (unexpected layout)");
return self.failMsg("comptime define(): info is a backing_type tagged union (unexpected layout)");
const info_addr = frame.get(bi.args[1].index());
const tag_size: Addr = @intCast(table.typeSizeBytes(tu.tag_type));
const tag = try self.machine.readWord(info_addr, tag_size);
if (tag >= tu.fields.len) return self.failMsg("comptime define: TypeInfo tag out of range");
if (tag >= tu.fields.len) return self.failMsg("comptime define(): TypeInfo tag out of range");
// The active payload (EnumInfo / StructInfo / TupleInfo) is a struct
// holding ONE slice field; its bytes live at `info_addr + tag_size`.
const payload_ty = tu.fields[@intCast(tag)].ty;
if (payload_ty.isBuiltin() or table.get(payload_ty) != .@"struct" or table.get(payload_ty).@"struct".fields.len != 1)
return self.failMsg("comptime define: TypeInfo payload is not a single-slice info struct");
return self.failMsg("comptime define(): TypeInfo payload is not a single-slice info struct");
return try self.defineFromInfo(table, handle, @intCast(tag), payload_ty, info_addr + tag_size);
},
// type_name(x) → the type's name as a string. The arg is a Type value
@@ -1488,20 +1646,19 @@ pub const Vm = struct {
.type_name => {
const table = try self.requireTable();
if (bi.args.len < 1) return self.failMsg("comptime type_name: missing argument");
const aty = try self.refTy(ref_types, bi.args[0]);
const w = frame.get(bi.args[0].index());
const tid: TypeId = blk: {
if (aty == .type_value) break :blk TypeId.fromIndex(@intCast(w));
if (aty == .any) {
const tag = try self.machine.readWord(w, 8);
if (tag == @as(u64, TypeId.type_value.index()))
break :blk TypeId.fromIndex(@intCast(try self.machine.readWord(w + 8, 8)));
break :blk TypeId.fromIndex(@intCast(tag));
}
return self.failMsg("comptime type_name: arg is not a Type value or an Any box");
};
const tid = try self.reflectArgTypeId(try self.refTy(ref_types, bi.args[0]), frame.get(bi.args[0].index()));
return try self.makeStringValue(table, table.typeName(tid));
},
// type_is_unsigned(x) → is x's type an unsigned int? Resolves the TypeId
// the same way as type_name (a `.type_value` word, or an Any box whose tag
// IS the boxed value's type), then queries `isUnsignedInt`. Mirrors the
// legacy `type_is_unsigned` builtin (`reflectTypeId` + `isUnsignedInt`).
.type_is_unsigned => {
const table = try self.requireTable();
if (bi.args.len < 1) return self.failMsg("comptime type_is_unsigned: missing argument");
const tid = try self.reflectArgTypeId(try self.refTy(ref_types, bi.args[0]), frame.get(bi.args[0].index()));
return @as(Reg, @intFromBool(table.isUnsignedInt(tid)));
},
// type_info($T) → reflect a type INTO a TypeInfo VALUE (the inverse of
// define's decode). The arg folded to a `const_type` (a `.type_value`
// word = the source TypeId); build the value in flat memory.
@@ -1528,8 +1685,8 @@ pub const Vm = struct {
const tbl = @constCast(table);
const cur = table.get(handle);
const ident = nominalIdentOf(cur) orelse
return self.failMsg("comptime define: handle is not a declare()'d nominal slot");
if (cur != .tagged_union) return self.failMsg("comptime define: handle is not a declare()'d slot");
return self.failMsg("comptime define(): handle is not a declare()'d nominal slot");
if (cur != .tagged_union) return self.failMsg("comptime define(): handle is not a declare()'d slot");
// The info struct's single field is the member/element slice; read its
// fat-pointer (embedded at field-0 offset within the info struct).
@@ -1541,7 +1698,7 @@ pub const Vm = struct {
var members = std.ArrayList(NamedMember).empty;
defer members.deinit(self.gpa);
try self.decodeMemberSlice(table, slice_word, slice_field_ty, &members);
if (members.items.len == 0) return self.failMsg("comptime define: enum has no variants");
if (members.items.len == 0) return self.failMsg("comptime define(): enum has no variants");
// A FULLY payloadless variant set (every payload `void`) is an actual
// `.@"enum"` (a kind change → `replaceKeyedInfo`); minting it as an
// all-void tagged_union trips `verifySizes` at codegen (issue 0142).
@@ -1551,16 +1708,16 @@ pub const Vm = struct {
break;
};
if (all_void) {
const names = self.gpa.alloc(types.StringId, members.items.len) catch return self.failMsg("comptime define: out of memory");
const names = self.gpa.alloc(types.StringId, members.items.len) catch return self.failMsg("comptime define(): out of memory");
for (members.items, 0..) |m, i| {
for (names[0..i]) |prev| if (prev == m.name) return self.failMsg("comptime define: duplicate variant name");
for (names[0..i]) |prev| if (prev == m.name) return self.failFmt("comptime define(): duplicate variant name '{s}'", .{tbl.getString(m.name)});
names[i] = m.name;
}
tbl.replaceKeyedInfo(handle, .{ .@"enum" = .{ .name = ident.name, .variants = names, .nominal_id = ident.nominal_id } });
} else {
const flds = self.gpa.alloc(types.TypeInfo.StructInfo.Field, members.items.len) catch return self.failMsg("comptime define: out of memory");
const flds = self.gpa.alloc(types.TypeInfo.StructInfo.Field, members.items.len) catch return self.failMsg("comptime define(): out of memory");
for (members.items, 0..) |m, i| {
for (flds[0..i]) |prev| if (prev.name == m.name) return self.failMsg("comptime define: duplicate variant name");
for (flds[0..i]) |prev| if (prev.name == m.name) return self.failFmt("comptime define(): duplicate variant name '{s}'", .{tbl.getString(m.name)});
flds[i] = .{ .name = m.name, .ty = m.ty };
}
// Name/id unchanged → still a tagged_union → stable key.
@@ -1571,10 +1728,10 @@ pub const Vm = struct {
var members = std.ArrayList(NamedMember).empty;
defer members.deinit(self.gpa);
try self.decodeMemberSlice(table, slice_word, slice_field_ty, &members);
if (members.items.len == 0) return self.failMsg("comptime define: struct has no fields");
const flds = self.gpa.alloc(types.TypeInfo.StructInfo.Field, members.items.len) catch return self.failMsg("comptime define: out of memory");
if (members.items.len == 0) return self.failMsg("comptime define(): struct has no fields");
const flds = self.gpa.alloc(types.TypeInfo.StructInfo.Field, members.items.len) catch return self.failMsg("comptime define(): out of memory");
for (members.items, 0..) |m, i| {
for (flds[0..i]) |prev| if (prev.name == m.name) return self.failMsg("comptime define: duplicate field name");
for (flds[0..i]) |prev| if (prev.name == m.name) return self.failFmt("comptime define(): duplicate field name '{s}'", .{tbl.getString(m.name)});
flds[i] = .{ .name = m.name, .ty = m.ty };
}
// tagged_union slot → struct is a kind change → `replaceKeyedInfo`.
@@ -1584,12 +1741,12 @@ pub const Vm = struct {
var elems = std.ArrayList(TypeId).empty;
defer elems.deinit(self.gpa);
try self.decodeTypeSlice(table, slice_word, slice_field_ty, &elems);
if (elems.items.len == 0) return self.failMsg("comptime define: tuple has no elements");
const tys = self.gpa.alloc(TypeId, elems.items.len) catch return self.failMsg("comptime define: out of memory");
if (elems.items.len == 0) return self.failMsg("comptime define(): tuple has no elements");
const tys = self.gpa.alloc(TypeId, elems.items.len) catch return self.failMsg("comptime define(): out of memory");
@memcpy(tys, elems.items);
tbl.replaceKeyedInfo(handle, .{ .tuple = .{ .fields = tys, .names = null } });
},
else => return self.failMsg("comptime define: unknown TypeInfo variant"),
else => return self.failMsg("comptime define(): unknown TypeInfo variant"),
}
return @as(Reg, handle.index());
}