diff --git a/current/CHECKPOINT-COMPILER-API.md b/current/CHECKPOINT-COMPILER-API.md index 7487be34..5a0ccbea 100644 --- a/current/CHECKPOINT-COMPILER-API.md +++ b/current/CHECKPOINT-COMPILER-API.md @@ -26,30 +26,33 @@ with ONE welded mechanism. Branch: `reify` (off `master`). Update after every st > breaks cross-compilation — host vs target layout — and loses the sandbox. A > flat-memory VM keeps both while getting native bytes + speed.) > -> **Next action (2026-06-18):** **Phase 3 is UNDER WAY.** The VM now hosts five read-only -> reflection readers, all on the same plain-`u32`-handle shape (a `TypeId` is a `u32`, like -> `StringId`), so the calls stay clean scalar host-calls — handle in, scalar out, no -> marshaling: `find_type(name) -> TypeId`, `type_field_count(t) -> i64`, -> `type_nominal_name(t) -> StringId`, `type_field_name(t, idx) -> StringId`, -> `type_field_type(t, idx) -> TypeId`. Each is backed by a `TypeTable` query -> (`findByName`/`memberCount`/`nominalName`/`memberName`/`memberType`) that BOTH the legacy -> handler and the VM call, so the two paths can't drift. Examples `0628` (`find_type` + -> `type_field_count`) and `0629` (field reflection over `Pair { lo: Point; hi: Point }`), -> both VM-HANDLED natively. Parity **690/690** (gate ON and OFF), VM unit tests added. -> Phase 1.final op-porting was already complete (the VM covers -> scalars/control-flow/aggregates/strings/optionals/enums, calls+recursion, the implicit -> context + full allocator protocol, globals, failables + return traces); both comptime -> call sites route through the VM with legacy fallback. -> **Forward (P3.3):** `register_struct` (the first MUTATING fn — mints a `TypeId`; resolve -> the mutable-table / host-ABI-vs-target-ABI boundary deliberately), plus any remaining -> read-only readers the metatype needs (kind query, `field_value_int`). Re-expressing -> `declare`/`define`/`type_info` as sx (the metatype, which runs at LOWERING time) needs the -> VM hardened against malformed lowering-time IR first — keep it on the legacy path until -> then. Phase 2 (bytecode) is the orthogonal speed work. **Decisions recorded:** `find_type` -> returns a non-optional `TypeId` using the `unresolved` (0) sentinel, NOT `?Type`; reader -> names use the `type_*` family to avoid colliding with the std metatype builtins -> (`field_name`/`type_name` in core.sx) — see `PLAN-COMPILER-VM.md` Phase 3 progress note. -> Build/verify: `zig build && zig build test` (690, gate OFF). Run the corpus ON the VM: +> **Next action (2026-06-18):** **Phase 3 is UNDER WAY — the READ side is COMPLETE.** The VM +> hosts seven read-only reflection readers, all on the same plain-`u32`-handle shape (a +> `TypeId` is a `u32`, like `StringId`), so the calls stay clean scalar host-calls — handle +> in, scalar out, no marshaling: `find_type(name) -> TypeId`, `type_kind(t) -> i64`, +> `type_field_count(t) -> i64`, `type_nominal_name(t) -> StringId`, +> `type_field_name(t, idx) -> StringId`, `type_field_type(t, idx) -> TypeId`, +> `type_field_value(t, idx) -> i64`. Each is backed by a `TypeTable` query +> (`findByName`/`kindCode`/`memberCount`/`nominalName`/`memberName`/`memberType`/`memberValue`) +> that BOTH the legacy handler and the VM call, so the two paths can't drift. Together they +> cover everything `reflectTypeInfo` reads. Examples `0628`–`0630`, all VM-HANDLED natively. +> Parity **691/691** (gate ON and OFF), VM unit tests added. Phase 1.final op-porting was +> already complete (the VM covers scalars/control-flow/aggregates/strings/optionals/enums, +> calls+recursion, the implicit context + full allocator protocol, globals, failables + +> return traces); both comptime call sites route through the VM with legacy fallback. +> **Forward (P3.3) — revised direction:** the WRITE side is ONE `register_type(info)` fn that +> takes a type-info value and **branches on the kind in the compiler** (subsuming +> `define`'s per-kind dispatch), NOT a per-kind `register_struct`. Open design points when +> reached: the flat-memory shape of `info`, the mutable-table / host-ABI-vs-target-ABI +> boundary, pointer-escape/lifetime. Re-expressing `declare`/`define`/`type_info` as sx (the +> metatype, which runs at LOWERING time) needs the VM hardened against malformed +> lowering-time IR first — keep it on the legacy path until then. Phase 2 (bytecode) is the +> orthogonal speed work. **Decisions recorded:** `find_type` returns a non-optional `TypeId` +> using the `unresolved` (0) sentinel, NOT `?Type`; reader names use the `type_*` family to +> avoid colliding with the std metatype builtins (`field_name`/`type_name` in core.sx); the +> write side is a single kind-branching `register_type` — see `PLAN-COMPILER-VM.md` Phase 3 +> progress note. +> Build/verify: `zig build && zig build test` (691, gate OFF). Run the corpus ON the VM: > `zig build test -Dcomptime-flat` (the build flag) OR env `SX_COMPTIME_FLAT=1`. Coverage > trace: `SX_COMPTIME_FLAT_TRACE=1`. @@ -322,6 +325,20 @@ when reached (sentinels or accessor fns; see the design doc Risks). `List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.) ## Log +- **Phase 3 P3.2b (VM plan) — kind + enum-value readers: `type_kind` + `type_field_value`; READ side complete (2026-06-18).** + The last two read-only readers the metatype's `type_info(T)` needs (added to + `compiler_lib.bound_fns` AND `Vm.callCompilerFn`, each backed by a `TypeTable` query both + call): `type_kind(t) -> i64` (`kindCode` — a stable, compiler-owned discriminant: 0 other · + 1 struct · 2 enum · 3 tagged_union · 4 tuple · 5 union · 6 array · 7 vector · 8 error_set; + TOTAL, never bails) and `type_field_value(t, idx) -> i64` (`memberValue` — an enum variant's + explicit value or ordinal; mirrors the `field_value_int` builtin; loud-bail for non-enum / + out-of-range). Example `0630-comptime-compiler-type-kind` reflects `Color` / `WindowFlags` + (flags) / `Point`. **The READ side is now COMPLETE** — `find_type` + `type_kind` + + `type_field_count` + `type_field_name`/`type_field_type`/`type_nominal_name` + + `type_field_value` cover everything `reflectTypeInfo` reads. VM unit test added. **Parity + 691/691** (gate ON and OFF). **Revised forward direction (per the user):** the WRITE side is + ONE `register_type(info)` fn that branches on the kind IN THE COMPILER (subsuming `define`'s + per-kind dispatch), not a per-kind `register_struct`. - **Phase 3 P3.2 (VM plan) — field-level reflection readers: `type_nominal_name` + `type_field_name` + `type_field_type` (2026-06-18).** Three more `compiler`-library readers on the same `TypeId`-handle shape (added to `compiler_lib.bound_fns` AND `Vm.callCompilerFn`), each backed by a new `TypeTable` query diff --git a/current/PLAN-COMPILER-VM.md b/current/PLAN-COMPILER-VM.md index 275c50e3..07e4f24e 100644 --- a/current/PLAN-COMPILER-VM.md +++ b/current/PLAN-COMPILER-VM.md @@ -289,11 +289,29 @@ host through it: exist in `core.sx`). Example `0629` reflects `Pair { lo: Point; hi: Point }` — reads each field name and the nominal name of a field's type, all folded at `#run`, all VM-HANDLED natively. Parity **690/690** (gate ON and OFF); VM unit test added. -- **Next (P3.3):** `register_struct` (the first MUTATING fn — mints a `TypeId`; resolve the - mutable-table / host-ABI-vs-target-ABI boundary deliberately, per the open questions), - plus any remaining read-only readers needed to re-express the metatype (kind query, - `field_value_int` for enums). Re-expressing `declare`/`define`/`type_info` as sx (the - metatype, which runs at LOWERING time) still needs the VM hardened against malformed +- **(P3.2b) Kind + enum-value readers — `type_kind` + `type_field_value` (DONE).** The last + two read-only readers the metatype's `type_info(T)` needs, completing the READ side: a + comptime sx fn can now fully reflect a struct/enum/tagged-union/tuple into data with no + `#builtin`. `type_kind(t: TypeId) -> i64` (`TypeTable.kindCode` — a stable, compiler-owned + discriminant: 0 other · 1 struct · 2 enum · 3 tagged_union · 4 tuple · 5 union · 6 array · + 7 vector · 8 error_set; TOTAL — never bails, an unnamed/non-aggregate type reads `other`) + and `type_field_value(t: TypeId, idx: i64) -> i64` (`TypeTable.memberValue` — an enum + variant's explicit value or ordinal; mirrors the `field_value_int` builtin; loud-bail for + a non-enum / out-of-range idx). Example `0630` reflects `Color`/`WindowFlags`(flags)/`Point`. + Parity **691/691** (gate ON and OFF); VM unit test added. + - **READ side now complete:** `find_type` + `type_kind` + `type_field_count` + + `type_field_name` + `type_field_type` + `type_nominal_name` + `type_field_value` cover + everything `reflectTypeInfo` reads. +- **Next (P3.3) — ONE `register_type(info)` write fn (revised direction, 2026-06-18):** per + the user, the mutating side is NOT per-kind (`register_struct`/`register_enum`/…) but a + SINGLE function that takes a type-info value and **branches on the kind in the compiler**, + minting the right `TypeInfo`. This subsumes `define`'s `defineStruct`/`defineEnum`/ + `defineTuple` dispatch into one host-side switch. Open design points to resolve when + reached: the flat-memory shape of the `info` argument the sx side passes (a tagged + `{ kind, payload }` over the readers' handle types), the mutable-table / host-ABI-vs- + target-ABI boundary, and pointer-escape/lifetime (escaping field arrays copied into + compiler-owned memory at the boundary). Re-expressing `declare`/`define`/`type_info` as sx + (the metatype, which runs at LOWERING time) still needs the VM hardened against malformed lowering-time IR first — keep that on the legacy path until then (see the resume note in CHECKPOINT-COMPILER-API.md). diff --git a/examples/0630-comptime-compiler-type-kind.sx b/examples/0630-comptime-compiler-type-kind.sx new file mode 100644 index 00000000..18829246 --- /dev/null +++ b/examples/0630-comptime-compiler-type-kind.sx @@ -0,0 +1,47 @@ +// Comptime compiler API — type-kind + enum-value reflection readers (Phase 3). +// +// Completes the READ side the metatype needs to re-express `type_info(T)` as sx: +// +// type_kind(t) → a stable kind discriminant, to branch on the shape +// (0 other · 1 struct · 2 enum · 3 tagged_union · +// 4 tuple · 5 union · 6 array · 7 vector · 8 error_set) +// type_field_value(t, i) → enum variant i's integer value (explicit or ordinal) +// +// Together with find_type / type_field_count / type_field_name / type_field_type +// / type_nominal_name (examples 0628–0629), a comptime sx function can now fully +// reflect a struct/enum/tuple into data — no `#builtin` needed. All folded at +// `#run`, all serviced natively by the flat-memory VM. + +#import "modules/std.sx"; + +compiler :: #library "compiler"; + +StringId :: u32; +TypeId :: u32; + +intern :: (s: string) -> StringId abi(.zig) extern compiler; +text_of :: (id: StringId) -> string abi(.zig) extern compiler; +find_type :: (name: StringId) -> TypeId abi(.zig) extern compiler; +type_kind :: (t: TypeId) -> i64 abi(.zig) extern compiler; +type_field_name :: (t: TypeId, idx: i64) -> StringId abi(.zig) extern compiler; +type_field_value :: (t: TypeId, idx: i64) -> i64 abi(.zig) extern compiler; + +Color :: enum { red; green; blue; } +WindowFlags :: enum flags u32 { vsync :: 64; resizable :: 4; hidden :: 128; } +Point :: struct { x: i64; y: i64; } + +color_kind :: #run type_kind(find_type(intern("Color"))); // 2 = enum +point_kind :: #run type_kind(find_type(intern("Point"))); // 1 = struct + +// Plain enum → ordinal values. +green_val :: #run type_field_value(find_type(intern("Color")), 1); // 1 + +// Flags enum → explicit values, read by name + value. +vsync_name :: #run text_of(type_field_name(find_type(intern("WindowFlags")), 0)); // "vsync" +vsync_val :: #run type_field_value(find_type(intern("WindowFlags")), 0); // 64 + +main :: () { + print("Color kind = {}, Point kind = {}\n", color_kind, point_kind); + print("Color.green = {}\n", green_val); + print("WindowFlags.{} = {}\n", vsync_name, vsync_val); +} diff --git a/examples/expected/0630-comptime-compiler-type-kind.exit b/examples/expected/0630-comptime-compiler-type-kind.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0630-comptime-compiler-type-kind.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0630-comptime-compiler-type-kind.stderr b/examples/expected/0630-comptime-compiler-type-kind.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0630-comptime-compiler-type-kind.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0630-comptime-compiler-type-kind.stdout b/examples/expected/0630-comptime-compiler-type-kind.stdout new file mode 100644 index 00000000..5b508f10 --- /dev/null +++ b/examples/expected/0630-comptime-compiler-type-kind.stdout @@ -0,0 +1,3 @@ +Color kind = 2, Point kind = 1 +Color.green = 1 +WindowFlags.vsync = 64 diff --git a/src/ir/compiler_lib.zig b/src/ir/compiler_lib.zig index 01ae8b89..0ca21321 100644 --- a/src/ir/compiler_lib.zig +++ b/src/ir/compiler_lib.zig @@ -52,6 +52,8 @@ pub const bound_fns = [_]BoundFn{ .{ .sx_name = "type_nominal_name", .handler = handleTypeNominalName }, .{ .sx_name = "type_field_name", .handler = handleTypeFieldName }, .{ .sx_name = "type_field_type", .handler = handleTypeFieldType }, + .{ .sx_name = "type_kind", .handler = handleTypeKind }, + .{ .sx_name = "type_field_value", .handler = handleTypeFieldValue }, }; /// Look up a compiler function by its sx name. Returns null when the name is not @@ -151,3 +153,20 @@ fn handleTypeFieldType(interp: *Interpreter, args: []const Value) InterpError!Va const mty = interp.module.types.memberType(tid, args[1].int) orelse return error.TypeError; return Value{ .int = mty.index() }; } + +/// `type_kind(t: TypeId) -> i64` — the stable kind discriminant (see +/// `TypeTable.kindCode`). Total: an unnamed/non-aggregate type reads `other` (0). +fn handleTypeKind(interp: *Interpreter, args: []const Value) InterpError!Value { + if (args.len != 1) return error.TypeError; + const tid: types.TypeId = @enumFromInt(try handleArg(args, 0)); + return Value{ .int = interp.module.types.kindCode(tid) }; +} + +/// `type_field_value(t: TypeId, idx: i64) -> i64` — enum variant `idx`'s integer +/// value (explicit or ordinal). Loud error for a non-enum or out-of-range idx. +fn handleTypeFieldValue(interp: *Interpreter, args: []const Value) InterpError!Value { + if (args.len != 2 or args[1] != .int) return error.TypeError; + const tid: types.TypeId = @enumFromInt(try handleArg(args, 0)); + const v = interp.module.types.memberValue(tid, args[1].int) orelse return error.TypeError; + return Value{ .int = v }; +} diff --git a/src/ir/comptime_vm.test.zig b/src/ir/comptime_vm.test.zig index 6e919af3..9be82967 100644 --- a/src/ir/comptime_vm.test.zig +++ b/src/ir/comptime_vm.test.zig @@ -907,6 +907,70 @@ test "comptime_vm exec: compiler-fn type_field_name/type/nominal_name (native re try std.testing.expectEqual(@as(i64, @intFromEnum(point_name)), toI64(try v.run(module.getFunction(main2), &.{}))); } +test "comptime_vm exec: compiler-fn type_kind + type_field_value (native reflection)" { + const alloc = std.testing.allocator; + var module = Module.init(alloc); + defer module.deinit(); + + // A struct and an enum with explicit values. + const pfields = [_]types.TypeInfo.StructInfo.Field{ + .{ .name = module.types.internString("x"), .ty = .i64 }, + .{ .name = module.types.internString("y"), .ty = .i64 }, + }; + const point = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Point"), .fields = &pfields } }); + const variants = [_]types.StringId{ module.types.internString("ok"), module.types.internString("missing") }; + const evals = [_]i64{ 200, 404 }; + const status = module.types.intern(.{ .@"enum" = .{ + .name = module.types.internString("Status"), + .variants = &variants, + .explicit_values = &evals, + } }); + + // extern type_kind(t: u32) -> i64 [compiler] (FuncId 0) + const kp = [_]Function.Param{param(.u32)}; + var kb = Fb.init(alloc, &kp, .i64); + kb.func.is_extern = true; + kb.func.compiler_welded = true; + kb.func.name = module.types.internString("type_kind"); + const kind_id = module.addFunction(kb.func); + + // extern type_field_value(t: u32, idx: i64) -> i64 [compiler] (FuncId 1) + const vp = [_]Function.Param{ param(.u32), param(.i64) }; + var vb = Fb.init(alloc, &vp, .i64); + vb.func.is_extern = true; + vb.func.compiler_welded = true; + vb.func.name = module.types.internString("type_field_value"); + const val_id = module.addFunction(vb.func); + + var v = vm.Vm.init(alloc); + v.table = &module.types; + v.module = &module; + defer v.deinit(); + + // type_kind(Point) → 1 (struct); type_kind(Status) → 2 (enum). + inline for (.{ .{ point, 1 }, .{ status, 2 } }) |case| { + var fb = Fb.init(alloc, &.{}, .i64); + const b0 = fb.block(&.{}); + const t = fb.add(b0, inst(.{ .const_int = @intFromEnum(case[0]) }, .u32)); + const kargs = [_]Ref{ref(t)}; + const k = fb.add(b0, inst(.{ .call = .{ .callee = kind_id, .args = &kargs } }, .i64)); + _ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(k) } }, .void)); + const mid = module.addFunction(fb.func); + try std.testing.expectEqual(@as(i64, case[1]), toI64(try v.run(module.getFunction(mid), &.{}))); + } + + // type_field_value(Status, 1) → 404 (explicit value). + var fb = Fb.init(alloc, &.{}, .i64); + const b0 = fb.block(&.{}); + const t = fb.add(b0, inst(.{ .const_int = @intFromEnum(status) }, .u32)); + const one = fb.add(b0, inst(.{ .const_int = 1 }, .i64)); + const vargs = [_]Ref{ ref(t), ref(one) }; + const val = fb.add(b0, inst(.{ .call = .{ .callee = val_id, .args = &vargs } }, .i64)); + _ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(val) } }, .void)); + const mid = module.addFunction(fb.func); + try std.testing.expectEqual(@as(i64, 404), toI64(try v.run(module.getFunction(mid), &.{}))); +} + test "comptime_vm exec: func_ref + call_indirect dispatch" { const alloc = std.testing.allocator; var module = Module.init(alloc); diff --git a/src/ir/comptime_vm.zig b/src/ir/comptime_vm.zig index 26ce5631..c79396a9 100644 --- a/src/ir/comptime_vm.zig +++ b/src/ir/comptime_vm.zig @@ -1067,6 +1067,19 @@ pub const Vm = struct { return self.failMsg("comptime type_field_type: out-of-range idx or member has no type"); return @as(Reg, mty.index()); } + if (std.mem.eql(u8, name, "type_kind")) { + if (args.len != 1) return self.failMsg("comptime type_kind: expected one TypeId arg"); + const tid = try self.argTypeId(args, frame, 0); + return @as(Reg, @bitCast(table.kindCode(tid))); // total — never bails + } + if (std.mem.eql(u8, name, "type_field_value")) { + if (args.len != 2) return self.failMsg("comptime type_field_value: expected (TypeId, idx)"); + const tid = try self.argTypeId(args, frame, 0); + const idx: i64 = @bitCast(frame.get(args[1].index())); + const v = table.memberValue(tid, idx) orelse + return self.failMsg("comptime type_field_value: non-enum or out-of-range idx"); + return @as(Reg, @bitCast(v)); + } return null; // not a known compiler function → caller bails to legacy } diff --git a/src/ir/types.zig b/src/ir/types.zig index 65d68578..d2463187 100644 --- a/src/ir/types.zig +++ b/src/ir/types.zig @@ -554,6 +554,46 @@ pub const TypeTable = struct { }; } + /// Stable kind discriminant of a type, for comptime reflection branching. + /// TOTAL (never fails): an unnamed / non-aggregate type or an out-of-range id + /// is `other` (0). Codes are compiler-owned and stable — NOT tied to any sx + /// enum's declaration order; the sx side maps them. Backs the `type_kind` + /// reader. (A `tagged_union` is a payload-carrying enum; the sx metatype folds + /// codes 2 and 3 onto its single `.enum` TypeInfo variant.) + /// 0 other · 1 struct · 2 enum · 3 tagged_union · 4 tuple + /// 5 union · 6 array · 7 vector · 8 error_set + pub fn kindCode(self: *const TypeTable, id: TypeId) i64 { + if (id.index() >= self.infos.items.len) return 0; + return switch (self.get(id)) { + .@"struct" => 1, + .@"enum" => 2, + .tagged_union => 3, + .tuple => 4, + .@"union" => 5, + .array => 6, + .vector => 7, + .error_set => 8, + else => 0, + }; + } + + /// Integer value of enum variant `idx`: its explicit value when the enum + /// declares one (custom values or flags), else its ordinal. Null for a + /// non-enum type, a negative / out-of-range `idx`, or an out-of-range id. + /// Backs the `type_field_value` reader (mirrors the `field_value_int` builtin). + pub fn memberValue(self: *const TypeTable, id: TypeId, idx: i64) ?i64 { + if (idx < 0 or id.index() >= self.infos.items.len) return null; + const i: usize = @intCast(idx); + return switch (self.get(id)) { + .@"enum" => |e| blk: { + if (i >= e.variants.len) break :blk null; + if (e.explicit_values) |vals| if (i < vals.len) break :blk vals[i]; + break :blk @intCast(i); // ordinal default + }, + else => null, + }; + } + /// Source-sensitive variant of `findByName`: asserts at most one named type /// matches, then returns it (or null). Quarantines the global first-match /// scan — new resolver code that must not silently pick a first-of-many