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:
@@ -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
|
||||
|
||||
142
issues/0141-comptime-list-growth-in-type-construction.md
Normal file
142
issues/0141-comptime-list-growth-in-type-construction.md
Normal 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.
|
||||
34
issues/0141-comptime-list-growth-in-type-construction.sx
Normal file
34
issues/0141-comptime-list-growth-in-type-construction.sx
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user