Files
sx/src/ir/protocols.zig
agra 1f9f944ca1 fix(ir): exhaustive named-const array dims (0083) + nested slice-literal coercion (0085)
Makes the F0.4 fixes exhaustive across every resolution / nesting path.

0083 — named-const array dimension, stateless paths. Attempt 1 fixed the
stateful resolver (direct local decls, struct fields, params, returns) but the
binding-free registration-time resolver (`type_bridge`, used for type aliases
`Arr :: [N]T` and inline union/enum field types) still resolved a named dim with
a silent `else 0`, so `Arr :: [N]s64; a : Arr` and `union { a: [N]s64 }` were
still miscompiled (garbage / bus error). Thread the module-global const table
(`ProgramIndex.module_const_map`) into `type_bridge` alongside the alias map, so
`StatelessInner.resolveArrayLen` resolves a named module-const dim to the same
length everywhere. The remaining unresolvable case (a computed/comptime dim on
the binding-free path, which the stateful path hard-errors) now bails LOUDLY
instead of fabricating a 0 length.

0085 — nested slice-literal elements. `lowerArrayLiteral` lowered each element
with the element type as target but appended the raw value. A nested `.[...]`
element at a slice element type (`[][]s64`) still lowers to an aggregate array
`[N]T`, so the outer aggregate held raw arrays where slice {ptr,len} headers
were expected — indexing the inner slice read a garbage pointer and segfaulted.
After lowering each element, coerce a same-element array to the slice element
type via the existing `array_to_slice` op. The coercion recurses with the
nesting, so `[][]T` and deeper materialize at every level — local-bound AND
direct-call-argument forms.

Regressions (fail-before/pass-after demonstrated on the pre-fix compiler):
  examples/0140-types-named-const-array-dim.sx — extended with type-alias,
    nested [N][M]T, and union-field named dims (s64 / string / struct elems)
  examples/0142-types-nested-slice-literal-elements.sx — [][]s64 + [][]string,
    local-bound vs direct-arg
  src/ir/type_bridge.test.zig — named-const dim resolves to literal length

Gate: zig build, zig build test, bash tests/run_examples.sh (388 passed).
Issues 0083 and 0085 marked RESOLVED.
2026-06-04 09:06:08 +03:00

573 lines
29 KiB
Zig

const std = @import("std");
const ast = @import("../ast.zig");
const types = @import("types.zig");
const type_bridge = @import("type_bridge.zig");
const lower = @import("lower.zig");
const program_index_mod = @import("program_index.zig");
const Node = ast.Node;
const TypeId = types.TypeId;
const Lowering = lower.Lowering;
const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo;
const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo;
/// Protocol / impl LOOKUP + REGISTRATION (architecture phase A4.2), extracted
/// from `Lowering`. Owns:
/// - read-only conformance queries: `getProtocolInfo` (is a type a registered
/// protocol + its method table), `hasImplPlain` (have a (protocol, type)
/// pair's thunks been materialized), `packArgConformsTo` (impl-declaration
/// conformance for protocol-pack `..xs: P` elements),
/// - registration: `registerProtocolDecl` (protocol struct + method table +
/// vtable type), `registerImplBlock` / `registerParamImpl` (populate the
/// impl maps + the `0410`/`0411`/`0412` visibility/duplicate diagnostics),
/// and the default-method synthesis they use.
///
/// A `*Lowering` facade (Principle 5, like `GenericResolver` / `CallResolver`):
/// it reads/writes the protocol/impl registries (`protocol_decl_map` /
/// `protocol_ast_map` in `ProgramIndex`; `protocol_thunk_map` / `param_impl_map`
/// / `param_impl_pack_map` / `protocol_vtable_type_map` on `Lowering`) plus the
/// type table, so it borrows `*Lowering` rather than re-threading every map.
/// IR EMISSION stays in `Lowering` for the later A4.2 increment — registration
/// calls `self.l.declareFunction` (the emission primitive) but the thunk/value
/// builders (`createProtocolThunk` / `buildProtocolValue` / `tryUserConversion`)
/// are NOT moved here.
pub const ProtocolResolver = struct {
l: *Lowering,
/// If `ty` is a registered protocol struct, return its decl info (method
/// table); else null.
pub fn getProtocolInfo(self: ProtocolResolver, ty: TypeId) ?ProtocolDeclInfo {
if (ty.isBuiltin()) return null;
const info = self.l.module.types.get(ty);
if (info != .@"struct") return null;
const name = self.l.module.types.getString(info.@"struct".name);
return self.l.program_index.protocol_decl_map.get(name);
}
/// Have the thunks for (protocol `p_name`, concrete `ty`) been materialized?
/// `protocol_thunk_map` is populated lazily when a protocol VALUE is created
/// with `xx`, so this answers "has erasure already happened for this pair".
pub fn hasImplPlain(self: ProtocolResolver, p_name: []const u8, ty: TypeId) bool {
const ty_name = self.l.formatTypeName(ty);
const thunk_key = std.fmt.allocPrint(self.l.alloc, "{s}\x00{s}", .{ p_name, ty_name }) catch return false;
return self.l.protocol_thunk_map.contains(thunk_key);
}
/// Does `ty` conform to protocol `p_name` (under SOME type-args for a
/// parameterised protocol)? Used to check protocol-pack elements
/// (`..xs: P`), where each element's protocol type-args are inferred from
/// its impl rather than written out.
///
/// Conformance is queried at the IMPL-DECLARATION level (not via
/// `protocol_thunk_map`, which is only populated lazily when a protocol
/// VALUE is created with `xx`):
/// - Parameterised `P`: any `param_impl_map` key `P\x00<args>\x00<mangle(ty)>`.
/// - Non-parameterised `P`: every required (non-default) method `m` is
/// registered as `<ty>.<m>` in `fn_ast_map` (how `registerImplBlock`
/// records a non-parameterised impl).
/// An arg already of the protocol's own (erased) type trivially conforms.
pub fn packArgConformsTo(self: ProtocolResolver, p_name: []const u8, ty: TypeId) bool {
// Arg already erased to the protocol struct itself (e.g. `xx a`).
if (!ty.isBuiltin()) {
const info = self.l.module.types.get(ty);
if (info == .@"struct" and info.@"struct".is_protocol and
std.mem.eql(u8, self.l.module.types.getString(info.@"struct".name), p_name)) return true;
}
const pd = self.l.program_index.protocol_ast_map.get(p_name) orelse return false;
if (pd.type_params.len > 0) {
const prefix = std.fmt.allocPrint(self.l.alloc, "{s}\x00", .{p_name}) catch return false;
const suffix = std.fmt.allocPrint(self.l.alloc, "\x00{s}", .{self.l.mangleTypeName(ty)}) catch return false;
var it = self.l.param_impl_map.keyIterator();
while (it.next()) |k| {
if (std.mem.startsWith(u8, k.*, prefix) and std.mem.endsWith(u8, k.*, suffix)) return true;
}
return false;
}
// Non-parameterised: require each non-default method as `<ty>.<m>`.
const ty_name = self.l.formatTypeName(ty);
for (pd.methods) |m| {
if (m.default_body != null) continue;
const q = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ ty_name, m.name }) catch return false;
if (!self.l.program_index.fn_ast_map.contains(q)) return false;
}
return true;
}
// ── Thunk / impl PLANNING (lookup only; emission stays in Lowering) ──
/// The dispatch method table for protocol `proto_name` — i.e. exactly which
/// methods `getOrCreateThunks` must materialize a thunk for. Null if the
/// name isn't a registered (non-parameterised) protocol.
pub fn protocolMethodInfos(self: ProtocolResolver, proto_name: []const u8) ?[]const ProtocolMethodInfo {
const pd = self.l.program_index.protocol_decl_map.get(proto_name) orelse return null;
return pd.methods;
}
/// Filter parameterised-impl `entries` to those reachable from the current
/// source file (the file itself + everything it transitively imports). The
/// cross-module visibility selection behind the `0410` path. Falls open
/// (all entries) when the source-file context or import graph isn't wired
/// (e.g. comptime callers). Appends the visible subset to `out`.
pub fn findVisibleImpls(self: ProtocolResolver, entries: []const Lowering.ParamImplEntry, out: *std.ArrayList(Lowering.ParamImplEntry)) void {
const here = self.l.current_source_file orelse {
out.appendSlice(self.l.alloc, entries) catch {};
return;
};
const graph = self.l.program_index.import_graph orelse {
out.appendSlice(self.l.alloc, entries) catch {};
return;
};
// BFS over the import graph to compute the visible set.
var visible = std.StringHashMap(void).init(self.l.alloc);
defer visible.deinit();
visible.put(here, {}) catch {};
var queue = std.ArrayList([]const u8).empty;
defer queue.deinit(self.l.alloc);
queue.append(self.l.alloc, here) catch {};
var head: usize = 0;
while (head < queue.items.len) : (head += 1) {
const node = queue.items[head];
const direct = graph.get(node) orelse continue;
var it = direct.iterator();
while (it.next()) |kv| {
const next = kv.key_ptr.*;
if (visible.contains(next)) continue;
visible.put(next, {}) catch {};
queue.append(self.l.alloc, next) catch {};
}
}
for (entries) |e| {
if (visible.contains(e.defining_module)) {
out.append(self.l.alloc, e) catch {};
}
}
}
/// A pack-impl selected for a concrete source closure/function: the matched
/// entry plus its `convert` method. Pure SELECTION — binding + monomorphise
/// + emission stay in `Lowering.tryPackImplMatch`.
pub const PackImplMatch = struct {
entry: Lowering.PackParamImplEntry,
convert_fd: *const ast.FnDecl,
/// The source closure/function's param + return types — the binding
/// step (in `Lowering`) reads these to bind the pack-var tail + ret-var.
src_params: []const TypeId,
src_ret: TypeId,
};
/// Among the pack impls under `pack_key`, find the first whose fixed prefix
/// matches `src_ty`'s leading params (and whose return matches, unless the
/// impl's return is a generic var). Returns the matched entry + its
/// `convert` method, or null when nothing matches. No emission.
pub fn matchPackImpl(self: ProtocolResolver, src_ty: TypeId, pack_key: []const u8) ?PackImplMatch {
const pack_entries = self.l.param_impl_pack_map.get(pack_key) orelse return null;
if (pack_entries.items.len == 0) return null;
const table = &self.l.module.types;
// Source must itself be a closure/function the pack can match.
const src_info = table.get(src_ty);
if (src_info != .closure and src_info != .function) return null;
const src_params: []const TypeId = switch (src_info) {
.closure => |c| c.params,
.function => |f| f.params,
else => unreachable,
};
const src_ret: TypeId = switch (src_info) {
.closure => |c| c.ret,
.function => |f| f.ret,
else => unreachable,
};
// Find pack impls whose fixed prefix matches src's leading params.
var matched_idx: ?usize = null;
for (pack_entries.items, 0..) |entry, i| {
const ent_info = table.get(entry.source_pack_ty);
// Pack impls always wear a closure (resolveClosureType routes
// both Closure and the future Fn pack forms through
// closureTypePack); a function-typed pack impl is not produced
// by current parser shapes.
if (ent_info != .closure) continue;
const ent_ci = ent_info.closure;
const pack_start = ent_ci.pack_start orelse continue;
// Fixed prefix must fit within the source's params.
if (pack_start > src_params.len) continue;
var prefix_ok = true;
var i_fix: u32 = 0;
while (i_fix < pack_start) : (i_fix += 1) {
if (ent_ci.params[i_fix] != src_params[i_fix]) {
prefix_ok = false;
break;
}
}
if (!prefix_ok) continue;
// Return type: if the impl's return is a generic var
// (ret_var_name set), any source return binds; otherwise it
// must equal the source's return exactly.
if (entry.ret_var_name == null and ent_ci.ret != src_ret) continue;
// First match wins for v1; concrete-wins-over-pack already
// happened by the caller checking concrete first. Multiple
// overlapping pack impls would be a separate diagnostic
// (deferred — same module duplicates are caught at registration).
matched_idx = i;
break;
}
const idx = matched_idx orelse return null;
const entry = pack_entries.items[idx];
// Find the `convert` method.
for (entry.methods) |m| {
if (std.mem.eql(u8, m.name, "convert")) {
return .{ .entry = entry, .convert_fd = m, .src_params = src_params, .src_ret = src_ret };
}
}
return null;
}
// ── Registration ────────────────────────────────────────────────────
pub fn registerProtocolDecl(self: ProtocolResolver, pd: *const ast.ProtocolDecl) void {
// Decision 4 soft-convention warning: a type-arg and a method (the
// "runtime accessor" namespace — protocols have no fields) sharing a
// name is allowed, but `..pack.<name>` then resolves by *position*
// rather than by precedence, which surprises readers. Alert at decl.
for (pd.type_params) |tp| {
for (pd.methods) |m| {
if (std.mem.eql(u8, tp.name, m.name)) {
if (self.l.diagnostics) |diags| {
diags.addFmt(.warn, null, "protocol '{s}' declares type-arg and method both named '{s}'; `..pack.{s}` resolves by position (type-arg in type position, method in value position)", .{ pd.name, tp.name, tp.name });
}
}
}
}
// Parameterised protocols are compile-time-only — no vtable, no boxed
// instance struct. Methods reference unbound type params (e.g.
// `convert :: () -> Target`) that only get a concrete TypeId per
// (Source, Target) pair at xx resolution time. Stash the AST so
// `param_impl_map` lookup can resolve method signatures lazily.
if (pd.type_params.len > 0) {
self.l.program_index.protocol_ast_map.put(pd.name, pd) catch {};
return;
}
const table = &self.l.module.types;
const name_id = table.internString(pd.name);
var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
// First field: ctx: *void
const void_ptr_ty = table.ptrTo(.void);
fields.append(self.l.alloc, .{
.name = table.internString("ctx"),
.ty = void_ptr_ty,
}) catch unreachable;
if (pd.is_inline) {
// One fn-ptr field per protocol method
for (pd.methods) |method| {
fields.append(self.l.alloc, .{
.name = table.internString(method.name),
.ty = void_ptr_ty, // fn ptrs are opaque pointers
}) catch unreachable;
}
} else {
// Vtable pointer
fields.append(self.l.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.update(id, struct_info);
// Build protocol method info for dispatch
var method_infos = std.ArrayList(ProtocolMethodInfo).empty;
for (pd.methods) |method| {
var ptypes = std.ArrayList(TypeId).empty;
for (method.params) |p| {
// Self → *void for protocol context; everything else
// goes through `resolveAstType`, threaded with the canonical
// alias map (`ProgramIndex.type_alias_map`).
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 type_bridge.resolveAstType(p, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map);
};
ptypes.append(self.l.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 type_bridge.resolveAstType(rt, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map);
} else .void;
method_infos.append(self.l.alloc, .{
.name = method.name,
.param_types = self.l.alloc.dupe(TypeId, ptypes.items) catch unreachable,
.ret_type = ret,
.ret_is_self = ret_is_self,
}) catch unreachable;
}
self.l.program_index.protocol_decl_map.put(pd.name, .{
.name = pd.name,
.is_inline = pd.is_inline,
.methods = self.l.alloc.dupe(ProtocolMethodInfo, method_infos.items) catch unreachable,
}) catch {};
self.l.program_index.protocol_ast_map.put(pd.name, pd) catch {};
// For vtable protocols, create the vtable struct type
if (!pd.is_inline) {
var vtable_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
for (pd.methods) |method| {
vtable_fields.append(self.l.alloc, .{
.name = table.internString(method.name),
.ty = void_ptr_ty,
}) catch unreachable;
}
var vtable_name_buf: [128]u8 = undefined;
const vtable_name = std.fmt.bufPrint(&vtable_name_buf, "__{s}__Vtable", .{pd.name}) catch "__Vtable";
const vtable_name_id = table.internString(vtable_name);
const vtable_info: types.TypeInfo = .{ .@"struct" = .{ .name = vtable_name_id, .fields = vtable_fields.items } };
const vtable_ty = table.intern(vtable_info);
self.l.protocol_vtable_type_map.put(pd.name, vtable_ty) catch {};
}
}
pub fn registerImplBlock(self: ProtocolResolver, ib: *const ast.ImplBlock, is_imported: bool, decl: *const Node) void {
// Parameterised-protocol impl (e.g. `impl Into(Block) for Closure() -> void`):
// record into `param_impl_map` for compile-time resolution by `lowerXX`.
// Methods are NOT registered in fn_ast_map — they're monomorphised lazily
// per (Source, Target) pair at the xx call site.
if (ib.protocol_type_args.len > 0) {
self.registerParamImpl(ib, decl, is_imported);
return;
}
// Collect explicitly implemented method names
var impl_methods = std.StringHashMap(void).init(self.l.alloc);
defer impl_methods.deinit();
for (ib.methods) |method_node| {
if (method_node.data == .fn_decl) {
const method_fd = &method_node.data.fn_decl;
const qualified = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ ib.target_type, method_fd.name }) catch continue;
self.l.program_index.fn_ast_map.put(qualified, method_fd) catch {};
self.l.program_index.import_flags.put(qualified, is_imported) catch {};
self.l.declareFunction(method_fd, qualified);
impl_methods.put(method_fd.name, {}) catch {};
}
}
// Synthesize default methods from protocol declaration
if (self.l.program_index.protocol_ast_map.get(ib.protocol_name)) |pd| {
for (pd.methods) |method| {
if (method.default_body != null and !impl_methods.contains(method.name)) {
// Create a synthesized fn_decl for the default method
const synth_fd = self.synthesizeDefaultMethod(method, ib.target_type);
const qualified = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ ib.target_type, method.name }) catch continue;
self.l.program_index.fn_ast_map.put(qualified, synth_fd) catch {};
self.l.program_index.import_flags.put(qualified, is_imported) catch {};
self.l.declareFunction(synth_fd, qualified);
}
}
}
}
/// Register a parameterised-protocol impl into `param_impl_map`.
/// Resolves the protocol's type args + the source type, mangles them, and
/// stashes the impl's method fn_decls for later monomorphisation by
/// `lowerXX`. Same-module duplicate impls produce a diagnostic here;
/// cross-module duplicates are detected at the xx resolution site.
///
/// Pack-shaped sources (`Closure(..$args) -> $R`, detected via
/// `pack_start != null`) are additionally registered into
/// `param_impl_pack_map` keyed without the source suffix — the matching
/// site walks that map to bind packs against any concrete closure shape.
pub fn registerParamImpl(self: ProtocolResolver, ib: *const ast.ImplBlock, decl: *const Node, is_imported: bool) void {
const table = &self.l.module.types;
// Resolve the protocol's type-arg list to concrete TypeIds.
var arg_tys = std.ArrayList(TypeId).empty;
for (ib.protocol_type_args) |arg_node| {
const t = type_bridge.resolveAstType(arg_node, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map);
arg_tys.append(self.l.alloc, t) catch return;
}
// Resolve the source type. Parser stores it on `target_type_expr` for
// parameterised impls (back-compat `target_type` string is kept for
// simple cases but the canonical form is the TypeExpr).
const src_ty: TypeId = if (ib.target_type_expr) |te|
type_bridge.resolveAstType(te, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map)
else if (ib.target_type.len > 0)
type_bridge.resolveAstType(&.{ .span = decl.span, .data = .{ .type_expr = .{ .name = ib.target_type } } }, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map)
else
return;
// Mangle into the lookup key.
var key_buf = std.ArrayList(u8).empty;
key_buf.appendSlice(self.l.alloc, ib.protocol_name) catch return;
for (arg_tys.items) |t| {
key_buf.append(self.l.alloc, 0) catch return;
key_buf.appendSlice(self.l.alloc, self.l.mangleTypeName(t)) catch return;
}
const pack_key_len = key_buf.items.len; // proto + args, no src — used for pack map
key_buf.append(self.l.alloc, 0) catch return;
key_buf.appendSlice(self.l.alloc, self.l.mangleTypeName(src_ty)) catch return;
const key = key_buf.items;
// Collect method fn_decl pointers.
var methods = std.ArrayList(*const ast.FnDecl).empty;
for (ib.methods) |method_node| {
if (method_node.data == .fn_decl) {
methods.append(self.l.alloc, &method_node.data.fn_decl) catch {};
}
}
const defining_module: []const u8 = self.l.current_source_file orelse "";
const entry: Lowering.ParamImplEntry = .{
.methods = self.l.alloc.dupe(*const ast.FnDecl, methods.items) catch return,
.source_ty = src_ty,
.target_args = self.l.alloc.dupe(TypeId, arg_tys.items) catch return,
.defining_module = defining_module,
.span = decl.span,
};
const gop = self.l.param_impl_map.getOrPut(key) catch return;
if (!gop.found_existing) {
gop.value_ptr.* = std.ArrayList(Lowering.ParamImplEntry).empty;
} else {
// Same-file duplicate is an immediate error. Cross-file overlaps
// are deferred to the xx resolution site (Phase 5) so the impl
// surface can be richer than any one file's view.
for (gop.value_ptr.items) |existing| {
if (std.mem.eql(u8, existing.defining_module, defining_module)) {
if (self.l.diagnostics) |diags| {
diags.addFmt(.err, decl.span, "duplicate impl '{s}' for source '{s}' in {s}", .{
ib.protocol_name, self.l.mangleTypeName(src_ty), defining_module,
});
}
return;
}
}
}
gop.value_ptr.append(self.l.alloc, entry) catch return;
// Concrete-struct source: also register the impl's methods as
// `<Source>.<method>` in fn_ast_map so UFCS resolves them (e.g.
// `xs[i].get()` on a pack element). For a concrete impl like
// `impl Box(s64) for IntCell`, the method is already fully concrete —
// nothing to monomorphize, unlike generic/pack sources (which stay
// lazy in param_impl_map and are handled below).
{
const si = table.get(src_ty);
if (!src_ty.isBuiltin() and si == .@"struct") {
const src_name = self.l.formatTypeName(src_ty);
// A generic-struct source (`impl VL($R) for Combined($R, ..$Ts)`)
// registers each method as a TEMPLATE only: its signature
// references unbound type params (`-> $R`), so declaring it as a
// standalone function would emit garbage (an unresolved return
// type). Concrete instances are monomorphized per-erasure by
// createProtocolThunk via this same fn_ast_map entry.
const is_generic_src = self.l.program_index.struct_template_map.contains(src_name);
for (methods.items) |mfd| {
const q = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ src_name, mfd.name }) catch continue;
if (self.l.program_index.fn_ast_map.contains(q)) continue; // first impl wins
self.l.program_index.fn_ast_map.put(q, mfd) catch {};
self.l.program_index.import_flags.put(q, is_imported) catch {};
if (!is_generic_src) self.l.declareFunction(mfd, q);
}
}
}
// Pack-shaped source: also register in the pack map. The source
// closure carries `pack_start` set; matching binds the source's
// tail param types to the pack-name and the source's return to
// the impl's return-type-var (when the return is generic).
const src_info = table.get(src_ty);
if (src_info == .closure and src_info.closure.pack_start != null) {
const target_expr_node = ib.target_type_expr orelse return;
if (target_expr_node.data != .closure_type_expr) return;
const ct = target_expr_node.data.closure_type_expr;
const pack_var = ct.pack_name orelse return;
// Extract the return-type-var name if the impl's return is generic.
// `Closure(...) -> $R` parses with the return-type node carrying
// `is_generic = true`. Concrete returns leave it null.
var ret_var: ?[]const u8 = null;
if (ct.return_type) |rt| {
if (rt.data == .type_expr and rt.data.type_expr.is_generic) {
ret_var = rt.data.type_expr.name;
}
}
const pack_entry: Lowering.PackParamImplEntry = .{
.methods = self.l.alloc.dupe(*const ast.FnDecl, methods.items) catch return,
.source_pack_ty = src_ty,
.target_args = self.l.alloc.dupe(TypeId, arg_tys.items) catch return,
.defining_module = defining_module,
.span = decl.span,
.pack_var_name = self.l.alloc.dupe(u8, pack_var) catch return,
.ret_var_name = if (ret_var) |rv| (self.l.alloc.dupe(u8, rv) catch return) else null,
};
const pack_key = key_buf.items[0..pack_key_len];
const pack_key_owned = self.l.alloc.dupe(u8, pack_key) catch return;
const pgop = self.l.param_impl_pack_map.getOrPut(pack_key_owned) catch return;
if (!pgop.found_existing) {
pgop.value_ptr.* = std.ArrayList(Lowering.PackParamImplEntry).empty;
} else {
for (pgop.value_ptr.items) |existing| {
if (std.mem.eql(u8, existing.defining_module, defining_module)) {
if (self.l.diagnostics) |diags| {
diags.addFmt(.err, decl.span, "duplicate pack impl '{s}' for source '{s}' in {s}", .{
ib.protocol_name, self.l.mangleTypeName(src_ty), defining_module,
});
}
return;
}
}
}
pgop.value_ptr.append(self.l.alloc, pack_entry) catch return;
}
}
/// Synthesize a fn_decl from a protocol default method for a concrete type.
fn synthesizeDefaultMethod(self: ProtocolResolver, method: ast.ProtocolMethodDecl, target_type: []const u8) *const ast.FnDecl {
// Build parameter list: self: *TargetType, then the protocol method params
var params_list = std.ArrayList(ast.Param).empty;
defer params_list.deinit(self.l.alloc);
// Add self parameter: self: *TargetType
const self_type_node = self.l.alloc.create(ast.Node) catch unreachable;
const pointee_node = self.l.alloc.create(ast.Node) catch unreachable;
pointee_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = target_type } } };
self_type_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{
.pointee_type = pointee_node,
} } };
params_list.append(self.l.alloc, .{
.name = "self",
.name_span = .{ .start = 0, .end = 0 },
.type_expr = self_type_node,
}) catch unreachable;
// Add remaining params from the protocol method
for (method.params, method.param_names) |pty, pname| {
params_list.append(self.l.alloc, .{
.name = pname,
.name_span = .{ .start = 0, .end = 0 },
.type_expr = pty,
}) catch unreachable;
}
const fd = self.l.alloc.create(ast.FnDecl) catch unreachable;
fd.* = .{
.name = method.name,
.params = self.l.alloc.dupe(ast.Param, params_list.items) catch unreachable,
.body = method.default_body.?,
.return_type = method.return_type,
};
return fd;
}
};