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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user