issue(0141): Direction 2 (defer eval) ruled out by experiment; Direction 1 is the path

Wired a minimal deferral (eval at a new Pass 1c' after the CAllocator
thunks exist) — the List repro STILL bailed with struct_get, and it
destabilized examples/0620. So deferring past the thunks isn't the cause
of the wrong IR; the field-access lowering only emits struct_gep at
body-lowering/emit time. No single pass slot satisfies both 'body lowers
correctly' and 'layout ready before use'. Pivot to Direction 1 (robust
*Struct field-access lowering). Experiment reverted; tree clean.
This commit is contained in:
agra
2026-06-17 08:34:50 +03:00
parent a448f50f7f
commit e2b2e22fa7

View File

@@ -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 (06140624, 11781182)
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)