refactor(ir): extract GenericResolver (generics.zig) for substitution + mono keys (A4.1 step 2)

Generic substitution and monomorphization-key construction now live in one
module, src/ir/generics.zig, behind a *Lowering facade (GenericResolver),
mirroring CallResolver / ExprTyper. Moved verbatim:
- mangleTypeName + mangleParamList (the mono-key fragment builder),
- mangleGenericName (generic mono key), appendComptimeValueMangle (comptime-value
  fragment),
- buildTypeBindings (call-site type-param inference), inferGenericReturnType
  (generic return resolution).

inferGenericReturnType now uses a scoped TypeBindingScope (enter/exit with defer)
instead of a manual type_bindings save/restore — the PLAN-ARCH A4.1 "scoped
substitution env" shape; a generics.test.zig assertion confirms the prior
bindings are restored (the issue-0048/0050 leak class, for this field).

Lowering keeps a thin pub mangleTypeName wrapper delegating to
genericResolver().mangleTypeName, because ~30 cross-cutting callers (impl-map
keys, conversion keys, shape keys) reach it well beyond generics. mangleParamList
(sole caller was mangleTypeName) moved fully. The other 4 originals are deleted
(no fallback); their 6 call sites now go through self.genericResolver()
(calls.zig via self.l.genericResolver()).

matchTypeParam / extractTypeParam / isTypeParamDecl widened to pub (the moved
substitution logic calls them); genericResolver() accessor added. The 2
mangleTypeName / inferGenericReturnType unit tests moved from lower.test.zig to
generics.test.zig (driving GenericResolver directly) and wired into the barrel.

monomorphizeFunction / monomorphizePackFn intentionally stay in lower.zig (they
save/restore three fields across nested mono and call emission helpers) — a
heavier scoped-env adoption deferred to an optional sub-step 3.

zig build, zig build test, and tests/run_examples.sh (357/0) all green — no .ir
snapshot churn, confirming the move preserved mono-key/substitution output.
This commit is contained in:
agra
2026-06-02 21:28:31 +03:00
parent e1f167a1c3
commit 3ca68189c0
6 changed files with 462 additions and 370 deletions

View File

@@ -24,6 +24,7 @@ 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 semantic_diagnostics = @import("semantic_diagnostics.zig");
const TypeId = types.TypeId;
@@ -7908,10 +7909,10 @@ pub const Lowering = struct {
eff_args.append(self.alloc, effective_obj_node) catch unreachable;
for (c.args) |a| eff_args.append(self.alloc, a) catch unreachable;
var gbindings = self.buildTypeBindings(gen_fd, eff_args.items);
var gbindings = self.genericResolver().buildTypeBindings(gen_fd, eff_args.items);
defer gbindings.deinit();
const gmangled = self.mangleGenericName(qualified, gen_fd, &gbindings);
const gmangled = self.genericResolver().mangleGenericName(qualified, gen_fd, &gbindings);
if (!self.lowered_functions.contains(gmangled)) {
self.monomorphizeFunction(gen_fd, gmangled, &gbindings);
}
@@ -9790,111 +9791,13 @@ pub const Lowering = struct {
/// `args_ast` must be parallel to `fd.params`; for dot-calls the caller
/// prepends the receiver's AST node so positions align with `fd.params[0] = self`.
/// Caller owns the returned map and must call `.deinit()`.
fn buildTypeBindings(
self: *Lowering,
fd: *const ast.FnDecl,
args_ast: []const *const Node,
) std.StringHashMap(TypeId) {
var bindings = std.StringHashMap(TypeId).init(self.alloc);
const types_passed_explicitly = args_ast.len == fd.params.len;
for (fd.type_params) |tp| {
var found = false;
// Strategy 1: explicit — the param whose name matches `tp.name` IS
// the `$T: Type` declaration; the arg at that position is a type expression.
if (types_passed_explicitly) {
for (fd.params, 0..) |param, pi| {
if (std.mem.eql(u8, param.name, tp.name)) {
if (pi < args_ast.len and type_bridge.isTypeShapedAstNode(args_ast[pi], &self.module.types)) {
const ty = self.resolveTypeArg(args_ast[pi]);
bindings.put(tp.name, ty) catch {};
found = true;
}
break;
}
}
}
if (found) continue;
// Strategy 2: infer from value params that USE the type param
// (e.g. a: $T, b: T, items: []$T). Pick widest type across matches.
var inferred_ty: ?TypeId = null;
var s2_arg_idx: usize = 0;
for (fd.params) |param| {
const is_type_decl = isTypeParamDecl(&param, fd.type_params);
defer if (!is_type_decl) {
s2_arg_idx += 1;
};
if (is_type_decl) {
if (types_passed_explicitly) s2_arg_idx += 1;
continue;
}
const matched = self.matchTypeParam(param.type_expr, tp.name);
if (matched) {
if (s2_arg_idx < args_ast.len) {
const arg_ty = self.inferExprType(args_ast[s2_arg_idx]);
const extracted = self.extractTypeParam(param.type_expr, arg_ty, tp.name);
if (extracted) |ety| {
if (inferred_ty) |prev| {
if (ety == .f64 and prev != .f64) {
inferred_ty = ety;
} else if (ety == .f32 and prev != .f64 and prev != .f32) {
inferred_ty = ety;
}
} else {
inferred_ty = ety;
}
}
}
}
}
if (inferred_ty) |ty| {
bindings.put(tp.name, ty) catch {};
}
}
return bindings;
}
/// Mangle a generic call site into "base__Type1_Type2".
/// Returns a heap-allocated string owned by self.alloc.
fn mangleGenericName(
self: *Lowering,
base_name: []const u8,
fd: *const ast.FnDecl,
bindings: *const std.StringHashMap(TypeId),
) []const u8 {
var mangled_buf: [256]u8 = undefined;
var mangled_len: usize = 0;
for (base_name) |ch| {
if (mangled_len < mangled_buf.len) {
mangled_buf[mangled_len] = ch;
mangled_len += 1;
}
}
for (fd.type_params) |tp| {
for ("__") |ch| {
if (mangled_len < mangled_buf.len) {
mangled_buf[mangled_len] = ch;
mangled_len += 1;
}
}
const ty = bindings.get(tp.name) orelse .unresolved;
const type_name_str = self.mangleTypeName(ty);
for (type_name_str) |ch| {
if (mangled_len < mangled_buf.len) {
mangled_buf[mangled_len] = ch;
mangled_len += 1;
}
}
}
return self.alloc.dupe(u8, mangled_buf[0..mangled_len]) catch base_name;
}
/// Lower a call to a generic function by monomorphizing it with inferred type arguments.
fn lowerGenericCall(self: *Lowering, fd: *const ast.FnDecl, base_name: []const u8, call_node: *const ast.Call, lowered_args: []Ref) Ref {
var bindings = self.buildTypeBindings(fd, call_node.args);
var bindings = self.genericResolver().buildTypeBindings(fd, call_node.args);
defer bindings.deinit();
const types_passed_explicitly = call_node.args.len == fd.params.len;
const mangled_name = self.mangleGenericName(base_name, fd, &bindings);
const mangled_name = self.genericResolver().mangleGenericName(base_name, fd, &bindings);
if (!self.lowered_functions.contains(mangled_name)) {
self.monomorphizeFunction(fd, mangled_name, &bindings);
@@ -10498,7 +10401,7 @@ pub const Lowering = struct {
if (ct_fi >= call_node.args.len) break;
if (p.is_comptime) {
name_buf.appendSlice(self.alloc, "__ct_") catch return self.builder.constInt(0, .void);
self.appendComptimeValueMangle(&name_buf, call_node.args[ct_fi]);
self.genericResolver().appendComptimeValueMangle(&name_buf, call_node.args[ct_fi]);
}
ct_fi += 1;
}
@@ -10529,40 +10432,6 @@ pub const Lowering = struct {
return self.builder.call(fid, final_args, ret_ty);
}
/// Append a stable mangle segment for a comptime call-arg literal.
/// Supports int / bool / float / string literals; non-literals
/// degrade to "?" (the mono is still cached but two different
/// non-literal expressions sharing one call site would collide,
/// which is acceptable since they'd lower the same body anyway).
fn appendComptimeValueMangle(self: *Lowering, buf: *std.ArrayList(u8), node: *const Node) void {
switch (node.data) {
.int_literal => |lit| {
var tmp: [32]u8 = undefined;
const written = std.fmt.bufPrint(&tmp, "{d}", .{lit.value}) catch return;
buf.appendSlice(self.alloc, written) catch return;
},
.bool_literal => |lit| {
buf.appendSlice(self.alloc, if (lit.value) "true" else "false") catch return;
},
.float_literal => |lit| {
var tmp: [64]u8 = undefined;
const written = std.fmt.bufPrint(&tmp, "{d}", .{lit.value}) catch return;
for (written) |c| {
buf.append(self.alloc, if (c == '.') '_' else if (c == '-') 'n' else c) catch return;
}
},
.string_literal => |lit| {
// Hash the string to a fixed-length tag — keeps the
// mangle short and stable for arbitrary content.
var h = std.hash.Wyhash.init(0);
h.update(lit.raw);
var tmp: [32]u8 = undefined;
const written = std.fmt.bufPrint(&tmp, "s{x}", .{h.final()}) catch return;
buf.appendSlice(self.alloc, written) catch return;
},
else => buf.append(self.alloc, '?') catch return,
}
}
/// Build a single mono fn for the given pack-fn + concrete arg types.
/// The mono carries N positional pack-params (synthesised names
@@ -11485,7 +11354,7 @@ pub const Lowering = struct {
/// Format a type name for function name mangling (identifier-safe).
/// E.g. *Point → "ptr_Point", []s32 → "slice_s32", [3]f64 → "array_3_f64".
/// Check if a param type expression references a type param name (possibly nested).
fn matchTypeParam(_: *Lowering, type_node: *const Node, tp_name: []const u8) bool {
pub fn matchTypeParam(_: *Lowering, type_node: *const Node, tp_name: []const u8) bool {
return switch (type_node.data) {
.type_expr => |te| std.mem.eql(u8, te.name, tp_name),
.identifier => |id| std.mem.eql(u8, id.name, tp_name),
@@ -11523,7 +11392,7 @@ pub const Lowering = struct {
/// Extract the concrete type that corresponds to a type param from an arg type.
/// E.g., param type []$T with arg type []s64 → T = s64.
fn extractTypeParam(self: *Lowering, type_node: *const Node, arg_ty: TypeId, tp_name: []const u8) ?TypeId {
pub fn extractTypeParam(self: *Lowering, type_node: *const Node, arg_ty: TypeId, tp_name: []const u8) ?TypeId {
return switch (type_node.data) {
.type_expr => |te| if (std.mem.eql(u8, te.name, tp_name)) arg_ty else null,
.identifier => |id| if (std.mem.eql(u8, id.name, tp_name)) arg_ty else null,
@@ -11590,70 +11459,12 @@ pub const Lowering = struct {
};
}
/// Mangle a TypeId into its mono-key fragment. Thin delegation to the
/// canonical owner (`GenericResolver`, `generics.zig`); kept on `Lowering`
/// because ~30 cross-cutting callers (impl-map keys, conversion keys, shape
/// keys) reach it here, well beyond generic monomorphization.
pub fn mangleTypeName(self: *Lowering, ty: TypeId) []const u8 {
// Builtin types
if (ty == .s8) return "s8";
if (ty == .s16) return "s16";
if (ty == .s32) return "s32";
if (ty == .s64) return "s64";
if (ty == .u8) return "u8";
if (ty == .u16) return "u16";
if (ty == .u32) return "u32";
if (ty == .u64) return "u64";
if (ty == .f32) return "f32";
if (ty == .f64) return "f64";
if (ty == .bool) return "bool";
if (ty == .void) return "void";
if (ty == .string) return "string";
if (ty == .any) return "Any";
if (ty == .usize) return "usize";
if (ty == .isize) return "isize";
const info = self.module.types.get(ty);
return switch (info) {
.@"struct" => |s| self.module.types.getString(s.name),
.@"union" => |u| self.module.types.getString(u.name),
.tagged_union => |u| self.module.types.getString(u.name),
.@"enum" => |e| self.module.types.getString(e.name),
.pointer => |p| blk: {
const inner = self.mangleTypeName(p.pointee);
break :blk std.fmt.allocPrint(self.alloc, "ptr_{s}", .{inner}) catch "pointer";
},
.many_pointer => |p| blk: {
const inner = self.mangleTypeName(p.element);
break :blk std.fmt.allocPrint(self.alloc, "mptr_{s}", .{inner}) catch "many_pointer";
},
.slice => |s| blk: {
const inner = self.mangleTypeName(s.element);
break :blk std.fmt.allocPrint(self.alloc, "SL_{s}", .{inner}) catch "slice";
},
.array => |a| blk: {
const inner = self.mangleTypeName(a.element);
break :blk std.fmt.allocPrint(self.alloc, "AR_{d}_{s}", .{ a.length, inner }) catch "array";
},
.signed => |w| std.fmt.allocPrint(self.alloc, "s{d}", .{w}) catch "signed",
.unsigned => |w| std.fmt.allocPrint(self.alloc, "u{d}", .{w}) catch "unsigned",
.optional => |o| blk: {
const inner = self.mangleTypeName(o.child);
break :blk std.fmt.allocPrint(self.alloc, "opt_{s}", .{inner}) catch "optional";
},
.vector => |v| blk: {
const inner = self.mangleTypeName(v.element);
break :blk std.fmt.allocPrint(self.alloc, "vec_{d}_{s}", .{ v.length, inner }) catch "vector";
},
.closure => |c| self.mangleParamList("cl", c.params, c.ret),
.function => |f| self.mangleParamList("fn", f.params, f.ret),
.tuple => |t| blk: {
var buf = std.ArrayList(u8).empty;
buf.appendSlice(self.alloc, "tu") catch break :blk "tuple";
for (t.fields) |fid| {
buf.append(self.alloc, '_') catch break :blk "tuple";
buf.appendSlice(self.alloc, self.mangleTypeName(fid)) catch break :blk "tuple";
}
break :blk buf.items;
},
else => @tagName(info),
};
return self.genericResolver().mangleTypeName(ty);
}
/// Collect impl entries visible from `current_source_file` — defined in
@@ -11697,18 +11508,6 @@ pub const Lowering = struct {
}
}
fn mangleParamList(self: *Lowering, prefix: []const u8, params: []const TypeId, ret: TypeId) []const u8 {
var buf = std.ArrayList(u8).empty;
buf.appendSlice(self.alloc, prefix) catch return prefix;
for (params) |p| {
buf.append(self.alloc, '_') catch return prefix;
buf.appendSlice(self.alloc, self.mangleTypeName(p)) catch return prefix;
}
buf.appendSlice(self.alloc, "__") catch return prefix;
buf.appendSlice(self.alloc, self.mangleTypeName(ret)) catch return prefix;
return buf.items;
}
/// Resolve type category names (like "int", "struct", "float") to matching TypeId tag values.
/// Returns a list of TypeId index values that match the category.
fn resolveTypeCategoryTags(self: *Lowering, name: []const u8) []const u64 {
@@ -12167,7 +11966,7 @@ pub const Lowering = struct {
/// Check if a param is a type param declaration ($T: Type).
/// A type param declaration has param.name == one of the type_params names.
fn isTypeParamDecl(param: *const ast.Param, type_params: []const ast.StructTypeParam) bool {
pub fn isTypeParamDecl(param: *const ast.Param, type_params: []const ast.StructTypeParam) bool {
for (type_params) |tp| {
if (std.mem.eql(u8, param.name, tp.name)) return true;
}
@@ -14454,71 +14253,8 @@ pub const Lowering = struct {
return .{ .l = self };
}
/// Infer the return type of a generic function call by resolving type bindings.
pub fn inferGenericReturnType(self: *Lowering, fd: *const ast.FnDecl, c: *const ast.Call) TypeId {
if (fd.return_type == null) return .void;
// Build ALL type bindings from call args before resolving return type
var tmp_bindings = std.StringHashMap(TypeId).init(self.alloc);
defer tmp_bindings.deinit();
for (fd.type_params) |tp| {
// Strategy 1: direct type param decl ($T: Type) — param.name == tp.name.
// Only fires when the caller actually supplied a type expression at
// that position; otherwise fall through to value-based inference.
var found = false;
for (fd.params, 0..) |param, pi| {
if (std.mem.eql(u8, param.name, tp.name)) {
if (pi < c.args.len and type_bridge.isTypeShapedAstNode(c.args[pi], &self.module.types)) {
const ty = self.resolveTypeArg(c.args[pi]);
tmp_bindings.put(tp.name, ty) catch {};
found = true;
}
break;
}
}
if (found) continue;
// Strategy 2: inferred from usage (a: $T, b: T) — check ALL matching params, pick widest
var inferred_ty: ?TypeId = null;
for (fd.params, 0..) |param, pi| {
if (param.type_expr.data == .type_expr) {
const te = param.type_expr.data.type_expr;
if (std.mem.eql(u8, te.name, tp.name)) {
if (pi < c.args.len) {
const arg_ty = self.inferExprType(c.args[pi]);
if (inferred_ty) |prev| {
if (arg_ty == .f64 and prev != .f64) {
inferred_ty = arg_ty;
} else if (arg_ty == .f32 and prev != .f64 and prev != .f32) {
inferred_ty = arg_ty;
}
} else {
inferred_ty = arg_ty;
}
}
}
}
}
if (inferred_ty) |ty| {
tmp_bindings.put(tp.name, ty) catch {};
}
}
// Resolve return type with whatever bindings we built. Even an
// empty `tmp_bindings` is a valid input — non-generic literal
// return types (e.g. `walk(..$args) -> string`) still need to
// resolve through `resolveTypeWithBindings`, not fall through
// to the historical `.s64` default. The default silently
// misclassified pack-fn calls whose return type was a fixed
// literal — every consumer (e.g. print's pack-shape mangling)
// inferred `s64` and routed the value through the wrong Any
// tag.
const saved = self.type_bindings;
self.type_bindings = tmp_bindings;
const ret = self.resolveTypeWithBindings(fd.return_type.?);
self.type_bindings = saved;
return ret;
pub fn genericResolver(self: *Lowering) GenericResolver {
return .{ .l = self };
}
/// Lower the `xx` operator (type coercion).