Files
sx/src/ir/lower/generic.zig
agra 3c94c14b5e fix(ffi-linkage): Phase 5.0 prereq — exclude extern imports from plain-free-fn classification
isPlainFreeFn / isPlainFreeFnDecl excluded a #foreign body but classified
an empty-block extern fn as a plain free function, so existing extern fns
were wrongly counted in the bare-call ambiguity verdict (and eligible for
the out-of-line-slot / shadow-author pass). Both predicates now also
exclude extern_export == .extern_ (an external C symbol with no
sx-lowerable body, name-keyed first-wins dispatch like #foreign); export
keeps a real body and stays plain-free. Greens example 1230 — same-name
extern authors compile like their #foreign twins (0729).

646 corpus / 444 unit, 0 failed.
2026-06-15 03:46:34 +03:00

1806 lines
85 KiB
Zig

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 type_bridge = @import("../type_bridge.zig");
const program_index_mod = @import("../program_index.zig");
const StructTemplate = program_index_mod.StructTemplate;
const GenericResolver = @import("../generics.zig").GenericResolver;
const TypeId = types.TypeId;
const Ref = inst_mod.Ref;
const FuncId = inst_mod.FuncId;
const Function = inst_mod.Function;
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 `<pack_name>.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 namespaced-fn-body 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;
}
// ── Type-arg resolution & matching ─────────────────────────────
/// 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 `.i64` 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[<lit>]`)
/// - 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. `i64`) is always static.
// A user-defined name that happens to be in scope as
// a runtime variable (`x: Type = i64; 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 (`i64`, `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 (`(i32, i32)` 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
/// `(i32, 1)` — is a user error. Emit a diagnostic pointing at the offending
/// element and return `.unresolved`; never fabricate a tuple with a bogus
/// field. 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. `(i32, i32)`", .{@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, i64)`)
// 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], i64)`). 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 .i64 (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 `$<name>` 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) == i64` fold the same as
// `inferExprType(a) == i64`.
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, i64)`) 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", "[]i32", "[3]f64").
pub fn formatTypeName(self: *Lowering, ty: TypeId) []const u8 {
// Builtin types: use their canonical name
if (ty == .i8) return "i8";
if (ty == .i16) return "i16";
if (ty == .i32) return "i32";
if (ty == .i64) return "i64";
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, "i{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 "() -> i32" or "(i32, i32) -> i32".
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", []i32 → "slice_i32", [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 []i64 → T = i64.
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. An array
// arg coerces to a slice at a `[]T` param (the same promotion
// concrete slice params perform), so it binds from its
// element type too (issue 0126).
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),
.array => |a| self.extractTypeParam(st.element_type, a.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.i8.index()) catch {};
tags.append(self.alloc, TypeId.i16.index()) catch {};
tags.append(self.alloc, TypeId.i32.index()) catch {};
tags.append(self.alloc, TypeId.i64.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` / `extern`). Only these get
/// an out-of-line identity-addressable slot — the bare-call disambiguation
/// 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;
// An `extern` import is an external C symbol with no sx-lowerable body —
// name-keyed first-wins dispatch like a `#foreign` body, never a plain free
// fn. `export` DEFINES a real body, so it stays plain-free.
if (fd.extern_export == .extern_) 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, i64)`), 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 :: i8`) to its underlying builtin so the range
// gate below treats it exactly like `$K: u32` / `$K: i8` (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 :: i8` → "i8") 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(i64)`,
/// `VL(i64)`) — 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;
}
// Const-alias head (`BoxAlias :: Box;` / `Box :: r.Box;`, issue 0120):
// follow the alias decl hop-by-hop to its authoring template, each hop
// resolved from that alias author's own source. Checked BEFORE the map:
// the alias may share its name with a same-name template that is NOT
// visible from here (a facade's `Box :: r.Box;` re-export of rich's
// `Box`), and the map branch would poison on that invisible author.
// Only fires when the single visible author (own-wins / single-flat)
// IS an alias-shaped const decl, so real template heads are untouched.
if (self.current_source_file) |from| {
if (self.aliasedStructTemplate(name, from)) |t| return .{ .template = t };
}
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 :: () -> i32`, 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);
if (self.module.types.findByName(name_id)) |t| return t;
// The callee names no known type constructor — not Vector, not a generic
// struct template (or alias), not a type-returning function, not a named
// type. A silent `.unresolved` here reaches LLVM emission as a panic;
// diagnose and poison (the parameterized sibling below already does).
if (self.diagnostics) |d|
d.addFmt(.err, cl.callee.span, "unknown type '{s}'", .{callee_name});
return .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(i64)`): 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, i64)` 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
/// `<inst_name>.<method>` 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__i64` 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__i64_i64`
// — 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, i64)`, `Make(3, i64)`, and `Make(M + 1, i64)` 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 = .i64,
} };
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 = .i64,
} };
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;
}