lang F1 Phase 6: canonical heterogeneous map — $R inference through closure params

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.
This commit is contained in:
agra
2026-05-30 03:46:46 +03:00
parent f2e1f401ce
commit 8e74e4acb2
5 changed files with 169 additions and 27 deletions

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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 => {},
}
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
42