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.
1806 lines
85 KiB
Zig
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;
|
|
}
|