issue(0141): refine root cause — wrong IR (struct_get vs struct_gep) at scanDecls
Instrumentation shows List.append lowers list.len/list.cap to struct_gep (correct) at #run/emit time but struct_get (wrong, value access on a *T receiver) at scanDecls/metatype time — same source, different IR. The function IS lowered both ways, just to wrong IR at scanDecls due to incomplete generic-instantiation context. So an interp-side lazy-lower hook can't fix it (IR is wrong before the interp runs); the fix is either robust field-access lowering or deferring the comptime type-construction eval to a complete-world pass (like #run). Supersedes the two-layer framing.
This commit is contained in:
@@ -43,7 +43,49 @@ program is lowered), whereas a metatype `::` const evaluates during `scanDecls`
|
||||
It is NOT metatype/EnumVariant-specific — a plain `List(i64)` grown in a
|
||||
`-> Type` body bails identically (`.sx-tmp/probe_li64.sx`).
|
||||
|
||||
## Root cause — TWO independent layers
|
||||
## Root cause — REFINED (2026-06-17): wrong IR at scanDecls, not just "not ready"
|
||||
|
||||
Deeper investigation overturns the "two independent layers" framing below. The
|
||||
real root cause is a single one: **a generic stdlib method's body is lowered to
|
||||
WRONG IR when that lowering is triggered at `scanDecls` time** (during the
|
||||
metatype `::` eval), because the generic struct instantiation context is
|
||||
incomplete then.
|
||||
|
||||
Proof (instrumented `interp.zig`'s `.struct_get` / `.struct_gep` arms, ran the
|
||||
same `List(i64)` append both ways):
|
||||
|
||||
| Eval time | `list.len` / `list.cap` (where `list: *List(T)`) lowers to | Result |
|
||||
|---|---|---|
|
||||
| `#run` (EMIT time, world complete) | `struct_gep` (pointer field access) — 38 hits | works |
|
||||
| metatype `::` (scanDecls time) | `struct_get` (VALUE field access) — fails on the 1st | bails |
|
||||
|
||||
So `List.append` is fully lowered in BOTH cases (no unlowered/extern call fires),
|
||||
but at `scanDecls` time `list.len` lowers as `struct_get` on the pointer VALUE
|
||||
instead of `struct_gep` THROUGH the pointer — `struct_get` on a `*List` receiver
|
||||
sees a `slot_ptr` whose load is another `slot_ptr` and bails. The two "layers"
|
||||
below are both symptoms of this same incomplete-context lowering (the null
|
||||
allocator is the same story for the CAllocator thunks).
|
||||
|
||||
**Consequence for the fix:** an interp-side "lazy-lower the missing function"
|
||||
hook does NOT help — the function is already lowered, just to wrong IR before the
|
||||
interp ever runs. The fix must ensure the bodies the metatype eval needs are
|
||||
lowered with a COMPLETE type context. Two viable directions:
|
||||
1. **Make field-access lowering robust** — `list.len` on a `*List(T)` receiver
|
||||
must emit `struct_gep` whenever the receiver is a pointer-to-struct, even if
|
||||
the pointee's generic instantiation isn't finalized yet (resolve the field
|
||||
index against the in-progress instantiation). Localized to the
|
||||
field-access / generic-struct-instantiation path; risk is mis-lowering other
|
||||
in-progress generics.
|
||||
2. **Defer the comptime type-construction eval** to a dedicated pass AFTER the
|
||||
stdlib/generic machinery the constructors call is lowered, but before general
|
||||
body lowering of code that USES the constructed types (their forward slots
|
||||
are already pre-registered, so `*Name` / annotations resolve in the interim).
|
||||
This is the true "lazy/deferred" shape — the eval runs in a complete world,
|
||||
exactly like `#run`. Bigger (pipeline ordering) but matches why `#run` works.
|
||||
Decision pending (see the conversation) — direction 2 is the principled match to
|
||||
the `#run`-works/metatype-fails asymmetry.
|
||||
|
||||
## 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