Codex corrective step before the A2 merge gate: A2.3 left type_bridge with a parallel structural type-resolution algorithm and an inline tuple-literal-spread shape in lower.zig with a `.void` fallback. Finding 1 — single owner for structural shapes: - TypeResolver.resolveCompound is now the sole structural type-shape constructor. Namespaced on `table` (so the stateless type_bridge can call it) and extended to own function types, plain `Closure(P...) -> R`, and plain positional/named tuples (it already owned *T/[*]T/[]T/?T/[N]T). It returns null only for the pack-shaped forms that need caller state (`Closure(..p)`, spread tuples); OOM yields `.unresolved`. - type_bridge: deleted its 8 independent structural resolvers (resolveArray/Slice/Pointer/ManyPointer/Optional/Function/Closure/TupleType). resolveAstType delegates those node kinds to resolveCompound via a binding-free StatelessInner adapter. The only residual stateless shape code is two tiny fallbacks for the pack-shaped forms resolveCompound defers (resolveClosurePackShape — used by Into(Block) at registration time — and resolveTupleSpreadShape) plus resolveParameterizedType (kept: generic-instantiation convergence is A4.1 per PLAN-ARCH). - lower.zig: stateful resolveTypeWithBindings uses resolveCompound; the `.function_type_expr` switch arm is gone. PackResolver.resolveFunctionTypeWithBindings deleted (subsumed). Plain closures/tuples now resolve via resolveCompound in both paths; only pack closures / spread tuples reach PackResolver. Finding 2 — no `.void` failure fallback in lower.zig pack handling: - the inline tuple_literal-with-spread type assembly moved into PackResolver.resolveTupleLiteralType (returns ?TypeId; OOM `catch return .void` became `catch return .unresolved`). Alias result preserved: TypeTable.aliases stays gone; no table.aliases reads; ProgramIndex.type_alias_map threaded explicitly. type_resolver.test.zig: resolveCompound test rewritten (namespaced + new function/closure/tuple/pack-shape arms, arena-backed). Gate green: zig build, zig build test, run_examples 350/0.
242 lines
12 KiB
Zig
242 lines
12 KiB
Zig
const std = @import("std");
|
|
const ast = @import("../ast.zig");
|
|
const types = @import("types.zig");
|
|
const lower = @import("lower.zig");
|
|
|
|
const Node = ast.Node;
|
|
const TypeId = types.TypeId;
|
|
const Lowering = lower.Lowering;
|
|
|
|
/// Canonical owner of pack-aware TYPE-position resolution (architecture phase
|
|
/// A2.3). Resolves the shapes whose meaning depends on active pack state —
|
|
/// pack-variadic `Closure(..p)` / `(Params...) -> R` / `(..xs)` tuples and the
|
|
/// pack projections (`..xs.T`) that back them — in one place instead of inline
|
|
/// in `Lowering`.
|
|
///
|
|
/// A `*Lowering` facade (Principle 5): pack projection reads the live pack
|
|
/// state (`pack_arg_types` / `pack_constraint` / `pack_bindings` /
|
|
/// `type_bindings` / `param_impl_map`) and recurses through the full stateful
|
|
/// type resolver, so it borrows `Lowering` rather than re-threading every
|
|
/// field. The dependency shrinks as later phases lift pack state into an
|
|
/// explicit context object.
|
|
pub const PackResolver = struct {
|
|
l: *Lowering,
|
|
|
|
/// Resolve a `Closure(...)` type expression with the active type/pack
|
|
/// bindings applied. Pack-shaped closure exprs (`Closure(Prefix..., ..$pack)`)
|
|
/// substitute `pack` from `pack_bindings`, producing a concrete closure
|
|
/// type — used when monomorphising a pack-variadic impl body against a
|
|
/// concrete source signature.
|
|
pub fn resolveClosureTypeWithBindings(self: PackResolver, ct: *const ast.ClosureTypeExpr) TypeId {
|
|
var param_ids = std.ArrayList(TypeId).empty;
|
|
defer param_ids.deinit(self.l.alloc);
|
|
for (ct.param_types) |pt| {
|
|
param_ids.append(self.l.alloc, self.l.resolveTypeWithBindings(pt)) catch return .unresolved;
|
|
}
|
|
if (ct.pack_name) |pn| {
|
|
// Protocol pack (`Closure(..sources.T)` / `Closure(..sources)`):
|
|
// expand the bound pack's per-element type-args.
|
|
if (self.packTypeArgs(pn, ct.pack_projection)) |elems| {
|
|
defer self.l.alloc.free(elems);
|
|
for (elems) |t| param_ids.append(self.l.alloc, t) catch return .unresolved;
|
|
const ret_ty = if (ct.return_type) |rt| self.l.resolveTypeWithBindings(rt) else .void;
|
|
return self.l.module.types.closureType(param_ids.items, ret_ty);
|
|
}
|
|
if (self.l.pack_bindings) |pb| {
|
|
if (pb.get(pn)) |pack_tys| {
|
|
for (pack_tys) |t| param_ids.append(self.l.alloc, t) catch return .unresolved;
|
|
// Fully bound — emit a concrete closure type, no pack_start.
|
|
const ret_ty = if (ct.return_type) |rt| self.l.resolveTypeWithBindings(rt) else .void;
|
|
return self.l.module.types.closureType(param_ids.items, ret_ty);
|
|
}
|
|
}
|
|
// Pack name in scope but no binding — preserve the pack-shape
|
|
// so downstream code can still see it's variadic. (Hit during
|
|
// impl-block parsing before any concrete monomorphisation.)
|
|
const ret_ty = if (ct.return_type) |rt| self.l.resolveTypeWithBindings(rt) else .void;
|
|
return self.l.module.types.closureTypePack(param_ids.items, ret_ty, @intCast(param_ids.items.len));
|
|
}
|
|
const ret_ty = if (ct.return_type) |rt| self.l.resolveTypeWithBindings(rt) else .void;
|
|
return self.l.module.types.closureType(param_ids.items, ret_ty);
|
|
}
|
|
|
|
/// Resolve a tuple type expression with active pack bindings: a spread field
|
|
/// `(..xs)` / `(..xs.T)` expands to the pack's per-element types via
|
|
/// `packTypeElems`. Non-spread fields resolve normally.
|
|
pub fn resolveTupleTypeWithBindings(self: PackResolver, tt: *const ast.TupleTypeExpr) TypeId {
|
|
var field_ids = std.ArrayList(TypeId).empty;
|
|
defer field_ids.deinit(self.l.alloc);
|
|
var had_spread = false;
|
|
for (tt.field_types) |ft| {
|
|
if (ft.data == .spread_expr) {
|
|
if (self.packTypeElems(ft.data.spread_expr.operand)) |elems| {
|
|
defer self.l.alloc.free(elems);
|
|
for (elems) |e| field_ids.append(self.l.alloc, e) catch return .unresolved;
|
|
had_spread = true;
|
|
continue;
|
|
}
|
|
}
|
|
field_ids.append(self.l.alloc, self.l.resolveTypeWithBindings(ft)) catch return .unresolved;
|
|
}
|
|
// Preserve field names for a named tuple `(x: T, y: U)` so `t.x` resolves
|
|
// (matches type_bridge.resolveTupleType). A spread expands to unnamed
|
|
// pack elements, so names only apply when there was no spread.
|
|
var name_ids: ?[]const types.StringId = null;
|
|
if (!had_spread) {
|
|
if (tt.field_names) |names| {
|
|
if (names.len == field_ids.items.len) {
|
|
var ids = std.ArrayList(types.StringId).empty;
|
|
for (names) |n| ids.append(self.l.alloc, self.l.module.types.internString(n)) catch return .unresolved;
|
|
name_ids = ids.toOwnedSlice(self.l.alloc) catch null;
|
|
}
|
|
}
|
|
}
|
|
return self.l.module.types.intern(.{ .tuple = .{
|
|
.fields = self.l.alloc.dupe(TypeId, field_ids.items) catch return .unresolved,
|
|
.names = name_ids,
|
|
} });
|
|
}
|
|
|
|
/// Resolve a tuple LITERAL used in a type position whose elements include a
|
|
/// pack spread (`(..$Ts)` / `(..xs.T)` — these parse as a tuple literal, not
|
|
/// a `tuple_type_expr`). Returns null when no element is a spread, so the
|
|
/// caller falls through to ordinary name/type resolution. A failed
|
|
/// allocation yields `.unresolved` (never a real `.void`).
|
|
pub fn resolveTupleLiteralType(self: PackResolver, tl: *const ast.TupleLiteral) ?TypeId {
|
|
var any_spread = false;
|
|
for (tl.elements) |el| {
|
|
if (el.value.data == .spread_expr) {
|
|
any_spread = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!any_spread) return null;
|
|
var field_ids = std.ArrayList(TypeId).empty;
|
|
defer field_ids.deinit(self.l.alloc);
|
|
for (tl.elements) |el| {
|
|
if (el.value.data == .spread_expr) {
|
|
if (self.packTypeElems(el.value.data.spread_expr.operand)) |elems| {
|
|
defer self.l.alloc.free(elems);
|
|
for (elems) |e| field_ids.append(self.l.alloc, e) catch return .unresolved;
|
|
continue;
|
|
}
|
|
}
|
|
field_ids.append(self.l.alloc, self.l.resolveTypeWithBindings(el.value)) catch return .unresolved;
|
|
}
|
|
return self.l.module.types.intern(.{ .tuple = .{
|
|
.fields = self.l.alloc.dupe(TypeId, field_ids.items) catch return .unresolved,
|
|
.names = null,
|
|
} });
|
|
}
|
|
|
|
/// TYPE-position pack expansion: given a spread operand, return the
|
|
/// per-element types. `..xs` → the pack's element types (`pack_arg_types`).
|
|
/// `..xs.T` → each element's protocol type-arg `T` (from its
|
|
/// `impl P(args) for elem` in `param_impl_map`). Null when not a pack spread.
|
|
/// Caller owns the returned slice.
|
|
pub fn packTypeElems(self: PackResolver, operand: *const Node) ?[]TypeId {
|
|
const pat = self.l.pack_arg_types orelse return null;
|
|
// `..F(Ts)` — apply a parameterized type `F` to each pack element:
|
|
// `(..VL(Ts))` → `(VL(T0), VL(T1), …)`. Per element, temporarily bind
|
|
// the pack name to that single element type and resolve `F(elem)`.
|
|
if (operand.data == .parameterized_type_expr) {
|
|
const pt = operand.data.parameterized_type_expr;
|
|
var pack_name_p: []const u8 = "";
|
|
for (pt.args) |a| {
|
|
const nm = switch (a.data) {
|
|
.identifier => |id| id.name,
|
|
.type_expr => |te| te.name,
|
|
else => continue,
|
|
};
|
|
if (pat.contains(nm)) {
|
|
pack_name_p = nm;
|
|
break;
|
|
}
|
|
}
|
|
if (pack_name_p.len == 0) return null;
|
|
const elems = pat.get(pack_name_p) orelse return null;
|
|
if (self.l.type_bindings == null) return null;
|
|
var out = std.ArrayList(TypeId).empty;
|
|
for (elems) |ti| {
|
|
const had = self.l.type_bindings.?.get(pack_name_p);
|
|
self.l.type_bindings.?.put(pack_name_p, ti) catch {};
|
|
out.append(self.l.alloc, self.l.resolveTypeWithBindings(operand)) catch return null;
|
|
if (had) |h| self.l.type_bindings.?.put(pack_name_p, h) catch {} else _ = self.l.type_bindings.?.remove(pack_name_p);
|
|
}
|
|
return out.toOwnedSlice(self.l.alloc) catch null;
|
|
}
|
|
// In type position `xs` / `xs.T` parse to a (possibly dotted) type_expr
|
|
// name; `field_access` covers any value-shaped form.
|
|
var pack_name: []const u8 = "";
|
|
var projection: ?[]const u8 = null;
|
|
switch (operand.data) {
|
|
.type_expr, .identifier => {
|
|
const full = if (operand.data == .type_expr) operand.data.type_expr.name else operand.data.identifier.name;
|
|
if (std.mem.indexOfScalar(u8, full, '.')) |dot| {
|
|
pack_name = full[0..dot];
|
|
projection = full[dot + 1 ..];
|
|
} else {
|
|
pack_name = full;
|
|
}
|
|
},
|
|
.field_access => |fa| {
|
|
pack_name = switch (fa.object.data) {
|
|
.identifier => |id| id.name,
|
|
.type_expr => |te| te.name,
|
|
else => return null,
|
|
};
|
|
projection = fa.field;
|
|
},
|
|
else => return null,
|
|
}
|
|
return self.packTypeArgs(pack_name, projection);
|
|
}
|
|
|
|
/// Per-element types for a bound protocol pack: `pack_name` alone → the
|
|
/// element types; with `projection` (`xs.T`) → each element's protocol
|
|
/// type-arg. Null when `pack_name` isn't a bound pack. Caller owns the slice.
|
|
pub fn packTypeArgs(self: PackResolver, pack_name: []const u8, projection: ?[]const u8) ?[]TypeId {
|
|
const pat = self.l.pack_arg_types orelse return null;
|
|
const elems = pat.get(pack_name) orelse return null;
|
|
if (projection == null) return self.l.alloc.dupe(TypeId, elems) catch null;
|
|
const proto = if (self.l.pack_constraint) |pc| (pc.get(pack_name) orelse return null) else return null;
|
|
const arg_idx = self.l.lookupProtocolArg(proto, projection.?) orelse return null;
|
|
var out = std.ArrayList(TypeId).empty;
|
|
for (elems) |elem| {
|
|
const proj_ty = self.elementProtocolTypeArg(proto, elem, arg_idx) orelse blk: {
|
|
// The projection named a protocol type-arg this element's impl
|
|
// does not provide — there is no type for the slot. Surface it
|
|
// loudly: a diagnostic plus the `.unresolved` sentinel (a real
|
|
// `.void` here would read as a legitimate type downstream and
|
|
// silently corrupt the pack).
|
|
if (self.l.diagnostics) |diags| {
|
|
diags.addFmt(.err, null, "pack projection '{s}.{s}' has no type for a pack element: no matching `impl {s}(...) for {s}`", .{
|
|
pack_name, projection.?, proto, self.l.mangleTypeName(elem),
|
|
});
|
|
}
|
|
break :blk .unresolved;
|
|
};
|
|
out.append(self.l.alloc, proj_ty) catch return null;
|
|
}
|
|
return out.toOwnedSlice(self.l.alloc) catch null;
|
|
}
|
|
|
|
/// For a concrete `elem` conforming to parameterised `proto`, return the
|
|
/// `arg_idx`-th protocol type-arg from its `impl proto(args) for elem`
|
|
/// (scans `param_impl_map` for `proto\x00…\x00mangle(elem)`).
|
|
pub fn elementProtocolTypeArg(self: PackResolver, proto: []const u8, elem: TypeId, arg_idx: u32) ?TypeId {
|
|
const prefix = std.fmt.allocPrint(self.l.alloc, "{s}\x00", .{proto}) catch return null;
|
|
const suffix = std.fmt.allocPrint(self.l.alloc, "\x00{s}", .{self.l.mangleTypeName(elem)}) catch return null;
|
|
var it = self.l.param_impl_map.iterator();
|
|
while (it.next()) |entry| {
|
|
const k = entry.key_ptr.*;
|
|
if (std.mem.startsWith(u8, k, prefix) and std.mem.endsWith(u8, k, suffix)) {
|
|
for (entry.value_ptr.items) |impl| {
|
|
if (arg_idx < impl.target_args.len) return impl.target_args[arg_idx];
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
};
|