diff --git a/issues/0141-comptime-list-growth-in-type-construction.md b/issues/0141-comptime-list-growth-in-type-construction.md index 779007f2..0972323c 100644 --- a/issues/0141-comptime-list-growth-in-type-construction.md +++ b/issues/0141-comptime-list-growth-in-type-construction.md @@ -85,6 +85,73 @@ lowered with a COMPLETE type context. Two viable directions: Decision pending (see the conversation) — direction 2 is the principled match to the `#run`-works/metatype-fails asymmetry. +## Implementation plan — Direction 2 (defer eval to a complete-world pass) + +Chosen direction: move the comptime type-construction eval out of `scanDecls` +(Pass 1) into a new pass that runs once the world is complete enough that the +constructor bodies lower correctly. + +**Pass map (`decl.zig:lowerRoot`).** Today the eval is at Pass 1 (`scanDecls`, +`decl.zig:777`). The CAllocator thunks are created at Pass 1c +(`emitDefaultContextGlobal`) — AFTER scanDecls — which is why the comptime +allocator is null. `checkInfiniteSize` (Pass 1g) and body lowering (Pass 2) +consume the constructed layouts, so the eval must finish before Pass 1g. Target +slot for the new pass: **between Pass 1c and Pass 1g** (call it Pass 1c′ +`lowerDeferredComptimeTypes`). + +**STEP 0 — DE-RISK FIRST (DONE 2026-06-17 — Direction 2 RULED OUT as scoped).** +Wired the minimal deferral (collect the consts in scanDecls + `preregisterForwardTypes` +eagerly; eval them in a new Pass 1c′ right after `emitDefaultContextGlobal`). +Result: the List repro STILL bailed with `struct_get` — deferring past the thunks +did NOT change `list.len` to `struct_gep`. So the wrong-IR cause is **not** +pass-position relative to `emitDefaultContextGlobal`; it's the field-access / +generic-struct-instantiation lowering itself, which only produces `struct_gep` at +**body-lowering (Pass 2) / emit** time, not at any pre-body-lowering pass. Worse, +the deferral DESTABILIZED a working case (`examples/0620` → +"define(): handle is not a declare()'d enum slot", a forward-slot/alias ordering +regression). Experiment reverted. + +**Revised conclusion.** There is no single pass slot where (a) the constructor body +lowers correctly AND (b) the constructed layout is ready before code that uses it: +the body only lowers right at Pass 2/emit, but the layout is consumed *during* +Pass 2. So a simple "defer the eval" (Direction 2) can't work; it would need a +genuine two-phase scheme. The tractable path is **Direction 1** — fix the +field-access lowering so `recv.field` on a `*Struct` receiver emits `struct_gep` +regardless of when it's lowered. Next step: find why `list.len` (`list: *List(T)`) +lowers as `struct_get` (value) instead of `struct_gep` (pointer) at scanDecls — +i.e. what about the generic `List(T)` instantiation is incomplete then that flips +the field-access decision (start at `lower/expr.zig:lowerFieldAccess` / +`lowerFieldAccessOnType` and the `*T`-receiver path). + +**Plumbing (only after STEP 0 is green):** +1. `scanDecls` (`decl.zig:777` site): instead of `evalComptimeType` now, + (a) pre-register a forward nominal slot named `cd.name` + bind the alias + `cd.name → slot` (so `c : Color`, Pass 1f's UnknownTypeChecker, etc. resolve in + the interim), and (b) push `{ name, value, source_file }` to a new + `deferred_comptime_types` list on `Lowering`. Don't eval. +2. New `lowerDeferredComptimeTypes` pass, called from `lowerRoot` after + `emitDefaultContextGlobal` and before `checkInfiniteSize`: for each entry, set + `current_source_file`, `const tid = evalComptimeType(value)`, `putTypeAlias`. + The interp's `declare("Color")` finds the pre-registered slot (findByName) and + `define` fills it in place (`updatePreservingKey`), so `tid` == the forward slot + — alias stays valid. +3. Self-ref (`List :: make_list()` + `*List`): the forward slot for `List` is + registered in step 1, so `*List` resolves while `make_list`'s body lowers during + the deferred eval. Verify `examples/0618` still passes. + +**Risks / watch:** +- **Name mismatch.** The minted type's name comes from `declare("X")` inside the + ctor, not the LHS `cd.name` (`decl.zig` comment "no rename"). For the + non-generic `::` path the two normally coincide, but if `declare`'s string ≠ + `cd.name` the pre-registered forward slot is orphaned (empty tagged_union → + could trip the declare-never-defined / infinite-size checks). Handle: either + require the names to match here, or reconcile the orphan after eval. +- **Generic type-fns** (`RecvResult($T)`) go through `instantiateTypeFunction`, a + DIFFERENT site (lazy, at use) — leave those as-is; only the non-generic `::` + site (`decl.zig:774`) defers. +- Re-run the FULL suite: every existing metatype example (0614–0624, 1178–1182) + must stay green — the deferral changes *when* they evaluate. + ## Root cause — TWO independent layers (SUPERSEDED by the refinement above) ### Layer 1 — null comptime allocator (has a known fix)