From 0f88525884232e61b234a361c4c7b7707c951ef6 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 17 Jun 2026 08:07:11 +0300 Subject: [PATCH] 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. --- current/CHECKPOINT-METATYPE.md | 19 ++- ...mptime-list-growth-in-type-construction.md | 142 ++++++++++++++++++ ...mptime-list-growth-in-type-construction.sx | 34 +++++ 3 files changed, 189 insertions(+), 6 deletions(-) create mode 100644 issues/0141-comptime-list-growth-in-type-construction.md create mode 100644 issues/0141-comptime-list-growth-in-type-construction.sx diff --git a/current/CHECKPOINT-METATYPE.md b/current/CHECKPOINT-METATYPE.md index 9387e546..dead75cc 100644 --- a/current/CHECKPOINT-METATYPE.md +++ b/current/CHECKPOINT-METATYPE.md @@ -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 diff --git a/issues/0141-comptime-list-growth-in-type-construction.md b/issues/0141-comptime-list-growth-in-type-construction.md new file mode 100644 index 00000000..36e7da4a --- /dev/null +++ b/issues/0141-comptime-list-growth-in-type-construction.md @@ -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. diff --git a/issues/0141-comptime-list-growth-in-type-construction.sx b/issues/0141-comptime-list-growth-in-type-construction.sx new file mode 100644 index 00000000..9cf4a01f --- /dev/null +++ b/issues/0141-comptime-list-growth-in-type-construction.sx @@ -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; +}