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:
agra
2026-06-17 08:21:55 +03:00
parent 86feced560
commit a448f50f7f

View File

@@ -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)