const std = @import("std");
const Allocator = std.mem.Allocator;
const ast = @import("../ast.zig");
const Node = ast.Node;
const sx_types = @import("../types.zig");
const ir_types = @import("types.zig");
const TypeId = ir_types.TypeId;
const TypeInfo = ir_types.TypeInfo;
const TypeTable = ir_types.TypeTable;
const StringId = ir_types.StringId;
const type_resolver = @import("type_resolver.zig");

/// The single-source type-alias table (`ProgramIndex.type_alias_map`), threaded
/// explicitly through every name-resolving entry point so a bare name like
/// `ShaderHandle` (declared `ShaderHandle :: u32`) resolves to its target
/// rather than a fresh empty-struct stub. Replaces the old `TypeTable.aliases`
/// borrow (A2.3): there is no hidden alias state — callers pass the map (or
/// `null` for contexts that never see aliases, e.g. unit tests).
pub const AliasMap = ?*const std.StringHashMap(TypeId);

/// Binding-free element-recursion adapter for `TypeResolver.resolveCompound`:
/// nested element types resolve through `type_bridge.resolveAstType` (the
/// registration-time path — no generic/pack bindings). Lets type_bridge reuse
/// the single canonical structural-shape constructor instead of carrying its
/// own compound algorithm (A2.3b).
const StatelessInner = struct {
    table: *TypeTable,
    alias_map: AliasMap,
    pub fn resolveInner(self: StatelessInner, node: *const Node) TypeId {
        return resolveAstType(node, self.table, self.alias_map);
    }
};

// ── AST Node → TypeId ───────────────────────────────────────────────────
// Resolve an AST type node into an IR TypeId. Used during lowering when
// we only have the parsed AST (no codegen type registry).

pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap) TypeId {
    // A null node means a caller reached type resolution without a type node.
    // Every current caller either passes a non-optional node or handles the
    // "no type" case itself (returning `.void`), so this is a caller bug — and
    // `.s64` here would silently fabricate an 8-byte int. Surface it via the
    // `.unresolved` sentinel (trips the sizeOf/toLLVMType panic at codegen).
    const n = node orelse return .unresolved;
    const si = StatelessInner{ .table = table, .alias_map = alias_map };
    return switch (n.data) {
        .type_expr => |te| resolveTypeName(te.name, table, alias_map),
        .identifier => |id| resolveTypeName(id.name, table, alias_map),
        // Structural shapes (`*T`/`[*]T`/`[]T`/`?T`/`[N]T`, functions, plain
        // closures, plain tuples) are owned by the single canonical
        // `TypeResolver.resolveCompound` — no independent compound algorithm
        // lives here (A2.3b). resolveCompound never returns null for these
        // kinds, so `.?` is total.
        .pointer_type_expr,
        .many_pointer_type_expr,
        .slice_type_expr,
        .optional_type_expr,
        .array_type_expr,
        .function_type_expr,
        => type_resolver.TypeResolver.resolveCompound(table, n, si).?,
        // Plain closures/tuples are owned by resolveCompound (above). It returns
        // null for the PACK-shaped forms — `Closure(..p)` and spread tuples —
        // because expanding a pack needs bindings. type_bridge has none, so it
        // preserves the pack SHAPE statelessly (e.g. `Into(Block)` resolves a
        // `Closure(..p)` field type at registration time). These tiny fallbacks
        // are the only stateless-specific shape code left; the stateful expand
        // lives in PackResolver.
        .closure_type_expr => |ct| type_resolver.TypeResolver.resolveCompound(table, n, si) orelse resolveClosurePackShape(&ct, table, alias_map),
        .tuple_type_expr => |tt| type_resolver.TypeResolver.resolveCompound(table, n, si) orelse resolveTupleSpreadShape(&tt, table, alias_map),
        .pack_index_type_expr => {
            // Pack-index `$args[N]` in a type position must be resolved
            // against an active pack binding — `type_bridge` has no access
            // to that state. The pack-aware caller (lowering's
            // `resolveTypeWithBindings`) handles this case directly *before*
            // delegating here, so reaching this bare path means the binding
            // was missing. `.s64` would silently fabricate an 8-byte int;
            // return `.unresolved` so it surfaces (trips the sizeOf/toLLVMType
            // panic at codegen).
            std.debug.print("type_bridge: pack-index type expression encountered outside a pack-aware context — returning .unresolved\n", .{});
            return .unresolved;
        },
        .tuple_literal => |tl| resolveTupleLiteralAsType(&tl, table, alias_map),
        .parameterized_type_expr => |pt| resolveParameterizedType(&pt, table, alias_map),
        // An unannotated param. Its type must be resolved from context
        // (contextual closure typing, generic binding, or pack substitution)
        // *before* reaching here; if it doesn't, returning a plausible `.s64`
        // silently fabricates an 8-byte int (the classic silent-default trap).
        // Return the dedicated `.unresolved` sentinel — never a legitimate
        // type — so the omission surfaces; the lowering-side `resolveParamType`
        // turns it into a real diagnostic.
        .inferred_type => .unresolved,
        // Inline type declarations (used as field types)
        .enum_decl => |ed| resolveInlineEnum(&ed, table, alias_map),
        .struct_decl => |sd| resolveInlineStruct(&sd, table, alias_map),
        .union_decl => |ud| resolveInlineUnion(&ud, table, alias_map),
        .error_set_decl => |esd| resolveInlineErrorSet(&esd, table),
        .error_type_expr => |ete| resolveErrorType(&ete, table, alias_map),
        else => {
            // A non-type AST node reached type resolution — a caller bug.
            // Returning a plausible `.s64` would silently fabricate an 8-byte
            // int; return the `.unresolved` sentinel so it surfaces (and trips
            // the sizeOf/toLLVMType panic if it ever reaches codegen).
            std.debug.print("type_bridge: unhandled node type {s} in type position — returning .unresolved\n", .{@tagName(n.data)});
            return .unresolved;
        },
    };
}

// ── types.Type → TypeId ─────────────────────────────────────────────────
// Translate an existing codegen Type value into an IR TypeId. Used when
// we have access to the codegen's resolved type info (Phase 3+).

pub fn bridgeType(ty: sx_types.Type, table: *TypeTable, alias_map: AliasMap) TypeId {
    return switch (ty) {
        .signed => |w| switch (w) {
            8 => .s8,
            16 => .s16,
            32 => .s32,
            64 => .s64,
            // Non-standard width: intern the exact width rather than quantising
            // to s64 (which would silently change the type's size).
            else => table.intern(.{ .signed = w }),
        },
        .unsigned => |w| switch (w) {
            8 => .u8,
            16 => .u16,
            32 => .u32,
            64 => .u64,
            else => table.intern(.{ .unsigned = w }),
        },
        .f32 => .f32,
        .f64 => .f64,
        .void_type => .void,
        .boolean => .bool,
        .string_type => .string,
        .any_type => .any,
        .usize_type => .usize,
        .isize_type => .isize,
        .enum_type => |name| resolveNamedType(name, .@"enum", table),
        .struct_type => |name| resolveNamedType(name, .@"struct", table),
        .union_type => |name| resolveNamedType(name, .@"union", table),
        .array_type => |info| blk: {
            const elem = resolveTypeName(info.element_name, table, alias_map);
            break :blk table.arrayOf(elem, info.length);
        },
        .slice_type => |info| blk: {
            const elem = resolveTypeName(info.element_name, table, alias_map);
            break :blk table.sliceOf(elem);
        },
        .pointer_type => |info| blk: {
            const pointee = resolveTypeName(info.pointee_name, table, alias_map);
            break :blk table.ptrTo(pointee);
        },
        .many_pointer_type => |info| blk: {
            const elem = resolveTypeName(info.element_name, table, alias_map);
            break :blk table.manyPtrTo(elem);
        },
        .optional_type => |info| blk: {
            const child = resolveTypeName(info.child_name, table, alias_map);
            break :blk table.optionalOf(child);
        },
        .vector_type => |info| blk: {
            const elem = resolveTypeName(info.element_name, table, alias_map);
            break :blk table.vectorOf(elem, info.length);
        },
        .function_type => |info| blk: {
            const alloc = table.alloc;
            var param_ids = std.ArrayList(TypeId).empty;
            for (info.param_types) |pt| {
                param_ids.append(alloc, bridgeType(pt, table, alias_map)) catch unreachable;
            }
            const ret_id = bridgeType(info.return_type.*, table, alias_map);
            break :blk table.functionType(param_ids.items, ret_id);
        },
        .closure_type => |info| blk: {
            const alloc = table.alloc;
            var param_ids = std.ArrayList(TypeId).empty;
            for (info.param_types) |pt| {
                param_ids.append(alloc, bridgeType(pt, table, alias_map)) catch unreachable;
            }
            const ret_id = bridgeType(info.return_type.*, table, alias_map);
            break :blk table.closureType(param_ids.items, ret_id);
        },
        .tuple_type => |info| blk: {
            const alloc = table.alloc;
            var field_ids = std.ArrayList(TypeId).empty;
            for (info.field_types) |ft| {
                field_ids.append(alloc, bridgeType(ft, table, alias_map)) catch unreachable;
            }
            var name_ids: ?[]const StringId = null;
            if (info.field_names) |names| {
                var ids = std.ArrayList(StringId).empty;
                for (names) |n| {
                    ids.append(alloc, table.internString(n)) catch unreachable;
                }
                name_ids = ids.items;
            }
            break :blk table.intern(.{ .tuple = .{
                .fields = field_ids.items,
                .names = name_ids,
            } });
        },
        .meta_type => .any, // meta types map to Any for now
        .unresolved => .unresolved,
    };
}

// ── Internal helpers ─────────────────────────────────────────────────────

const NamedKind = enum { @"struct", @"enum", @"union" };

fn resolveNamedType(name: []const u8, kind: NamedKind, table: *TypeTable) TypeId {
    // Check if primitive first
    if (resolveTypePrimitive(name)) |id| return id;

    // Register as a named type
    const name_id = table.internString(name);
    return switch (kind) {
        .@"struct" => table.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }),
        .@"enum" => table.intern(.{ .@"enum" = .{ .name = name_id, .variants = &.{} } }),
        .@"union" => table.intern(.{ .@"union" = .{ .name = name_id, .fields = &.{} } }),
    };
}

/// Resolve a bare type name. The algorithm lives in `type_resolver.zig`
/// (`TypeResolver.resolveNamed`, the single source); `type_bridge` forwards the
/// caller-threaded `alias_map` (the single-source `ProgramIndex.type_alias_map`).
fn resolveTypeName(name: []const u8, table: *TypeTable, alias_map: AliasMap) TypeId {
    return type_resolver.TypeResolver.resolveNamed(name, table, alias_map);
}

/// Builtin primitive keyword → TypeId. The keyword table now lives in
/// `type_resolver.zig` (architecture phase A2.1, `TypeResolver.resolvePrimitive`);
/// re-exported here so existing callers are unaffected while `type_bridge` is
/// retired (A2.2). Single source of truth: the table is defined once, there.
pub const resolveTypePrimitive = type_resolver.TypeResolver.resolvePrimitive;

/// Pack-shaped `Closure(..p)` resolved without bindings: the canonical
/// `resolveCompound` builds plain closures and defers pack-shaped ones (returns
/// null). type_bridge can't expand the pack (no state), so it preserves the
/// pack SHAPE — a `closureTypePack` whose prefix is the fixed params. The
/// stateful expand lives in `PackResolver.resolveClosureTypeWithBindings`.
fn resolveClosurePackShape(ct: *const ast.ClosureTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId {
    const alloc = table.alloc;
    var param_ids = std.ArrayList(TypeId).empty;
    for (ct.param_types) |pt| {
        param_ids.append(alloc, resolveAstType(pt, table, alias_map)) catch unreachable;
    }
    const ret_id = if (ct.return_type) |rt| resolveAstType(rt, table, alias_map) else TypeId.void;
    return table.closureTypePack(param_ids.items, ret_id, @intCast(param_ids.items.len));
}

/// Spread tuple `(..xs)` resolved without bindings: `resolveCompound` builds
/// plain tuples and defers spread ones. type_bridge can't expand the pack, so
/// each field resolves individually (a spread field is not a type → resolves to
/// `.unresolved`). The stateful expand lives in
/// `PackResolver.resolveTupleTypeWithBindings`.
fn resolveTupleSpreadShape(tt: *const ast.TupleTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId {
    const alloc = table.alloc;
    var field_ids = std.ArrayList(TypeId).empty;
    for (tt.field_types) |ft| {
        field_ids.append(alloc, resolveAstType(ft, table, alias_map)) catch unreachable;
    }
    var name_ids: ?[]const StringId = null;
    if (tt.field_names) |names| {
        var ids = std.ArrayList(StringId).empty;
        for (names) |n| {
            ids.append(alloc, table.internString(n)) catch unreachable;
        }
        name_ids = ids.items;
    }
    return table.intern(.{ .tuple = .{
        .fields = field_ids.items,
        .names = name_ids,
    } });
}

// Treat a tuple value literal as the corresponding tuple TYPE — valid only when
// every element is itself a type expression. A non-type element (e.g. the `1`
// in `(s32, 1)`) means this literal is NOT a type: refuse to fabricate a tuple
// and return the `.unresolved` sentinel (never `.s64`, which would silently lie
// about the size — issue 0067). type_bridge is stateless and has no diagnostics;
// the user-facing diagnostic is emitted by the stateful caller
// (`Lowering.resolveTupleLiteralTypeArg`), which validates before delegating
// here, so the valid path below builds the tuple and the invalid path never
// reaches it from lowering. The sentinel is the backstop for any other
// (binding-free) caller.
fn resolveTupleLiteralAsType(tl: *const ast.TupleLiteral, table: *TypeTable, alias_map: AliasMap) TypeId {
    const alloc = table.alloc;
    var field_ids = std.ArrayList(TypeId).empty;
    var name_ids_list = std.ArrayList(StringId).empty;
    var any_named = false;
    for (tl.elements) |el| {
        if (!isTypeShapedAstNode(el.value, table)) return .unresolved;
        field_ids.append(alloc, resolveAstType(el.value, table, alias_map)) catch unreachable;
        if (el.name) |n| {
            any_named = true;
            name_ids_list.append(alloc, table.internString(n)) catch unreachable;
        } else {
            name_ids_list.append(alloc, table.internString("")) catch unreachable;
        }
    }
    const names: ?[]const StringId = if (any_named) name_ids_list.items else null;
    return table.intern(.{ .tuple = .{
        .fields = field_ids.items,
        .names = names,
    } });
}

// Returns true when this AST node, on its own, denotes a type rather than a
// value. Used to guard tuple-literal-as-type reinterpretation: a tuple literal
// becomes a tuple type only when every element is a type.
pub fn isTypeShapedAstNode(node: *const Node, table: *TypeTable) bool {
    return switch (node.data) {
        .type_expr,
        .pointer_type_expr,
        .many_pointer_type_expr,
        .array_type_expr,
        .slice_type_expr,
        .optional_type_expr,
        .function_type_expr,
        .closure_type_expr,
        .tuple_type_expr,
        .parameterized_type_expr,
        .pack_index_type_expr,
        .comptime_pack_ref,
        => true,
        .identifier => |id| table.findByName(table.internString(id.name)) != null,
        .tuple_literal => |tl| blk: {
            for (tl.elements) |el| {
                if (!isTypeShapedAstNode(el.value, table)) break :blk false;
            }
            break :blk true;
        },
        else => false,
    };
}

fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId {
    // Strip module prefix (e.g. "std.Vector" → "Vector")
    const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name;
    // Vector(N, T) is a built-in parameterized type
    if (std.mem.eql(u8, base_name, "Vector")) {
        if (pt.args.len == 2) {
            const length: u32 = switch (pt.args[0].data) {
                .int_literal => |lit| @intCast(@as(u64, @bitCast(lit.value))),
                else => 0,
            };
            const elem = resolveAstType(pt.args[1], table, alias_map);
            return table.vectorOf(elem, length);
        }
    }
    // Generic struct instantiation — register as named type
    const name_id = table.internString(pt.name);
    return table.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } });
}

// ── Inline type declarations ─────────────────────────────────────────

fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: AliasMap) TypeId {
    const alloc = table.alloc;
    const name_id = table.internString(ed.name);

    // Check if already registered
    if (table.findByName(name_id)) |existing| return existing;

    // Enum with payloads → tagged union
    const has_payloads = ed.variant_types.len > 0;
    if (has_payloads) {
        var fields = std.ArrayList(TypeInfo.StructInfo.Field).empty;
        for (ed.variant_names, 0..) |vn, i| {
            var field_ty: TypeId = .void;
            if (i < ed.variant_types.len) {
                if (ed.variant_types[i]) |vt| {
                    // For inline structs (__anon), rename to EnumName.variant_name
                    if (vt.data == .struct_decl) {
                        const sd = &vt.data.struct_decl;
                        if (std.mem.eql(u8, sd.name, "__anon")) {
                            const qualified = std.fmt.allocPrint(alloc, "{s}.{s}", .{ ed.name, vn }) catch "__anon";
                            const qname_id = table.internString(qualified);
                            if (table.findByName(qname_id)) |existing| {
                                field_ty = existing;
                            } else {
                                var sfields = std.ArrayList(TypeInfo.StructInfo.Field).empty;
                                for (sd.field_names, sd.field_types) |fname, ftype_node| {
                                    const fty = resolveAstType(ftype_node, table, alias_map);
                                    sfields.append(alloc, .{
                                        .name = table.internString(fname),
                                        .ty = fty,
                                    }) catch unreachable;
                                }
                                const sinfo: TypeInfo = .{ .@"struct" = .{
                                    .name = qname_id,
                                    .fields = sfields.items,
                                } };
                                field_ty = table.intern(sinfo);
                                table.update(field_ty, sinfo);
                            }
                        } else {
                            field_ty = resolveAstType(vt, table, alias_map);
                        }
                    } else {
                        field_ty = resolveAstType(vt, table, alias_map);
                    }
                }
            }
            fields.append(alloc, .{
                .name = table.internString(vn),
                .ty = field_ty,
            }) catch unreachable;
        }
        // Resolve backing type and tag type from enum struct
        // e.g. enum struct { tag: u32; _: u32; payload: [30]u32; } { ... }
        var backing_type: ?TypeId = null;
        var tag_type: ?TypeId = null;
        if (ed.backing_type) |bt| {
            const backing_ty = resolveAstType(bt, table, alias_map);
            backing_type = backing_ty;
            // Extract tag type from first field of backing struct
            const backing_info = table.get(backing_ty);
            if (backing_info == .@"struct") {
                if (backing_info.@"struct".fields.len > 0) {
                    tag_type = backing_info.@"struct".fields[0].ty;
                }
            }
        }

        // Build explicit tag values from variant_values (e.g., quit :: 0x100)
        var explicit_tag_vals: ?[]const i64 = null;
        if (ed.variant_values.len > 0) {
            var vals = std.ArrayList(i64).empty;
            for (0..ed.variant_names.len) |i| {
                if (i < ed.variant_values.len) {
                    if (ed.variant_values[i]) |vv| {
                        if (vv.data == .int_literal) {
                            vals.append(alloc, vv.data.int_literal.value) catch unreachable;
                            continue;
                        }
                    }
                }
                vals.append(alloc, @intCast(i)) catch unreachable;
            }
            explicit_tag_vals = vals.items;
        }

        const info: TypeInfo = .{ .tagged_union = .{
            .name = name_id,
            .fields = fields.items,
            .tag_type = tag_type orelse .s64, // enum unions are always tagged (default i64)
            .backing_type = backing_type,
            .explicit_tag_values = explicit_tag_vals,
        } };
        const id = table.intern(info);
        table.update(id, info);
        return id;
    }

    // Plain enum (no payloads)
    var variants = std.ArrayList(StringId).empty;
    for (ed.variant_names) |vn| {
        variants.append(alloc, table.internString(vn)) catch unreachable;
    }
    // Build explicit values for flags (power-of-2) or custom values
    var explicit_vals: ?[]const i64 = null;
    if (ed.is_flags) {
        var vals = std.ArrayList(i64).empty;
        for (ed.variant_names, 0..) |_, i| {
            if (i < ed.variant_values.len) {
                if (ed.variant_values[i]) |vv| {
                    if (vv.data == .int_literal) {
                        vals.append(alloc, vv.data.int_literal.value) catch unreachable;
                        continue;
                    }
                }
            }
            // Auto power-of-2: 1, 2, 4, 8, ...
            vals.append(alloc, @as(i64, 1) << @intCast(i)) catch unreachable;
        }
        explicit_vals = vals.items;
    } else if (ed.variant_values.len > 0) {
        var vals = std.ArrayList(i64).empty;
        for (0..ed.variant_names.len) |i| {
            if (i < ed.variant_values.len) {
                if (ed.variant_values[i]) |vv| {
                    if (vv.data == .int_literal) {
                        vals.append(alloc, vv.data.int_literal.value) catch unreachable;
                        continue;
                    }
                }
            }
            vals.append(alloc, @intCast(i)) catch unreachable;
        }
        explicit_vals = vals.items;
    }
    // Resolve backing type for sized enums (e.g. enum u32 { ... })
    var enum_backing: ?TypeId = null;
    if (ed.backing_type) |bt| {
        // Only use simple backing types (u8, u16, u32, etc.), not struct backing (enum struct)
        if (bt.data != .struct_decl) {
            enum_backing = resolveAstType(bt, table, alias_map);
        }
    }

    const info: TypeInfo = .{ .@"enum" = .{
        .name = name_id,
        .variants = variants.items,
        .is_flags = ed.is_flags,
        .explicit_values = explicit_vals,
        .backing_type = enum_backing,
    } };
    const id = table.intern(info);
    table.update(id, info);
    return id;
}

fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable, alias_map: AliasMap) TypeId {
    const alloc = table.alloc;
    const name_id = table.internString(sd.name);

    if (table.findByName(name_id)) |existing| return existing;

    var fields = std.ArrayList(TypeInfo.StructInfo.Field).empty;
    for (sd.field_names, sd.field_types) |fname, ftype_node| {
        const field_ty = resolveAstType(ftype_node, table, alias_map);
        fields.append(alloc, .{
            .name = table.internString(fname),
            .ty = field_ty,
        }) catch unreachable;
    }
    const info: TypeInfo = .{ .@"struct" = .{
        .name = name_id,
        .fields = fields.items,
    } };
    const id = table.intern(info);
    table.update(id, info);
    return id;
}

fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable, alias_map: AliasMap) TypeId {
    const alloc = table.alloc;
    const name_id = table.internString(ud.name);

    if (table.findByName(name_id)) |existing| return existing;

    var fields = std.ArrayList(TypeInfo.StructInfo.Field).empty;
    for (ud.field_names, ud.field_types) |fname, ftype_node| {
        const field_ty = resolveAstType(ftype_node, table, alias_map);
        fields.append(alloc, .{
            .name = table.internString(fname),
            .ty = field_ty,
        }) catch unreachable;
    }
    const info: TypeInfo = .{ .@"union" = .{
        .name = name_id,
        .fields = fields.items,
    } };
    const id = table.intern(info);
    table.update(id, info);
    return id;
}

/// `Foo :: error { A, B }` → a registered `.error_set` type. Tag names are
/// interned into the global tag pool; the set stores their (sorted) ids. The
/// caller (lowering) is responsible for rejecting an empty set, so this only
/// sees non-empty declarations.
fn resolveInlineErrorSet(esd: *const ast.ErrorSetDecl, table: *TypeTable) TypeId {
    const alloc = table.alloc;
    const name_id = table.internString(esd.name);

    if (table.findByName(name_id)) |existing| return existing;

    var tag_ids = std.ArrayList(u32).empty;
    defer tag_ids.deinit(alloc);
    for (esd.tag_names) |tn| {
        tag_ids.append(alloc, table.internTag(tn)) catch unreachable;
    }
    return table.errorSetType(name_id, tag_ids.items);
}

/// The error channel of a failable signature: `!Named` → the declared error
/// set (registered by `resolveInlineErrorSet`); bare `!` → a shared inferred
/// placeholder set. The placeholder's members are refined per failable
/// function by the whole-program SCC pass (E1.4); for now every bare `!`
/// resolves to the same empty inferred set, which is correct while no
/// function raises (E1.3+).
fn resolveErrorType(ete: *const ast.ErrorTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId {
    if (ete.name) |name| return resolveTypeName(name, table, alias_map);
    // `!` is not a legal type/identifier name, so this reserved StringId can
    // never collide with a user-declared set.
    const name_id = table.internString("!");
    if (table.findByName(name_id)) |existing| return existing;
    return table.errorSetType(name_id, &.{});
}
