From ac8c68951874d94da7258510d789d2ea860fb90a Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 16 Jun 2026 19:06:57 +0300 Subject: [PATCH] green(reify): field_type($T, i) -> Type over the type table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit REIFY Phase 2.1. fieldTypeOf (lower/generic.zig, re-exported on Lowering) returns the i-th member type of T: struct field / tagged-union + union variant payload (.void for a tagless variant) / tuple element / array + vector element. Out-of-range and memberless types poison to .unresolved with a loud diagnostic (never a silent default). Wired into resolveTypeCallWithBindings (replacing the Phase-2 bail); since it folds to a TypeId at lower time it composes inside type_eq / type_name / any type-arg slot. examples/0616 green: struct fields (name via field_name + type via field_type), type_eq fold, tagged-union payloads incl. quit -> void. Suite green (672 examples, 447 unit). type_info($T) -> TypeInfo (reflect into a value, inverse of reify) is NOT done — still bails loudly; it's the larger Phase 2.2 step (widen the TypeInfo data model + comptime value construction). Plan/checkpoint updated. --- current/CHECKPOINT-REIFY.md | 48 ++++++++++++---- current/PLAN-REIFY.md | 9 ++- .../expected/0616-comptime-field-type.exit | 1 + .../expected/0616-comptime-field-type.stderr | 1 + .../expected/0616-comptime-field-type.stdout | 9 +++ src/ir/lower.zig | 1 + src/ir/lower/generic.zig | 55 ++++++++++++++++++- 7 files changed, 107 insertions(+), 17 deletions(-) create mode 100644 examples/expected/0616-comptime-field-type.stderr create mode 100644 examples/expected/0616-comptime-field-type.stdout diff --git a/current/CHECKPOINT-REIFY.md b/current/CHECKPOINT-REIFY.md index 6f5e7bc0..e499fa07 100644 --- a/current/CHECKPOINT-REIFY.md +++ b/current/CHECKPOINT-REIFY.md @@ -4,7 +4,21 @@ Companion to [PLAN-REIFY.md](PLAN-REIFY.md). Update after every step (one step a time, per the cadence rule). ## Last completed step -**Phase 1 (type-fn → reify identity) — COMPLETE.** A type-fn body that returns +**Phase 2.1 (green) — `field_type` done.** `field_type($T, i) -> Type` is +implemented over the type table (`fieldTypeOf` in `lower/generic.zig`, re-exported +on `Lowering`): struct field / tagged-union + `union` variant payload (`.void` for +a tagless variant) / tuple element / array + vector element; OOB and memberless +types poison to `.unresolved` with a loud diagnostic (never a silent default). +It folds at lower time, so it composes inside `type_eq` / `type_name` / any type-arg +slot. `examples/0616` green (struct fields name+type, `type_eq` fold, tagged-union +payloads incl. `quit → void`). Full suite green (672 examples, 447 unit). Cadence: +2.0 xfail (empty marker, RED) → 2.1 green (this commit). + +**`type_info` is NOT done** — it still bails loudly in +`call.zig:tryLowerReflectionCall`. It builds a full `TypeInfo` *value* (inverse of +reify) and is the larger Phase 2.2 step (see Next step). + +### (prior) Phase 1 (type-fn → reify identity) — COMPLETE. A type-fn body that returns `reify(...)` now mints the enum under the instantiation's name: `instantiateTypeFunction` (`lower/generic.zig`) detects a reify-returning body (`findReturnReifyCall`) and routes it to `reifyType(, @@ -65,16 +79,23 @@ on-demand import keeps the prelude clean; reify users `#import "modules/std/meta.sx"`. (User-directed.) ## Next step -**Phase 2 (`type_info` + `field_type`).** Reflect a struct/tuple → read variant / -field names + **types** (`field_type($T, i) -> Type`, `type_info($T) -> TypeInfo`). -xfail → green by implementing both over the type table (reuse the -`field_count`/`field_name` reflection path; both currently bail loudly — -`type_info` in `call.zig:tryLowerReflectionCall`, `field_type` in -`generic.zig:resolveTypeCallWithBindings`). NOTE: `reifyType` still reads a LITERAL -`TypeInfo` off the AST (works for the inline-literal and type-fn-over-literal cases -Phases 0–1 use); a `type_info`-derived (computed, non-literal) `TypeInfo` fed back -into `reify` would need the reader generalized (or interp evaluation) — call that out -when Phase 2 enables round-tripping. +**Phase 2.2 (`type_info($T) -> TypeInfo`).** Reflect a type into a `TypeInfo` +*value* — the inverse of reify. Two sub-pieces, both non-trivial: (a) widen the +`meta.sx` `TypeInfo` data model beyond `` `enum `` (struct / tuple variants); (b) +build the value at comptime — a `[]EnumVariant`-style slice of structs holding +strings (`name`) + `Type` tags (`payload`), populated from the type table. This is +comptime value-CONSTRUCTION (allocating slice/struct/string/type-tag values in the +interpreter), materially larger than 2.1's fold-to-a-TypeId. Currently bails loudly +in `call.zig:tryLowerReflectionCall`. Best done as its own session for context room. + +NOTE (round-tripping): `reifyType` still reads a LITERAL `TypeInfo` off the AST +(fine for the inline-literal + type-fn-over-literal cases of Phases 0–1). Once 2.2 +produces a COMPUTED `TypeInfo`, feeding it back into `reify` needs the reader +generalized (or interp evaluation of the `TypeInfo` value) — handle that when 2.2 +enables round-tripping. + +Alternatively jump to **Phase 3** (`make_enum` + `RecvResult`/`TryResult` sx lib over +`reify`) — that only needs reify (have it) + type-fns (have them), not `type_info`. SELF-REFERENCE = Phase 4, API DECIDED (user-directed): explicit **`declare()` → `define(h, info)`** (the declaration-vs-definition split; NOT a `reify_rec((self)=>…)` @@ -92,6 +113,11 @@ in PLAN-REIFY Phase 4. None yet. ## Log +- **2.1 (green) — `field_type` done.** `fieldTypeOf` over the type table + (struct/tagged-union/union/tuple/array/vector; OOB+memberless = loud poison); + folds at lower time, composes in `type_eq`/`type_name`; `0616` green. `type_info` + still pending (Phase 2.2 — builds a TypeInfo value). +- **2.0 (xfail).** `0616` + empty `.exit` marker → RED (field_type bailed). - **1.1 (green) — Phase 1 done.** Type-fn body `return reify(...)` routes through `reifyType` under the instantiation name (`findReturnReifyCall` + mangled-name registration); `Box(i64)` at two sites = one type (Contract 1); diff --git a/current/PLAN-REIFY.md b/current/PLAN-REIFY.md index 0de9468e..2bbc1a00 100644 --- a/current/PLAN-REIFY.md +++ b/current/PLAN-REIFY.md @@ -70,11 +70,12 @@ Examples: `06xx` (comptime, deterministic), `11xx` (diagnostics for loud failure | 1.0 | xfail | `examples/06xx-comptime-reify-typefn-identity.sx` — `R :: ($T)->Type { reify(...) }`; assert `R(i64)` from two sites is ONE type (assignable/matchable across sites). Red if reify-result not registered by mangled name. | `examples/06xx-*` | | 1.1 | green | register a reify-returning type-fn's result under the instantiation mangled name (mirror the inline-struct path `generic.zig:1663-1689`). Identity holds (Contract 1). | `src/ir/lower/generic.zig` | -### Phase 2 — `type_info` (reflect) + `field_type` +### Phase 2 — `field_type` (done) + `type_info` (reflect → value, pending) | Step | Commit | What | Files | |---|---|---|---| -| 2.0 | xfail | reflect a struct/tuple → read variant/field names + **types** (`field_type($T,i)`). Red. | `examples/06xx-*` | -| 2.1 | green | implement `type_info`/`field_type` over the type table (reuse the `field_count`/`field_name` reflection path). | `src/ir/interp.zig` | +| 2.0 | xfail | reflect a struct/tuple/tagged-union → read field/variant names + **types** (`field_type($T,i)`). Red. | `examples/0616-*` | +| 2.1 | green | **DONE.** `field_type($T, i) -> Type` over the type table (`fieldTypeOf` in `generic.zig`): struct field / tagged-union+union variant payload / tuple element / array+vector element; OOB + memberless → loud poison. Folds at lower time, composes in `type_eq`/`type_name`. | `src/ir/lower/generic.zig` | +| 2.2 | xfail→green | **PENDING.** `type_info($T) -> TypeInfo` — reflect a type into a `TypeInfo` *value* (the inverse of reify). Needs the `TypeInfo` data model widened (struct/tuple variants beyond `` `enum ``) AND comptime construction of a `[]EnumVariant`-style value (slice of structs holding strings + `Type` tags) read from the type table. Still bails loudly in `call.zig:tryLowerReflectionCall`. Larger than 2.1 — its own step. | `library/modules/std/meta.sx`, `src/ir/interp.zig` / `call.zig` | ### Phase 3 — `make_enum` + `RecvResult`/`TryResult` (sx lib) | Step | Commit | What | Files | @@ -130,6 +131,8 @@ Examples: `06xx` (comptime, deterministic), `11xx` (diagnostics for loud failure shared `buildEnumInfo` path; `examples/0614` green; Contract 2 confirmed) - [x] Phase 1 — type-fn identity (`Box :: ($T)->Type { reify(...) }` memoizes by mangled name; `Box(i64)` at two sites is one type; `examples/0615` green) +- [~] Phase 2 — `field_type` DONE (`examples/0616` green); `type_info` (reflect → a + `TypeInfo` value) PENDING as step 2.2 - [ ] Phase 2 — `type_info` + `field_type` - [ ] Phase 3 — `make_enum` + `RecvResult`/`TryResult` - [ ] Phase 4 — reference self-reference diff --git a/examples/expected/0616-comptime-field-type.exit b/examples/expected/0616-comptime-field-type.exit index e69de29b..573541ac 100644 --- a/examples/expected/0616-comptime-field-type.exit +++ b/examples/expected/0616-comptime-field-type.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0616-comptime-field-type.stderr b/examples/expected/0616-comptime-field-type.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0616-comptime-field-type.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0616-comptime-field-type.stdout b/examples/expected/0616-comptime-field-type.stdout new file mode 100644 index 00000000..c7455279 --- /dev/null +++ b/examples/expected/0616-comptime-field-type.stdout @@ -0,0 +1,9 @@ +Point has 3 fields + 0: x : i64 + 1: y : f64 + 2: on : bool +field 0 is i64: true +field 1 is f64: true +Msg.num payload: i64 +Msg.tag payload: bool +Msg.quit payload: void diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 96c609bd..2f8fe2a3 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1894,6 +1894,7 @@ pub const Lowering = struct { pub const flatFnAuthorAmbiguous = lower_generic.flatFnAuthorAmbiguous; pub const flatFnAuthorVisible = lower_generic.flatFnAuthorVisible; pub const resolveTypeCallWithBindings = lower_generic.resolveTypeCallWithBindings; + pub const fieldTypeOf = lower_generic.fieldTypeOf; pub const resolveParameterizedWithBindings = lower_generic.resolveParameterizedWithBindings; pub const resolveValueParamArg = lower_generic.resolveValueParamArg; pub const canonicalIntConstraintName = lower_generic.canonicalIntConstraintName; diff --git a/src/ir/lower/generic.zig b/src/ir/lower/generic.zig index 94cdadcb..bc0fd3ab 100644 --- a/src/ir/lower/generic.zig +++ b/src/ir/lower/generic.zig @@ -1209,6 +1209,39 @@ pub fn flatFnAuthorVisible(self: *Lowering, name: []const u8, from: []const u8) } /// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)). +/// The `idx`-th member type of `t` for `field_type($T, i)`: a struct field, +/// a tagged-union variant payload (`.void` for a tagless variant), a tuple +/// element, a `union` field, or the element type of an array/vector (index +/// ignored — every element shares it). Out-of-range or a memberless type +/// diagnoses and poisons to `.unresolved` (never a silent default). +pub fn fieldTypeOf(self: *Lowering, t: TypeId, idx: usize, span: ?ast.Span) TypeId { + const oob = struct { + fn err(s: *Lowering, sp: ?ast.Span, i: usize, n: usize) TypeId { + if (s.diagnostics) |d| + d.addFmt(.err, sp, "field_type index {d} out of range ({d} field{s})", .{ i, n, if (n == 1) @as([]const u8, "") else "s" }); + return .unresolved; + } + }; + if (t.isBuiltin()) { + if (self.diagnostics) |d| + d.addFmt(.err, span, "field_type: '{s}' has no fields", .{self.formatTypeName(t)}); + return .unresolved; + } + return switch (self.module.types.get(t)) { + .@"struct" => |s| if (idx < s.fields.len) s.fields[idx].ty else oob.err(self, span, idx, s.fields.len), + .tagged_union => |u| if (idx < u.fields.len) u.fields[idx].ty else oob.err(self, span, idx, u.fields.len), + .@"union" => |u| if (idx < u.fields.len) u.fields[idx].ty else oob.err(self, span, idx, u.fields.len), + .tuple => |tup| if (idx < tup.fields.len) tup.fields[idx] else oob.err(self, span, idx, tup.fields.len), + .array => |a| if (idx < a.length) a.element else oob.err(self, span, idx, a.length), + .vector => |v| if (idx < v.length) v.element else oob.err(self, span, idx, v.length), + else => blk: { + if (self.diagnostics) |d| + d.addFmt(.err, span, "field_type: '{s}' has no indexable fields", .{self.formatTypeName(t)}); + break :blk .unresolved; + }, + }; +} + pub fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId { // A namespaced callee (`ns.Box(..)`) is an explicit qualified reach and is // exempt from the bare-head visibility gate; only a plain identifier head @@ -1230,9 +1263,25 @@ pub fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId 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; + // field_type($T, i) -> Type — the i-th field / variant-payload / + // element type of `T`. Folds at lower time (it's a `$T: Type` builtin), + // so it composes inside `type_eq` / `type_name` / any type-arg slot. + if (cl.args.len != 2) { + if (self.diagnostics) |d| + d.addFmt(.err, cl.callee.span, "field_type takes a type and an index: field_type($T, i)", .{}); + return .unresolved; + } + const t = self.resolveTypeArg(cl.args[0]); + if (t == .unresolved) return .unresolved; + const idx: usize = switch (program_index_mod.foldDimU32(cl.args[1], self, 0)) { + .ok => |n| n, + else => { + if (self.diagnostics) |d| + d.addFmt(.err, cl.args[1].span, "field_type index must be a non-negative compile-time integer", .{}); + return .unresolved; + }, + }; + return self.fieldTypeOf(t, idx, cl.callee.span); } // Built-in: Vector(N, T) if (std.mem.eql(u8, callee_name, "Vector") and cl.args.len == 2) {