// Phase 1.1 — the compiler-internal heap-copy that backs `xx ` // protocol erasure must dispatch through `context.allocator`, not call // libc malloc directly. So when a `push Context.{ allocator = tracer }` // block is active, a `xx StructLiteral.{}` inside it MUST be allocated // by the tracker. // // Note: `xx` only heap-copies for RVALUES (struct literals, call results). // `xx ` (an identifier, field access, index, or deref) borrows // the operand's storage, so it never allocates and never reaches this // path. See specs.md §3 — Protocol value ownership and lifetime. #import "modules/std.sx"; #import "modules/std/mem.sx"; // `Allocator` is non-transitive: name it, import it. Tracer :: struct { count: i64; init :: () -> *Tracer { t : *Tracer = xx libc_malloc(size_of(Tracer)); t.count = 0; t } } impl Allocator for Tracer { alloc_bytes :: (self: *Tracer, size: i64) -> *void { self.count += 1; return libc_malloc(size); } dealloc_bytes :: (self: *Tracer, ptr: *void) { libc_free(ptr); } } main :: () -> i32 { tracer := Tracer.init(); push Context.{ allocator = xx tracer, data = null } { // Struct-literal operand: rvalue → heap-copy through context.allocator. // The erased type must actually `impl Allocator` (erasure is // impl-driven, issue 0176), so use a fresh `Tracer.{}` rvalue — the // copy is what we want routed through the active allocator. ignore : Allocator = xx Tracer.{ count = 0 }; _ = ignore; } print("Tracer.count = {}\n", tracer.count); 0 }