// 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}") }