From 8e74e4acb25b7a2720e5d18480b13addaa0635c6 Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 30 May 2026 03:46:46 +0300 Subject: [PATCH] =?UTF-8?q?lang=20F1=20Phase=206:=20canonical=20heterogene?= =?UTF-8?q?ous=20map=20=E2=80=94=20$R=20inference=20through=20closure=20pa?= =?UTF-8?q?rams?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The full canonical `map` now compiles and runs (examples/213 → 42): map :: (mapper: Closure(..sources.T) -> $R, ..sources: VL) -> VL($R) Final piece: infer a pack-fn's generic return `$R` from a closure-typed prefix param's lowered return type. - collectGenericNames descends into closure_type_expr (params + return), so `$R` in `Closure(..) -> $R` registers as a function type-param. - matchTypeParam/extractTypeParam descend into closures: `$R` is extracted from the lowered mapper's closure `.ret`. - lowerPackFnCall infers type-param bindings from the lowered prefix args, folds them into the mangle, and threads them into monomorphizePackFn, which installs self.type_bindings for return-type resolution + body lowering (`-> VL($R)` ⇒ VL(s64); `Combined($R, ..)` ⇒ Combined(s64, ..)). s64-elimination follow-through: - An unbound generic `$R` resolves to `.unresolved` in resolveTypeWithBindings rather than fabricating an empty-struct stub (`R{}`). - Lambda return-type inference skips an `.unresolved` target-closure ret and infers from the body, so the concrete return drives `$R`. - The `.unresolved` codegen tripwire then caught a latent bug: a generic-struct source impl (`impl VL($R) for Combined($R, ..$Ts)`) was declaring its template method `Combined.get` (`-> $R`) as a standalone IR function. Fixed: a generic-struct source registers methods as TEMPLATES only (findable in fn_ast_map for per-instance monomorphization via createProtocolThunk), never declareFunction'd. Feature 1 (heterogeneous variadic packs) all six phases complete. 248 examples + all unit tests green. --- examples/213-canonical-map.sx | 36 ++++++ src/ir/lower.zig | 154 +++++++++++++++++++++----- src/parser.zig | 4 + tests/expected/213-canonical-map.exit | 1 + tests/expected/213-canonical-map.txt | 1 + 5 files changed, 169 insertions(+), 27 deletions(-) create mode 100644 examples/213-canonical-map.sx create mode 100644 tests/expected/213-canonical-map.exit create mode 100644 tests/expected/213-canonical-map.txt diff --git a/examples/213-canonical-map.sx b/examples/213-canonical-map.sx new file mode 100644 index 0000000..5c2c2d6 --- /dev/null +++ b/examples/213-canonical-map.sx @@ -0,0 +1,36 @@ +// Phase 6 — the canonical heterogeneous `map`, end to end. A pack-fn whose +// return type `$R` is inferred from the mapper's closure return: +// - `mapper: Closure(..sources.T) -> $R` types the lambda's params from the +// projected pack element types, and its body (`a + b`) drives `$R`. +// - `$R` is inferred at the call site from the lowered mapper's closure ret, +// bound into the mono (`-> VL($R)` ⇒ `VL(s64)`, `Combined($R, ..)` ⇒ +// `Combined(s64, ..)`), and folded into the mangle. +// - `(..sources)` materializes the pack into the `(..VL(Ts))` field (per-element +// erase) and `mapper(..sources.get)` projects+spreads; `xx c` erases the +// generic-struct instance to `VL(s64)` via the generic impl's monomorphized +// thunk. + +#import "modules/std.sx"; + +VL :: protocol(T: Type) { get :: () -> T; } +IntCell :: struct { v: s64; } +impl VL(s64) for IntCell { get :: (self: *IntCell) -> s64 => self.v; } + +Combined :: struct($R: Type, ..$Ts: []Type) { + sources: (..VL(Ts)); + value: $R; +} +impl VL($R) for Combined($R, ..$Ts) { get :: (self: *Combined) -> $R => self.value; } + +map :: (mapper: Closure(..sources.T) -> $R, ..sources: VL) -> VL($R) { + c : Combined($R, ..sources.T) = ---; + c.sources = (..sources); + c.value = mapper(..sources.get); + return xx c; +} + +main :: () -> s32 { + r := map((a, b) => a + b, IntCell.{ v = 40 }, IntCell.{ v = 2 }); + print("{}\n", r.get()); // 42 + 0; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 0809aab..844b9dc 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -7334,17 +7334,20 @@ pub const Lowering = struct { if (lam.return_type) |rt| { break :blk type_bridge.resolveAstType(rt, &self.module.types); } - // Use target closure return type if available + // Use target closure return type if available — but only when it's + // a resolved type. An `.unresolved` ret comes from an unbound + // generic (`Closure(..) -> $R`); fall through to infer it from the + // body so the concrete return drives `$R` inference at the call site. if (self.target_type) |tt| { if (!tt.isBuiltin()) { const tti = self.module.types.get(tt); - if (tti == .closure) break :blk tti.closure.ret; + if (tti == .closure and tti.closure.ret != .unresolved) break :blk tti.closure.ret; // Unwrap ?Closure(...) → Closure(...) if (tti == .optional) { const inner = tti.optional.child; if (!inner.isBuiltin()) { const inner_info = self.module.types.get(inner); - if (inner_info == .closure) break :blk inner_info.closure.ret; + if (inner_info == .closure and inner_info.closure.ret != .unresolved) break :blk inner_info.closure.ret; } } } @@ -9165,12 +9168,14 @@ pub const Lowering = struct { // comptime type-pack `..$args` has no constraint to check. var pack_protocol: ?[]const u8 = null; var pack_is_comptime = false; + var pack_name: []const u8 = ""; { var fi: usize = 0; for (fd.params) |p| { if (isPackParam(p)) { pack_start = fi; pack_is_comptime = p.is_comptime; + pack_name = p.name; if (p.is_pack and p.type_expr.data == .type_expr) { pack_protocol = p.type_expr.data.type_expr.name; } @@ -9181,12 +9186,39 @@ pub const Lowering = struct { } } - // Lower the runtime prefix args and the pack args up front, taking each - // pack type from the lowered value (`getRefType`) rather than a - // pre-lowering `inferExprType` guess: a lowered value always has a - // concrete type, so a monomorphised pack param can never end up - // `.unresolved` from incomplete static inference. (A comptime `..$args` - // pack still uses `inferExprType` — its args may be type-position.) + // Lower the PACK args first, taking each type from the lowered value + // (`getRefType`) — never a pre-lowering `inferExprType` guess. Knowing + // the pack element types up front lets the prefix args (e.g. + // `mapper: Closure(..sources.T) -> $R`) resolve against them, so a + // lambda arg types its params from the projected closure signature. + // (A comptime `..$args` pack keeps `inferExprType` — its args may be + // type-position.) + var pack_refs = std.ArrayList(Ref).empty; + defer pack_refs.deinit(self.alloc); + for (call_node.args[pack_start..]) |a| { + const r = self.lowerExpr(a); + pack_refs.append(self.alloc, r) catch return self.builder.constInt(0, .void); + if (pack_is_comptime) { + const it = self.inferExprType(a); + pack_arg_types.append(self.alloc, if (it == .unresolved) self.builder.getRefType(r) else it) catch return self.builder.constInt(0, .void); + } else { + pack_arg_types.append(self.alloc, self.builder.getRefType(r)) catch return self.builder.constInt(0, .void); + } + } + + // Install the pack's element types + constraint so prefix-arg param + // types like `Closure(..sources.T)` resolve while lowering the prefix. + var pat_map = std.StringHashMap([]const TypeId).init(self.alloc); + defer pat_map.deinit(); + pat_map.put(pack_name, pack_arg_types.items) catch {}; + var pcon_map = std.StringHashMap([]const u8).init(self.alloc); + defer pcon_map.deinit(); + if (pack_protocol) |proto| pcon_map.put(pack_name, proto) catch {}; + const saved_pat = self.pack_arg_types; + const saved_pcon = self.pack_constraint; + self.pack_arg_types = pat_map; + if (pack_protocol != null) self.pack_constraint = pcon_map; + var args = std.ArrayList(Ref).empty; defer args.deinit(self.alloc); { @@ -9206,25 +9238,37 @@ pub const Lowering = struct { ri += 1; } } - for (call_node.args[pack_start..]) |a| { - if (pack_is_comptime) { - // A comptime `..$args` arg's intended type follows the - // language default (e.g. an int literal is s64) which - // `inferExprType` encodes; the lowered value may be narrower - // (s32). Prefer inference; fall back to the lowered value's - // type only when inference genuinely can't tell. - const r = self.lowerExpr(a); - args.append(self.alloc, r) catch return self.builder.constInt(0, .void); - const it = self.inferExprType(a); - const ty = if (it == .unresolved) self.builder.getRefType(r) else it; - pack_arg_types.append(self.alloc, ty) catch return self.builder.constInt(0, .void); - } else { - const r = self.lowerExpr(a); - args.append(self.alloc, r) catch return self.builder.constInt(0, .void); - pack_arg_types.append(self.alloc, self.builder.getRefType(r)) catch return self.builder.constInt(0, .void); + self.pack_arg_types = saved_pat; + self.pack_constraint = saved_pcon; + + // Infer type-param bindings (e.g. `$R` in `mapper: Closure(..) -> $R`) + // from the lowered prefix args. `args.items` holds the non-comptime + // prefix refs in declaration order; match each prefix param's declared + // type against its arg's concrete type to bind the function's + // type-params. These flow into the mangle and the mono's + // `self.type_bindings` so `-> VL($R)` / `Combined($R, ..)` resolve. + var tparam_bindings = std.StringHashMap(TypeId).init(self.alloc); + defer tparam_bindings.deinit(); + if (fd.type_params.len > 0) { + var pref_ref_idx: usize = 0; + for (fd.params) |p| { + if (isPackParam(p)) break; + if (p.is_comptime) continue; + if (pref_ref_idx >= args.items.len) break; + const arg_ty = self.builder.getRefType(args.items[pref_ref_idx]); + for (fd.type_params) |tp| { + if (tparam_bindings.contains(tp.name)) continue; + if (self.extractTypeParam(p.type_expr, arg_ty, tp.name)) |ety| { + if (ety != .unresolved) tparam_bindings.put(tp.name, ety) catch {}; + } + } + pref_ref_idx += 1; } } + // Append the (already-lowered) pack args after the prefix args. + for (pack_refs.items) |r| args.append(self.alloc, r) catch return self.builder.constInt(0, .void); + // Per-position conformance: each pack arg must impl the constraint // protocol. Only enforced for a known protocol constraint — an unknown // name (e.g. a plain type used as a pack constraint) is left alone. @@ -9258,6 +9302,13 @@ pub const Lowering = struct { } ct_fi += 1; } + // Inferred type-param bindings (deterministic by fd.type_params order). + for (fd.type_params) |tp| { + if (tparam_bindings.get(tp.name)) |ty| { + name_buf.appendSlice(self.alloc, "__tp_") catch return self.builder.constInt(0, .void); + name_buf.appendSlice(self.alloc, self.mangleTypeName(ty)) catch return self.builder.constInt(0, .void); + } + } name_buf.appendSlice(self.alloc, "__pack") catch return self.builder.constInt(0, .void); for (pack_arg_types.items) |t| { name_buf.append(self.alloc, '_') catch return self.builder.constInt(0, .void); @@ -9266,7 +9317,7 @@ pub const Lowering = struct { const mangled = name_buf.items; if (!self.lowered_functions.contains(mangled)) { - self.monomorphizePackFn(fd, mangled, pack_arg_types.items, call_node); + self.monomorphizePackFn(fd, mangled, pack_arg_types.items, call_node, &tparam_bindings); } const fid = self.resolveFuncByName(mangled) orelse return self.builder.constInt(0, .void); @@ -9325,6 +9376,7 @@ pub const Lowering = struct { mangled_name: []const u8, arg_types: []const TypeId, call_node: *const ast.Call, + type_bindings: *const std.StringHashMap(TypeId), ) void { const owned_name = self.alloc.dupe(u8, mangled_name) catch return; self.lowered_functions.put(owned_name, {}) catch {}; @@ -9362,10 +9414,16 @@ pub const Lowering = struct { const saved_pcon = self.pack_constraint; const saved_iri = self.inline_return_target; const saved_ctx_ref = self.current_ctx_ref; + const saved_type_bindings = self.type_bindings; self.func_defer_base = self.defer_stack.items.len; self.block_terminated = false; self.inline_return_target = null; + // Generic type-params inferred at the call site (e.g. `$R` from the + // mapper's closure return). Installed for the whole mono so + // return-type resolution and body lowering substitute them. + self.type_bindings = type_bindings.*; defer { + self.type_bindings = saved_type_bindings; self.scope = saved_scope; self.func_defer_base = saved_defer_base; self.block_terminated = saved_block_terminated; @@ -10187,6 +10245,11 @@ pub const Lowering = struct { .many_pointer_type_expr => |mp| matchTypeParamStatic(mp.element_type, tp_name), .optional_type_expr => |ot| matchTypeParamStatic(ot.inner_type, tp_name), .array_type_expr => |at| matchTypeParamStatic(at.element_type, tp_name), + .closure_type_expr => |ct| blk: { + for (ct.param_types) |pt| if (matchTypeParamStatic(pt, tp_name)) break :blk true; + if (ct.return_type) |rt| if (matchTypeParamStatic(rt, tp_name)) break :blk true; + break :blk false; + }, else => false, }; } @@ -10200,6 +10263,11 @@ pub const Lowering = struct { .many_pointer_type_expr => |mp| matchTypeParamStatic(mp.element_type, tp_name), .optional_type_expr => |ot| matchTypeParamStatic(ot.inner_type, tp_name), .array_type_expr => |at| matchTypeParamStatic(at.element_type, tp_name), + .closure_type_expr => |ct| blk: { + for (ct.param_types) |pt| if (matchTypeParamStatic(pt, tp_name)) break :blk true; + if (ct.return_type) |rt| if (matchTypeParamStatic(rt, tp_name)) break :blk true; + break :blk false; + }, else => false, }; } @@ -10251,6 +10319,24 @@ pub const Lowering = struct { else => null, }; }, + .closure_type_expr => |ct| blk: { + if (arg_ty.isBuiltin()) break :blk null; + const info = self.module.types.get(arg_ty); + const c_params: []const TypeId, const c_ret: TypeId = switch (info) { + .closure => |c| .{ c.params, c.ret }, + .function => |f| .{ f.params, f.ret }, + else => break :blk null, + }; + // Prefer the return position (`Closure(...) -> $R`), then params. + if (ct.return_type) |rt| { + if (self.extractTypeParam(rt, c_ret, tp_name)) |ety| break :blk ety; + } + for (ct.param_types, 0..) |pt, i| { + if (i >= c_params.len) break; + if (self.extractTypeParam(pt, c_params[i], tp_name)) |ety| break :blk ety; + } + break :blk null; + }, else => null, }; } @@ -11209,6 +11295,13 @@ pub const Lowering = struct { }, else => {}, } + // An unbound generic type param (`$R` with no active binding) must not + // fabricate an empty-struct stub — that surfaces as `R{}` downstream. + // Return `.unresolved` so callers (e.g. lambda return-type inference, + // call-site `$R` inference) treat it as not-yet-known. + if (node.data == .type_expr and node.data.type_expr.is_generic) { + return .unresolved; + } // Alias resolution (`ShaderHandle :: u32`, `Vec4 :: // Vector(4,f32)`) is now handled inside `resolveTypeName` // via the `TypeTable.aliases` borrow loaned at lowerRoot. @@ -12657,12 +12750,19 @@ pub const Lowering = struct { const si = table.get(src_ty); if (!src_ty.isBuiltin() and si == .@"struct") { const src_name = self.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.struct_template_map.contains(src_name); for (methods.items) |mfd| { const q = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ src_name, mfd.name }) catch continue; if (self.fn_ast_map.contains(q)) continue; // first impl wins self.fn_ast_map.put(q, mfd) catch {}; self.import_flags.put(q, is_imported) catch {}; - self.declareFunction(mfd, q); + if (!is_generic_src) self.declareFunction(mfd, q); } } } diff --git a/src/parser.zig b/src/parser.zig index e094cb7..6ef1920 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -1742,6 +1742,10 @@ pub const Parser = struct { .parameterized_type_expr => |pte| { for (pte.args) |arg| collectGenericNames(arg, list, allocator); }, + .closure_type_expr => |cte| { + for (cte.param_types) |pt| collectGenericNames(pt, list, allocator); + if (cte.return_type) |rt| collectGenericNames(rt, list, allocator); + }, else => {}, } } diff --git a/tests/expected/213-canonical-map.exit b/tests/expected/213-canonical-map.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/213-canonical-map.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/213-canonical-map.txt b/tests/expected/213-canonical-map.txt new file mode 100644 index 0000000..d81cc07 --- /dev/null +++ b/tests/expected/213-canonical-map.txt @@ -0,0 +1 @@ +42