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);

View File

@@ -456,6 +456,12 @@ pub const Function = struct {
/// sites apply the standard default argument promotions (s8/s16/bool →
/// s32, f32 → f64) to extras past the fixed param count.
is_variadic: bool = false,
/// True if `params[0]` is the synthetic `__sx_ctx: *Context`
/// parameter that every default-conv sx function receives. Callers
/// read this flag to decide whether to prepend their current
/// `__sx_ctx` value to the args of a call. Foreign decls and
/// `callconv(.c)` functions have it false.
has_implicit_ctx: bool = false,
pub const Param = struct {
name: StringId,

View File

@@ -97,6 +97,16 @@ pub const Lowering = struct {
module_scopes: ?*std.StringHashMap(std.StringHashMap(void)) = null, // per-module visible names (from import resolution)
import_graph: ?*std.StringHashMap(std.StringHashMap(void)) = null, // module path → set of directly imported paths (used by param_impl_map visibility filter)
current_source_file: ?[]const u8 = null, // source file of function currently being lowered
// Implicit Context parameter machinery. When the program imports
// `std.sx` (and therefore declares `Context :: struct {...}`), every
// default-conv sx function gains a synthetic `__sx_ctx: *void` param
// at slot 0, and `current_ctx_ref` is bound to that param on each
// function-body entry. `lowerCall` / `call_indirect` prepend this ref
// to the args of every sx-to-sx call. push Context.{...} rebinds it
// to a stack-allocated Context for the lexical body. See
// `~/.claude/plans/lets-see-options-for-merry-dijkstra.md`.
implicit_ctx_enabled: bool = false,
current_ctx_ref: Ref = Ref.none,
sel_register_name_fid: ?FuncId = null, // lazily-declared `sel_registerName` extern (non-literal selector fallback)
jni_env_stack: std.ArrayList(Ref) = std.ArrayList(Ref).empty, // lexical `#jni_env(env)` Ref stack — top is current scope's env for omitted-env `#jni_call`
jni_env_stack_base: usize = 0, // index above which the currently-lowering fn's `#jni_env` scopes live; outer-fn Refs aren't valid in this fn's instruction stream
@@ -214,6 +224,13 @@ pub const Lowering = struct {
.root => |r| r.decls,
else => return,
};
// Pass 0: pre-scan for `Context :: struct {...}`. If the program
// imports `std.sx` it has Context, and every default-conv sx
// function gets the implicit `__sx_ctx` param. Otherwise the
// implicit-ctx machinery stays fully disabled — programs that
// call only libc directly keep their bare C ABI.
self.implicit_ctx_enabled = detectContextDecl(decls);
self.module.has_implicit_ctx = self.implicit_ctx_enabled;
// Pass 1: scan — register all function ASTs, struct types, extern stubs
self.scanDecls(decls);
// Pass 1b: inject compile-time constants (OS, ARCH, POINTER_SIZE) from target config
@@ -400,6 +417,55 @@ pub const Lowering = struct {
}
}
/// Detect whether `Context :: struct {...}` is declared anywhere in the
/// program. Used to gate the implicit `__sx_ctx` param machinery: when
/// `std.sx` is in the dep graph, `Context` is declared and every sx
/// function gets the implicit param. Otherwise the program runs with a
/// bare C ABI (no global Context, no implicit param, no FFI wrappers).
fn detectContextDecl(decls: []const *const Node) bool {
for (decls) |decl| {
const found = switch (decl.data) {
.struct_decl => |sd| std.mem.eql(u8, sd.name, "Context"),
.const_decl => |cd|
std.mem.eql(u8, cd.name, "Context") and cd.value.data == .struct_decl,
.namespace_decl => |ns| detectContextDecl(ns.decls),
else => false,
};
if (found) return true;
}
return false;
}
/// Returns true if a sx function declaration should receive the
/// implicit `__sx_ctx` parameter. False for foreign-libc bindings,
/// #builtin / #compiler bodies, and C-conv functions (which keep
/// their literal C ABI). Also false for OS-called entry points
/// (`isExportedEntryName`): main and JNI hooks are invoked by the
/// dyld / JVM with no `__sx_ctx` arg, so the visible signature must
/// not include one. Their bodies are still sx code — they
/// synthesise `&__sx_default_context` at entry and use it as their
/// own `current_ctx_ref`. Full FFI-wrapper split (a separate
/// `__sx_<name>_impl` with the ctx param) lands in Step 4 proper.
fn funcWantsImplicitCtx(self: *const Lowering, fd: *const ast.FnDecl) bool {
if (!self.implicit_ctx_enabled) return false;
if (fd.call_conv == .c) return false;
return switch (fd.body.data) {
.foreign_expr, .builtin_expr, .compiler_expr => false,
else => !isExportedEntryName(fd.name),
};
}
/// Returns true if a fn-pointer of the given type carries an implicit
/// `__sx_ctx` at LLVM slot 0. Default-conv sx fn-pointers do; C-conv
/// (and any non-function type) does not.
fn fnPtrTypeWantsCtx(self: *const Lowering, ty: TypeId) bool {
if (!self.implicit_ctx_enabled) return false;
if (ty.isBuiltin()) return false;
const ti = self.module.types.get(ty);
if (ti != .function) return false;
return ti.function.call_conv != .c;
}
/// Pass 1: Scan declarations — register ASTs and extern stubs, but don't lower bodies.
fn scanDecls(self: *Lowering, decls: []const *const Node) void {
for (decls) |decl| {
@@ -714,7 +780,14 @@ pub const Lowering = struct {
effective_params = fd.params[0 .. fd.params.len - 1];
}
const wants_ctx = self.funcWantsImplicitCtx(fd);
var params = std.ArrayList(Function.Param).empty;
if (wants_ctx) {
params.append(self.alloc, .{
.name = self.module.types.internString("__sx_ctx"),
.ty = self.module.types.ptrTo(.void),
}) catch unreachable;
}
for (effective_params) |p| {
const pty = self.resolveParamType(&p);
params.append(self.alloc, .{
@@ -735,6 +808,7 @@ pub const Lowering = struct {
func.call_conv = cc;
func.source_file = self.current_source_file;
func.is_variadic = is_variadic;
func.has_implicit_ctx = wants_ctx;
self.foreign_name_map.put(name, c_name) catch {};
return;
}
@@ -746,6 +820,7 @@ pub const Lowering = struct {
func.call_conv = cc;
func.source_file = self.current_source_file;
func.is_variadic = is_variadic;
func.has_implicit_ctx = wants_ctx;
}
/// Check if a C-imported function is visible from the current source file.
@@ -893,8 +968,11 @@ pub const Lowering = struct {
func.is_extern = false; // promote from extern stub to real function
func.linkage = if (isExportedEntryName(name)) .external else .internal;
if (fd.call_conv == .c) func.call_conv = .c;
// Set inst_counter to param count (params occupy refs 0..N-1)
std.debug.assert(func.params.len == fd.params.len); // AST and IR param counts must match
// Set inst_counter to param count (params occupy refs 0..N-1).
// IR params = AST params + 1 if the function carries `__sx_ctx`
// at slot 0.
const ctx_slots: usize = if (func.has_implicit_ctx) 1 else 0;
std.debug.assert(func.params.len == fd.params.len + ctx_slots);
self.builder.inst_counter = @intCast(func.params.len);
// Create entry block
@@ -907,14 +985,35 @@ pub const Lowering = struct {
defer scope.deinit();
self.scope = &scope;
// The implicit `__sx_ctx` param (when present) lives at slot 0;
// user params shift by one. `current_ctx_ref` is bound to slot 0
// so call-site lowering can prepend it to every sx-to-sx call.
// For OS-called entry points (main / JNI hooks), there's no
// ctx param at all — we synthesise `&__sx_default_context` and
// bind `current_ctx_ref` to its address so the body's sx-to-sx
// calls have a sensible Context to forward.
const wants_ctx = self.funcWantsImplicitCtx(fd);
const saved_ctx_ref = self.current_ctx_ref;
defer self.current_ctx_ref = saved_ctx_ref;
const user_param_base: u32 = if (wants_ctx) 1 else 0;
if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0);
for (fd.params, 0..) |p, i| {
const pty = self.resolveParamType(&p);
const slot = self.builder.alloca(pty);
const param_ref = Ref.fromIndex(@intCast(i));
const param_ref = Ref.fromIndex(@intCast(i + user_param_base));
self.builder.store(slot, param_ref);
scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
}
// Inbound entry points: bind current_ctx_ref to the static default
// before any user code runs.
if (!wants_ctx and self.implicit_ctx_enabled and isExportedEntryName(name)) {
if (self.global_names.get("__sx_default_context")) |dctx_gi| {
self.current_ctx_ref = self.builder.emit(.{ .global_addr = dctx_gi.id }, self.module.types.ptrTo(.void));
}
}
// Auto-initialize context with default GPA at the start of main()
if (std.mem.eql(u8, name, "main")) {
self.emitDefaultContextInit();
@@ -964,8 +1063,16 @@ pub const Lowering = struct {
const name_id = self.module.types.internString(name);
const ret_ty = self.resolveReturnType(fd);
const wants_ctx = self.funcWantsImplicitCtx(fd);
// Build param list
var params = std.ArrayList(Function.Param).empty;
if (wants_ctx) {
params.append(self.alloc, .{
.name = self.module.types.internString("__sx_ctx"),
.ty = self.module.types.ptrTo(.void),
}) catch unreachable;
}
for (fd.params) |p| {
const pty = self.resolveParamType(&p);
params.append(self.alloc, .{
@@ -982,13 +1089,15 @@ pub const Lowering = struct {
// Skip generic functions (they have type parameters and are templates, not concrete)
if (fd.type_params.len > 0) {
_ = self.builder.declareExtern(name_id, params.items, ret_ty);
const fid = self.builder.declareExtern(name_id, params.items, ret_ty);
self.module.getFunctionMut(fid).has_implicit_ctx = wants_ctx;
return;
}
// Imported functions: declare as extern (don't lower bodies from other files)
if (is_imported) {
_ = self.builder.declareExtern(name_id, params.items, ret_ty);
const fid = self.builder.declareExtern(name_id, params.items, ret_ty);
self.module.getFunctionMut(fid).has_implicit_ctx = wants_ctx;
return;
}
@@ -998,6 +1107,7 @@ pub const Lowering = struct {
ret_ty,
);
_ = func_id;
self.builder.currentFunc().has_implicit_ctx = wants_ctx;
// Set linkage. Default for fn defs is `internal` (LLVM DCE-friendly,
// matches C `static`). isExportedEntryName lists the names the OS
@@ -1023,16 +1133,32 @@ pub const Lowering = struct {
self.scope = &scope;
defer self.scope = scope.parent;
// Implicit `__sx_ctx` at slot 0 when funcWantsImplicitCtx is true;
// user params shift by one. Bind `current_ctx_ref` for call-site
// forwarding inside the body.
const wants_ctx_lf = self.funcWantsImplicitCtx(fd);
const saved_ctx_ref_lf = self.current_ctx_ref;
defer self.current_ctx_ref = saved_ctx_ref_lf;
const user_param_base_lf: u32 = if (wants_ctx_lf) 1 else 0;
if (wants_ctx_lf) self.current_ctx_ref = Ref.fromIndex(0);
for (fd.params, 0..) |p, i| {
const pty = self.resolveParamType(&p);
// Allocate stack slot for param, store initial value.
// Refs 0..N-1 are reserved for function parameters by beginFunction.
const slot = self.builder.alloca(pty);
const param_ref = Ref.fromIndex(@intCast(i));
const param_ref = Ref.fromIndex(@intCast(i + user_param_base_lf));
self.builder.store(slot, param_ref);
scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
}
// Inbound entry points: bind current_ctx_ref to &__sx_default_context.
if (!wants_ctx_lf and self.implicit_ctx_enabled and isExportedEntryName(name)) {
if (self.global_names.get("__sx_default_context")) |dctx_gi| {
self.current_ctx_ref = self.builder.emit(.{ .global_addr = dctx_gi.id }, self.module.types.ptrTo(.void));
}
}
// Lower the function body, capturing the last expression's value for implicit return
const saved_target = self.target_type;
self.target_type = if (ret_ty != .void) ret_ty else null;
@@ -3216,8 +3342,10 @@ pub const Lowering = struct {
/// If a method's first param expects a pointer (*T) but we're passing T by value,
/// swap the first arg with the alloca address (implicit address-of).
fn fixupMethodReceiver(self: *Lowering, method_args: *std.ArrayList(Ref), func: *const Function, obj_node: *const Node, obj_ty: TypeId) void {
if (func.params.len == 0) return;
const first_param_ty = func.params[0].ty;
// Skip the implicit __sx_ctx param when inspecting the receiver slot.
const skip: usize = if (func.has_implicit_ctx) 1 else 0;
if (func.params.len <= skip) return;
const first_param_ty = func.params[skip].ty;
// Check if first param expects a pointer
if (!first_param_ty.isBuiltin()) {
const pi = self.module.types.get(first_param_ty);
@@ -4546,10 +4674,12 @@ pub const Lowering = struct {
}
if (self.resolveFuncByName(fn_name)) |fid| {
const func = &self.module.functions.items[@intFromEnum(fid)];
// Build closure type from function signature
// Build closure type from user-visible params only —
// skip the implicit __sx_ctx param.
var param_types_list = std.ArrayList(TypeId).empty;
defer param_types_list.deinit(self.alloc);
for (func.params) |p| {
const skip: usize = if (func.has_implicit_ctx) 1 else 0;
for (func.params[skip..]) |p| {
param_types_list.append(self.alloc, p.ty) catch unreachable;
}
const closure_ty = self.module.types.closureType(param_types_list.items, func.ret);
@@ -4731,7 +4861,16 @@ pub const Lowering = struct {
const ty_info = self.module.types.get(binding.ty);
if (ty_info == .closure) {
const callee_ref = if (binding.is_alloca) self.builder.load(binding.ref, binding.ty) else binding.ref;
const owned = self.alloc.dupe(Ref, args.items) catch unreachable;
// Closure trampolines carry `__sx_ctx` at
// slot 0; emit_llvm's `call_closure` builds
// the call as [ctx, env, user_args], so we
// prepend ctx here. args[0] becomes ctx.
const owned = if (self.implicit_ctx_enabled) blk: {
const arr = self.alloc.alloc(Ref, args.items.len + 1) catch unreachable;
arr[0] = self.current_ctx_ref;
@memcpy(arr[1..], args.items);
break :blk arr;
} else self.alloc.dupe(Ref, args.items) catch unreachable;
const ret_ty = ty_info.closure.ret;
return self.builder.emit(.{ .call_closure = .{ .callee = callee_ref, .args = owned } }, ret_ty);
}
@@ -4771,21 +4910,28 @@ pub const Lowering = struct {
if (self.fn_ast_map.get(func_name)) |fd| {
self.packVariadicCallArgs(fd, c, &args);
}
const final_args = self.prependCtxIfNeeded(func, args.items);
// Coerce arguments to match parameter types
self.coerceCallArgs(args.items, params);
if (func.is_variadic) self.promoteCVariadicArgs(args.items, params.len);
return self.builder.call(fid, args.items, ret_ty);
self.coerceCallArgs(final_args, params);
if (func.is_variadic) self.promoteCVariadicArgs(final_args, params.len);
return self.builder.call(fid, final_args, ret_ty);
}
}
// May be a variable holding a function pointer (non-closure)
if (self.scope) |scope| {
if (scope.lookup(id.name)) |binding| {
const callee_ref = if (binding.is_alloca) self.builder.load(binding.ref, binding.ty) else binding.ref;
const owned = self.alloc.dupe(Ref, args.items) catch unreachable;
const ret_ty = if (!binding.ty.isBuiltin()) blk: {
const bti = self.module.types.get(binding.ty);
break :blk if (bti == .function) bti.function.ret else .s64;
} else .s64;
var final_args = std.ArrayList(Ref).empty;
defer final_args.deinit(self.alloc);
if (self.fnPtrTypeWantsCtx(binding.ty)) {
final_args.append(self.alloc, self.current_ctx_ref) catch unreachable;
}
final_args.appendSlice(self.alloc, args.items) catch unreachable;
const owned = self.alloc.dupe(Ref, final_args.items) catch unreachable;
return self.builder.emit(.{ .call_indirect = .{ .callee = callee_ref, .args = owned } }, ret_ty);
}
}
@@ -4825,7 +4971,13 @@ pub const Lowering = struct {
arg.* = self.coerceToType(arg.*, src_ty, dst_ty);
}
}
const owned = self.alloc.dupe(Ref, args.items) catch unreachable;
var final_args = std.ArrayList(Ref).empty;
defer final_args.deinit(self.alloc);
if (self.fnPtrTypeWantsCtx(gi.ty)) {
final_args.append(self.alloc, self.current_ctx_ref) catch unreachable;
}
final_args.appendSlice(self.alloc, args.items) catch unreachable;
const owned = self.alloc.dupe(Ref, final_args.items) catch unreachable;
return self.builder.emit(.{ .call_indirect = .{ .callee = callee_ref, .args = owned } }, gti.function.ret);
}
}
@@ -4886,8 +5038,9 @@ pub const Lowering = struct {
}
if (self.resolveFuncByName(mangled)) |fid| {
const func = &self.module.functions.items[@intFromEnum(fid)];
self.coerceCallArgs(args.items, func.params);
return self.builder.call(fid, args.items, func.ret);
const final_args = self.prependCtxIfNeeded(func, args.items);
self.coerceCallArgs(final_args, func.params);
return self.builder.call(fid, final_args, func.ret);
}
}
}
@@ -4981,9 +5134,10 @@ pub const Lowering = struct {
if (self.fn_ast_map.get(effective_name)) |fd| {
self.packVariadicCallArgs(fd, c, &args);
}
self.coerceCallArgs(args.items, params);
if (func.is_variadic) self.promoteCVariadicArgs(args.items, params.len);
return self.builder.call(fid, args.items, ret_ty);
const final_args = self.prependCtxIfNeeded(func, args.items);
self.coerceCallArgs(final_args, params);
if (func.is_variadic) self.promoteCVariadicArgs(final_args, params.len);
return self.builder.call(fid, final_args, ret_ty);
}
// Check if this is Type.variant(payload) — qualified enum construction
if (ns_name) |type_name| {
@@ -5045,7 +5199,13 @@ pub const Lowering = struct {
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;
// Prepend ctx for sx-side closure call ABI.
const owned = if (self.implicit_ctx_enabled) blk: {
const arr = self.alloc.alloc(Ref, args.items.len + 1) catch unreachable;
arr[0] = self.current_ctx_ref;
@memcpy(arr[1..], args.items);
break :blk arr;
} else self.alloc.dupe(Ref, args.items) catch unreachable;
return self.builder.emit(.{ .call_closure = .{ .callee = closure_val, .args = owned } }, fti.closure.ret);
}
}
@@ -5125,8 +5285,9 @@ pub const Lowering = struct {
const ret_ty = func.ret;
const params = func.params;
self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty);
self.coerceCallArgs(method_args.items, params);
return self.builder.call(fid, method_args.items, ret_ty);
const final_args = self.prependCtxIfNeeded(func, method_args.items);
self.coerceCallArgs(final_args, params);
return self.builder.call(fid, final_args, ret_ty);
}
}
}
@@ -5173,8 +5334,9 @@ pub const Lowering = struct {
arg_idx += 1;
}
self.fixupMethodReceiver(&gvalue_args, gfunc, effective_obj_node, obj_ty);
self.coerceCallArgs(gvalue_args.items, gparams);
return self.builder.call(gfid, gvalue_args.items, gret_ty);
const final_args = self.prependCtxIfNeeded(gfunc, gvalue_args.items);
self.coerceCallArgs(final_args, gparams);
return self.builder.call(gfid, final_args, gret_ty);
}
}
}
@@ -5190,19 +5352,30 @@ pub const Lowering = struct {
const func = &self.module.functions.items[@intFromEnum(fid)];
const ret_ty = func.ret;
const params = func.params;
const has_ctx = func.has_implicit_ctx;
self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty);
// Note: coerceCallArgs can trigger protocol thunk creation
// (module.addFunction), invalidating func pointer.
// Use pre-extracted params/ret_ty instead of func.* after this.
self.coerceCallArgs(method_args.items, params);
return self.builder.call(fid, method_args.items, ret_ty);
// Use pre-extracted params/ret_ty (+ has_ctx) instead of
// func.* after this.
const final_args = blk: {
if (!has_ctx) break :blk method_args.items;
const new_args = self.alloc.alloc(Ref, method_args.items.len + 1) catch break :blk method_args.items;
new_args[0] = self.current_ctx_ref;
@memcpy(new_args[1..], method_args.items);
break :blk new_args;
};
self.coerceCallArgs(final_args, params);
return self.builder.call(fid, final_args, ret_ty);
}
}
// Try to resolve as bare function name (method)
if (self.resolveFuncByName(fa.field)) |fid| {
const ret_ty = self.module.functions.items[@intFromEnum(fid)].ret;
return self.builder.call(fid, method_args.items, ret_ty);
const func = &self.module.functions.items[@intFromEnum(fid)];
const ret_ty = func.ret;
const final_args = self.prependCtxIfNeeded(func, method_args.items);
return self.builder.call(fid, final_args, ret_ty);
}
return self.emitError(fa.field, c.callee.span);
},
@@ -5228,8 +5401,9 @@ pub const Lowering = struct {
const func = &self.module.functions.items[@intFromEnum(fid)];
const ret_ty = func.ret;
const params = func.params;
self.coerceCallArgs(args.items, params);
return self.builder.call(fid, args.items, ret_ty);
const final_args = self.prependCtxIfNeeded(func, args.items);
self.coerceCallArgs(final_args, params);
return self.builder.call(fid, final_args, ret_ty);
}
}
}
@@ -5299,7 +5473,14 @@ pub const Lowering = struct {
// field 0 = receiver ctx, field 1 = alloc fn-ptr.
const alloc_ctx = self.builder.structGet(allocator, 0, void_ptr_ty);
const fn_ptr = self.builder.structGet(allocator, 1, void_ptr_ty);
const args = self.alloc.dupe(Ref, &.{ alloc_ctx, size_ref }) catch unreachable;
// Allocator thunks are sx-side and carry the implicit __sx_ctx at
// slot 0. Forward our caller's current_ctx_ref so the thunk's body
// (and the concrete alloc method it forwards to) has a real
// Context to thread on.
const args = if (self.implicit_ctx_enabled)
self.alloc.dupe(Ref, &.{ self.current_ctx_ref, alloc_ctx, size_ref }) catch unreachable
else
self.alloc.dupe(Ref, &.{ alloc_ctx, size_ref }) catch unreachable;
return self.builder.emit(.{ .call_indirect = .{
.callee = fn_ptr,
.args = args,
@@ -5315,6 +5496,22 @@ pub const Lowering = struct {
return self.builder.call(fid, args, ret_ty);
}
/// Prepend the caller's current `__sx_ctx` to `args` when the callee
/// has the implicit context param. Returns either the original `args`
/// (when no prepend is needed) or a newly-allocated slice with ctx at
/// slot 0. The returned slice is mutable so callers can pass it
/// straight into `coerceCallArgs`. Direct callers that built the args
/// themselves with __sx_ctx already prepended (protocol thunks, FFI
/// wrappers in Step 4) should NOT call this — they already manage
/// slot 0.
fn prependCtxIfNeeded(self: *Lowering, callee: *const Function, args: []Ref) []Ref {
if (!callee.has_implicit_ctx) return args;
const new_args = self.alloc.alloc(Ref, args.len + 1) catch return args;
new_args[0] = self.current_ctx_ref;
@memcpy(new_args[1..], args);
return new_args;
}
/// Pattern-match `context.allocator.alloc(size)` → heap_alloc,
/// `context.allocator.dealloc(ptr)` → heap_free.
fn matchContextAllocCall(self: *Lowering, fa: ast.FieldAccess, call_args: []const Ref) ?Ref {
@@ -5433,9 +5630,20 @@ pub const Lowering = struct {
const saved_counter = self.builder.inst_counter;
const saved_scope = self.scope;
// Build param list — trampoline convention: env: *void is first param
// Build param list. Convention when implicit_ctx is enabled:
// slot 0 = __sx_ctx: *void
// slot 1 = env: *void
// slot 2+ = user params
// Without implicit_ctx, env is slot 0 and user params follow.
var params = std.ArrayList(Function.Param).empty;
const env_ptr_ty = self.module.types.ptrTo(.void);
const lambda_wants_ctx = self.implicit_ctx_enabled and lam.call_conv != .c;
if (lambda_wants_ctx) {
params.append(self.alloc, .{
.name = self.module.types.internString("__sx_ctx"),
.ty = env_ptr_ty,
}) catch unreachable;
}
params.append(self.alloc, .{
.name = self.module.types.internString("env"),
.ty = env_ptr_ty,
@@ -5508,6 +5716,19 @@ pub const Lowering = struct {
if (lam.call_conv == .c) {
self.module.getFunctionMut(func_id).call_conv = .c;
}
self.builder.currentFunc().has_implicit_ctx = lambda_wants_ctx;
// Param-slot layout: ctx at 0 (if present), env at ctx_slots,
// user args at ctx_slots+1.
const lambda_ctx_slots: u32 = if (lambda_wants_ctx) 1 else 0;
const env_param_idx: u32 = lambda_ctx_slots;
const user_param_base_lam: u32 = lambda_ctx_slots + 1;
// Save + rebind current_ctx_ref so the body's sx-to-sx calls
// forward the trampoline's own ctx (slot 0).
const saved_ctx_ref_lam = self.current_ctx_ref;
defer self.current_ctx_ref = saved_ctx_ref_lam;
if (lambda_wants_ctx) self.current_ctx_ref = Ref.fromIndex(0);
// Create entry block
const entry_name = self.module.types.internString("entry");
@@ -5518,9 +5739,9 @@ pub const Lowering = struct {
var lambda_scope = Scope.init(self.alloc, null);
self.scope = &lambda_scope;
// Bind captures from env struct (param 0)
// Bind captures from env struct (at env_param_idx)
if (capture_list.len > 0) {
const env_param_ref = @as(Ref, @enumFromInt(0));
const env_param_ref = Ref.fromIndex(env_param_idx);
// Alloca env struct locally so struct_gep can resolve the type
const env_local = self.builder.alloca(env_struct_ty);
// Compute env size
@@ -5555,11 +5776,11 @@ pub const Lowering = struct {
}
}
// Bind params
// Bind params (user args start at user_param_base_lam, shifted past ctx + env)
for (lam.params, 0..) |p, i| {
const pty = self.resolveParamType(&p);
const slot = self.builder.alloca(pty);
const param_ref = @as(Ref, @enumFromInt(i + 1)); // +1: env is param 0
const param_ref = Ref.fromIndex(user_param_base_lam + @as(u32, @intCast(i)));
self.builder.store(slot, param_ref);
lambda_scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
}
@@ -5586,9 +5807,10 @@ pub const Lowering = struct {
self.builder.current_block = saved_block;
self.builder.inst_counter = saved_counter;
// Create proper closure type (user-visible params only, no env)
// Create proper closure type (user-visible params only — skip ctx + env).
const skip_count: usize = if (lambda_wants_ctx) 2 else 1;
var param_types_list = std.ArrayList(TypeId).empty;
for (params.items[1..]) |p| { // skip env (index 0)
for (params.items[skip_count..]) |p| {
param_types_list.append(self.alloc, p.ty) catch unreachable;
}
const closure_ty = self.module.types.closureType(param_types_list.items, ret_ty);
@@ -5626,11 +5848,19 @@ pub const Lowering = struct {
/// The trampoline has signature `(env: *void, args...) -> ret` and simply calls the
/// bare function with `(args...)`, ignoring the env parameter.
fn createBareFnTrampoline(self: *Lowering, bare_func_id: FuncId, closure_info: types.TypeInfo.ClosureInfo) FuncId {
// Build trampoline params: env + closure params
// Build trampoline params: [__sx_ctx]? + env + closure params.
// When the program uses Context, every sx-side trampoline carries
// the implicit ctx at slot 0 and forwards it to the wrapped
// function (which is also sx-side and expects it at slot 0).
var params = std.ArrayList(inst_mod.Function.Param).empty;
defer params.deinit(self.alloc);
const void_ptr_ty = self.module.types.ptrTo(.void);
const wants_ctx = self.implicit_ctx_enabled;
if (wants_ctx) {
params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = void_ptr_ty }) catch unreachable;
}
const env_name = self.module.types.internString("env");
params.append(self.alloc, .{ .name = env_name, .ty = self.module.types.ptrTo(.void) }) catch unreachable;
params.append(self.alloc, .{ .name = env_name, .ty = void_ptr_ty }) catch unreachable;
for (closure_info.params, 0..) |pty, i| {
var buf: [32]u8 = undefined;
const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg";
@@ -5651,7 +5881,8 @@ pub const Lowering = struct {
// Create function
const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable;
const func = inst_mod.Function.init(tramp_name_id, owned_params, closure_info.ret);
var func = inst_mod.Function.init(tramp_name_id, owned_params, closure_info.ret);
func.has_implicit_ctx = wants_ctx;
const func_id = self.module.addFunction(func);
self.builder.func = func_id;
self.builder.inst_counter = @intCast(owned_params.len); // params occupy refs 0..N-1
@@ -5659,11 +5890,17 @@ pub const Lowering = struct {
const entry_block = self.builder.appendBlock(entry_name, &.{});
self.builder.switchToBlock(entry_block);
// Build call args: skip env (param 0), forward params 1..N
// Build call args: forward [__sx_ctx]? + user_params (skip env).
// Trampoline slots: 0=ctx (if present), {0|1}=env, then user args.
const ctx_slots: usize = if (wants_ctx) 1 else 0;
const user_arg_start: u32 = @intCast(ctx_slots + 1); // skip ctx + env
var call_args = std.ArrayList(Ref).empty;
defer call_args.deinit(self.alloc);
if (wants_ctx and bare_func.has_implicit_ctx) {
call_args.append(self.alloc, Ref.fromIndex(0)) catch unreachable; // forward our ctx
}
for (closure_info.params, 0..) |_, i| {
call_args.append(self.alloc, Ref.fromIndex(@intCast(i + 1))) catch unreachable;
call_args.append(self.alloc, Ref.fromIndex(user_arg_start + @as(u32, @intCast(i)))) catch unreachable;
}
const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable;
const result = self.builder.emit(.{ .call = .{ .callee = bare_func_id, .args = owned_args } }, closure_info.ret);
@@ -6117,7 +6354,12 @@ pub const Lowering = struct {
const func_id = self.createComptimeFunction("__ct", expr, ret_ty);
// Emit a call to the comptime function. At interpretation time,
// this will be evaluated and the result inlined as a constant.
return self.builder.call(func_id, &.{}, ret_ty);
const func = &self.module.functions.items[@intFromEnum(func_id)];
const final_args: []const Ref = if (func.has_implicit_ctx)
self.alloc.dupe(Ref, &.{self.current_ctx_ref}) catch &.{}
else
&.{};
return self.builder.call(func_id, final_args, ret_ty);
}
/// Lower a `#insert expr` statement. Evaluates `expr` at compile time to get
@@ -6671,8 +6913,9 @@ pub const Lowering = struct {
}
arg_idx += 1;
}
self.coerceCallArgs(value_args.items, params);
return self.builder.call(fid, value_args.items, ret_ty);
const final_args = self.prependCtxIfNeeded(func, value_args.items);
self.coerceCallArgs(final_args, params);
return self.builder.call(fid, final_args, ret_ty);
}
return self.emitError(base_name, call_node.callee.span);
@@ -6883,8 +7126,9 @@ pub const Lowering = struct {
}
}
}
self.coerceCallArgs(call_args.items, callee_params);
const result = self.builder.call(fid, call_args.items, callee_ret);
const final_args = self.prependCtxIfNeeded(func, call_args.items);
self.coerceCallArgs(final_args, callee_params);
const result = self.builder.call(fid, final_args, callee_ret);
if (result_slot) |slot| {
self.builder.store(slot, result);
}
@@ -6896,16 +7140,20 @@ pub const Lowering = struct {
self.lazyLowerFunction(resolve_name);
}
if (self.resolveFuncByName(resolve_name)) |fid| {
const callee_ret = self.module.functions.items[@intFromEnum(fid)].ret;
const callee_params = self.module.functions.items[@intFromEnum(fid)].params;
const callee_func = &self.module.functions.items[@intFromEnum(fid)];
const callee_ret = callee_func.ret;
const callee_params = callee_func.params;
const callee_has_ctx = callee_func.has_implicit_ctx;
var call_args = std.ArrayList(Ref).empty;
defer call_args.deinit(self.alloc);
for (fd.params, 0..) |_, pi| {
if (pi == cast_arg_idx) {
// Coerce unboxed value (typed as ty_id) to param type
var arg = unboxed;
if (pi < callee_params.len) {
arg = self.coerceToType(arg, ty_id, callee_params[pi].ty);
// callee param index shifts by +1 if it carries __sx_ctx
const callee_pi = pi + @as(usize, if (callee_has_ctx) 1 else 0);
if (callee_pi < callee_params.len) {
arg = self.coerceToType(arg, ty_id, callee_params[callee_pi].ty);
}
call_args.append(self.alloc, arg) catch unreachable;
} else if (pi < other_args.items.len) {
@@ -6914,13 +7162,25 @@ pub const Lowering = struct {
}
}
}
// Coerce non-cast args (source type unknown, use s64 default)
for (0..@min(call_args.items.len, callee_params.len)) |ci| {
if (ci != cast_arg_idx) {
call_args.items[ci] = self.coerceToType(call_args.items[ci], .s64, callee_params[ci].ty);
// Prepend __sx_ctx if needed BEFORE coercion so indices line up.
var final_call_args: []Ref = call_args.items;
if (callee_has_ctx) {
final_call_args = self.alloc.alloc(Ref, call_args.items.len + 1) catch call_args.items;
if (final_call_args.len == call_args.items.len + 1) {
final_call_args[0] = self.current_ctx_ref;
@memcpy(final_call_args[1..], call_args.items);
}
}
const result = self.builder.call(fid, call_args.items, callee_ret);
// Coerce non-cast args (source type unknown, use s64 default).
// cast_arg_idx is in user-space (skips __sx_ctx); offset by ctx_slots.
const ctx_slots: usize = if (callee_has_ctx) 1 else 0;
for (0..@min(final_call_args.len, callee_params.len)) |ci| {
if (ci < ctx_slots) continue; // skip __sx_ctx slot
if ((ci - ctx_slots) != cast_arg_idx) {
final_call_args[ci] = self.coerceToType(final_call_args[ci], .s64, callee_params[ci].ty);
}
}
const result = self.builder.call(fid, final_call_args, callee_ret);
if (result_slot) |slot| {
self.builder.store(slot, result);
}
@@ -6975,8 +7235,19 @@ pub const Lowering = struct {
const ret_ty = self.resolveReturnType(fd);
self.target_type = ret_ty;
// Build param list (substituting type params, skipping type param declarations)
const wants_ctx = self.funcWantsImplicitCtx(fd);
const saved_ctx_ref_mono = self.current_ctx_ref;
defer self.current_ctx_ref = saved_ctx_ref_mono;
// Build param list (substituting type params, skipping type param declarations).
// Prepend `__sx_ctx: *void` at slot 0 if the function gets the implicit param.
var params = std.ArrayList(Function.Param).empty;
if (wants_ctx) {
params.append(self.alloc, .{
.name = self.module.types.internString("__sx_ctx"),
.ty = self.module.types.ptrTo(.void),
}) catch unreachable;
}
for (fd.params) |p| {
if (isTypeParamDecl(&p, fd.type_params)) continue;
const pty = self.resolveParamType(&p);
@@ -6990,11 +7261,13 @@ pub const Lowering = struct {
const name_id = self.module.types.internString(owned_name);
const func_id = self.builder.beginFunction(name_id, params.items, ret_ty);
_ = func_id;
self.builder.currentFunc().has_implicit_ctx = wants_ctx;
// Create entry block
const entry_name = self.module.types.internString("entry");
const entry = self.builder.appendBlock(entry_name, &.{});
self.builder.switchToBlock(entry);
if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0);
// Create scope and bind params
var scope = Scope.init(self.alloc, null);
@@ -7002,7 +7275,7 @@ pub const Lowering = struct {
self.scope = &scope;
{
var param_idx: u32 = 0;
var param_idx: u32 = if (wants_ctx) 1 else 0;
for (fd.params) |p| {
if (isTypeParamDecl(&p, fd.type_params)) continue;
const pty = self.resolveParamType(&p);
@@ -7683,6 +7956,21 @@ pub const Lowering = struct {
/// Resolve parameter types for a call expression (for target_type context).
/// Returns empty slice if the function can't be resolved.
/// Return the param types of a Function from the caller's POV — i.e.
/// skipping the synthetic `__sx_ctx` slot when present. lowerCall's
/// arg-lowering uses these to set `target_type` per arg, and user
/// args don't include `__sx_ctx`, so the slot must be elided.
fn userParamTypes(self: *Lowering, func: *const Function) []TypeId {
const start: usize = if (func.has_implicit_ctx) 1 else 0;
var types_list = std.ArrayList(TypeId).empty;
if (func.params.len > start) {
for (func.params[start..]) |p| {
types_list.append(self.alloc, p.ty) catch unreachable;
}
}
return types_list.items;
}
fn resolveCallParamTypes(self: *Lowering, c: *const ast.Call) []const TypeId {
// Method calls: obj.method(args) — resolve param types from the method signature,
// skipping the first param (self) since it's prepended later.
@@ -7707,11 +7995,7 @@ pub const Lowering = struct {
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ obj_name, fa.field }) catch return &.{};
if (self.resolveFuncByName(qualified)) |fid| {
const func = &self.module.functions.items[@intFromEnum(fid)];
var types_list = std.ArrayList(TypeId).empty;
for (func.params) |p| {
types_list.append(self.alloc, p.ty) catch unreachable;
}
return types_list.items;
return self.userParamTypes(func);
}
if (self.fn_ast_map.get(qualified)) |fd| {
var types_list = std.ArrayList(TypeId).empty;
@@ -7762,10 +8046,12 @@ pub const Lowering = struct {
// Try already-lowered functions first
if (self.resolveFuncByName(qualified)) |fid| {
const func = &self.module.functions.items[@intFromEnum(fid)];
if (func.params.len > 0) {
// Skip self param — caller args don't include self
// Skip both `__sx_ctx` (if present) AND `self` param;
// caller args include neither.
const skip: usize = (if (func.has_implicit_ctx) @as(usize, 1) else 0) + 1;
if (func.params.len > skip) {
var types_list = std.ArrayList(TypeId).empty;
for (func.params[1..]) |p| {
for (func.params[skip..]) |p| {
types_list.append(self.alloc, p.ty) catch unreachable;
}
return types_list.items;
@@ -7817,12 +8103,7 @@ pub const Lowering = struct {
// Check declared functions
if (self.resolveFuncByName(name)) |fid| {
const func = &self.module.functions.items[@intFromEnum(fid)];
// Return param types (allocated as slice of TypeId)
var types_list = std.ArrayList(TypeId).empty;
for (func.params) |p| {
types_list.append(self.alloc, p.ty) catch unreachable;
}
return types_list.items;
return self.userParamTypes(func);
}
// Check AST map for function signatures
@@ -7888,18 +8169,39 @@ pub const Lowering = struct {
const saved_block = self.builder.current_block;
const saved_counter = self.builder.inst_counter;
const saved_scope = self.scope;
const saved_ctx_ref = self.current_ctx_ref;
defer self.current_ctx_ref = saved_ctx_ref;
// Create the comptime function (no params, returns ret_ty)
// Build params: implicit `__sx_ctx` at slot 0 when the program
// uses Context (so the body's `context.X` reads + transitive calls
// resolve cleanly). The comptime function's top-level invocation
// supplies `&__sx_default_context` (interp via callWithDefaultContext;
// codegen via the comptime-eval glue in emit_llvm).
const wants_ctx = self.implicit_ctx_enabled;
const params_slice = blk: {
if (!wants_ctx) break :blk &[_]Function.Param{};
const owned = self.alloc.alloc(Function.Param, 1) catch break :blk &[_]Function.Param{};
owned[0] = .{
.name = self.module.types.internString("__sx_ctx"),
.ty = self.module.types.ptrTo(.void),
};
break :blk owned;
};
// Create the comptime function
const name_id = self.module.types.internString(name);
const func_id = self.builder.beginFunction(name_id, &.{}, ret_ty);
const func_id = self.builder.beginFunction(name_id, params_slice, ret_ty);
// Mark as comptime
self.module.getFunctionMut(func_id).is_comptime = true;
// Mark as comptime + has_implicit_ctx
const fn_mut = self.module.getFunctionMut(func_id);
fn_mut.is_comptime = true;
fn_mut.has_implicit_ctx = wants_ctx;
// Create entry block
const entry_name = self.module.types.internString("entry");
const entry = self.builder.appendBlock(entry_name, &.{});
self.builder.switchToBlock(entry);
if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0);
// Create a scope that chains to the enclosing scope (so the
// expression can reference names visible at the #run site).
@@ -9078,10 +9380,17 @@ pub const Lowering = struct {
/// Create a thunk function: __thunk_ConcreteType_Protocol_method(ctx: *void, args...) -> ret
/// The thunk calls ConcreteType.method(ctx, args...).
fn createProtocolThunk(self: *Lowering, proto_name: []const u8, concrete_type_name: []const u8, method: ProtocolMethodInfo) FuncId {
// Build params: ctx: *void + method params
// Build params: [__sx_ctx]? + ctx: *void + method params.
// Thunks are sx-side functions, so they get the implicit __sx_ctx
// at slot 0 when it's enabled program-wide. The concrete protocol
// receiver (ctx) follows at slot 1; user method args at slot 2+.
var params = std.ArrayList(inst_mod.Function.Param).empty;
defer params.deinit(self.alloc);
const void_ptr = self.module.types.ptrTo(.void);
const thunk_has_ctx = self.implicit_ctx_enabled;
if (thunk_has_ctx) {
params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = void_ptr }) catch unreachable;
}
params.append(self.alloc, .{ .name = self.module.types.internString("ctx"), .ty = void_ptr }) catch unreachable;
for (method.param_types, 0..) |pty, i| {
var buf: [32]u8 = undefined;
@@ -9098,12 +9407,16 @@ pub const Lowering = struct {
const saved_func = self.builder.func;
const saved_block = self.builder.current_block;
const saved_counter = self.builder.inst_counter;
const saved_ctx_ref_thunk = self.current_ctx_ref;
defer self.current_ctx_ref = saved_ctx_ref_thunk;
const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable;
const func = inst_mod.Function.init(thunk_name_id, owned_params, method.ret_type);
var func = inst_mod.Function.init(thunk_name_id, owned_params, method.ret_type);
func.has_implicit_ctx = thunk_has_ctx;
const func_id = self.module.addFunction(func);
self.builder.func = func_id;
self.builder.inst_counter = @intCast(owned_params.len);
if (thunk_has_ctx) self.current_ctx_ref = Ref.fromIndex(0);
const entry_block = self.builder.appendBlock(self.module.types.internString("entry"), &.{});
self.builder.switchToBlock(entry_block);
@@ -9113,16 +9426,30 @@ pub const Lowering = struct {
self.lazyLowerFunction(qualified);
}
// Call the concrete method: ConcreteType.method(ctx, args...)
// Call the concrete method: ConcreteType.method(__sx_ctx?, ctx, args...).
// The concrete method is itself an sx function that takes the
// implicit __sx_ctx at slot 0 (when implicit_ctx is enabled); we
// forward the thunk's own __sx_ctx.
if (self.resolveFuncByName(qualified)) |concrete_fid| {
const concrete_func = &self.module.functions.items[@intFromEnum(concrete_fid)];
var call_args = std.ArrayList(Ref).empty;
defer call_args.deinit(self.alloc);
// Pass ctx (ref 0) as first arg (it's the concrete *Type disguised as *void)
// If the concrete method expects a value (e.g., f32) not a pointer, load from ctx
const ctx_ref = Ref.fromIndex(0);
if (concrete_func.params.len > 0) {
const first_concrete_ty = concrete_func.params[0].ty;
// Slot offsets inside the thunk: __sx_ctx at 0 (if present),
// protocol receiver (ctx) at slot user_base, user args at +1, +2...
const user_base: u32 = if (thunk_has_ctx) 1 else 0;
// Forward our __sx_ctx to the concrete method's __sx_ctx slot.
if (concrete_func.has_implicit_ctx) {
call_args.append(self.alloc, self.current_ctx_ref) catch unreachable;
}
// Pass ctx as the next arg (it's the concrete *Type disguised as *void).
// If the concrete method expects a value (e.g., f32) not a pointer, load from ctx.
const ctx_ref = Ref.fromIndex(user_base);
const concrete_receiver_idx: usize = if (concrete_func.has_implicit_ctx) 1 else 0;
if (concrete_receiver_idx < concrete_func.params.len) {
const first_concrete_ty = concrete_func.params[concrete_receiver_idx].ty;
const first_info = self.module.types.get(first_concrete_ty);
if (first_info != .pointer) {
// Concrete expects value — load from ctx pointer
@@ -9134,10 +9461,10 @@ pub const Lowering = struct {
call_args.append(self.alloc, ctx_ref) catch unreachable;
}
for (method.param_types, 0..) |proto_pty, i| {
var arg_ref = Ref.fromIndex(@intCast(i + 1));
var arg_ref = Ref.fromIndex(@intCast(user_base + 1 + i));
// If protocol param is a pointer (Self→*void) but concrete method
// expects a value type, load the value from the pointer.
const concrete_idx = i + 1; // +1 for self/ctx
const concrete_idx = concrete_receiver_idx + 1 + i;
if (concrete_idx < concrete_func.params.len) {
const concrete_pty = concrete_func.params[concrete_idx].ty;
const proto_info = self.module.types.get(proto_pty);
@@ -9288,12 +9615,16 @@ pub const Lowering = struct {
};
_ = proto_ty;
// Build call args: ctx + user args
// Protocol method params use *void for Self-typed params. If the caller passes
// a struct value, we need to alloca+store and pass the pointer instead.
// Also coerce argument types to match declared param types (e.g., s64 → s32).
// Build call args: [__sx_ctx]? + receiver_ctx + user args.
// Protocol thunks are sx-side, so they carry the implicit __sx_ctx
// at slot 0 when the program uses Context — forward our caller's
// ctx so the thunk's body (and the concrete method it forwards to)
// sees the same Context as the dispatching code.
var call_args = std.ArrayList(Ref).empty;
defer call_args.deinit(self.alloc);
if (self.implicit_ctx_enabled) {
call_args.append(self.alloc, self.current_ctx_ref) catch unreachable;
}
call_args.append(self.alloc, ctx) catch unreachable;
for (args, 0..) |a, i| {
const expected_ty = if (i < mi.param_types.len) mi.param_types[i] else void_ptr;
@@ -9901,9 +10232,10 @@ pub const Lowering = struct {
const func = &self.module.functions.items[@intFromEnum(fid)];
const ret_ty = func.ret;
const params = func.params;
var args = [_]Ref{operand};
self.coerceCallArgs(args[0..], params);
return self.builder.call(fid, args[0..], ret_ty);
var single = [_]Ref{operand};
const final_args = self.prependCtxIfNeeded(func, single[0..]);
self.coerceCallArgs(final_args, params);
return self.builder.call(fid, final_args, ret_ty);
}
/// Build a protocol value from a concrete value via xx conversion.

View File

@@ -34,6 +34,11 @@ pub const Module = struct {
/// `lookupObjcSelector` / `appendObjcSelector` to read/write it.
objc_selector_cache: std.ArrayList(ObjcSelectorEntry),
alloc: Allocator,
/// True when this module's program imports `std.sx` (and therefore
/// has the `Context` type). Set by lowering's Pass 0 pre-scan. Read
/// by emit_llvm to decide whether closure/fn-pointer call sites
/// need `__sx_ctx` prepended to their LLVM args/types.
has_implicit_ctx: bool = false,
pub const ObjcSelectorEntry = struct { sel: []const u8, slot: GlobalId };