diff --git a/current/CHECKPOINT-REIFY.md b/current/CHECKPOINT-REIFY.md index d85661af..965d8dba 100644 --- a/current/CHECKPOINT-REIFY.md +++ b/current/CHECKPOINT-REIFY.md @@ -4,28 +4,51 @@ Companion to [PLAN-REIFY.md](PLAN-REIFY.md). Update after every step (one step a time, per the cadence rule). ## Last completed step -**None — stream just carved.** Design validated (3 codebase reviewers; all five reify -contracts confirmed feasible). No code written yet. +**Phase 0.0 (lock).** Added the comptime type-metaprogramming surface as the +on-demand module `library/modules/std/meta.sx` (NOT the prelude — see decision +below): `EnumVariant`/`EnumInfo`/`TypeInfo` data types + bodyless `#builtin` +decls `reify` / `type_info` / `field_type`. Each builtin bails LOUDLY when +reached unimplemented (no silent default). Unit test `src/parser.test.zig` +(registered in `src/root.zig`) locks that the decls parse. `zig build test` +green (447/447 unit, 669/669 examples). ## Current state -- The plan + the five locked contracts exist in `PLAN-REIFY.md`; design-of-record is - `design/execution-evolution-roadmap.md` §7 step 3 + §8.1. -- **Nothing built.** `reify`/`type_info`/`field_type` do not exist in the compiler. -- Confirmed against the source (anchors in the plan): type minting via - `intern`/`internNominal` is programmatic and AST-free; type-fns memoize by mangled - name; enum codegen is fully type-table-driven (zero AST coupling); recursive - forward-declaration (reserve→complete) already exists for source types. +- `modules/std/meta.sx` declares the surface; the variant uses the backtick raw + escape `` `enum `` (reads as the keyword, not a mangled `enum_`). +- **Loud bails wired (unimplemented → diagnostic, never a silent type):** + - `reify(...)` in a `::` type-alias position → `decl.zig` (the `.call` + const-decl branch) emits "reify is not yet implemented (REIFY Phase 0.2)" + and poisons the alias to `.unresolved`. This is also where Phase 0.2 will + hook the real construction. + - `reify` / `field_type` in any other type position → + `generic.zig:resolveTypeCallWithBindings` (defense-in-depth). + - `type_info(...)` in expression position → + `call.zig:tryLowerReflectionCall`. +- No interpreter-side construction yet — `reify` mints nothing. + +## Decision (0.0) +**Meta lives in `modules/std/meta.sx`, not the prelude (`core.sx`).** Declaring +the data types in the always-loaded prelude interns them into every module's +type table and shifts every `.ir` snapshot (broke 37 examples in a trial). An +on-demand import keeps the prelude clean; reify users `#import +"modules/std/meta.sx"`. (User-directed.) ## Next step -**Phase 0.0 (lock):** add `TypeInfo`/`EnumInfo`/`EnumVariant` data types + bodyless -`#builtin` decls for `reify`/`type_info`/`field_type` to `library/modules/std/core.sx` -(parsed, unimplemented → loud bail), with a unit test that the decls parse. Then 0.1 -(xfail: `examples/06xx-comptime-reify-enum.sx`) → 0.2 (green: implement `reify(.enum_)`). +**Phase 0.1 (xfail):** add `examples/06xx-comptime-reify-enum.sx` — `reify(.`enum( +.{ variants = .[ .{name="value",payload=i64}, .{name="closed",payload=void} ] }))`, +construct `.value(3)`, match it. RED (reify unimplemented → the loud bail above). +Seed `examples/expected/.exit`. Then 0.2 (green: implement `reify(.`enum)` +in the interpreter / `decl.zig` reify hook). ## Known issues None yet. ## Log +- **0.0 (lock).** Meta surface in `modules/std/meta.sx` (data types + 3 bodyless + `#builtin` decls), loud bails at all three reach points, `src/parser.test.zig` + parse-lock. Two user-directed refinements folded in: variant uses `` `enum `` + raw escape; surface moved out of the prelude into its own module to avoid + type-table / `.ir`-snapshot churn. `zig build test` green. - **Stream carved.** Selected as the first async-first foundation: `reify` gates both channel result types (`RecvResult($T)`) and `race`'s synthesized union, is fully validated (3 reviewers), and is a self-contained compiler/type-system feature diff --git a/library/modules/std/core.sx b/library/modules/std/core.sx index fff1c41f..72437786 100644 --- a/library/modules/std/core.sx +++ b/library/modules/std/core.sx @@ -31,6 +31,11 @@ field_value_int :: ($T: Type, idx: i64) -> i64 #builtin; field_index :: ($T: Type, val: T) -> i64 #builtin; error_tag_name :: (e: $T) -> string #builtin; +// Comptime type metaprogramming (`type_info` / `reify` / `field_type`) lives in +// the on-demand `modules/std/meta.sx`, NOT here — declaring its data types in +// the always-loaded prelude would intern them into every module's type table +// and shift every `.ir` snapshot. Import `modules/std/meta.sx` to use reify. + // Call-site location, synthesized by the `#caller_location` directive when it // is a parameter's default value (ERR E4.1b). `process.exit` / `assert` use it // to report where they were invoked. diff --git a/library/modules/std/meta.sx b/library/modules/std/meta.sx new file mode 100644 index 00000000..76f91732 --- /dev/null +++ b/library/modules/std/meta.sx @@ -0,0 +1,38 @@ +// Comptime type metaprogramming (REIFY) — `type_info` / `reify` / `field_type` +// plus the data model they reflect INTO and construct FROM. Mirrors the Zig +// `@typeInfo` / `@Type` split: reflect a type → data, construct a NEW nominal +// type from data. +// +// This is a SEPARATE on-demand module rather than part of the prelude: its data +// types would otherwise intern into every module's type table and shift every +// `.ir` snapshot. Import it explicitly: #import "modules/std/meta.sx"; +// +// `reify` / `type_info` / `field_type` are comptime-only builtins — a `reify` +// reached at runtime is a hard error (the type must be minted at compile time). + +// One variant of a reify'd enum: a name plus an optional payload type. +// `payload = void` means a tagless variant (e.g. `closed`). +EnumVariant :: struct { + name: string; + payload: Type; +} + +// The shape of an enum/tagged-union being reflected or constructed. +EnumInfo :: struct { + variants: []EnumVariant; +} + +// The reflected/constructed type shape. A tagged union over the kinds of type +// `reify` can mint. Phase 0 ships only `` .`enum ``; struct/tuple land later. +// The variant uses the backtick raw-identifier escape so it reads as the +// keyword `enum` (`` reify(.`enum(...)) ``) rather than a mangled `enum_`. +TypeInfo :: enum { + `enum: EnumInfo; +} + +// reify(info) — mint a NEW nominal type from a `TypeInfo` (comptime-only). +// type_info($T) — reflect an existing type into a `TypeInfo`. +// field_type($T, i) — the i-th field/variant payload type of `$T`. +reify :: (info: TypeInfo) -> Type #builtin; +type_info :: ($T: Type) -> TypeInfo #builtin; +field_type :: ($T: Type, idx: i64) -> Type #builtin; diff --git a/src/ir/lower/call.zig b/src/ir/lower/call.zig index 63ebb416..a2c64ad0 100644 --- a/src/ir/lower/call.zig +++ b/src/ir/lower/call.zig @@ -1676,6 +1676,16 @@ pub fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.C // classification covers all 7; it runs before dispatch. if (self.reflectionTypeArgGuard(name, c)) |sentinel| return sentinel; + if (std.mem.eql(u8, name, "type_info")) { + // Comptime reflection-into-data (REIFY). Until the interpreter-side + // reflection lands (Phase 2), bail loudly rather than fall through to + // the no-body `#builtin` const_decl path (which would mis-lower as a + // zero-arg call). A silent fall-through would hand the caller a + // garbage TypeInfo value. + if (self.diagnostics) |d| + d.addFmt(.err, c.callee.span, "type_info is not yet implemented (REIFY Phase 2)", .{}); + return Ref.none; + } if (std.mem.eql(u8, name, "size_of")) { // size_of(T) → const_int(sizeof(T)) const ty = self.resolveTypeArg(c.args[0]); diff --git a/src/ir/lower/decl.zig b/src/ir/lower/decl.zig index 2927f790..33eb6b68 100644 --- a/src/ir/lower/decl.zig +++ b/src/ir/lower/decl.zig @@ -651,6 +651,17 @@ pub fn scanDecls(self: *Lowering, decls: []const *const Node) void { .field_access => |fa| fa.field, else => "", }; + // `E :: reify(...)` — mint a nominal type from a `TypeInfo` + // and register `E` as an alias to it. The interpreter-side + // construction lands in Phase 0.2; until then bail LOUDLY + // and poison `E` to `.unresolved` (so downstream `E.value` + // gets a clean follow-on, not a silent default type). + if (std.mem.eql(u8, callee_name, "reify")) { + if (self.diagnostics) |d| + d.addFmt(.err, cd.value.span, "reify is not yet implemented (REIFY Phase 0.2)", .{}); + self.putTypeAlias(self.current_source_file, cd.name, .unresolved); + continue; + } // A namespaced callee (`ns.Box(..)`) is an explicit qualified // reach, exempt from the bare-head visibility gate (E4). const head_qualified = call_data.callee.data == .field_access; diff --git a/src/ir/lower/generic.zig b/src/ir/lower/generic.zig index 46d2ee04..50728f9f 100644 --- a/src/ir/lower/generic.zig +++ b/src/ir/lower/generic.zig @@ -1219,6 +1219,22 @@ pub fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId .field_access => |fa| fa.field, else => return .unresolved, }; + // Comptime type-construction builtins (REIFY). `reify`/`field_type` + // appear in type position (`E :: reify(...)`, `field_type(T, i)` as a + // type arg). Until the interpreter-side construction lands (Phase 0.2 / + // Phase 2), bail LOUDLY rather than fall through to the misleading + // "unknown type 'reify'" diagnostic below — a silent default here would + // poison every downstream use of the type. + if (std.mem.eql(u8, callee_name, "reify")) { + if (self.diagnostics) |d| + d.addFmt(.err, cl.callee.span, "reify is not yet implemented (REIFY Phase 0.2)", .{}); + return .unresolved; + } + if (std.mem.eql(u8, callee_name, "field_type")) { + if (self.diagnostics) |d| + d.addFmt(.err, cl.callee.span, "field_type is not yet implemented (REIFY Phase 2)", .{}); + return .unresolved; + } // Built-in: Vector(N, T) if (std.mem.eql(u8, callee_name, "Vector") and cl.args.len == 2) { const length = self.resolveVectorLane(cl.args[0]) orelse return .unresolved; diff --git a/src/parser.test.zig b/src/parser.test.zig new file mode 100644 index 00000000..75c32c9e --- /dev/null +++ b/src/parser.test.zig @@ -0,0 +1,79 @@ +// Parser tests — pin parse-level shapes the example corpus can't isolate +// (the corpus runs the full `sx run` pipeline, never the parser alone). + +const std = @import("std"); +const ast = @import("ast.zig"); +const Node = ast.Node; +const Parser = @import("parser.zig").Parser; + +// REIFY Phase 0.0 (lock): the comptime type-metaprogramming surface added to +// `library/modules/std/meta.sx` must PARSE — the data types as struct/enum +// decls, and `reify`/`type_info`/`field_type` as bodyless `#builtin` consts. +// This locks the declared shape before the interpreter-side construction lands +// (Phase 0.2). Mirrors the exact spellings in meta.sx. +test "parser: reify TypeInfo data types + #builtin decls parse" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + const src = + \\EnumVariant :: struct { + \\ name: string; + \\ payload: Type; + \\} + \\EnumInfo :: struct { + \\ variants: []EnumVariant; + \\} + \\TypeInfo :: enum { + \\ `enum: EnumInfo; + \\} + \\reify :: (info: TypeInfo) -> Type #builtin; + \\type_info :: ($T: Type) -> TypeInfo #builtin; + \\field_type :: ($T: Type, idx: i64) -> Type #builtin; + \\ + ; + var parser = Parser.init(alloc, src); + const root = try parser.parse(); + + try std.testing.expect(root.data == .root); + const decls = root.data.root.decls; + try std.testing.expectEqual(@as(usize, 6), decls.len); + + const Found = struct { + // A top-level `Name :: struct/enum {…}` parses to a `.struct_decl` / + // `.enum_decl` node DIRECTLY (not wrapped in a const_decl); only the + // `#builtin` forms are `.const_decl`. Match on the shared `declName`. + fn byName(ds: []const *Node, name: []const u8) ?*const Node { + for (ds) |d| { + if (d.data.declName()) |n| { + if (std.mem.eql(u8, n, name)) return d; + } + } + return null; + } + }; + + // Data types: struct / struct / enum, parsed as their decl nodes directly. + const ev = Found.byName(decls, "EnumVariant") orelse return error.MissingDecl; + try std.testing.expect(ev.data == .struct_decl); + const ei = Found.byName(decls, "EnumInfo") orelse return error.MissingDecl; + try std.testing.expect(ei.data == .struct_decl); + const ti = Found.byName(decls, "TypeInfo") orelse return error.MissingDecl; + try std.testing.expect(ti.data == .enum_decl); + + // The single `` `enum `` variant of TypeInfo. The backtick raw escape + // stores the bare keyword as the variant name. + const ed = ti.data.enum_decl; + try std.testing.expectEqual(@as(usize, 1), ed.variant_names.len); + try std.testing.expectEqualStrings("enum", ed.variant_names[0]); + + // Builtins: the `(params) -> Ret #builtin;` form parses as a `.fn_decl` + // (the `->` triggers the function-def path) whose body is a `#builtin` + // marker — same shape as the existing reflection builtins in core.sx. + for ([_][]const u8{ "reify", "type_info", "field_type" }) |bn| { + const d = Found.byName(decls, bn) orelse return error.MissingDecl; + try std.testing.expect(d.data == .fn_decl); + try std.testing.expect(d.data.fn_decl.body.data == .builtin_expr); + try std.testing.expect(d.data.fn_decl.return_type != null); + } +} diff --git a/src/root.zig b/src/root.zig index 9834401a..fb8af253 100644 --- a/src/root.zig +++ b/src/root.zig @@ -4,6 +4,7 @@ pub const lexer = @import("lexer.zig"); pub const lexer_tests = @import("lexer.test.zig"); pub const ast = @import("ast.zig"); pub const parser = @import("parser.zig"); +pub const parser_tests = @import("parser.test.zig"); pub const print = @import("print.zig"); pub const types = @import("types.zig"); pub const target = @import("target.zig");