mem: List(T) mutations gain optional alloc: Allocator = context.allocator
The chess panel-text regression (text vanished after the first move on macOS) had a single root cause: GlyphCache's entries List, hash table, and shaped_buf grew through `context.allocator` — which during render is the per-frame arena. On the next arena reset the backing died, and subsequent glyph lookups read garbage / wrote into freshly-allocated view-tree memory. Fix is shaped as the user proposed: `List(T)`'s mutations take an optional trailing `alloc: Allocator = context.allocator` argument. No allocator stored on the container, no init ceremony, every existing `list.append(item)` callsite keeps working unchanged. Long-lived owners now write `list.append(item, self.parent_allocator)` and the arena-leak bug becomes impossible to write accidentally. Default-arg substitution previously only fired for identifier callees (`expandCallDefaults` at lower.zig:7978). Extended to the generic struct-method dispatch path (`list.append(...)` lands here) via a new `appendDefaultArgs` helper that lowers fd.params[i].default_expr in the caller's scope and appends to the lowered args slice. Long-lived owners updated to capture `parent_allocator: Allocator` at init and use it for every internal growth: - GlyphCache (the chess bug) — entries, shaped_buf, hash_keys, hash_vals, atlas bitmap. - DockInteraction — drops the existing `push Context` workaround in `ensure_capacity` for the explicit-arg form. - StateStore — entries list + per-entry data buffer. - Gles3Gpu, MetalGPU — shaders, buffers, textures (atlas-grow during render would otherwise leak resources into the frame arena). Also kept: an operator-precedence fix in pipeline.sx (`(self.frame_index & 1) == 0` instead of `self.frame_index & 1 == 0`, which parses as `self.frame_index & (1 == 0)` = always 0). That was a stealth single-arena-only bug that masked the GlyphCache one for a long time. Docs: - specs.md §11 documents `param: T = expr` default parameter values. The parser already supported it — formalised in the spec now. - current/CHECKPOINT-MEM.md logs the change. - CLAUDE.md REJECTED PATTERNS gains a "Long-lived containers growing through context.allocator" section with the `parent_allocator` capture template and the list of existing examples to mirror. 155/155 example tests pass — zero-diff against snapshots since every existing callsite still resolves to `context.allocator`.
This commit is contained in:
@@ -5,14 +5,18 @@ Tracking checkpoint for the mem.sx Zig-aligned implementation
|
||||
|
||||
## Last completed step
|
||||
|
||||
- **Interp silent-arm sweep + typed raw-pointer stores.** Every
|
||||
`else =>` arm in the interp now bails with a `bailDetail("...")`
|
||||
reason that surfaces through the host diagnostic as
|
||||
`op=X/X: <reason>`. `inst.Store` carries `val_ty: TypeId` so
|
||||
comptime raw-pointer stores honour the declared destination width
|
||||
(no more 8-byte-everywhere assumption). New CLAUDE.md REJECTED
|
||||
PATTERN forbids silent unimplemented arms going forward.
|
||||
154/154 example tests + chess on macOS / iOS sim / Android green.
|
||||
- **`List(T)` mutations gain an optional `alloc: Allocator =
|
||||
context.allocator` argument** (plan:
|
||||
`~/.claude/plans/lets-see-options-for-merry-dijkstra.md`). One
|
||||
language-level change (function-param defaults already supported)
|
||||
closes a whole bug class where long-lived containers' growth
|
||||
silently landed in a per-frame arena. The chess panel-text
|
||||
regression that triggered this work is fixed end-to-end on macOS.
|
||||
GlyphCache, DockInteraction, StateStore, Gles3Gpu, and MetalGPU
|
||||
all carry `parent_allocator` and pass it explicitly to internal
|
||||
list growth. 155/155 example tests pass — zero-diff against
|
||||
snapshots since every existing callsite still resolves to
|
||||
`context.allocator`.
|
||||
|
||||
## Current state
|
||||
|
||||
@@ -146,21 +150,23 @@ chain at comptime in the interp. No remaining shortcut.
|
||||
|
||||
## Next step
|
||||
|
||||
Phase 1.3 (closure env allocation through context) and Phase 1.4
|
||||
(codegen serializer for all interp Value variants) are unblocked.
|
||||
Phase 1.2 (free / malloc through context) was considered and
|
||||
**skipped** — `context.allocator.alloc/dealloc` already works
|
||||
directly; wrapper-only `malloc`/`free` would be lossy renames.
|
||||
Phase 1.3 (closure env allocation through context) shipped in commit
|
||||
`8e21cc5`. Phase 1.4 (codegen serializer for all interp Value
|
||||
variants) remains open. Phase 1.2 (free / malloc through context) was
|
||||
considered and **skipped** — `context.allocator.alloc/dealloc`
|
||||
already works directly; wrapper-only `malloc`/`free` would be lossy
|
||||
renames.
|
||||
|
||||
Suggested next move: **Phase 1.3**. Closure trampolines in
|
||||
[lower.zig:lowerLambda](../src/ir/lower.zig#L5549) call
|
||||
`.heap_alloc` directly for the env pointer; routing through
|
||||
`context.allocator.alloc` means closures respect
|
||||
`push Context.{ allocator = ... }` and get leak-tracked by
|
||||
`TrackingAllocator`. Contained change. Regression test pattern:
|
||||
mirror `examples/130-xx-value-routes-through-context-allocator.sx`
|
||||
with a closure that captures a variable, install a tracker via
|
||||
`push`, verify the tracker's counter incremented.
|
||||
The `List(T)` allocator-arg work documented above is **outside the
|
||||
original MEM plan** but lives in the same problem space (long-lived
|
||||
container growth silently capturing the wrong allocator). It
|
||||
generalises the `parent_allocator` capture pattern that
|
||||
`ChessGameState` / `UIPipeline` already used.
|
||||
|
||||
Suggested next move: verify on iOS sim + Android via
|
||||
`tools/verify-step.sh` to confirm the GlyphCache fix + Metal/Gles3
|
||||
sweep behave on those platforms, then either commit the verify-step
|
||||
goldens or move to Phase 1.4.
|
||||
|
||||
## Phase 0.3 audit findings — chess allocator usage (closed)
|
||||
|
||||
@@ -186,6 +192,25 @@ Allocator value naturally.
|
||||
|
||||
## Log
|
||||
|
||||
- **2026-05-25 (latest)** — `List(T)` mutation API gained an optional
|
||||
trailing `alloc: Allocator = context.allocator` argument
|
||||
(`library/modules/std.sx`). Default-arg substitution previously
|
||||
only fired for identifier callees; extended to the generic-method
|
||||
dispatch path via new `appendDefaultArgs` helper at
|
||||
`lower.zig:7974-7991`, wired in at `lower.zig:5332`. Long-lived
|
||||
owners that grew internal Lists during render — `GlyphCache`,
|
||||
`DockInteraction`, `StateStore`, `Gles3Gpu`, `MetalGPU` — now
|
||||
capture `parent_allocator: Allocator` at init and forward it to
|
||||
every internal `.append` / `.alloc` / `.dealloc`. Chess panel-text
|
||||
regression (text vanished after the first move because GlyphCache
|
||||
hash + entries grew into the per-frame arena and died on reset)
|
||||
fixed end-to-end on macOS. specs.md §11 gains a "Default Parameter
|
||||
Values" subsection documenting the existing capability. Operator-
|
||||
precedence fix kept in `pipeline.sx` (`(self.frame_index & 1) == 0`
|
||||
instead of `self.frame_index & 1 == 0`, which was parsing as
|
||||
`self.frame_index & (1 == 0)` = always 0). All diagnostic logging
|
||||
added during the bug hunt has been stripped. 155/155 example tests
|
||||
green.
|
||||
- **2026-05-25 (late)** — Interp silent-arm sweep (`e9df33a`).
|
||||
Every `else =>` arm has a `bailDetail` reason; `.deref` /
|
||||
`.unbox_any` previously silently passed through arbitrary Value
|
||||
|
||||
Reference in New Issue
Block a user