From 8990bd4978791dd7d2e5466b5294b6a3dad407ca Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 10 Jun 2026 13:54:59 +0300 Subject: [PATCH] refactor(B5.1): move protocol emission to lower/protocol.zig Verbatim relocation of the 13-method protocol cluster (protocol decl registration, param-protocol instantiation, thunk creation, vtable globals, protocol-value construction, dispatch emission, impl lookup) into src/ir/lower/protocol.zig. 13 fn aliases on Lowering keep all call sites unchanged. Two pub nested types travelled with the run (ProjectionPosition, PackProjection) and are re-exposed via Lowering type aliases; they are pack-domain types and may relocate to lower/pack.zig in B7.2. Method pub-flips: allocViaContext, callForeign, genericInstanceMethod, monomorphizeFunction. Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero expected/ snapshot churn. --- src/ir/lower.zig | 623 ++---------------------------------- src/ir/lower/protocol.zig | 646 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 668 insertions(+), 601 deletions(-) create mode 100644 src/ir/lower/protocol.zig diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 20e43af..38e1c3c 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -39,6 +39,7 @@ const lower_stmt = @import("lower/stmt.zig"); const lower_control_flow = @import("lower/control_flow.zig"); const lower_decl = @import("lower/decl.zig"); const lower_nominal = @import("lower/nominal.zig"); +const lower_protocol = @import("lower/protocol.zig"); const TypeId = types.TypeId; const StringId = types.StringId; @@ -1413,44 +1414,6 @@ pub const Lowering = struct { // ── Control flow ──────────────────────────────────────────────── - /// 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, - } - } - fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, span: ast.Span) Ref { // Check for tagged enum construction: .Variant.{ payload_fields } // This happens when type_expr is an enum_literal and target_type is a union @@ -4838,7 +4801,7 @@ pub const Lowering = struct { /// NOT fall back to a direct libc malloc — that was the silent escape /// hatch that bit us through the implicit-context refactor (see the /// "Silent unimplemented arms" REJECTED PATTERN in CLAUDE.md). - fn allocViaContext(self: *Lowering, size_ref: Ref, void_ptr_ty: TypeId) Ref { + pub fn allocViaContext(self: *Lowering, size_ref: Ref, void_ptr_ty: TypeId) Ref { if (!self.implicit_ctx_enabled or self.current_ctx_ref == Ref.none) { return self.diagnoseMissingContext("heap allocation"); } @@ -4874,7 +4837,7 @@ pub const Lowering = struct { /// Used for the compiler-internal byte-copy in the protocol-erasure /// heap path and the closure env-copy path, both of which need /// libc `memcpy` after the `#builtin` form was dropped. - fn callForeign(self: *Lowering, name: []const u8, args: []const Ref, ret_ty: TypeId) Ref { + pub fn callForeign(self: *Lowering, name: []const u8, args: []const Ref, ret_ty: TypeId) Ref { const fid = self.resolveFuncByName(name) orelse @panic("foreign symbol missing — std.sx not imported?"); return self.builder.call(fid, args, ret_ty); } @@ -6845,7 +6808,7 @@ pub const Lowering = struct { self.builder.finalize(); } - fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name: []const u8, bindings: *std.StringHashMap(TypeId)) void { + pub fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name: []const u8, bindings: *std.StringHashMap(TypeId)) void { // Mark as lowered before lowering (prevents infinite recursion) // Need to dupe the name since mangled_name may be stack-allocated const owned_name = self.alloc.dupe(u8, mangled_name) catch return; @@ -9210,7 +9173,7 @@ pub const Lowering = struct { /// no such `method` (a normal unresolved-method, handled downstream). A /// confirmed instance whose author is present but whose bindings are missing is /// a LOUD invariant failure — instantiation writes both together (CP-2). - fn genericInstanceMethod(self: *Lowering, inst_name: []const u8, method: []const u8) ?GenericStructMethod { + pub fn genericInstanceMethod(self: *Lowering, inst_name: []const u8, method: []const u8) ?GenericStructMethod { const author = self.struct_instance_author.get(inst_name) orelse return null; const bindings = self.struct_instance_bindings.getPtr(inst_name) orelse std.debug.panic("generic struct instance '{s}' has an author but no bindings", .{inst_name}); @@ -9667,152 +9630,6 @@ pub const Lowering = struct { // ── Type registration ─────────────────────────────────────────── - /// 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`. - 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; - } - - // ── Pack projection name resolution (Feature 1, Decision 4) ────────── - // - // 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. - - pub const ProjectionPosition = enum { type_position, value_position }; - - pub const PackProjection = union(enum) { - type_arg: u32, // index into the protocol's `type_params` - method: u32, // index into the protocol's `methods` - not_found, // `name` absent from the position-selected namespace - }; - - /// 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; - } - /// Resolve `..pack.` against `protocol_name` by position (Decision 4). /// No cross-namespace fallback: a value-position name that exists only as a /// type-arg (or vice versa) is `.not_found`, letting the caller emit a @@ -10084,419 +9901,6 @@ pub const Lowering = struct { // ── Protocol dispatch ────────────────────────────────────────── - /// Check if a type name is a registered protocol. - 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. - 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...). - 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). - 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. - 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; - } - - // ── Helpers ───────────────────────────────────────────────────── - /// Infer the type of an expression from its AST node (used for untyped var decls). pub fn inferExprType(self: *Lowering, node: *const Node) TypeId { return switch (node.data) { @@ -13158,6 +12562,23 @@ pub const Lowering = struct { pub const bareVisibleStructDecl = lower_nominal.bareVisibleStructDecl; pub const bareVisibleStructTemplate = lower_nominal.bareVisibleStructTemplate; pub const registerGenericStructAlias = lower_nominal.registerGenericStructAlias; + + // --- moved to lower/protocol.zig (lower_protocol) --- + pub const ProjectionPosition = lower_protocol.ProjectionPosition; + pub const PackProjection = lower_protocol.PackProjection; + pub const registerProtocolDecl = lower_protocol.registerProtocolDecl; + pub const instantiateParamProtocol = lower_protocol.instantiateParamProtocol; + pub const lookupProtocolArg = lower_protocol.lookupProtocolArg; + pub const lookupProtocolField = lower_protocol.lookupProtocolField; + pub const isProtocolType = lower_protocol.isProtocolType; + pub const getProtocolInfo = lower_protocol.getProtocolInfo; + pub const getOrCreateThunks = lower_protocol.getOrCreateThunks; + pub const emitDefaultContextGlobal = lower_protocol.emitDefaultContextGlobal; + pub const createProtocolThunk = lower_protocol.createProtocolThunk; + pub const buildProtocolValue = lower_protocol.buildProtocolValue; + pub const emitProtocolDispatch = lower_protocol.emitProtocolDispatch; + pub const resolveConcreteTypeName = lower_protocol.resolveConcreteTypeName; + pub const computeHasImpl = lower_protocol.computeHasImpl; }; /// JNI param/return type resolution: user-declared types pass through diff --git a/src/ir/lower/protocol.zig b/src/ir/lower/protocol.zig new file mode 100644 index 0000000..fad30da --- /dev/null +++ b/src/ir/lower/protocol.zig @@ -0,0 +1,646 @@ +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 mod_mod = @import("../module.zig"); +const type_bridge = @import("../type_bridge.zig"); +const unescape = @import("../../unescape.zig"); +const parser_mod = @import("../../parser.zig"); +const interp_mod = @import("../interp.zig"); +const errors = @import("../../errors.zig"); +const jni_descriptor = @import("../jni_descriptor.zig"); +const program_index_mod = @import("../program_index.zig"); +const resolver_mod = @import("../resolver.zig"); +const imports_mod = @import("../../imports.zig"); +const ProgramIndex = program_index_mod.ProgramIndex; +const GlobalInfo = program_index_mod.GlobalInfo; +const StructTemplate = program_index_mod.StructTemplate; +const TemplateParam = program_index_mod.TemplateParam; +const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo; +const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo; +const ModuleConstInfo = program_index_mod.ModuleConstInfo; +const TypeResolver = @import("../type_resolver.zig").TypeResolver; +const ResolveEnv = @import("../type_resolver.zig").ResolveEnv; +const PackResolver = @import("../packs.zig").PackResolver; +const ExprTyper = @import("../expr_typer.zig").ExprTyper; +const CallResolver = @import("../calls.zig").CallResolver; +const GenericResolver = @import("../generics.zig").GenericResolver; +const ProtocolResolver = @import("../protocols.zig").ProtocolResolver; +const CoercionResolver = @import("../conversions.zig").CoercionResolver; +const ErrorAnalysis = @import("../error_analysis.zig").ErrorAnalysis; +const ErrorFlow = @import("../error_flow.zig").ErrorFlow; +const ObjcLowering = @import("../ffi_objc.zig").ObjcLowering; +const semantic_diagnostics = @import("../semantic_diagnostics.zig"); + +const TypeId = types.TypeId; +const StringId = types.StringId; +const Ref = inst_mod.Ref; +const BlockId = inst_mod.BlockId; +const FuncId = inst_mod.FuncId; +const Function = inst_mod.Function; +const Module = mod_mod.Module; +const Builder = mod_mod.Builder; + + +const lower = @import("../lower.zig"); +const Lowering = lower.Lowering; +const Scope = lower.Scope; + +/// 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; +} + +// ── Pack projection name resolution (Feature 1, Decision 4) ────────── +// +// 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. + +pub const ProjectionPosition = enum { type_position, value_position }; + +pub const PackProjection = union(enum) { + type_arg: u32, // index into the protocol's `type_params` + method: u32, // index into the protocol's `methods` + not_found, // `name` absent from the position-selected namespace +}; + +/// 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; +} + +// ── Helpers ─────────────────────────────────────────────────────