User-directed redirection. The compiler should expose ONLY declare() and
define(handle, info) as comptime type-table primitives; reify / make_enum /
RecvResult / TryResult all become plain sx in meta.sx (reify ==
{ h := declare(); define(h, info); return h; }). The AST-walking reifyType
and every syntactic reify recognition (decl.zig E :: reify hook, generic.zig
findReturnReifyCall routing) are to be DELETED, replaced by generic comptime
evaluation of a Type-returning expression.
PLAN-REIFY gains a RE-ARCHITECTURE section: the irreducible compiler floor
(declare = empty nominal slot; define = decode a TypeInfo VALUE + fill via
updatePreservingKey; comptime-eval a Type-returning ::-RHS/type-fn body),
the resolved naming/identity story (declare mints anonymous, the binding site
names it; identity via the existing instantiation cache), and an F1-F5 phase
table that re-greens 0614/0615/0617 on the floor.
No code change in this commit — the in-session Phase 3.2 attempt (make_enum +
eval-decode reader) was reverted (reset to 9306ad5) so the floor is built
first. Checkpoint records the revert + sets next step = F1.
206 lines
16 KiB
Markdown
206 lines
16 KiB
Markdown
# PLAN-REIFY — comptime type reflection + construction (`type_info` / `reify`)
|
|
|
|
## Goal
|
|
|
|
Add the two comptime metaprogramming builtins — **`type_info($T) -> TypeInfo`**
|
|
(reflect a type → data) and **`reify(info: TypeInfo) -> Type`** (construct a *new
|
|
nominal type* from data) — plus the sx-lib helpers (`make_enum`, `field_type`,
|
|
`RecvResult`/`TryResult`) built over them. This is **step 3 of the async-first
|
|
sequence** ([../design/execution-evolution-roadmap.md](../design/execution-evolution-roadmap.md)
|
|
§7); it gates channel result types (`RecvResult($T)`) and `race`'s synthesized
|
|
tagged-union, and **replaces** a would-be `enum($T)` generic-enum language feature.
|
|
|
|
> Rationale + the five validated contracts: design doc §7 step 3 + §8.1. The approach
|
|
> was grounded by three codebase reviewers — it is a **small extension reusing existing
|
|
> machinery**, not net-new architecture.
|
|
|
|
## Locked design (the five reify contracts — all codebase-validated)
|
|
|
|
1. **Nominal identity via type-fn memoization.** `RecvResult(i64)` is one `TypeId`
|
|
because type-fns dedup by mangled `(fn,args)` name (`generic.zig:1620-1629`) +
|
|
reify `findByName`. NOT structural dedup — enums are nominal (`types.zig:1110`).
|
|
2. **Functional through codegen.** A reify'd enum has **no backing AST decl**, and
|
|
every enum stage is type-table-driven (layout, construct, match+exhaustiveness,
|
|
`toLLVMType`, `type_name`/format) — so it flows through **unmodified**.
|
|
3. **Validate loudly** at the `intern`/`internNominal` choke point (`types.zig:411-439`).
|
|
4. **Comptime-only, JIT-free** — a type-table op in the interpreter; no S1 dependency.
|
|
5. **Reference-based self-reference** (`*Self`/`[]Self`) via an explicit
|
|
**`declare()` → `define()`** pair (reserve-placeholder→complete reuse of
|
|
`nominal.zig:86/108/120`, `types.zig:442`); **by-value recursion rejected**. (See
|
|
Phase 4 — the `declare`/`define` API supersedes the earlier `reify_rec` closure.)
|
|
|
|
Surface follows the **`#builtin`** pattern of the existing reflection builtins
|
|
(`type_of`/`field_count`/`field_name` in `library/modules/std/core.sx`,
|
|
`specs.md:2594-2600`) — NOT the BuildOptions compiler-hook registry.
|
|
|
|
## Key code anchors (verified by review)
|
|
|
|
- Type minting: `TypeTable.intern` / `internNominal` — `src/ir/types.zig:411-439`.
|
|
- Type-fn instantiation + mangled-name cache — `src/ir/lower/generic.zig:1575-1689`
|
|
(cache check `:1620-1629`; register inline-struct result `:1663-1689`).
|
|
- Forward-declare reserve (recursive types) — `src/ir/lower/nominal.zig:86/108/120`;
|
|
complete a forward-declared type — `src/ir/types.zig:442`.
|
|
- Enum codegen (all type-table-driven, the reify target shape): size `types.zig:633-636`;
|
|
`resolveVariantIndex` `lower/expr.zig:1159-1177`; match `lower/control_flow.zig:748-945`;
|
|
`toLLVMType` `backend/llvm/types.zig:111-154`; `type_name` `types.zig:846-882`.
|
|
- Existing reflection builtins to mirror — `core.sx` (`#builtin`) + their interp/lower
|
|
handlers (`src/ir/interp.zig` `type_name`/reflection at ~`:1911`).
|
|
- Match form — `specs.md:408-424`.
|
|
|
|
## Cadence (IMPASSIBLE)
|
|
|
|
No commit may both add a test AND make it pass (xfail-then-green, or a behavior-lock).
|
|
`zig build && zig build test` after every step. Never regenerate snapshots while red.
|
|
Examples: `06xx` (comptime, deterministic), `11xx` (diagnostics for loud failures).
|
|
|
|
---
|
|
|
|
## RE-ARCHITECTURE (user-directed, 2026-06-16) — `declare`/`define` are the ONLY compiler primitives
|
|
|
|
**Decision:** the compiler must expose **only** the two comptime type-table
|
|
primitives `declare()` and `define(handle, info)`. **`reify`, `make_enum`,
|
|
`RecvResult`, `TryResult` all become plain sx** in `meta.sx` — `reify` is literally
|
|
`{ h := declare(); define(h, info); return h; }`. The AST-walking `reifyType` and
|
|
every *syntactic* recognition of `reify` (`decl.zig`'s `E :: reify(...)` hook,
|
|
`generic.zig`'s `findReturnReifyCall` type-fn routing) are **deleted** — replaced by
|
|
generic **comptime evaluation of a Type-returning expression**.
|
|
|
|
This supersedes the old builtin-`reify` approach of Phases 0/1/3 below (which are
|
|
GREEN today but compiler-coupled). Phase 3.2 (`make_enum` + the eval-decode reader)
|
|
was implemented then **reverted** (commit reset to 9306ad5) to rebuild on this floor.
|
|
|
|
### The irreducible compiler floor (cannot be sx — mints `TypeId`s / evaluates types)
|
|
1. **`declare() -> Type`** — intern a fresh EMPTY forward nominal slot (unique
|
|
`nominal_id`, anonymous name), return it as a first-class `Type` value. Reuses the
|
|
`reserveShadowEnumSlot` shape (`nominal.zig:98`).
|
|
2. **`define(handle: Type, info: TypeInfo)`** — decode the `TypeInfo` **value** (the
|
|
reverted `reifyEnumFromVariantsValue` decode logic moves here: interp `Value` →
|
|
names + payload `Type`-tags → `tagged_union` byte-identical to `buildEnumInfo`),
|
|
fill the slot via `updatePreservingKey` (`types.zig:442`). Loud on bad input.
|
|
3. **Comptime-evaluate a Type-returning `::` RHS / type-fn body** through the
|
|
interpreter to a `type_tag`, then bind / rename / identity-cache. This is the new
|
|
general capability that REPLACES the `reify`-name pattern-matching.
|
|
|
|
### Naming + identity (resolved)
|
|
- `declare()` mints ANONYMOUS; the **binding site** names it: a direct `X :: <expr>`
|
|
renames the handle to `X`; a type-fn instantiation renames to the mangled name.
|
|
- **Identity (Contract 1)** still rides the existing `instantiateTypeFunction`
|
|
mangled-name cache — `RecvResult(i64)` memoizes, body (hence `declare`) runs once.
|
|
|
|
### Floor phases (REPLACES old Phases 0/1/3; re-greens 0614/0615/0617)
|
|
| Step | Commit | What | Files |
|
|
|---|---|---|---|
|
|
| F1.0 | xfail | `examples/06xx` mints a **recursive** enum directly: `List :: declare();` + `define(List, .enum(.{ variants = .[ … payload = *List … ] }))`; construct + match a cons/nil list. Red (declare/define undefined). | `examples/06xx-*` |
|
|
| F1.1 | green | `declare`/`define` as comptime builtins + the `X :: declare()` decl hook + scan-time `define(...)` side-effect + the value-decode (from the reverted 3.2). Self-ref works because the handle predates the body. | `src/ir/lower/{decl,nominal}.zig`, `src/ir/interp.zig`, `meta.sx` |
|
|
| F2.0 | xfail | Reimplement `reify` in `meta.sx` as `declare`+`define`+return; switch `E :: reify(...)` to the **general comptime-Type-expr** decl path; delete the `E :: reify` AST special-case + `reifyType` literal walk. Red until the general path lands. | `meta.sx`, `src/ir/lower/decl.zig` |
|
|
| F2.1 | green | General comptime evaluation of a Type-returning `::` RHS (run interp → `type_tag` → rename/bind). `examples/0614` green again with ZERO `reify` knowledge in the compiler. | `src/ir/lower/decl.zig`, `src/ir/interp.zig` |
|
|
| F3.0 | xfail | Delete `findReturnReifyCall` routing; make `instantiateTypeFunction` run a Type-returning body through the interp generally. Red for 0615/0617 until it lands. | `src/ir/lower/generic.zig` |
|
|
| F3.1 | green | General type-fn body comptime-eval + rename to mangled name + identity cache. `examples/0615` (cross-site identity) + `0617` (RecvResult/TryResult) green on the floor. | `src/ir/lower/generic.zig` |
|
|
| F4 | xfail→green | `make_enum(variants)` in `meta.sx` — now TRIVIAL (reify is sx, type-fns eval generally). New `examples/06xx` (inline + computed variants). | `meta.sx`, `examples/06xx-*` |
|
|
| F5 | xfail→green | Validation + loud diagnostics: dup variant names, by-VALUE self-ref (`payload = List` not `*List` → "infinite size"), a `declare()` never `define()`d (hard error at end of comptime). | `src/ir/interp.zig` / `src/ir/types.zig`, `examples/11xx-*` |
|
|
|
|
> `type_info($T) -> TypeInfo` (old Phase 2.2 — reflect a type INTO a value) is
|
|
> orthogonal to this floor and still pending; it CONSTRUCTS a `TypeInfo` value
|
|
> (inverse of `define`'s decode). Round-trips through the floor once it lands.
|
|
|
|
---
|
|
|
|
## (SUPERSEDED by the re-architecture above) Original builtin-`reify` phases
|
|
|
|
### Phase 0 — `reify` of a flat enum (the core)
|
|
| Step | Commit | What | Files |
|
|
|---|---|---|---|
|
|
| 0.0 | lock | `TypeInfo`/`EnumInfo`/`EnumVariant` lib types in `core.sx` (data only); `reify`/`type_info`/`field_type` as bodyless `#builtin` decls (parsed, unimplemented → loud bail). Unit: decls parse. | `library/modules/std/core.sx`, `src/ir/interp.zig` |
|
|
| 0.1 | xfail | `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). | `examples/06xx-*` |
|
|
| 0.2 | green | implement `reify(.enum_)` → build `EnumInfo`/`TaggedUnionInfo` `TypeInfo`, `internNominal(info, fresh_nominal_id)`, return `TypeId`. Example green; construct + match work unmodified (Contract 2). | `src/ir/interp.zig`, (`src/ir/types.zig` if a helper is wanted) |
|
|
|
|
### Phase 1 — type-fn → reify identity
|
|
| Step | Commit | What | Files |
|
|
|---|---|---|---|
|
|
| 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 — `field_type` (done) + `type_info` (reflect → value, pending)
|
|
| Step | Commit | What | Files |
|
|
|---|---|---|---|
|
|
| 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 — `RecvResult`/`TryResult` (done) + `make_enum` (pending)
|
|
| Step | Commit | What | Files |
|
|
|---|---|---|---|
|
|
| 3.0 | xfail | `examples/0617` uses `RecvResult(i64)`/`TryResult(i64)` (construct + match). Red (undefined). | `examples/0617-*` |
|
|
| 3.1 | green | **DONE.** `RecvResult($T)`/`TryResult($T)` as type-fns over `reify` in `meta.sx` — needs NO new machinery (reify-of-literal in a type-fn body = Phase 1). `0617` green. | `library/modules/std/meta.sx` |
|
|
| 3.2 | pending | **`make_enum(variants: []EnumVariant) -> Type`** — sx lib over `reify` with a RUNTIME (non-literal) `variants` arg. BLOCKED on the generalized reify reader: `reifyType` currently reads a LITERAL `TypeInfo` off the AST, so `reify(.enum(.{ variants = variants }))` (variable, not a literal) can't be read yet. Pairs with Phase 2.2 (interp-evaluate the `TypeInfo` value instead of AST-walking). | `library/modules/std/meta.sx`, `src/ir/lower/nominal.zig` |
|
|
|
|
### Phase 4 — reference-based self-reference (explicit declare → define)
|
|
> **API decision (user-directed):** an explicit **`declare()` / `define(h, info)`**
|
|
> pair (the canonical declaration-vs-definition split), NOT a `reify_rec((self) => …)`
|
|
> closure. `declare()` returns a forward-declared nominal `Type` HANDLE — a real
|
|
> `Type` value usable freely in any later `TypeInfo` (`*List`, `[]List`, and across
|
|
> types for MUTUAL recursion, which a one-`self` closure can't express).
|
|
> `define(handle, info)` fills the body. `reify(info)` stays as the one-shot sugar
|
|
> (= declare + define + return) for the non-recursive case. No special self-ref node
|
|
> in `TypeInfo` — a declared type is just a `Type`.
|
|
> ```sx
|
|
> List :: declare(); // name from the `::` LHS
|
|
> define(List, .enum(.{ variants = .[
|
|
> EnumVariant.{ name = "cons", payload = *List }, // reference it freely
|
|
> EnumVariant.{ name = "nil", payload = void } ] }));
|
|
> ```
|
|
> Loud invariants: (a) a `declare()` never `define()`d = hard error at end of
|
|
> comptime (no silent empty type); (b) by-VALUE self-inclusion (`payload = List`, not
|
|
> `*List`) rejected — infinite size, same check recursive source types get.
|
|
|
|
| Step | Commit | What | Files |
|
|
|---|---|---|---|
|
|
| 4.0 | xfail | `declare()` returns a forward nominal `Type`; `define()` fills it. Recursive enum via `*Self` (tree/list). Red. | `examples/06xx-*` |
|
|
| 4.1 | green | `declare` = intern empty nominal slot (`reserveShadowEnumSlot`-style); `define` = `buildEnumInfo` + `updatePreservingKey`; struct-stub→tagged_union re-key already handled by `internNamedTypeDecl` (`adoptsForwardStructStub`). | `src/ir/lower/nominal.zig`, `src/ir/lower/decl.zig`, `src/ir/types.zig` |
|
|
|
|
### Phase 5 — validation + loud diagnostics
|
|
| Step | Commit | What | Files |
|
|
|---|---|---|---|
|
|
| 5.0 | xfail | `examples/11xx-diagnostics-reify-*` — dup variant names, non-integer backing, **by-value self-reference** ("infinite size; use `*Self`"). Pin the messages. | `examples/11xx-*` |
|
|
| 5.1 | green | validate `TypeInfo` at the `intern`/`internNominal` choke point; emit diagnostics, never a broken type (Contract 3). | `src/ir/interp.zig` / `src/ir/types.zig` |
|
|
|
|
> `RaceResult` (tuple→tagged-union synthesis) is **not** in this stream — it lands with
|
|
> `race` (async cluster), but it consumes exactly the `type_info`+`field_type`+`reify`
|
|
> primitives built here.
|
|
|
|
## Risks / watch
|
|
|
|
- **Mangled-name plumbing (Phase 1)** is the one real unknown — confirm the type-fn
|
|
path registers a *reify-returned* result (not just inline `struct {…}` literals).
|
|
Fallback: have `reify` itself name the type by the instantiation key + `findByName`.
|
|
- **Self-ref completion (Phase 4)** must reuse the existing recursive-type
|
|
reserve→complete path; do not invent a new mutate-after-intern mechanism.
|
|
- Keep `reify` **comptime-only**: a `reify` reached at runtime is a hard error.
|
|
|
|
## Status
|
|
|
|
- [x] Phase 0 — `reify` flat enum (`reify(.enum(...))` mints a flat enum via the
|
|
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 3 — `RecvResult($T)`/`TryResult($T)` DONE (`examples/0617` green, type-fns
|
|
over `reify` in `meta.sx`); `make_enum(variants)` PENDING (step 3.2 — needs the
|
|
generalized reify reader, pairs with 2.2)
|
|
- [ ] Phase 2 — `type_info` + `field_type`
|
|
- [ ] Phase 3 — `make_enum` + `RecvResult`/`TryResult`
|
|
- [ ] Phase 4 — reference self-reference
|
|
- [ ] Phase 5 — validation + diagnostics
|
|
|
|
## Kickoff prompt (paste into a fresh session)
|
|
|
|
> Work the REIFY stream per `current/PLAN-REIFY.md` (+ checkpoint
|
|
> `current/CHECKPOINT-REIFY.md`). Read the plan header (goal, five locked contracts,
|
|
> key anchors) first; rationale is in `design/execution-evolution-roadmap.md` §7 step 3
|
|
> + §8.1. **This session = Phase 0 only** (`TypeInfo` lib types + `reify` of a flat
|
|
> enum: construct + match). Cadence (IMPASSIBLE): no commit both adds a test and makes
|
|
> it pass — lock, then xfail→green. `zig build && zig build test` after every step. If
|
|
> you hit an unrelated compiler bug, follow the CLAUDE.md IMPASSIBLE RULE (file an
|
|
> issue, stop). Stop at the end of Phase 0; update the checkpoint.
|