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:
73
CLAUDE.md
73
CLAUDE.md
@@ -218,6 +218,79 @@ history. If an existing allocator type still uses the old `create`
|
|||||||
pattern, migrate it OR ask the user — never propagate the pattern
|
pattern, migrate it OR ask the user — never propagate the pattern
|
||||||
in new code, docstrings, examples, or tests.
|
in new code, docstrings, examples, or tests.
|
||||||
|
|
||||||
|
### Long-lived containers growing through `context.allocator`
|
||||||
|
|
||||||
|
❌ **Forbidden:** a struct that **outlives any single
|
||||||
|
`push Context { ... }` scope** (caches, persistent UI state, GPU
|
||||||
|
resource tables, anything-accessed-across-frame-boundaries) appending
|
||||||
|
to or growing an internal List/hash/buffer using whatever
|
||||||
|
`context.allocator` happens to be at the call site:
|
||||||
|
|
||||||
|
```sx
|
||||||
|
GlyphCache :: struct {
|
||||||
|
entries: List(GlyphEntry);
|
||||||
|
// ...
|
||||||
|
rasterize :: (self: *GlyphCache, ...) {
|
||||||
|
// BAD — during render, context.allocator is a per-frame arena,
|
||||||
|
// so the entries List backing dies on the next arena reset.
|
||||||
|
self.entries.append(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The chess panel-text bug (text vanished after the first move) was
|
||||||
|
exactly this shape: `GlyphCache.entries`, `hash_keys`, `hash_vals`,
|
||||||
|
and `shaped_buf` all grew through the per-frame arena.
|
||||||
|
|
||||||
|
✅ **Required:** capture the long-lived allocator at init time on a
|
||||||
|
`parent_allocator: Allocator` field, and forward it explicitly to
|
||||||
|
every internal growth point. `List(T)` mutations take an optional
|
||||||
|
trailing `alloc: Allocator = context.allocator`, so the call site
|
||||||
|
just names the owner:
|
||||||
|
|
||||||
|
```sx
|
||||||
|
GlyphCache :: struct {
|
||||||
|
entries: List(GlyphEntry);
|
||||||
|
parent_allocator: Allocator;
|
||||||
|
|
||||||
|
init :: (self: *GlyphCache, ...) {
|
||||||
|
self.parent_allocator = context.allocator; // libc / GPA at init
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
rasterize :: (self: *GlyphCache, ...) {
|
||||||
|
// GOOD — entries always grows through the long-lived owner,
|
||||||
|
// regardless of who's pushed what context above us.
|
||||||
|
self.entries.append(entry, self.parent_allocator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Heuristic for "is this struct long-lived?" — if its `init` is called
|
||||||
|
once at startup (or once per logical instance) and its methods are
|
||||||
|
called from a frame/event/render hot path, it's long-lived. Capture
|
||||||
|
`parent_allocator` and use it for every internal growth call.
|
||||||
|
|
||||||
|
The same applies to direct `context.allocator.alloc(...)` /
|
||||||
|
`.dealloc(...)` inside such structs — replace with
|
||||||
|
`self.parent_allocator.alloc(...)` / `.dealloc(...)`.
|
||||||
|
|
||||||
|
Existing examples of this pattern (use as templates):
|
||||||
|
|
||||||
|
- `library/modules/ui/glyph_cache.sx` — atlas, hash table, entries,
|
||||||
|
shaped_buf.
|
||||||
|
- `library/modules/ui/dock.sx` — DockInteraction's nine per-child
|
||||||
|
Lists.
|
||||||
|
- `library/modules/ui/state.sx` — StateStore.entries.
|
||||||
|
- `library/modules/gpu/gles3.sx`, `library/modules/gpu/metal.sx` —
|
||||||
|
shaders / buffers / textures.
|
||||||
|
- `library/modules/ui/pipeline.sx` — UIPipeline (used for arena
|
||||||
|
parents).
|
||||||
|
|
||||||
|
`RenderTree.nodes` in `pipeline.sx` is the **opposite** case — it's
|
||||||
|
*intentionally* per-frame arena-allocated and gets its `items` field
|
||||||
|
zeroed at the top of `tick_with_body`. Don't migrate that one.
|
||||||
|
|
||||||
## On every session start
|
## On every session start
|
||||||
|
|
||||||
Three active workstreams run in parallel — **IR** (the language compiler),
|
Three active workstreams run in parallel — **IR** (the language compiler),
|
||||||
|
|||||||
@@ -5,14 +5,18 @@ Tracking checkpoint for the mem.sx Zig-aligned implementation
|
|||||||
|
|
||||||
## Last completed step
|
## Last completed step
|
||||||
|
|
||||||
- **Interp silent-arm sweep + typed raw-pointer stores.** Every
|
- **`List(T)` mutations gain an optional `alloc: Allocator =
|
||||||
`else =>` arm in the interp now bails with a `bailDetail("...")`
|
context.allocator` argument** (plan:
|
||||||
reason that surfaces through the host diagnostic as
|
`~/.claude/plans/lets-see-options-for-merry-dijkstra.md`). One
|
||||||
`op=X/X: <reason>`. `inst.Store` carries `val_ty: TypeId` so
|
language-level change (function-param defaults already supported)
|
||||||
comptime raw-pointer stores honour the declared destination width
|
closes a whole bug class where long-lived containers' growth
|
||||||
(no more 8-byte-everywhere assumption). New CLAUDE.md REJECTED
|
silently landed in a per-frame arena. The chess panel-text
|
||||||
PATTERN forbids silent unimplemented arms going forward.
|
regression that triggered this work is fixed end-to-end on macOS.
|
||||||
154/154 example tests + chess on macOS / iOS sim / Android green.
|
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
|
## Current state
|
||||||
|
|
||||||
@@ -146,21 +150,23 @@ chain at comptime in the interp. No remaining shortcut.
|
|||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
|
|
||||||
Phase 1.3 (closure env allocation through context) and Phase 1.4
|
Phase 1.3 (closure env allocation through context) shipped in commit
|
||||||
(codegen serializer for all interp Value variants) are unblocked.
|
`8e21cc5`. Phase 1.4 (codegen serializer for all interp Value
|
||||||
Phase 1.2 (free / malloc through context) was considered and
|
variants) remains open. Phase 1.2 (free / malloc through context) was
|
||||||
**skipped** — `context.allocator.alloc/dealloc` already works
|
considered and **skipped** — `context.allocator.alloc/dealloc`
|
||||||
directly; wrapper-only `malloc`/`free` would be lossy renames.
|
already works directly; wrapper-only `malloc`/`free` would be lossy
|
||||||
|
renames.
|
||||||
|
|
||||||
Suggested next move: **Phase 1.3**. Closure trampolines in
|
The `List(T)` allocator-arg work documented above is **outside the
|
||||||
[lower.zig:lowerLambda](../src/ir/lower.zig#L5549) call
|
original MEM plan** but lives in the same problem space (long-lived
|
||||||
`.heap_alloc` directly for the env pointer; routing through
|
container growth silently capturing the wrong allocator). It
|
||||||
`context.allocator.alloc` means closures respect
|
generalises the `parent_allocator` capture pattern that
|
||||||
`push Context.{ allocator = ... }` and get leak-tracked by
|
`ChessGameState` / `UIPipeline` already used.
|
||||||
`TrackingAllocator`. Contained change. Regression test pattern:
|
|
||||||
mirror `examples/130-xx-value-routes-through-context-allocator.sx`
|
Suggested next move: verify on iOS sim + Android via
|
||||||
with a closure that captures a variable, install a tracker via
|
`tools/verify-step.sh` to confirm the GlyphCache fix + Metal/Gles3
|
||||||
`push`, verify the tracker's counter incremented.
|
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)
|
## Phase 0.3 audit findings — chess allocator usage (closed)
|
||||||
|
|
||||||
@@ -186,6 +192,25 @@ Allocator value naturally.
|
|||||||
|
|
||||||
## Log
|
## 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`).
|
- **2026-05-25 (late)** — Interp silent-arm sweep (`e9df33a`).
|
||||||
Every `else =>` arm has a `bailDetail` reason; `.deref` /
|
Every `else =>` arm has a `bailDetail` reason; `.deref` /
|
||||||
`.unbox_any` previously silently passed through arbitrary Value
|
`.unbox_any` previously silently passed through arbitrary Value
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ Gles3Gpu :: struct {
|
|||||||
shaders: List(Gles3ShaderSlot) = .{};
|
shaders: List(Gles3ShaderSlot) = .{};
|
||||||
buffers: List(u32) = .{};
|
buffers: List(u32) = .{};
|
||||||
textures: List(Gles3TextureSlot) = .{};
|
textures: List(Gles3TextureSlot) = .{};
|
||||||
|
|
||||||
|
// Captured at init() time so resource creation always grows the cache
|
||||||
|
// lists through the long-lived allocator, even when a caller (e.g.
|
||||||
|
// glyph_cache atlas-grow during render) is currently inside a transient
|
||||||
|
// arena context.
|
||||||
|
parent_allocator: Allocator = .{};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── GPU impl ───────────────────────────────────────────────────────────
|
// ── GPU impl ───────────────────────────────────────────────────────────
|
||||||
@@ -79,6 +85,7 @@ impl GPU for Gles3Gpu {
|
|||||||
inline if OS != .android { return false; }
|
inline if OS != .android { return false; }
|
||||||
self.pixel_w = pixel_w;
|
self.pixel_w = pixel_w;
|
||||||
self.pixel_h = pixel_h;
|
self.pixel_h = pixel_h;
|
||||||
|
self.parent_allocator = context.allocator;
|
||||||
|
|
||||||
if !self.gl_loaded {
|
if !self.gl_loaded {
|
||||||
load_gl(@eglGetProcAddress);
|
load_gl(@eglGetProcAddress);
|
||||||
@@ -133,7 +140,7 @@ impl GPU for Gles3Gpu {
|
|||||||
proj_loc = glGetUniformLocation(prog, "uProj".ptr),
|
proj_loc = glGetUniformLocation(prog, "uProj".ptr),
|
||||||
tex_loc = glGetUniformLocation(prog, "uTex".ptr),
|
tex_loc = glGetUniformLocation(prog, "uTex".ptr),
|
||||||
};
|
};
|
||||||
self.shaders.append(slot);
|
self.shaders.append(slot, self.parent_allocator);
|
||||||
xx self.shaders.len;
|
xx self.shaders.len;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +151,7 @@ impl GPU for Gles3Gpu {
|
|||||||
glGenBuffers(1, @b);
|
glGenBuffers(1, @b);
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, b);
|
glBindBuffer(GL_ARRAY_BUFFER, b);
|
||||||
glBufferData(GL_ARRAY_BUFFER, xx size_bytes, null, GL_DYNAMIC_DRAW);
|
glBufferData(GL_ARRAY_BUFFER, xx size_bytes, null, GL_DYNAMIC_DRAW);
|
||||||
self.buffers.append(b);
|
self.buffers.append(b, self.parent_allocator);
|
||||||
xx self.buffers.len;
|
xx self.buffers.len;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +205,7 @@ impl GPU for Gles3Gpu {
|
|||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, xx GL_CLAMP_TO_EDGE);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, xx GL_CLAMP_TO_EDGE);
|
||||||
|
|
||||||
slot : Gles3TextureSlot = .{ tex = t, bytes_per_pixel = bpp };
|
slot : Gles3TextureSlot = .{ tex = t, bytes_per_pixel = bpp };
|
||||||
self.textures.append(slot);
|
self.textures.append(slot, self.parent_allocator);
|
||||||
xx self.textures.len;
|
xx self.textures.len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ MetalGPU :: struct {
|
|||||||
shaders: List(*void) = .{}; // MTLRenderPipelineState*
|
shaders: List(*void) = .{}; // MTLRenderPipelineState*
|
||||||
buffers: List(*void) = .{}; // MTLBuffer*
|
buffers: List(*void) = .{}; // MTLBuffer*
|
||||||
textures: List(TextureSlot) = .{};
|
textures: List(TextureSlot) = .{};
|
||||||
|
|
||||||
|
// Captured at init() so resource creation always grows the cache lists
|
||||||
|
// through the long-lived allocator, even when the caller is currently
|
||||||
|
// inside a transient arena context (e.g. glyph atlas grow during render).
|
||||||
|
parent_allocator: Allocator = .{};
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GPU for MetalGPU {
|
impl GPU for MetalGPU {
|
||||||
@@ -101,6 +106,7 @@ impl GPU for MetalGPU {
|
|||||||
self.pixel_w = pixel_w;
|
self.pixel_w = pixel_w;
|
||||||
self.pixel_h = pixel_h;
|
self.pixel_h = pixel_h;
|
||||||
}
|
}
|
||||||
|
self.parent_allocator = context.allocator;
|
||||||
metal_init_ios(self);
|
metal_init_ios(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,7 +438,7 @@ metal_create_shader_ios :: (self: *MetalGPU, src: string) -> u32 {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.shaders.append(state);
|
self.shaders.append(state, self.parent_allocator);
|
||||||
xx self.shaders.len;
|
xx self.shaders.len;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,7 +458,7 @@ metal_create_buffer_ios :: (self: *MetalGPU, size_bytes: s64) -> u32 {
|
|||||||
xx size_bytes, 0);
|
xx size_bytes, 0);
|
||||||
if buf == null { return 0; }
|
if buf == null { return 0; }
|
||||||
|
|
||||||
self.buffers.append(buf);
|
self.buffers.append(buf, self.parent_allocator);
|
||||||
xx self.buffers.len;
|
xx self.buffers.len;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,7 +547,7 @@ metal_create_texture_ios :: (self: *MetalGPU, w: s32, h: s32, format: TextureFor
|
|||||||
if tex == null { return 0; }
|
if tex == null { return 0; }
|
||||||
|
|
||||||
slot : TextureSlot = .{ tex = tex, bytes_per_pixel = bytes_per_pixel };
|
slot : TextureSlot = .{ tex = tex, bytes_per_pixel = bytes_per_pixel };
|
||||||
self.textures.append(slot);
|
self.textures.append(slot, self.parent_allocator);
|
||||||
|
|
||||||
if pixels != null {
|
if pixels != null {
|
||||||
handle : u32 = xx self.textures.len;
|
handle : u32 = xx self.textures.len;
|
||||||
|
|||||||
@@ -408,13 +408,13 @@ List :: struct ($T: Type) {
|
|||||||
len: s64 = 0;
|
len: s64 = 0;
|
||||||
cap: s64 = 0;
|
cap: s64 = 0;
|
||||||
|
|
||||||
append :: (list: *List(T), item: T) {
|
append :: (list: *List(T), item: T, alloc: Allocator = context.allocator) {
|
||||||
if list.len >= list.cap {
|
if list.len >= list.cap {
|
||||||
new_cap := if list.cap == 0 then 4 else list.cap * 2;
|
new_cap := if list.cap == 0 then 4 else list.cap * 2;
|
||||||
new_items : [*]T = xx context.allocator.alloc(new_cap * size_of(T));
|
new_items : [*]T = xx alloc.alloc(new_cap * size_of(T));
|
||||||
if list.len > 0 {
|
if list.len > 0 {
|
||||||
memcpy(new_items, list.items, list.len * size_of(T));
|
memcpy(new_items, list.items, list.len * size_of(T));
|
||||||
context.allocator.dealloc(list.items);
|
alloc.dealloc(list.items);
|
||||||
}
|
}
|
||||||
list.items = new_items;
|
list.items = new_items;
|
||||||
list.cap = new_cap;
|
list.cap = new_cap;
|
||||||
@@ -422,4 +422,26 @@ List :: struct ($T: Type) {
|
|||||||
list.items[list.len] = item;
|
list.items[list.len] = item;
|
||||||
list.len += 1;
|
list.len += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensure_capacity :: (list: *List(T), n: s64, alloc: Allocator = context.allocator) {
|
||||||
|
if list.cap >= n { return; }
|
||||||
|
new_cap := if list.cap == 0 then 4 else list.cap;
|
||||||
|
while new_cap < n { new_cap = new_cap * 2; }
|
||||||
|
new_items : [*]T = xx alloc.alloc(new_cap * size_of(T));
|
||||||
|
if list.len > 0 {
|
||||||
|
memcpy(new_items, list.items, list.len * size_of(T));
|
||||||
|
alloc.dealloc(list.items);
|
||||||
|
}
|
||||||
|
list.items = new_items;
|
||||||
|
list.cap = new_cap;
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit :: (list: *List(T), alloc: Allocator = context.allocator) {
|
||||||
|
if list.items != null {
|
||||||
|
alloc.dealloc(list.items);
|
||||||
|
}
|
||||||
|
list.items = null;
|
||||||
|
list.len = 0;
|
||||||
|
list.cap = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -134,22 +134,19 @@ DockInteraction :: struct {
|
|||||||
self.header_pressed = List(bool).{};
|
self.header_pressed = List(bool).{};
|
||||||
}
|
}
|
||||||
|
|
||||||
// BLOCKED on issue-0009: should use push instead of manual save/restore
|
|
||||||
ensure_capacity :: (self: *DockInteraction, count: s64) {
|
ensure_capacity :: (self: *DockInteraction, count: s64) {
|
||||||
if self.child_count >= count { return; }
|
if self.child_count >= count { return; }
|
||||||
push Context.{ allocator = self.parent_allocator, data = context.data } {
|
while self.child_count < count {
|
||||||
while self.child_count < count {
|
self.natural_sizes.append(Size.zero(), self.parent_allocator);
|
||||||
self.natural_sizes.append(Size.zero());
|
self.alignment_overrides.append(ALIGN_CENTER, self.parent_allocator);
|
||||||
self.alignment_overrides.append(ALIGN_CENTER);
|
self.has_alignment_override.append(false, self.parent_allocator);
|
||||||
self.has_alignment_override.append(false);
|
self.is_floating.append(false, self.parent_allocator);
|
||||||
self.is_floating.append(false);
|
self.is_fill.append(false, self.parent_allocator);
|
||||||
self.is_fill.append(false);
|
self.floating_positions.append(Point.zero(), self.parent_allocator);
|
||||||
self.floating_positions.append(Point.zero());
|
self.child_bounds.append(Frame.zero(), self.parent_allocator);
|
||||||
self.child_bounds.append(Frame.zero());
|
self.anim_sizes.append(Animated(Size).make(Size.zero()), self.parent_allocator);
|
||||||
self.anim_sizes.append(Animated(Size).make(Size.zero()));
|
self.header_pressed.append(false, self.parent_allocator);
|
||||||
self.header_pressed.append(false);
|
self.child_count += 1;
|
||||||
self.child_count += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -179,6 +179,11 @@ GlyphCache :: struct {
|
|||||||
last_shape_len: s64;
|
last_shape_len: s64;
|
||||||
last_shape_size_q: u16;
|
last_shape_size_q: u16;
|
||||||
|
|
||||||
|
// Allocator that owns every dynamically-grown buffer on this cache —
|
||||||
|
// entries list, hash table, shaped_buf. Captured at init time so growth
|
||||||
|
// never accidentally lands in a transient per-frame arena.
|
||||||
|
parent_allocator: Allocator;
|
||||||
|
|
||||||
// GPU protocol backend. When set, atlas creation + dirty uploads route
|
// GPU protocol backend. When set, atlas creation + dirty uploads route
|
||||||
// through `gpu` instead of raw GL.
|
// through `gpu` instead of raw GL.
|
||||||
gpu: ?GPU = null;
|
gpu: ?GPU = null;
|
||||||
@@ -190,6 +195,7 @@ GlyphCache :: struct {
|
|||||||
// Zero out the entire struct first (parent may be uninitialized with = ---)
|
// Zero out the entire struct first (parent may be uninitialized with = ---)
|
||||||
memset(self, 0, size_of(GlyphCache));
|
memset(self, 0, size_of(GlyphCache));
|
||||||
self.gpu = saved_gpu;
|
self.gpu = saved_gpu;
|
||||||
|
self.parent_allocator = context.allocator;
|
||||||
|
|
||||||
// Load font file
|
// Load font file
|
||||||
file_size : s32 = 0;
|
file_size : s32 = 0;
|
||||||
@@ -204,7 +210,7 @@ GlyphCache :: struct {
|
|||||||
self.font_data_size = file_size;
|
self.font_data_size = file_size;
|
||||||
|
|
||||||
// Init stbtt_fontinfo
|
// Init stbtt_fontinfo
|
||||||
self.font_info = context.allocator.alloc(FONTINFO_SIZE);
|
self.font_info = self.parent_allocator.alloc(FONTINFO_SIZE);
|
||||||
memset(self.font_info, 0, FONTINFO_SIZE);
|
memset(self.font_info, 0, FONTINFO_SIZE);
|
||||||
stbtt_InitFont(self.font_info, font_data, 0);
|
stbtt_InitFont(self.font_info, font_data, 0);
|
||||||
|
|
||||||
@@ -235,7 +241,7 @@ GlyphCache :: struct {
|
|||||||
self.atlas_width = GLYPH_ATLAS_W;
|
self.atlas_width = GLYPH_ATLAS_W;
|
||||||
self.atlas_height = GLYPH_ATLAS_H;
|
self.atlas_height = GLYPH_ATLAS_H;
|
||||||
bitmap_size : s64 = xx self.atlas_width * xx self.atlas_height;
|
bitmap_size : s64 = xx self.atlas_width * xx self.atlas_height;
|
||||||
self.bitmap = xx context.allocator.alloc(bitmap_size);
|
self.bitmap = xx self.parent_allocator.alloc(bitmap_size);
|
||||||
memset(self.bitmap, 0, bitmap_size);
|
memset(self.bitmap, 0, bitmap_size);
|
||||||
|
|
||||||
// Shelf packer init
|
// Shelf packer init
|
||||||
@@ -251,10 +257,10 @@ GlyphCache :: struct {
|
|||||||
// Init hash table (256 slots)
|
// Init hash table (256 slots)
|
||||||
self.hash_cap = 256;
|
self.hash_cap = 256;
|
||||||
hash_bytes : s64 = self.hash_cap * 4; // u32 per slot
|
hash_bytes : s64 = self.hash_cap * 4; // u32 per slot
|
||||||
self.hash_keys = xx context.allocator.alloc(hash_bytes);
|
self.hash_keys = xx self.parent_allocator.alloc(hash_bytes);
|
||||||
memset(self.hash_keys, 0, hash_bytes);
|
memset(self.hash_keys, 0, hash_bytes);
|
||||||
val_bytes : s64 = self.hash_cap * 8; // s64 per slot (s32 would suffice but alignment)
|
val_bytes : s64 = self.hash_cap * 8; // s64 per slot (s32 would suffice but alignment)
|
||||||
self.hash_vals = xx context.allocator.alloc(val_bytes);
|
self.hash_vals = xx self.parent_allocator.alloc(val_bytes);
|
||||||
|
|
||||||
// Create the atlas texture. In GPU-protocol mode we create empty and
|
// Create the atlas texture. In GPU-protocol mode we create empty and
|
||||||
// let the first `flush()` push the (zero-initialized) bitmap via
|
// let the first `flush()` push the (zero-initialized) bitmap via
|
||||||
@@ -328,7 +334,7 @@ GlyphCache :: struct {
|
|||||||
advance = advance
|
advance = advance
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.entries.append(entry);
|
self.entries.append(entry, self.parent_allocator);
|
||||||
self.hash_insert(key, self.entries.len - 1);
|
self.hash_insert(key, self.entries.len - 1);
|
||||||
return @self.entries.items[self.entries.len - 1].glyph;
|
return @self.entries.items[self.entries.len - 1].glyph;
|
||||||
}
|
}
|
||||||
@@ -371,7 +377,7 @@ GlyphCache :: struct {
|
|||||||
advance = advance
|
advance = advance
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.entries.append(entry);
|
self.entries.append(entry, self.parent_allocator);
|
||||||
self.hash_insert(key, self.entries.len - 1);
|
self.hash_insert(key, self.entries.len - 1);
|
||||||
return @self.entries.items[self.entries.len - 1].glyph;
|
return @self.entries.items[self.entries.len - 1].glyph;
|
||||||
}
|
}
|
||||||
@@ -399,10 +405,10 @@ GlyphCache :: struct {
|
|||||||
|
|
||||||
self.hash_cap = old_cap * 2;
|
self.hash_cap = old_cap * 2;
|
||||||
hash_bytes : s64 = self.hash_cap * 4;
|
hash_bytes : s64 = self.hash_cap * 4;
|
||||||
self.hash_keys = xx context.allocator.alloc(hash_bytes);
|
self.hash_keys = xx self.parent_allocator.alloc(hash_bytes);
|
||||||
memset(self.hash_keys, 0, hash_bytes);
|
memset(self.hash_keys, 0, hash_bytes);
|
||||||
val_bytes : s64 = self.hash_cap * 8;
|
val_bytes : s64 = self.hash_cap * 8;
|
||||||
self.hash_vals = xx context.allocator.alloc(val_bytes);
|
self.hash_vals = xx self.parent_allocator.alloc(val_bytes);
|
||||||
|
|
||||||
// Rehash
|
// Rehash
|
||||||
mask := self.hash_cap - 1;
|
mask := self.hash_cap - 1;
|
||||||
@@ -420,8 +426,8 @@ GlyphCache :: struct {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.allocator.dealloc(old_keys);
|
self.parent_allocator.dealloc(old_keys);
|
||||||
context.allocator.dealloc(old_vals);
|
self.parent_allocator.dealloc(old_vals);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload dirty atlas to GPU. On the Metal path, defer the upload to
|
// Upload dirty atlas to GPU. On the Metal path, defer the upload to
|
||||||
@@ -482,7 +488,7 @@ GlyphCache :: struct {
|
|||||||
new_w := self.atlas_width * 2;
|
new_w := self.atlas_width * 2;
|
||||||
new_h := self.atlas_height * 2;
|
new_h := self.atlas_height * 2;
|
||||||
new_size : s64 = xx new_w * xx new_h;
|
new_size : s64 = xx new_w * xx new_h;
|
||||||
new_bitmap : [*]u8 = xx context.allocator.alloc(new_size);
|
new_bitmap : [*]u8 = xx self.parent_allocator.alloc(new_size);
|
||||||
memset(new_bitmap, 0, new_size);
|
memset(new_bitmap, 0, new_size);
|
||||||
|
|
||||||
// Copy old rows into new bitmap
|
// Copy old rows into new bitmap
|
||||||
@@ -494,7 +500,7 @@ GlyphCache :: struct {
|
|||||||
y += 1;
|
y += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.allocator.dealloc(self.bitmap);
|
self.parent_allocator.dealloc(self.bitmap);
|
||||||
self.bitmap = new_bitmap;
|
self.bitmap = new_bitmap;
|
||||||
self.atlas_width = new_w;
|
self.atlas_width = new_w;
|
||||||
self.atlas_height = new_h;
|
self.atlas_height = new_h;
|
||||||
@@ -599,7 +605,7 @@ GlyphCache :: struct {
|
|||||||
x = total,
|
x = total,
|
||||||
y = 0.0,
|
y = 0.0,
|
||||||
advance = adv
|
advance = adv
|
||||||
});
|
}, self.parent_allocator);
|
||||||
total += adv;
|
total += adv;
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
@@ -632,7 +638,7 @@ GlyphCache :: struct {
|
|||||||
x = gx,
|
x = gx,
|
||||||
y = gy,
|
y = gy,
|
||||||
advance = adv
|
advance = adv
|
||||||
});
|
}, self.parent_allocator);
|
||||||
total += adv;
|
total += adv;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ UIPipeline :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tick_with_body :: (self: *UIPipeline) {
|
tick_with_body :: (self: *UIPipeline) {
|
||||||
build_arena : *Arena = if self.frame_index & 1 == 0 then self.arena_a else self.arena_b;
|
build_arena : *Arena = if (self.frame_index & 1) == 0 then self.arena_a else self.arena_b;
|
||||||
build_arena.reset();
|
build_arena.reset();
|
||||||
|
|
||||||
// Reset render_tree nodes (backing is stale after arena reset)
|
// Reset render_tree nodes (backing is stale after arena reset)
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ StateEntry :: struct {
|
|||||||
StateStore :: struct {
|
StateStore :: struct {
|
||||||
entries: List(StateEntry);
|
entries: List(StateEntry);
|
||||||
current_generation: s64;
|
current_generation: s64;
|
||||||
|
parent_allocator: Allocator;
|
||||||
|
|
||||||
init :: (self: *StateStore) {
|
init :: (self: *StateStore) {
|
||||||
self.entries = List(StateEntry).{};
|
self.entries = List(StateEntry).{};
|
||||||
self.current_generation = 0;
|
self.current_generation = 0;
|
||||||
|
self.parent_allocator = context.allocator;
|
||||||
}
|
}
|
||||||
|
|
||||||
get_or_create :: (self: *StateStore, id: s64, $T: Type, default: T) -> State(T) {
|
get_or_create :: (self: *StateStore, id: s64, $T: Type, default: T) -> State(T) {
|
||||||
@@ -42,14 +44,14 @@ StateStore :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create new entry
|
// Create new entry
|
||||||
data : [*]u8 = xx context.allocator.alloc(size_of(T));
|
data : [*]u8 = xx self.parent_allocator.alloc(size_of(T));
|
||||||
memcpy(data, @default, size_of(T));
|
memcpy(data, @default, size_of(T));
|
||||||
self.entries.append(.{
|
self.entries.append(.{
|
||||||
id = id,
|
id = id,
|
||||||
data = data,
|
data = data,
|
||||||
size = size_of(T),
|
size = size_of(T),
|
||||||
generation = self.current_generation
|
generation = self.current_generation
|
||||||
});
|
}, self.parent_allocator);
|
||||||
State(T).{ ptr = xx data };
|
State(T).{ ptr = xx data };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
41
specs.md
41
specs.md
@@ -1061,6 +1061,45 @@ main :: () {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Default Parameter Values
|
||||||
|
|
||||||
|
A parameter can declare a default value with `name: type = expr`. When a
|
||||||
|
caller omits the trailing positional argument, the compiler substitutes
|
||||||
|
the default expression at the call site:
|
||||||
|
|
||||||
|
```sx
|
||||||
|
greet :: (name: string, prefix: string = "Hello") {
|
||||||
|
print("{} {}!\n", prefix, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
greet("world"); // prints "Hello world!"
|
||||||
|
greet("world", "Good morning"); // prints "Good morning world!"
|
||||||
|
```
|
||||||
|
|
||||||
|
The default expression is captured as an AST node at parse time and
|
||||||
|
re-lowered fresh at each call site, so runtime expressions like
|
||||||
|
`context.allocator` resolve in the **caller's** scope, not the callee's
|
||||||
|
definition site. This is the mechanism that lets stdlib containers like
|
||||||
|
`List(T)` expose an optional allocator argument that defaults to
|
||||||
|
`context.allocator` without requiring callers to thread one through:
|
||||||
|
|
||||||
|
```sx
|
||||||
|
// In std.sx:
|
||||||
|
List :: struct ($T: Type) {
|
||||||
|
append :: (list: *List(T), item: T, alloc: Allocator = context.allocator) {
|
||||||
|
// ... grows via `alloc.alloc(...)` ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call sites:
|
||||||
|
list.append(42); // alloc = current context.allocator
|
||||||
|
list.append(42, self.parent_allocator); // alloc = the named long-lived owner
|
||||||
|
```
|
||||||
|
|
||||||
|
Defaults are only consulted for **trailing** missing positional args; once
|
||||||
|
a position is provided, all earlier positions must also be provided. There
|
||||||
|
is no named-argument syntax for skipping middle defaults.
|
||||||
|
|
||||||
### Enum Definition
|
### Enum Definition
|
||||||
|
|
||||||
```sx
|
```sx
|
||||||
@@ -2041,7 +2080,7 @@ struct_decl = IDENT '::' 'struct' '{' struct_member* '}'
|
|||||||
struct_member = field_group | '#using' IDENT ';'
|
struct_member = field_group | '#using' IDENT ';'
|
||||||
field_group = IDENT (',' IDENT)* ':' type ('=' expr)? ';'
|
field_group = IDENT (',' IDENT)* ':' type ('=' expr)? ';'
|
||||||
params = param (',' param)* ','?
|
params = param (',' param)* ','?
|
||||||
param = IDENT ':' type
|
param = IDENT ':' type ('=' expr)?
|
||||||
block = '{' stmt* '}'
|
block = '{' stmt* '}'
|
||||||
stmt = decl | assignment ';' | multi_assign ';' | return_stmt | defer_stmt | insert_stmt
|
stmt = decl | assignment ';' | multi_assign ';' | return_stmt | defer_stmt | insert_stmt
|
||||||
| push_stmt | break_stmt | continue_stmt | expr ';'
|
| push_stmt | break_stmt | continue_stmt | expr ';'
|
||||||
|
|||||||
@@ -5329,6 +5329,7 @@ pub const Lowering = struct {
|
|||||||
const ret_ty = func.ret;
|
const ret_ty = func.ret;
|
||||||
const params = func.params;
|
const params = func.params;
|
||||||
self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty);
|
self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty);
|
||||||
|
self.appendDefaultArgs(fd, &method_args);
|
||||||
const final_args = self.prependCtxIfNeeded(func, method_args.items);
|
const final_args = self.prependCtxIfNeeded(func, method_args.items);
|
||||||
self.coerceCallArgs(final_args, params);
|
self.coerceCallArgs(final_args, params);
|
||||||
return self.builder.call(fid, final_args, ret_ty);
|
return self.builder.call(fid, final_args, ret_ty);
|
||||||
@@ -7971,6 +7972,23 @@ pub const Lowering = struct {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// After args have been lowered, append the lowered values of any
|
||||||
|
/// `param: T = default_expr` defaults for positions past `args.items.len`.
|
||||||
|
/// Stops at the first param without a default. Used at method-dispatch
|
||||||
|
/// sites whose callee is a field_access (so `expandCallDefaults` can't
|
||||||
|
/// handle them up front). The default expression is lowered in the
|
||||||
|
/// caller's current scope, so identifiers like `context.allocator`
|
||||||
|
/// resolve to the caller's runtime context.
|
||||||
|
fn appendDefaultArgs(self: *Lowering, fd: *const ast.FnDecl, args: *std.ArrayList(Ref)) void {
|
||||||
|
if (args.items.len >= fd.params.len) return;
|
||||||
|
var i: usize = args.items.len;
|
||||||
|
while (i < fd.params.len) : (i += 1) {
|
||||||
|
const dflt = fd.params[i].default_expr orelse break;
|
||||||
|
const v = self.lowerExpr(dflt);
|
||||||
|
args.append(self.alloc, v) catch unreachable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// When a bare-identifier call omits trailing positional args and the
|
/// When a bare-identifier call omits trailing positional args and the
|
||||||
/// callee's signature provides defaults for them, return a fresh Call
|
/// callee's signature provides defaults for them, return a fresh Call
|
||||||
/// node with the defaults filled in. Returns null when no expansion is
|
/// node with the defaults filled in. Returns null when no expansion is
|
||||||
|
|||||||
Reference in New Issue
Block a user