Review follow-up to the ARCH-B split (comment/import hygiene only, no code changes): - Section banners that travelled to the wrong file with the B1-B8 cuts are reworded to describe the section that actually follows (e.g. stmt.zig's trailing "Expression lowering", expr.zig's "Control flow" before lowerChainedComparison) or deleted where nothing follows (4 trailing-at-EOF banners). ffi.zig's facade note no longer claims the IMP builders "stay here" (they live in lower/objc_class.zig); protocol.zig's namespace-lookup banner now points at pack.zig:resolvePackProjection for the orchestrator. - lower.zig's two lower/expr.zig alias blocks (B8.1 + B8.2 appends) merged into one. - 448 unused header decls pruned from the 15 lower/*.zig files (each had inherited lower.zig's full import block; pruned to fixpoint so cascading type-extraction consts went too). Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero expected/ snapshot churn.
609 lines
30 KiB
Zig
609 lines
30 KiB
Zig
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<T>"]`.
|
|
/// - Parameterised `Into(Block)` (call) → walks `param_impl_map`
|
|
/// keyed by `"<P>\x00<arg_mangled>\x00<T_mangled>"`.
|
|
/// 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.<name>` 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;
|
|
}
|