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:
47
examples/133-closure-env-routes-through-context-allocator.sx
Normal file
47
examples/133-closure-env-routes-through-context-allocator.sx
Normal file
@@ -0,0 +1,47 @@
|
||||
// Phase 1.3 — the closure env-buffer heap-copy in `lowerLambda` must
|
||||
// dispatch through `context.allocator`, not `.heap_alloc` directly.
|
||||
// So when a `push Context.{ allocator = tracer }` block is active, a
|
||||
// capturing closure created inside it MUST allocate its env through
|
||||
// the tracker.
|
||||
//
|
||||
// Mirrors the shape of `130-xx-value-routes-through-context-allocator.sx`
|
||||
// for the protocol-erasure heap path — same Tracer, same install via
|
||||
// `push Context`, same `Tracer.count = 1` assertion. Different
|
||||
// allocation site (closure env vs xx-value heap copy).
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Tracer :: struct {
|
||||
count: s64;
|
||||
|
||||
init :: () -> *Tracer {
|
||||
t : *Tracer = xx libc_malloc(size_of(Tracer));
|
||||
t.count = 0;
|
||||
t;
|
||||
}
|
||||
}
|
||||
|
||||
impl Allocator for Tracer {
|
||||
alloc :: (self: *Tracer, size: s64) -> *void {
|
||||
self.count += 1;
|
||||
return libc_malloc(size);
|
||||
}
|
||||
dealloc :: (self: *Tracer, ptr: *void) {
|
||||
libc_free(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
tracer := Tracer.init();
|
||||
push Context.{ allocator = xx tracer, data = null } {
|
||||
// Capturing closure. lowerLambda allocates an env struct on the
|
||||
// stack, copies the captures in, then heap-copies the env via
|
||||
// `allocViaContext` — which dispatches through the installed
|
||||
// tracer's `alloc`.
|
||||
captured : s64 = 100;
|
||||
add_capture := closure((y: s64) -> s64 => y + captured);
|
||||
_ = add_capture(1);
|
||||
}
|
||||
print("Tracer.count = {}\n", tracer.count);
|
||||
0;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
Tracer.count = 1
|
||||
@@ -517,7 +517,7 @@ closure-form: no cancel
|
||||
closure-null-env: true
|
||||
closure-slice: 10 20 30
|
||||
closure-arena: 15
|
||||
closure-gpa: 17 allocs=-1
|
||||
closure-gpa: 17 allocs=0
|
||||
closure-opt: 42
|
||||
closure-ropt: 50
|
||||
closure-ropt: none
|
||||
|
||||
Reference in New Issue
Block a user