issue(0141): comptime List growth in type construction (two-layer)

File the last METATYPE deferred enhancement: List(T).append at comptime
bails ('struct_get: base has no fields') in a type-construction ::.
Standalone repro + two-layer root cause (null comptime allocator at
scanDecls; *T slot_ptr struct_get) + investigation prompt. Non-blocking:
array-literal locals already build variant lists (examples/0620/0624).
Checkpoint + Known issues reference 0141.
This commit is contained in:
agra
2026-06-17 08:07:11 +03:00
parent 85c1b85f8b
commit 0f88525884
3 changed files with 189 additions and 6 deletions

View File

@@ -114,7 +114,9 @@ shifts every `.ir` snapshot. On-demand import keeps the prelude clean.
## Next step
The reflect/construct triad is COMPLETE — `` `enum `` (`0619`), `` `struct ``
(`0622`), `` `tuple `` (`0623`) all reflect AND construct + round-trip. Remaining
METATYPE work is ONE deferred enhancement, a clean diagnostic rather than a crash:
METATYPE work is ONE deferred enhancement, a clean diagnostic rather than a crash
— filed as **issue 0141** (repro `issues/0141-*.sx` + full two-layer writeup +
investigation prompt):
- **Comptime `List` growth** — `List(T).append` at comptime bails ("struct_get:
base has no fields"). Doesn't block anything: array-literal locals already build
variant lists (`examples/0620`/`0624`). Probe `.sx-tmp/probe_makeenum.sx` /
@@ -166,17 +168,22 @@ capabilities would let the variant list be built more freely; both error cleanly
an array from a `{ptr,len}` slice, folded open-ended `hi` to a fixed array's
static length at lower time (no runtime/.ir change), and added
`interp.zig:subsliceElements`. `examples/0621` locks it.
- **Comptime `List` growth.** `List(T).append` at comptime bails ("struct_get:
base has no fields"). Investigated — two layers (null comptime allocator at
scanDecls + `struct_get` through a `*T` slot_ptr chain); see the detailed writeup
under "Next step". Layer 1 has a known fix; layer 2 is deep. Probe
`.sx-tmp/probe_makeenum.sx`.
- **Comptime `List` growth** (issue 0141). `List(T).append` at comptime bails
("struct_get: base has no fields"). Investigated — two layers (null comptime
allocator at scanDecls + `struct_get` through a `*T` slot_ptr chain); see the
detailed writeup under "Next step" and `issues/0141-*.md`. Layer 1 has a known
fix; layer 2 is deep. Probe `.sx-tmp/probe_makeenum.sx`.
- ~~Generic type-fn body locals~~ — DONE. A generic `($T) -> Type` now
comptime-evaluates its FULL body (prelude statements + return), so a local
before the return resolves. `createComptimeFunctionWithPrelude` +
`evalComptimeTypeBody`; no-prelude bodies stay on the old path. `examples/0624`.
## Known issues
- issue 0141 (OPEN, deferred enhancement — not a blocker) — `List(T).append` at
comptime bails in a type-construction `::` (two layers: null comptime allocator
+ `*T` slot_ptr `struct_get`). Workaround: array-literal locals
(`examples/0620`/`0624`). Full writeup + investigation prompt in
`issues/0141-*.md`.
- issue 0140 — comptime type-construction bail panicked instead of diagnosing —
RESOLVED. `evalComptimeType` now clears `last_bail_detail` before the interp
call and, on the `catch`, emits a build-gating `.err` at the construction span

View File

@@ -0,0 +1,142 @@
# 0141 — `List(T).append` at comptime (in a type-construction `::`) bails
> **Status: OPEN — deferred enhancement, NOT a blocker.** Building a comptime
> variant/field list with an array-literal local already works
> (`examples/0620`/`0624`); only the `List`-grown form fails. Filed to record the
> two-layer root cause for a dedicated session. Surfaces a CLEAN diagnostic, not a
> crash.
## Symptom
One-line: a `List(T)` created and `.append`-ed at compile time inside a
type-construction `::` const bails — `comptime type construction failed:
comptime struct_get: base has no fields (not an aggregate/string/int)` — even
though the identical `List` code runs fine at RUNTIME and via `#run`.
- **Observed:** the `::` const evaluates to `.unresolved` after the interp bails
on the first `vs.append(...)`; the user sees the construction-failed diagnostic
plus a follow-on "cannot infer enum type for '.green'".
- **Expected:** the `List`-built variant list mints the enum exactly as the
array-literal form does (`examples/0620`): `Color` constructs, `.green(7)`
matches, prints `green=7`, exit 0.
## Reproduction
`issues/0141-comptime-list-growth-in-type-construction.sx` (standalone; only
`modules/std.sx` + `modules/std/meta.sx`). Run:
`./zig-out/bin/sx run issues/0141-comptime-list-growth-in-type-construction.sx`
→ bails today; the fix should print `green=7`, exit 0.
### Bisection (key signal: WHEN the comptime eval runs)
| Form | Path / eval time | Result |
|---|---|---|
| `List(i64)` append, read at RUNTIME (in `main`) | codegen | **works** |
| `v :: #run build()` where `build` grows a `List(i64)` | EMIT-time interp | **works** (`.sx-tmp/probe_list4.sx`) |
| `T :: makeListType()` where the body grows a `List` | `scanDecls`-time interp (`evalComptimeType`) | **BAILS** |
| same metatype `::` but with an array-LITERAL local instead of `List` | `scanDecls`-time interp | **works** (`examples/0620`/`0624`) |
The discriminator is eval time: `#run` evaluates at EMIT time (after the whole
program is lowered), whereas a metatype `::` const evaluates during `scanDecls`
(early, mid-lowering). Two things are not yet ready at `scanDecls` time.
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
### Layer 1 — null comptime allocator (has a known fix)
`src/ir/interp.zig:defaultContextValue` builds the comptime `context.allocator`
by looking up the CAllocator→Allocator protocol thunks BY NAME in the module's
functions:
```zig
const alloc_thunk_name = tbl.internString("__thunk_CAllocator_Allocator_alloc_bytes");
// ... scan self.module.functions for that name ...
```
At `scanDecls` time those thunks aren't lowered yet, so `alloc_fn` / `dealloc_fn`
stay `.null_val` and ANY comptime allocation (List growth, direct
`context.allocator.alloc`) fails. Confirmed with a debug print: metatype path →
`alloc_fn=null_val`; `#run` path → `alloc_fn=func_ref`.
**Fix (verified for this layer):** force the thunks to exist before the interp
runs, in `src/ir/lower/comptime.zig:runComptimeTypeFunc`, guarded exactly like
`emitDefaultContextGlobal` (skip when Allocator/CAllocator aren't registered):
```zig
const tbl = &self.module.types;
if (tbl.findByName(tbl.internString("Allocator")) != null and
tbl.findByName(tbl.internString("CAllocator")) != null)
{
_ = self.getOrCreateThunks("Allocator", "CAllocator");
}
```
`createProtocolThunk` saves/restores builder state (`saved_func`/`saved_block`/
`saved_counter`), so calling it mid-lowering is safe (same as
`emitDefaultContextGlobal`). After this, `alloc_fn=func_ref` — but layer 2 still
bails.
### Layer 2 — `struct_get` through a `*T` slot_ptr chain (the deep part)
With the allocator fixed, `vs.append(…)` still bails. `List.append` takes
`self: *List`; the `vs.append(…)` UFCS desugars to `append(@vs, …)`, so inside
`append` the receiver `self` is a `*List`. At comptime it lands as a frame slot
whose CONTENTS are a `slot_ptr` to the actual `List` value, so `self.field` does
`struct_get` on `base=slot_ptr field_index=1` and falls through to the bail.
`src/ir/interp.zig`'s `.struct_get` arm auto-derefs a `slot_ptr` base with a
SINGLE `loadSlot` (+ `resolveFieldLoad` for field-pointer aggregates). A
chain-resolve loop (`while (loaded == .slot_ptr) loaded = loadSlot(...)`) did NOT
fix it: the final loaded value is a field-pointer aggregate that
`resolveFieldLoad` turns back into a `slot_ptr`. List's comptime in-memory
representation mixes field-pointers and slot_ptrs that the `struct_get` /
`resolveFieldLoad` path doesn't fully resolve for a `*T` receiver.
This is the substantive work: comptime pointer/struct/slot resolution for `*T`
struct receivers — its own focused interp session.
## Investigation prompt
> A `List(T)` grown at comptime inside a type-construction `::` bails
> ("struct_get: base has no fields"), though the same code works at runtime and
> via `#run`. Repro: `issues/0141-comptime-list-growth-in-type-construction.sx`
> (expect a bail today; the fix should print `green=7`, exit 0).
>
> It's two layers (see this file's Root cause). START with layer 1 (the known
> fix: force `getOrCreateThunks("Allocator","CAllocator")` in
> `comptime.zig:runComptimeTypeFunc` before the interp runs, guarded like
> `emitDefaultContextGlobal`). Verify with a debug print that `defaultContextValue`
> then sees `alloc_fn=func_ref`.
>
> THEN layer 2 (the real work): make the interp's `.struct_get` (and
> `index_get`/store paths) resolve a `*T` struct receiver whose slot holds a
> `slot_ptr` to the value. Reproduce in isolation with a plain non-generic
> `Box :: struct { x: i64; }` and a `bump :: (b: *Box) { b.x += 1; }` called at
> comptime, so you debug the pointer-receiver `struct_get` without List's
> generics. Trace what `frame.getRef(fa.base)` / `loadSlot` / `resolveFieldLoad`
> return for `self.field` and make the deref fully resolve to the backing
> aggregate (mirror `resolveSlotChain`, but for the field-pointer + slot_ptr mix
> that a `*T` receiver produces). Don't add a silent fallback — bail loudly if a
> shape still isn't handled (per CLAUDE.md REJECTED PATTERNS).
>
> Verification: the repro prints `green=7`, exit 0; then `zig build && zig build
> test` green. Move the repro to `examples/06xx-comptime-metatype-make-enum-list.sx`
> (resolving-an-issue workflow) and add a focused `*T`-comptime-receiver example
> too. Update `current/CHECKPOINT-METATYPE.md` (the last deferred enhancement).
## Notes
- Bail site (symptom): `src/ir/interp.zig` `.struct_get` arm, `else =>`
"struct_get: base has no fields".
- Layer-1 site: `src/ir/interp.zig:defaultContextValue` (thunk-by-name lookup);
fix in `src/ir/lower/comptime.zig:runComptimeTypeFunc`.
- Layer-2 site: `src/ir/interp.zig` `.struct_get` auto-deref (single `loadSlot` +
`resolveFieldLoad`); `*T` receiver slot_ptr chain unresolved.
- Both layers reproduce with a plain `List(i64)` — not metatype-specific. The
metatype `::` path just happens to be the first `scanDecls`-time comptime eval
that needs heap allocation.
- Workaround (no fix needed for callers): build the variant/field list with an
array-literal local — `examples/0620` / `0624` already do this.

View File

@@ -0,0 +1,34 @@
// Repro for issue 0141 — a `List(T)` grown at comptime inside a type-construction
// `::` const bails. `make_enum` assembles its variant list in a `List`, appends,
// then mints from `vs.items`. The append fails at comptime ("struct_get: base has
// no fields") even though the identical code works at runtime AND via `#run`.
//
// Expected: `Color` constructs from the List-built variant list and `.green(7)`
// matches (prints "green=7"), exit 0 — the same as the array-literal form
// (examples/0620), which already works.
#import "modules/std.sx";
#import "modules/std/meta.sx";
make_enum :: (name: string, variants: []EnumVariant) -> Type {
return define(declare(name), .enum(.{ variants = variants }));
}
build_color :: () -> Type {
vs : List(EnumVariant) = .{};
vs.append(EnumVariant.{ name = "red", payload = void });
vs.append(EnumVariant.{ name = "green", payload = i64 });
vs.append(EnumVariant.{ name = "blue", payload = void });
return make_enum("Color", vs.items);
}
Color :: build_color();
main :: () -> i32 {
c : Color = .green(7);
if c == {
case .red: { print("red\n"); }
case .green: (v) { print("green={}\n", v); }
case .blue: { print("blue\n"); }
}
return 0;
}