mem: Step 3 — thread __sx_ctx through closure/fn-pointer/method dispatch

Continues the implicit-Context refactor. Bare-fn trampolines, lambda
trampolines, and protocol thunks now carry __sx_ctx at slot 0; call
sites for closures, fn-pointer variables, and method dispatch prepend
the caller's current ctx.

- emit_llvm.zig:1687 call_indirect treats `fp_ctx_slots` leading args
  as opaque ptr (the implicit ctx) when the fn-pointer is default-conv
  under has_implicit_ctx.
- lower.zig:fnPtrTypeWantsCtx predicate gates the prepend at both
  scope-local and global fn-pointer call sites.
- lower.zig:fixupMethodReceiver skips __sx_ctx when probing the
  receiver param's type.
- lower.zig:lowerLambda builds closure type from user-visible params
  only (skip ctx + env).
- lower.zig:closure(bare_fn) builds closure type from user-visible
  params only.
- module.zig: Module.has_implicit_ctx flag mirrors Lowering's switch
  so emit_llvm can read it without a back-pointer.

Tests updated:
- 5 ObjC-block/runtime tests get `callconv(.c)` on fn-ptr types
  cast from `objc_msgSend` / Block.invoke (C-side calls into sx).
- ffi-06-callback gets `callconv(.c)` on double_it/add_with_ctx —
  the registered C-side callbacks.
- 08-types snapshot regen (undefined-init drift from layout shift).
- 11 JNI/ObjC .ir snapshots regen for the ctx-prepended thunk
  signatures.

151/152 example tests pass. Remaining failure (05-run) is the
comptime/interp path that requires Step 7 (callWithDefaultContext).
This commit is contained in:
agra
2026-05-25 08:41:50 +03:00
parent 29784c22a8
commit 92c6b47f12
22 changed files with 1601 additions and 1196 deletions

View File

@@ -1717,6 +1717,19 @@ pub const LLVMEmitter = struct {
break :blk false;
} else false;
// Default-conv fn-pointers under implicit-ctx carry a hidden
// `*void` (the implicit __sx_ctx) at LLVM slot 0. The IR fn
// type does not include it, so shift fn_params lookups by 1.
const fp_ctx_slots: usize = if (callee_ir_ty) |cty| blk: {
if (!self.ir_mod.has_implicit_ctx) break :blk 0;
if (cty.isBuiltin()) break :blk 0;
const ci = self.ir_mod.types.get(cty);
switch (ci) {
.function => |f| break :blk if (f.call_conv == .c) @as(usize, 0) else 1,
else => break :blk 0,
}
} else 0;
const ret_ty = if (callee_ir_ty) |cty| blk: {
if (!cty.isBuiltin()) {
const ci = self.ir_mod.types.get(cty);
@@ -1733,9 +1746,17 @@ pub const LLVMEmitter = struct {
defer self.alloc.free(param_tys);
if (fn_params) |fp| {
for (0..call_op.args.len) |j| {
if (j < fp.len) {
const raw_struct = self.toLLVMType(fp[j]);
if (fp_is_c_abi and self.needsByval(fp[j], raw_struct)) {
// Slots 0..fp_ctx_slots are the implicit __sx_ctx
// (passed as opaque ptr; not in fp).
if (j < fp_ctx_slots) {
param_tys[j] = self.cached_ptr;
args[j] = self.coerceArg(args[j], self.cached_ptr);
continue;
}
const fp_idx = j - fp_ctx_slots;
if (fp_idx < fp.len) {
const raw_struct = self.toLLVMType(fp[fp_idx]);
if (fp_is_c_abi and self.needsByval(fp[fp_idx], raw_struct)) {
args[j] = self.materializeByvalArg(args[j], raw_struct);
param_tys[j] = self.cached_ptr;
continue;
@@ -2294,7 +2315,18 @@ pub const LLVMEmitter = struct {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
},
.call_closure => |call_op| {
// Closure: { fn_ptr, env } — extract fn_ptr, prepend env as first arg
// Closure: { fn_ptr, env }.
//
// ABI (when module.has_implicit_ctx):
// trampoline signature: (__sx_ctx, env, args...)
// call_op.args[0] = __sx_ctx (prepended by lowering)
// call_op.args[1..] = user args
// extracted env_ptr = inserted at LLVM slot 1
//
// ABI (without implicit_ctx):
// trampoline signature: (env, args...)
// call_op.args = user args (no ctx prepend)
// extracted env_ptr = inserted at LLVM slot 0
const closure = self.resolveRef(call_op.callee);
const cl_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(closure));
if (cl_kind != c.LLVMStructTypeKind) {
@@ -2314,36 +2346,44 @@ pub const LLVMEmitter = struct {
break :blk null;
} else null;
// Build args: env_ptr + call args
const total_args = call_op.args.len + 1;
const has_ctx = self.ir_mod.has_implicit_ctx;
const user_args_offset_in_op: usize = if (has_ctx) 1 else 0;
const user_args_count: usize = call_op.args.len -| user_args_offset_in_op;
const ctx_slots: usize = if (has_ctx) 1 else 0;
const total_args = ctx_slots + 1 + user_args_count; // [ctx?] + env + user_args
const args = self.alloc.alloc(c.LLVMValueRef, total_args) catch unreachable;
defer self.alloc.free(args);
args[0] = env_ptr;
for (call_op.args, 0..) |arg_ref, j| {
args[j + 1] = self.resolveRef(arg_ref);
if (has_ctx) {
args[0] = self.resolveRef(call_op.args[0]); // ctx
}
args[ctx_slots] = env_ptr;
for (0..user_args_count) |j| {
args[ctx_slots + 1 + j] = self.resolveRef(call_op.args[user_args_offset_in_op + j]);
}
// Build function type using declared param types (not arg types)
// Build function type using declared param types (not arg types).
// closure_params is user-visible (no ctx, no env), so they line
// up with args[ctx_slots+1..].
const ret_ty = self.toLLVMType(instruction.ty);
const param_tys = self.alloc.alloc(c.LLVMTypeRef, total_args) catch unreachable;
defer self.alloc.free(param_tys);
param_tys[0] = self.cached_ptr; // env
if (has_ctx) param_tys[0] = self.cached_ptr; // __sx_ctx
param_tys[ctx_slots] = self.cached_ptr; // env
if (closure_params) |cp| {
// Use declared closure param types and coerce args to match
// cp contains user-visible params only (no env)
for (0..call_op.args.len) |j| {
for (0..user_args_count) |j| {
const param_ir_ty = if (j < cp.len) cp[j] else null;
if (param_ir_ty) |pty| {
const llvm_pty = self.toLLVMType(pty);
param_tys[j + 1] = llvm_pty;
args[j + 1] = self.coerceArg(args[j + 1], llvm_pty);
param_tys[ctx_slots + 1 + j] = llvm_pty;
args[ctx_slots + 1 + j] = self.coerceArg(args[ctx_slots + 1 + j], llvm_pty);
} else {
param_tys[j + 1] = c.LLVMTypeOf(args[j + 1]);
param_tys[ctx_slots + 1 + j] = c.LLVMTypeOf(args[ctx_slots + 1 + j]);
}
}
} else {
for (args[1..], 0..) |arg, j| {
param_tys[j + 1] = c.LLVMTypeOf(arg);
for (0..user_args_count) |j| {
param_tys[ctx_slots + 1 + j] = c.LLVMTypeOf(args[ctx_slots + 1 + j]);
}
}
const fn_ty = c.LLVMFunctionType(ret_ty, param_tys.ptr, @intCast(total_args), 0);