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:
36
examples/213-canonical-map.sx
Normal file
36
examples/213-canonical-map.sx
Normal 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;
|
||||
}
|
||||
154
src/ir/lower.zig
154
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 => {},
|
||||
}
|
||||
}
|
||||
|
||||
1
tests/expected/213-canonical-map.exit
Normal file
1
tests/expected/213-canonical-map.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
tests/expected/213-canonical-map.txt
Normal file
1
tests/expected/213-canonical-map.txt
Normal file
@@ -0,0 +1 @@
|
||||
42
|
||||
Reference in New Issue
Block a user