diff --git a/src/ir/lower.zig b/src/ir/lower.zig index d6e1961..2c6179a 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -45,6 +45,7 @@ const lower_ffi = @import("lower/ffi.zig"); const lower_objc_class = @import("lower/objc_class.zig"); const lower_call = @import("lower/call.zig"); const lower_pack = @import("lower/pack.zig"); +const lower_generic = @import("lower/generic.zig"); const TypeId = types.TypeId; const StringId = types.StringId; @@ -3524,798 +3525,6 @@ pub const Lowering = struct { // ── Defer/Push/MultiAssign ────────────────────────────────────── - pub fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name: []const u8, bindings: *std.StringHashMap(TypeId)) void { - // Mark as lowered before lowering (prevents infinite recursion) - // Need to dupe the name since mangled_name may be stack-allocated - const owned_name = self.alloc.dupe(u8, mangled_name) catch return; - self.lowered_functions.put(owned_name, {}) catch {}; - - // Save builder state - const saved_func = self.builder.func; - const saved_block = self.builder.current_block; - const saved_counter = self.builder.inst_counter; - const saved_scope = self.scope; - const saved_bindings = self.type_bindings; - const saved_defer_base = self.func_defer_base; - const saved_block_terminated = self.block_terminated; - const saved_target = self.target_type; - // Pack-fn mono state is lexical to the pack-fn body. A generic - // function called from inside a pack-fn mono (e.g. - // `build(args: []Type, $ret: Type)` invoked from - // `probe(..$args) { build($args, void) }`) must not inherit the - // caller's pack maps — `lowerFieldAccess`'s `.len` - // intercept would otherwise constant-fold the callee's - // same-named param to whichever shape triggered the first mono - // and bake the wrong arity into the cached IR. Same shape of - // fix as `lazyLowerFunction` (issue-0048, commit 0ede097). - const saved_pan = self.pack_arg_nodes; - const saved_ppc = self.pack_param_count; - const saved_pat = self.pack_arg_types; - const saved_iri = self.inline_return_target; - self.pack_arg_nodes = null; - self.pack_param_count = null; - self.pack_arg_types = null; - self.inline_return_target = null; - defer { - self.pack_arg_nodes = saved_pan; - self.pack_param_count = saved_ppc; - self.pack_arg_types = saved_pat; - self.inline_return_target = saved_iri; - } - self.func_defer_base = self.defer_stack.items.len; - self.block_terminated = false; - - // Install type bindings - self.type_bindings = bindings.*; - - // Pin to the template's defining module for the whole monomorphization - // (return type, param types, body), so a library-internal bare TYPE ref - // — e.g. `List(T).append`'s `alloc: Allocator` default-param type, or a - // body reference to a type visible only in the template's module — - // resolves where it is visible, not at the (possibly cross-module) call - // site. This is the issue-0100-F1 plain-fn pin extended to generic - // instantiation; without it the non-transitive bare-TYPE gate (E4) would - // reject a 2-flat-hop library type the call site cannot see directly. - // A synthesized / sourceless body keeps the caller's context. - const saved_source_mono = self.current_source_file; - defer self.setCurrentSourceFile(saved_source_mono); - if (fd.body.source_file) |src| self.setCurrentSourceFile(src); - - // Resolve return type with type bindings active. The body's tail - // expression inherits this as its target_type so bare `.{...}` - // literals resolve to the monomorphised return type instead of - // whatever leaked in from the caller (e.g. caller's xx target). - const ret_ty = self.resolveReturnType(fd); - self.target_type = ret_ty; - - const wants_ctx = self.funcWantsImplicitCtx(fd); - const saved_ctx_ref_mono = self.current_ctx_ref; - defer self.current_ctx_ref = saved_ctx_ref_mono; - - // Build param list (substituting type params, skipping type param declarations). - // Prepend `__sx_ctx: *void` at slot 0 if the function gets the implicit param. - var params = std.ArrayList(Function.Param).empty; - if (wants_ctx) { - params.append(self.alloc, .{ - .name = self.module.types.internString("__sx_ctx"), - .ty = self.module.types.ptrTo(.void), - }) catch unreachable; - } - for (fd.params) |p| { - if (isTypeParamDecl(&p, fd.type_params)) continue; - const pty = self.resolveParamType(&p); - params.append(self.alloc, .{ - .name = self.module.types.internString(p.name), - .ty = pty, - }) catch unreachable; - } - - // Create the monomorphized function - const name_id = self.module.types.internString(owned_name); - const func_id = self.builder.beginFunction(name_id, params.items, ret_ty); - _ = func_id; - self.builder.currentFunc().has_implicit_ctx = wants_ctx; - - // Create entry block - const entry_name = self.module.types.internString("entry"); - const entry = self.builder.appendBlock(entry_name, &.{}); - self.builder.switchToBlock(entry); - if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); - - // Create scope and bind params - var scope = Scope.init(self.alloc, null); - defer scope.deinit(); - self.scope = &scope; - - { - var param_idx: u32 = if (wants_ctx) 1 else 0; - for (fd.params) |p| { - if (isTypeParamDecl(&p, fd.type_params)) continue; - const pty = self.resolveParamType(&p); - const slot = self.builder.alloca(pty); - const param_ref = Ref.fromIndex(param_idx); - self.builder.store(slot, param_ref); - scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); - param_idx += 1; - } - } - - // Handle builtin function bodies (e.g. #builtin sqrt monomorphized to sqrt__f32) - if (fd.body.data == .builtin_expr) { - // Emit builtin call with param 0, then return - if (resolveBuiltin(fd.name)) |bid| { - const param0 = Ref.fromIndex(0); - const result = self.builder.callBuiltin(bid, &.{param0}, ret_ty); - self.builder.ret(result, ret_ty); - } else { - self.ensureTerminator(ret_ty); - } - self.builder.finalize(); - } else { - // Lower the function body - if (ret_ty != .void) { - const body_val = self.lowerBlockValue(fd.body); - if (!self.currentBlockHasTerminator()) { - if (body_val) |val| { - const val_ty = self.builder.getRefType(val); - const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val; - self.builder.ret(coerced, ret_ty); - } else { - self.ensureTerminator(ret_ty); - } - } - } else { - self.lowerBlock(fd.body); - self.ensureTerminator(ret_ty); - } - self.builder.finalize(); - } - - // Restore builder state - self.type_bindings = saved_bindings; - self.scope = saved_scope; - self.func_defer_base = saved_defer_base; - self.block_terminated = saved_block_terminated; - self.target_type = saved_target; - self.builder.func = saved_func; - self.builder.current_block = saved_block; - self.builder.inst_counter = saved_counter; - } - - // ── Reflection builtins ──────────────────────────────────────── - - /// Resolve a type argument from a call expression. Handles: - /// - Type param bindings ($T → concrete type via type_bindings) - /// - Direct type names (Vec4 → lookup in TypeTable) - /// - type_expr AST nodes - /// True iff `node` matches an AST shape that `resolveTypeArg` - /// can resolve to a concrete TypeId without falling through to - /// the silent `.s64` default. Used by `tryLowerReflectionCall` - /// to split static-fold from dynamic-builtin-call paths. - /// - /// Static-arg shapes mirror the explicit arms of `resolveTypeArg`: - /// - type_expr / identifier (type name or bound generic) - /// - pack_index_type_expr (`$pack[]`) - /// - compound type literals (pointer, array, slice, optional, - /// many_pointer, function_type_expr) - /// - parameterised type-constructor `call` (Vector, List, etc.) - /// - tuple_literal as a tuple TYPE - /// - /// Dynamic shapes (index_expr, field_access, runtime locals, - /// etc.) fall to the alternative path that emits a builtin_call. - pub fn isStaticTypeArg(self: *Lowering, node: *const Node) bool { - switch (node.data) { - .type_expr => |te| { - // A type-keyword name (e.g. `s64`) is always static. - // A user-defined name that happens to be in scope as - // a runtime variable (`x: Type = s64; type_name(x)`) - // is NOT static — route through the dynamic builtin - // call so the runtime lookup table fires. - if (self.scope) |scope| { - if (scope.lookup(te.name) != null) return false; - } - return true; - }, - .identifier => |id| { - if (self.scope) |scope| { - if (scope.lookup(id.name) != null) return false; - } - return true; - }, - .pack_index_type_expr, - .pointer_type_expr, - .many_pointer_type_expr, - .array_type_expr, - .slice_type_expr, - .optional_type_expr, - .function_type_expr, - .tuple_literal, - .call, - => return true, - else => return false, - } - } - - /// True iff `node` is a Type-shaped expression that resolves to a - /// concrete TypeId at lower time WITHOUT being a runtime variable - /// reference. Differs from `isStaticTypeArg` in that we exclude - /// identifiers that are in scope as runtime locals/globals — those - /// are runtime Type values (e.g. `t: Type = f64`) and the - /// comparison fold can't statically resolve them. - fn isStaticTypeRef(self: *Lowering, node: *const Node) bool { - switch (node.data) { - .type_expr => |te| { - // Compound type names (`s64`, `Point`, `Vec4`) resolve - // statically. If the name is also a runtime var in - // scope, it's a value reference, not a type ref. - if (self.scope) |scope| { - if (scope.lookup(te.name) != null) return false; - } - return self.isKnownTypeName(te.name) or - self.module.types.findByName(self.module.types.internString(te.name)) != null or - self.program_index.type_alias_map.get(te.name) != null; - }, - .identifier => |id| { - if (self.scope) |scope| { - if (scope.lookup(id.name) != null) return false; - } - return self.isKnownTypeName(id.name) or - self.module.types.findByName(self.module.types.internString(id.name)) != null or - self.program_index.type_alias_map.get(id.name) != null; - }, - .pointer_type_expr, - .many_pointer_type_expr, - .array_type_expr, - .slice_type_expr, - .optional_type_expr, - .function_type_expr, - .pack_index_type_expr, - => return true, - .call => |cl| { - // `type_of(x)` resolves statically when `x`'s type is - // known — which it always is for a typed expression. - if (cl.callee.data == .identifier and - std.mem.eql(u8, cl.callee.data.identifier.name, "type_of") and - cl.args.len == 1) - { - return true; - } - return false; - }, - else => return false, - } - } - - /// Resolve a tuple LITERAL used in a type position (`(s32, s32)` reinterpreted - /// as a tuple type at a type-demanding site such as `size_of`). Every element - /// must itself denote a type; a non-type element — e.g. the `1` in - /// `(s32, 1)` — is a user error. Emit a diagnostic pointing at the offending - /// element and return `.unresolved`; never fabricate a tuple with a bogus - /// field (issue 0067). type_bridge.resolveAstType builds the tuple only after - /// this validation passes. - fn resolveTupleLiteralTypeArg(self: *Lowering, node: *const Node) TypeId { - for (node.data.tuple_literal.elements) |el| { - if (!type_bridge.isTypeShapedAstNode(el.value, &self.module.types)) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, el.value.span, "tuple type element is not a type (found `{s}`); a tuple used as a type must list only types, e.g. `(s32, s32)`", .{@tagName(el.value.data)}); - } - return .unresolved; - } - // E4 single-hop visibility gate: each element leaf is resolved through - // the source-aware resolver, so a 2-flat-hop inner leaf (`(COnly, s64)`) - // emits "not visible" + poisons rather than leaking through - // `type_bridge`'s ungated global lookup. A valid element resolves to the - // same TypeId the delegated build produces below (no diagnostic, no - // drift); only the poison short-circuits. - if (self.resolveTypeWithBindings(el.value) == .unresolved) return .unresolved; - } - return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); - } - - pub fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId { - // Pack-index access in a type-arg slot (e.g. `type_name($args[0])` - // or `type_eq($args[i], s64)`). Same shape as the - // `resolveTypeWithBindings` arm — looks up the bound pack types - // and returns the i-th. OOB and no-active-binding emit focused - // diagnostics rather than silently defaulting to .s64 (the - // catch-all `else` below) — that fall-through is exactly the - // "silent unimplemented arm" the project's REJECTED PATTERNS - // forbid. - if (node.data == .pack_index_type_expr) { - const pi = node.data.pack_index_type_expr; - if (self.pack_arg_types) |pat| { - if (pat.get(pi.pack_name)) |arg_tys| { - if (pi.index < arg_tys.len) return arg_tys[pi.index]; - if (self.diagnostics) |diags| { - diags.addFmt(.err, node.span, "pack-index ${s}[{}] out of bounds: '{s}' has {} element{s}", .{ - pi.pack_name, pi.index, pi.pack_name, arg_tys.len, - if (arg_tys.len == 1) @as([]const u8, "") else @as([]const u8, "s"), - }); - } - return .unresolved; - } - } - if (self.diagnostics) |diags| { - diags.addFmt(.err, node.span, "pack-index ${s}[{}] used outside an active pack binding", .{ - pi.pack_name, pi.index, - }); - } - return .unresolved; - } - // Bare `$` in a type-arg position. Single-type generic - // bindings (`$R: Type` in `Closure(..$args) -> $R`) live in - // `type_bindings`; if the name is bound there, return the - // bound TypeId directly. Pack bindings would otherwise resolve - // to a slice value, not a single Type — the caller (e.g. - // `type_name(...)`) expects a single arg. - if (node.data == .comptime_pack_ref) { - const cpr = node.data.comptime_pack_ref; - if (self.type_bindings) |tb| { - if (tb.get(cpr.pack_name)) |ty| return ty; - } - } - switch (node.data) { - .identifier => |id| { - // Check type bindings first (from generic monomorphization) - if (self.type_bindings) |tb| { - if (tb.get(id.name)) |ty| return ty; - } - // E4 single-hop visibility + ambiguity gate: a bare type name - // reachable only over 2+ flat hops is not bare-visible in a - // reflection / type-arg slot (consistent with normal annotations / - // 0763); ≥2 direct flat same-name authors are ambiguous (loud - // diagnostic, consistent with the leaf / 0755) instead of a global - // first-/last-wins pick; a single source-keyed author resolves to - // ITS TypeId. A genuinely-undeclared name is NOT authored as a type - // anywhere → `.proceed`, falling to the "unresolved type" - // diagnostic below. - switch (self.headTypeGate(id.name, node.span)) { - .ambiguous, .not_visible => return .unresolved, - .resolved => |tid| return tid, - .proceed => {}, - } - if (self.program_index.type_alias_map.get(id.name)) |alias_ty| return alias_ty; - const name_id = self.module.types.internString(id.name); - if (self.module.types.findByName(name_id)) |t| return t; - if (self.diagnostics) |diags| { - diags.addFmt(.err, node.span, "unresolved type: '{s}'", .{id.name}); - } - return .unresolved; - }, - .type_expr => |te| { - if (self.headTypeLeak(te.name, node.span)) return .unresolved; - if (self.program_index.type_alias_map.get(te.name)) |alias_ty| return alias_ty; - return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); - }, - .call => |cl| { - // `type_of(x)` resolves to `inferExprType(x)` at lower - // time when `x`'s type is statically known (which it - // is for any expression — type inference always - // produces a concrete TypeId). Lets - // `type_of(a) == s64` fold the same as - // `inferExprType(a) == s64`. - if (cl.callee.data == .identifier and - std.mem.eql(u8, cl.callee.data.identifier.name, "type_of") and - cl.args.len == 1) - { - return self.inferExprType(cl.args[0]); - } - // Handle type constructor calls: size_of(Sx(f32)), size_of(Complex(u32)) - return self.resolveTypeCallWithBindings(&cl); - }, - // Wrapped / structural forms (`*T`, `[N]T`, `[]T`, `?T`, fn-ptr, tuple) - // route through the gated `resolveTypeWithBindings`, whose - // `resolveCompound` recurses each element through the source-aware leaf - // (`resolveNominalLeaf`) — so a 2-hop inner leaf (`*COnly`, `[2]COnly`, - // `(COnly, s64)`) is rejected exactly as in a normal annotation, instead - // of `type_bridge.resolveAstType`'s ungated global lookup (E4). - .tuple_literal, - .pointer_type_expr, - .many_pointer_type_expr, - .array_type_expr, - .slice_type_expr, - .optional_type_expr, - .function_type_expr, - => return self.resolveTypeWithBindings(node), - else => return .unresolved, - } - } - - /// Format a type name for display (e.g. "*Point", "[]s32", "[3]f64"). - pub fn formatTypeName(self: *Lowering, ty: TypeId) []const u8 { - // Builtin types: use their canonical name - 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.formatTypeName(p.pointee); - break :blk std.fmt.allocPrint(self.alloc, "*{s}", .{inner}) catch "pointer"; - }, - .many_pointer => |p| blk: { - const inner = self.formatTypeName(p.element); - break :blk std.fmt.allocPrint(self.alloc, "[*]{s}", .{inner}) catch "many_pointer"; - }, - .slice => |s| blk: { - const inner = self.formatTypeName(s.element); - break :blk std.fmt.allocPrint(self.alloc, "[]{s}", .{inner}) catch "slice"; - }, - .array => |a| blk: { - const inner = self.formatTypeName(a.element); - break :blk std.fmt.allocPrint(self.alloc, "[{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.formatTypeName(o.child); - break :blk std.fmt.allocPrint(self.alloc, "?{s}", .{inner}) catch "optional"; - }, - .vector => |v| blk: { - const inner = self.formatTypeName(v.element); - break :blk std.fmt.allocPrint(self.alloc, "Vector({d},{s})", .{ v.length, inner }) catch "vector"; - }, - else => @tagName(info), - }; - } - - /// Format a function type string like "() -> s32" or "(s32, s32) -> s32". - fn formatFnTypeString(self: *Lowering, fd: *const ast.FnDecl) []const u8 { - var buf: [512]u8 = undefined; - var pos: usize = 0; - buf[pos] = '('; - pos += 1; - for (fd.params, 0..) |p, i| { - if (i > 0) { - @memcpy(buf[pos..][0..2], ", "); - pos += 2; - } - const pty = self.resolveParamType(&p); - const name = self.formatTypeName(pty); - @memcpy(buf[pos..][0..name.len], name); - pos += name.len; - } - buf[pos] = ')'; - pos += 1; - const ret_ty = self.resolveReturnType(fd); - if (ret_ty != .void) { - @memcpy(buf[pos..][0..4], " -> "); - pos += 4; - const rname = self.formatTypeName(ret_ty); - @memcpy(buf[pos..][0..rname.len], rname); - pos += rname.len; - } - const result = self.alloc.alloc(u8, pos) catch unreachable; - @memcpy(result, buf[0..pos]); - return result; - } - - /// 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). - 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), - .slice_type_expr => |st| matchTypeParamStatic(st.element_type, tp_name), - .pointer_type_expr => |pt| matchTypeParamStatic(pt.pointee_type, tp_name), - .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, - }; - } - - fn matchTypeParamStatic(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), - .slice_type_expr => |st| matchTypeParamStatic(st.element_type, tp_name), - .pointer_type_expr => |pt| matchTypeParamStatic(pt.pointee_type, tp_name), - .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, - }; - } - - /// 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. - 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, - .slice_type_expr => |st| blk: { - // arg_ty should be a slice → extract element type - if (arg_ty.isBuiltin()) break :blk null; - const info = self.module.types.get(arg_ty); - break :blk switch (info) { - .slice => |s| self.extractTypeParam(st.element_type, s.element, tp_name), - else => null, - }; - }, - .pointer_type_expr => |pt| blk: { - if (arg_ty.isBuiltin()) break :blk null; - const info = self.module.types.get(arg_ty); - break :blk switch (info) { - .pointer => |p| self.extractTypeParam(pt.pointee_type, p.pointee, tp_name), - else => null, - }; - }, - .many_pointer_type_expr => |mp| blk: { - if (arg_ty.isBuiltin()) break :blk null; - const info = self.module.types.get(arg_ty); - break :blk switch (info) { - .many_pointer => |p| self.extractTypeParam(mp.element_type, p.element, tp_name), - else => null, - }; - }, - .optional_type_expr => |ot| blk: { - if (arg_ty.isBuiltin()) break :blk null; - const info = self.module.types.get(arg_ty); - break :blk switch (info) { - .optional => |o| self.extractTypeParam(ot.inner_type, o.child, tp_name), - else => null, - }; - }, - .array_type_expr => |at| blk: { - if (arg_ty.isBuiltin()) break :blk null; - const info = self.module.types.get(arg_ty); - break :blk switch (info) { - .array => |a| self.extractTypeParam(at.element_type, a.element, tp_name), - 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, - }; - } - - /// 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 { - return self.genericResolver().mangleTypeName(ty); - } - - /// Resolve type category names (like "int", "struct", "float") to matching TypeId tag values. - /// Returns a list of TypeId index values that match the category. - pub fn resolveTypeCategoryTags(self: *Lowering, name: []const u8) []const u64 { - var tags = std.ArrayList(u64).empty; - - // Fixed builtin categories - if (std.mem.eql(u8, name, "int")) { - tags.append(self.alloc, TypeId.s8.index()) catch {}; - tags.append(self.alloc, TypeId.s16.index()) catch {}; - tags.append(self.alloc, TypeId.s32.index()) catch {}; - tags.append(self.alloc, TypeId.s64.index()) catch {}; - tags.append(self.alloc, TypeId.u8.index()) catch {}; - tags.append(self.alloc, TypeId.u16.index()) catch {}; - tags.append(self.alloc, TypeId.u32.index()) catch {}; - tags.append(self.alloc, TypeId.u64.index()) catch {}; - tags.append(self.alloc, TypeId.usize.index()) catch {}; - tags.append(self.alloc, TypeId.isize.index()) catch {}; - return tags.items; - } - if (std.mem.eql(u8, name, "float")) { - tags.append(self.alloc, TypeId.f32.index()) catch {}; - tags.append(self.alloc, TypeId.f64.index()) catch {}; - return tags.items; - } - if (std.mem.eql(u8, name, "bool")) { - tags.append(self.alloc, TypeId.bool.index()) catch {}; - return tags.items; - } - if (std.mem.eql(u8, name, "string")) { - tags.append(self.alloc, TypeId.string.index()) catch {}; - return tags.items; - } - if (std.mem.eql(u8, name, "void")) { - tags.append(self.alloc, TypeId.void.index()) catch {}; - return tags.items; - } - if (std.mem.eql(u8, name, "type") or std.mem.eql(u8, name, "Type")) { - tags.append(self.alloc, TypeId.any.index()) catch {}; - return tags.items; - } - - // Dynamic categories: scan TypeTable for matching types - const Category = enum { @"struct", @"enum", @"union", slice, array, pointer, vector, optional, error_set }; - const cat: ?Category = if (std.mem.eql(u8, name, "struct")) - .@"struct" - else if (std.mem.eql(u8, name, "enum") or std.mem.eql(u8, name, "union")) - .@"enum" - else if (std.mem.eql(u8, name, "slice")) - .slice - else if (std.mem.eql(u8, name, "array")) - .array - else if (std.mem.eql(u8, name, "pointer")) - .pointer - else if (std.mem.eql(u8, name, "vector")) - .vector - else if (std.mem.eql(u8, name, "optional")) - .optional - else if (std.mem.eql(u8, name, "error_set")) - .error_set - else - null; - - if (cat) |c| { - for (self.module.types.infos.items, 0..) |info, idx| { - const matches = switch (c) { - .@"struct" => info == .@"struct", - .@"enum" => info == .@"enum" or info == .tagged_union, - .@"union" => info == .@"union" or info == .tagged_union, - .slice => info == .slice, - .array => info == .array, - .pointer => info == .pointer or info == .many_pointer, - .vector => info == .vector, - .optional => info == .optional, - .error_set => info == .error_set, - }; - if (matches) { - tags.append(self.alloc, @intCast(idx)) catch {}; - } - } - } - - // Specific type name (e.g., Point, Color) — look up in type registry - if (tags.items.len == 0) { - const name_id = self.module.types.internString(name); - if (self.module.types.findByName(name_id)) |tid| { - tags.append(self.alloc, tid.index()) catch {}; - } - } - - return tags.items; - } - - /// Check if a match expression is a type-category match (patterns are type/category names). - pub fn inferMatchResultType(self: *Lowering, me: *const ast.MatchExpr) TypeId { - // Infer result type from the first non-null arm body. - // If we skip null_literal arms and find a concrete type T, and there - // were null arms, the result is ?T (optional). - var has_null = false; - var saw_unresolved = false; - var saw_noreturn = false; - for (me.arms) |arm| { - const last_node = if (arm.body.data == .block) blk: { - if (arm.body.data.block.stmts.len > 0) { - break :blk arm.body.data.block.stmts[arm.body.data.block.stmts.len - 1]; - } - break :blk arm.body; - } else arm.body; - - if (last_node.data == .null_literal) { - has_null = true; - continue; - } - - // First arm with a statically-inferable type determines the result. - // An arm whose type isn't inferable from the AST alone (e.g. a bare - // enum literal) doesn't decide — keep looking; the caller falls back - // to the contextual target type if none of the arms resolve. - const arm_ty = self.inferExprType(last_node); - // A diverging arm (`noreturn` — `return` / `raise` / `break` / - // `continue`) doesn't produce a value, so it doesn't decide the - // result type; keep looking. The match is `noreturn` only if EVERY - // arm diverges (handled after the loop). - if (arm_ty == .noreturn) { - saw_noreturn = true; - continue; - } - if (arm_ty == .unresolved) { - saw_unresolved = true; - continue; - } - if (has_null and arm_ty != .void) { - return self.module.types.optionalOf(arm_ty); - } - return arm_ty; - } - if (saw_unresolved) return .unresolved; - if (saw_noreturn) return .noreturn; // all arms diverge - return .void; - } - - pub fn isTypeCategoryMatch(me: *const ast.MatchExpr) bool { - for (me.arms) |arm| { - if (arm.pattern) |pat| { - const name = switch (pat.data) { - .identifier => |id| id.name, - .type_expr => |te| te.name, - else => continue, - }; - const categories = [_][]const u8{ - "int", "float", "bool", "string", "void", "type", "Type", - "struct", "enum", "union", "slice", "array", "pointer", "vector", - }; - for (categories) |cat| { - if (std.mem.eql(u8, name, cat)) return true; - } - // Also match specific struct/enum type names (e.g., case Point:) - if (name.len > 0 and name[0] >= 'A' and name[0] <= 'Z') return true; - } - } - return false; - } - - /// Check if a param is a type param declaration ($T: Type). - /// A type param declaration has param.name == one of the type_params names. - 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; - } - return false; - } - - /// Check if a function has comptime (non-Type) value parameters. - pub fn hasComptimeParams(fd: *const ast.FnDecl) bool { - for (fd.params) |p| { - if (p.is_comptime) return true; - } - return false; - } - - /// A plain free function: no type params (not generic) and an ordinary sx - /// body (not `#foreign` / `#builtin` / `#compiler`). Only these get an - /// out-of-line identity-addressable slot — the bare-call disambiguation - /// (fix-0102c) and the shadow-author lowering pass leave every other shape - /// to the existing name-keyed dispatch. - pub fn isPlainFreeFn(fd: *const ast.FnDecl) bool { - if (fd.type_params.len > 0) return false; - return switch (fd.body.data) { - .foreign_expr, .builtin_expr, .compiler_expr => false, - else => true, - }; - } - pub fn resolveReturnType(self: *Lowering, fd: *const ast.FnDecl) TypeId { if (fd.return_type) |rt| { return self.resolveTypeWithBindings(rt); @@ -4685,7 +3894,7 @@ pub const Lowering = struct { /// Bind a `PackResolver` to this Lowering for pack-aware TYPE-position /// resolution (`Closure(..p)` / `(Params...) -> R` / `(..xs)` tuples and /// their `..xs.T` projections). A2.3 moved that logic into `packs.zig`. - fn packResolver(self: *Lowering) PackResolver { + pub fn packResolver(self: *Lowering) PackResolver { return .{ .l = self }; } @@ -4698,7 +3907,7 @@ pub const Lowering = struct { /// non-positive one emits a clean diagnostic and returns null; the caller /// yields `.unresolved` rather than fabricating a `<0 x float>` lane count /// that crashes LLVM verification. - fn resolveVectorLane(self: *Lowering, lane_node: *const Node) ?u32 { + pub fn resolveVectorLane(self: *Lowering, lane_node: *const Node) ?u32 { switch (program_index_mod.foldDimU32(lane_node, self, 1)) { .ok => |n| return n, .too_large => |v| { @@ -4719,970 +3928,6 @@ pub const Lowering = struct { } } - /// Resolve a generic value-param argument (`$K: u32`) to its compile-time - /// integer AND verify it fits the param's declared integer type. The folded - /// value is bound and mangled into the instantiation name, so a module/generic - /// const arg (`Vec(N, f32)`), a const expression (`Make(M + 1, s64)`), an - /// integral float (`Box(4.0)` → 4), and a literal (`Vec(3, f32)`) all bind the - /// same value a literal would. An out-of-range arg (`Box(5_000_000_000)` for a - /// `u32` param) or a non-const arg emits a clean diagnostic and returns null; - /// the caller bails rather than binding a truncated / fabricated value under a - /// wrong mangled name. - /// - /// `type_name` is the param's declared constraint type (`"u32"`, null if - /// unknown). A `u32` count routes through the shared - /// `program_index.foldDimU32` — the SAME fold-and-narrow gate an array dim / - /// Vector lane uses — so the documented "single u32 gate for value-param - /// counts" holds; any other integer type range-checks against - /// `program_index.intTypeRange`; an unrecognised type folds without bounding. - fn resolveValueParamArg(self: *Lowering, arg_node: *const Node, param_name: []const u8, type_name: ?[]const u8) ?i64 { - // Resolve an ALIASED integer constraint (`$K: Count` where `Count :: u32`, - // `$K: Small` where `Small :: s8`) to its underlying builtin so the range - // gate below treats it exactly like `$K: u32` / `$K: s8` (issue 0083 — an - // alias previously slipped past `intTypeRange`, so `Box(5_000_000_000)` - // with `$K: Count` bound a truncated value). A non-integer / unrecognised - // constraint yields null → no range bound (fold only), as before. - const tn_canon: ?[]const u8 = if (type_name) |tn| self.canonicalIntConstraintName(tn) else null; - if (tn_canon) |tn| { - if (std.mem.eql(u8, tn, "u32")) { - switch (program_index_mod.foldDimU32(arg_node, self, 0)) { - .ok => |n| return n, - .not_const, .non_integral_float => { - self.diagValueParamNotConst(arg_node, param_name); - return null; - }, - .below_min => |v| { - self.diagValueParamRange(arg_node, param_name, tn, v); - return null; - }, - .too_large => |v| { - self.diagValueParamRange(arg_node, param_name, tn, v); - return null; - }, - } - } - } - // Non-`u32` integer constraint: fold through the SAME unified count fold - // so an integral float arg (`Box(4.0)`, `Make(F + 1.5, ...)`) binds the - // integer it equals, exactly as the `u32` gate above does; a non-integral - // float / non-const arg is not a valid count. - const v = switch (program_index_mod.foldCountI64(arg_node, self)) { - .int => |iv| iv, - .non_integral, .not_const => { - self.diagValueParamNotConst(arg_node, param_name); - return null; - }, - }; - if (tn_canon) |tn| { - if (program_index_mod.intTypeRange(tn)) |r| { - if (v < r.min or v > r.max) { - self.diagValueParamRange(arg_node, param_name, tn, v); - return null; - } - } - } - return v; - } - - /// Resolve a generic value-param constraint type NAME to its canonical builtin - /// integer type name, chasing a type alias (`Count :: u32` → "u32", - /// `Small :: s8` → "s8") so an ALIASED integer constraint range-checks exactly - /// like the builtin it names. Returns the name unchanged when it is already a - /// builtin integer; null when it isn't an integer type (directly or via alias) - /// — the caller then folds without a range bound rather than guessing. The - /// alias map + type table are the same single sources every other resolver - /// reads, so this can't diverge from how the alias is laid out elsewhere. - fn canonicalIntConstraintName(self: *Lowering, name: []const u8) ?[]const u8 { - if (program_index_mod.intTypeRange(name) != null) return name; - if (self.program_index.type_alias_map.get(name)) |tid| { - const canon = self.module.types.typeName(tid); - if (program_index_mod.intTypeRange(canon) != null) return canon; - } - return null; - } - - fn diagValueParamNotConst(self: *Lowering, arg_node: *const Node, param_name: []const u8) void { - if (self.diagnostics) |d| - d.addFmt(.err, arg_node.span, "generic value parameter '{s}' must be a compile-time integer constant", .{param_name}); - } - - fn diagValueParamRange(self: *Lowering, arg_node: *const Node, param_name: []const u8, type_name: []const u8, value: i64) void { - if (self.diagnostics) |d| - d.addFmt(.err, arg_node.span, "value {} does not fit in {s} parameter {s}", .{ value, type_name, param_name }); - } - - /// The poison-vs-proceed projection of `headTypeGate` for an UNQUALIFIED - /// parameterized type HEAD that names a generic STRUCT, a parameterized - /// PROTOCOL, or a type-returning function used as a head (`Box(s64)`, - /// `VL(s64)`) — and the alias-registration / type-match sites that likewise - /// only need "poison or proceed". Returns TRUE (the gate's loud diagnostic is - /// already emitted) when the head is `.not_visible` (a 2-flat-hop leak) or - /// `.ambiguous` (≥2 direct flat same-name authors — consistent with the leaf / - /// 0755); FALSE when it resolves or falls open. See `headTypeGate` for the full - /// non-transitive visibility + ambiguity model and the fall-open conditions. - fn headTypeLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool { - // A head site INSTANTIATES (template / type-fn) rather than substituting a - // nominal TypeId, so it consumes only the poison-vs-proceed bit of the - // full author outcome: `.ambiguous` / `.not_visible` (loud diagnostic - // already emitted by `headTypeGate`) poison; `.resolved` / `.proceed` - // proceed to instantiation. - return switch (self.headTypeGate(name, span)) { - .ambiguous, .not_visible => true, - .proceed, .resolved => false, - }; - } - - /// Control-flow outcome of the generic-struct LAYOUT-head selector. Carries no - /// diagnostic for the caller to emit — `selectGenericStructHead` emits inline. - const HeadTemplate = union(enum) { - template: StructTemplate, // visible bare author OR qualified author → instantiate - poisoned, // gate already diagnosed → caller returns .unresolved / Ref.none - not_generic, // name is not a generic struct head → caller's non-struct path - }; - - /// THE single selector every generic-struct LAYOUT-head site funnels through — - /// no head site reads `struct_template_map` for selection directly. Decides the - /// authoring template for a head named `name`, qualified by namespace `alias` - /// (non-null only for `ns.Box(..)` with an identifier object) and flagged - /// `is_qualified` (any `.field_access` callee, including a non-identifier - /// object). Emits the visibility / missing-member diagnostics INLINE at `span`, - /// at the same program point and ordering the sites used before (0767/0769/0775), - /// and returns a control-flow-only outcome: - /// - qualified, namespace authors `name` as a generic struct → that author. - /// - qualified, namespace exists but lacks `name` → diagnose missing member, - /// `.poisoned` (never the bare global map, E4 #2). - /// - qualified, namespace authors `name` but NOT as a generic struct (a - /// type-fn / named type) → `.not_generic` (caller's non-struct path). - /// - qualified with no usable alias (nested-ns object) → the global template - /// if one exists (pre-existing behavior; no namespace edge to consult). - /// - bare, ≥2 visible authors / 2-flat-hop only → `headTypeLeak` diagnosed → - /// `.poisoned`. - /// - bare, single visible author → that author (own / 1-hop flat), source-keyed. - /// - bare, visible author IS the canonical map author → the global template - /// (byte-identical single-author path). - /// - not in `struct_template_map` at all → `.not_generic`. - pub fn selectGenericStructHead(self: *Lowering, name: []const u8, alias: ?[]const u8, is_qualified: bool, span: ?ast.Span) HeadTemplate { - if (is_qualified) { - if (alias) |a| { - if (self.qualifiedStructTemplate(a, name)) |tmpl| return .{ .template = tmpl }; - if (self.qualifiedMemberMissing(a, name)) { - if (self.diagnostics) |d| - d.addFmt(.err, span, "namespace '{s}' has no member '{s}'", .{ a, name }); - return .poisoned; - } - return .not_generic; - } - // Qualified but un-aliasable object (nested namespace / non-identifier): - // no namespace edge to select from — use the global template if present. - if (self.program_index.struct_template_map.getPtr(name)) |tmpl| return .{ .template = tmpl.* }; - return .not_generic; - } - if (self.program_index.struct_template_map.getPtr(name)) |tmpl| { - if (self.headTypeLeak(name, span)) return .poisoned; - if (self.bareVisibleStructTemplate(name)) |vt| return .{ .template = vt }; - return .{ .template = tmpl.* }; - } - return .not_generic; - } - - /// Decompose a head callee NODE (`.identifier Box` or `.field_access ns.Box`) - /// into the `(name, alias, is_qualified)` triple `selectGenericStructHead` - /// consumes. `alias` is the namespace identifier only for a `.field_access` - /// whose object is a plain identifier; a nested / non-identifier object is - /// qualified-but-unaliased. - const HeadName = struct { name: []const u8, alias: ?[]const u8, is_qualified: bool }; - pub fn headNameOfCallee(callee: *const Node) ?HeadName { - return switch (callee.data) { - .identifier => |id| .{ .name = id.name, .alias = null, .is_qualified = false }, - .field_access => |fa| .{ - .name = fa.field, - .alias = if (fa.object.data == .identifier) fa.object.data.identifier.name else null, - .is_qualified = true, - }, - else => null, - }; - } - - /// The complete source-aware author outcome of an UNQUALIFIED bare TYPE head — - /// the unified non-transitive visibility + ambiguity gate every bare-type- - /// reference site OUTSIDE the nominal leaf routes through (E4 attempt-5): - /// reflection / type-arg slots, typed array/vector-literal heads, parameterized - /// generic / protocol / type-fn heads, type-as-value, and type-category match - /// arms. Mirrors `selectNominalLeaf`'s author model so a 2-flat-hop type is - /// `.not_visible`, ≥2 direct flat same-name authors are `.ambiguous` (the LOUD - /// diagnostic, consistent with the leaf / 0755 — never a silent global - /// `findByName` / `struct_template_map` first-/last-wins pick), and a single - /// direct flat author resolves to ITS source-keyed TypeId. Falls open - /// (`.proceed`) when import facts are unwired, the source context is absent, - /// the default-Context emitter is running (built-in infrastructure resolves - /// independent of the user's import style, F1), the querying source is the OWN - /// author, a single flat author is not registered yet (a forward / foreign / - /// generic template — the caller instantiates it), or `name` is a block-local - /// of this source / no type author at all. Library-internal heads stay visible - /// because every instantiation kind is source-pinned to the template's defining - /// module (E3/E4 #1): the query originates THERE, where the head is a direct - /// flat import. A namespaced `ns.Box(..)` head is an explicit qualified reach - /// and is exempt (the caller skips this gate). - const HeadTypeGate = union(enum) { - proceed, - resolved: TypeId, - ambiguous, - not_visible, - }; - pub fn headTypeGate(self: *Lowering, name: []const u8, span: ?ast.Span) HeadTypeGate { - if (self.emitting_default_context) return .proceed; - if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) return .proceed; - const from = self.current_source_file orelse return .proceed; - - var res_walk = self.resolver(); - const author_set = res_walk.collectVisibleAuthors(name, from, .user_bare_flat); - defer if (author_set.flat.len > 0) self.alloc.free(author_set.flat); - - // Own author wins outright (own-wins, 0754). Pending / unregistered → .proceed. - if (author_set.own) |own| switch (own.raw) { - .const_decl => { - if (self.program_index.type_aliases_by_source.get(own.source)) |inner| { - if (inner.get(name)) |tid| return .{ .resolved = tid }; - } - return .proceed; - }, - else => if (isNamedTypeKind(own.raw)) { - if (self.namedRefTid(own.raw, name)) |tid| return .{ .resolved = tid }; - return .proceed; - }, - }; - - // Flat type authors - var flat_type_count: usize = 0; - var found_tid: ?TypeId = null; - var flat_tid_count: usize = 0; - for (author_set.flat) |fa| { - const is_type = switch (fa.raw) { - .const_decl => blk: { - if (self.program_index.type_aliases_by_source.get(fa.source)) |inner| - break :blk inner.contains(name); - break :blk false; - }, - else => isNamedTypeKind(fa.raw), - }; - if (!is_type) continue; - flat_type_count += 1; - const fa_tid: ?TypeId = switch (fa.raw) { - .const_decl => blk: { - if (self.program_index.type_aliases_by_source.get(fa.source)) |inner| - break :blk inner.get(name); - break :blk null; - }, - else => self.namedRefTid(fa.raw, name), - }; - if (fa_tid) |t| { - flat_tid_count += 1; - if (found_tid) |f| { if (t != f) { - if (self.diagnostics) |d| - d.addFmt(.err, span, "type '{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{name}); - return .ambiguous; - } } else found_tid = t; - } - } - if (flat_type_count > 0) { - // ≥2 authors but not all resolved to one TypeId → ambiguous - if (flat_type_count >= 2 and !(flat_tid_count == flat_type_count and found_tid != null)) { - if (self.diagnostics) |d| - d.addFmt(.err, span, "type '{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{name}); - return .ambiguous; - } - if (found_tid) |t| return .{ .resolved = t }; - return .proceed; // single author exists but TypeId not registered - } - - if (self.localTypeInSource(from, name)) return .proceed; - if (!self.nameAuthoredAsTypeAnywhere(name)) return .proceed; - if (self.diagnostics) |d| - d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name}); - return .not_visible; - } - - /// Single-hop non-transitive visibility + ambiguity gate for an UNQUALIFIED - /// type-returning FUNCTION head used as a type (`Make(N, T)` where - /// `Make :: ($K, $T) -> Type`). A type-fn is a `fn_decl`, so visibility is - /// decided from the ELIGIBLE FUNCTION authors directly reachable from the use - /// site (`flatFnAuthorVisible`) — NOT the module-scope NAME predicate - /// (`isNameVisible`), which a same-name NON-function (a value const, a named - /// type) would wrongly vouch for. Returns TRUE (loud diagnostic already - /// emitted) when the head is AMBIGUOUS (≥2 distinct direct flat same-name - /// type-fn authors, no own author — consistent with the parameterized struct / - /// protocol heads and the leaf, 0755/0767, never a silent `fn_ast_map` - /// first-/last-wins pick) or NOT-VISIBLE (its only directly-visible same-name - /// author is a non-function and the real type-fn author is ≥2 flat hops away). - /// A scope-local (mangled) type-fn or the querying source's OWN function author - /// wins outright (own-wins) and is exempt; falls open when unwired / - /// default-context. Diagnostic mirrors the type form (the head IS used as a type - /// here). - pub fn headFnLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool { - if (self.emitting_default_context) return false; - const from = self.current_source_file orelse return false; - if (self.scope) |s| if (s.lookupFn(name) != null) return false; - // Fall open when the import facts aren't wired (comptime callers, - // directory imports without a main file): the author collector would - // otherwise return an empty set and wrongly report a genuinely-visible - // type-fn as not-visible. Mirrors `headTypeGate`'s guard. - if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) return false; - // ≥2 distinct direct flat type-fn authors with no own author — a genuine - // collision the source cannot disambiguate. Diagnose loudly BEFORE the - // visibility short-circuit, which would otherwise let the single - // `fn_ast_map[name]` author silently win. - if (self.flatFnAuthorAmbiguous(name, from)) { - if (self.diagnostics) |d| - d.addFmt(.err, span, "type '{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{name}); - return true; - } - // KIND-AWARE: visible iff a directly-reachable (own or 1-hop flat) author - // is itself a TYPE-FUNCTION. A same-name 1-hop non-function (attempt-7) OR - // ordinary non-type function (attempt-8) does NOT vouch for a type-fn head - // whose real author is 2 flat hops away. - if (self.flatFnAuthorVisible(name, from)) return false; - if (self.diagnostics) |d| - d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name}); - return true; - } - - /// TRUE iff bare `name` has ≥2 DISTINCT direct flat-import authors that are - /// TYPE-FUNCTIONS (`typeFnAuthor`: a `fn_decl` with ≥1 `$`-param — an ordinary - /// same-name function does not count) and the querying source authors NONE - /// itself. The querying source's OWN - /// author wins outright (own-wins), so an own author short-circuits to "not - /// ambiguous" — the existing single-author path instantiates it. Diamond - /// imports of the SAME author collapse in `collectVisibleAuthors`'s - /// author-identity de-dup, so two edges onto one type-fn are NOT ambiguous. The - /// type-fn ambiguity analogue of `flatTypeAuthorCount`'s `.ambiguous` for named - /// type / template heads. - fn flatFnAuthorAmbiguous(self: *Lowering, name: []const u8, from: []const u8) bool { - var res = self.resolver(); - const set = res.collectVisibleAuthors(name, from, .user_bare_flat); - defer if (set.flat.len > 0) self.alloc.free(set.flat); - if (set.own != null) return false; // own-wins - var fn_authors: usize = 0; - for (set.flat) |fa| { - if (typeFnAuthor(fa.raw)) fn_authors += 1; - } - return fn_authors >= 2; - } - - /// TRUE iff bare `name` has at least one DIRECTLY-visible author — the - /// querying source's OWN author or a 1-hop flat-import author — that is a - /// TYPE-FUNCTION (`typeFnAuthor`: a `fn_decl` with ≥1 `$`-param). The KIND-AWARE - /// analogue of `isNameVisible` for a type-fn head: a same-name 1-hop - /// NON-function (a value const `Make :: 123`, a named type) does NOT vouch - /// (attempt-7), and — crucially — neither does a same-name 1-hop ORDINARY - /// function (`Make :: () -> s32`, zero `$`-params), which cannot be the type - /// head being instantiated (attempt-8). So a type-fn whose only directly- - /// visible same-name author is a non-fn OR a non-type-fn — its real author 2 - /// flat hops away — is correctly invisible. Mirrors `flatFnAuthorAmbiguous`'s - /// type-fn-only author view. - fn flatFnAuthorVisible(self: *Lowering, name: []const u8, from: []const u8) bool { - var res = self.resolver(); - const set = res.collectVisibleAuthors(name, from, .user_bare_flat); - defer if (set.flat.len > 0) self.alloc.free(set.flat); - if (set.own) |own| { - if (typeFnAuthor(own.raw)) return true; - } - for (set.flat) |fa| { - if (typeFnAuthor(fa.raw)) return true; - } - return false; - } - - /// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)). - pub fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId { - // A namespaced callee (`ns.Box(..)`) is an explicit qualified reach and is - // exempt from the bare-head visibility gate; only a plain identifier head - // is policed (E4). - const is_qualified = cl.callee.data == .field_access; - const callee_name: []const u8 = switch (cl.callee.data) { - .identifier => |id| id.name, - .field_access => |fa| fa.field, - else => return .unresolved, - }; - // Built-in: Vector(N, T) - if (std.mem.eql(u8, callee_name, "Vector") and cl.args.len == 2) { - const length = self.resolveVectorLane(cl.args[0]) orelse return .unresolved; - const elem = self.resolveTypeWithBindings(cl.args[1]); - return self.module.types.vectorOf(elem, length); - } - // Generic-struct head: route through the single layout choke-point (CP-1). - // Bare → the single bare-VISIBLE author (own / 1-hop flat), source-keyed; - // qualified `ns.Box(..)` → ns's OWN template (or a missing-member diagnostic); - // never the global last-wins map for a visible-shadowed or qualified head. - if (headNameOfCallee(cl.callee)) |hn| { - switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, cl.callee.span)) { - .template => |t| return self.instantiateGenericStruct(&t, cl.args), - .poisoned => return .unresolved, - .not_generic => {}, - } - } - // User-defined type-returning function: Complex(u32), Sx(f32) - // Also resolve via scope fn_names (local functions get mangled names) - const resolved_name = if (self.scope) |scope| (scope.lookupFn(callee_name) orelse callee_name) else callee_name; - if (self.program_index.fn_ast_map.get(resolved_name)) |fd| { - if (fd.type_params.len > 0) { - if (!is_qualified and self.headFnLeak(callee_name, cl.callee.span)) return .unresolved; - if (self.instantiateTypeFunction(callee_name, callee_name, fd, cl.args)) |ty| { - return ty; - } - } - } - // Try as a named type - const name_id = self.module.types.internString(callee_name); - return self.module.types.findByName(name_id) orelse .unresolved; - } - - /// Resolve a parameterized type expr, substituting bindings for type/value params. - /// Handles both built-in types (Vector) and user-defined generic structs. - /// `span` locates the reference for the unresolved-base diagnostic. - fn resolveParameterizedWithBindings(self: *Lowering, pt: *const ast.ParameterizedTypeExpr, span: ?ast.Span) TypeId { - const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name; - const table = &self.module.types; - // A namespaced base (`ns.Box(..)`) is an explicit qualified reach and is - // exempt from the bare-head visibility gate; only a dotless head is - // policed (E4). - const is_qualified = std.mem.indexOfScalar(u8, pt.name, '.') != null; - - // Vector(N, T) — built-in parameterized type. A backtick raw base - // (`` `Vector(…) ``) is the LITERAL user type named `Vector`, so it - // skips this intrinsic and resolves through the template map (0089). - if (!pt.is_raw and std.mem.eql(u8, base_name, "Vector")) { - if (pt.args.len == 2) { - const length = self.resolveVectorLane(pt.args[0]) orelse return .unresolved; - const elem = self.resolveTypeWithBindings(pt.args[1]); - return table.vectorOf(elem, length); - } - } - - // Generic-struct base: route through the single layout choke-point (CP-1). - // Bare → the single bare-VISIBLE author (own / 1-hop flat), source-keyed; - // qualified `ns.Box(..)` → ns's OWN template (or a missing-member diagnostic); - // never the global last-wins map for a visible-shadowed or qualified head. - { - const alias: ?[]const u8 = if (std.mem.indexOfScalar(u8, pt.name, '.')) |dot| pt.name[0..dot] else null; - switch (self.selectGenericStructHead(base_name, alias, is_qualified, span)) { - .template => |t| return self.instantiateGenericStruct(&t, pt.args), - .poisoned => return .unresolved, - .not_generic => {}, - } - } - - // Parameterized protocol used as a value type (`VL(s64)`): materialize a - // 16-byte protocol value with the type-arg bound (not a 0-field stub). - if (self.program_index.protocol_ast_map.get(base_name)) |pd| { - if (pd.type_params.len > 0) { - if (!is_qualified and self.headTypeLeak(base_name, span)) return .unresolved; - return self.instantiateParamProtocol(pd, pt.args); - } - } - - // User-defined type-returning function used as a TYPE annotation - // (`b : Make(N, s64)` where `Make :: ($K: u32, $T: Type) -> Type`). The - // `.call`-node path (`resolveTypeCallWithBindings`) already routes here; - // a `parameterized_type_expr` must too, or the function name falls through - // to the empty-struct stub below and `b.field` / `b.len` fails. - const resolved_name = if (self.scope) |scope| (scope.lookupFn(base_name) orelse base_name) else base_name; - if (self.program_index.fn_ast_map.get(resolved_name)) |fd| { - if (fd.type_params.len > 0) { - if (!is_qualified and self.headFnLeak(base_name, span)) return .unresolved; - if (self.instantiateTypeFunction(base_name, base_name, fd, pt.args)) |ty| { - return ty; - } - } - } - - // The base names no known type constructor — not Vector, not a generic - // struct template, not a parameterized protocol, not a type-returning - // function. A silent 0-field stub here would mis-size every downstream - // `b.field` / `b.len`; emit the diagnostic and poison with `.unresolved` - // (the `.call`-node sibling `resolveTypeCallWithBindings` already poisons). - if (self.diagnostics) |d| - d.addFmt(.err, span, "unknown type '{s}'", .{base_name}); - return .unresolved; - } - - /// Instantiate a generic struct template with concrete args. - /// E.g., Vec(3, f32) → struct Vec__3_f32 { data: Vector(3, f32) } - /// A generic-struct instance method selected via the STAMPED authoring decl: - /// the `fn_decl` to monomorphize, the instance's stored type bindings, and the - /// instance (mangled / alias) name the monomorphized function is keyed under. - const GenericStructMethod = struct { - fd: *const ast.FnDecl, - bindings: *std.StringHashMap(TypeId), - inst_name: []const u8, - }; - - /// THE single body-axis reader: select `method` of generic-struct instance - /// `inst_name` via the instance's STAMPED author (`struct_instance_author`), - /// so body-author ≡ layout-author by construction — never the global last-wins - /// `fn_ast_map["Template.method"]` a 2-flat-hop same-name template's method - /// could win. Null when `inst_name` is NOT a generic instance (no author stamp) - /// — the caller's existing non-generic `fn_ast_map` path then handles it - /// (non-generic structs, free fns, FFI), or when the confirmed author declares - /// no such `method` (a normal unresolved-method, handled downstream). A - /// confirmed instance whose author is present but whose bindings are missing is - /// a LOUD invariant failure — instantiation writes both together (CP-2). - pub fn genericInstanceMethod(self: *Lowering, inst_name: []const u8, method: []const u8) ?GenericStructMethod { - const author = self.struct_instance_author.get(inst_name) orelse return null; - const bindings = self.struct_instance_bindings.getPtr(inst_name) orelse - std.debug.panic("generic struct instance '{s}' has an author but no bindings", .{inst_name}); - // INLINE struct method (`Box :: struct { make :: ... }`): selected via the - // instance's STAMPED author, so the body is the one authored alongside the - // layout — never the global last-wins `fn_ast_map["Template.method"]` a - // 2-flat-hop same-name template's method could win (finding #1). - if (structMethodFn(author, method)) |fd| - return .{ .fd = fd, .bindings = bindings, .inst_name = inst_name }; - // IMPL-block method (`impl P for Box { ... }`): registered under the - // template name in `fn_ast_map`, not on the struct decl, so it is keyed by - // template name (protocol dispatch). The author confirms this IS a generic - // instance; the method body is the template's registered impl method. - const tmpl_name = self.struct_instance_template.get(inst_name) orelse return null; - const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, method }) catch return null; - if (self.program_index.fn_ast_map.get(tmpl_qualified)) |fd| - return .{ .fd = fd, .bindings = bindings, .inst_name = inst_name }; - return null; - } - - /// Monomorphize (once) the selected generic-instance method under - /// `.` and return its FuncId. The source-pin follows the - /// selected `fd` for free: `monomorphizeFunction` pins to `fd.body.source_file`, - /// which is the template's defining module (the author's own method node). - /// Null when the function fails to resolve post-monomorphization. - pub fn ensureGenericInstanceMethodLowered(self: *Lowering, m: GenericStructMethod) ?FuncId { - const mangled = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ m.inst_name, m.fd.name }) catch return null; - if (!self.lowered_functions.contains(mangled)) { - self.monomorphizeFunction(m.fd, mangled, m.bindings); - } - return self.resolveFuncByName(mangled); - } - - /// Debug invariant (CP coverage lock): the two generic-instance maps written - /// in lockstep at the SAME two writers (instantiation + alias copy) — - /// `struct_instance_template` and `struct_instance_author` — must have - /// coincident keysets. A future writer that registers an instance's layout - /// without stamping its author (a silent body-axis reopen) trips this in a - /// debug `zig build test`, not in production. - pub fn assertInstanceMapsCoincide(self: *Lowering) void { - if (!std.debug.runtime_safety) return; - var it = self.struct_instance_template.keyIterator(); - while (it.next()) |k| { - if (!self.struct_instance_author.contains(k.*)) - std.debug.panic("generic instance '{s}' has a template but no author stamp", .{k.*}); - } - var it2 = self.struct_instance_author.keyIterator(); - while (it2.next()) |k| { - if (!self.struct_instance_template.contains(k.*)) - std.debug.panic("generic instance '{s}' has an author but no template stamp", .{k.*}); - } - } - - pub fn instantiateGenericStruct(self: *Lowering, tmpl: *const StructTemplate, args: []const *const Node) TypeId { - const table = &self.module.types; - - // Build mangled name dynamically: StructName__arg1_arg2 - var name_parts = std.ArrayList(u8).empty; - name_parts.appendSlice(self.alloc, tmpl.name) catch {}; - - // A qualified `ns.Box(..)` head can select a generic template whose bare - // name also belongs to a DIFFERENT module's same-name template (the one - // that won the last-wins `struct_template_map`). Both would mangle to - // `Box__s64` and the second instantiation would alias the first's layout. - // Tag the NON-canonical author's mangled name with its source so each - // author's instantiation is a distinct type. The canonical (bare-map) - // author keeps the untagged name — no churn for single-author generics. - if (self.program_index.struct_template_map.get(tmpl.name)) |canon| { - const canon_src = canon.source_file orelse ""; - const this_src = tmpl.source_file orelse ""; - if (!std.mem.eql(u8, canon_src, this_src)) { - var tag_buf: [24]u8 = undefined; - const tag = std.fmt.bufPrint(&tag_buf, "$m{x}", .{std.hash.Wyhash.hash(0, this_src)}) catch ""; - name_parts.appendSlice(self.alloc, tag) catch {}; - } - } - - // Bind type params to args and build name suffix - const saved_type_bindings = self.type_bindings; - const saved_value_bindings = self.comptime_value_bindings; - const saved_pack_bindings = self.pack_bindings; - const saved_pack_arg_types = self.pack_arg_types; - var tb = std.StringHashMap(TypeId).init(self.alloc); - var cvb = std.StringHashMap(i64).init(self.alloc); - var pb = std.StringHashMap([]const TypeId).init(self.alloc); - - for (tmpl.type_params, 0..) |tp, i| { - if (i >= args.len) break; - - // `..$Ts: []Type` — bind the REMAINING args as a type pack. - if (tp.is_variadic) { - var pack_tys = std.ArrayList(TypeId).empty; - for (args[i..]) |a| { - // A spread arg `..sources.T` expands to the source pack's - // per-element (projected) types; a plain arg is one type. - if (a.data == .spread_expr) { - if (self.packResolver().packTypeElems(a.data.spread_expr.operand)) |elems| { - defer self.alloc.free(elems); - for (elems) |ty| { - pack_tys.append(self.alloc, ty) catch {}; - name_parts.appendSlice(self.alloc, "__") catch {}; - name_parts.appendSlice(self.alloc, self.formatTypeName(ty)) catch {}; - } - continue; - } - } - const ty = self.resolveTypeWithBindings(a); - pack_tys.append(self.alloc, ty) catch {}; - name_parts.appendSlice(self.alloc, "__") catch {}; - name_parts.appendSlice(self.alloc, self.formatTypeName(ty)) catch {}; - } - pb.put(tp.name, pack_tys.toOwnedSlice(self.alloc) catch &.{}) catch {}; - break; // a pack param is always last - } - - name_parts.appendSlice(self.alloc, "__") catch {}; - - if (tp.is_type_param) { - const ty = self.resolveTypeWithBindings(args[i]); - tb.put(tp.name, ty) catch {}; - const tname = self.formatTypeName(ty); - name_parts.appendSlice(self.alloc, tname) catch {}; - } else { - // Value param (e.g., $N: u32) — fold to a compile-time integer - // and range-check against its declared type. - const val = self.resolveValueParamArg(args[i], tp.name, tp.value_type) orelse return .unresolved; - cvb.put(tp.name, val) catch {}; - var val_buf: [32]u8 = undefined; - const val_str = std.fmt.bufPrint(&val_buf, "{d}", .{val}) catch "0"; - name_parts.appendSlice(self.alloc, val_str) catch {}; - } - } - - const mangled_name = name_parts.items; - - // Check if already instantiated - const name_id = table.internString(mangled_name); - if (table.findByName(name_id)) |existing| { - // Already registered — check if it has fields - const info = table.get(existing); - if (info == .@"struct" and info.@"struct".fields.len > 0) { - // A confirmed generic instance must never be returned without an - // author stamp — the body axis (CP-4) keys method selection off - // it. The template/bindings were written at first instantiation; - // re-stamp the author from THIS `tmpl` if the dedup fast-path is - // the first to reach this mangled name (e.g. a layout interned by - // a forward reference before any method dispatch). - if (!self.struct_instance_author.contains(mangled_name)) { - const owned = self.alloc.dupe(u8, mangled_name) catch return existing; - self.struct_instance_author.put(owned, tmpl.decl) catch {}; - } - return existing; - } - } - - // Set up bindings and resolve fields. `pack_bindings` makes a - // pack-shaped field type like `(..$Ts)` resolve to the bound type list. - self.type_bindings = tb; - self.comptime_value_bindings = cvb; - self.pack_bindings = pb; - self.pack_arg_types = pb; - - // Resolve the field type nodes in the TEMPLATE's source context, not the - // (possibly cross-module) instantiation site. A field naming a type - // visible only in the template's module then resolves correctly, and the - // source-aware nominal leaf classifies main vs imported by the TEMPLATE's - // file — so an undeclared field type (`y: Missing`) or a value param used - // as a type (`x: N` for `$N: u32`) is diagnosed at the right authority - // (the leaf for an imported template, the `UnknownTypeChecker` for a - // main-file one) instead of silently fabricating a stub / poisoning with - // `.unresolved` that panics at LLVM emission. - const saved_src = self.current_source_file; - const saved_diag_src = if (self.diagnostics) |d| d.current_source_file else null; - if (tmpl.source_file) |sf| { - self.current_source_file = sf; - if (self.diagnostics) |d| d.current_source_file = sf; - } - - var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; - for (tmpl.field_names, tmpl.field_type_nodes) |fname, ftype_node| { - const field_ty = self.resolveTypeWithBindings(ftype_node); - fields.append(self.alloc, .{ - .name = table.internString(fname), - .ty = field_ty, - }) catch unreachable; - } - - self.current_source_file = saved_src; - if (self.diagnostics) |d| d.current_source_file = saved_diag_src; - - // Restore bindings - self.type_bindings = saved_type_bindings; - self.comptime_value_bindings = saved_value_bindings; - self.pack_bindings = saved_pack_bindings; - self.pack_arg_types = saved_pack_arg_types; - - // Register the monomorphized struct - const info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items } }; - const id = if (table.findByName(name_id)) |existing| existing else table.intern(info); - table.updatePreservingKey(id, info); - - // Bind the template name to this concrete instance so a method's - // `self: *Combined` (the template name) resolves to `*Combined__s64_s64` - // — otherwise `self.field` hits the 0-field generic stub. - tb.put(tmpl.name, id) catch {}; - - // Store the type bindings, template name, and authoring decl for method - // resolution. The author is stamped from the SAME `tmpl` that built the - // layout above, so the body axis (CP-4) selects this instance's methods - // via the layout author — never the global last-wins `fn_ast_map`. - const owned_mangled = self.alloc.dupe(u8, mangled_name) catch return id; - self.struct_instance_bindings.put(owned_mangled, tb) catch {}; - self.struct_instance_template.put(owned_mangled, tmpl.name) catch {}; - self.struct_instance_author.put(owned_mangled, tmpl.decl) catch {}; - - return id; - } - - /// Instantiate a type-returning function: `Foo :: Complex(u32)` where - /// `Complex :: ($T:Type) -> Type { return struct { value: T; count: u32; }; }` - /// Walks the function body to find the returned struct/enum, resolves field types - /// with the provided type bindings, and registers the result. - pub fn instantiateTypeFunction(self: *Lowering, alias_name: []const u8, template_name: []const u8, fd: *const ast.FnDecl, args: []const *const Node) ?TypeId { - const table = &self.module.types; - - // Build type bindings from params + args - const saved_type_bindings = self.type_bindings; - const saved_value_bindings = self.comptime_value_bindings; - var tb = std.StringHashMap(TypeId).init(self.alloc); - var cvb = std.StringHashMap(i64).init(self.alloc); - - // Build mangled name - var name_parts = std.ArrayList(u8).empty; - name_parts.appendSlice(self.alloc, template_name) catch {}; - - for (fd.type_params, 0..) |tp, i| { - if (i >= args.len) break; - name_parts.appendSlice(self.alloc, "__") catch {}; - - // Check if this is a Type param ($T: Type) or a value param ($N: u32) - const is_type_param = if (tp.constraint.data == .type_expr) - std.mem.eql(u8, tp.constraint.data.type_expr.name, "Type") - else - true; // default to type param - - if (is_type_param) { - const ty = self.resolveTypeWithBindings(args[i]); - tb.put(tp.name, ty) catch {}; - const tname = self.formatTypeName(ty); - name_parts.appendSlice(self.alloc, tname) catch {}; - } else { - // Value param (e.g., $N: u32) — fold to a compile-time integer - // and range-check against its declared type. A failed bind has - // already diagnosed itself, so poison to `.unresolved` rather - // than `null`: `null` makes the caller fall through to the - // empty-struct placeholder named after the fn, which then - // cascades a bogus `field not found` on any later access. The - // struct binder (`instantiateGenericStruct`) poisons the same way. - const vp_type: ?[]const u8 = if (tp.constraint.data == .type_expr) tp.constraint.data.type_expr.name else null; - const val = self.resolveValueParamArg(args[i], tp.name, vp_type) orelse return .unresolved; - cvb.put(tp.name, val) catch {}; - var val_buf: [32]u8 = undefined; - const val_str = std.fmt.bufPrint(&val_buf, "{d}", .{val}) catch "0"; - name_parts.appendSlice(self.alloc, val_str) catch {}; - } - } - - const mangled_name = name_parts.items; - - // Check if already instantiated - const mangled_name_id = table.internString(mangled_name); - if (table.findByName(mangled_name_id)) |existing| { - const info = table.get(existing); - if ((info == .@"struct" and info.@"struct".fields.len > 0) or info == .@"union" or info == .tagged_union) { - return existing; - } - } - - // Activate bindings - self.type_bindings = tb; - self.comptime_value_bindings = cvb; - defer { - self.type_bindings = saved_type_bindings; - self.comptime_value_bindings = saved_value_bindings; - } - - // Resolve the type fn's body (inline struct/union fields, or the returned - // type expression) in its OWN module (E4), so a 2-flat-hop library type - // named there is bare-visible — not the cross-module call site. The arg - // exprs above were already resolved in the caller's context. - const saved_tf_src = self.current_source_file; - defer self.setCurrentSourceFile(saved_tf_src); - if (fd.body.source_file) |src| self.setCurrentSourceFile(src); - - // Determine if alias_name is a real alias (e.g., "Foo" for "Complex(u32)") - // or just the template name itself (inline use like "Sx(f32)") - const has_alias = !std.mem.eql(u8, alias_name, template_name); - - // Try struct first - if (findStructInBody(fd.body)) |struct_decl| { - // Resolve struct fields with type bindings active - var struct_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; - for (struct_decl.field_names, struct_decl.field_types) |fname, ftype_node| { - const field_ty = self.resolveTypeWithBindings(ftype_node); - struct_fields.append(self.alloc, .{ - .name = table.internString(fname), - .ty = field_ty, - }) catch {}; - } - - // Always register under mangled name - const mangled_info: types.TypeInfo = .{ .@"struct" = .{ - .name = mangled_name_id, - .fields = struct_fields.items, - } }; - const mangled_id = if (table.findByName(mangled_name_id)) |existing| existing else table.intern(mangled_info); - table.updatePreservingKey(mangled_id, mangled_info); - - // If there's a real alias, also register under alias name and in alias map - if (has_alias) { - const alias_name_id = table.internString(alias_name); - const alias_info: types.TypeInfo = .{ .@"struct" = .{ - .name = alias_name_id, - .fields = struct_fields.items, - } }; - const alias_id = if (table.findByName(alias_name_id)) |existing| existing else table.intern(alias_info); - table.updatePreservingKey(alias_id, alias_info); - - // Store defaults if any - if (struct_decl.field_defaults.len > 0) { - self.struct_defaults_map.put(alias_name, struct_decl.field_defaults) catch {}; - } - - return alias_id; - } - - return mangled_id; - } - - // Try tagged enum/union - if (findUnionInBody(fd.body)) |enum_decl| { - return self.instantiateTypeUnion(if (has_alias) alias_name else mangled_name, mangled_name, &enum_decl); - } - - // General case: the body returns a TYPE EXPRESSION that is not an inline - // struct/union/enum — `return [K]T`, `Vector(K, T)`, `*T`, an alias, etc. - // Resolve it with the value/type bindings active (so `[K]T` folds K to a - // compile-time integer). The result is interned structurally, so - // `Make(N, s64)`, `Make(3, s64)`, and `Make(M + 1, s64)` all yield the - // same TypeId. `.unresolved` means the return wasn't a type expression - // (e.g. a value-returning function in a type position) → fall through to - // the caller's fallback rather than fabricating a type. - if (findReturnTypeExpr(fd.body)) |ret_node| { - const ty = self.resolveTypeWithBindings(ret_node); - if (ty != .unresolved) return ty; - } - - return null; - } - - /// The type expression a type-returning function yields: the value of its - /// `return` (block body) or the bare expression (arrow body / `=> [K]T`). - /// Used for a non-struct/union return shape, which the struct/union body - /// walkers above don't match. - fn findReturnTypeExpr(body: *const Node) ?*const Node { - if (body.data == .block) { - for (body.data.block.stmts) |stmt| { - if (stmt.data == .return_stmt) return stmt.data.return_stmt.value; - } - return null; - } - return body; - } - - /// Instantiate a tagged enum from a type function body. - fn instantiateTypeUnion(self: *Lowering, alias_name: []const u8, mangled_name: []const u8, ed: *const ast.EnumDecl) ?TypeId { - const table = &self.module.types; - - // Build variant fields (tagged enum variants stored as StructInfo.Field) - var variant_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; - for (ed.variant_names, 0..) |vname, i| { - const payload_ty: TypeId = if (i < ed.variant_types.len and ed.variant_types[i] != null) - self.resolveTypeWithBindings(ed.variant_types[i].?) - else - .void; - variant_fields.append(self.alloc, .{ - .name = table.internString(vname), - .ty = payload_ty, - }) catch {}; - } - - const alias_name_id = table.internString(alias_name); - const info: types.TypeInfo = .{ .tagged_union = .{ - .name = alias_name_id, - .fields = variant_fields.items, - .tag_type = .s64, - } }; - const id = if (table.findByName(alias_name_id)) |existing| existing else table.intern(info); - table.updatePreservingKey(id, info); - - // Also register under mangled name - if (!std.mem.eql(u8, alias_name, mangled_name)) { - const mangled_name_id = table.internString(mangled_name); - const mangled_info: types.TypeInfo = .{ .tagged_union = .{ - .name = mangled_name_id, - .fields = variant_fields.items, - .tag_type = .s64, - } }; - const mid = if (table.findByName(mangled_name_id)) |existing| existing else table.intern(mangled_info); - table.updatePreservingKey(mid, mangled_info); - } - - return id; - } - - /// Walk an AST body to find a struct declaration (from `return struct { ... }` or bare struct expr). - fn findStructInBody(body: *const Node) ?ast.StructDecl { - if (body.data == .struct_decl) return body.data.struct_decl; - if (body.data == .block) { - for (body.data.block.stmts) |stmt| { - if (stmt.data == .return_stmt) { - if (stmt.data.return_stmt.value) |val| { - if (val.data == .struct_decl) return val.data.struct_decl; - } - } - if (stmt.data == .struct_decl) return stmt.data.struct_decl; - } - } - return null; - } - - /// Walk an AST body to find a tagged enum declaration. - fn findUnionInBody(body: *const Node) ?ast.EnumDecl { - const isTaggedEnum = struct { - fn check(node: *const Node) ?ast.EnumDecl { - if (node.data == .enum_decl and node.data.enum_decl.variant_types.len > 0) { - return node.data.enum_decl; - } - return null; - } - }; - if (isTaggedEnum.check(body)) |ed| return ed; - const stmts = if (body.data == .block) body.data.block.stmts else return null; - for (stmts) |stmt| { - if (stmt.data == .return_stmt) { - if (stmt.data.return_stmt.value) |val| { - if (isTaggedEnum.check(val)) |ed| return ed; - } - } - if (isTaggedEnum.check(stmt)) |ed| return ed; - } - return null; - } - - // ── Type registration ─────────────────────────────────────────── - /// Infer the type of an expression from its AST node (used for untyped var decls). pub fn inferExprType(self: *Lowering, node: *const Node) TypeId { return switch (node.data) { @@ -6334,4 +4579,45 @@ pub const Lowering = struct { pub const resolvePackProjection = lower_pack.resolvePackProjection; pub const isPackFn = lower_pack.isPackFn; pub const isPackParam = lower_pack.isPackParam; + + // --- moved to lower/generic.zig (lower_generic) --- + pub const monomorphizeFunction = lower_generic.monomorphizeFunction; + pub const instantiateGenericStruct = lower_generic.instantiateGenericStruct; + pub const instantiateTypeFunction = lower_generic.instantiateTypeFunction; + pub const instantiateTypeUnion = lower_generic.instantiateTypeUnion; + pub const findStructInBody = lower_generic.findStructInBody; + pub const findUnionInBody = lower_generic.findUnionInBody; + pub const findReturnTypeExpr = lower_generic.findReturnTypeExpr; + pub const genericInstanceMethod = lower_generic.genericInstanceMethod; + pub const ensureGenericInstanceMethodLowered = lower_generic.ensureGenericInstanceMethodLowered; + pub const assertInstanceMapsCoincide = lower_generic.assertInstanceMapsCoincide; + pub const isStaticTypeArg = lower_generic.isStaticTypeArg; + pub const isStaticTypeRef = lower_generic.isStaticTypeRef; + pub const resolveTupleLiteralTypeArg = lower_generic.resolveTupleLiteralTypeArg; + pub const resolveTypeArg = lower_generic.resolveTypeArg; + pub const formatTypeName = lower_generic.formatTypeName; + pub const formatFnTypeString = lower_generic.formatFnTypeString; + pub const matchTypeParam = lower_generic.matchTypeParam; + pub const matchTypeParamStatic = lower_generic.matchTypeParamStatic; + pub const extractTypeParam = lower_generic.extractTypeParam; + pub const mangleTypeName = lower_generic.mangleTypeName; + pub const resolveTypeCategoryTags = lower_generic.resolveTypeCategoryTags; + pub const inferMatchResultType = lower_generic.inferMatchResultType; + pub const isTypeCategoryMatch = lower_generic.isTypeCategoryMatch; + pub const isTypeParamDecl = lower_generic.isTypeParamDecl; + pub const hasComptimeParams = lower_generic.hasComptimeParams; + pub const isPlainFreeFn = lower_generic.isPlainFreeFn; + pub const selectGenericStructHead = lower_generic.selectGenericStructHead; + pub const headTypeLeak = lower_generic.headTypeLeak; + pub const headNameOfCallee = lower_generic.headNameOfCallee; + pub const headTypeGate = lower_generic.headTypeGate; + pub const headFnLeak = lower_generic.headFnLeak; + pub const flatFnAuthorAmbiguous = lower_generic.flatFnAuthorAmbiguous; + pub const flatFnAuthorVisible = lower_generic.flatFnAuthorVisible; + pub const resolveTypeCallWithBindings = lower_generic.resolveTypeCallWithBindings; + pub const resolveParameterizedWithBindings = lower_generic.resolveParameterizedWithBindings; + pub const resolveValueParamArg = lower_generic.resolveValueParamArg; + pub const canonicalIntConstraintName = lower_generic.canonicalIntConstraintName; + pub const diagValueParamNotConst = lower_generic.diagValueParamNotConst; + pub const diagValueParamRange = lower_generic.diagValueParamRange; }; diff --git a/src/ir/lower/generic.zig b/src/ir/lower/generic.zig new file mode 100644 index 0000000..03b7e73 --- /dev/null +++ b/src/ir/lower/generic.zig @@ -0,0 +1,1810 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ast = @import("../../ast.zig"); +const Node = ast.Node; +const types = @import("../types.zig"); +const inst_mod = @import("../inst.zig"); +const mod_mod = @import("../module.zig"); +const type_bridge = @import("../type_bridge.zig"); +const unescape = @import("../../unescape.zig"); +const parser_mod = @import("../../parser.zig"); +const interp_mod = @import("../interp.zig"); +const errors = @import("../../errors.zig"); +const jni_descriptor = @import("../jni_descriptor.zig"); +const program_index_mod = @import("../program_index.zig"); +const resolver_mod = @import("../resolver.zig"); +const imports_mod = @import("../../imports.zig"); +const ProgramIndex = program_index_mod.ProgramIndex; +const GlobalInfo = program_index_mod.GlobalInfo; +const StructTemplate = program_index_mod.StructTemplate; +const TemplateParam = program_index_mod.TemplateParam; +const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo; +const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo; +const ModuleConstInfo = program_index_mod.ModuleConstInfo; +const TypeResolver = @import("../type_resolver.zig").TypeResolver; +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 ProtocolResolver = @import("../protocols.zig").ProtocolResolver; +const CoercionResolver = @import("../conversions.zig").CoercionResolver; +const ErrorAnalysis = @import("../error_analysis.zig").ErrorAnalysis; +const ErrorFlow = @import("../error_flow.zig").ErrorFlow; +const ObjcLowering = @import("../ffi_objc.zig").ObjcLowering; +const semantic_diagnostics = @import("../semantic_diagnostics.zig"); + +const TypeId = types.TypeId; +const StringId = types.StringId; +const Ref = inst_mod.Ref; +const BlockId = inst_mod.BlockId; +const FuncId = inst_mod.FuncId; +const Function = inst_mod.Function; +const Module = mod_mod.Module; +const Builder = mod_mod.Builder; + + +const lower = @import("../lower.zig"); +const Lowering = lower.Lowering; +const Scope = lower.Scope; +const inferExprType = Lowering.inferExprType; +const isNamedTypeKind = Lowering.isNamedTypeKind; +const resolveBuiltin = Lowering.resolveBuiltin; +const structMethodFn = Lowering.structMethodFn; +const typeFnAuthor = Lowering.typeFnAuthor; + +pub fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name: []const u8, bindings: *std.StringHashMap(TypeId)) void { + // Mark as lowered before lowering (prevents infinite recursion) + // Need to dupe the name since mangled_name may be stack-allocated + const owned_name = self.alloc.dupe(u8, mangled_name) catch return; + self.lowered_functions.put(owned_name, {}) catch {}; + + // Save builder state + const saved_func = self.builder.func; + const saved_block = self.builder.current_block; + const saved_counter = self.builder.inst_counter; + const saved_scope = self.scope; + const saved_bindings = self.type_bindings; + const saved_defer_base = self.func_defer_base; + const saved_block_terminated = self.block_terminated; + const saved_target = self.target_type; + // Pack-fn mono state is lexical to the pack-fn body. A generic + // function called from inside a pack-fn mono (e.g. + // `build(args: []Type, $ret: Type)` invoked from + // `probe(..$args) { build($args, void) }`) must not inherit the + // caller's pack maps — `lowerFieldAccess`'s `.len` + // intercept would otherwise constant-fold the callee's + // same-named param to whichever shape triggered the first mono + // and bake the wrong arity into the cached IR. Same shape of + // fix as `lazyLowerFunction` (issue-0048, commit 0ede097). + const saved_pan = self.pack_arg_nodes; + const saved_ppc = self.pack_param_count; + const saved_pat = self.pack_arg_types; + const saved_iri = self.inline_return_target; + self.pack_arg_nodes = null; + self.pack_param_count = null; + self.pack_arg_types = null; + self.inline_return_target = null; + defer { + self.pack_arg_nodes = saved_pan; + self.pack_param_count = saved_ppc; + self.pack_arg_types = saved_pat; + self.inline_return_target = saved_iri; + } + self.func_defer_base = self.defer_stack.items.len; + self.block_terminated = false; + + // Install type bindings + self.type_bindings = bindings.*; + + // Pin to the template's defining module for the whole monomorphization + // (return type, param types, body), so a library-internal bare TYPE ref + // — e.g. `List(T).append`'s `alloc: Allocator` default-param type, or a + // body reference to a type visible only in the template's module — + // resolves where it is visible, not at the (possibly cross-module) call + // site. This is the issue-0100-F1 plain-fn pin extended to generic + // instantiation; without it the non-transitive bare-TYPE gate (E4) would + // reject a 2-flat-hop library type the call site cannot see directly. + // A synthesized / sourceless body keeps the caller's context. + const saved_source_mono = self.current_source_file; + defer self.setCurrentSourceFile(saved_source_mono); + if (fd.body.source_file) |src| self.setCurrentSourceFile(src); + + // Resolve return type with type bindings active. The body's tail + // expression inherits this as its target_type so bare `.{...}` + // literals resolve to the monomorphised return type instead of + // whatever leaked in from the caller (e.g. caller's xx target). + const ret_ty = self.resolveReturnType(fd); + self.target_type = ret_ty; + + const wants_ctx = self.funcWantsImplicitCtx(fd); + const saved_ctx_ref_mono = self.current_ctx_ref; + defer self.current_ctx_ref = saved_ctx_ref_mono; + + // Build param list (substituting type params, skipping type param declarations). + // Prepend `__sx_ctx: *void` at slot 0 if the function gets the implicit param. + var params = std.ArrayList(Function.Param).empty; + if (wants_ctx) { + params.append(self.alloc, .{ + .name = self.module.types.internString("__sx_ctx"), + .ty = self.module.types.ptrTo(.void), + }) catch unreachable; + } + for (fd.params) |p| { + if (isTypeParamDecl(&p, fd.type_params)) continue; + const pty = self.resolveParamType(&p); + params.append(self.alloc, .{ + .name = self.module.types.internString(p.name), + .ty = pty, + }) catch unreachable; + } + + // Create the monomorphized function + const name_id = self.module.types.internString(owned_name); + const func_id = self.builder.beginFunction(name_id, params.items, ret_ty); + _ = func_id; + self.builder.currentFunc().has_implicit_ctx = wants_ctx; + + // Create entry block + const entry_name = self.module.types.internString("entry"); + const entry = self.builder.appendBlock(entry_name, &.{}); + self.builder.switchToBlock(entry); + if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); + + // Create scope and bind params + var scope = Scope.init(self.alloc, null); + defer scope.deinit(); + self.scope = &scope; + + { + var param_idx: u32 = if (wants_ctx) 1 else 0; + for (fd.params) |p| { + if (isTypeParamDecl(&p, fd.type_params)) continue; + const pty = self.resolveParamType(&p); + const slot = self.builder.alloca(pty); + const param_ref = Ref.fromIndex(param_idx); + self.builder.store(slot, param_ref); + scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); + param_idx += 1; + } + } + + // Handle builtin function bodies (e.g. #builtin sqrt monomorphized to sqrt__f32) + if (fd.body.data == .builtin_expr) { + // Emit builtin call with param 0, then return + if (resolveBuiltin(fd.name)) |bid| { + const param0 = Ref.fromIndex(0); + const result = self.builder.callBuiltin(bid, &.{param0}, ret_ty); + self.builder.ret(result, ret_ty); + } else { + self.ensureTerminator(ret_ty); + } + self.builder.finalize(); + } else { + // Lower the function body + if (ret_ty != .void) { + const body_val = self.lowerBlockValue(fd.body); + if (!self.currentBlockHasTerminator()) { + if (body_val) |val| { + const val_ty = self.builder.getRefType(val); + const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val; + self.builder.ret(coerced, ret_ty); + } else { + self.ensureTerminator(ret_ty); + } + } + } else { + self.lowerBlock(fd.body); + self.ensureTerminator(ret_ty); + } + self.builder.finalize(); + } + + // Restore builder state + self.type_bindings = saved_bindings; + self.scope = saved_scope; + self.func_defer_base = saved_defer_base; + self.block_terminated = saved_block_terminated; + self.target_type = saved_target; + self.builder.func = saved_func; + self.builder.current_block = saved_block; + self.builder.inst_counter = saved_counter; +} + +// ── Reflection builtins ──────────────────────────────────────── + +/// Resolve a type argument from a call expression. Handles: +/// - Type param bindings ($T → concrete type via type_bindings) +/// - Direct type names (Vec4 → lookup in TypeTable) +/// - type_expr AST nodes +/// True iff `node` matches an AST shape that `resolveTypeArg` +/// can resolve to a concrete TypeId without falling through to +/// the silent `.s64` default. Used by `tryLowerReflectionCall` +/// to split static-fold from dynamic-builtin-call paths. +/// +/// Static-arg shapes mirror the explicit arms of `resolveTypeArg`: +/// - type_expr / identifier (type name or bound generic) +/// - pack_index_type_expr (`$pack[]`) +/// - compound type literals (pointer, array, slice, optional, +/// many_pointer, function_type_expr) +/// - parameterised type-constructor `call` (Vector, List, etc.) +/// - tuple_literal as a tuple TYPE +/// +/// Dynamic shapes (index_expr, field_access, runtime locals, +/// etc.) fall to the alternative path that emits a builtin_call. +pub fn isStaticTypeArg(self: *Lowering, node: *const Node) bool { + switch (node.data) { + .type_expr => |te| { + // A type-keyword name (e.g. `s64`) is always static. + // A user-defined name that happens to be in scope as + // a runtime variable (`x: Type = s64; type_name(x)`) + // is NOT static — route through the dynamic builtin + // call so the runtime lookup table fires. + if (self.scope) |scope| { + if (scope.lookup(te.name) != null) return false; + } + return true; + }, + .identifier => |id| { + if (self.scope) |scope| { + if (scope.lookup(id.name) != null) return false; + } + return true; + }, + .pack_index_type_expr, + .pointer_type_expr, + .many_pointer_type_expr, + .array_type_expr, + .slice_type_expr, + .optional_type_expr, + .function_type_expr, + .tuple_literal, + .call, + => return true, + else => return false, + } +} + +/// True iff `node` is a Type-shaped expression that resolves to a +/// concrete TypeId at lower time WITHOUT being a runtime variable +/// reference. Differs from `isStaticTypeArg` in that we exclude +/// identifiers that are in scope as runtime locals/globals — those +/// are runtime Type values (e.g. `t: Type = f64`) and the +/// comparison fold can't statically resolve them. +pub fn isStaticTypeRef(self: *Lowering, node: *const Node) bool { + switch (node.data) { + .type_expr => |te| { + // Compound type names (`s64`, `Point`, `Vec4`) resolve + // statically. If the name is also a runtime var in + // scope, it's a value reference, not a type ref. + if (self.scope) |scope| { + if (scope.lookup(te.name) != null) return false; + } + return self.isKnownTypeName(te.name) or + self.module.types.findByName(self.module.types.internString(te.name)) != null or + self.program_index.type_alias_map.get(te.name) != null; + }, + .identifier => |id| { + if (self.scope) |scope| { + if (scope.lookup(id.name) != null) return false; + } + return self.isKnownTypeName(id.name) or + self.module.types.findByName(self.module.types.internString(id.name)) != null or + self.program_index.type_alias_map.get(id.name) != null; + }, + .pointer_type_expr, + .many_pointer_type_expr, + .array_type_expr, + .slice_type_expr, + .optional_type_expr, + .function_type_expr, + .pack_index_type_expr, + => return true, + .call => |cl| { + // `type_of(x)` resolves statically when `x`'s type is + // known — which it always is for a typed expression. + if (cl.callee.data == .identifier and + std.mem.eql(u8, cl.callee.data.identifier.name, "type_of") and + cl.args.len == 1) + { + return true; + } + return false; + }, + else => return false, + } +} + +/// Resolve a tuple LITERAL used in a type position (`(s32, s32)` reinterpreted +/// as a tuple type at a type-demanding site such as `size_of`). Every element +/// must itself denote a type; a non-type element — e.g. the `1` in +/// `(s32, 1)` — is a user error. Emit a diagnostic pointing at the offending +/// element and return `.unresolved`; never fabricate a tuple with a bogus +/// field (issue 0067). type_bridge.resolveAstType builds the tuple only after +/// this validation passes. +pub fn resolveTupleLiteralTypeArg(self: *Lowering, node: *const Node) TypeId { + for (node.data.tuple_literal.elements) |el| { + if (!type_bridge.isTypeShapedAstNode(el.value, &self.module.types)) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, el.value.span, "tuple type element is not a type (found `{s}`); a tuple used as a type must list only types, e.g. `(s32, s32)`", .{@tagName(el.value.data)}); + } + return .unresolved; + } + // E4 single-hop visibility gate: each element leaf is resolved through + // the source-aware resolver, so a 2-flat-hop inner leaf (`(COnly, s64)`) + // emits "not visible" + poisons rather than leaking through + // `type_bridge`'s ungated global lookup. A valid element resolves to the + // same TypeId the delegated build produces below (no diagnostic, no + // drift); only the poison short-circuits. + if (self.resolveTypeWithBindings(el.value) == .unresolved) return .unresolved; + } + return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); +} + +pub fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId { + // Pack-index access in a type-arg slot (e.g. `type_name($args[0])` + // or `type_eq($args[i], s64)`). Same shape as the + // `resolveTypeWithBindings` arm — looks up the bound pack types + // and returns the i-th. OOB and no-active-binding emit focused + // diagnostics rather than silently defaulting to .s64 (the + // catch-all `else` below) — that fall-through is exactly the + // "silent unimplemented arm" the project's REJECTED PATTERNS + // forbid. + if (node.data == .pack_index_type_expr) { + const pi = node.data.pack_index_type_expr; + if (self.pack_arg_types) |pat| { + if (pat.get(pi.pack_name)) |arg_tys| { + if (pi.index < arg_tys.len) return arg_tys[pi.index]; + if (self.diagnostics) |diags| { + diags.addFmt(.err, node.span, "pack-index ${s}[{}] out of bounds: '{s}' has {} element{s}", .{ + pi.pack_name, pi.index, pi.pack_name, arg_tys.len, + if (arg_tys.len == 1) @as([]const u8, "") else @as([]const u8, "s"), + }); + } + return .unresolved; + } + } + if (self.diagnostics) |diags| { + diags.addFmt(.err, node.span, "pack-index ${s}[{}] used outside an active pack binding", .{ + pi.pack_name, pi.index, + }); + } + return .unresolved; + } + // Bare `$` in a type-arg position. Single-type generic + // bindings (`$R: Type` in `Closure(..$args) -> $R`) live in + // `type_bindings`; if the name is bound there, return the + // bound TypeId directly. Pack bindings would otherwise resolve + // to a slice value, not a single Type — the caller (e.g. + // `type_name(...)`) expects a single arg. + if (node.data == .comptime_pack_ref) { + const cpr = node.data.comptime_pack_ref; + if (self.type_bindings) |tb| { + if (tb.get(cpr.pack_name)) |ty| return ty; + } + } + switch (node.data) { + .identifier => |id| { + // Check type bindings first (from generic monomorphization) + if (self.type_bindings) |tb| { + if (tb.get(id.name)) |ty| return ty; + } + // E4 single-hop visibility + ambiguity gate: a bare type name + // reachable only over 2+ flat hops is not bare-visible in a + // reflection / type-arg slot (consistent with normal annotations / + // 0763); ≥2 direct flat same-name authors are ambiguous (loud + // diagnostic, consistent with the leaf / 0755) instead of a global + // first-/last-wins pick; a single source-keyed author resolves to + // ITS TypeId. A genuinely-undeclared name is NOT authored as a type + // anywhere → `.proceed`, falling to the "unresolved type" + // diagnostic below. + switch (self.headTypeGate(id.name, node.span)) { + .ambiguous, .not_visible => return .unresolved, + .resolved => |tid| return tid, + .proceed => {}, + } + if (self.program_index.type_alias_map.get(id.name)) |alias_ty| return alias_ty; + const name_id = self.module.types.internString(id.name); + if (self.module.types.findByName(name_id)) |t| return t; + if (self.diagnostics) |diags| { + diags.addFmt(.err, node.span, "unresolved type: '{s}'", .{id.name}); + } + return .unresolved; + }, + .type_expr => |te| { + if (self.headTypeLeak(te.name, node.span)) return .unresolved; + if (self.program_index.type_alias_map.get(te.name)) |alias_ty| return alias_ty; + return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + }, + .call => |cl| { + // `type_of(x)` resolves to `inferExprType(x)` at lower + // time when `x`'s type is statically known (which it + // is for any expression — type inference always + // produces a concrete TypeId). Lets + // `type_of(a) == s64` fold the same as + // `inferExprType(a) == s64`. + if (cl.callee.data == .identifier and + std.mem.eql(u8, cl.callee.data.identifier.name, "type_of") and + cl.args.len == 1) + { + return self.inferExprType(cl.args[0]); + } + // Handle type constructor calls: size_of(Sx(f32)), size_of(Complex(u32)) + return self.resolveTypeCallWithBindings(&cl); + }, + // Wrapped / structural forms (`*T`, `[N]T`, `[]T`, `?T`, fn-ptr, tuple) + // route through the gated `resolveTypeWithBindings`, whose + // `resolveCompound` recurses each element through the source-aware leaf + // (`resolveNominalLeaf`) — so a 2-hop inner leaf (`*COnly`, `[2]COnly`, + // `(COnly, s64)`) is rejected exactly as in a normal annotation, instead + // of `type_bridge.resolveAstType`'s ungated global lookup (E4). + .tuple_literal, + .pointer_type_expr, + .many_pointer_type_expr, + .array_type_expr, + .slice_type_expr, + .optional_type_expr, + .function_type_expr, + => return self.resolveTypeWithBindings(node), + else => return .unresolved, + } +} + +/// Format a type name for display (e.g. "*Point", "[]s32", "[3]f64"). +pub fn formatTypeName(self: *Lowering, ty: TypeId) []const u8 { + // Builtin types: use their canonical name + 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.formatTypeName(p.pointee); + break :blk std.fmt.allocPrint(self.alloc, "*{s}", .{inner}) catch "pointer"; + }, + .many_pointer => |p| blk: { + const inner = self.formatTypeName(p.element); + break :blk std.fmt.allocPrint(self.alloc, "[*]{s}", .{inner}) catch "many_pointer"; + }, + .slice => |s| blk: { + const inner = self.formatTypeName(s.element); + break :blk std.fmt.allocPrint(self.alloc, "[]{s}", .{inner}) catch "slice"; + }, + .array => |a| blk: { + const inner = self.formatTypeName(a.element); + break :blk std.fmt.allocPrint(self.alloc, "[{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.formatTypeName(o.child); + break :blk std.fmt.allocPrint(self.alloc, "?{s}", .{inner}) catch "optional"; + }, + .vector => |v| blk: { + const inner = self.formatTypeName(v.element); + break :blk std.fmt.allocPrint(self.alloc, "Vector({d},{s})", .{ v.length, inner }) catch "vector"; + }, + else => @tagName(info), + }; +} + +/// Format a function type string like "() -> s32" or "(s32, s32) -> s32". +pub fn formatFnTypeString(self: *Lowering, fd: *const ast.FnDecl) []const u8 { + var buf: [512]u8 = undefined; + var pos: usize = 0; + buf[pos] = '('; + pos += 1; + for (fd.params, 0..) |p, i| { + if (i > 0) { + @memcpy(buf[pos..][0..2], ", "); + pos += 2; + } + const pty = self.resolveParamType(&p); + const name = self.formatTypeName(pty); + @memcpy(buf[pos..][0..name.len], name); + pos += name.len; + } + buf[pos] = ')'; + pos += 1; + const ret_ty = self.resolveReturnType(fd); + if (ret_ty != .void) { + @memcpy(buf[pos..][0..4], " -> "); + pos += 4; + const rname = self.formatTypeName(ret_ty); + @memcpy(buf[pos..][0..rname.len], rname); + pos += rname.len; + } + const result = self.alloc.alloc(u8, pos) catch unreachable; + @memcpy(result, buf[0..pos]); + return result; +} + +/// 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). +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), + .slice_type_expr => |st| matchTypeParamStatic(st.element_type, tp_name), + .pointer_type_expr => |pt| matchTypeParamStatic(pt.pointee_type, tp_name), + .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, + }; +} + +pub fn matchTypeParamStatic(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), + .slice_type_expr => |st| matchTypeParamStatic(st.element_type, tp_name), + .pointer_type_expr => |pt| matchTypeParamStatic(pt.pointee_type, tp_name), + .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, + }; +} + +/// 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. +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, + .slice_type_expr => |st| blk: { + // arg_ty should be a slice → extract element type + if (arg_ty.isBuiltin()) break :blk null; + const info = self.module.types.get(arg_ty); + break :blk switch (info) { + .slice => |s| self.extractTypeParam(st.element_type, s.element, tp_name), + else => null, + }; + }, + .pointer_type_expr => |pt| blk: { + if (arg_ty.isBuiltin()) break :blk null; + const info = self.module.types.get(arg_ty); + break :blk switch (info) { + .pointer => |p| self.extractTypeParam(pt.pointee_type, p.pointee, tp_name), + else => null, + }; + }, + .many_pointer_type_expr => |mp| blk: { + if (arg_ty.isBuiltin()) break :blk null; + const info = self.module.types.get(arg_ty); + break :blk switch (info) { + .many_pointer => |p| self.extractTypeParam(mp.element_type, p.element, tp_name), + else => null, + }; + }, + .optional_type_expr => |ot| blk: { + if (arg_ty.isBuiltin()) break :blk null; + const info = self.module.types.get(arg_ty); + break :blk switch (info) { + .optional => |o| self.extractTypeParam(ot.inner_type, o.child, tp_name), + else => null, + }; + }, + .array_type_expr => |at| blk: { + if (arg_ty.isBuiltin()) break :blk null; + const info = self.module.types.get(arg_ty); + break :blk switch (info) { + .array => |a| self.extractTypeParam(at.element_type, a.element, tp_name), + 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, + }; +} + +/// 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 { + return self.genericResolver().mangleTypeName(ty); +} + +/// Resolve type category names (like "int", "struct", "float") to matching TypeId tag values. +/// Returns a list of TypeId index values that match the category. +pub fn resolveTypeCategoryTags(self: *Lowering, name: []const u8) []const u64 { + var tags = std.ArrayList(u64).empty; + + // Fixed builtin categories + if (std.mem.eql(u8, name, "int")) { + tags.append(self.alloc, TypeId.s8.index()) catch {}; + tags.append(self.alloc, TypeId.s16.index()) catch {}; + tags.append(self.alloc, TypeId.s32.index()) catch {}; + tags.append(self.alloc, TypeId.s64.index()) catch {}; + tags.append(self.alloc, TypeId.u8.index()) catch {}; + tags.append(self.alloc, TypeId.u16.index()) catch {}; + tags.append(self.alloc, TypeId.u32.index()) catch {}; + tags.append(self.alloc, TypeId.u64.index()) catch {}; + tags.append(self.alloc, TypeId.usize.index()) catch {}; + tags.append(self.alloc, TypeId.isize.index()) catch {}; + return tags.items; + } + if (std.mem.eql(u8, name, "float")) { + tags.append(self.alloc, TypeId.f32.index()) catch {}; + tags.append(self.alloc, TypeId.f64.index()) catch {}; + return tags.items; + } + if (std.mem.eql(u8, name, "bool")) { + tags.append(self.alloc, TypeId.bool.index()) catch {}; + return tags.items; + } + if (std.mem.eql(u8, name, "string")) { + tags.append(self.alloc, TypeId.string.index()) catch {}; + return tags.items; + } + if (std.mem.eql(u8, name, "void")) { + tags.append(self.alloc, TypeId.void.index()) catch {}; + return tags.items; + } + if (std.mem.eql(u8, name, "type") or std.mem.eql(u8, name, "Type")) { + tags.append(self.alloc, TypeId.any.index()) catch {}; + return tags.items; + } + + // Dynamic categories: scan TypeTable for matching types + const Category = enum { @"struct", @"enum", @"union", slice, array, pointer, vector, optional, error_set }; + const cat: ?Category = if (std.mem.eql(u8, name, "struct")) + .@"struct" + else if (std.mem.eql(u8, name, "enum") or std.mem.eql(u8, name, "union")) + .@"enum" + else if (std.mem.eql(u8, name, "slice")) + .slice + else if (std.mem.eql(u8, name, "array")) + .array + else if (std.mem.eql(u8, name, "pointer")) + .pointer + else if (std.mem.eql(u8, name, "vector")) + .vector + else if (std.mem.eql(u8, name, "optional")) + .optional + else if (std.mem.eql(u8, name, "error_set")) + .error_set + else + null; + + if (cat) |c| { + for (self.module.types.infos.items, 0..) |info, idx| { + const matches = switch (c) { + .@"struct" => info == .@"struct", + .@"enum" => info == .@"enum" or info == .tagged_union, + .@"union" => info == .@"union" or info == .tagged_union, + .slice => info == .slice, + .array => info == .array, + .pointer => info == .pointer or info == .many_pointer, + .vector => info == .vector, + .optional => info == .optional, + .error_set => info == .error_set, + }; + if (matches) { + tags.append(self.alloc, @intCast(idx)) catch {}; + } + } + } + + // Specific type name (e.g., Point, Color) — look up in type registry + if (tags.items.len == 0) { + const name_id = self.module.types.internString(name); + if (self.module.types.findByName(name_id)) |tid| { + tags.append(self.alloc, tid.index()) catch {}; + } + } + + return tags.items; +} + +/// Check if a match expression is a type-category match (patterns are type/category names). +pub fn inferMatchResultType(self: *Lowering, me: *const ast.MatchExpr) TypeId { + // Infer result type from the first non-null arm body. + // If we skip null_literal arms and find a concrete type T, and there + // were null arms, the result is ?T (optional). + var has_null = false; + var saw_unresolved = false; + var saw_noreturn = false; + for (me.arms) |arm| { + const last_node = if (arm.body.data == .block) blk: { + if (arm.body.data.block.stmts.len > 0) { + break :blk arm.body.data.block.stmts[arm.body.data.block.stmts.len - 1]; + } + break :blk arm.body; + } else arm.body; + + if (last_node.data == .null_literal) { + has_null = true; + continue; + } + + // First arm with a statically-inferable type determines the result. + // An arm whose type isn't inferable from the AST alone (e.g. a bare + // enum literal) doesn't decide — keep looking; the caller falls back + // to the contextual target type if none of the arms resolve. + const arm_ty = self.inferExprType(last_node); + // A diverging arm (`noreturn` — `return` / `raise` / `break` / + // `continue`) doesn't produce a value, so it doesn't decide the + // result type; keep looking. The match is `noreturn` only if EVERY + // arm diverges (handled after the loop). + if (arm_ty == .noreturn) { + saw_noreturn = true; + continue; + } + if (arm_ty == .unresolved) { + saw_unresolved = true; + continue; + } + if (has_null and arm_ty != .void) { + return self.module.types.optionalOf(arm_ty); + } + return arm_ty; + } + if (saw_unresolved) return .unresolved; + if (saw_noreturn) return .noreturn; // all arms diverge + return .void; +} + +pub fn isTypeCategoryMatch(me: *const ast.MatchExpr) bool { + for (me.arms) |arm| { + if (arm.pattern) |pat| { + const name = switch (pat.data) { + .identifier => |id| id.name, + .type_expr => |te| te.name, + else => continue, + }; + const categories = [_][]const u8{ + "int", "float", "bool", "string", "void", "type", "Type", + "struct", "enum", "union", "slice", "array", "pointer", "vector", + }; + for (categories) |cat| { + if (std.mem.eql(u8, name, cat)) return true; + } + // Also match specific struct/enum type names (e.g., case Point:) + if (name.len > 0 and name[0] >= 'A' and name[0] <= 'Z') return true; + } + } + return false; +} + +/// Check if a param is a type param declaration ($T: Type). +/// A type param declaration has param.name == one of the type_params names. +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; + } + return false; +} + +/// Check if a function has comptime (non-Type) value parameters. +pub fn hasComptimeParams(fd: *const ast.FnDecl) bool { + for (fd.params) |p| { + if (p.is_comptime) return true; + } + return false; +} + +/// A plain free function: no type params (not generic) and an ordinary sx +/// body (not `#foreign` / `#builtin` / `#compiler`). Only these get an +/// out-of-line identity-addressable slot — the bare-call disambiguation +/// (fix-0102c) and the shadow-author lowering pass leave every other shape +/// to the existing name-keyed dispatch. +pub fn isPlainFreeFn(fd: *const ast.FnDecl) bool { + if (fd.type_params.len > 0) return false; + return switch (fd.body.data) { + .foreign_expr, .builtin_expr, .compiler_expr => false, + else => true, + }; +} + +/// Resolve a generic value-param argument (`$K: u32`) to its compile-time +/// integer AND verify it fits the param's declared integer type. The folded +/// value is bound and mangled into the instantiation name, so a module/generic +/// const arg (`Vec(N, f32)`), a const expression (`Make(M + 1, s64)`), an +/// integral float (`Box(4.0)` → 4), and a literal (`Vec(3, f32)`) all bind the +/// same value a literal would. An out-of-range arg (`Box(5_000_000_000)` for a +/// `u32` param) or a non-const arg emits a clean diagnostic and returns null; +/// the caller bails rather than binding a truncated / fabricated value under a +/// wrong mangled name. +/// +/// `type_name` is the param's declared constraint type (`"u32"`, null if +/// unknown). A `u32` count routes through the shared +/// `program_index.foldDimU32` — the SAME fold-and-narrow gate an array dim / +/// Vector lane uses — so the documented "single u32 gate for value-param +/// counts" holds; any other integer type range-checks against +/// `program_index.intTypeRange`; an unrecognised type folds without bounding. +pub fn resolveValueParamArg(self: *Lowering, arg_node: *const Node, param_name: []const u8, type_name: ?[]const u8) ?i64 { + // Resolve an ALIASED integer constraint (`$K: Count` where `Count :: u32`, + // `$K: Small` where `Small :: s8`) to its underlying builtin so the range + // gate below treats it exactly like `$K: u32` / `$K: s8` (issue 0083 — an + // alias previously slipped past `intTypeRange`, so `Box(5_000_000_000)` + // with `$K: Count` bound a truncated value). A non-integer / unrecognised + // constraint yields null → no range bound (fold only), as before. + const tn_canon: ?[]const u8 = if (type_name) |tn| self.canonicalIntConstraintName(tn) else null; + if (tn_canon) |tn| { + if (std.mem.eql(u8, tn, "u32")) { + switch (program_index_mod.foldDimU32(arg_node, self, 0)) { + .ok => |n| return n, + .not_const, .non_integral_float => { + self.diagValueParamNotConst(arg_node, param_name); + return null; + }, + .below_min => |v| { + self.diagValueParamRange(arg_node, param_name, tn, v); + return null; + }, + .too_large => |v| { + self.diagValueParamRange(arg_node, param_name, tn, v); + return null; + }, + } + } + } + // Non-`u32` integer constraint: fold through the SAME unified count fold + // so an integral float arg (`Box(4.0)`, `Make(F + 1.5, ...)`) binds the + // integer it equals, exactly as the `u32` gate above does; a non-integral + // float / non-const arg is not a valid count. + const v = switch (program_index_mod.foldCountI64(arg_node, self)) { + .int => |iv| iv, + .non_integral, .not_const => { + self.diagValueParamNotConst(arg_node, param_name); + return null; + }, + }; + if (tn_canon) |tn| { + if (program_index_mod.intTypeRange(tn)) |r| { + if (v < r.min or v > r.max) { + self.diagValueParamRange(arg_node, param_name, tn, v); + return null; + } + } + } + return v; +} + +/// Resolve a generic value-param constraint type NAME to its canonical builtin +/// integer type name, chasing a type alias (`Count :: u32` → "u32", +/// `Small :: s8` → "s8") so an ALIASED integer constraint range-checks exactly +/// like the builtin it names. Returns the name unchanged when it is already a +/// builtin integer; null when it isn't an integer type (directly or via alias) +/// — the caller then folds without a range bound rather than guessing. The +/// alias map + type table are the same single sources every other resolver +/// reads, so this can't diverge from how the alias is laid out elsewhere. +pub fn canonicalIntConstraintName(self: *Lowering, name: []const u8) ?[]const u8 { + if (program_index_mod.intTypeRange(name) != null) return name; + if (self.program_index.type_alias_map.get(name)) |tid| { + const canon = self.module.types.typeName(tid); + if (program_index_mod.intTypeRange(canon) != null) return canon; + } + return null; +} + +pub fn diagValueParamNotConst(self: *Lowering, arg_node: *const Node, param_name: []const u8) void { + if (self.diagnostics) |d| + d.addFmt(.err, arg_node.span, "generic value parameter '{s}' must be a compile-time integer constant", .{param_name}); +} + +pub fn diagValueParamRange(self: *Lowering, arg_node: *const Node, param_name: []const u8, type_name: []const u8, value: i64) void { + if (self.diagnostics) |d| + d.addFmt(.err, arg_node.span, "value {} does not fit in {s} parameter {s}", .{ value, type_name, param_name }); +} + +/// The poison-vs-proceed projection of `headTypeGate` for an UNQUALIFIED +/// parameterized type HEAD that names a generic STRUCT, a parameterized +/// PROTOCOL, or a type-returning function used as a head (`Box(s64)`, +/// `VL(s64)`) — and the alias-registration / type-match sites that likewise +/// only need "poison or proceed". Returns TRUE (the gate's loud diagnostic is +/// already emitted) when the head is `.not_visible` (a 2-flat-hop leak) or +/// `.ambiguous` (≥2 direct flat same-name authors — consistent with the leaf / +/// 0755); FALSE when it resolves or falls open. See `headTypeGate` for the full +/// non-transitive visibility + ambiguity model and the fall-open conditions. +pub fn headTypeLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool { + // A head site INSTANTIATES (template / type-fn) rather than substituting a + // nominal TypeId, so it consumes only the poison-vs-proceed bit of the + // full author outcome: `.ambiguous` / `.not_visible` (loud diagnostic + // already emitted by `headTypeGate`) poison; `.resolved` / `.proceed` + // proceed to instantiation. + return switch (self.headTypeGate(name, span)) { + .ambiguous, .not_visible => true, + .proceed, .resolved => false, + }; +} + +/// Control-flow outcome of the generic-struct LAYOUT-head selector. Carries no +/// diagnostic for the caller to emit — `selectGenericStructHead` emits inline. +const HeadTemplate = union(enum) { + template: StructTemplate, // visible bare author OR qualified author → instantiate + poisoned, // gate already diagnosed → caller returns .unresolved / Ref.none + not_generic, // name is not a generic struct head → caller's non-struct path +}; + +/// THE single selector every generic-struct LAYOUT-head site funnels through — +/// no head site reads `struct_template_map` for selection directly. Decides the +/// authoring template for a head named `name`, qualified by namespace `alias` +/// (non-null only for `ns.Box(..)` with an identifier object) and flagged +/// `is_qualified` (any `.field_access` callee, including a non-identifier +/// object). Emits the visibility / missing-member diagnostics INLINE at `span`, +/// at the same program point and ordering the sites used before (0767/0769/0775), +/// and returns a control-flow-only outcome: +/// - qualified, namespace authors `name` as a generic struct → that author. +/// - qualified, namespace exists but lacks `name` → diagnose missing member, +/// `.poisoned` (never the bare global map, E4 #2). +/// - qualified, namespace authors `name` but NOT as a generic struct (a +/// type-fn / named type) → `.not_generic` (caller's non-struct path). +/// - qualified with no usable alias (nested-ns object) → the global template +/// if one exists (pre-existing behavior; no namespace edge to consult). +/// - bare, ≥2 visible authors / 2-flat-hop only → `headTypeLeak` diagnosed → +/// `.poisoned`. +/// - bare, single visible author → that author (own / 1-hop flat), source-keyed. +/// - bare, visible author IS the canonical map author → the global template +/// (byte-identical single-author path). +/// - not in `struct_template_map` at all → `.not_generic`. +pub fn selectGenericStructHead(self: *Lowering, name: []const u8, alias: ?[]const u8, is_qualified: bool, span: ?ast.Span) HeadTemplate { + if (is_qualified) { + if (alias) |a| { + if (self.qualifiedStructTemplate(a, name)) |tmpl| return .{ .template = tmpl }; + if (self.qualifiedMemberMissing(a, name)) { + if (self.diagnostics) |d| + d.addFmt(.err, span, "namespace '{s}' has no member '{s}'", .{ a, name }); + return .poisoned; + } + return .not_generic; + } + // Qualified but un-aliasable object (nested namespace / non-identifier): + // no namespace edge to select from — use the global template if present. + if (self.program_index.struct_template_map.getPtr(name)) |tmpl| return .{ .template = tmpl.* }; + return .not_generic; + } + if (self.program_index.struct_template_map.getPtr(name)) |tmpl| { + if (self.headTypeLeak(name, span)) return .poisoned; + if (self.bareVisibleStructTemplate(name)) |vt| return .{ .template = vt }; + return .{ .template = tmpl.* }; + } + return .not_generic; +} + +/// Decompose a head callee NODE (`.identifier Box` or `.field_access ns.Box`) +/// into the `(name, alias, is_qualified)` triple `selectGenericStructHead` +/// consumes. `alias` is the namespace identifier only for a `.field_access` +/// whose object is a plain identifier; a nested / non-identifier object is +/// qualified-but-unaliased. +const HeadName = struct { name: []const u8, alias: ?[]const u8, is_qualified: bool }; +pub fn headNameOfCallee(callee: *const Node) ?HeadName { + return switch (callee.data) { + .identifier => |id| .{ .name = id.name, .alias = null, .is_qualified = false }, + .field_access => |fa| .{ + .name = fa.field, + .alias = if (fa.object.data == .identifier) fa.object.data.identifier.name else null, + .is_qualified = true, + }, + else => null, + }; +} + +/// The complete source-aware author outcome of an UNQUALIFIED bare TYPE head — +/// the unified non-transitive visibility + ambiguity gate every bare-type- +/// reference site OUTSIDE the nominal leaf routes through (E4 attempt-5): +/// reflection / type-arg slots, typed array/vector-literal heads, parameterized +/// generic / protocol / type-fn heads, type-as-value, and type-category match +/// arms. Mirrors `selectNominalLeaf`'s author model so a 2-flat-hop type is +/// `.not_visible`, ≥2 direct flat same-name authors are `.ambiguous` (the LOUD +/// diagnostic, consistent with the leaf / 0755 — never a silent global +/// `findByName` / `struct_template_map` first-/last-wins pick), and a single +/// direct flat author resolves to ITS source-keyed TypeId. Falls open +/// (`.proceed`) when import facts are unwired, the source context is absent, +/// the default-Context emitter is running (built-in infrastructure resolves +/// independent of the user's import style, F1), the querying source is the OWN +/// author, a single flat author is not registered yet (a forward / foreign / +/// generic template — the caller instantiates it), or `name` is a block-local +/// of this source / no type author at all. Library-internal heads stay visible +/// because every instantiation kind is source-pinned to the template's defining +/// module (E3/E4 #1): the query originates THERE, where the head is a direct +/// flat import. A namespaced `ns.Box(..)` head is an explicit qualified reach +/// and is exempt (the caller skips this gate). +const HeadTypeGate = union(enum) { + proceed, + resolved: TypeId, + ambiguous, + not_visible, +}; +pub fn headTypeGate(self: *Lowering, name: []const u8, span: ?ast.Span) HeadTypeGate { + if (self.emitting_default_context) return .proceed; + if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) return .proceed; + const from = self.current_source_file orelse return .proceed; + + var res_walk = self.resolver(); + const author_set = res_walk.collectVisibleAuthors(name, from, .user_bare_flat); + defer if (author_set.flat.len > 0) self.alloc.free(author_set.flat); + + // Own author wins outright (own-wins, 0754). Pending / unregistered → .proceed. + if (author_set.own) |own| switch (own.raw) { + .const_decl => { + if (self.program_index.type_aliases_by_source.get(own.source)) |inner| { + if (inner.get(name)) |tid| return .{ .resolved = tid }; + } + return .proceed; + }, + else => if (isNamedTypeKind(own.raw)) { + if (self.namedRefTid(own.raw, name)) |tid| return .{ .resolved = tid }; + return .proceed; + }, + }; + + // Flat type authors + var flat_type_count: usize = 0; + var found_tid: ?TypeId = null; + var flat_tid_count: usize = 0; + for (author_set.flat) |fa| { + const is_type = switch (fa.raw) { + .const_decl => blk: { + if (self.program_index.type_aliases_by_source.get(fa.source)) |inner| + break :blk inner.contains(name); + break :blk false; + }, + else => isNamedTypeKind(fa.raw), + }; + if (!is_type) continue; + flat_type_count += 1; + const fa_tid: ?TypeId = switch (fa.raw) { + .const_decl => blk: { + if (self.program_index.type_aliases_by_source.get(fa.source)) |inner| + break :blk inner.get(name); + break :blk null; + }, + else => self.namedRefTid(fa.raw, name), + }; + if (fa_tid) |t| { + flat_tid_count += 1; + if (found_tid) |f| { if (t != f) { + if (self.diagnostics) |d| + d.addFmt(.err, span, "type '{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{name}); + return .ambiguous; + } } else found_tid = t; + } + } + if (flat_type_count > 0) { + // ≥2 authors but not all resolved to one TypeId → ambiguous + if (flat_type_count >= 2 and !(flat_tid_count == flat_type_count and found_tid != null)) { + if (self.diagnostics) |d| + d.addFmt(.err, span, "type '{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{name}); + return .ambiguous; + } + if (found_tid) |t| return .{ .resolved = t }; + return .proceed; // single author exists but TypeId not registered + } + + if (self.localTypeInSource(from, name)) return .proceed; + if (!self.nameAuthoredAsTypeAnywhere(name)) return .proceed; + if (self.diagnostics) |d| + d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name}); + return .not_visible; +} + +/// Single-hop non-transitive visibility + ambiguity gate for an UNQUALIFIED +/// type-returning FUNCTION head used as a type (`Make(N, T)` where +/// `Make :: ($K, $T) -> Type`). A type-fn is a `fn_decl`, so visibility is +/// decided from the ELIGIBLE FUNCTION authors directly reachable from the use +/// site (`flatFnAuthorVisible`) — NOT the module-scope NAME predicate +/// (`isNameVisible`), which a same-name NON-function (a value const, a named +/// type) would wrongly vouch for. Returns TRUE (loud diagnostic already +/// emitted) when the head is AMBIGUOUS (≥2 distinct direct flat same-name +/// type-fn authors, no own author — consistent with the parameterized struct / +/// protocol heads and the leaf, 0755/0767, never a silent `fn_ast_map` +/// first-/last-wins pick) or NOT-VISIBLE (its only directly-visible same-name +/// author is a non-function and the real type-fn author is ≥2 flat hops away). +/// A scope-local (mangled) type-fn or the querying source's OWN function author +/// wins outright (own-wins) and is exempt; falls open when unwired / +/// default-context. Diagnostic mirrors the type form (the head IS used as a type +/// here). +pub fn headFnLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool { + if (self.emitting_default_context) return false; + const from = self.current_source_file orelse return false; + if (self.scope) |s| if (s.lookupFn(name) != null) return false; + // Fall open when the import facts aren't wired (comptime callers, + // directory imports without a main file): the author collector would + // otherwise return an empty set and wrongly report a genuinely-visible + // type-fn as not-visible. Mirrors `headTypeGate`'s guard. + if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) return false; + // ≥2 distinct direct flat type-fn authors with no own author — a genuine + // collision the source cannot disambiguate. Diagnose loudly BEFORE the + // visibility short-circuit, which would otherwise let the single + // `fn_ast_map[name]` author silently win. + if (self.flatFnAuthorAmbiguous(name, from)) { + if (self.diagnostics) |d| + d.addFmt(.err, span, "type '{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{name}); + return true; + } + // KIND-AWARE: visible iff a directly-reachable (own or 1-hop flat) author + // is itself a TYPE-FUNCTION. A same-name 1-hop non-function (attempt-7) OR + // ordinary non-type function (attempt-8) does NOT vouch for a type-fn head + // whose real author is 2 flat hops away. + if (self.flatFnAuthorVisible(name, from)) return false; + if (self.diagnostics) |d| + d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name}); + return true; +} + +/// TRUE iff bare `name` has ≥2 DISTINCT direct flat-import authors that are +/// TYPE-FUNCTIONS (`typeFnAuthor`: a `fn_decl` with ≥1 `$`-param — an ordinary +/// same-name function does not count) and the querying source authors NONE +/// itself. The querying source's OWN +/// author wins outright (own-wins), so an own author short-circuits to "not +/// ambiguous" — the existing single-author path instantiates it. Diamond +/// imports of the SAME author collapse in `collectVisibleAuthors`'s +/// author-identity de-dup, so two edges onto one type-fn are NOT ambiguous. The +/// type-fn ambiguity analogue of `flatTypeAuthorCount`'s `.ambiguous` for named +/// type / template heads. +pub fn flatFnAuthorAmbiguous(self: *Lowering, name: []const u8, from: []const u8) bool { + var res = self.resolver(); + const set = res.collectVisibleAuthors(name, from, .user_bare_flat); + defer if (set.flat.len > 0) self.alloc.free(set.flat); + if (set.own != null) return false; // own-wins + var fn_authors: usize = 0; + for (set.flat) |fa| { + if (typeFnAuthor(fa.raw)) fn_authors += 1; + } + return fn_authors >= 2; +} + +/// TRUE iff bare `name` has at least one DIRECTLY-visible author — the +/// querying source's OWN author or a 1-hop flat-import author — that is a +/// TYPE-FUNCTION (`typeFnAuthor`: a `fn_decl` with ≥1 `$`-param). The KIND-AWARE +/// analogue of `isNameVisible` for a type-fn head: a same-name 1-hop +/// NON-function (a value const `Make :: 123`, a named type) does NOT vouch +/// (attempt-7), and — crucially — neither does a same-name 1-hop ORDINARY +/// function (`Make :: () -> s32`, zero `$`-params), which cannot be the type +/// head being instantiated (attempt-8). So a type-fn whose only directly- +/// visible same-name author is a non-fn OR a non-type-fn — its real author 2 +/// flat hops away — is correctly invisible. Mirrors `flatFnAuthorAmbiguous`'s +/// type-fn-only author view. +pub fn flatFnAuthorVisible(self: *Lowering, name: []const u8, from: []const u8) bool { + var res = self.resolver(); + const set = res.collectVisibleAuthors(name, from, .user_bare_flat); + defer if (set.flat.len > 0) self.alloc.free(set.flat); + if (set.own) |own| { + if (typeFnAuthor(own.raw)) return true; + } + for (set.flat) |fa| { + if (typeFnAuthor(fa.raw)) return true; + } + return false; +} + +/// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)). +pub fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId { + // A namespaced callee (`ns.Box(..)`) is an explicit qualified reach and is + // exempt from the bare-head visibility gate; only a plain identifier head + // is policed (E4). + const is_qualified = cl.callee.data == .field_access; + const callee_name: []const u8 = switch (cl.callee.data) { + .identifier => |id| id.name, + .field_access => |fa| fa.field, + else => return .unresolved, + }; + // Built-in: Vector(N, T) + if (std.mem.eql(u8, callee_name, "Vector") and cl.args.len == 2) { + const length = self.resolveVectorLane(cl.args[0]) orelse return .unresolved; + const elem = self.resolveTypeWithBindings(cl.args[1]); + return self.module.types.vectorOf(elem, length); + } + // Generic-struct head: route through the single layout choke-point (CP-1). + // Bare → the single bare-VISIBLE author (own / 1-hop flat), source-keyed; + // qualified `ns.Box(..)` → ns's OWN template (or a missing-member diagnostic); + // never the global last-wins map for a visible-shadowed or qualified head. + if (headNameOfCallee(cl.callee)) |hn| { + switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, cl.callee.span)) { + .template => |t| return self.instantiateGenericStruct(&t, cl.args), + .poisoned => return .unresolved, + .not_generic => {}, + } + } + // User-defined type-returning function: Complex(u32), Sx(f32) + // Also resolve via scope fn_names (local functions get mangled names) + const resolved_name = if (self.scope) |scope| (scope.lookupFn(callee_name) orelse callee_name) else callee_name; + if (self.program_index.fn_ast_map.get(resolved_name)) |fd| { + if (fd.type_params.len > 0) { + if (!is_qualified and self.headFnLeak(callee_name, cl.callee.span)) return .unresolved; + if (self.instantiateTypeFunction(callee_name, callee_name, fd, cl.args)) |ty| { + return ty; + } + } + } + // Try as a named type + const name_id = self.module.types.internString(callee_name); + return self.module.types.findByName(name_id) orelse .unresolved; +} + +/// Resolve a parameterized type expr, substituting bindings for type/value params. +/// Handles both built-in types (Vector) and user-defined generic structs. +/// `span` locates the reference for the unresolved-base diagnostic. +pub fn resolveParameterizedWithBindings(self: *Lowering, pt: *const ast.ParameterizedTypeExpr, span: ?ast.Span) TypeId { + const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name; + const table = &self.module.types; + // A namespaced base (`ns.Box(..)`) is an explicit qualified reach and is + // exempt from the bare-head visibility gate; only a dotless head is + // policed (E4). + const is_qualified = std.mem.indexOfScalar(u8, pt.name, '.') != null; + + // Vector(N, T) — built-in parameterized type. A backtick raw base + // (`` `Vector(…) ``) is the LITERAL user type named `Vector`, so it + // skips this intrinsic and resolves through the template map (0089). + if (!pt.is_raw and std.mem.eql(u8, base_name, "Vector")) { + if (pt.args.len == 2) { + const length = self.resolveVectorLane(pt.args[0]) orelse return .unresolved; + const elem = self.resolveTypeWithBindings(pt.args[1]); + return table.vectorOf(elem, length); + } + } + + // Generic-struct base: route through the single layout choke-point (CP-1). + // Bare → the single bare-VISIBLE author (own / 1-hop flat), source-keyed; + // qualified `ns.Box(..)` → ns's OWN template (or a missing-member diagnostic); + // never the global last-wins map for a visible-shadowed or qualified head. + { + const alias: ?[]const u8 = if (std.mem.indexOfScalar(u8, pt.name, '.')) |dot| pt.name[0..dot] else null; + switch (self.selectGenericStructHead(base_name, alias, is_qualified, span)) { + .template => |t| return self.instantiateGenericStruct(&t, pt.args), + .poisoned => return .unresolved, + .not_generic => {}, + } + } + + // Parameterized protocol used as a value type (`VL(s64)`): materialize a + // 16-byte protocol value with the type-arg bound (not a 0-field stub). + if (self.program_index.protocol_ast_map.get(base_name)) |pd| { + if (pd.type_params.len > 0) { + if (!is_qualified and self.headTypeLeak(base_name, span)) return .unresolved; + return self.instantiateParamProtocol(pd, pt.args); + } + } + + // User-defined type-returning function used as a TYPE annotation + // (`b : Make(N, s64)` where `Make :: ($K: u32, $T: Type) -> Type`). The + // `.call`-node path (`resolveTypeCallWithBindings`) already routes here; + // a `parameterized_type_expr` must too, or the function name falls through + // to the empty-struct stub below and `b.field` / `b.len` fails. + const resolved_name = if (self.scope) |scope| (scope.lookupFn(base_name) orelse base_name) else base_name; + if (self.program_index.fn_ast_map.get(resolved_name)) |fd| { + if (fd.type_params.len > 0) { + if (!is_qualified and self.headFnLeak(base_name, span)) return .unresolved; + if (self.instantiateTypeFunction(base_name, base_name, fd, pt.args)) |ty| { + return ty; + } + } + } + + // The base names no known type constructor — not Vector, not a generic + // struct template, not a parameterized protocol, not a type-returning + // function. A silent 0-field stub here would mis-size every downstream + // `b.field` / `b.len`; emit the diagnostic and poison with `.unresolved` + // (the `.call`-node sibling `resolveTypeCallWithBindings` already poisons). + if (self.diagnostics) |d| + d.addFmt(.err, span, "unknown type '{s}'", .{base_name}); + return .unresolved; +} + +/// Instantiate a generic struct template with concrete args. +/// E.g., Vec(3, f32) → struct Vec__3_f32 { data: Vector(3, f32) } +/// A generic-struct instance method selected via the STAMPED authoring decl: +/// the `fn_decl` to monomorphize, the instance's stored type bindings, and the +/// instance (mangled / alias) name the monomorphized function is keyed under. +const GenericStructMethod = struct { + fd: *const ast.FnDecl, + bindings: *std.StringHashMap(TypeId), + inst_name: []const u8, +}; + +/// THE single body-axis reader: select `method` of generic-struct instance +/// `inst_name` via the instance's STAMPED author (`struct_instance_author`), +/// so body-author ≡ layout-author by construction — never the global last-wins +/// `fn_ast_map["Template.method"]` a 2-flat-hop same-name template's method +/// could win. Null when `inst_name` is NOT a generic instance (no author stamp) +/// — the caller's existing non-generic `fn_ast_map` path then handles it +/// (non-generic structs, free fns, FFI), or when the confirmed author declares +/// no such `method` (a normal unresolved-method, handled downstream). A +/// confirmed instance whose author is present but whose bindings are missing is +/// a LOUD invariant failure — instantiation writes both together (CP-2). +pub fn genericInstanceMethod(self: *Lowering, inst_name: []const u8, method: []const u8) ?GenericStructMethod { + const author = self.struct_instance_author.get(inst_name) orelse return null; + const bindings = self.struct_instance_bindings.getPtr(inst_name) orelse + std.debug.panic("generic struct instance '{s}' has an author but no bindings", .{inst_name}); + // INLINE struct method (`Box :: struct { make :: ... }`): selected via the + // instance's STAMPED author, so the body is the one authored alongside the + // layout — never the global last-wins `fn_ast_map["Template.method"]` a + // 2-flat-hop same-name template's method could win (finding #1). + if (structMethodFn(author, method)) |fd| + return .{ .fd = fd, .bindings = bindings, .inst_name = inst_name }; + // IMPL-block method (`impl P for Box { ... }`): registered under the + // template name in `fn_ast_map`, not on the struct decl, so it is keyed by + // template name (protocol dispatch). The author confirms this IS a generic + // instance; the method body is the template's registered impl method. + const tmpl_name = self.struct_instance_template.get(inst_name) orelse return null; + const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, method }) catch return null; + if (self.program_index.fn_ast_map.get(tmpl_qualified)) |fd| + return .{ .fd = fd, .bindings = bindings, .inst_name = inst_name }; + return null; +} + +/// Monomorphize (once) the selected generic-instance method under +/// `.` and return its FuncId. The source-pin follows the +/// selected `fd` for free: `monomorphizeFunction` pins to `fd.body.source_file`, +/// which is the template's defining module (the author's own method node). +/// Null when the function fails to resolve post-monomorphization. +pub fn ensureGenericInstanceMethodLowered(self: *Lowering, m: GenericStructMethod) ?FuncId { + const mangled = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ m.inst_name, m.fd.name }) catch return null; + if (!self.lowered_functions.contains(mangled)) { + self.monomorphizeFunction(m.fd, mangled, m.bindings); + } + return self.resolveFuncByName(mangled); +} + +/// Debug invariant (CP coverage lock): the two generic-instance maps written +/// in lockstep at the SAME two writers (instantiation + alias copy) — +/// `struct_instance_template` and `struct_instance_author` — must have +/// coincident keysets. A future writer that registers an instance's layout +/// without stamping its author (a silent body-axis reopen) trips this in a +/// debug `zig build test`, not in production. +pub fn assertInstanceMapsCoincide(self: *Lowering) void { + if (!std.debug.runtime_safety) return; + var it = self.struct_instance_template.keyIterator(); + while (it.next()) |k| { + if (!self.struct_instance_author.contains(k.*)) + std.debug.panic("generic instance '{s}' has a template but no author stamp", .{k.*}); + } + var it2 = self.struct_instance_author.keyIterator(); + while (it2.next()) |k| { + if (!self.struct_instance_template.contains(k.*)) + std.debug.panic("generic instance '{s}' has an author but no template stamp", .{k.*}); + } +} + +pub fn instantiateGenericStruct(self: *Lowering, tmpl: *const StructTemplate, args: []const *const Node) TypeId { + const table = &self.module.types; + + // Build mangled name dynamically: StructName__arg1_arg2 + var name_parts = std.ArrayList(u8).empty; + name_parts.appendSlice(self.alloc, tmpl.name) catch {}; + + // A qualified `ns.Box(..)` head can select a generic template whose bare + // name also belongs to a DIFFERENT module's same-name template (the one + // that won the last-wins `struct_template_map`). Both would mangle to + // `Box__s64` and the second instantiation would alias the first's layout. + // Tag the NON-canonical author's mangled name with its source so each + // author's instantiation is a distinct type. The canonical (bare-map) + // author keeps the untagged name — no churn for single-author generics. + if (self.program_index.struct_template_map.get(tmpl.name)) |canon| { + const canon_src = canon.source_file orelse ""; + const this_src = tmpl.source_file orelse ""; + if (!std.mem.eql(u8, canon_src, this_src)) { + var tag_buf: [24]u8 = undefined; + const tag = std.fmt.bufPrint(&tag_buf, "$m{x}", .{std.hash.Wyhash.hash(0, this_src)}) catch ""; + name_parts.appendSlice(self.alloc, tag) catch {}; + } + } + + // Bind type params to args and build name suffix + const saved_type_bindings = self.type_bindings; + const saved_value_bindings = self.comptime_value_bindings; + const saved_pack_bindings = self.pack_bindings; + const saved_pack_arg_types = self.pack_arg_types; + var tb = std.StringHashMap(TypeId).init(self.alloc); + var cvb = std.StringHashMap(i64).init(self.alloc); + var pb = std.StringHashMap([]const TypeId).init(self.alloc); + + for (tmpl.type_params, 0..) |tp, i| { + if (i >= args.len) break; + + // `..$Ts: []Type` — bind the REMAINING args as a type pack. + if (tp.is_variadic) { + var pack_tys = std.ArrayList(TypeId).empty; + for (args[i..]) |a| { + // A spread arg `..sources.T` expands to the source pack's + // per-element (projected) types; a plain arg is one type. + if (a.data == .spread_expr) { + if (self.packResolver().packTypeElems(a.data.spread_expr.operand)) |elems| { + defer self.alloc.free(elems); + for (elems) |ty| { + pack_tys.append(self.alloc, ty) catch {}; + name_parts.appendSlice(self.alloc, "__") catch {}; + name_parts.appendSlice(self.alloc, self.formatTypeName(ty)) catch {}; + } + continue; + } + } + const ty = self.resolveTypeWithBindings(a); + pack_tys.append(self.alloc, ty) catch {}; + name_parts.appendSlice(self.alloc, "__") catch {}; + name_parts.appendSlice(self.alloc, self.formatTypeName(ty)) catch {}; + } + pb.put(tp.name, pack_tys.toOwnedSlice(self.alloc) catch &.{}) catch {}; + break; // a pack param is always last + } + + name_parts.appendSlice(self.alloc, "__") catch {}; + + if (tp.is_type_param) { + const ty = self.resolveTypeWithBindings(args[i]); + tb.put(tp.name, ty) catch {}; + const tname = self.formatTypeName(ty); + name_parts.appendSlice(self.alloc, tname) catch {}; + } else { + // Value param (e.g., $N: u32) — fold to a compile-time integer + // and range-check against its declared type. + const val = self.resolveValueParamArg(args[i], tp.name, tp.value_type) orelse return .unresolved; + cvb.put(tp.name, val) catch {}; + var val_buf: [32]u8 = undefined; + const val_str = std.fmt.bufPrint(&val_buf, "{d}", .{val}) catch "0"; + name_parts.appendSlice(self.alloc, val_str) catch {}; + } + } + + const mangled_name = name_parts.items; + + // Check if already instantiated + const name_id = table.internString(mangled_name); + if (table.findByName(name_id)) |existing| { + // Already registered — check if it has fields + const info = table.get(existing); + if (info == .@"struct" and info.@"struct".fields.len > 0) { + // A confirmed generic instance must never be returned without an + // author stamp — the body axis (CP-4) keys method selection off + // it. The template/bindings were written at first instantiation; + // re-stamp the author from THIS `tmpl` if the dedup fast-path is + // the first to reach this mangled name (e.g. a layout interned by + // a forward reference before any method dispatch). + if (!self.struct_instance_author.contains(mangled_name)) { + const owned = self.alloc.dupe(u8, mangled_name) catch return existing; + self.struct_instance_author.put(owned, tmpl.decl) catch {}; + } + return existing; + } + } + + // Set up bindings and resolve fields. `pack_bindings` makes a + // pack-shaped field type like `(..$Ts)` resolve to the bound type list. + self.type_bindings = tb; + self.comptime_value_bindings = cvb; + self.pack_bindings = pb; + self.pack_arg_types = pb; + + // Resolve the field type nodes in the TEMPLATE's source context, not the + // (possibly cross-module) instantiation site. A field naming a type + // visible only in the template's module then resolves correctly, and the + // source-aware nominal leaf classifies main vs imported by the TEMPLATE's + // file — so an undeclared field type (`y: Missing`) or a value param used + // as a type (`x: N` for `$N: u32`) is diagnosed at the right authority + // (the leaf for an imported template, the `UnknownTypeChecker` for a + // main-file one) instead of silently fabricating a stub / poisoning with + // `.unresolved` that panics at LLVM emission. + const saved_src = self.current_source_file; + const saved_diag_src = if (self.diagnostics) |d| d.current_source_file else null; + if (tmpl.source_file) |sf| { + self.current_source_file = sf; + if (self.diagnostics) |d| d.current_source_file = sf; + } + + var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; + for (tmpl.field_names, tmpl.field_type_nodes) |fname, ftype_node| { + const field_ty = self.resolveTypeWithBindings(ftype_node); + fields.append(self.alloc, .{ + .name = table.internString(fname), + .ty = field_ty, + }) catch unreachable; + } + + self.current_source_file = saved_src; + if (self.diagnostics) |d| d.current_source_file = saved_diag_src; + + // Restore bindings + self.type_bindings = saved_type_bindings; + self.comptime_value_bindings = saved_value_bindings; + self.pack_bindings = saved_pack_bindings; + self.pack_arg_types = saved_pack_arg_types; + + // Register the monomorphized struct + const info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items } }; + const id = if (table.findByName(name_id)) |existing| existing else table.intern(info); + table.updatePreservingKey(id, info); + + // Bind the template name to this concrete instance so a method's + // `self: *Combined` (the template name) resolves to `*Combined__s64_s64` + // — otherwise `self.field` hits the 0-field generic stub. + tb.put(tmpl.name, id) catch {}; + + // Store the type bindings, template name, and authoring decl for method + // resolution. The author is stamped from the SAME `tmpl` that built the + // layout above, so the body axis (CP-4) selects this instance's methods + // via the layout author — never the global last-wins `fn_ast_map`. + const owned_mangled = self.alloc.dupe(u8, mangled_name) catch return id; + self.struct_instance_bindings.put(owned_mangled, tb) catch {}; + self.struct_instance_template.put(owned_mangled, tmpl.name) catch {}; + self.struct_instance_author.put(owned_mangled, tmpl.decl) catch {}; + + return id; +} + +/// Instantiate a type-returning function: `Foo :: Complex(u32)` where +/// `Complex :: ($T:Type) -> Type { return struct { value: T; count: u32; }; }` +/// Walks the function body to find the returned struct/enum, resolves field types +/// with the provided type bindings, and registers the result. +pub fn instantiateTypeFunction(self: *Lowering, alias_name: []const u8, template_name: []const u8, fd: *const ast.FnDecl, args: []const *const Node) ?TypeId { + const table = &self.module.types; + + // Build type bindings from params + args + const saved_type_bindings = self.type_bindings; + const saved_value_bindings = self.comptime_value_bindings; + var tb = std.StringHashMap(TypeId).init(self.alloc); + var cvb = std.StringHashMap(i64).init(self.alloc); + + // Build mangled name + var name_parts = std.ArrayList(u8).empty; + name_parts.appendSlice(self.alloc, template_name) catch {}; + + for (fd.type_params, 0..) |tp, i| { + if (i >= args.len) break; + name_parts.appendSlice(self.alloc, "__") catch {}; + + // Check if this is a Type param ($T: Type) or a value param ($N: u32) + const is_type_param = if (tp.constraint.data == .type_expr) + std.mem.eql(u8, tp.constraint.data.type_expr.name, "Type") + else + true; // default to type param + + if (is_type_param) { + const ty = self.resolveTypeWithBindings(args[i]); + tb.put(tp.name, ty) catch {}; + const tname = self.formatTypeName(ty); + name_parts.appendSlice(self.alloc, tname) catch {}; + } else { + // Value param (e.g., $N: u32) — fold to a compile-time integer + // and range-check against its declared type. A failed bind has + // already diagnosed itself, so poison to `.unresolved` rather + // than `null`: `null` makes the caller fall through to the + // empty-struct placeholder named after the fn, which then + // cascades a bogus `field not found` on any later access. The + // struct binder (`instantiateGenericStruct`) poisons the same way. + const vp_type: ?[]const u8 = if (tp.constraint.data == .type_expr) tp.constraint.data.type_expr.name else null; + const val = self.resolveValueParamArg(args[i], tp.name, vp_type) orelse return .unresolved; + cvb.put(tp.name, val) catch {}; + var val_buf: [32]u8 = undefined; + const val_str = std.fmt.bufPrint(&val_buf, "{d}", .{val}) catch "0"; + name_parts.appendSlice(self.alloc, val_str) catch {}; + } + } + + const mangled_name = name_parts.items; + + // Check if already instantiated + const mangled_name_id = table.internString(mangled_name); + if (table.findByName(mangled_name_id)) |existing| { + const info = table.get(existing); + if ((info == .@"struct" and info.@"struct".fields.len > 0) or info == .@"union" or info == .tagged_union) { + return existing; + } + } + + // Activate bindings + self.type_bindings = tb; + self.comptime_value_bindings = cvb; + defer { + self.type_bindings = saved_type_bindings; + self.comptime_value_bindings = saved_value_bindings; + } + + // Resolve the type fn's body (inline struct/union fields, or the returned + // type expression) in its OWN module (E4), so a 2-flat-hop library type + // named there is bare-visible — not the cross-module call site. The arg + // exprs above were already resolved in the caller's context. + const saved_tf_src = self.current_source_file; + defer self.setCurrentSourceFile(saved_tf_src); + if (fd.body.source_file) |src| self.setCurrentSourceFile(src); + + // Determine if alias_name is a real alias (e.g., "Foo" for "Complex(u32)") + // or just the template name itself (inline use like "Sx(f32)") + const has_alias = !std.mem.eql(u8, alias_name, template_name); + + // Try struct first + if (findStructInBody(fd.body)) |struct_decl| { + // Resolve struct fields with type bindings active + var struct_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; + for (struct_decl.field_names, struct_decl.field_types) |fname, ftype_node| { + const field_ty = self.resolveTypeWithBindings(ftype_node); + struct_fields.append(self.alloc, .{ + .name = table.internString(fname), + .ty = field_ty, + }) catch {}; + } + + // Always register under mangled name + const mangled_info: types.TypeInfo = .{ .@"struct" = .{ + .name = mangled_name_id, + .fields = struct_fields.items, + } }; + const mangled_id = if (table.findByName(mangled_name_id)) |existing| existing else table.intern(mangled_info); + table.updatePreservingKey(mangled_id, mangled_info); + + // If there's a real alias, also register under alias name and in alias map + if (has_alias) { + const alias_name_id = table.internString(alias_name); + const alias_info: types.TypeInfo = .{ .@"struct" = .{ + .name = alias_name_id, + .fields = struct_fields.items, + } }; + const alias_id = if (table.findByName(alias_name_id)) |existing| existing else table.intern(alias_info); + table.updatePreservingKey(alias_id, alias_info); + + // Store defaults if any + if (struct_decl.field_defaults.len > 0) { + self.struct_defaults_map.put(alias_name, struct_decl.field_defaults) catch {}; + } + + return alias_id; + } + + return mangled_id; + } + + // Try tagged enum/union + if (findUnionInBody(fd.body)) |enum_decl| { + return self.instantiateTypeUnion(if (has_alias) alias_name else mangled_name, mangled_name, &enum_decl); + } + + // General case: the body returns a TYPE EXPRESSION that is not an inline + // struct/union/enum — `return [K]T`, `Vector(K, T)`, `*T`, an alias, etc. + // Resolve it with the value/type bindings active (so `[K]T` folds K to a + // compile-time integer). The result is interned structurally, so + // `Make(N, s64)`, `Make(3, s64)`, and `Make(M + 1, s64)` all yield the + // same TypeId. `.unresolved` means the return wasn't a type expression + // (e.g. a value-returning function in a type position) → fall through to + // the caller's fallback rather than fabricating a type. + if (findReturnTypeExpr(fd.body)) |ret_node| { + const ty = self.resolveTypeWithBindings(ret_node); + if (ty != .unresolved) return ty; + } + + return null; +} + +/// The type expression a type-returning function yields: the value of its +/// `return` (block body) or the bare expression (arrow body / `=> [K]T`). +/// Used for a non-struct/union return shape, which the struct/union body +/// walkers above don't match. +pub fn findReturnTypeExpr(body: *const Node) ?*const Node { + if (body.data == .block) { + for (body.data.block.stmts) |stmt| { + if (stmt.data == .return_stmt) return stmt.data.return_stmt.value; + } + return null; + } + return body; +} + +/// Instantiate a tagged enum from a type function body. +pub fn instantiateTypeUnion(self: *Lowering, alias_name: []const u8, mangled_name: []const u8, ed: *const ast.EnumDecl) ?TypeId { + const table = &self.module.types; + + // Build variant fields (tagged enum variants stored as StructInfo.Field) + var variant_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; + for (ed.variant_names, 0..) |vname, i| { + const payload_ty: TypeId = if (i < ed.variant_types.len and ed.variant_types[i] != null) + self.resolveTypeWithBindings(ed.variant_types[i].?) + else + .void; + variant_fields.append(self.alloc, .{ + .name = table.internString(vname), + .ty = payload_ty, + }) catch {}; + } + + const alias_name_id = table.internString(alias_name); + const info: types.TypeInfo = .{ .tagged_union = .{ + .name = alias_name_id, + .fields = variant_fields.items, + .tag_type = .s64, + } }; + const id = if (table.findByName(alias_name_id)) |existing| existing else table.intern(info); + table.updatePreservingKey(id, info); + + // Also register under mangled name + if (!std.mem.eql(u8, alias_name, mangled_name)) { + const mangled_name_id = table.internString(mangled_name); + const mangled_info: types.TypeInfo = .{ .tagged_union = .{ + .name = mangled_name_id, + .fields = variant_fields.items, + .tag_type = .s64, + } }; + const mid = if (table.findByName(mangled_name_id)) |existing| existing else table.intern(mangled_info); + table.updatePreservingKey(mid, mangled_info); + } + + return id; +} + +/// Walk an AST body to find a struct declaration (from `return struct { ... }` or bare struct expr). +pub fn findStructInBody(body: *const Node) ?ast.StructDecl { + if (body.data == .struct_decl) return body.data.struct_decl; + if (body.data == .block) { + for (body.data.block.stmts) |stmt| { + if (stmt.data == .return_stmt) { + if (stmt.data.return_stmt.value) |val| { + if (val.data == .struct_decl) return val.data.struct_decl; + } + } + if (stmt.data == .struct_decl) return stmt.data.struct_decl; + } + } + return null; +} + +/// Walk an AST body to find a tagged enum declaration. +pub fn findUnionInBody(body: *const Node) ?ast.EnumDecl { + const isTaggedEnum = struct { + fn check(node: *const Node) ?ast.EnumDecl { + if (node.data == .enum_decl and node.data.enum_decl.variant_types.len > 0) { + return node.data.enum_decl; + } + return null; + } + }; + if (isTaggedEnum.check(body)) |ed| return ed; + const stmts = if (body.data == .block) body.data.block.stmts else return null; + for (stmts) |stmt| { + if (stmt.data == .return_stmt) { + if (stmt.data.return_stmt.value) |val| { + if (isTaggedEnum.check(val)) |ed| return ed; + } + } + if (isTaggedEnum.check(stmt)) |ed| return ed; + } + return null; +} + +// ── Type registration ───────────────────────────────────────────