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
|
Decision pending (see the conversation) — direction 2 is the principled match to
|
||||||
the `#run`-works/metatype-fails asymmetry.
|
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)
|
## Root cause — TWO independent layers (SUPERSEDED by the refinement above)
|
||||||
|
|
||||||
### Layer 1 — null comptime allocator (has a known fix)
|
### Layer 1 — null comptime allocator (has a known fix)
|
||||||
|
|||||||
Reference in New Issue
Block a user