const std = @import("std"); const Allocator = std.mem.Allocator; const ast = @import("../../ast.zig"); const Node = ast.Node; const types = @import("../types.zig"); const inst_mod = @import("../inst.zig"); const program_index_mod = @import("../program_index.zig"); const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo; const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo; const ProtocolResolver = @import("../protocols.zig").ProtocolResolver; const TypeId = types.TypeId; const Ref = inst_mod.Ref; const FuncId = inst_mod.FuncId; const Function = inst_mod.Function; const lower = @import("../lower.zig"); const Lowering = lower.Lowering; /// Shared implementation for the `has_impl(P, T)` builtin and its /// `tryConstBoolCondition` arm. The protocol expression is either: /// - Plain `Hash` (identifier / type_expr) → walks /// `protocol_thunk_map["Hash\x00"]`. /// - Parameterised `Into(Block)` (call) → walks `param_impl_map` /// keyed by `"

\x00\x00"`. /// Returns false on any malformed protocol-arg shape (caller /// reports a diagnostic if it wants). pub fn computeHasImpl(self: *Lowering, proto_node: *const Node, ty: TypeId) bool { switch (proto_node.data) { .identifier => |id| return self.protocolResolver().hasImplPlain(id.name, ty), .type_expr => |te| return self.protocolResolver().hasImplPlain(te.name, ty), .call => |c| { const p_name: []const u8 = switch (c.callee.data) { .identifier => |id| id.name, .type_expr => |te| te.name, else => return false, }; // Resolve protocol type args. Each goes through // `resolveTypeArg` so type aliases / generics / pack- // indexed types all work as protocol args. var arg_mangles = std.ArrayList(u8).empty; defer arg_mangles.deinit(self.alloc); for (c.args, 0..) |a, i| { if (i > 0) arg_mangles.append(self.alloc, 0) catch return false; const aty = self.resolveTypeArg(a); arg_mangles.appendSlice(self.alloc, self.mangleTypeName(aty)) catch return false; } const ty_mangled = self.mangleTypeName(ty); const key = std.fmt.allocPrint(self.alloc, "{s}\x00{s}\x00{s}", .{ p_name, arg_mangles.items, ty_mangled, }) catch return false; return self.param_impl_map.contains(key); }, else => return false, } } /// Register a protocol declaration as a struct type in the IR type table. /// Inline protocols: { ctx: *void, method1: *void, method2: *void, ... } /// Non-inline protocols: { ctx: *void, __vtable: *void } /// Also stores protocol info for dispatch and vtable struct type for vtable protocols. /// Register a protocol declaration. Thin delegation to the canonical owner /// (`ProtocolResolver`, `protocols.zig`); kept on `Lowering` as a `pub` /// entry point because the scan pass + several unit tests reach it here. pub fn registerProtocolDecl(self: *Lowering, pd: *const ast.ProtocolDecl) void { return self.protocolResolver().registerProtocolDecl(pd); } /// Instantiate a parameterized protocol as a runtime VALUE type: /// `VL(s64)` → a 16-byte `{ctx, __vtable}` protocol value (`is_protocol`), /// with method infos resolved under the type-arg binding (so `get -> T` /// becomes `get -> s64`) and the binding recorded for projection. Cached by /// the mangled name `VL__s64`. Mirrors the non-parameterized path in /// `registerProtocolDecl`. pub fn instantiateParamProtocol(self: *Lowering, pd: *const ast.ProtocolDecl, args: []const *const Node) TypeId { const table = &self.module.types; const void_ptr_ty = table.ptrTo(.void); var np = std.ArrayList(u8).empty; np.appendSlice(self.alloc, pd.name) catch {}; var tb = std.StringHashMap(TypeId).init(self.alloc); for (pd.type_params, 0..) |tp, i| { if (i >= args.len) break; const ty = self.resolveTypeWithBindings(args[i]); tb.put(tp.name, ty) catch {}; np.appendSlice(self.alloc, "__") catch {}; np.appendSlice(self.alloc, self.formatTypeName(ty)) catch {}; } const mangled = np.items; const name_id = table.internString(mangled); if (table.findByName(name_id)) |existing| { const info = table.get(existing); if (info == .@"struct" and info.@"struct".is_protocol) return existing; } // Value struct: {ctx, __vtable} (or ctx + fn-ptrs for an inline protocol). var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; fields.append(self.alloc, .{ .name = table.internString("ctx"), .ty = void_ptr_ty }) catch unreachable; if (pd.is_inline) { for (pd.methods) |m| fields.append(self.alloc, .{ .name = table.internString(m.name), .ty = void_ptr_ty }) catch unreachable; } else { fields.append(self.alloc, .{ .name = table.internString("__vtable"), .ty = void_ptr_ty }) catch unreachable; } const struct_info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items, .is_protocol = true } }; const id = if (table.findByName(name_id)) |existing| existing else table.intern(struct_info); table.updatePreservingKey(id, struct_info); // Method infos resolved with the type-arg binding (T → s64), pinned to // the protocol's OWN module (E4) so a method-signature type visible only // there resolves correctly when instantiated cross-module. `Self` and the // bound type-args short-circuit before the leaf; a concrete library type // in a signature is the case this pin protects. const saved_tb = self.type_bindings; self.type_bindings = tb; const saved_pp_src = self.current_source_file; defer self.setCurrentSourceFile(saved_pp_src); if (pd.source_file) |src| self.setCurrentSourceFile(src); var method_infos = std.ArrayList(ProtocolMethodInfo).empty; for (pd.methods) |method| { var ptypes = std.ArrayList(TypeId).empty; for (method.params) |p| { const pty = blk: { if (p.data == .type_expr and std.mem.eql(u8, p.data.type_expr.name, "Self")) break :blk void_ptr_ty; break :blk self.resolveTypeWithBindings(p); }; ptypes.append(self.alloc, pty) catch unreachable; } var ret_is_self = false; const ret = if (method.return_type) |rt| blk: { if (rt.data == .type_expr and std.mem.eql(u8, rt.data.type_expr.name, "Self")) { ret_is_self = true; break :blk void_ptr_ty; } break :blk self.resolveTypeWithBindings(rt); } else .void; method_infos.append(self.alloc, .{ .name = method.name, .param_types = self.alloc.dupe(TypeId, ptypes.items) catch unreachable, .ret_type = ret, .ret_is_self = ret_is_self, }) catch unreachable; } self.type_bindings = saved_tb; const owned = self.alloc.dupe(u8, mangled) catch return id; self.program_index.protocol_decl_map.put(owned, .{ .name = owned, .is_inline = pd.is_inline, .methods = self.alloc.dupe(ProtocolMethodInfo, method_infos.items) catch unreachable, }) catch {}; // Record the type-arg binding so projection (`xs.T`, `.value`) and // method-arg resolution on this instance can recover it. self.struct_instance_bindings.put(owned, tb) catch {}; if (!pd.is_inline) { var vtable_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; for (pd.methods) |m| vtable_fields.append(self.alloc, .{ .name = table.internString(m.name), .ty = void_ptr_ty }) catch unreachable; var vtable_name_buf: [192]u8 = undefined; const vtable_name = std.fmt.bufPrint(&vtable_name_buf, "__{s}__Vtable", .{mangled}) catch "__Vtable"; const vtable_ty = table.intern(.{ .@"struct" = .{ .name = table.internString(vtable_name), .fields = vtable_fields.items } }); self.protocol_vtable_type_map.put(owned, vtable_ty) catch {}; } return id; } // ── Protocol namespace lookups (pack projection, Feature 1, Decision 4) ── // (The position-driven orchestrator `resolvePackProjection` lives in // lower/pack.zig; these two lookups are its per-namespace halves.) // // A `..pack.` projection can target two protocol namespaces: // - type-arg namespace: the `protocol($T, ...)` params. // - runtime-accessor namespace: the protocol's methods (protocols have // no fields; a zero-arg method like `value` is the accessor). // Resolution is POSITION-driven, not precedence-driven: type position // consults type-args, value position consults methods, with NO // cross-namespace fallback. /// Find `name` in `protocol_name`'s type-arg namespace (`protocol($T,...)`). /// Returns the `type_params` index, or null (also for unknown protocols). pub fn lookupProtocolArg(self: *Lowering, protocol_name: []const u8, name: []const u8) ?u32 { const pd = self.program_index.protocol_ast_map.get(protocol_name) orelse return null; for (pd.type_params, 0..) |tp, i| { if (std.mem.eql(u8, tp.name, name)) return @intCast(i); } return null; } /// Find `name` in `protocol_name`'s runtime-accessor namespace (its methods /// — protocols have no fields). Returns the `methods` index, or null. pub fn lookupProtocolField(self: *Lowering, protocol_name: []const u8, name: []const u8) ?u32 { const pd = self.program_index.protocol_ast_map.get(protocol_name) orelse return null; for (pd.methods, 0..) |m, i| { if (std.mem.eql(u8, m.name, name)) return @intCast(i); } return null; } /// Check if a type name is a registered protocol. pub fn isProtocolType(self: *Lowering, type_name: []const u8) bool { return self.program_index.protocol_decl_map.contains(type_name); } /// Get protocol info for a TypeId (if it's a protocol type). /// Protocol lookup. Thin delegation to the canonical owner /// (`ProtocolResolver`, `protocols.zig`); kept on `Lowering` because ~9 /// callers (dispatch sites here + `calls.zig`) reach it. pub fn getProtocolInfo(self: *Lowering, ty: TypeId) ?ProtocolDeclInfo { return self.protocolResolver().getProtocolInfo(ty); } /// Get or create thunks for a (protocol, concrete_type) pair. /// Returns a slice of FuncIds, one per protocol method. pub fn getOrCreateThunks(self: *Lowering, proto_name: []const u8, concrete_type_name: []const u8) []const FuncId { // Key: "Proto\x00Type" const key = std.fmt.allocPrint(self.alloc, "{s}\x00{s}", .{ proto_name, concrete_type_name }) catch return &.{}; if (self.protocol_thunk_map.get(key)) |thunks| return thunks; // PLANNING: which methods need a thunk (owned by the registry). const methods = self.protocolResolver().protocolMethodInfos(proto_name) orelse return &.{}; var thunk_ids = std.ArrayList(FuncId).empty; defer thunk_ids.deinit(self.alloc); // EMISSION: materialize one thunk per method (stays in Lowering). for (methods) |method| { const thunk_id = self.createProtocolThunk(proto_name, concrete_type_name, method); thunk_ids.append(self.alloc, thunk_id) catch unreachable; } const owned = self.alloc.dupe(FuncId, thunk_ids.items) catch unreachable; self.protocol_thunk_map.put(key, owned) catch {}; return owned; } /// Emit the process-wide default Context as an LLVM static constant. /// /// @__sx_default_context = internal constant %Context { /// %Allocator { ptr null, /// ptr @__thunk_CAllocator_Allocator_alloc, /// ptr @__thunk_CAllocator_Allocator_dealloc }, /// ptr null /// } /// /// Used by FFI inbound wrappers (Step 4) and the interp's default- /// context call entry (Step 7). Only emitted when the program imports /// `std.sx` — without that, Context / Allocator / CAllocator aren't /// registered and the global has no purpose. pub fn emitDefaultContextGlobal(self: *Lowering) void { const saved_edc = self.emitting_default_context; self.emitting_default_context = true; defer self.emitting_default_context = saved_edc; const tbl = &self.module.types; const ctx_name_id = tbl.internString("Context"); const ctx_ty = tbl.findByName(ctx_name_id) orelse return; if (tbl.findByName(tbl.internString("Allocator")) == null) return; if (tbl.findByName(tbl.internString("CAllocator")) == null) return; // Force the CAllocator → Allocator thunks to exist so we can // reference them by FuncId in the static initializer. const thunks = self.getOrCreateThunks("Allocator", "CAllocator"); if (thunks.len < 2) return; // Inline Allocator value: { ctx: *void, alloc_fn: *void, dealloc_fn: *void } // CAllocator is stateless, so ctx is null. const alloc_fields = self.alloc.alloc(inst_mod.ConstantValue, 3) catch return; alloc_fields[0] = .null_val; alloc_fields[1] = .{ .func_ref = thunks[0] }; alloc_fields[2] = .{ .func_ref = thunks[1] }; // Context value: { allocator: Allocator, data: *void } const ctx_fields = self.alloc.alloc(inst_mod.ConstantValue, 2) catch return; ctx_fields[0] = .{ .aggregate = alloc_fields }; ctx_fields[1] = .null_val; const global_name = "__sx_default_context"; const global_name_id = tbl.internString(global_name); const gid = self.module.addGlobal(.{ .name = global_name_id, .ty = ctx_ty, .init_val = .{ .aggregate = ctx_fields }, .is_const = true, }); self.putGlobal(self.current_source_file, global_name, .{ .id = gid, .ty = ctx_ty }); } /// Create a thunk function: __thunk_ConcreteType_Protocol_method(ctx: *void, args...) -> ret /// The thunk calls ConcreteType.method(ctx, args...). pub fn createProtocolThunk(self: *Lowering, proto_name: []const u8, concrete_type_name: []const u8, method: ProtocolMethodInfo) FuncId { // 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; const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg"; params.append(self.alloc, .{ .name = self.module.types.internString(pname), .ty = pty }) catch unreachable; } // Generate unique name var name_buf: [192]u8 = undefined; const thunk_name = std.fmt.bufPrint(&name_buf, "__thunk_{s}_{s}_{s}", .{ concrete_type_name, proto_name, method.name }) catch "__thunk"; const thunk_name_id = self.module.types.internString(thunk_name); // Save builder state 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; 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); // Ensure the concrete method is lowered const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ concrete_type_name, method.name }) catch method.name; if (!self.lowered_functions.contains(qualified)) { if (self.program_index.fn_ast_map.contains(qualified)) { self.lazyLowerFunction(qualified); } else if (self.genericInstanceMethod(concrete_type_name, method.name)) |gm| { // Generic-struct instance (`Combined__s64_s64`): the impl method is // authored on the instance's STAMPED decl (CP-4). Monomorphize it // for this instance's bindings so the thunk has a concrete // `Combined__s64_s64.get` to call. self.monomorphizeFunction(gm.fd, qualified, gm.bindings); } } // 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); // 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 call_args.append(self.alloc, self.builder.load(ctx_ref, first_concrete_ty)) catch unreachable; } else { call_args.append(self.alloc, ctx_ref) catch unreachable; } } else { call_args.append(self.alloc, ctx_ref) catch unreachable; } for (method.param_types, 0..) |proto_pty, i| { 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 = 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); const concrete_info = self.module.types.get(concrete_pty); if (proto_info == .pointer and concrete_info != .pointer) { arg_ref = self.builder.load(arg_ref, concrete_pty); } } call_args.append(self.alloc, arg_ref) catch unreachable; } const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable; const concrete_ret = concrete_func.ret; const result = self.builder.call(concrete_fid, owned_args, concrete_ret); if (method.ret_type != .void) { // If protocol returns *void (Self) but concrete returns a value type, // box the value: alloca+store and return the pointer const ret_info = self.module.types.get(method.ret_type); const concrete_ret_info = self.module.types.get(concrete_ret); if (ret_info == .pointer and concrete_ret_info != .pointer) { const slot = self.builder.alloca(concrete_ret); self.builder.store(slot, result); self.builder.ret(slot, method.ret_type); } else { self.builder.ret(result, method.ret_type); } } else { self.builder.retVoid(); } } else { // Can't resolve concrete method — emit unreachable _ = self.builder.emit(.{ .@"unreachable" = {} }, .void); } self.builder.finalize(); // Restore builder state self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; return func_id; } /// Build a protocol value from a concrete pointer. /// For inline protocols: struct_init { ctx, thunk1, thunk2, ... } /// For vtable protocols: struct_init { ctx, vtable_ptr } where vtable is stack-allocated /// When `heap_copy` is true, the concrete data is heap-copied so the protocol value /// outlives the current stack frame (used when source is a value, not an explicit pointer). /// When false, the pointer is used directly (user manages the pointee's lifetime). pub fn buildProtocolValue(self: *Lowering, concrete_ptr: Ref, proto_name: []const u8, concrete_type_name: []const u8, proto_ty: TypeId, concrete_ty: TypeId, heap_copy: bool) Ref { const pd = self.program_index.protocol_decl_map.get(proto_name) orelse return concrete_ptr; const thunks = self.getOrCreateThunks(proto_name, concrete_type_name); if (thunks.len != pd.methods.len) return concrete_ptr; const void_ptr_ty = self.module.types.ptrTo(.void); // When source is a value (not an explicit pointer), heap-allocate // so the protocol value outlives the current stack frame. // When source is an explicit pointer (xx @obj), use it directly — // the user is responsible for the pointee's lifetime. var ctx_ptr = concrete_ptr; if (heap_copy) { const concrete_size = self.module.types.typeSizeBytes(concrete_ty); const size_ref = self.builder.constInt(@intCast(concrete_size), .s64); const heap_ptr = self.allocViaContext(size_ref, void_ptr_ty); _ = self.callForeign("memcpy", &.{ heap_ptr, concrete_ptr, size_ref }, void_ptr_ty); ctx_ptr = heap_ptr; } if (pd.is_inline) { // Inline: { ctx, fn1, fn2, ... } var field_vals = std.ArrayList(Ref).empty; defer field_vals.deinit(self.alloc); field_vals.append(self.alloc, ctx_ptr) catch unreachable; for (thunks) |thunk_id| { const fn_ref = self.builder.emit(.{ .func_ref = thunk_id }, void_ptr_ty); field_vals.append(self.alloc, fn_ref) catch unreachable; } const owned = self.alloc.dupe(Ref, field_vals.items) catch unreachable; return self.builder.emit(.{ .struct_init = .{ .fields = owned } }, proto_ty); } else { // Vtable: { ctx, vtable_ptr } // Vtable is a global constant (same function pointers for every instance // of the same Protocol+ConcreteType pair). Cached per pair. const vtable_ty = self.protocol_vtable_type_map.get(proto_name) orelse return concrete_ptr; // Build cache key: "Proto\x00Type" const key = std.fmt.allocPrint(self.alloc, "{s}\x00{s}", .{ proto_name, concrete_type_name }) catch unreachable; const vtable_global_id = self.protocol_vtable_global_map.get(key) orelse blk: { // Create vtable global with function pointer initializer const global_name = std.fmt.allocPrint(self.alloc, "__{s}__{s}__vtable", .{ proto_name, concrete_type_name }) catch unreachable; const global_name_id = self.module.types.strings.intern(self.alloc, global_name); const thunk_ids = self.alloc.dupe(FuncId, thunks) catch unreachable; const gid = self.module.addGlobal(.{ .name = global_name_id, .ty = vtable_ty, .init_val = .{ .vtable = thunk_ids }, .is_const = true, }); self.protocol_vtable_global_map.put(key, gid) catch {}; break :blk gid; }; // Reference the vtable global's address const vtable_ptr_ty = self.module.types.ptrTo(vtable_ty); const vtable_addr = self.builder.emit(.{ .global_addr = vtable_global_id }, vtable_ptr_ty); // Build protocol struct: { ctx, &vtable } var proto_fields = std.ArrayList(Ref).empty; defer proto_fields.deinit(self.alloc); proto_fields.append(self.alloc, ctx_ptr) catch unreachable; proto_fields.append(self.alloc, vtable_addr) catch unreachable; const proto_owned = self.alloc.dupe(Ref, proto_fields.items) catch unreachable; return self.builder.emit(.{ .struct_init = .{ .fields = proto_owned } }, proto_ty); } } /// Emit protocol method dispatch for a protocol-typed receiver. /// Returns the call result ref. pub fn emitProtocolDispatch(self: *Lowering, receiver: Ref, proto_info: ProtocolDeclInfo, method_name: []const u8, args: []const Ref, proto_ty: TypeId) Ref { // Find method index var method_idx: ?usize = null; var method_info: ?ProtocolMethodInfo = null; for (proto_info.methods, 0..) |m, i| { if (std.mem.eql(u8, m.name, method_name)) { method_idx = i; method_info = m; break; } } const mi = method_info orelse return self.emitError(method_name, null); const midx = method_idx orelse 0; // Extract ctx from protocol struct (field 0) const void_ptr = self.module.types.ptrTo(.void); const ctx = self.builder.structGet(receiver, 0, void_ptr); // Extract fn_ptr const fn_ptr = if (proto_info.is_inline) blk: { // Inline: fn_ptr at field 1+method_idx break :blk self.builder.structGet(receiver, @intCast(1 + midx), void_ptr); } else blk: { // Vtable: load vtable struct, extract fn_ptr at method_idx const vtable_ptr = self.builder.structGet(receiver, 1, void_ptr); const vtable_ty = self.protocol_vtable_type_map.get(proto_info.name) orelse return self.emitError("vtable", null); const vtable = self.builder.emit(.{ .deref = .{ .operand = vtable_ptr } }, vtable_ty); break :blk self.builder.structGet(vtable, @intCast(midx), void_ptr); }; _ = proto_ty; // 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; const arg_ty = self.builder.getRefType(a); // Untargeted `null` lowers as const_null with type .void. Re-emit it // as a null of the expected pointer type instead of alloca'ing void. if (arg_ty == .void and expected_ty == void_ptr) { call_args.append(self.alloc, self.builder.constNull(void_ptr)) catch unreachable; continue; } // A protocol method that expects `*void` accepts any single-pointer // value directly (`*T`, `[*]T`). Only wrap non-pointer values in an // alloca-slot — wrapping a pointer would pass the stack slot's // address instead of the actual pointer, and the callee would read // 8 bytes of pointer plus garbage from beyond the stack. const is_pointer_ty = if (!arg_ty.isBuiltin()) blk: { const info = self.module.types.get(arg_ty); break :blk info == .pointer or info == .many_pointer; } else false; if (expected_ty == void_ptr and arg_ty != void_ptr and !is_pointer_ty) { const slot = self.builder.alloca(arg_ty); self.builder.store(slot, a); call_args.append(self.alloc, slot) catch unreachable; } else { // Coerce to match declared parameter type (critical for WASM strict signatures) const coerced = self.coerceToType(a, arg_ty, expected_ty); call_args.append(self.alloc, coerced) catch unreachable; } } const owned = self.alloc.dupe(Ref, call_args.items) catch unreachable; const raw_result = self.builder.emit(.{ .call_indirect = .{ .callee = fn_ptr, .args = owned } }, mi.ret_type); // If the protocol method was declared `-> Self` (encoded here as *void) // and the caller expects a value type, unbox: load the concrete value // from the returned pointer. A literal `-> *void` return is NOT // auto-loaded — it's a real pointer whose pointee size we don't know. if (mi.ret_is_self) { if (self.target_type) |target| { const target_info = self.module.types.get(target); if (target_info != .pointer) { return self.builder.load(raw_result, target); } } } return raw_result; } /// Resolve the concrete type name for protocol erasure. /// Handles both direct types and pointer-to-types. pub fn resolveConcreteTypeName(self: *Lowering, ty: TypeId) ?[]const u8 { if (ty.isBuiltin()) { // Primitive types like s64 — check if they have toName() return self.module.types.typeName(ty); } const info = self.module.types.get(ty); if (info == .pointer) { // *ConcreteType → resolve pointee const pointee = info.pointer.pointee; if (pointee.isBuiltin()) return self.module.types.typeName(pointee); const pi = self.module.types.get(pointee); if (pi == .@"struct") return self.module.types.getString(pi.@"struct".name); return null; } if (info == .@"struct") return self.module.types.getString(info.@"struct".name); return null; }