comptime VM: Phase 3 — register_type write side + payloadless-enum fixes
The mutating compiler-API, minting types LAZILY at lowering time (single pass,
the existing runComptimeTypeFunc path — so the write side is legacy-only; the
VM isn't wired at lowering time, and the read-side readers stay dual-path):
declare_type(name) -> Type forward nominal handle (≈ declare)
pointer_to(t) -> Type build *T references
register_type(handle, kind, members) ONE kind-branching fill (≈ unified define)
register_type branches on kind IN THE COMPILER (subsuming define's per-kind
dispatch); codes match type_kind: 1 struct, 2 actual .@"enum", 3 tagged_union,
4 tuple. Members are {name: string, ty: Type}. A non-generic `-> Type` builder is
now flagged is_comptime (decl.zig) so its dead body permits the welded calls.
Graph support: forward declare_type handles + pointer_to express a mutually-
recursive A<->B graph (*A, *B, B-by-value) before bodies are filled. register_type
is idempotent — re-filling a nominal slot (a minting module reached via two import
edges) re-mints identically rather than erroring (nominalIdent reads identity from
any nominal kind).
Fixes (issue 0142):
- A fully payloadless comptime-minted enum was minted as an all-void tagged_union,
whose IR size disagrees with its LLVM size -> verifySizes panic. Now mints a real
.@"enum" (register_type kind 2 AND the metatype defineEnum).
- Bare `EnumType.variant` qualified construction of a payloadless variant wasn't
supported (failed for hand-written enums too — the type name lowered to a Type
value). Added in lowerFieldAccess via isPayloadlessVariant; payload-carrying
variants keep their call form.
Examples: 0631 (graph + actual enum + reflection), 0632 (make_enum all-void),
0633/0634/0635 (namespaced / bare / multi-edge import of a minted type), 0187
(qualified variant construction). Unit tests added.
Parity 697/697 (gate OFF and -Dcomptime-flat).
This commit is contained in:
@@ -7,9 +7,18 @@ const compiler_lib = @import("compiler_lib.zig");
|
||||
// rejects unexported names (the boundary `weldedCompilerFn` + the interp's
|
||||
// dispatch consult).
|
||||
test "compiler_lib: findFn resolves exported functions, rejects others" {
|
||||
// Seed readers.
|
||||
try std.testing.expect(compiler_lib.findFn("intern") != null);
|
||||
try std.testing.expect(compiler_lib.findFn("text_of") != null);
|
||||
try std.testing.expectEqualStrings("intern", compiler_lib.findFn("intern").?.sx_name);
|
||||
// Phase 3 read-only reflection readers.
|
||||
for ([_][]const u8{ "find_type", "type_field_count", "type_nominal_name", "type_field_name", "type_field_type", "type_kind", "type_field_value" }) |n| {
|
||||
try std.testing.expect(compiler_lib.findFn(n) != null);
|
||||
}
|
||||
// Phase 3 write side.
|
||||
for ([_][]const u8{ "declare_type", "pointer_to", "register_type" }) |n| {
|
||||
try std.testing.expect(compiler_lib.findFn(n) != null);
|
||||
}
|
||||
try std.testing.expect(compiler_lib.findFn("not_exported") == null);
|
||||
try std.testing.expect(compiler_lib.findFn("") == null);
|
||||
}
|
||||
|
||||
@@ -54,8 +54,20 @@ pub const bound_fns = [_]BoundFn{
|
||||
.{ .sx_name = "type_field_type", .handler = handleTypeFieldType },
|
||||
.{ .sx_name = "type_kind", .handler = handleTypeKind },
|
||||
.{ .sx_name = "type_field_value", .handler = handleTypeFieldValue },
|
||||
// ── write side (lowering-time, mints into the type table) ────────────────
|
||||
.{ .sx_name = "declare_type", .handler = handleDeclareType },
|
||||
.{ .sx_name = "pointer_to", .handler = handlePointerTo },
|
||||
.{ .sx_name = "register_type", .handler = handleRegisterType },
|
||||
};
|
||||
|
||||
// Kind codes accepted by `register_type` — mirror `TypeTable.kindCode`. An
|
||||
// enum-like type is minted as a `tagged_union` (the general payload-carrying
|
||||
// form, as `define` does), so both 2 (`enum`) and 3 (`tagged_union`) are taken.
|
||||
const kind_struct: i64 = 1;
|
||||
const kind_enum: i64 = 2;
|
||||
const kind_tagged_union: i64 = 3;
|
||||
const kind_tuple: i64 = 4;
|
||||
|
||||
/// Look up a compiler function by its sx name. Returns null when the name is not
|
||||
/// on the export list.
|
||||
pub fn findFn(sx_name: []const u8) ?*const BoundFn {
|
||||
@@ -170,3 +182,128 @@ fn handleTypeFieldValue(interp: *Interpreter, args: []const Value) InterpError!V
|
||||
const v = interp.module.types.memberValue(tid, args[1].int) orelse return error.TypeError;
|
||||
return Value{ .int = v };
|
||||
}
|
||||
|
||||
// ── write side: declare_type / pointer_to / register_type ───────────────────
|
||||
//
|
||||
// These MINT into the type table, so they only make sense at LOWERING time —
|
||||
// where the compiler still resolves references to the new types and the `mint`
|
||||
// target is open (`runComptimeTypeFunc`). They take/return real `Type` values
|
||||
// (`.type_tag`), the comptime-native form, matching meta.sx's `StructField` /
|
||||
// `declare` / `define`. This is the unified re-expression of the metatype:
|
||||
// `declare_type` ≈ `declare`, `register_type` ≈ a single kind-branching `define`,
|
||||
// and `pointer_to` builds `*T` references so a graph of types can refer to each
|
||||
// other (forward handles + pointers) before their bodies are filled.
|
||||
|
||||
/// `declare_type(name: string) -> Type` — mint a NEW empty forward nominal type
|
||||
/// named `name` (or return the existing slot, so a self/sibling reference by name
|
||||
/// resolves to the same one). Mirrors the `declare` builtin: the forward slot is
|
||||
/// an empty `tagged_union` until `register_type` fills it.
|
||||
fn handleDeclareType(interp: *Interpreter, args: []const Value) InterpError!Value {
|
||||
if (args.len != 1 or args[0] != .string) return error.TypeError;
|
||||
const tbl = mintTable(interp);
|
||||
const name_id = tbl.internString(args[0].string);
|
||||
if (tbl.findByName(name_id)) |existing| return Value{ .type_tag = existing };
|
||||
const info: types.TypeInfo = .{ .tagged_union = .{ .name = name_id, .fields = &.{}, .tag_type = .i64 } };
|
||||
return Value{ .type_tag = tbl.internNominal(info, 0) };
|
||||
}
|
||||
|
||||
/// `pointer_to(t: Type) -> Type` — intern `*t`. Lets a member reference a type by
|
||||
/// pointer (e.g. a recursive `*A`) from a `Type` handle.
|
||||
fn handlePointerTo(interp: *Interpreter, args: []const Value) InterpError!Value {
|
||||
if (args.len != 1 or args[0] != .type_tag) return error.TypeError;
|
||||
const tbl = mintTable(interp);
|
||||
return Value{ .type_tag = tbl.intern(.{ .pointer = .{ .pointee = args[0].type_tag } }) };
|
||||
}
|
||||
|
||||
/// `register_type(handle: Type, kind: i64, members: []Member) -> Type` — fill a
|
||||
/// `declare_type`'d forward slot, branching on `kind` IN THE COMPILER (subsuming
|
||||
/// `define`'s per-kind dispatch). `Member` is `{ name: string, ty: Type }`:
|
||||
/// struct → fields `{ name, ty }` (dup names rejected)
|
||||
/// enum/t-union → variants `{ name, payload = ty }` (minted as a tagged_union)
|
||||
/// tuple → positional element types (names ignored)
|
||||
/// Returns the (now completed) handle. Every malformed input is a loud error.
|
||||
fn handleRegisterType(interp: *Interpreter, args: []const Value) InterpError!Value {
|
||||
if (args.len != 3 or args[0] != .type_tag or args[1] != .int) return error.TypeError;
|
||||
const handle = args[0].type_tag;
|
||||
const kind = args[1].int;
|
||||
const elems = interp_mod.decodeVariantElements(args[2]) orelse return error.TypeError;
|
||||
if (elems.len == 0) return error.TypeError; // a type with no members is never valid
|
||||
const tbl = mintTable(interp);
|
||||
// The slot's nominal identity. Accept the forward `tagged_union` from
|
||||
// `declare_type` AND an already-completed nominal of the same name — so
|
||||
// re-evaluating the same type-fn (e.g. a minting module reached via two
|
||||
// import edges) RE-FILLS the slot idempotently instead of erroring. A
|
||||
// non-nominal handle is rejected (not a `declare_type`'d slot).
|
||||
const ident = nominalIdent(tbl.get(handle)) orelse return error.TypeError;
|
||||
|
||||
if (kind == kind_tuple) {
|
||||
var tys = std.ArrayList(types.TypeId).empty;
|
||||
for (elems) |elem| {
|
||||
const m = memberPair(elem) orelse return error.TypeError;
|
||||
tys.append(interp.alloc, m.ty) catch return error.CannotEvalComptime;
|
||||
}
|
||||
tbl.replaceKeyedInfo(handle, .{ .tuple = .{ .fields = tys.items, .names = null } });
|
||||
return Value{ .type_tag = handle };
|
||||
}
|
||||
|
||||
if (kind == kind_enum) {
|
||||
// An ACTUAL (payloadless) enum: members are variant NAMES. A non-void
|
||||
// payload means the caller wants a payload-carrying variant — that's a
|
||||
// tagged_union (kind 3), so reject it loudly rather than dropping it.
|
||||
var variants = std.ArrayList(StringId).empty;
|
||||
for (elems) |elem| {
|
||||
const m = memberPair(elem) orelse return error.TypeError;
|
||||
if (m.ty != .void) return error.TypeError; // payload variant → use kind 3 (tagged_union)
|
||||
const name_id = tbl.internString(m.name);
|
||||
for (variants.items) |existing| if (existing == name_id) return error.TypeError; // dup variant
|
||||
variants.append(interp.alloc, name_id) catch return error.CannotEvalComptime;
|
||||
}
|
||||
tbl.replaceKeyedInfo(handle, .{ .@"enum" = .{ .name = ident.name, .variants = variants.items, .nominal_id = ident.nominal_id } });
|
||||
return Value{ .type_tag = handle };
|
||||
}
|
||||
|
||||
// struct / tagged_union collect `{ name, ty }` fields.
|
||||
var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
|
||||
for (elems) |elem| {
|
||||
const m = memberPair(elem) orelse return error.TypeError;
|
||||
const name_id = tbl.internString(m.name);
|
||||
for (fields.items) |existing| if (existing.name == name_id) return error.TypeError; // dup member name
|
||||
fields.append(interp.alloc, .{ .name = name_id, .ty = m.ty }) catch return error.CannotEvalComptime;
|
||||
}
|
||||
const full: types.TypeInfo = switch (kind) {
|
||||
kind_struct => .{ .@"struct" = .{ .name = ident.name, .fields = fields.items, .nominal_id = ident.nominal_id } },
|
||||
kind_tagged_union => .{ .tagged_union = .{ .name = ident.name, .fields = fields.items, .tag_type = .i64, .nominal_id = ident.nominal_id } },
|
||||
else => return error.TypeError, // unknown kind code
|
||||
};
|
||||
tbl.replaceKeyedInfo(handle, full);
|
||||
return Value{ .type_tag = handle };
|
||||
}
|
||||
|
||||
/// The nominal identity (`name` + stable `nominal_id`) of a declare_type'd slot —
|
||||
/// from the forward `tagged_union` OR an already-completed nominal (so a re-fill
|
||||
/// preserves identity). A `tuple` is structural (no nominal name); null for a
|
||||
/// non-nominal handle (not a `declare_type` result).
|
||||
fn nominalIdent(info: types.TypeInfo) ?struct { name: StringId, nominal_id: u32 } {
|
||||
return switch (info) {
|
||||
.tagged_union => |u| .{ .name = u.name, .nominal_id = u.nominal_id },
|
||||
.@"enum" => |e| .{ .name = e.name, .nominal_id = e.nominal_id },
|
||||
.@"struct" => |s| .{ .name = s.name, .nominal_id = s.nominal_id },
|
||||
.tuple => .{ .name = StringId.empty, .nominal_id = 0 }, // structural; name vestigial
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Decode one `Member` value — a `{ name: string, ty: Type }` aggregate.
|
||||
fn memberPair(elem: Value) ?struct { name: []const u8, ty: types.TypeId } {
|
||||
const f = switch (elem) {
|
||||
.aggregate => |a| a,
|
||||
else => return null,
|
||||
};
|
||||
if (f.len != 2) return null;
|
||||
const name = switch (f[0]) {
|
||||
.string => |s| s,
|
||||
else => return null,
|
||||
};
|
||||
const ty = f[1].asTypeId() orelse return null;
|
||||
return .{ .name = name, .ty = ty };
|
||||
}
|
||||
|
||||
@@ -2197,10 +2197,37 @@ pub const Interpreter = struct {
|
||||
}
|
||||
|
||||
// Complete the declared slot IN PLACE: it already has its name + nominal
|
||||
// id (from `declare`); fill the body. Name/id unchanged → the intern key
|
||||
// is stable, so `updatePreservingKey`.
|
||||
// id (from `declare`); fill the body.
|
||||
const cur = tbl.get(handle);
|
||||
if (cur != .tagged_union) return bailDetail("comptime define(): handle is not a declare()'d enum slot");
|
||||
|
||||
// A FULLY payloadless variant set (every payload `void`) is an actual
|
||||
// enum — mint a `.@"enum"`, exactly like a hand-written `enum { a; b; }`.
|
||||
// Minting it as an all-void `tagged_union` instead gives a type whose IR
|
||||
// size disagrees with its LLVM size (a tag, but no payload storage), which
|
||||
// trips `verifySizes` at codegen. A kind change re-keys the slot, so
|
||||
// `replaceKeyedInfo` (not `updatePreservingKey`, which asserts the kind is
|
||||
// stable — true only for the tagged_union path below).
|
||||
var all_void = true;
|
||||
for (fields.items) |f| {
|
||||
if (f.ty != .void) {
|
||||
all_void = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (all_void) {
|
||||
var variants = std.ArrayList(types.StringId).empty;
|
||||
for (fields.items) |f| variants.append(self.alloc, f.name) catch return error.CannotEvalComptime;
|
||||
const en: types.TypeInfo = .{ .@"enum" = .{
|
||||
.name = cur.tagged_union.name,
|
||||
.variants = variants.items,
|
||||
.nominal_id = cur.tagged_union.nominal_id,
|
||||
} };
|
||||
tbl.replaceKeyedInfo(handle, en);
|
||||
return .{ .value = .{ .type_tag = handle } };
|
||||
}
|
||||
|
||||
// Payload-carrying enum → tagged_union. Name/id unchanged → stable key.
|
||||
const full: types.TypeInfo = .{ .tagged_union = .{
|
||||
.name = cur.tagged_union.name,
|
||||
.fields = fields.items,
|
||||
@@ -2305,7 +2332,7 @@ pub const Interpreter = struct {
|
||||
/// A `[]EnumVariant` slice evaluates to a `{ data, len }` aggregate (`len` an
|
||||
/// int); a `[N]EnumVariant` array literal evaluates to the element aggregate
|
||||
/// directly. Returns null for any other shape (the caller bails loudly).
|
||||
fn decodeVariantElements(result: Value) ?[]const Value {
|
||||
pub fn decodeVariantElements(result: Value) ?[]const Value {
|
||||
const fields = switch (result) {
|
||||
.aggregate => |f| f,
|
||||
else => return null,
|
||||
|
||||
@@ -1931,6 +1931,7 @@ pub const Lowering = struct {
|
||||
pub const findTaggedVariant = lower_expr.findTaggedVariant;
|
||||
pub const emitBadVariant = lower_expr.emitBadVariant;
|
||||
pub const emitBadEnumVariant = lower_expr.emitBadEnumVariant;
|
||||
pub const isPayloadlessVariant = lower_expr.isPayloadlessVariant;
|
||||
pub const dedupeExternSymbol = lower_decl.dedupeExternSymbol;
|
||||
pub const resolveVariantValue = lower_expr.resolveVariantValue;
|
||||
pub const resolveVariantIndex = lower_expr.resolveVariantIndex;
|
||||
|
||||
@@ -2298,6 +2298,14 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8)
|
||||
func.is_variadic = is_variadic;
|
||||
func.has_implicit_ctx = wants_ctx;
|
||||
if (weldedCompilerFn(self, fd, name)) func.compiler_welded = true;
|
||||
// A non-generic `-> Type` builder is a comptime type constructor — only ever
|
||||
// evaluated at lowering time (`runComptimeTypeFunc`) to mint a type, never
|
||||
// called at runtime. Flag it `is_comptime` so its emitted body is dead: the
|
||||
// comptime-only `compiler`-library gate then permits welded calls inside it
|
||||
// (`register_type`/`declare_type`/`pointer_to`), exactly as in a #run/`::`
|
||||
// wrapper. Without this, a builder that calls a welded fn would be rejected
|
||||
// as "comptime-only fn called at runtime" even though it never runs at runtime.
|
||||
if (fnReturnsTypeValue(fd)) func.is_comptime = true;
|
||||
self.fn_decl_fids.put(fd, fid) catch {};
|
||||
}
|
||||
|
||||
|
||||
@@ -490,6 +490,33 @@ pub fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.S
|
||||
}
|
||||
}
|
||||
|
||||
// Bare `Enum.variant` — a qualified enum literal. When the object is a type
|
||||
// NAME resolving to an enum / tagged-union (not shadowed by a value binding /
|
||||
// global value) and `field` is a PAYLOADLESS variant, construct it like the
|
||||
// leading-dot `.variant` in a typed context. Mirrors the `alias.Enum.variant`
|
||||
// namespace path above. Restricted to payloadless variants so a payload-
|
||||
// carrying `Ev.a(5)` still flows through the call path (which supplies the
|
||||
// payload) rather than being hijacked into a zero-arg `.a` here.
|
||||
if (fa.object.data == .identifier) {
|
||||
const oname = fa.object.data.identifier.name;
|
||||
const shadowed = if (self.scope) |s| s.lookup(oname) != null else false;
|
||||
if (!shadowed and !self.program_index.global_names.contains(oname)) {
|
||||
if (self.module.types.findByName(self.module.types.internString(oname))) |ty| {
|
||||
if (!ty.isBuiltin() and self.isPayloadlessVariant(ty, fa.field)) {
|
||||
const synth = self.alloc.create(Node) catch null;
|
||||
if (synth) |n| {
|
||||
n.* = .{ .span = span, .data = .{ .enum_literal = .{ .name = fa.field } } };
|
||||
const saved_tt = self.target_type;
|
||||
self.target_type = ty;
|
||||
const ref = self.lowerExpr(n);
|
||||
self.target_type = saved_tt;
|
||||
return ref;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pack-arity intercept: `<pack_name>.len` in a pack-fn mono's
|
||||
// body resolves to the comptime-known N. The mono doesn't
|
||||
// materialise the `[]Any` slice that the inline path used, so
|
||||
@@ -965,6 +992,25 @@ pub fn lowerEnumLiteral(self: *Lowering, el: *const ast.EnumLiteral) Ref {
|
||||
return self.builder.enumInit(tag, Ref.none, target);
|
||||
}
|
||||
|
||||
/// Is `field` a PAYLOADLESS variant of enum/tagged-union `ty`? A plain `.@"enum"`
|
||||
/// variant is always payloadless; a `tagged_union` variant is payloadless iff its
|
||||
/// payload is `void`. Used by `lowerFieldAccess` to recognise a bare
|
||||
/// `Enum.variant` qualified literal (payload-carrying variants stay on the call
|
||||
/// path, which supplies the payload). False for any non-enum type / unknown field.
|
||||
pub fn isPayloadlessVariant(self: *Lowering, ty: TypeId, field: []const u8) bool {
|
||||
return switch (self.module.types.get(ty)) {
|
||||
.@"enum" => |e| blk: {
|
||||
for (e.variants) |v| if (std.mem.eql(u8, self.module.types.getString(v), field)) break :blk true;
|
||||
break :blk false;
|
||||
},
|
||||
.tagged_union => |u| blk: {
|
||||
for (u.fields) |f| if (std.mem.eql(u8, self.module.types.getString(f.name), field)) break :blk (f.ty == .void);
|
||||
break :blk false;
|
||||
},
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// The enum twin of `emitBadVariant`: an unknown variant of a plain enum,
|
||||
/// with the legal variants listed.
|
||||
pub fn emitBadEnumVariant(
|
||||
|
||||
Reference in New Issue
Block a user