mem: Phase 1.3 — closure env allocation through context.allocator
The closure trampoline's env-buffer heap-copy in `lowerLambda` used to
call `.heap_alloc` directly (libc malloc, no protocol). Now it routes
through `allocViaContext` like every other compiler-internal alloc,
so a closure created inside `push Context.{ allocator = ... }` honors
the installed allocator — trackers count the env, arenas absorb it,
custom allocators see it. Closes the last `.heap_alloc` shortcut for
sx-internal allocations.
One ordering subtlety fixed alongside: the deferred restore of
`current_ctx_ref` at lowerLambda exit fired AFTER the env-and-closure
build section, so `allocViaContext` was reading `Ref.fromIndex(0)`
(the lambda's own ctx param, only valid inside the lambda body) when
emitting the alloc in the CALLER's scope. Without the explicit
restore, the env_heap dispatch silently routed through the default
context — the captured tracker never saw it. Fixed by restoring
`current_ctx_ref` right after `self.builder.func = saved_func`, before
the env build.
Regression test: `examples/133-closure-env-routes-through-context-allocator.sx`
mirrors the 130-xx-value pattern — install a Tracer via `push Context`,
create a capturing closure inside, assert `Tracer.count = 1`. Without
the fix the count is 0 (env goes through default context). Verified
by stashing the lower.zig change and re-running.
Bonus: `examples/50-smoke.sx` "closure-gpa" output flips from
`allocs=-1` to `allocs=0`. The old `-1` was the bug's signature —
the test manually `dealloc`'d the env after the closure ran, but the
GPA had never seen the matching alloc, so its counter went negative.
With Phase 1.3 the alloc/dealloc balance at 0. Snapshot regen.
155/155 example tests pass (133 new + 50-smoke regen). Chess green on
macOS / iOS sim / Android.
This commit is contained in:
@@ -5828,6 +5828,15 @@ pub const Lowering = struct {
|
||||
self.builder.func = saved_func;
|
||||
self.builder.current_block = saved_block;
|
||||
self.builder.inst_counter = saved_counter;
|
||||
// Restore the caller's `current_ctx_ref` BEFORE we emit the env
|
||||
// alloc/memcpy below — those run in the caller's scope, and
|
||||
// `allocViaContext` reads `current_ctx_ref` to find the
|
||||
// installed allocator. Without this, the env_heap dispatch
|
||||
// would still see `Ref.fromIndex(0)` (the lambda's own ctx
|
||||
// param), which doesn't exist in the caller's frame and
|
||||
// silently routes through the default context instead of any
|
||||
// surrounding `push Context.{ allocator = ... }`.
|
||||
self.current_ctx_ref = saved_ctx_ref_lam;
|
||||
|
||||
// Create proper closure type (user-visible params only — skip ctx + env).
|
||||
const skip_count: usize = if (lambda_wants_ctx) 2 else 1;
|
||||
@@ -5852,11 +5861,15 @@ pub const Lowering = struct {
|
||||
self.builder.store(gep, val);
|
||||
}
|
||||
|
||||
// Copy env to heap (so it outlives the stack frame)
|
||||
// Copy env to heap (so it outlives the stack frame).
|
||||
// Route through `context.allocator.alloc` rather than calling
|
||||
// libc malloc directly so closures respect a surrounding
|
||||
// `push Context.{ allocator = ... }` and a tracker / arena
|
||||
// counts the env allocation alongside everything else.
|
||||
const env_byte_size = self.computeEnvSize(capture_list);
|
||||
const env_size = self.builder.constInt(@intCast(env_byte_size), .s64);
|
||||
const ptr_void = self.module.types.ptrTo(.void);
|
||||
const env_heap = self.builder.emit(.{ .heap_alloc = .{ .operand = env_size } }, ptr_void);
|
||||
const env_heap = self.allocViaContext(env_size, ptr_void);
|
||||
// memcpy(heap, stack_alloca, size)
|
||||
_ = self.callForeign("memcpy", &.{ env_heap, env_local, env_size }, ptr_void);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user