ajhahn.de
← Flash
Flash 2362 lines
// Flash eval — the compile-time evaluator: the value/type pool (the Pool)
// and the walking pass that folds constants over it (the Evaluator).
//
// The Pool is the substrate: one flat interned store holding types *and*
// values, addressed by `Vid`. A type is a value whose type is `type`, so a
// folded constant, a builtin type name, and a generic's type argument all live
// in the same space and compare by integer equality — two equal compile-time
// entities always intern to the same Vid, so equality never walks a structure
// twice.
//
// The Evaluator walks the program after the binding checks and evaluates
// every `const` initializer it can. Evaluation is three-valued: a *known*
// value (interned), a *definite error* (a collected diagnostic — a division
// or remainder by a known zero, or a malformed generic application, which the
// downstream compiler would also reject), or *`unknown`* — the deliberate
// silent state for everything outside the evaluator's current reach (floats,
// `#`-builtins, comptime mutation, field access, …), which stays checked
// downstream exactly as before. The standing contract: the evaluator never
// diagnoses anything the downstream compiler would accept — when in doubt it
// degrades to `unknown` (an `i128`-overflowing fold degrades rather than
// misreport, because compile-time integers downstream are
// arbitrary-precision) — and it never changes what is emitted (lowering does
// not consult this module).
//
// Generic applications are checked natively. A top-level `fn` with a
// `comptime` parameter or a `type` return is a *generic declaration*; every
// application of one — `Name(args…)` in type position (a parameter, return,
// binding, or field annotation) or as a value-position call — is checked for
// argument arity and, where the parameter's declared type resolves at file
// scope, for argument kind: a `type`-typed parameter wants a type argument,
// a concretely typed one wants a value. A parameter whose type cannot be
// resolved (it names another parameter, an import, or a cycle) is unchecked,
// and an argument evaluating to `unknown` is never wrong — the three-valued
// contract extends to applications. A well-formed application over fully
// known arguments is then *instantiated*: the `(owner, arguments)` pair is
// interned as its own pool key — interning is the instantiate-once
// memoization — and when the generic's body is a single `return` of a
// container definition or a type expression, the instance's result type is
// evaluated with the parameters bound to the argument values. A returned
// container definition is nominal *per instance* (the instance key is the
// type, so `List(u8)` and `List(u16)` are distinct types and `List(u8)`
// twice is one); any other body shape — or a result that is not a type —
// leaves the instance `unknown`, still arity/kind-checked. A depth cap and
// an instantiation budget guard runaway recursion with targeted diagnostics.
//
// Identity rules:
//   * well-known entries (the builtin simple types, `true`/`false`, the void
//     value, `unknown`) occupy fixed leading indices — they exist in every
//     pool, before any dynamic entry, so their Vids are named constants;
//   * composite types (optional, the pointer/slice/array families, error
//     union, function type, tuple) are *structural*: keyed on their child
//     Vids, so `?u8` interns once no matter how often it is spelled;
//   * declared `struct`/`enum`/`union` types are *nominal*: identity is the
//     defining AST node, unique per declaration. This leans on the AST being
//     arena-stable for the whole compile — node addresses never move — which
//     holds because the pool only reads the AST, never rewrites it;
//   * generic instances are keyed on `(owner declaration, argument Vids)`,
//     so the same application memoizes and distinct arguments split identity.
//
// Values carry small payloads: `int` is an `i128` (a fold that needs more
// degrades to `unknown` rather than misfold), `string` is a slice of the
// original source buffer (the ast.flash zero-copy invariant extends here),
// and `unknown` is the deliberate third state — "not evaluated at compile
// time" — that downstream checks treat as silence, never as an error.
//
// Dedup is a linear scan over the flat store (getOrPut by walking existing
// entries). Deliberate: the pool needs no hash map, the store is append-only,
// and Flash-program scale keeps the scan cheap; a faster index can replace the
// scan later without changing a caller.
//
// The pool is checking machinery only: lowering never consults it, so the
// emitted Zig is byte-for-byte independent of anything interned here.

use "ast"
use "parser"
use "support" as sup

const Oom = error{OutOfMemory}

// A pool index — the identity of one interned type or value. The named
// constants below are the well-known entries every pool is seeded with, in
// order from index 0; dynamic entries follow. Two Vids are the same
// type/value exactly when they are the same integer.
pub const Vid = u32

// the builtin simple types
pub const type_bool Vid = 0
pub const type_void Vid = 1
pub const type_type Vid = 2
pub const type_noreturn Vid = 3
pub const type_anyopaque Vid = 4
pub const type_comptime_int Vid = 5
pub const type_comptime_float Vid = 6
pub const type_usize Vid = 7
pub const type_isize Vid = 8
pub const type_u8 Vid = 9
pub const type_u16 Vid = 10
pub const type_u32 Vid = 11
pub const type_u64 Vid = 12
pub const type_u128 Vid = 13
pub const type_i8 Vid = 14
pub const type_i16 Vid = 15
pub const type_i32 Vid = 16
pub const type_i64 Vid = 17
pub const type_i128 Vid = 18
pub const type_f16 Vid = 19
pub const type_f32 Vid = 20
pub const type_f64 Vid = 21
pub const type_f80 Vid = 22
pub const type_f128 Vid = 23

// the well-known values
pub const true_value Vid = 24
pub const false_value Vid = 25
pub const void_value Vid = 26
pub const unknown Vid = 27

// The number of well-known entries — the index where dynamic interning starts.
pub const static_count usize = 28

// A simple (word-named, non-composite) builtin type with no parameters of its
// own. The sized integer types are not here — they are `int_type` keys, so an
// arbitrary width (`u7`) interns through the same shape as a well-known one.
pub const Simple = enum {
    bool,
    void,
    type,
    noreturn_type,
    anyopaque_type,
    comptime_int,
    comptime_float,
    usize,
    isize,
    f16,
    f32,
    f64,
    f80,
    f128,
}

// The signedness of a sized integer type — the `u`/`i` split of `uN`/`iN`.
pub const Signedness = enum {
    signed,
    unsigned,
}

// A sized integer type `uN` / `iN`. The common widths are pre-seeded as
// well-known entries; any other width interns dynamically with the same key.
pub const IntType = struct {
    signedness Signedness,
    bits u16,
}

// How many items a pointer addresses — the `*T` / `[*]T` / `[]T` split.
pub const PtrSize = enum {
    one,
    many,
    slice,
}

// A pointer-family type: single-item pointers, many-item pointers, and slices
// share one key, distinguished by `size`. `sentinel`, when set, is the Vid of
// the terminator *value* (`[:0]u8`'s zero); `is_mut`/`is_volatile` mirror the
// surface qualifiers (const pointee is the default, so it is the false state).
// `alignment`, when set, is the Vid of the `align(N)` *value* — part of the
// type's identity in Zig (`[]align(16) u8` != `[]u8`), so it joins the key.
pub const Ptr = struct {
    size PtrSize,
    is_mut bool = false,
    is_volatile bool = false,
    sentinel ?Vid = null,
    alignment ?Vid = null,
    elem Vid,
}

// A fixed-length array type `[N]T` / `[N:s]T`. `len` is the Vid of the length
// *value* (an interned int), keeping the key structural on children like every
// other composite. An inferred-length `[_]T` never reaches the pool — its
// concrete type is not known until the initializer is, and an unevaluated one
// stays `unknown`.
pub const ArrayType = struct {
    len Vid,
    sentinel ?Vid = null,
    elem Vid,
}

// An error-union type `E!T` / `!T`. `set` is the error-set type's Vid, or null
// for the inferred-set form (whose set only the declaring function knows).
pub const ErrorUnion = struct {
    set ?Vid,
    payload Vid,
}

// A function type `fn(P, …) R`. Parameter types in source order; a missing
// return type is normalized to `void` by the caller, never encoded as absence
// here, so spelling variants of the same type cannot make distinct keys.
// `conv` is the calling convention's variant name when the type spells one
// (`fn(…) callconv(.c) R` records "c"); null when unmarked. The convention is
// part of the type's identity, exactly as it is downstream — `fn() callconv(.c)
// void` and `fn() void` are distinct types.
pub const FnType = struct {
    params []Vid,
    conv ?[]u8,
    ret Vid,
}

// A declared container type — the nominal identity. The payload is the
// address of the defining AST node: each `struct { … }` / `enum { … }` /
// `union { … }` / `error { … }` expression is one node, so two textually
// identical declarations are two distinct types, exactly as declared
// types behave (an error set's node is its whole Expr — the variant
// carries only the member names, which have no address of their own).
pub const Container = union(enum) {
    struct_type *ast.StructDef,
    enum_type *ast.EnumDef,
    union_type *ast.UnionDef,
    error_set_type *ast.Expr,
}

// A generic instance — a recognized generic applied to fully known
// compile-time arguments. Identity is the owning declaration plus the
// argument Vids, so interning an application *is* the instantiate-once
// memoization: the same generic over the same arguments is the same Vid.
// For a generic whose body returns a container definition, this Vid is
// itself the instance's nominal type — distinct arguments make distinct
// types, exactly as instantiated generics behave downstream.
pub const Instance = struct {
    owner *ast.FnDecl,
    args []Vid,
}

// The full shape of one interned entry — what a Vid resolves back to. Types
// first, then values; `unknown` is both the not-a-compile-time-constant value
// and the not-resolved type, one deliberate third state for both spaces.
pub const Key = union(enum) {
    // types
    simple Simple,
    int_type IntType,
    optional Vid, // ?T — the child type's Vid
    ptr Ptr,
    array ArrayType,
    error_union ErrorUnion,
    fn_type FnType,
    tuple []Vid, // (A, B) — element types in order
    container Container,
    instance Instance, // a generic application — `List(u8)` — as a type of its own
    // values
    int i128,
    bool_value bool,
    string []u8, // a slice of the original source buffer
    void_value,
    unknown,
}

// The interned store. Append-only: a Vid handed out stays valid for the
// pool's lifetime, and `get` is a plain index.
pub const Pool = struct {
    arena sup.Allocator,
    items sup.List(Key),

    // A fresh pool, seeded with the well-known entries — appended in the
    // exact order of the named Vid constants, so constant N lands at index N
    // by construction and `intern` finds them like any other entry.
    pub fn init(arena sup.Allocator) Oom!Pool {
        var pool = Pool{ .arena = arena, .items = .empty }
        try pool.items.append(arena, .{ .simple = .bool })
        try pool.items.append(arena, .{ .simple = .void })
        try pool.items.append(arena, .{ .simple = .type })
        try pool.items.append(arena, .{ .simple = .noreturn_type })
        try pool.items.append(arena, .{ .simple = .anyopaque_type })
        try pool.items.append(arena, .{ .simple = .comptime_int })
        try pool.items.append(arena, .{ .simple = .comptime_float })
        try pool.items.append(arena, .{ .simple = .usize })
        try pool.items.append(arena, .{ .simple = .isize })
        try pool.items.append(arena, .{ .int_type = .{ .signedness = .unsigned, .bits = 8 } })
        try pool.items.append(arena, .{ .int_type = .{ .signedness = .unsigned, .bits = 16 } })
        try pool.items.append(arena, .{ .int_type = .{ .signedness = .unsigned, .bits = 32 } })
        try pool.items.append(arena, .{ .int_type = .{ .signedness = .unsigned, .bits = 64 } })
        try pool.items.append(arena, .{ .int_type = .{ .signedness = .unsigned, .bits = 128 } })
        try pool.items.append(arena, .{ .int_type = .{ .signedness = .signed, .bits = 8 } })
        try pool.items.append(arena, .{ .int_type = .{ .signedness = .signed, .bits = 16 } })
        try pool.items.append(arena, .{ .int_type = .{ .signedness = .signed, .bits = 32 } })
        try pool.items.append(arena, .{ .int_type = .{ .signedness = .signed, .bits = 64 } })
        try pool.items.append(arena, .{ .int_type = .{ .signedness = .signed, .bits = 128 } })
        try pool.items.append(arena, .{ .simple = .f16 })
        try pool.items.append(arena, .{ .simple = .f32 })
        try pool.items.append(arena, .{ .simple = .f64 })
        try pool.items.append(arena, .{ .simple = .f80 })
        try pool.items.append(arena, .{ .simple = .f128 })
        try pool.items.append(arena, .{ .bool_value = true })
        try pool.items.append(arena, .{ .bool_value = false })
        try pool.items.append(arena, .void_value)
        try pool.items.append(arena, .unknown)
        return pool
    }

    // The getOrPut: return the Vid of an entry equal to `key`, interning it
    // first when absent. A linear front-to-back scan over the flat store — no
    // hash map (see the header). Slice-carrying keys are duped into the pool's
    // arena on insert, so a caller may pass a temporary.
    pub fn intern(self *mut Pool, key Key) Oom!Vid {
        var i usize = 0
        while i < self.items.items.len {
            if keyEql(self.items.items[i], key) {
                return #as(Vid, #intCast(i))
            }
            i += 1
        }
        const owned Key = switch key {
            .fn_type => |f| .{ .fn_type = .{ .params = try self.arena.dupe(Vid, f.params), .conv = f.conv, .ret = f.ret } },
            .tuple => |elems| .{ .tuple = try self.arena.dupe(Vid, elems) },
            .instance => |inst| .{ .instance = .{ .owner = inst.owner, .args = try self.arena.dupe(Vid, inst.args) } },
            else => key,
        }
        try self.items.append(self.arena, owned)
        return #as(Vid, #intCast(self.items.items.len - 1))
    }

    // Resolve a Vid back to its key. Every Vid this pool handed out is a
    // valid index; anything else is a caller bug.
    pub fn get(self *Pool, vid Vid) Key {
        return self.items.items[vid]
    }

    // The number of interned entries (the well-known seed included).
    pub fn count(self *Pool) usize {
        return self.items.items.len
    }
}

// Structural key equality — the dedup predicate `intern` scans with. Strings
// compare by content (two spellings of the same literal are one value);
// containers compare by node address (the nominal rule); slice-carrying keys
// compare element-wise.
fn keyEql(a Key, b Key) bool {
    return switch a {
        .simple => |s| switch b {
            .simple => |o| s == o,
            else => false,
        },
        .int_type => |t| switch b {
            .int_type => |o| t.signedness == o.signedness && t.bits == o.bits,
            else => false,
        },
        .optional => |child| switch b {
            .optional => |o| child == o,
            else => false,
        },
        .ptr => |p| switch b {
            .ptr => |o| p.size == o.size && p.is_mut == o.is_mut && p.is_volatile == o.is_volatile && optVidEql(p.sentinel, o.sentinel) && optVidEql(p.alignment, o.alignment) && p.elem == o.elem,
            else => false,
        },
        .array => |arr| switch b {
            .array => |o| arr.len == o.len && optVidEql(arr.sentinel, o.sentinel) && arr.elem == o.elem,
            else => false,
        },
        .error_union => |eu| switch b {
            .error_union => |o| optVidEql(eu.set, o.set) && eu.payload == o.payload,
            else => false,
        },
        .fn_type => |f| switch b {
            .fn_type => |o| f.ret == o.ret && optStrEql(f.conv, o.conv) && sup.eql(Vid, f.params, o.params),
            else => false,
        },
        .tuple => |elems| switch b {
            .tuple => |o| sup.eql(Vid, elems, o),
            else => false,
        },
        .container => |c| switch b {
            .container => |o| containerEql(c, o),
            else => false,
        },
        .instance => |inst| switch b {
            .instance => |o| inst.owner == o.owner && sup.eql(Vid, inst.args, o.args),
            else => false,
        },
        .int => |v| switch b {
            .int => |o| v == o,
            else => false,
        },
        .bool_value => |v| switch b {
            .bool_value => |o| v == o,
            else => false,
        },
        .string => |s| switch b {
            .string => |o| sup.eql(u8, s, o),
            else => false,
        },
        .void_value => switch b {
            .void_value => true,
            else => false,
        },
        .unknown => switch b {
            .unknown => true,
            else => false,
        },
    }
}

// Optional-string equality: both null, or both set and byte-equal.
fn optStrEql(a ?[]u8, b ?[]u8) bool {
    if a |x| {
        if b |y| {
            return sup.eql(u8, x, y)
        }
        return false
    }
    return b == null
}

// Optional-Vid equality: both null, or both set and equal.
fn optVidEql(a ?Vid, b ?Vid) bool {
    if a |x| {
        if b |y| {
            return x == y
        }
        return false
    }
    return b == null
}

// Container identity: the same variant kind on the same defining node.
fn containerEql(a Container, b Container) bool {
    return switch a {
        .struct_type => |p| switch b {
            .struct_type => |o| p == o,
            else => false,
        },
        .enum_type => |p| switch b {
            .enum_type => |o| p == o,
            else => false,
        },
        .union_type => |p| switch b {
            .union_type => |o| p == o,
            else => false,
        },
        .error_set_type => |p| switch b {
            .error_set_type => |o| p == o,
            else => false,
        },
    }
}

// The Simple tag a word-named builtin type resolves to, or null when the
// word names no simple builtin.
fn simpleFromName(name []u8) ?Simple {
    if sup.eql(u8, name, "bool") {
        return .bool
    }
    if sup.eql(u8, name, "void") {
        return .void
    }
    if sup.eql(u8, name, "type") {
        return .type
    }
    if sup.eql(u8, name, "noreturn") {
        return .noreturn_type
    }
    if sup.eql(u8, name, "anyopaque") {
        return .anyopaque_type
    }
    if sup.eql(u8, name, "comptime_int") {
        return .comptime_int
    }
    if sup.eql(u8, name, "comptime_float") {
        return .comptime_float
    }
    if sup.eql(u8, name, "usize") {
        return .usize
    }
    if sup.eql(u8, name, "isize") {
        return .isize
    }
    if sup.eql(u8, name, "f16") {
        return .f16
    }
    if sup.eql(u8, name, "f32") {
        return .f32
    }
    if sup.eql(u8, name, "f64") {
        return .f64
    }
    if sup.eql(u8, name, "f80") {
        return .f80
    }
    if sup.eql(u8, name, "f128") {
        return .f128
    }
    return null
}

// The Vid of a builtin type *name* (`u8`, `bool`, `comptime_int`, `f32`,
// `u7`, …), or null when the name is no builtin. The word-named builtins
// resolve to their well-known entries; a sized integer name parses its width,
// so an uncommon `u7` interns dynamically through the same key the seeded
// widths use.
pub fn builtinType(pool *mut Pool, name []u8) Oom!?Vid {
    if simpleFromName(name) |s| {
        return try pool.intern(.{ .simple = s })
    }
    if name.len >= 2 && (name[0] == 'u' || name[0] == 'i') {
        bits := sup.parseInt(u16, name[1..], 10) catch return null
        return try pool.intern(.{ .int_type = .{ .signedness = if (name[0] == 'u') .unsigned else .signed, .bits = bits } })
    }
    return null
}

// A collected evaluator diagnostic — the same shape sema collects (anchor =
// a slice into the source buffer; see sema.flash), kept as this module's own
// type so the evaluator depends on the AST alone. The caller copies entries
// into its own list.
pub const Diag = struct {
    anchor []u8,
    msg []u8,
    note_anchor ?[]u8 = null,
    note_msg ?[]u8 = null,
}

// One name whose compile-time value is known (or known to be `unknown`).
const EnvEntry = struct {
    name []u8,
    value Vid,
}

// One evaluated generic instance: the interned instance key and the result
// type its body evaluated to (`unknown` where out of reach). Kept beside the
// pool rather than in it — a result is an attribute of an instance, not part
// of its identity.
const InstanceResult = struct {
    instance Vid,
    result Vid,
}

// The instantiation nesting bound — past it a recursive generic is reported
// and degrades to `unknown` (the downstream compiler has its own quota).
const max_inst_depth = 64

// What one generic parameter accepts, resolved from its declared type at
// file scope. `wants_type` is a parameter whose type is `type` itself
// (directly or through an alias); `wants_value` one whose type resolved to
// any other concrete type; `unchecked` everything unresolvable — a type
// naming another parameter, an import, a resolution cycle — which stays
// silent per the three-valued contract.
const ParamKind = enum {
    wants_type,
    wants_value,
    unchecked,
}

// One recognized generic declaration: a top-level `fn` with a `comptime`
// parameter or a `type` return. Parameter kinds are resolved lazily (a
// parameter's type may alias a constant declared further down the file) and
// memoized once the file scope is complete; `resolving` guards against a
// parameter-type cycle re-entering its own resolution.
const Generic = struct {
    name []u8,
    decl *ast.FnDecl,
    kinds ?[]ParamKind = null,
    resolving bool = false,
}

// The walking pass. It mirrors sema's scope discipline — one flat stack of
// frames, file level at the bottom — but records *values*, not bindings:
// only an immutable binding lands in the environment (a `var` is evaluated
// for definite errors, never recorded — mutation tracking is out of reach).
// Lookups that miss resolve to `unknown`, never to a diagnostic; the binding
// errors are sema's, not the evaluator's.
pub const Evaluator = struct {
    arena sup.Allocator,
    pool *mut Pool,
    diags sup.List(Diag),
    env sup.List(EnvEntry),
    frame_base usize = 0,
    // The recognized generic declarations, collected before any evaluation so
    // a constant's initializer can apply a generic declared later in the file.
    generics sup.List(Generic),
    // How many leading env entries are file-scope constants so far — the only
    // names parameter-kind resolution may see (a body's locals are the
    // caller's scope, never the declaration's).
    file_limit usize = 0,
    // Set once the file-scope pass is complete: parameter kinds resolved from
    // here on are final and may be memoized.
    kinds_final bool = false,
    // When set, `lookup` sees only the first `lookup_limit` env entries —
    // parameter-kind resolution restricting itself to the file scope.
    lookup_limit ?usize = null,
    // When set, `report` drops diagnostics — parameter-kind resolution walks
    // type expressions whose definite errors the regular walk already owns.
    quiet bool = false,
    // The memoized instance results, one entry per interned instance key
    // (linear scan, like every other lookup here).
    instance_results sup.List(InstanceResult),
    // The active instantiation nesting depth, bounded by `max_inst_depth`.
    inst_depth usize = 0,
    // How many instances this run has evaluated (memoized hits are free) and
    // the cap — a field so a test can lower it. The branch-quota analog.
    inst_count usize = 0,
    inst_budget usize = 1000,
    budget_reported bool = false,

    pub fn init(arena sup.Allocator, pool *mut Pool) Evaluator {
        return .{ .arena = arena, .pool = pool, .diags = .empty, .env = .empty, .generics = .empty, .instance_results = .empty }
    }

    // Evaluate a whole program. Generic declarations are collected first, so
    // every later application resolves its owner. Top-level constants are
    // then evaluated and recorded in file order — a reference to a constant
    // declared *later* in the file is not yet evaluated and degrades to
    // `unknown` (silent; the downstream compiler evaluates lazily and accepts
    // it) — and finally every function, test, and comptime-block body is
    // walked for the local constants and definite errors inside it.
    pub fn run(self *mut Evaluator, program ast.Program) Oom!void {
        var i usize = 0
        while i < program.items.len {
            item := &program.items[i]
            switch item.* {
                .fn_decl => {
                    f := &item.fn_decl
                    if isGenericDecl(f) {
                        try self.generics.append(self.arena, .{ .name = f.name, .decl = f })
                    }
                },
                else => {},
            }
            i += 1
        }
        i = 0
        while i < program.items.len {
            item := &program.items[i]
            switch item.* {
                .const_decl => {
                    d := &item.const_decl
                    if d.type |t| {
                        _ = try self.internTypeRef(t)
                    }
                    // An `extern var` has no initializer to fold (and is
                    // always mutable, so it never enters the env either way).
                    if d.value != null {
                        v := try self.evalExpr(&d.value.?)
                        if !d.is_mut {
                            try self.env.append(self.arena, .{ .name = d.name, .value = v })
                        }
                    }
                    self.file_limit = self.env.items.len
                },
                else => {},
            }
            i += 1
        }
        // The file scope is complete — resolve and memoize every generic's
        // parameter kinds before the body walks (whose local frames must
        // never leak into a declaration's resolution).
        self.kinds_final = true
        i = 0
        while i < self.generics.items.len {
            _ = try self.kindsFor(&self.generics.items[i])
            i += 1
        }
        i = 0
        while i < program.items.len {
            item := &program.items[i]
            switch item.* {
                .fn_decl => try self.evalFn(&item.fn_decl),
                .comptime_block => |stmts| try self.evalBlock(stmts),
                .test_decl => |t| try self.evalBlock(t.body),
                else => {},
            }
            i += 1
        }
    }

    // The value recorded for `name`, innermost frame first, or null. Public
    // surface for the callers that resolve a folded constant by name.
    pub fn lookup(self *Evaluator, name []u8) ?Vid {
        var i usize = self.lookup_limit orelse self.env.items.len
        while i > 0 {
            i -= 1
            if sup.eql(u8, self.env.items[i].name, name) {
                return self.env.items[i].value
            }
        }
        return null
    }

    fn report(self *mut Evaluator, anchor []u8, msg []u8) Oom!void {
        if self.quiet {
            return
        }
        try self.diags.append(self.arena, .{ .anchor = anchor, .msg = msg })
    }

    // A nested statement list in its own frame, popped on exit — sibling
    // blocks may reuse a constant's name (sema enforces the rules; the
    // evaluator only mirrors the visibility).
    fn evalBlock(self *mut Evaluator, stmts []ast.Stmt) Oom!void {
        saved := self.frame_base
        self.frame_base = self.env.items.len
        defer {
            self.env.items.len = self.frame_base
            self.frame_base = saved
        }
        var i usize = 0
        while i < stmts.len {
            try self.evalStmt(&stmts[i])
            i += 1
        }
    }

    fn evalStmt(self *mut Evaluator, s *ast.Stmt) Oom!void {
        switch s.* {
            .bind => {
                if s.bind.type |t| {
                    _ = try self.internTypeRef(t)
                }
                v := try self.evalExpr(&s.bind.value)
                if !s.bind.is_mut {
                    try self.env.append(self.arena, .{ .name = s.bind.name, .value = v })
                }
            },
            .destructure => {
                _ = try self.evalExpr(&s.destructure.value)
            },
            .assign => {
                _ = try self.evalExpr(&s.assign.value)
            },
            .destructure_assign => {
                _ = try self.evalExpr(&s.destructure_assign.value)
            },
            .discard => {
                _ = try self.evalExpr(&s.discard)
            },
            .expr => {
                _ = try self.evalExpr(&s.expr)
            },
            // A known condition prunes the dead arm, exactly as the
            // downstream compiler does (a definite error in an unreached
            // branch is not an error there, so it must not be one here).
            .if_stmt => {
                cond := try self.evalExpr(&s.if_stmt.cond)
                if cond != false_value {
                    try self.evalBlock(s.if_stmt.body)
                }
                if cond != true_value {
                    if s.if_stmt.else_body |eb| {
                        try self.evalBlock(eb)
                    }
                }
            },
            .while_stmt => {
                cond := try self.evalExpr(&s.while_stmt.cond)
                if cond != false_value {
                    try self.evalBlock(s.while_stmt.body)
                }
                if s.while_stmt.else_body |eb| {
                    try self.evalBlock(eb)
                }
            },
            .for_stmt => {
                _ = try self.evalExpr(&s.for_stmt.iter)
                if s.for_stmt.range_hi != null {
                    _ = try self.evalExpr(&s.for_stmt.range_hi.?)
                }
                try self.evalBlock(s.for_stmt.body)
                if s.for_stmt.else_body |eb| {
                    try self.evalBlock(eb)
                }
            },
            .defer_stmt => |inner| try self.evalStmt(inner),
            // The `|err|` capture is runtime-only — the evaluator walks the
            // deferred body for its comptime effects and ignores the binding.
            .errdefer_stmt => |ed| try self.evalStmt(ed.body),
            .defer_block => |stmts| try self.evalBlock(stmts),
            .errdefer_block => |ed| try self.evalBlock(ed.body),
        }
    }

    // Evaluate one expression to a Vid. Takes a pointer into the AST (never
    // a copy): a container definition's identity is its node address, so the
    // expression must be the arena-resident original.
    fn evalExpr(self *mut Evaluator, e *ast.Expr) Oom!Vid {
        switch e.* {
            .int => |lex| {
                n := sup.parseInt(i128, lex, 0) catch return unknown
                return self.pool.intern(.{ .int = n })
            },
            .string => |lex| return self.pool.intern(.{ .string = lex }),
            .value_word => |w| {
                if sup.eql(u8, w, "true") {
                    return true_value
                }
                if sup.eql(u8, w, "false") {
                    return false_value
                }
                return unknown
            },
            .ident => |name| {
                if self.lookup(name) |v| {
                    return v
                }
                return (try builtinType(self.pool, name)) orelse unknown
            },
            .group => |g| return self.evalExpr(g),
            .unary => return self.evalUnary(e.unary),
            .binary => return self.evalBinary(e),
            // A known condition selects one arm and the other is never
            // evaluated (dead-branch pruning, as downstream); an unknown
            // condition evaluates both for their definite errors.
            .if_expr => {
                cond := try self.evalExpr(e.if_expr.cond)
                if cond == true_value {
                    return self.evalExpr(e.if_expr.then)
                }
                if cond == false_value {
                    return self.evalExpr(e.if_expr.else_)
                }
                _ = try self.evalExpr(e.if_expr.then)
                _ = try self.evalExpr(e.if_expr.else_)
                return unknown
            },
            .type_lit => |t| return self.internTypeRef(t.*),
            // A container definition is its own (nominal) type — and its
            // field types, associated constants, and method bodies are walked
            // here, so a definite error inside one is found wherever the
            // container is defined.
            .struct_def => {
                var i usize = 0
                while i < e.struct_def.fields.len {
                    _ = try self.internTypeRef(e.struct_def.fields[i].type)
                    i += 1
                }
                try self.evalContainerDecls(e.struct_def.decls)
                return self.pool.intern(.{ .container = .{ .struct_type = &e.struct_def } })
            },
            .enum_def => {
                try self.evalContainerDecls(e.enum_def.decls)
                return self.pool.intern(.{ .container = .{ .enum_type = &e.enum_def } })
            },
            .union_def => {
                var i usize = 0
                while i < e.union_def.variants.len {
                    if e.union_def.variants[i].payload |t| {
                        _ = try self.internTypeRef(t)
                    }
                    i += 1
                }
                try self.evalContainerDecls(e.union_def.decls)
                return self.pool.intern(.{ .container = .{ .union_type = &e.union_def } })
            },
            // Everything below is outside the evaluator's reach — the result
            // is `unknown` — but subexpressions are still walked, so a
            // definite error inside an argument or operand is found.
            .member => |m| {
                _ = try self.evalExpr(m.base)
                return unknown
            },
            // A call's result is out of reach unless the callee is a known
            // generic declaration — then the call is an application site:
            // checked for arity and argument kinds, and a well-formed one
            // instantiated to the type it denotes (the arguments are
            // evaluated either way, so their definite errors are found).
            .call => {
                _ = try self.evalExpr(e.call.callee)
                if self.genericCallee(e.call.callee.*) |g| {
                    return self.checkApplication(firstLexeme(e.call.callee.*) orelse g.name, g, e.call.args)
                }
                var i usize = 0
                while i < e.call.args.len {
                    _ = try self.evalExpr(&e.call.args[i])
                    i += 1
                }
                return unknown
            },
            .builtin_call => |b| {
                var i usize = 0
                while i < b.args.len {
                    _ = try self.evalExpr(&b.args[i])
                    i += 1
                }
                return unknown
            },
            .index => |ix| {
                _ = try self.evalExpr(ix.base)
                _ = try self.evalExpr(ix.index)
                return unknown
            },
            .slice => |sl| {
                _ = try self.evalExpr(sl.base)
                _ = try self.evalExpr(sl.lo)
                if sl.hi |hi| {
                    _ = try self.evalExpr(hi)
                }
                if sl.sentinel |sen| {
                    _ = try self.evalExpr(sen)
                }
                return unknown
            },
            .deref => |d| {
                _ = try self.evalExpr(d)
                return unknown
            },
            .optional_unwrap => |u| {
                _ = try self.evalExpr(u)
                return unknown
            },
            .try_expr => |t| {
                _ = try self.evalExpr(t)
                return unknown
            },
            .catch_expr => |c| {
                _ = try self.evalExpr(c.lhs)
                _ = try self.evalExpr(c.handler)
                return unknown
            },
            .switch_expr => |sw| {
                _ = try self.evalExpr(sw.subject)
                return unknown
            },
            .struct_lit => |fields| {
                var i usize = 0
                while i < fields.len {
                    _ = try self.evalExpr(&fields[i].value)
                    i += 1
                }
                return unknown
            },
            .typed_lit => {
                _ = try self.evalExpr(e.typed_lit.type)
                var i usize = 0
                while i < e.typed_lit.fields.len {
                    _ = try self.evalExpr(&e.typed_lit.fields[i].value)
                    i += 1
                }
                return unknown
            },
            .block_expr => |blk| {
                try self.evalBlock(blk.body)
                return unknown
            },
            .brk => |b| {
                if b.value |v| {
                    _ = try self.evalExpr(v)
                }
                return unknown
            },
            .ret => |maybe| {
                if maybe |vals| {
                    var i usize = 0
                    while i < vals.len {
                        _ = try self.evalExpr(&vals[i])
                        i += 1
                    }
                }
                return unknown
            },
            // An error-set definition is its own (nominal) type, like the
            // containers above — interned so a type-position misuse of it
            // (say, under `||`) is recognizable as a type.
            .error_set => return self.pool.intern(.{ .container = .{ .error_set_type = e } }),
            .float, .multiline_str, .char, .enum_lit, .error_lit, .asm_expr, .cont => return unknown,
        }
    }

    // The associated declarations of a container body: each constant's
    // type annotation is walked and its initializer evaluated (definite
    // errors found, value not recorded — an associated constant is
    // referenced through its container, which is out of reach), and each
    // method is walked like a top-level function.
    fn evalContainerDecls(self *mut Evaluator, decls []ast.ContainerDecl) Oom!void {
        var i usize = 0
        while i < decls.len {
            d := &decls[i]
            switch d.* {
                .constant => {
                    if d.constant.type |t| {
                        _ = try self.internTypeRef(t)
                    }
                    // The value field is optional (`extern var` at file scope);
                    // an associated constant always carries one — unwrap.
                    if d.constant.value != null {
                        _ = try self.evalExpr(&d.constant.value.?)
                    }
                },
                .method => try self.evalFn(&d.method),
                .use_import => {},
            }
            i += 1
        }
    }

    // A function's signature and body. The parameter and return types are
    // walked first (generic applications and definite errors in an
    // annotation are found here), then the body in its own frame with every
    // named parameter recorded as `unknown` — a parameter's name must
    // resolve to silence, never to an unrelated file-scope constant or a
    // same-named generic.
    fn evalFn(self *mut Evaluator, f *ast.FnDecl) Oom!void {
        var i usize = 0
        while i < f.params.len {
            _ = try self.internTypeRef(f.params[i].type)
            i += 1
        }
        if f.ret |r| {
            _ = try self.internTypeRef(r)
        }
        body := f.body orelse return
        saved := self.frame_base
        self.frame_base = self.env.items.len
        defer {
            self.env.items.len = self.frame_base
            self.frame_base = saved
        }
        i = 0
        while i < f.params.len {
            if f.params[i].name |n| {
                try self.env.append(self.arena, .{ .name = n, .value = unknown })
            }
            i += 1
        }
        i = 0
        while i < body.len {
            try self.evalStmt(&body[i])
            i += 1
        }
    }

    // The generic a value-position call applies: the callee is a bare,
    // unbound identifier naming a recognized generic declaration. A name
    // bound in the environment — a parameter, a local, a folded constant —
    // is never the file-level generic.
    fn genericCallee(self *mut Evaluator, callee ast.Expr) ?*mut Generic {
        switch callee {
            .ident => |name| {
                if self.lookup(name) != null {
                    return null
                }
                return self.findGeneric(name)
            },
            else => return null,
        }
    }

    fn findGeneric(self *mut Evaluator, name []u8) ?*mut Generic {
        var i usize = 0
        while i < self.generics.items.len {
            if sup.eql(u8, self.generics.items[i].name, name) {
                return &self.generics.items[i]
            }
            i += 1
        }
        return null
    }

    // The parameter kinds of a generic, resolved from the declared parameter
    // types. Resolution is quiet (the regular annotation walk owns any
    // definite error inside these types) and sees only the file scope (a
    // declaration's types never resolve against a caller's locals). Before
    // the file scope is complete the result is recomputed per call and not
    // memoized — an alias a parameter type needs may not be recorded yet.
    // Null means the entry is already being resolved (a parameter-type
    // cycle); the caller skips kind checks, arity still holds.
    fn kindsFor(self *mut Evaluator, g *mut Generic) Oom!?[]ParamKind {
        if g.kinds |k| {
            return k
        }
        if g.resolving {
            return null
        }
        g.resolving = true
        saved_quiet := self.quiet
        saved_limit := self.lookup_limit
        self.quiet = true
        self.lookup_limit = self.file_limit
        defer {
            g.resolving = false
            self.quiet = saved_quiet
            self.lookup_limit = saved_limit
        }
        kinds := try self.arena.alloc(ParamKind, g.decl.params.len)
        var i usize = 0
        while i < g.decl.params.len {
            v := try self.internTypeRef(g.decl.params[i].type)
            if v == type_type {
                kinds[i] = .wants_type
            } else if v == unknown {
                kinds[i] = .unchecked
            } else {
                kinds[i] = .wants_value
            }
            i += 1
        }
        if self.kinds_final {
            g.kinds = kinds
        }
        return kinds
    }

    // Check one generic application — `Name(args…)` in type or value
    // position — for arity and per-argument kind, then instantiate a
    // well-formed one to the type it denotes. The arguments are evaluated
    // here either way, so a definite error inside one is found even when
    // the application itself is malformed; a malformed application is
    // never instantiated.
    fn checkApplication(self *mut Evaluator, anchor []u8, g *mut Generic, args []ast.Expr) Oom!Vid {
        if args.len != g.decl.params.len {
            try self.report(anchor, try sup.allocPrint(self.arena, "generic '{s}' expects {d} argument{s}, found {d}", .{ g.name, g.decl.params.len, plural(g.decl.params.len), args.len }))
            var i usize = 0
            while i < args.len {
                _ = try self.evalExpr(&args[i])
                i += 1
            }
            return unknown
        }
        kinds := try self.kindsFor(g)
        arg_vids := try self.arena.alloc(Vid, args.len)
        var well_kinded = true
        var i usize = 0
        while i < args.len {
            v := try self.evalExpr(&args[i])
            arg_vids[i] = v
            if kinds |ks| {
                arg_anchor := firstLexeme(args[i]) orelse anchor
                switch ks[i] {
                    .wants_type => {
                        if isKnownValue(self.pool.get(v)) {
                            well_kinded = false
                            try self.report(arg_anchor, try sup.allocPrint(self.arena, "argument {d} to generic '{s}' expects a type, found a value", .{ i + 1, g.name }))
                        }
                    },
                    .wants_value => {
                        if self.isType(v) {
                            well_kinded = false
                            try self.report(arg_anchor, try sup.allocPrint(self.arena, "argument {d} to generic '{s}' expects a value, found a type", .{ i + 1, g.name }))
                        }
                    },
                    .unchecked => {},
                }
            }
            i += 1
        }
        if !well_kinded {
            return unknown
        }
        return self.instantiate(anchor, g, arg_vids)
    }

    // Instantiate a well-formed application over fully known arguments. The
    // `(owner, args)` pair interns as an instance key — interning IS the
    // memoization, so re-applying the same arguments costs one lookup — and
    // the result type is evaluated only when the body is a single `return`:
    // a returned container definition's type is the instance Vid itself
    // (distinct arguments are distinct nominal types, as downstream — the
    // definition node alone would collapse every argument list onto one
    // type), a returned type expression evaluates with the parameters bound
    // to the argument values, and anything else — any other body shape, a
    // result that is not a type — stays `unknown`.
    fn instantiate(self *mut Evaluator, anchor []u8, g *mut Generic, arg_vids []Vid) Oom!Vid {
        body := g.decl.body orelse return unknown
        ret_expr := singleReturnExpr(body) orelse return unknown
        var i usize = 0
        while i < arg_vids.len {
            if arg_vids[i] == unknown {
                return unknown
            }
            i += 1
        }
        iv := try self.pool.intern(.{ .instance = .{ .owner = g.decl, .args = arg_vids } })
        if self.instanceResult(iv) |r| {
            return r
        }
        // The depth and budget diagnostics bypass `quiet`: an instance
        // evaluates once (memoized), possibly during quiet parameter-kind
        // resolution, so no later loud walk would re-find them.
        if self.inst_depth >= max_inst_depth {
            try self.diags.append(self.arena, .{ .anchor = anchor, .msg = try sup.allocPrint(self.arena, "generic '{s}' exceeds the instantiation depth limit ({d})", .{ g.name, max_inst_depth }) })
            try self.recordInstance(iv, unknown)
            return unknown
        }
        if self.inst_count >= self.inst_budget {
            if !self.budget_reported {
                self.budget_reported = true
                try self.diags.append(self.arena, .{ .anchor = anchor, .msg = try sup.allocPrint(self.arena, "generic '{s}' exceeds the instantiation budget ({d})", .{ g.name, self.inst_budget }) })
            }
            try self.recordInstance(iv, unknown)
            return unknown
        }
        self.inst_count += 1
        switch ret_expr.* {
            // A returned container definition: the instance key IS the
            // nominal type — no body evaluation needed.
            .struct_def, .enum_def, .union_def => {
                try self.recordInstance(iv, iv)
                return iv
            },
            else => {},
        }
        // The instance frame sits directly on the file scope: `lookup_limit`
        // lifts so the bound parameters (appended past any caller frame) are
        // visible, and the body evaluates quiet — every definite error
        // spelled in it is owned by the loud body walk, and errors that
        // exist only under substituted arguments stay out of the boundary.
        // A caller's locals are technically in view, but a generic body
        // referencing a caller-local name is already a binding error, so
        // that space is unreachable in accepted programs.
        saved_base := self.frame_base
        saved_limit := self.lookup_limit
        saved_quiet := self.quiet
        self.frame_base = self.env.items.len
        self.lookup_limit = null
        self.quiet = true
        self.inst_depth += 1
        defer {
            self.inst_depth -= 1
            self.env.items.len = self.frame_base
            self.frame_base = saved_base
            self.lookup_limit = saved_limit
            self.quiet = saved_quiet
        }
        i = 0
        while i < g.decl.params.len {
            if g.decl.params[i].name |n| {
                try self.env.append(self.arena, .{ .name = n, .value = arg_vids[i] })
            }
            i += 1
        }
        r := try self.evalExpr(ret_expr)
        var result Vid = unknown
        if self.isType(r) {
            result = r
        }
        try self.recordInstance(iv, result)
        return result
    }

    // The memoized result of an interned instance, or null when it has not
    // been evaluated yet.
    fn instanceResult(self *Evaluator, iv Vid) ?Vid {
        var i usize = 0
        while i < self.instance_results.items.len {
            if self.instance_results.items[i].instance == iv {
                return self.instance_results.items[i].result
            }
            i += 1
        }
        return null
    }

    // Record an instance's result once; a re-entrant instantiation (the
    // depth-capped recursion unwinding) finds the first record and skips.
    fn recordInstance(self *mut Evaluator, iv Vid, result Vid) Oom!void {
        if self.instanceResult(iv) != null {
            return
        }
        try self.instance_results.append(self.arena, .{ .instance = iv, .result = result })
    }

    fn evalUnary(self *mut Evaluator, u ast.Unary) Oom!Vid {
        v := try self.evalExpr(u.operand)
        if sup.eql(u8, u.op, "!") {
            if v == true_value {
                return false_value
            }
            if v == false_value {
                return true_value
            }
            return unknown
        }
        if sup.eql(u8, u.op, "-") {
            n := self.knownInt(v) orelse return unknown
            if n == sup.minInt(i128) {
                return unknown // -(min) overflows
            }
            return self.pool.intern(.{ .int = -n })
        }
        // `~` needs the operand's bit width (a typed constant the evaluator
        // does not track) and `&` is an address — both out of reach.
        return unknown
    }

    fn evalBinary(self *mut Evaluator, e *ast.Expr) Oom!Vid {
        op := e.binary.op
        lv := try self.evalExpr(e.binary.lhs)
        rv := try self.evalExpr(e.binary.rhs)

        // The one definite error: dividing by a *known* zero is rejected
        // downstream no matter what the numerator is, so it is safe — and
        // useful — to report it at the Flash source line.
        if sup.eql(u8, op, "/") || sup.eql(u8, op, "%") {
            if self.knownInt(rv) |d| {
                if d == 0 {
                    if firstLexeme(e.*) |anchor| {
                        try self.report(anchor, "division by zero")
                    }
                    return unknown
                }
            }
        }

        // `||` is boolean or — Flash has no error-set merge. A type operand
        // (an error set, most likely) would otherwise lower to invalid Zig
        // with no Flash-side diagnostic, so it is rejected here by name.
        if sup.eql(u8, op, "||") && (self.isType(lv) || self.isType(rv)) {
            // An inline `error{…}` operand has no leftmost lexeme; the
            // operator itself anchors the report then.
            try self.report(firstLexeme(e.*) orelse op, "'||' is boolean or and cannot merge error sets — declare the combined error set explicitly")
            return unknown
        }

        // Boolean logic. The left side decides where it can (`false && _`,
        // `true || _`); otherwise both sides must be known.
        if sup.eql(u8, op, "&&") {
            if lv == false_value {
                return false_value
            }
            if lv == true_value && (rv == true_value || rv == false_value) {
                return rv
            }
            return unknown
        }
        if sup.eql(u8, op, "||") {
            if lv == true_value {
                return true_value
            }
            if lv == false_value && (rv == true_value || rv == false_value) {
                return rv
            }
            return unknown
        }

        // Bool equality (`==` / `!=` on two known bools).
        if knownBool(lv) != null && knownBool(rv) != null {
            equal := lv == rv
            if sup.eql(u8, op, "==") {
                return boolVid(equal)
            }
            if sup.eql(u8, op, "!=") {
                return boolVid(!equal)
            }
            return unknown
        }

        // Integer arithmetic, comparison, and bitwise folding.
        a := self.knownInt(lv) orelse return unknown
        c := self.knownInt(rv) orelse return unknown
        return self.foldInts(op, a, c)
    }

    // Fold `a op c` over known i128 operands. Any result the i128 cannot
    // hold — and any shape the downstream compiler treats specially (signed
    // division rounding, oversized shifts) — degrades to `unknown`.
    fn foldInts(self *mut Evaluator, op []u8, a i128, c i128) Oom!Vid {
        if sup.eql(u8, op, "+") {
            r := #addWithOverflow(a, c)
            return if (r[1] != 0) unknown else self.pool.intern(.{ .int = r[0] })
        }
        if sup.eql(u8, op, "-") {
            r := #subWithOverflow(a, c)
            return if (r[1] != 0) unknown else self.pool.intern(.{ .int = r[0] })
        }
        if sup.eql(u8, op, "*") {
            r := #mulWithOverflow(a, c)
            return if (r[1] != 0) unknown else self.pool.intern(.{ .int = r[0] })
        }
        // Plain `/` and `%` on a negative operand have rounding rules the
        // downstream compiler gates behind dedicated builtins — degrade
        // rather than guess (the known-zero divisor was already handled).
        if sup.eql(u8, op, "/") {
            if a < 0 || c <= 0 {
                return unknown
            }
            return self.pool.intern(.{ .int = #divTrunc(a, c) })
        }
        if sup.eql(u8, op, "%") {
            if a < 0 || c <= 0 {
                return unknown
            }
            return self.pool.intern(.{ .int = #rem(a, c) })
        }
        if sup.eql(u8, op, "&") {
            return self.pool.intern(.{ .int = a & c })
        }
        if sup.eql(u8, op, "|") {
            return self.pool.intern(.{ .int = a | c })
        }
        if sup.eql(u8, op, "^") {
            return self.pool.intern(.{ .int = a ^ c })
        }
        if sup.eql(u8, op, "<<") {
            // A compile-time integer may legally shift past 127 downstream
            // (arbitrary precision); past the i128 it degrades here.
            if c < 0 || c > 127 {
                return unknown
            }
            r := #shlWithOverflow(a, #as(u7, #intCast(c)))
            return if (r[1] != 0) unknown else self.pool.intern(.{ .int = r[0] })
        }
        if sup.eql(u8, op, ">>") {
            if c < 0 || c > 127 {
                return unknown
            }
            return self.pool.intern(.{ .int = a >> #as(u7, #intCast(c)) })
        }
        if sup.eql(u8, op, "==") {
            return boolVid(a == c)
        }
        if sup.eql(u8, op, "!=") {
            return boolVid(a != c)
        }
        if sup.eql(u8, op, "<") {
            return boolVid(a < c)
        }
        if sup.eql(u8, op, "<=") {
            return boolVid(a <= c)
        }
        if sup.eql(u8, op, ">") {
            return boolVid(a > c)
        }
        if sup.eql(u8, op, ">=") {
            return boolVid(a >= c)
        }
        return unknown // `orelse` and anything new
    }

    // Intern the type a TypeRef denotes, or `unknown` where a child is out
    // of reach. A dotted name roots in an import (out of reach); a bare name
    // is a recorded constant holding a type, or a builtin.
    fn internTypeRef(self *mut Evaluator, t ast.TypeRef) Oom!Vid {
        switch t {
            .name => |n| {
                if sup.indexOfScalar(u8, n, '.') != null {
                    return unknown
                }
                if self.lookup(n) |v| {
                    return if (self.isType(v)) v else unknown
                }
                return (try builtinType(self.pool, n)) orelse unknown
            },
            .optional => |inner| {
                child := try self.internTypeRef(inner.*)
                if child == unknown {
                    return unknown
                }
                return self.pool.intern(.{ .optional = child })
            },
            .slice => |p| return self.internPtr(p.elem.*, .slice, false, false, null, p.align_expr),
            .slice_mut => |p| return self.internPtr(p.elem.*, .slice, true, false, null, p.align_expr),
            .many_ptr => |p| return self.internPtr(p.elem.*, .many, false, false, null, p.align_expr),
            .many_ptr_mut => |p| return self.internPtr(p.elem.*, .many, true, false, null, p.align_expr),
            .many_ptr_volatile => |p| return self.internPtr(p.elem.*, .many, false, true, null, p.align_expr),
            .many_ptr_mut_volatile => |p| return self.internPtr(p.elem.*, .many, true, true, null, p.align_expr),
            .ptr => |p| return self.internPtr(p.elem.*, .one, false, false, null, p.align_expr),
            .ptr_mut => |p| return self.internPtr(p.elem.*, .one, true, false, null, p.align_expr),
            .ptr_volatile => |p| return self.internPtr(p.elem.*, .one, false, true, null, p.align_expr),
            .ptr_mut_volatile => |p| return self.internPtr(p.elem.*, .one, true, true, null, p.align_expr),
            .slice_sentinel => |sp| return self.internSentinelPtr(sp, .slice, false),
            .slice_sentinel_mut => |sp| return self.internSentinelPtr(sp, .slice, true),
            .many_ptr_sentinel => |sp| return self.internSentinelPtr(sp, .many, false),
            .many_ptr_sentinel_mut => |sp| return self.internSentinelPtr(sp, .many, true),
            .array => |arr| {
                len := try self.evalExpr(arr.len)
                if self.knownInt(len) == null {
                    return unknown
                }
                elem := try self.internTypeRef(arr.elem.*)
                if elem == unknown {
                    return unknown
                }
                return self.pool.intern(.{ .array = .{ .len = len, .elem = elem } })
            },
            .array_sentinel => |arr| {
                len := try self.evalExpr(arr.len)
                if self.knownInt(len) == null {
                    return unknown
                }
                sen := try self.evalExpr(arr.sentinel)
                if sen == unknown {
                    return unknown
                }
                elem := try self.internTypeRef(arr.elem.*)
                if elem == unknown {
                    return unknown
                }
                return self.pool.intern(.{ .array = .{ .len = len, .sentinel = sen, .elem = elem } })
            },
            // An inferred length is not a concrete type until its
            // initializer is — out of reach.
            .array_inferred, .array_inferred_sentinel => return unknown,
            .errunion => |eu| {
                var set ?Vid = null
                if eu.set |s| {
                    sv := try self.internTypeRef(s.*)
                    if sv == unknown {
                        return unknown
                    }
                    set = sv
                }
                payload := try self.internTypeRef(eu.payload.*)
                if payload == unknown {
                    return unknown
                }
                return self.pool.intern(.{ .error_union = .{ .set = set, .payload = payload } })
            },
            // The calling convention joins the key as its variant name (`.c`
            // records "c") — part of the type's identity, see FnType.conv. A
            // convention spelled as anything but an inferred enum literal
            // would need evaluation, so it conservatively stays `unknown`.
            .fn_type => |ft| {
                params := try self.arena.alloc(Vid, ft.params.len)
                var i usize = 0
                while i < ft.params.len {
                    params[i] = try self.internTypeRef(ft.params[i])
                    if params[i] == unknown {
                        return unknown
                    }
                    i += 1
                }
                var conv ?[]u8 = null
                if ft.call_conv |cc| {
                    if cc.* != .enum_lit {
                        return unknown
                    }
                    conv = cc.enum_lit
                }
                var ret Vid = type_void
                if ft.ret |r| {
                    rv := try self.internTypeRef(r.*)
                    if rv == unknown {
                        return unknown
                    }
                    ret = rv
                }
                return self.pool.intern(.{ .fn_type = .{ .params = params, .conv = conv, .ret = ret } })
            },
            // A generic application in type position is checked when its
            // name is a known, unshadowed generic, and a well-formed one
            // denotes its instance type (or `unknown` where instantiation
            // is out of reach); a dotted name roots in an import.
            .generic => |g| {
                if sup.indexOfScalar(u8, g.name, '.') == null && self.lookup(g.name) == null {
                    if self.findGeneric(g.name) |gen| {
                        return self.checkApplication(g.name, gen, g.args)
                    }
                }
                var i usize = 0
                while i < g.args.len {
                    _ = try self.evalExpr(&g.args[i])
                    i += 1
                }
                return unknown
            },
            .tuple => |elems| {
                vids := try self.arena.alloc(Vid, elems.len)
                var i usize = 0
                while i < elems.len {
                    vids[i] = try self.internTypeRef(elems[i])
                    if vids[i] == unknown {
                        return unknown
                    }
                    i += 1
                }
                return self.pool.intern(.{ .tuple = vids })
            },
        }
    }

    fn internPtr(self *mut Evaluator, inner ast.TypeRef, size PtrSize, is_mut bool, is_volatile bool, sentinel ?Vid, align_expr ?*mut ast.Expr) Oom!Vid {
        // An `align(N)` joins the key as the Vid of its evaluated value —
        // part of the type's identity in Zig. One the evaluator cannot fold
        // conservatively keeps the whole type `unknown` (the sentinel's rule).
        var alignment ?Vid = null
        if align_expr |ae| {
            av := try self.evalExpr(ae)
            if av == unknown {
                return unknown
            }
            alignment = av
        }
        elem := try self.internTypeRef(inner)
        if elem == unknown {
            return unknown
        }
        return self.pool.intern(.{ .ptr = .{ .size = size, .is_mut = is_mut, .is_volatile = is_volatile, .sentinel = sentinel, .alignment = alignment, .elem = elem } })
    }

    fn internSentinelPtr(self *mut Evaluator, sp ast.SentinelPtr, size PtrSize, is_mut bool) Oom!Vid {
        sen := try self.evalExpr(sp.sentinel)
        if sen == unknown {
            return unknown
        }
        return self.internPtr(sp.elem.*, size, is_mut, false, sen, sp.align_expr)
    }

    fn knownInt(self *Evaluator, v Vid) ?i128 {
        return switch self.pool.get(v) {
            .int => |n| n,
            else => null,
        }
    }

    fn isType(self *Evaluator, v Vid) bool {
        return switch self.pool.get(v) {
            .simple, .int_type, .optional, .ptr, .array, .error_union, .fn_type, .tuple, .container, .instance => true,
            .int, .bool_value, .string, .void_value, .unknown => false,
        }
    }
}

// Is this declaration generic — applicable as `Name(args…)`? A `comptime`
// parameter or a `type` return marks it; everything else is an ordinary
// function whose call sites are out of this module's reach.
fn isGenericDecl(f *ast.FnDecl) bool {
    var i usize = 0
    while i < f.params.len {
        if f.params[i].is_comptime {
            return true
        }
        i += 1
    }
    if f.ret |r| {
        switch r {
            .name => |n| {
                if sup.eql(u8, n, "type") {
                    return true
                }
            },
            else => {},
        }
    }
    return false
}

// The expression a single-`return` function body returns, or null for any
// other body shape — the only shape whose instance result type is evaluated
// (a multi-value return is a tuple of values, never a type).
fn singleReturnExpr(body []ast.Stmt) ?*ast.Expr {
    if body.len != 1 {
        return null
    }
    switch body[0] {
        .expr => {},
        else => return null,
    }
    e := &body[0].expr
    switch e.* {
        .ret => {},
        else => return null,
    }
    vals := e.ret orelse return null
    if vals.len != 1 {
        return null
    }
    return &vals[0]
}

// Is this a *known* non-type value — the shapes a `type` parameter
// definitely rejects? `unknown` is neither a value nor a type here.
fn isKnownValue(k Key) bool {
    return switch k {
        .int, .bool_value, .string, .void_value => true,
        else => false,
    }
}

fn plural(n usize) []u8 {
    return if (n == 1) "" else "s"
}

fn knownBool(v Vid) ?bool {
    if v == true_value {
        return true
    }
    if v == false_value {
        return false
    }
    return null
}

fn boolVid(b bool) Vid {
    return if (b) true_value else false_value
}

// The leftmost source slice of an expression — its diagnostic anchor (the
// same descent sema's anchoring uses). Null when no single stored slice
// exists; the caller then skips the diagnostic rather than anchor it nowhere.
fn firstLexeme(x ast.Expr) ?[]u8 {
    return switch x {
        .int, .float, .string, .char, .ident, .value_word, .enum_lit, .error_lit => |s| s,
        .member => |m| firstLexeme(m.base.*),
        .deref => |d| firstLexeme(d.*),
        .optional_unwrap => |u| firstLexeme(u.*),
        .index => |ix| firstLexeme(ix.base.*),
        .slice => |s| firstLexeme(s.base.*),
        .call => |c| firstLexeme(c.callee.*),
        .binary => |b| firstLexeme(b.lhs.*),
        .unary => |u| u.op,
        .group => |g| firstLexeme(g.*),
        .try_expr => |t| firstLexeme(t.*),
        .typed_lit => |tl| firstLexeme(tl.type.*),
        else => null,
    }
}

// --- tests ---------------------------------------------------------------

test "well-known entries sit at their named indices and round-trip" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())

    // The seed fills exactly the static range.
    try sup.expectEqual(static_count, pool.count())

    // get() at a named Vid yields its key …
    try sup.expectEqual(Key{ .simple = .bool }, pool.get(type_bool))
    try sup.expectEqual(Key{ .simple = .comptime_int }, pool.get(type_comptime_int))
    try sup.expectEqual(Key{ .int_type = .{ .signedness = .unsigned, .bits = 8 } }, pool.get(type_u8))
    try sup.expectEqual(Key{ .bool_value = true }, pool.get(true_value))
    try sup.expectEqual(Key.void_value, pool.get(void_value))
    try sup.expectEqual(Key.unknown, pool.get(unknown))

    // … and interning that key finds the static entry, never a duplicate.
    try sup.expectEqual(type_u8, try pool.intern(.{ .int_type = .{ .signedness = .unsigned, .bits = 8 } }))
    try sup.expectEqual(type_i64, try pool.intern(.{ .int_type = .{ .signedness = .signed, .bits = 64 } }))
    try sup.expectEqual(type_void, try pool.intern(.{ .simple = .void }))
    try sup.expectEqual(false_value, try pool.intern(.{ .bool_value = false }))
    try sup.expectEqual(unknown, try pool.intern(.unknown))
    try sup.expectEqual(static_count, pool.count())
}

test "an uncommon integer width interns dynamically and dedups" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())

    u7_key := Key{ .int_type = .{ .signedness = .unsigned, .bits = 7 } }
    first := try pool.intern(u7_key)
    try sup.expect(first >= static_count)
    try sup.expectEqual(first, try pool.intern(u7_key))
    // Same width, other signedness is a distinct type.
    signed_7 := try pool.intern(.{ .int_type = .{ .signedness = .signed, .bits = 7 } })
    try sup.expect(first != signed_7)
}

test "the same composite type interned twice is the same Vid" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())

    // ?u8 — once.
    opt_u8 := try pool.intern(.{ .optional = type_u8 })
    try sup.expectEqual(opt_u8, try pool.intern(.{ .optional = type_u8 }))

    // []u8 / *u32 — each once, however often spelled.
    slice_u8 := try pool.intern(.{ .ptr = .{ .size = .slice, .elem = type_u8 } })
    try sup.expectEqual(slice_u8, try pool.intern(.{ .ptr = .{ .size = .slice, .elem = type_u8 } }))
    ptr_u32 := try pool.intern(.{ .ptr = .{ .size = .one, .elem = type_u32 } })
    try sup.expectEqual(ptr_u32, try pool.intern(.{ .ptr = .{ .size = .one, .elem = type_u32 } }))

    // Nesting composes on child Vids: ?[]u8 dedups through its child.
    opt_slice := try pool.intern(.{ .optional = slice_u8 })
    try sup.expectEqual(opt_slice, try pool.intern(.{ .optional = slice_u8 }))
}

test "distinct composite types get distinct Vids" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())

    opt_u8 := try pool.intern(.{ .optional = type_u8 })
    opt_u16 := try pool.intern(.{ .optional = type_u16 })
    try sup.expect(opt_u8 != opt_u16)

    // []u8 vs []mut u8 vs [*]u8 — qualifier and size each split identity.
    slice_const := try pool.intern(.{ .ptr = .{ .size = .slice, .elem = type_u8 } })
    slice_mut := try pool.intern(.{ .ptr = .{ .size = .slice, .is_mut = true, .elem = type_u8 } })
    many := try pool.intern(.{ .ptr = .{ .size = .many, .elem = type_u8 } })
    try sup.expect(slice_const != slice_mut)
    try sup.expect(slice_const != many)

    // [:0]u8 vs []u8 — a sentinel splits identity.
    zero := try pool.intern(.{ .int = 0 })
    slice_z := try pool.intern(.{ .ptr = .{ .size = .slice, .sentinel = zero, .elem = type_u8 } })
    try sup.expect(slice_z != slice_const)
    try sup.expectEqual(slice_z, try pool.intern(.{ .ptr = .{ .size = .slice, .sentinel = zero, .elem = type_u8 } }))

    // []align(16) u8 vs []u8 — an alignment splits identity (Zig's rule);
    // the aligned spelling dedups against itself.
    sixteen := try pool.intern(.{ .int = 16 })
    slice_a := try pool.intern(.{ .ptr = .{ .size = .slice, .alignment = sixteen, .elem = type_u8 } })
    try sup.expect(slice_a != slice_const)
    try sup.expectEqual(slice_a, try pool.intern(.{ .ptr = .{ .size = .slice, .alignment = sixteen, .elem = type_u8 } }))
}

test "array types are structural over the length value" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())

    four := try pool.intern(.{ .int = 4 })
    eight := try pool.intern(.{ .int = 8 })
    arr4 := try pool.intern(.{ .array = .{ .len = four, .elem = type_u8 } })
    arr8 := try pool.intern(.{ .array = .{ .len = eight, .elem = type_u8 } })
    try sup.expect(arr4 != arr8)
    try sup.expectEqual(arr4, try pool.intern(.{ .array = .{ .len = four, .elem = type_u8 } }))
}

test "error-union and function types dedup; an inferred set is its own identity" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())

    // !void (inferred set) vs E!void — distinct; each dedups.
    inferred := try pool.intern(.{ .error_union = .{ .set = null, .payload = type_void } })
    try sup.expectEqual(inferred, try pool.intern(.{ .error_union = .{ .set = null, .payload = type_void } }))
    some_set := try pool.intern(.unknown) // a stand-in set Vid is enough for identity
    explicit := try pool.intern(.{ .error_union = .{ .set = some_set, .payload = type_void } })
    try sup.expect(inferred != explicit)

    // fn(u8) u8 dedups even when the caller's parameter slice is a temporary.
    var params [1]Vid = .{type_u8}
    f1 := try pool.intern(.{ .fn_type = .{ .params = params[0..], .conv = null, .ret = type_u8 } })
    params[0] = type_u16 // clobber the caller's buffer …
    var params2 [1]Vid = .{type_u8}
    f2 := try pool.intern(.{ .fn_type = .{ .params = params2[0..], .conv = null, .ret = type_u8 } })
    try sup.expectEqual(f1, f2) // … the pool kept its own copy
    f3 := try pool.intern(.{ .fn_type = .{ .params = params[0..], .conv = null, .ret = type_u8 } })
    try sup.expect(f1 != f3) // fn(u16) u8 is a different type

    // The calling convention is part of the identity: fn(u8) callconv(.c) u8
    // is a new type, dedups against itself, and never against the unmarked one.
    var params3 [1]Vid = .{type_u8}
    fc := try pool.intern(.{ .fn_type = .{ .params = params3[0..], .conv = "c", .ret = type_u8 } })
    try sup.expect(fc != f1)
    try sup.expectEqual(fc, try pool.intern(.{ .fn_type = .{ .params = params3[0..], .conv = "c", .ret = type_u8 } }))
    try sup.expect(fc != try pool.intern(.{ .fn_type = .{ .params = params3[0..], .conv = "naked", .ret = type_u8 } }))
}

test "tuple types dedup element-wise and differ by arity and order" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())

    ab := try pool.intern(.{ .tuple = &.{ type_u8, type_bool } })
    try sup.expectEqual(ab, try pool.intern(.{ .tuple = &.{ type_u8, type_bool } }))
    ba := try pool.intern(.{ .tuple = &.{ type_bool, type_u8 } })
    abc := try pool.intern(.{ .tuple = &.{ type_u8, type_bool, type_void } })
    try sup.expect(ab != ba)
    try sup.expect(ab != abc)
}

test "container identity is nominal — the defining node, not the shape" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())

    // Two textually identical (here: both empty) struct definitions are two
    // distinct nodes, hence two distinct types; the same node twice is one.
    // The nodes are arena-allocated, as the parser allocates real ones —
    // stack/const locals could share storage and fake a collision.
    node_a := try arena.allocator().create(ast.StructDef)
    node_a.* = .{ .layout = null, .fields = &.{}, .decls = &.{} }
    node_b := try arena.allocator().create(ast.StructDef)
    node_b.* = .{ .layout = null, .fields = &.{}, .decls = &.{} }
    t_a := try pool.intern(.{ .container = .{ .struct_type = node_a } })
    t_b := try pool.intern(.{ .container = .{ .struct_type = node_b } })
    try sup.expect(t_a != t_b)
    try sup.expectEqual(t_a, try pool.intern(.{ .container = .{ .struct_type = node_a } }))

    // The container kind is part of identity, independent of the address.
    e := try arena.allocator().create(ast.EnumDef)
    e.* = .{ .tag_type = null, .variants = &.{}, .decls = &.{} }
    t_e := try pool.intern(.{ .container = .{ .enum_type = e } })
    try sup.expectEqual(t_e, try pool.intern(.{ .container = .{ .enum_type = e } }))
    try sup.expect(t_e != t_a)
}

test "int values dedup by value; strings by content" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())

    n42 := try pool.intern(.{ .int = 42 })
    try sup.expectEqual(n42, try pool.intern(.{ .int = 42 }))
    n43 := try pool.intern(.{ .int = 43 })
    try sup.expect(n42 != n43)
    neg := try pool.intern(.{ .int = -42 })
    try sup.expect(n42 != neg)

    // Two equal strings at different addresses are one value.
    src2 := try arena.allocator().dupe(u8, "hello")
    s1 := try pool.intern(.{ .string = "hello" })
    s2 := try pool.intern(.{ .string = src2 })
    try sup.expectEqual(s1, s2)
    s3 := try pool.intern(.{ .string = "world" })
    try sup.expect(s1 != s3)
}

test "a dynamic entry round-trips through get" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())

    opt := try pool.intern(.{ .optional = type_bool })
    switch pool.get(opt) {
        .optional => |child| try sup.expectEqual(type_bool, child),
        else => return error.WrongKeyShape,
    }
    n := try pool.intern(.{ .int = 7 })
    switch pool.get(n) {
        .int => |v| try sup.expectEqual(7, v),
        else => return error.WrongKeyShape,
    }
}

// --- evaluator tests ------------------------------------------------------

// Parse `src` and run the evaluator over it; the pool and evaluator are
// backed by the caller's arena and inspectable afterwards.
fn evalSrc(alloc sup.Allocator, pool *mut Pool, src []u8) !Evaluator {
    var p = parser.Parser.init(alloc, src)
    prog := try p.parseProgram()
    var ev = Evaluator.init(alloc, pool)
    try ev.run(prog)
    return ev
}

// Assert the file-scope constant `name` folded to the integer `expected`.
fn expectConstInt(src []u8, name []u8, expected i128) !void {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())
    ev := try evalSrc(arena.allocator(), &pool, src)
    v := ev.lookup(name) orelse return error.ConstNotRecorded
    switch pool.get(v) {
        .int => |n| try sup.expectEqual(expected, n),
        else => return error.NotAnInt,
    }
}

// Assert the file-scope constant `name` evaluated to exactly `expected`.
fn expectConstVid(src []u8, name []u8, expected Vid) !void {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())
    ev := try evalSrc(arena.allocator(), &pool, src)
    v := ev.lookup(name) orelse return error.ConstNotRecorded
    try sup.expectEqual(expected, v)
}

// Assert evaluation of `src` produced no diagnostics.
fn expectEvalClean(src []u8) !void {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())
    ev := try evalSrc(arena.allocator(), &pool, src)
    try sup.expectEqual(0, ev.diags.items.len)
}

// Assert evaluation of `src` produced exactly `n` diagnostics whose message
// contains `frag`.
fn expectEvalDiags(src []u8, frag []u8, n usize) !void {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())
    ev := try evalSrc(arena.allocator(), &pool, src)
    var hits usize = 0
    for d in ev.diags.items {
        if sup.indexOf(u8, d.msg, frag) != null {
            hits += 1
        }
    }
    try sup.expectEqual(n, hits)
}

test "integer literals and arithmetic fold" {
    try expectConstInt("const N = 1 + 2 * 3", "N", 7)
    try expectConstInt("const H = 0xFF", "H", 255)
    try expectConstInt("const B = 0b101", "B", 5)
    try expectConstInt("const Neg = -42", "Neg", -42)
    try expectConstInt("const G = (1 + 2) * 3", "G", 9)
    try expectConstInt("const Q = 7 / 2", "Q", 3)
    try expectConstInt("const R = 7 % 2", "R", 1)
}

test "a reference to an already-evaluated constant folds through" {
    try expectConstInt("const A = 2\nconst B = A * 21", "B", 42)
}

test "a forward reference degrades to unknown, silently" {
    src := "const B = A * 21\nconst A = 2"
    try expectConstVid(src, "B", unknown)
    try expectEvalClean(src)
}

test "comparisons and boolean logic fold" {
    try expectConstVid("const T = 1 < 2", "T", true_value)
    try expectConstVid("const F = 3 == 4", "F", false_value)
    try expectConstVid("const N = !true", "N", false_value)
    try expectConstVid("const A = true && false", "A", false_value)
    try expectConstVid("const O = false || true", "O", true_value)
    try expectConstVid("const E = true == true", "E", true_value)
    // The left side decides where it can, even with an unknown right side.
    try expectConstVid("fn f(x bool) bool {\n    return x\n}\nconst D = false && f(true)", "D", false_value)
}

test "bitwise operators and shifts fold" {
    try expectConstInt("const M = 0xF0 | 0x0F", "M", 255)
    try expectConstInt("const A = 0xFF & 0x0F", "A", 15)
    try expectConstInt("const X = 0xFF ^ 0x0F", "X", 0xF0)
    try expectConstInt("const S = 1 << 10", "S", 1024)
    try expectConstInt("const D = 256 >> 4", "D", 16)
}

test "string constants intern by content" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())
    ev := try evalSrc(arena.allocator(), &pool, "const A = \"flash\"\nconst B = \"flash\"\nconst C = \"other\"")
    va := ev.lookup("A") orelse return error.ConstNotRecorded
    vb := ev.lookup("B") orelse return error.ConstNotRecorded
    vc := ev.lookup("C") orelse return error.ConstNotRecorded
    try sup.expectEqual(va, vb)
    try sup.expect(va != vc)
}

test "builtin type names and type aliases evaluate to type Vids" {
    try expectConstVid("const T = u8", "T", type_u8)
    try expectConstVid("const B = bool", "B", type_bool)
    // An alias of an alias lands on the same Vid.
    try expectConstVid("const T = u8\nconst U = T", "U", type_u8)
}

test "composite type expressions intern structurally through aliases" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())
    ev := try evalSrc(arena.allocator(), &pool, "const T = u8\nconst O1 = ?u8\nconst O2 = ?T\nconst S = []u8\nconst F = *fn(u8) u8\nconst C = *fn(u8) callconv(.c) u8")
    // `?u8` and `?T` (T = u8) are the same type — one Vid.
    o1 := ev.lookup("O1") orelse return error.ConstNotRecorded
    o2 := ev.lookup("O2") orelse return error.ConstNotRecorded
    try sup.expectEqual(o1, o2)
    switch pool.get(o1) {
        .optional => |child| try sup.expectEqual(type_u8, child),
        else => return error.WrongKeyShape,
    }
    // `[]u8` is the slice key on u8.
    s := ev.lookup("S") orelse return error.ConstNotRecorded
    switch pool.get(s) {
        .ptr => |p| {
            try sup.expectEqual(PtrSize.slice, p.size)
            try sup.expectEqual(type_u8, p.elem)
        },
        else => return error.WrongKeyShape,
    }
    // `*fn(u8) u8` is a one-pointer to the fn type.
    f := ev.lookup("F") orelse return error.ConstNotRecorded
    switch pool.get(f) {
        .ptr => |p| {
            switch pool.get(p.elem) {
                .fn_type => |ft| {
                    try sup.expectEqual(1, ft.params.len)
                    try sup.expectEqual(type_u8, ft.ret)
                    try sup.expect(ft.conv == null)
                },
                else => return error.WrongKeyShape,
            }
        },
        else => return error.WrongKeyShape,
    }
    // `*fn(u8) callconv(.c) u8` records the convention — a distinct pointer
    // type from F's, because the pointee fn types differ.
    c := ev.lookup("C") orelse return error.ConstNotRecorded
    try sup.expect(c != f)
    switch pool.get(c) {
        .ptr => |p| {
            switch pool.get(p.elem) {
                .fn_type => |ft| try sup.expectEqualStrings("c", ft.conv.?),
                else => return error.WrongKeyShape,
            }
        },
        else => return error.WrongKeyShape,
    }
}

test "a container definition evaluates to a nominal type" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())
    ev := try evalSrc(arena.allocator(), &pool, "const W = struct {\n    fd i32,\n}\nconst K = enum { a, b }")
    w := ev.lookup("W") orelse return error.ConstNotRecorded
    switch pool.get(w) {
        .container => |c| {
            switch c {
                .struct_type => {},
                else => return error.WrongContainerKind,
            }
        },
        else => return error.WrongKeyShape,
    }
    k := ev.lookup("K") orelse return error.ConstNotRecorded
    try sup.expect(w != k)
}

test "a known if-expression condition selects one arm and prunes the other" {
    // The taken arm folds; the dead arm holds a division by zero that must
    // NOT be diagnosed — downstream never analyzes it either.
    src := "const X = if (true) 1 else 2 / 0"
    try expectConstInt(src, "X", 1)
    try expectEvalClean(src)
    // With an unknown condition both arms are evaluated, so the definite
    // error in either arm IS found.
    try expectEvalDiags("fn f(c bool) usize {\n    x := if (c) 1 else 2 / 0\n    return x\n}", "division by zero", 1)
}

test "division and remainder by a known zero are definite errors" {
    try expectEvalDiags("const D = 1 / 0", "division by zero", 1)
    try expectEvalDiags("const M = 5 % 0", "division by zero", 1)
    // A known-zero divisor under an unknown numerator is still definite.
    try expectEvalDiags("fn f(n usize) usize {\n    return n / 0\n}", "division by zero", 1)
    // A var's initializer is evaluated for definite errors too.
    try expectEvalDiags("var V = 1 / 0", "division by zero", 1)
}

test "'||' on type operands is a definite error" {
    // The motivating shape: two named error sets, merged Zig-style.
    try expectEvalDiags("const AError = error{Bad}\nconst BError = error{Worse}\nconst Both = AError || BError", "cannot merge error sets", 1)
    // Inline error-set literals and any other type operand are the same
    // mistake; one side being a type is enough.
    try expectEvalDiags("const Both = error{A} || error{B}", "cannot merge error sets", 1)
    try expectEvalDiags("const T = u8 || u16", "cannot merge error sets", 1)
    // Boolean `||` is untouched.
    try expectEvalClean("const B = true || false")
}

test "an error-set definition evaluates to a nominal type" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())
    ev := try evalSrc(arena.allocator(), &pool, "const AError = error{Bad}\nconst BError = error{Bad}")
    ae := ev.lookup("AError") orelse return error.ConstNotRecorded
    switch pool.get(ae) {
        .container => |c| {
            switch c {
                .error_set_type => {},
                else => return error.WrongContainerKind,
            }
        },
        else => return error.WrongKeyShape,
    }
    // Identity is the defining node: a textually identical set is a
    // distinct type, exactly as containers behave.
    be := ev.lookup("BError") orelse return error.ConstNotRecorded
    try sup.expect(ae != be)
}

test "out-of-reach folds degrade to unknown without a diagnostic" {
    // i128 overflow — downstream comptime ints are arbitrary-precision, so
    // this is a boundary, never an error.
    overflow := "const Big = 170141183460469231731687303715884105727 + 1"
    try expectConstVid(overflow, "Big", unknown)
    try expectEvalClean(overflow)
    // Signed division rounding is gated behind dedicated builtins downstream.
    try expectEvalClean("const Q = -7 / 2")
    try expectConstVid("const Q = -7 / 2", "Q", unknown)
    // Floats, #-builtins, and member access are out of boundary.
    try expectConstVid("const F = 1.5", "F", unknown)
    try expectEvalClean("const F = 1.5")
    try expectConstVid("const C = #sizeOf(u8)", "C", unknown)
    try expectEvalClean("const C = #sizeOf(u8)")
    // An oversized shift is legal on downstream comptime ints — degrade.
    try expectConstVid("const S = 1 << 200", "S", unknown)
    try expectEvalClean("const S = 1 << 200")
}

test "definite errors inside bodies and containers are found" {
    // A local constant inside a function body.
    try expectEvalDiags("fn f() usize {\n    d := 1 / 0\n    return d\n}", "division by zero", 1)
    // Inside a known-true if body.
    try expectEvalDiags("fn f() {\n    if true {\n        _ = 1 / 0\n    }\n}", "division by zero", 1)
    // A known-FALSE if statement prunes its body, as downstream does.
    try expectEvalClean("fn f() {\n    if false {\n        _ = 1 / 0\n    }\n}")
    // A container-associated constant and a method body are both walked.
    try expectEvalDiags("const W = struct {\n    fd i32,\n\n    const Z = 1 / 0\n\n    fn m(self W) usize {\n        return 2 / 0\n    }\n}", "division by zero", 2)
}

test "a generic application with wrong arity is a definite error" {
    // In value position …
    try expectEvalDiags("fn Box(comptime T type) type {\n    return T\n}\nconst B = Box(u8, u8)", "generic 'Box' expects 1 argument, found 2", 1)
    // … and in type position, through a parameter annotation.
    try expectEvalDiags("fn Pair(comptime A type, comptime B type) type {\n    return A\n}\nfn f(x Pair(u8)) void {\n    _ = x\n}", "generic 'Pair' expects 2 arguments, found 1", 1)
}

test "a generic declared later in the file is checked at an earlier application" {
    try expectEvalDiags("const B = Box(u8, u8)\nfn Box(comptime T type) type {\n    return T\n}", "expects 1 argument, found 2", 1)
}

test "a type parameter wants a type argument; a value parameter wants a value" {
    try expectEvalDiags("fn Box(comptime T type) type {\n    return T\n}\nconst B = Box(5)", "argument 1 to generic 'Box' expects a type, found a value", 1)
    try expectEvalDiags("fn Ring(comptime n usize) type {\n    return u8\n}\nconst R = Ring(u8)", "argument 1 to generic 'Ring' expects a value, found a type", 1)
}

test "a parameter kind resolves through a file-scope type alias" {
    // `MyT` aliases `type` itself, so `T` wants a type argument.
    try expectEvalDiags("const MyT = type\nfn G(comptime T MyT) type {\n    return T\n}\nconst B = G(5)", "expects a type, found a value", 1)
}

test "well-formed generic applications stay silent" {
    try expectEvalClean("fn Box(comptime T type) type {\n    return T\n}\nfn Ring(comptime n usize) type {\n    return u8\n}\nconst B = Box(u8)\nconst C = Box([]u8)\nconst R = Ring(64)\n\nfn f(x Box(u8)) Box(u8) {\n    return x\n}\n\nconst S = struct {\n    b Box(u8),\n}")
}

test "unknown arguments and unresolvable parameter kinds stay silent" {
    // A runtime argument is out of reach — never wrong.
    try expectEvalClean("fn Ring(comptime n usize) type {\n    return u8\n}\nfn f(n usize) void {\n    const R = Ring(n)\n    _ = R\n}")
    // A parameter whose type names another parameter is unresolvable —
    // unchecked, so a value rides where the downstream compiler decides.
    try expectEvalClean("fn Wrap(comptime T type, x T) type {\n    return T\n}\nconst W = Wrap(u8, 5)")
    // Parameter-type resolution cycles never recurse: the re-entered
    // resolution degrades to unchecked. Instantiation then breaks the
    // benign half of the cycle — `A(u8)` evaluates to `u8`, so B's
    // parameter kind resolves after all, and the type argument in A's own
    // signature is a definite kind error (rejected downstream too) —
    // reported once, by the loud signature walk.
    try expectEvalDiags("fn A(comptime x B(u8)) type {\n    return u8\n}\nfn B(comptime x A(u8)) type {\n    return u8\n}\nconst C = A(5)", "argument 1 to generic 'B' expects a value, found a type", 1)
}

test "out-of-scope applications are not checked" {
    // A locally bound name is not the file-level generic (the binding checks
    // own the shadowing rules; this walk only mirrors the visibility).
    try expectEvalClean("fn Box(comptime T type) type {\n    return T\n}\nfn f(Box fn(u8) u8) u8 {\n    return Box(1)\n}")
    // A dotted (imported) generic name is out of reach.
    try expectEvalClean("use pkg\n\nfn f(x pkg.List(u8, u8, u8)) void {\n    _ = x\n}")
    // An ordinary function's call arity is not this walk's business.
    try expectEvalClean("fn add(a u8, b u8) u8 {\n    return a + b\n}\nconst S = add(1)")
}

test "generic applications in annotations are checked" {
    // A binding's type annotation …
    try expectEvalDiags("fn Box(comptime T type) type {\n    return T\n}\nfn f() void {\n    var x Box(u8, u8) = undefined\n    _ = x\n}", "expects 1 argument, found 2", 1)
    // … a container field's type …
    try expectEvalDiags("fn Box(comptime T type) type {\n    return T\n}\nconst S = struct {\n    b Box(u8, u8),\n}", "expects 1 argument, found 2", 1)
    // … and a return type.
    try expectEvalDiags("fn Box(comptime T type) type {\n    return T\n}\nfn f() Box(5) {\n    return undefined\n}", "expects a type, found a value", 1)
}

test "a definite error inside an application's argument is still found" {
    try expectEvalDiags("fn Box(comptime T type) type {\n    return T\n}\nconst B = Box(1 / 0, u8)", "division by zero", 1)
}

test "the same generic over the same arguments is one instance; distinct arguments split" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())
    ev := try evalSrc(arena.allocator(), &pool, "fn List(comptime T type) type {\n    return struct {\n        item T,\n    }\n}\nconst A = List(u8)\nconst B = List(u8)\nconst C = List(u16)")
    va := ev.lookup("A") orelse return error.ConstNotRecorded
    vb := ev.lookup("B") orelse return error.ConstNotRecorded
    vc := ev.lookup("C") orelse return error.ConstNotRecorded
    try sup.expectEqual(va, vb)
    try sup.expect(va != vc)
    // The instance is a nominal type of its own — the instance key, not the
    // body's container node (which would collapse every argument list).
    switch pool.get(va) {
        .instance => |inst| try sup.expectEqual(1, inst.args.len),
        else => return error.WrongKeyShape,
    }
}

test "a type-expression body evaluates to the instance's result type" {
    // `return T` denotes the argument itself.
    try expectConstVid("fn Box(comptime T type) type {\n    return T\n}\nconst B = Box(u8)", "B", type_u8)
    // Parameters bind positionally.
    try expectConstVid("fn Pick(comptime A type, comptime B type) type {\n    return B\n}\nconst P = Pick(u8, bool)", "P", type_bool)
    // `return ?T` lands on the same structural Vid as a spelled `?u8`.
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())
    ev := try evalSrc(arena.allocator(), &pool, "fn Opt(comptime T type) type {\n    return ?T\n}\nconst A = Opt(u8)\nconst B = ?u8")
    va := ev.lookup("A") orelse return error.ConstNotRecorded
    vb := ev.lookup("B") orelse return error.ConstNotRecorded
    try sup.expectEqual(vb, va)
}

test "an instance type is a known type to later kind checks" {
    // `B` folds to the type `Box(u8)` denotes, so passing it where a value
    // parameter is declared is now a definite kind error.
    try expectEvalDiags("fn Box(comptime T type) type {\n    return T\n}\nfn Ring(comptime n usize) type {\n    return u8\n}\nconst B = Box(u8)\nconst R = Ring(B)", "argument 1 to generic 'Ring' expects a value, found a type", 1)
    // A container instance rides as a type argument.
    try expectEvalClean("fn List(comptime T type) type {\n    return struct {\n        item T,\n    }\n}\nfn Box(comptime T type) type {\n    return T\n}\nconst L = List(u8)\nconst B = Box(L)")
}

test "bodies that are not a single return stay unknown, silently" {
    // Two statements.
    two := "fn Two(comptime T type) type {\n    const U = T\n    return U\n}\nconst A = Two(u8)"
    try expectConstVid(two, "A", unknown)
    try expectEvalClean(two)
    // A single return of a non-type result degrades too — no type-mismatch
    // diagnostics here, that stays downstream's call.
    val := "fn N(comptime T type) type {\n    return 5\n}\nconst A = N(u8)"
    try expectConstVid(val, "A", unknown)
    try expectEvalClean(val)
}

test "an unknown argument is never instantiated" {
    // A forward reference is unknown at the application — the instance is
    // not interned, the result stays unknown, nothing is reported.
    src := "const B = Box(Later)\nconst Later = u8\nfn Box(comptime T type) type {\n    return T\n}"
    try expectConstVid(src, "B", unknown)
    try expectEvalClean(src)
}

test "an alias of a generic is a boundary, not an application site" {
    // `L` is env-bound (to unknown), so `L(u8)` is no application — silent.
    src := "fn List(comptime T type) type {\n    return struct {\n        item T,\n    }\n}\nconst L = List\nconst X = L(u8)"
    try expectConstVid(src, "X", unknown)
    try expectEvalClean(src)
}

test "recursive instantiation is capped with one targeted diagnostic" {
    // Direct self-application — the same key re-enters until the cap.
    try expectEvalDiags("fn A(comptime T type) type {\n    return A(T)\n}\nconst X = A(u8)", "generic 'A' exceeds the instantiation depth limit (64)", 1)
    // Growing-argument recursion makes a fresh key per level — also capped.
    try expectEvalDiags("fn G(comptime T type) type {\n    return G(?T)\n}\nconst X = G(u8)", "generic 'G' exceeds the instantiation depth limit (64)", 1)
}

test "the instantiation budget caps a run; memoized hits are free" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    alloc := arena.allocator()
    // Three distinct instances against a budget of two — the third is over.
    var pool = try Pool.init(alloc)
    var p = parser.Parser.init(alloc, "fn Box(comptime T type) type {\n    return T\n}\nconst A = Box(u8)\nconst B = Box(u16)\nconst C = Box(u32)")
    var ev = Evaluator.init(alloc, &pool)
    ev.inst_budget = 2
    try ev.run(try p.parseProgram())
    try sup.expectEqual(1, ev.diags.items.len)
    try sup.expect(sup.indexOf(u8, ev.diags.items[0].msg, "exceeds the instantiation budget (2)") != null)
    // The same application three times costs one instantiation — well
    // inside the same budget, and every alias lands on the same result.
    var pool2 = try Pool.init(alloc)
    var p2 = parser.Parser.init(alloc, "fn Box(comptime T type) type {\n    return T\n}\nconst A = Box(u8)\nconst B = Box(u8)\nconst C = Box(u8)")
    var ev2 = Evaluator.init(alloc, &pool2)
    ev2.inst_budget = 2
    try ev2.run(try p2.parseProgram())
    try sup.expectEqual(0, ev2.diags.items.len)
    try sup.expectEqual(ev2.lookup("A"), ev2.lookup("C"))
}

test "a definite error written in a generic's body reports once despite instantiation" {
    // The loud body walk owns the diagnostic; the (quiet) instance
    // evaluation of `A(u8)` must not duplicate it.
    try expectEvalDiags("fn Box(comptime T type) type {\n    return T\n}\nfn A(comptime T type) type {\n    return Box(T, T)\n}\nconst X = A(u8)", "expects 1 argument, found 2", 1)
}

test "local constants fold and stay scoped to their block" {
    var arena = sup.ArenaAllocator.init(sup.testAlloc)
    defer arena.deinit()
    var pool = try Pool.init(arena.allocator())
    ev := try evalSrc(arena.allocator(), &pool, "fn f() usize {\n    const k = 6 * 7\n    return k\n}")
    // The function frame was popped after the walk — nothing leaks to the
    // file scope.
    try sup.expect(ev.lookup("k") == null)
    try expectEvalClean("fn f() usize {\n    const k = 6 * 7\n    return k\n}")
}