From efb087559dd53150e6479b841c40c800472800b3 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 19 May 2026 00:22:35 +0300 Subject: [PATCH] ir: auto-deref *Self when invoking a Closure-typed field (issue-0035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When lowering `self.cb()` from inside a method whose receiver is *Self, the field-access path passed the receiver pointer (not the aggregate) to `structGet`, which then produced `call void undef(ptr undef)` at the LLVM level — undefined at runtime, corrupted adjacent globals when it transferred control to a garbage pointer. Auto-load through the pointer first so structGet receives a real aggregate. Discovered while building the new AndroidPlatform's `run_frame_loop` — calling the stored frame closure as `self.frame_closure()` zeroed out adjacent globals because the undef call jumped into random memory. Added examples/100-closure-field-call-via-self-ptr.sx as the locked-in regression: both direct (`self.cb()`) and hoisted (`fn := self.cb; fn();`) forms must yield identical IR + behavior. 86/86 regression tests pass. --- .../100-closure-field-call-via-self-ptr.sx | 40 +++++++++++++++++++ src/ir/lower.zig | 9 ++++- .../100-closure-field-call-via-self-ptr.exit | 1 + .../100-closure-field-call-via-self-ptr.txt | 1 + 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 examples/100-closure-field-call-via-self-ptr.sx create mode 100644 tests/expected/100-closure-field-call-via-self-ptr.exit create mode 100644 tests/expected/100-closure-field-call-via-self-ptr.txt diff --git a/examples/100-closure-field-call-via-self-ptr.sx b/examples/100-closure-field-call-via-self-ptr.sx new file mode 100644 index 0000000..87eab7e --- /dev/null +++ b/examples/100-closure-field-call-via-self-ptr.sx @@ -0,0 +1,40 @@ +// Invoking a Closure-typed struct field as `self.field()` from a +// method whose receiver is `*Self`. The field access must auto-deref +// the pointer before extracting the closure value. + +#import "modules/std.sx"; + +Holder :: struct { + cb: Closure() = ---; + has: bool = false; + + set :: (self: *Holder, fn: Closure()) { + self.cb = fn; + self.has = true; + } + + // Direct invocation through *self. + call_direct :: (self: *Holder) { + if self.has == false { return; } + self.cb(); + } + + // Hoist-then-call form — must agree with the direct form. + call_hoisted :: (self: *Holder) { + if self.has == false { return; } + fn := self.cb; + fn(); + } +} + +ticks : s32 = 0; + +main :: () -> s32 { + h : Holder = .{}; + h.set(() => { ticks += 1; }); + + h.call_direct(); + h.call_hoisted(); + + return ticks; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index e72d0b6..26e9299 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -4215,8 +4215,13 @@ pub const Lowering = struct { if (f.name == field_name_id and !f.ty.isBuiltin()) { const fti = self.module.types.get(f.ty); if (fti == .closure) { - // Extract closure from struct field - const closure_val = self.builder.structGet(obj, @intCast(fi), f.ty); + // structGet requires an aggregate value; if obj is *T, load through it first. + var agg = obj; + const oi = self.module.types.get(obj_ty); + if (oi == .pointer) { + agg = self.builder.load(obj, oi.pointer.pointee); + } + const closure_val = self.builder.structGet(agg, @intCast(fi), f.ty); const owned = self.alloc.dupe(Ref, args.items) catch unreachable; return self.builder.emit(.{ .call_closure = .{ .callee = closure_val, .args = owned } }, fti.closure.ret); } diff --git a/tests/expected/100-closure-field-call-via-self-ptr.exit b/tests/expected/100-closure-field-call-via-self-ptr.exit new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/tests/expected/100-closure-field-call-via-self-ptr.exit @@ -0,0 +1 @@ +2 diff --git a/tests/expected/100-closure-field-call-via-self-ptr.txt b/tests/expected/100-closure-field-call-via-self-ptr.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/expected/100-closure-field-call-via-self-ptr.txt @@ -0,0 +1 @@ +