// Flash sema — native semantic checks over the parsed program. // // Tier 0 leans on Zig's type checker downstream: the emitted source is fully // type-checked when FlashOS builds it, so Flash sema is deliberately thin — it // owns the checks Zig cannot phrase against Flash source lines (bindings, // scopes, mutability), never types. It walks the program with a single scope // stack of `Binding`s — the file-level frame seeded from the `use` / `const` / // `fn` declarations, then a frame pushed per function body and per block — and // *collects* `Diag`s instead of failing on the first error, so one run reports // every problem at once. // // A `Diag` is anchored by a source slice, not a line number. Because every AST // string is a byte-slice into the original source (see ast.zig), a diagnostic's // line and column are recovered from the anchor's address at render time // (`locate`); the AST carries no span bookkeeping. This makes the slice // invariant load-bearing: an anchor must always be a real slice into the source // buffer, never a synthesized string. // // Active checks (the binding/scope/mutability teeth — never types): // * member-access root resolution — every `X.field…` must have a root `X` // that is an import, a top-level declaration, a parameter, or a binding in // scope. A bare identifier (a function passed by name, a direct call) is // not checked: it resolves downstream against the emitted Zig. // * mutability — a store to an immutable bare-identifier target (a `const`/ // `:=` local, a global `const`, a parameter, a capture) is rejected; a // projection (`s.f`, `a[i]`, `p.*`) is left to the downstream type checker. // * unused bindings — a local, parameter, or capture declared and never // referenced is rejected; `_` and a `_ = name` discard are the escapes. // * ignored value — a bare expression statement that only produces a value // (the statement-split hazard) is rejected; effectful and control-flow // statements are exempt. // * redeclaration and shadowing — reusing a name already in scope is rejected // (Flash forbids shadowing outright, Zig-exact). // // After the binding passes, `check` also drives the compile-time evaluator // (eval.zig), which folds constant initializers and contributes the definite // compile-time errors (a division by a known zero) to the same diagnostic // list. Type checking proper stays Zig's job downstream (Tier 0); deeper // typing arrives with the self-hosting work later on the milestone ladder. const std = @import("std"); const ast = @import("ast.zig"); const eval = @import("eval.zig"); // Re-exported so an integration test rooted at this module can parse its // own sources — importing src/parser.zig as a second module root would // place the file in two module graphs at once (a compile error). pub const Parser = @import("parser.zig").Parser; // A collected diagnostic. `anchor` is a slice into the source buffer whose // address *is* the location — `locate` turns it into a line:col at render time. // `note_*` is an optional secondary location (e.g. a prior declaration). pub const Diag = struct { anchor: []const u8, msg: []const u8, note_anchor: ?[]const u8 = null, note_msg: ?[]const u8 = null, }; // A 1-based source position, recovered from an anchor by `locate`. pub const Loc = struct { line: u32, col: u32 }; // Recover the 1-based line and column of `anchor` within `src`. `anchor` MUST // be a slice into `src` (the ast.zig invariant): the byte offset is the pointer // difference, the line is one plus the newline count before it, and the column // counts from the last newline. pub fn locate(src: []const u8, anchor: []const u8) Loc { const off = @intFromPtr(anchor.ptr) - @intFromPtr(src.ptr); std.debug.assert(off <= src.len); var line: u32 = 1; var line_start: usize = 0; // index just past the most recent newline var i: usize = 0; while (i < off) : (i += 1) { if (src[i] == '\n') { line += 1; line_start = i + 1; } } return .{ .line = line, .col = @as(u32, @intCast(off - line_start)) + 1 }; } // Where a name came into scope. Recorded on every binding as the substrate the // binding checks key on — a mutability check on `is_mut`, an unused-binding // check on `origin` — so the file-level origins (import / global / func) are // distinguished from the in-body ones (param / capture / local). Today only // member-access root resolution runs; the provenance is plumbing for the rest. const Origin = enum { import, global, func, param, capture, local }; // One name in scope. The declaring occurrence (`name`) doubles as the // diagnostic anchor. `used` is set when the name is referenced (plumbing for // the unused-binding check); nothing consumes it yet. const Binding = struct { name: []const u8, is_mut: bool, origin: Origin, used: bool = false, }; const Oom = error{OutOfMemory}; // The walk state: an arena for diagnostic text, the collected diagnostics, and // the single scope stack (frame 0 is the file level). `check_roots` is cleared // inside struct-method bodies, whose roots (`self`, the type name, sibling // decls) this thin pass does not model; nothing clears it yet. const Checker = struct { arena: std.mem.Allocator, diags: std.ArrayList(Diag), scope: std.ArrayList(Binding), check_roots: bool = true, frame_base: usize = 0, // start index of the current (innermost) scope frame // Declare a name in the current frame. Flash forbids reusing a name already // visible (Zig-exact): a clash inside the current frame is a redeclaration, // a clash with an enclosing frame is shadowing — both are errors. The // binding is still pushed afterwards so later references resolve. fn declare(self: *Checker, b: Binding) Oom!void { if (self.lookupIndex(b.name)) |idx| { const prior = self.scope.items[idx]; if (idx >= self.frame_base) { try self.reportNote( b.name, try std.fmt.allocPrint(self.arena, "redeclaration of '{s}'", .{b.name}), prior.name, "previously declared here", ); } else { try self.reportNote( b.name, try std.fmt.allocPrint(self.arena, "'{s}' shadows {s}", .{ b.name, originNoun(prior.origin) }), prior.name, "declared here", ); } } try self.scope.append(self.arena, b); } // Innermost-wins lookup by linear back-scan, returning the stack index. fn lookupIndex(self: *Checker, name: []const u8) ?usize { var i = self.scope.items.len; while (i > 0) { i -= 1; if (std.mem.eql(u8, self.scope.items[i].name, name)) return i; } return null; } // The binding for `name`, or null. The pointer is valid only until the next // `declare` (which may reallocate the stack), so callers use it at once. fn lookup(self: *Checker, name: []const u8) ?*Binding { return if (self.lookupIndex(name)) |i| &self.scope.items[i] else null; } fn markUsed(self: *Checker, name: []const u8) void { if (self.lookup(name)) |b| b.used = true; } // Enter a new scope frame at the current stack top, returning the enclosing // frame's base for the matching leaveFrame. fn enterFrame(self: *Checker) usize { const saved = self.frame_base; self.frame_base = self.scope.items.len; return saved; } // Leave the current frame: report any binding in it that was never // referenced, then pop the frame and restore the enclosing one. The // file-level origins (import / global / func) never sit in a popped frame, // so they are never flagged; a `_`-named binding is the explicit discard and // is exempt. A write-only binding counts as used (a read/write split is // later sema work). fn leaveFrame(self: *Checker, saved_base: usize) Oom!void { for (self.scope.items[self.frame_base..]) |b| { const checked = switch (b.origin) { .local, .param, .capture => true, .import, .global, .func => false, }; if (checked and !b.used and !isUnderscore(b.name)) { const kind: []const u8 = switch (b.origin) { .local => "local binding", .param => "parameter", .capture => "capture", else => unreachable, }; try self.report(b.name, try std.fmt.allocPrint(self.arena, "unused {s} '{s}'", .{ kind, b.name })); } } self.scope.items.len = self.frame_base; self.frame_base = saved_base; } // Walk an expression only to mark the bindings it uses, with root resolution // suppressed. Used for type-position and `align(…)` expressions, whose names // resolve downstream (Tier 0) — the walk marks uses but never reports. fn markExpr(self: *Checker, e: ast.Expr) Oom!void { const saved = self.check_roots; self.check_roots = false; defer self.check_roots = saved; try self.checkExpr(e); } // Mark every in-scope binding a type reference uses, so a binding referenced // only in type position — a local `const T = u8` used as `var x T`, or a // `comptime T type` parameter used as another parameter's type — is not // flagged unused. Names resolve downstream, so this only marks, never reports. fn markType(self: *Checker, t: ast.TypeRef) Oom!void { switch (t) { // The first dotted segment is the binding root (`T` in `T`, `pkg` in // `pkg.Type`); a builtin like `u8` resolves to nothing. .name => |n| self.markUsed(firstSegment(n)), .slice, .slice_mut, .many_ptr, .many_ptr_mut, .many_ptr_volatile, .many_ptr_mut_volatile, .ptr, .ptr_mut, .ptr_volatile, .ptr_mut_volatile, .optional, .array_inferred => |inner| try self.markType(inner.*), .slice_sentinel, .slice_sentinel_mut, .many_ptr_sentinel, .many_ptr_sentinel_mut, .array_inferred_sentinel => |sp| { try self.markExpr(sp.sentinel.*); try self.markType(sp.elem.*); }, .array => |arr| { try self.markExpr(arr.len.*); try self.markType(arr.elem.*); }, .array_sentinel => |arr| { try self.markExpr(arr.len.*); try self.markExpr(arr.sentinel.*); try self.markType(arr.elem.*); }, .errunion => |eu| { if (eu.set) |s| try self.markType(s.*); try self.markType(eu.payload.*); }, .fn_type => |ft| { for (ft.params) |p| try self.markType(p); if (ft.ret) |r| try self.markType(r.*); }, .generic => |g| { self.markUsed(firstSegment(g.name)); for (g.args) |a| try self.markExpr(a); }, .tuple => |elems| for (elems) |e| try self.markType(e), } } fn report(self: *Checker, anchor: []const u8, msg: []const u8) Oom!void { try self.diags.append(self.arena, .{ .anchor = anchor, .msg = msg }); } fn reportNote(self: *Checker, anchor: []const u8, msg: []const u8, note_anchor: []const u8, note_msg: []const u8) Oom!void { try self.diags.append(self.arena, .{ .anchor = anchor, .msg = msg, .note_anchor = note_anchor, .note_msg = note_msg, }); } // An assignment whose target is an immutable binding `b`, anchored at the // target occurrence `at`. The message names the binding's kind and the note // points at its declaration with the reason or the fix. fn reportImmutableAssign(self: *Checker, at: []const u8, b: Binding) Oom!void { const kind: []const u8 = switch (b.origin) { .param => "parameter", .capture => "capture", .import, .global, .func, .local => "binding", }; const note: []const u8 = switch (b.origin) { .param => "parameters are immutable", .capture => "captures are immutable", .func => "a function is not an assignable binding", .import => "an import is not an assignable binding", .global, .local => "declared here; use 'var' for a mutable binding", }; try self.reportNote( at, try std.fmt.allocPrint(self.arena, "cannot assign to immutable {s} '{s}'", .{ kind, at }), b.name, note, ); } // Check a function: parameters and the body share one frame (as in Zig), so // a body binding reusing a parameter name is a same-scope redeclaration. A // bodyless `extern fn` prototype has nothing to walk. fn checkFn(self: *Checker, f: ast.FnDecl) Oom!void { const saved = self.enterFrame(); for (f.params) |p| { if (p.name) |n| try self.declare(.{ .name = n, .is_mut = false, .origin = .param }); } // Mark signature uses once the parameters are in scope, so a // `comptime T type` used only as another parameter's or the return type // is not flagged unused. for (f.params) |p| try self.markType(p.type); if (f.ret) |r| try self.markType(r); if (f.body) |body| { for (body) |s| try self.checkStmt(s); try self.leaveFrame(saved); } else { // A bodyless prototype (`extern fn …`) has no body to use its // parameters — they are the C-ABI signature, never unused — so the // frame is discarded without the unused check. self.scope.items.len = self.frame_base; self.frame_base = saved; } } // Descend a container type definition's associated declarations (struct, // enum, or union alike) for binding checks: method bodies are walked with // root resolution suppressed, and a nested type-defining constant recurses // (its own methods are descended). Data fields, variants, field defaults, // and constant values are not walked — they root at sibling decls and // types, resolved downstream (Tier 0). fn checkContainerDecls(self: *Checker, decls: []const ast.ContainerDecl) Oom!void { const saved = self.check_roots; self.check_roots = false; defer self.check_roots = saved; for (decls) |decl| switch (decl) { .method => |m| try self.checkFn(m), .constant => |c| try self.descendTypeDef(c.value), .use_import => {}, }; } // Mark the bindings a container definition's data shape uses — field and // variant payload types, field defaults, and explicit discriminants — so a // binding referenced only there (a generic's `return struct { item T }`) is // not flagged unused. Mark-only: these positions also root at sibling decls // and type names this pass does not model, resolved downstream (Tier 0). fn markContainerShape(self: *Checker, x: ast.Expr) Oom!void { switch (x) { .struct_def => |sd| for (sd.fields) |f| { try self.markType(f.type); if (f.default) |d| try self.markExpr(d); }, .enum_def => |ed| for (ed.variants) |v| { if (v.value) |val| try self.markExpr(val.*); }, .union_def => |ud| for (ud.variants) |v| { if (v.payload) |p| try self.markType(p); }, else => {}, } } // A top-level or associated constant's value is not name-checked (its roots // are siblings and type names, resolved downstream), but when the value is a // container type definition its method bodies are descended. fn descendTypeDef(self: *Checker, value: ast.Expr) Oom!void { switch (value) { .struct_def => |sd| try self.checkContainerDecls(sd.decls), .enum_def => |ed| try self.checkContainerDecls(ed.decls), .union_def => |ud| try self.checkContainerDecls(ud.decls), else => {}, } } // A nested block (an `if`/`while`/`for` body, a labeled block, the top-level // comptime block) with zero or more leading captures. The captures and the // block's own bindings live in one frame, popped on exit, so a name declared // in the block is invisible to a sibling block or to code after it. fn checkBlock(self: *Checker, captures: []const []const u8, stmts: []const ast.Stmt) Oom!void { const saved = self.enterFrame(); // A `_` capture (a `for _ in …` discard) binds nothing. for (captures) |cap| { if (isUnderscore(cap)) continue; try self.declare(.{ .name = cap, .is_mut = false, .origin = .capture }); } for (stmts) |s| try self.checkStmt(s); try self.leaveFrame(saved); } fn checkStmt(self: *Checker, s: ast.Stmt) Oom!void { switch (s) { .discard => |x| try self.checkExpr(x), .expr => |x| { try self.checkExpr(x); // A bare expression statement whose top node yields a value but // has no effect is almost always a mistake: most often a // continuation line a leading `-`/`&` split into its own // statement, or a value that wants an explicit `_ =` discard. // Effectful and control-flow shapes (a call, `try`/`catch`, a // statement `if`/`switch`, `break`/`return`, …) are exempt. if (!stmtExprAllowed(x)) { if (firstLexeme(x)) |anchor| try self.report( anchor, "expression value is ignored; discard it with '_ = expr', or end the previous line with its operator to continue it", ); } }, // A binding's value is checked before the name is declared, so a // binding cannot refer to itself. Its type and `align(…)` reference // names too (a local type alias, a length constant) — marked as uses // so a binding used only there is not flagged unused. .bind => |b| { try self.checkExpr(b.value); if (b.type) |t| try self.markType(t); if (b.align_expr) |ae| try self.markExpr(ae); try self.declare(.{ .name = b.name, .is_mut = b.is_mut, .origin = .local }); }, // A destructuring bind: the value is checked first (no // self-reference, as with .bind), then every non-`_` name is // declared in the current frame — the dup/shadow rules unchanged. .destructure => |d| { try self.checkExpr(d.value); for (d.names) |maybe| if (maybe) |name| { try self.declare(.{ .name = name, .is_mut = d.is_mut, .origin = .local }); }; }, .assign => |a| { try self.checkExpr(a.target); try self.checkExpr(a.value); // Mutability — only a bare-identifier target is checked. A // projection (`s.f`, `a[i]`, `p.*`) turns on pointee/aggregate // mutability this thin pass has no types for, so Zig owns those // downstream (Tier 0). An ident that names no binding resolves // downstream too, so an absent lookup is silently skipped. switch (a.target) { .ident => |name| if (self.lookup(name)) |b| { if (!b.is_mut) try self.reportImmutableAssign(name, b.*); }, else => {}, } }, // A destructuring assignment: every target is checked as a use and // — when it is a bare identifier — for mutability, exactly as the // single assign above; projections resolve downstream the same way. .destructure_assign => |da| { for (da.targets) |t| { try self.checkExpr(t); switch (t) { .ident => |name| if (self.lookup(name)) |b| { if (!b.is_mut) try self.reportImmutableAssign(name, b.*); }, else => {}, } } try self.checkExpr(da.value); }, .if_stmt => |iff| { try self.checkExpr(iff.cond); // An optional capture is in scope for the matched body only. if (iff.capture) |cap| { try self.checkBlock(&.{cap}, iff.body); } else { try self.checkBlock(&.{}, iff.body); } // The else arm's error capture (`else |err|`) is in scope for // that arm only. if (iff.else_body) |eb| { if (iff.else_capture) |cap| { try self.checkBlock(&.{cap}, eb); } else { try self.checkBlock(&.{}, eb); } } }, .while_stmt => |w| { try self.checkExpr(w.cond); // An optional payload capture is in scope for the body only. if (w.capture) |cap| { try self.checkBlock(&.{cap}, w.body); } else { try self.checkBlock(&.{}, w.body); } // The loop else arm; its error capture (`else |err|`) is in // scope for that arm only. if (w.else_body) |eb| { if (w.else_capture) |cap| { try self.checkBlock(&.{cap}, eb); } else { try self.checkBlock(&.{}, eb); } } }, .for_stmt => |fr| { try self.checkExpr(fr.iter); if (fr.range_hi) |hi| try self.checkExpr(hi); // The capture name(s) — element, and the optional index — are in // scope for the body only. try self.checkBlock(fr.captures, fr.body); // The loop else arm — capture-less, its own scope. if (fr.else_body) |eb| try self.checkBlock(&.{}, eb); }, .defer_stmt => |inner| try self.checkStmt(inner.*), .errdefer_stmt => |inner| try self.checkStmt(inner.*), // The block forms open their own scope, like any `{ … }` body. .defer_block => |stmts| try self.checkBlock(&.{}, stmts), .errdefer_block => |stmts| try self.checkBlock(&.{}, stmts), } } fn checkExpr(self: *Checker, x: ast.Expr) Oom!void { switch (x) { .int, .float, .string, .multiline_str, .char, .value_word => {}, // A bare identifier is not a member root: it resolves downstream, so // it never errors here — but mark it used if it names a binding. .ident => |name| self.markUsed(name), .member => |m| { try self.checkExpr(m.base.*); // Resolve the chain's root exactly once, at the innermost member // (the one whose base holds no further member). Any enclosing // member shares the same root, so resolving there too would // double-report it now that diagnostics are collected, not bailed. if (!spineHasMember(m.base.*)) { if (rootIdent(m.base.*)) |root| { if (self.lookup(root)) |b| { // A member root is a use even where roots are not // reported (inside a struct method), so mark it // regardless of `check_roots`. b.used = true; } else if (self.check_roots) { try self.report(root, try std.fmt.allocPrint( self.arena, "unknown name '{s}': not an import, parameter, or binding in scope", .{root}, )); } } } }, // `p.*` / `opt.?` resolve at their root, like any other postfix form. .deref => |d| try self.checkExpr(d.*), .optional_unwrap => |u| try self.checkExpr(u.*), .call => |c| { try self.checkExpr(c.callee.*); for (c.args) |a| try self.checkExpr(a); }, .index => |ix| { try self.checkExpr(ix.base.*); try self.checkExpr(ix.index.*); }, .slice => |s| { try self.checkExpr(s.base.*); try self.checkExpr(s.lo.*); if (s.hi) |hi| try self.checkExpr(hi.*); // The sentinel is an ordinary expression (e.g. a `pkg.NUL` // constant), so a bad member root in it is still resolved. if (s.sentinel) |sen| try self.checkExpr(sen.*); }, .builtin_call => |b| { for (b.args) |a| try self.checkExpr(a); }, .unary => |u| try self.checkExpr(u.operand.*), .binary => |b| { try self.checkExpr(b.lhs.*); try self.checkExpr(b.rhs.*); }, .struct_lit => |fields| { for (fields) |f| try self.checkExpr(f.value); }, // A typed initializer `Type{ .x = v }`: the type prefix is an // ordinary expression (a bad member root in a `pkg.Type` is caught), // and each field value resolves like an anonymous literal's. .typed_lit => |tl| { try self.checkExpr(tl.type.*); for (tl.fields) |f| try self.checkExpr(f.value); }, // A container definition's data fields/variants and their types are // downstream concerns — though the bindings they reference are // marked used, so a generic's `return struct { item T }` does not // flag `T` — and its method bodies ARE descended for binding checks // (mutability, …) — with root resolution off, since a method roots // at the container's type name, `self`, and sibling decls this thin // pass does not model. Reached here when a container type is // defined as a local or nested value; the top-level `const T = // struct …` form is descended directly from `check`. .struct_def => |sd| { try self.markContainerShape(x); try self.checkContainerDecls(sd.decls); }, .enum_def => |ed| { try self.markContainerShape(x); try self.checkContainerDecls(ed.decls); }, .union_def => |ud| { try self.markContainerShape(x); try self.checkContainerDecls(ud.decls); }, // A composite type in value position (`return ?T` in a generic's // body) references bindings in type position only — mark them, so // the parameter spelt only there is not flagged unused. .type_lit => |t| try self.markType(t.*), // An inferred enum literal `.red` is a bare tag; an `error.Name` / // `error{ … }` is a pure declaration. Neither is a name reference. .enum_lit, .error_lit, .error_set => {}, .group => |g| try self.checkExpr(g.*), .if_expr => |iff| { try self.checkExpr(iff.cond.*); try self.checkExpr(iff.then.*); try self.checkExpr(iff.else_.*); }, // A switch resolves its subject, every pattern bound (a pattern may be // a constant reference like `pkg.MAX`), and every prong body. A // `=> |x|` capture binds the active variant's payload in the prong // body only. .switch_expr => |sw| { try self.checkExpr(sw.subject.*); for (sw.prongs) |prong| { for (prong.patterns) |p| { try self.checkExpr(p.lo); if (p.hi) |hi| try self.checkExpr(hi); } if (prong.capture) |cap| { const saved = self.enterFrame(); try self.declare(.{ .name = cap, .is_mut = false, .origin = .capture }); try self.checkExpr(prong.body); try self.leaveFrame(saved); } else { try self.checkExpr(prong.body); } } }, .try_expr => |t| try self.checkExpr(t.*), .catch_expr => |c| { try self.checkExpr(c.lhs.*); // `catch |e| handler` binds the error in scope for the handler only. if (c.capture) |cap| { const saved = self.enterFrame(); try self.declare(.{ .name = cap, .is_mut = false, .origin = .capture }); try self.checkExpr(c.handler.*); try self.leaveFrame(saved); } else { try self.checkExpr(c.handler.*); } }, // Inline assembly. The template and constraint strings are opaque // assembler/LLVM text, but the operand bodies and the clobber // expression are ordinary expressions — a bad member root in them is // still resolved. A `(-> T)` output body is a type reference, resolved // downstream. .asm_expr => |a| { try self.checkExpr(a.template.*); for (a.outputs) |op| switch (op.body) { .expr => |e| try self.checkExpr(e), .ret_type => {}, }; for (a.inputs) |op| switch (op.body) { .expr => |e| try self.checkExpr(e), .ret_type => {}, }; if (a.clobbers) |c| try self.checkExpr(c.*); }, // A labeled block runs in the enclosing scope plus its own bindings; // the label is a break target, not a binding. .block_expr => |blk| try self.checkBlock(&.{}, blk.body), // A labelled break may carry a value (`break :blk v`) — resolve it. .brk => |b| if (b.value) |v| try self.checkExpr(v.*), .cont => {}, .ret => |maybe| if (maybe) |vals| for (vals) |v| try self.checkExpr(v), } } }; pub fn check(arena: std.mem.Allocator, program: ast.Program) Oom![]Diag { var c = Checker{ .arena = arena, .diags = .empty, .scope = .empty, }; // Seed the file-level frame (frame 0): every `use` binds its alias (or the // bare module name), and every top-level declaration — a `const` (including a // type like `union(enum)`) or a `fn` — is referenceable by name. Declared in // a first pass so a body may reference a sibling declared later in the file. for (program.items) |item| { switch (item) { .use_decl => |u| try c.declare(.{ .name = u.alias orelse u.module, .is_mut = false, .origin = .import }), .const_decl => |d| try c.declare(.{ .name = d.name, .is_mut = d.is_mut, .origin = .global }), .fn_decl => |f| try c.declare(.{ .name = f.name, .is_mut = false, .origin = .func }), // A test block declares no binding — its string name is not an // identifier and cannot be referenced. .link_decl, .comptime_block, .test_decl => {}, } } for (program.items) |item| { switch (item) { .fn_decl => |f| try c.checkFn(f), // A top-level `comptime { … }` block's body is checked like a // function body — against the file's ambient frame, with no // parameters of its own. .comptime_block => |stmts| try c.checkBlock(&.{}, stmts), // A test block's body is checked like a function body — against // the file's ambient frame, with no parameters of its own. .test_decl => |t| try c.checkBlock(&.{}, t.body), // A top-level const's value is not name-checked, but a `struct` type // definition's method bodies are descended for binding checks. .const_decl => |d| try c.descendTypeDef(d.value), else => {}, } } // The compile-time evaluator (eval.zig) runs after the binding passes: // it folds the constant initializers it can reach and reports the // definite compile-time errors (a division by a known zero) at the Flash // source line; everything outside its boundary stays `unknown` — silent, // checked downstream as before. Its diagnostics share the Diag shape and // join the one collected list. var pool = try eval.Pool.init(arena); var ev = eval.Evaluator.init(arena, &pool); try ev.run(program); for (ev.diags.items) |d| { try c.diags.append(arena, .{ .anchor = d.anchor, .msg = d.msg, .note_anchor = d.note_anchor, .note_msg = d.note_msg, }); } return c.diags.toOwnedSlice(arena); } // The leftmost identifier a chain bottoms out at, or null if it bottoms out at a // literal (a member/call on an int or string is not a name reference). fn rootIdent(x: ast.Expr) ?[]const u8 { return switch (x) { .ident => |i| i, .member => |m| rootIdent(m.base.*), .call => |c| rootIdent(c.callee.*), .index => |ix| rootIdent(ix.base.*), .slice => |s| rootIdent(s.base.*), .deref => |d| rootIdent(d.*), // `p.*.field` roots at `p` .optional_unwrap => |u| rootIdent(u.*), // `opt.?.field` roots at `opt` .unary => |u| rootIdent(u.operand.*), .group => |g| rootIdent(g.*), .try_expr => |t| rootIdent(t.*), .int, .float, .string, .multiline_str, .char, .value_word, .builtin_call, .binary, .struct_lit, .typed_lit, .type_lit, .enum_lit, .error_lit, .error_set, .block_expr, .struct_def, .enum_def, .union_def, .catch_expr, .if_expr, .switch_expr, .asm_expr, .brk, .cont, .ret => null, }; } // May this expression stand alone as a statement? True for the effectful and // control-flow shapes — a call, a `#builtin` call, `try`/`catch`, inline asm, a // `break`/`continue`/`return`, a statement `if`/`switch`, a labeled block, the // `unreachable` assertion, and the anchorless `.{}` literal. Every other shape // yields a value that, as a bare statement, is silently dropped. fn stmtExprAllowed(x: ast.Expr) bool { return switch (x) { .call, .builtin_call, .try_expr, .catch_expr, .asm_expr, .brk, .cont, .ret, .if_expr, .switch_expr, .block_expr, .struct_lit => true, .value_word => |w| std.mem.eql(u8, w, "unreachable"), else => false, }; } // The leftmost source slice of an expression — its diagnostic anchor. Descends // the access/operator spine to the leftmost stored lexeme. Null when no single // stored slice exists (an empty `.{}`, a type-in-value-position); the caller // skips the diagnostic rather than anchor it nowhere, as with `struct_lit`. fn firstLexeme(x: ast.Expr) ?[]const u8 { return switch (x) { .int, .float, .string, .char, .ident, .value_word, .enum_lit, .error_lit => |s| s, .multiline_str => |lines| if (lines.len > 0) lines[0] else null, .error_set => |names| if (names.len > 0) names[0] else null, .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, // the prefix operator is the leftmost token .group => |g| firstLexeme(g.*), .try_expr => |t| firstLexeme(t.*), .typed_lit => |tl| firstLexeme(tl.type.*), .struct_def => |sd| if (sd.fields.len > 0) sd.fields[0].name else null, .enum_def => |ed| if (ed.variants.len > 0) ed.variants[0].name else null, .union_def => |ud| if (ud.variants.len > 0) ud.variants[0].name else null, // Allowed-as-statement shapes never reach here; `.{}` and a bare type // have no single stored leftmost slice. .struct_lit, .type_lit, .builtin_call, .catch_expr, .if_expr, .switch_expr, .block_expr, .asm_expr, .brk, .cont, .ret => null, }; } // The first dotted segment of a (possibly qualified) type name — the binding // root: `T` from `T`, `pkg` from `pkg.Type`. fn firstSegment(name: []const u8) []const u8 { return if (std.mem.indexOfScalar(u8, name, '.')) |dot| name[0..dot] else name; } fn isUnderscore(name: []const u8) bool { return std.mem.eql(u8, name, "_"); } // The noun naming a shadowed binding's kind, for the shadowing diagnostic. fn originNoun(o: Origin) []const u8 { return switch (o) { .param => "a parameter", .capture => "a capture", .local => "a local binding", .global => "a file-scope binding", .import => "an import", .func => "a function", }; } // Does the spine of `x` (the access path `rootIdent` peels) pass through a // member node? When it does, a deeper member already resolves the shared root, // so the enclosing member must not resolve it again. fn spineHasMember(x: ast.Expr) bool { return switch (x) { .member => true, .call => |c| spineHasMember(c.callee.*), .index => |ix| spineHasMember(ix.base.*), .slice => |s| spineHasMember(s.base.*), .deref => |d| spineHasMember(d.*), .optional_unwrap => |u| spineHasMember(u.*), .unary => |u| spineHasMember(u.operand.*), .group => |g| spineHasMember(g.*), .try_expr => |t| spineHasMember(t.*), else => false, }; } // --- tests --------------------------------------------------------------- const testing = std.testing; // Parse and check `src`, asserting no diagnostics. On failure the collected // diagnostics are printed for the test log. fn expectClean(src: []const u8) !void { var a = std.heap.ArenaAllocator.init(testing.allocator); defer a.deinit(); const arena = a.allocator(); var p = Parser.init(arena, src); const program = try p.parseProgram(); const diags = try check(arena, program); if (diags.len != 0) { std.debug.print("expected a clean check, got {d} diagnostic(s):\n", .{diags.len}); for (diags) |d| { const loc = locate(src, d.anchor); std.debug.print(" {d}:{d}: {s}\n", .{ loc.line, loc.col, d.msg }); } return error.UnexpectedDiag; } } // Parse and check `src`, asserting at least one diagnostic whose message // contains `frag` lands at `line`:`col`. fn expectDiag(src: []const u8, frag: []const u8, line: u32, col: u32) !void { var a = std.heap.ArenaAllocator.init(testing.allocator); defer a.deinit(); const arena = a.allocator(); var p = Parser.init(arena, src); const program = try p.parseProgram(); const diags = try check(arena, program); for (diags) |d| { if (std.mem.indexOf(u8, d.msg, frag) != null) { const loc = locate(src, d.anchor); if (loc.line == line and loc.col == col) return; } } std.debug.print("expected a diagnostic containing '{s}' at {d}:{d}; got {d}:\n", .{ frag, line, col, diags.len }); for (diags) |d| { const loc = locate(src, d.anchor); std.debug.print(" {d}:{d}: {s}\n", .{ loc.line, loc.col, d.msg }); } return error.DiagNotFound; } // Parse and check `src`, asserting exactly `n` diagnostics. fn expectDiagCount(src: []const u8, n: usize) !void { var a = std.heap.ArenaAllocator.init(testing.allocator); defer a.deinit(); const arena = a.allocator(); var p = Parser.init(arena, src); const program = try p.parseProgram(); const diags = try check(arena, program); try testing.expectEqual(n, diags.len); } test "locate recovers 1-based line and column from an anchor" { const src = "abc\ndef\nghij"; try testing.expectEqual(Loc{ .line = 1, .col = 1 }, locate(src, src[0..3])); // "abc" try testing.expectEqual(Loc{ .line = 2, .col = 1 }, locate(src, src[4..7])); // "def" try testing.expectEqual(Loc{ .line = 3, .col = 3 }, locate(src, src[10..12])); // "ij" in "ghij" } test "imported module, parameter, and binding roots all resolve" { try expectClean( \\use flibc \\ \\export fn main(_ usize, _ argv) noreturn { \\ msg := "hi" \\ _ = flibc.sys.write_fd(1, msg.ptr, msg.len) \\ flibc.exit() \\} ); } test "a use alias resolves under its alias name" { try expectClean( \\use console_ui as ui \\ \\fn f() { \\ ui.screen.clear() \\} ); } test "a member root that is no import, param, or binding is rejected" { try expectDiag( \\fn f() { \\ _ = nope.sys.write() \\} , "unknown name 'nope'", 2, 9); } test "two unresolved roots are both reported, not bailed on the first" { try expectDiagCount( \\fn f() { \\ _ = nope.a() \\ _ = also.b() \\} , 2); } test "a binding from an inner block does not leak past it" { // `x` is bound inside the if-body; using `x` as a member root after the // block must fail, proving block-locals are popped on exit. try expectDiag( \\fn f(n usize) { \\ if n > 0 { \\ x := n \\ } \\ _ = x.field \\} , "unknown name 'x'", 5, 9); } test "a for-loop capture resolves as a member root in the body" { try expectClean( \\fn f(xs []u8) { \\ for e in xs { \\ _ = e.len \\ } \\} ); } test "a bad member root inside a loop body is rejected" { try expectDiag( \\fn f(n usize) { \\ while n > 0 { \\ _ = nope.x() \\ } \\} , "unknown name 'nope'", 3, 13); } test "an optional-capture if binds its capture as a member root in the body" { try expectClean( \\use pwfile \\ \\fn f(xs []u8) { \\ if pwfile.lookup(xs) |entry| { \\ _ = entry.user \\ } \\} ); } test "an if capture does not leak past the if body" { try expectDiag( \\use pwfile \\ \\fn f(xs []u8) { \\ if pwfile.lookup(xs) |entry| { \\ _ = entry \\ } \\ _ = entry.user \\} , "unknown name 'entry'", 7, 9); } test "a catch error capture resolves in the handler only" { try expectClean( \\use sys \\ \\fn f() { \\ _ = sys.run() catch |e| e.code() \\} ); } test "a catch handler's capture does not leak to a sibling statement" { try expectDiag( \\use sys \\ \\fn f() { \\ _ = sys.run() catch |e| e.code() \\ _ = e.code() \\} , "unknown name 'e'", 5, 9); } test "a catch capture resolves inside a recovery block" { try expectClean( \\use sys \\ \\fn f() { \\ sys.run() catch |e| { \\ _ = e.code() \\ } \\} ); } test "an unused capture on a catch recovery block is rejected" { try expectDiag( \\fn f() { \\ run() catch |e| {} \\} , "unused capture 'e'", 2, 18); } test "a bad member root inside a defer is rejected" { try expectDiag( \\fn f() { \\ defer _ = nope.close() \\} , "unknown name 'nope'", 2, 15); } test "struct and enum definitions resolve without false references" { // Field types (`flibc.Dirent`) are type references, not member-access roots, // and variant names are declarations — neither should trip name resolution. try expectClean( \\use flibc \\ \\const Entry = struct { \\ info flibc.Dirent, \\ kind u8, \\} \\const Kind = enum(u8) { file, dir } ); } test "error origination and error sets do not trip name resolution" { // `error.X` is an origination (a bare error name, not a member-access root) // and `error{ … }` is a set definition — neither resolves against the name // environment, so a function originating an error resolves clean. try expectClean( \\const AllocError = error{ OutOfMemory, Overflow } \\ \\fn fail() AllocError!void { \\ return error.OutOfMemory \\} ); } test "a struct method body does not trip name resolution" { // Method bodies inside a struct are checked downstream by Zig, not here: // they root at the struct's type name and sibling decls, which this thin // pass does not model. The definition must therefore resolve clean — no // false diagnostic from `self`, the type name, or the sibling constant. try expectClean( \\use flibc \\ \\const Writer = struct { \\ fd i32, \\ \\ const STDOUT i32 = 1 \\ \\ fn flush(self Writer) { \\ _ = flibc.sys.fsync(self.fd) \\ } \\} ); } test "a top-level declaration resolves as a member-access root" { // A top-level `const` (here a union type) is in scope everywhere in the file, // so `Action.eof` resolves the same way an imported module's member would — // no false diagnostic from a free function referencing a sibling type. try expectClean( \\const Action = union(enum) { \\ none, \\ eof, \\} \\ \\fn classify(n usize) Action { \\ return if (n == 0) Action.eof else Action.none \\} ); } test "a named struct-literal field value is still resolved" { // The value side of `.field = value` is a real expression: a bad member root // there must still be rejected. try expectDiag( \\fn f() { \\ _ = .{ .x = nope.value } \\} , "unknown name 'nope'", 2, 17); } test "a switch prong capture resolves as a member root in its body" { // A `=> |x|` capture binds the variant payload for the prong body, so // `x.field` there resolves — like a `catch |e|` handler. try expectClean( \\use sys \\ \\fn f() { \\ switch sys.poll() { \\ .ready => |r| r.consume(), \\ else => {}, \\ } \\} ); } test "a switch prong capture does not leak past the switch" { try expectDiag( \\use sys \\ \\fn f() { \\ switch sys.poll() { \\ .ready => |r| r.consume(), \\ else => {}, \\ } \\ _ = r.consume() \\} , "unknown name 'r'", 8, 9); } // --- mutability: assign-to-immutable ------------------------------------- test "assigning to a const local is rejected" { try expectDiag( \\fn f() { \\ const x = 1 \\ x = 2 \\} , "cannot assign to immutable binding 'x'", 3, 5); } test "a compound assignment to a const local is rejected" { try expectDiag( \\fn f() { \\ const x = 1 \\ x += 1 \\} , "cannot assign to immutable binding 'x'", 3, 5); } test "assigning to a parameter is rejected" { try expectDiag( \\fn f(n usize) { \\ n = 1 \\} , "cannot assign to immutable parameter 'n'", 2, 5); } test "assigning to a for-loop capture is rejected" { try expectDiag( \\fn f(xs []u8) { \\ for e in xs { \\ e = 1 \\ } \\} , "cannot assign to immutable capture 'e'", 3, 9); } test "assigning to an if-capture is rejected" { try expectDiag( \\fn f(o ?u8) { \\ if o |x| { \\ x = 1 \\ } \\} , "cannot assign to immutable capture 'x'", 3, 9); } test "assigning to a global const is rejected" { try expectDiag( \\const N = 1 \\ \\fn f() { \\ N = 2 \\} , "cannot assign to immutable binding 'N'", 4, 5); } test "assigning to a var local is clean" { try expectClean( \\fn f() { \\ var x = 1 \\ x = 2 \\} ); } test "assigning to a global var is clean" { try expectClean( \\var N = 1 \\ \\fn f() { \\ N = 2 \\} ); } test "a projection target is not checked for mutability" { // `s.f`, `a[i]`, `p.*` turn on pointee/aggregate mutability this pass has no // types for, so they are left to the downstream checker, not flagged here — // even though `s`, `a`, `p` are all immutable parameters. try expectClean( \\fn f(s S, a []u8, p *u8) { \\ s.field = 1 \\ a[0] = 2 \\ p.* = 3 \\} ); } test "a struct method body is checked for assign-to-immutable" { // Proves method bodies are descended: the immutable parameter `n` is caught, // while the struct's type name `W` and the receiver `self` (whose roots this // pass does not model) raise no false diagnostic. try expectDiag( \\const W = struct { \\ fd i32, \\ \\ fn bump(self W, n i32) { \\ n = n + 1 \\ } \\} , "cannot assign to immutable parameter 'n'", 5, 9); } test "a projection target inside a method is left to the downstream checker" { try expectClean( \\const W = struct { \\ fd i32, \\ \\ fn set(self *mut W, v i32) { \\ self.fd = v \\ } \\} ); } test "enum and union method bodies are descended like struct methods" { // The container-decl descent is one path for all three containers: the // immutable parameter `n` inside an enum method is caught … try expectDiag( \\const Color = enum { \\ red, \\ \\ fn next(n i32) i32 { \\ n = n + 1 \\ return n \\ } \\} , "cannot assign to immutable parameter 'n'", 5, 9); // … and a well-formed union method (self-reference, sibling roots this // pass does not model) raises no false diagnostic. try expectClean( \\const Tok = union(enum) { \\ eof, \\ int usize, \\ \\ fn width(self Tok) usize { \\ return helpers.width(self) \\ } \\} ); } // --- unused bindings ----------------------------------------------------- test "an unused local binding is rejected" { try expectDiag( \\fn f() { \\ x := 1 \\} , "unused local binding 'x'", 2, 5); } test "an unused parameter is rejected" { try expectDiag( \\fn f(n usize) { \\} , "unused parameter 'n'", 1, 6); } test "an unused for-loop capture is rejected" { try expectDiag( \\fn f(xs []u8) { \\ for e in xs { \\ } \\} , "unused capture 'e'", 2, 9); } test "an unused for-loop index capture is rejected" { try expectDiag( \\fn f(xs []u8) { \\ for x, i in xs { \\ _ = x \\ } \\} , "unused capture 'i'", 2, 12); } test "an unused catch capture is rejected" { try expectDiag( \\fn f() { \\ _ = run() catch |e| 0 \\} , "unused capture 'e'", 2, 22); } test "an unused switch-prong capture is rejected" { try expectDiag( \\fn f() { \\ switch poll() { \\ .ready => |r| consume(), \\ else => {}, \\ } \\} , "unused capture 'r'", 3, 20); } test "a discarded local counts as used" { try expectClean( \\fn f() { \\ x := 1 \\ _ = x \\} ); } test "a written-but-unread local counts as used" { try expectClean( \\fn f() { \\ var x = 0 \\ x = 5 \\} ); } test "an underscore for-capture binds nothing and is exempt" { try expectClean( \\fn f(n usize) { \\ for _ in 0..n { \\ } \\} ); } test "an underscore index capture is exempt" { try expectClean( \\fn f(xs []u8) { \\ for x, _ in xs { \\ _ = x \\ } \\} ); } test "an underscore parameter is exempt" { try expectClean( \\fn f(_ usize) { \\} ); } test "a binding used only in type position is not unused" { // `T` is referenced only as `x`'s type; the type walker marks it used so it // is not falsely flagged. try expectClean( \\fn f() { \\ const T = u8 \\ var x T = 0 \\ _ = x \\} ); } test "a comptime type parameter used only in the signature is not unused" { try expectClean( \\fn id(comptime T type, x T) T { \\ return x \\} ); } test "a bodyless extern prototype's parameters are exempt from the unused check" { try expectClean( \\extern fn exit(code i32) noreturn ); } test "a parameter used only in a returned type expression is not unused" { // `?T` in value position is a type_lit; the mark-only type walk reaches it, // so a generic whose parameter is spelt only there is not falsely flagged. try expectClean( \\fn Opt(comptime T type) type { \\ return ?T \\} ); // An array length is an expression inside the type — also marked. try expectClean( \\fn Ring(comptime n usize) type { \\ return [n]u8 \\} ); } test "a parameter used only in a returned container's data shape is not unused" { // A struct field's type, a union variant's payload, and an enum variant's // explicit discriminant are downstream concerns, but the bindings they // reference are marked used. try expectClean( \\fn List(comptime T type) type { \\ return struct { \\ item T, \\ } \\} ); try expectClean( \\fn Either(comptime A type, comptime B type) type { \\ return union(enum) { \\ left A, \\ right B, \\ } \\} ); try expectClean( \\fn Tag(comptime base usize) type { \\ return enum(usize) { \\ first = base, \\ } \\} ); } // --- ignored value / the statement-split hazard -------------------------- test "a continuation line split by a leading '-' is rejected" { // The hazard: `x := a` then `- b` parses as two statements, not `x := a - b`. // The orphaned `- b` is a value with no effect, anchored at the `-`. try expectDiag( \\fn f(a usize, b usize) usize { \\ x := a \\ - b \\ return x \\} , "expression value is ignored", 3, 5); } test "a leading '&' statement is rejected" { try expectDiag( \\fn f(p usize) { \\ &p \\} , "expression value is ignored", 2, 5); } test "a bare identifier statement is rejected" { try expectDiag( \\fn f(n usize) { \\ n \\} , "expression value is ignored", 2, 5); } test "a bare comparison statement is rejected" { try expectDiag( \\fn f(a usize, b usize) { \\ a == b \\} , "expression value is ignored", 2, 5); } test "a bare member access statement is rejected" { try expectDiag( \\use pkg \\ \\fn f() { \\ pkg.field \\} , "expression value is ignored", 4, 5); } test "an ignored 'orelse' value statement is rejected" { try expectDiag( \\fn f(o ?usize) { \\ o orelse return \\} , "expression value is ignored", 2, 5); } test "a bare call statement is allowed" { try expectClean( \\fn f() { \\ doThing() \\} ); } test "a 'try' statement is allowed" { try expectClean( \\fn f() !void { \\ try doThing() \\} ); } test "a 'catch' statement is allowed" { try expectClean( \\fn f() { \\ doThing() catch |e| handle(e) \\} ); } test "an 'unreachable' statement is allowed" { try expectClean( \\fn f() { \\ unreachable \\} ); } test "a switch used as a statement is allowed" { try expectClean( \\fn f() { \\ switch poll() { \\ .ready => consume(), \\ else => {}, \\ } \\} ); } // --- redeclaration and shadowing (forbidden, Zig-exact) ------------------ test "a same-block redeclaration is rejected" { try expectDiag( \\fn f() { \\ x := 1 \\ x := 2 \\ _ = x \\} , "redeclaration of 'x'", 3, 5); } test "a body binding reusing a parameter name is a redeclaration" { try expectDiag( \\fn f(n usize) { \\ n := 1 \\ _ = n \\} , "redeclaration of 'n'", 2, 5); } test "a duplicate parameter is rejected" { try expectDiag( \\fn f(a usize, a usize) { \\ _ = a \\} , "redeclaration of 'a'", 1, 15); } test "a duplicate top-level declaration is rejected" { try expectDiag( \\const N = 1 \\const N = 2 , "redeclaration of 'N'", 2, 7); } test "an inner binding shadowing an outer local is rejected" { try expectDiag( \\fn f(o ?u8) { \\ x := 1 \\ if o |x| { \\ _ = x \\ } \\ _ = x \\} , "'x' shadows a local binding", 3, 11); } test "an inner binding shadowing a parameter is rejected" { try expectDiag( \\fn f(x usize) { \\ if true { \\ x := 1 \\ _ = x \\ } \\ _ = x \\} , "'x' shadows a parameter", 3, 9); } test "a local shadowing a file-scope binding is rejected" { try expectDiag( \\const N = 1 \\ \\fn f() { \\ N := 2 \\ _ = N \\} , "'N' shadows a file-scope binding", 4, 5); } test "sibling blocks may reuse a name (separate frames)" { try expectClean( \\fn f(c bool) { \\ if c { \\ x := 1 \\ _ = x \\ } else { \\ x := 2 \\ _ = x \\ } \\} ); } test "a test block body sees imports and file-scope declarations" { try expectClean( \\use std \\ \\const Answer = 42 \\ \\fn add(a i32, b i32) i32 { \\ return a + b \\} \\ \\test "add" { \\ try std.testing.expectEqual(Answer, add(40, 2)) \\} ); } test "an unknown member root in a test body is rejected" { try expectDiag( \\test "broken" { \\ _ = missing.thing() \\} , "unknown name 'missing'", 2, 9); } test "a test body is its own scope — bindings do not leak between tests" { try expectClean( \\test "first" { \\ n := 1 \\ _ = n \\} \\ \\test "second" { \\ n := 2 \\ _ = n \\} ); } test "an else capture is in scope for its arm; the arm is its own scope" { try expectClean( \\fn f() void { \\ if next() |v| { \\ _ = v.field \\ } else |err| { \\ _ = err.field \\ } \\ while next() |v| { \\ _ = v.field \\ } else |err| { \\ _ = err.field \\ } \\} ); } test "an else capture does not leak past its arm" { try expectDiag( \\fn f() void { \\ if next() |v| { \\ _ = v.field \\ } else |err| { \\ _ = err.field \\ } \\ _ = err.field \\} , "unknown name 'err'", 7, 9); } test "a loop else arm is checked: an unknown root inside it is rejected" { try expectDiag( \\fn f(xs []u8) void { \\ for x in xs { \\ _ = x \\ } else { \\ _ = missing.thing() \\ } \\} , "unknown name 'missing'", 5, 13); } test "names inside a tuple type are marked used; multi-return values are checked" { // `Tok` is used only inside tuple types (parameter + return position) — // the tuple descent must mark it, or it would be flagged unused; both // multi-return values are each walked (the unknown member root `bogus` // is caught; a bare unknown ident is outside this checker's scope). try expectDiag( \\const Tok = struct { \\ kind u8, \\} \\fn next(t (Tok, bool)) (Tok, bool) { \\ return t[0], bogus.kind \\} , "unknown name 'bogus'", 5, 18); } test "the evaluator's definite errors surface through check" { // A division by a known zero is reported with the standard Diag shape, // anchored at the expression's leftmost lexeme. try expectDiag( \\const D = 1 / 0 , "division by zero", 1, 11); try expectDiag( \\fn f(n usize) usize { \\ return n / 0 \\} , "division by zero", 2, 12); // `||` on error sets (a type operand) is rejected at the Flash line // instead of surfacing as invalid emitted Zig. try expectDiag( \\const AError = error{Bad} \\const BError = error{Worse} \\const Both = AError || BError , "cannot merge error sets", 3, 14); } test "folded constants raise no diagnostics through check" { try expectClean( \\const N = 1 + 2 * 3 \\const T = ?u8 \\const S = "hi" \\ \\fn f() usize { \\ const k = N * 2 \\ return k \\} ); } test "generic application errors surface through check" { // Arity, anchored at the application's name. try expectDiag( \\fn Box(comptime T type) type { \\ return T \\} \\const B = Box(u8, u8) , "generic 'Box' expects 1 argument, found 2", 4, 11); // Kind, anchored at the offending argument. try expectDiag( \\fn Box(comptime T type) type { \\ return T \\} \\const B = Box(5) , "argument 1 to generic 'Box' expects a type, found a value", 4, 15); } test "well-formed generic applications check clean" { try expectClean( \\fn Box(comptime T type) type { \\ return T \\} \\const B = Box(u8) \\ \\fn f(x Box(u8)) Box(u8) { \\ return x \\} ); } test "instance typing surfaces through check" { // `B` folds to the type `Box(u8)` denotes, so a value parameter // receiving it is a kind error, anchored at the argument. try expectDiag( \\fn Box(comptime T type) type { \\ return T \\} \\fn Ring(comptime n usize) type { \\ return u8 \\} \\const B = Box(u8) \\const R = Ring(B) , "argument 1 to generic 'Ring' expects a value, found a type", 8, 16); } test "runaway generic recursion is reported through check" { // Anchored at the recursive application inside the generic's body. try expectDiag( \\fn A(comptime T type) type { \\ return A(T) \\} \\const X = A(u8) , "generic 'A' exceeds the instantiation depth limit (64)", 2, 12); } test "a destructure declares its names: redeclaration and immutable assignment are flagged" { // The names land in the current frame under the unchanged dup rules. try expectDiag( \\fn pair() (u8, u8) { \\ return 1, 2 \\} \\fn demo() void { \\ a, a := pair() \\ _ = a \\} , "redeclaration of 'a'", 5, 8); // A ':='-destructured name is immutable, exactly as a single ':=' bind. try expectDiag( \\fn pair() (u8, u8) { \\ return 1, 2 \\} \\fn demo() void { \\ a, b := pair() \\ _ = b \\ a = 3 \\} , "cannot assign to immutable binding 'a'", 7, 5); // A destructuring assignment checks each bare-identifier target's // mutability, like the single assign. try expectDiag( \\fn pair() (u8, u8) { \\ return 1, 2 \\} \\fn demo() void { \\ p := 1 \\ var q = 2 \\ p, q = pair() \\} , "cannot assign to immutable binding 'p'", 7, 5); }