// 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.flash), 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.flash), which folds constant initializers and contributes the definite // compile-time errors (a division by a known zero) to the same diagnostic // list. The evaluator defines its own Diag (the same shape) and this module // copies its entries — the evaluator never imports sema. Type checking proper // stays Zig's job downstream (Tier 0). use "ast" use "eval" use "parser" use "support" as sup // Re-exported so a harness driving this module (parse, then check) reaches // the parser through one import — the same public surface the handwritten // sema exposes to its consumers. pub const Parser = parser.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 []u8, msg []u8, note_anchor ?[]u8 = null, note_msg ?[]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.flash 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 []u8, anchor []u8) Loc { off := #intFromPtr(anchor.ptr) - #intFromPtr(src.ptr) sup.assert(off <= src.len) var line u32 = 1 // Index just past the most recent newline. var line_start usize = 0 var i usize = 0 while i < off { if src[i] == '\n' { line += 1 line_start = i + 1 } 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). 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. const Binding = struct { name []u8, is_mut bool, origin Origin, used bool = false, } // One label in scope — a labeled loop (`outer: while`) or a labeled block // (`blk: { … }`). The declaring occurrence doubles as the Diag anchor (the // same rule a Binding follows). `is_loop` gates `continue :label` — only a // loop can be continued; `used` is set when a break/continue targets it. const LabelInfo = struct { name []u8, is_loop bool, 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. const Checker = struct { arena sup.Allocator, diags sup.List(Diag), scope sup.List(Binding), // The label stack: every enclosing labeled loop / labeled block, innermost // last. Pushed around the labeled body only (a loop's else arm, like a // sibling statement, sees nothing), so resolution is purely lexical. labels sup.List(LabelInfo), 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 *mut Checker, b Binding) Oom!void { if self.lookupIndex(b.name) |idx| { prior := self.scope.items[idx] if idx >= self.frame_base { try self.reportNote(b.name, try sup.allocPrint(self.arena, "redeclaration of '{s}'", .{b.name}), prior.name, "previously declared here") } else { try self.reportNote(b.name, try sup.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 *mut Checker, name []u8) ?usize { var i = self.scope.items.len while i > 0 { i -= 1 if sup.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 *mut Checker, name []u8) ?*mut Binding { if self.lookupIndex(name) |i| { return &self.scope.items[i] } return null } fn markUsed(self *mut Checker, name []u8) void { if self.lookup(name) |b| { b.used = true } } // Push a label for the duration of its labeled body. A name clash with an // enclosing label is shadowing — Zig rejects it downstream (a label is not // a binding, so the dup/shadow machinery above does not apply). fn pushLabel(self *mut Checker, name []u8, is_loop bool) Oom!void { try self.labels.append(self.arena, .{ .name = name, .is_loop = is_loop }) } // Pop the innermost label, reporting it when no break/continue ever // targeted it (Zig-exact: an unused label is an error). fn popLabel(self *mut Checker) Oom!void { l := self.labels.items[self.labels.items.len - 1] self.labels.items.len -= 1 if !l.used { const kind []u8 = if (l.is_loop) "loop" else "block" try self.report(l.name, try sup.allocPrint(self.arena, "unused {s} label '{s}'", .{ kind, l.name })) } } // Resolve a `break :name` / `continue :name` target against the label // stack, innermost-first. The pointer is valid only until the next // pushLabel (which may reallocate), so callers use it at once. fn resolveLabel(self *mut Checker, name []u8) ?*mut LabelInfo { var i = self.labels.items.len while i > 0 { i -= 1 if sup.eql(u8, self.labels.items[i].name, name) { return &self.labels.items[i] } } return null } // Enter a new scope frame at the current stack top, returning the enclosing // frame's base for the matching leaveFrame. fn enterFrame(self *mut Checker) usize { 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 *mut Checker, saved_base usize) Oom!void { for b in self.scope.items[self.frame_base..] { checked := switch b.origin { .local, .param, .capture => true, .import, .global, .func => false, } if checked && !b.used && !isUnderscore(b.name) { const kind []u8 = switch b.origin { .local => "local binding", .param => "parameter", .capture => "capture", else => unreachable, } try self.report(b.name, try sup.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 *mut Checker, e ast.Expr) Oom!void { 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 *mut 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 => |p| { if p.align_expr |ae| { try self.markExpr(ae.*) } try self.markType(p.elem.*) }, .optional, .array_inferred => |inner| try self.markType(inner.*), .slice_sentinel, .slice_sentinel_mut, .many_ptr_sentinel, .many_ptr_sentinel_mut, .array_inferred_sentinel => |sp| { if sp.align_expr |ae| { try self.markExpr(ae.*) } 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 p in ft.params { try self.markType(p) } if ft.call_conv |cc| { try self.markExpr(cc.*) } if ft.ret |r| { try self.markType(r.*) } }, .generic => |g| { self.markUsed(firstSegment(g.name)) for a in g.args { try self.markExpr(a) } }, .tuple => |elems| { for e in elems { try self.markType(e) } }, } } fn report(self *mut Checker, anchor []u8, msg []u8) Oom!void { try self.diags.append(self.arena, .{ .anchor = anchor, .msg = msg }) } fn reportNote(self *mut Checker, anchor []u8, msg []u8, note_anchor []u8, note_msg []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 *mut Checker, at []u8, b Binding) Oom!void { const kind []u8 = switch b.origin { .param => "parameter", .capture => "capture", .import, .global, .func, .local => "binding", } const note []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 sup.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 — its parameters are // the C-ABI signature, never unused, so its frame is discarded without the // unused check. fn checkFn(self *mut Checker, f ast.FnDecl) Oom!void { saved := self.enterFrame() for p in f.params { 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 p in f.params { try self.markType(p.type) } if f.ret |r| { try self.markType(r) } if f.body |body| { for s in body { try self.checkStmt(s) } try self.leaveFrame(saved) } else { 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 *mut Checker, decls []ast.ContainerDecl) Oom!void { saved := self.check_roots self.check_roots = false defer self.check_roots = saved for decl in decls { switch decl { .method => |m| try self.checkFn(m), // An associated constant always carries a value (`extern var` // is file-scope-only), but the field is optional — unwrap. .constant => |c| { if c.value |v| { try self.descendTypeDef(v) } }, .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 *mut Checker, x ast.Expr) Oom!void { switch x { .struct_def => |sd| { for f in sd.fields { try self.markType(f.type) if f.default |d| { try self.markExpr(d) } } }, .enum_def => |ed| { for v in ed.variants { if v.value |val| { try self.markExpr(val.*) } } }, .union_def => |ud| { for v in ud.variants { 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 *mut 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 *mut Checker, captures [][]u8, stmts []ast.Stmt) Oom!void { saved := self.enterFrame() for cap in captures { // A `_` capture (a `for _ in …` discard) binds nothing. if isUnderscore(cap) { continue } try self.declare(.{ .name = cap, .is_mut = false, .origin = .capture }) } for s in stmts { try self.checkStmt(s) } try self.leaveFrame(saved) } fn checkStmt(self *mut 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 maybe in d.names { if maybe |name| { try self.declare(.{ .name = name, .is_mut = d.is_mut, .origin = .local }) } } }, // 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. .assign => |a| { try self.checkExpr(a.target) try self.checkExpr(a.value) 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 t in da.targets { 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) }, // An optional capture is in scope for the matched body only; the // else arm's error capture (`else |err|`) for that arm only. .if_stmt => |iff| { try self.checkExpr(iff.cond) if iff.capture |cap| { try self.checkBlock(&.{cap}, iff.body) } else { try self.checkBlock(&.{}, iff.body) } if iff.else_body |eb| { if iff.else_capture |cap| { try self.checkBlock(&.{cap}, eb) } else { try self.checkBlock(&.{}, eb) } } }, // An optional payload capture is in scope for the body only; the // loop else arm's error capture (`else |err|`) for that arm only. // A loop label is targetable from the body only — the else arm runs // after the loop, so it sees nothing (matching Zig). .while_stmt => |w| { try self.checkExpr(w.cond) if w.label |l| { try self.pushLabel(l, true) } if w.capture |cap| { try self.checkBlock(&.{cap}, w.body) } else { try self.checkBlock(&.{}, w.body) } if w.label != null { try self.popLabel() } if w.else_body |eb| { if w.else_capture |cap| { try self.checkBlock(&.{cap}, eb) } else { try self.checkBlock(&.{}, eb) } } }, // The capture name(s) — element, and the optional index — are in // scope for the body only; the loop else arm is capture-less and // its own scope. .for_stmt => |fr| { try self.checkExpr(fr.iter) if fr.range_hi |hi| { try self.checkExpr(hi) } if fr.label |l| { try self.pushLabel(l, true) } try self.checkBlock(fr.captures, fr.body) if fr.label != null { try self.popLabel() } if fr.else_body |eb| { try self.checkBlock(&.{}, eb) } }, .defer_stmt => |inner| try self.checkStmt(inner.*), // `errdefer |err| …` binds the error in scope for the deferred // statement / block only — by value, like the catch capture. .errdefer_stmt => |ed| { if ed.capture |cap| { saved := self.enterFrame() try self.declare(.{ .name = cap, .is_mut = false, .origin = .capture }) try self.checkStmt(ed.body.*) try self.leaveFrame(saved) } else { try self.checkStmt(ed.body.*) } }, // The block forms open their own scope, like any `{ … }` body. .defer_block => |stmts| try self.checkBlock(&.{}, stmts), .errdefer_block => |ed| { if ed.capture |cap| { saved := self.enterFrame() try self.declare(.{ .name = cap, .is_mut = false, .origin = .capture }) try self.checkBlock(&.{}, ed.body) try self.leaveFrame(saved) } else { try self.checkBlock(&.{}, ed.body) } }, } } fn checkExpr(self *mut 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 sup.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 a in c.args { try self.checkExpr(a) } }, .index => |ix| { try self.checkExpr(ix.base.*) try self.checkExpr(ix.index.*) }, // The sentinel is an ordinary expression (e.g. a `pkg.NUL` // constant), so a bad member root in it is still resolved. .slice => |s| { try self.checkExpr(s.base.*) try self.checkExpr(s.lo.*) if s.hi |hi| { try self.checkExpr(hi.*) } if s.sentinel |sen| { try self.checkExpr(sen.*) } }, .builtin_call => |b| { for a in b.args { 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 f in fields { 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 f in tl.fields { 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 prong in sw.prongs { for p in prong.patterns { try self.checkExpr(p.lo) if p.hi |hi| { try self.checkExpr(hi) } } if prong.capture |cap| { 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 |e| handler` binds the error in scope for the handler only. .catch_expr => |c| { try self.checkExpr(c.lhs.*) if c.capture |cap| { 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 op in a.outputs { switch op.body { .expr => |e| try self.checkExpr(e), .ret_type => {}, } } for op in a.inputs { switch op.body { .expr => |e| try self.checkExpr(e), .ret_type => {}, } } if a.clobbers |cl| { try self.checkExpr(cl.*) } }, // A labeled block runs in the enclosing scope plus its own bindings; // the label is a break target, not a binding — it rides the label // stack for the body (an unlabelled block, a switch-prong body, // pushes nothing). .block_expr => |blk| { if blk.label |l| { try self.pushLabel(l, false) } try self.checkBlock(&.{}, blk.body) if blk.label != null { try self.popLabel() } }, // A labelled break targets an enclosing labeled loop or block — // unknown labels are reported here (the value, when present, is an // ordinary expression). .brk => |b| { if b.label |l| { if self.resolveLabel(l) |target| { target.used = true } else { try self.report(l, try sup.allocPrint(self.arena, "no enclosing loop or block is labeled '{s}'", .{l})) } } if b.value |v| { try self.checkExpr(v.*) } }, // A labelled continue targets an enclosing labeled LOOP — a block // label is a break-only target (there is no next iteration). .cont => |maybe| { if maybe |l| { if self.resolveLabel(l) |target| { target.used = true if !target.is_loop { try self.reportNote(l, try sup.allocPrint(self.arena, "cannot continue the block label '{s}' — only a loop label can be continued", .{l}), target.name, "label declared here") } } else { try self.report(l, try sup.allocPrint(self.arena, "no enclosing loop is labeled '{s}'", .{l})) } } }, .ret => |maybe| { if maybe |vals| { for v in vals { try self.checkExpr(v) } } }, } } } pub fn check(arena sup.Allocator, program ast.Program) Oom![]mut Diag { var c = Checker{ .arena = arena, .diags = .empty, .scope = .empty, .labels = .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 item in program.items { 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 item in program.items { 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. An // `extern var` carries no value — nothing to descend. .const_decl => |d| { if d.value |v| { try c.descendTypeDef(v) } }, else => {}, } } // The compile-time evaluator (eval.flash) 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 d in ev.diags.items { 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) ?[]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| sup.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) ?[]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 []u8) []u8 { if sup.indexOfScalar(u8, name, '.') |dot| { return name[0..dot] } return name } fn isUnderscore(name []u8) bool { return sup.eql(u8, name, "_") } // The noun naming a shadowed binding's kind, for the shadowing diagnostic. fn originNoun(o Origin) []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 --------------------------------------------------------------- // The handwritten checker's test suite, ported — the evaluator-driven // diagnostics (definite errors, generic application checks) included, since // `check` drives the evaluator. // Parse and check `src`, asserting no diagnostics. fn expectClean(src []u8) !void { var arena = sup.ArenaAllocator.init(sup.testAlloc) defer arena.deinit() var p = Parser.init(arena.allocator(), src) prog := try p.parseProgram() diags := try check(arena.allocator(), prog) try sup.expectEqual(0, diags.len) } // Parse and check `src`, asserting at least one diagnostic whose message // contains `frag` lands at `line`:`col`. fn expectDiag(src []u8, frag []u8, line u32, col u32) !void { var arena = sup.ArenaAllocator.init(sup.testAlloc) defer arena.deinit() var p = Parser.init(arena.allocator(), src) prog := try p.parseProgram() diags := try check(arena.allocator(), prog) for d in diags { if sup.indexOf(u8, d.msg, frag) != null { loc := locate(src, d.anchor) if loc.line == line && loc.col == col { return } } } return error.DiagNotFound } // Parse and check `src`, asserting exactly `n` diagnostics. fn expectDiagCount(src []u8, n usize) !void { var arena = sup.ArenaAllocator.init(sup.testAlloc) defer arena.deinit() var p = Parser.init(arena.allocator(), src) prog := try p.parseProgram() diags := try check(arena.allocator(), prog) try sup.expectEqual(n, diags.len) } test "locate recovers 1-based line and column from an anchor" { src := "abc\ndef\nghij" try sup.expectEqual(Loc{ .line = 1, .col = 1 }, locate(src, src[0..3])) try sup.expectEqual(Loc{ .line = 2, .col = 1 }, locate(src, src[4..7])) try sup.expectEqual(Loc{ .line = 3, .col = 3 }, locate(src, src[10..12])) } test "imported module, parameter, and binding roots all resolve" { try expectClean("use flibc\n\nexport fn main(_ usize, _ argv) noreturn {\n msg := \"hi\"\n _ = flibc.sys.write_fd(1, msg.ptr, msg.len)\n flibc.exit()\n}") } test "a use alias resolves under its alias name" { try expectClean("use console_ui as ui\n\nfn f() {\n ui.screen.clear()\n}") } test "a member root that is no import, param, or binding is rejected" { try expectDiag("fn f() {\n _ = nope.sys.write()\n}", "unknown name 'nope'", 2, 9) } test "two unresolved roots are both reported, not bailed on the first" { try expectDiagCount("fn f() {\n _ = nope.a()\n _ = also.b()\n}", 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) {\n if n > 0 {\n x := n\n }\n _ = x.field\n}", "unknown name 'x'", 5, 9) } test "a for-loop capture resolves as a member root in the body" { try expectClean("fn f(xs []u8) {\n for e in xs {\n _ = e.len\n }\n}") } test "a bad member root inside a loop body is rejected" { try expectDiag("fn f(n usize) {\n while n > 0 {\n _ = nope.x()\n }\n}", "unknown name 'nope'", 3, 13) } test "an optional-capture if binds its capture as a member root in the body" { try expectClean("use pwfile\n\nfn f(xs []u8) {\n if pwfile.lookup(xs) |entry| {\n _ = entry.user\n }\n}") } test "an if capture does not leak past the if body" { try expectDiag("use pwfile\n\nfn f(xs []u8) {\n if pwfile.lookup(xs) |entry| {\n _ = entry\n }\n _ = entry.user\n}", "unknown name 'entry'", 7, 9) } test "a catch error capture resolves in the handler only" { try expectClean("use sys\n\nfn f() {\n _ = sys.run() catch |e| e.code()\n}") } test "a catch handler's capture does not leak to a sibling statement" { try expectDiag("use sys\n\nfn f() {\n _ = sys.run() catch |e| e.code()\n _ = e.code()\n}", "unknown name 'e'", 5, 9) } test "a catch capture resolves inside a recovery block" { try expectClean("use sys\n\nfn f() {\n sys.run() catch |e| {\n _ = e.code()\n }\n}") } test "an unused capture on a catch recovery block is rejected" { try expectDiag("fn f() {\n run() catch |e| {}\n}", "unused capture 'e'", 2, 18) } test "a bad member root inside a defer is rejected" { try expectDiag("fn f() {\n defer _ = nope.close()\n}", "unknown name 'nope'", 2, 15) } test "an errdefer capture resolves in the deferred statement and block" { try expectClean("fn f() !void {\n errdefer |err| _ = err.code()\n errdefer |err| {\n _ = err.code()\n }\n}") } test "an errdefer capture does not leak to a sibling statement" { try expectDiag("fn f() !void {\n errdefer |err| _ = err.code()\n _ = err.code()\n}", "unknown name 'err'", 3, 9) } test "an unused errdefer capture is rejected on both forms" { try expectDiag("fn f() !void {\n errdefer |err| cleanup()\n}", "unused capture 'err'", 2, 15) try expectDiag("fn f() !void {\n errdefer |err| {\n cleanup()\n }\n}", "unused capture 'err'", 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\n\nconst Entry = struct {\n info flibc.Dirent,\n kind u8,\n}\nconst 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 }\n\nfn fail() AllocError!void {\n return error.OutOfMemory\n}") } 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\n\nconst Writer = struct {\n fd i32,\n\n const STDOUT i32 = 1\n\n fn flush(self Writer) {\n _ = flibc.sys.fsync(self.fd)\n }\n}") } 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) {\n none,\n eof,\n}\n\nfn classify(n usize) Action {\n return if (n == 0) Action.eof else Action.none\n}") } 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() {\n _ = .{ .x = nope.value }\n}", "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\n\nfn f() {\n switch sys.poll() {\n .ready => |r| r.consume(),\n else => {},\n }\n}") } test "a switch prong capture does not leak past the switch" { try expectDiag("use sys\n\nfn f() {\n switch sys.poll() {\n .ready => |r| r.consume(),\n else => {},\n }\n _ = r.consume()\n}", "unknown name 'r'", 8, 9) } // --- mutability: assign-to-immutable ------------------------------------- test "assigning to a const local is rejected" { try expectDiag("fn f() {\n const x = 1\n x = 2\n}", "cannot assign to immutable binding 'x'", 3, 5) } test "a compound assignment to a const local is rejected" { try expectDiag("fn f() {\n const x = 1\n x += 1\n}", "cannot assign to immutable binding 'x'", 3, 5) } test "a wrapping compound assignment to a const local is rejected" { try expectDiag("fn f() {\n const x = 1\n x +%= 1\n}", "cannot assign to immutable binding 'x'", 3, 5) } test "export var and extern var declare assignable globals" { // The consumer-side `extern var` binds like any mutable global — typed, // valueless, and writable; the defining `export var` likewise. try expectClean("extern var nr_tasks i32\n\nfn bump() {\n nr_tasks += 1\n}") try expectClean("export var next_pid i32 = 1\n\nfn alloc_pid() i32 {\n next_pid += 1\n return next_pid\n}") // The forms share the global namespace — a duplicate is a redeclaration. try expectDiag("export var n i32 = 0\nextern var n i32", "redeclaration of 'n'", 2, 12) } test "assigning to a parameter is rejected" { try expectDiag("fn f(n usize) {\n n = 1\n}", "cannot assign to immutable parameter 'n'", 2, 5) } test "assigning to a for-loop capture is rejected" { try expectDiag("fn f(xs []u8) {\n for e in xs {\n e = 1\n }\n}", "cannot assign to immutable capture 'e'", 3, 9) } test "assigning to an if-capture is rejected" { try expectDiag("fn f(o ?u8) {\n if o |x| {\n x = 1\n }\n}", "cannot assign to immutable capture 'x'", 3, 9) } test "a pointer capture binds the bare name: write-through is clean, reassignment is not" { // `for *p in arr` — `p` is a binding named without the `*` (the sigil is a // flag, not part of the name), so `p.*` resolves and the write-through is a // projection this pass leaves to the downstream checker. The POINTER itself // stays an immutable capture. try expectClean("fn f(arr *mut [4]u8, o ?u8) {\n for *p in arr {\n p.* = 0\n }\n if o |*x| {\n x.* += 1\n }\n}") try expectDiag("fn f(arr *mut [4]u8) {\n for *p in arr {\n p = undefined\n }\n}", "cannot assign to immutable capture 'p'", 3, 9) } test "an unused pointer capture is flagged under its bare name" { try expectDiag("fn f(arr *mut [4]u8) {\n for *p in arr {\n work()\n }\n}", "unused capture 'p'", 2, 10) } test "assigning to a global const is rejected" { try expectDiag("const N = 1\n\nfn f() {\n N = 2\n}", "cannot assign to immutable binding 'N'", 4, 5) } test "assigning to a var local is clean" { try expectClean("fn f() {\n var x = 1\n x = 2\n}") } test "assigning to a global var is clean" { try expectClean("var N = 1\n\nfn f() {\n N = 2\n}") } 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) {\n s.field = 1\n a[0] = 2\n p.* = 3\n}") } 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 {\n fd i32,\n\n fn bump(self W, n i32) {\n n = n + 1\n }\n}", "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 {\n fd i32,\n\n fn set(self *mut W, v i32) {\n self.fd = v\n }\n}") } 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 {\n red,\n\n fn next(n i32) i32 {\n n = n + 1\n return n\n }\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) {\n eof,\n int usize,\n\n fn width(self Tok) usize {\n return helpers.width(self)\n }\n}") } // --- unused bindings ----------------------------------------------------- test "an unused local binding is rejected" { try expectDiag("fn f() {\n x := 1\n}", "unused local binding 'x'", 2, 5) } test "an unused parameter is rejected" { try expectDiag("fn f(n usize) {\n}", "unused parameter 'n'", 1, 6) } test "an unused for-loop capture is rejected" { try expectDiag("fn f(xs []u8) {\n for e in xs {\n }\n}", "unused capture 'e'", 2, 9) } test "an unused for-loop index capture is rejected" { try expectDiag("fn f(xs []u8) {\n for x, i in xs {\n _ = x\n }\n}", "unused capture 'i'", 2, 12) } test "an unused catch capture is rejected" { try expectDiag("fn f() {\n _ = run() catch |e| 0\n}", "unused capture 'e'", 2, 22) } test "an unused switch-prong capture is rejected" { try expectDiag("fn f() {\n switch poll() {\n .ready => |r| consume(),\n else => {},\n }\n}", "unused capture 'r'", 3, 20) } test "a discarded local counts as used" { try expectClean("fn f() {\n x := 1\n _ = x\n}") } test "a written-but-unread local counts as used" { try expectClean("fn f() {\n var x = 0\n x = 5\n}") } test "an underscore for-capture binds nothing and is exempt" { try expectClean("fn f(n usize) {\n for _ in 0..n {\n }\n}") } test "an underscore index capture is exempt" { try expectClean("fn f(xs []u8) {\n for x, _ in xs {\n _ = x\n }\n}") } test "an underscore parameter is exempt" { try expectClean("fn f(_ usize) {\n}") } 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() {\n const T = u8\n var x T = 0\n _ = x\n}") } test "a comptime type parameter used only in the signature is not unused" { try expectClean("fn id(comptime T type, x T) T {\n return x\n}") } 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 {\n return ?T\n}") // An array length is an expression inside the type — also marked. try expectClean("fn Ring(comptime n usize) type {\n return [n]u8\n}") } 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 {\n return struct {\n item T,\n }\n}") try expectClean("fn Either(comptime A type, comptime B type) type {\n return union(enum) {\n left A,\n right B,\n }\n}") try expectClean("fn Tag(comptime base usize) type {\n return enum(usize) {\n first = base,\n }\n}") } // --- 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 {\n x := a\n - b\n return x\n}", "expression value is ignored", 3, 5) } test "a leading '&' statement is rejected" { try expectDiag("fn f(p usize) {\n &p\n}", "expression value is ignored", 2, 5) } test "a bare identifier statement is rejected" { try expectDiag("fn f(n usize) {\n n\n}", "expression value is ignored", 2, 5) } test "a bare comparison statement is rejected" { try expectDiag("fn f(a usize, b usize) {\n a == b\n}", "expression value is ignored", 2, 5) } test "a bare member access statement is rejected" { try expectDiag("use pkg\n\nfn f() {\n pkg.field\n}", "expression value is ignored", 4, 5) } test "an ignored 'orelse' value statement is rejected" { try expectDiag("fn f(o ?usize) {\n o orelse return\n}", "expression value is ignored", 2, 5) } test "a bare call statement is allowed" { try expectClean("fn f() {\n doThing()\n}") } test "a 'try' statement is allowed" { try expectClean("fn f() !void {\n try doThing()\n}") } test "a 'catch' statement is allowed" { try expectClean("fn f() {\n doThing() catch |e| handle(e)\n}") } test "an 'unreachable' statement is allowed" { try expectClean("fn f() {\n unreachable\n}") } test "a switch used as a statement is allowed" { try expectClean("fn f() {\n switch poll() {\n .ready => consume(),\n else => {},\n }\n}") } // --- redeclaration and shadowing (forbidden, Zig-exact) ------------------ test "a same-block redeclaration is rejected" { try expectDiag("fn f() {\n x := 1\n x := 2\n _ = x\n}", "redeclaration of 'x'", 3, 5) } test "a body binding reusing a parameter name is a redeclaration" { try expectDiag("fn f(n usize) {\n n := 1\n _ = n\n}", "redeclaration of 'n'", 2, 5) } test "a duplicate parameter is rejected" { try expectDiag("fn f(a usize, a usize) {\n _ = a\n}", "redeclaration of 'a'", 1, 15) } test "a duplicate top-level declaration is rejected" { try expectDiag("const N = 1\nconst N = 2", "redeclaration of 'N'", 2, 7) } test "an inner binding shadowing an outer local is rejected" { try expectDiag("fn f(o ?u8) {\n x := 1\n if o |x| {\n _ = x\n }\n _ = x\n}", "'x' shadows a local binding", 3, 11) } test "an inner binding shadowing a parameter is rejected" { try expectDiag("fn f(x usize) {\n if true {\n x := 1\n _ = x\n }\n _ = x\n}", "'x' shadows a parameter", 3, 9) } test "a local shadowing a file-scope binding is rejected" { try expectDiag("const N = 1\n\nfn f() {\n N := 2\n _ = N\n}", "'N' shadows a file-scope binding", 4, 5) } test "sibling blocks may reuse a name (separate frames)" { try expectClean("fn f(c bool) {\n if c {\n x := 1\n _ = x\n } else {\n x := 2\n _ = x\n }\n}") } test "a test block body sees imports and file-scope declarations" { try expectClean("use std\n\nconst Answer = 42\n\nfn add(a i32, b i32) i32 {\n return a + b\n}\n\ntest \"add\" {\n try std.testing.expectEqual(Answer, add(40, 2))\n}") } test "an unknown member root in a test body is rejected" { try expectDiag("test \"broken\" {\n _ = missing.thing()\n}", "unknown name 'missing'", 2, 9) } test "a test body is its own scope — bindings do not leak between tests" { try expectClean("test \"first\" {\n n := 1\n _ = n\n}\n\ntest \"second\" {\n n := 2\n _ = n\n}") } test "an else capture is in scope for its arm; the arm is its own scope" { try expectClean("fn f() void {\n if next() |v| {\n _ = v.field\n } else |err| {\n _ = err.field\n }\n while next() |v| {\n _ = v.field\n } else |err| {\n _ = err.field\n }\n}") } test "an else capture does not leak past its arm" { try expectDiag("fn f() void {\n if next() |v| {\n _ = v.field\n } else |err| {\n _ = err.field\n }\n _ = err.field\n}", "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 {\n for x in xs {\n _ = x\n } else {\n _ = missing.thing()\n }\n}", "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 {\n kind u8,\n}\nfn next(t (Tok, bool)) (Tok, bool) {\n return t[0], bogus.kind\n}", "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 {\n return n / 0\n}", "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}\nconst BError = error{Worse}\nconst Both = AError || BError", "cannot merge error sets", 3, 14) } test "folded constants raise no diagnostics through check" { try expectClean("const N = 1 + 2 * 3\nconst T = ?u8\nconst S = \"hi\"\n\nfn f() usize {\n const k = N * 2\n return k\n}") } test "generic application errors surface through check" { // Arity, anchored at the application's name. try expectDiag("fn Box(comptime T type) type {\n return T\n}\nconst 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 {\n return T\n}\nconst 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 {\n return T\n}\nconst B = Box(u8)\n\nfn f(x Box(u8)) Box(u8) {\n return x\n}") } 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 {\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", 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 {\n return A(T)\n}\nconst 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) {\n return 1, 2\n}\nfn demo() void {\n a, a := pair()\n _ = a\n}", "redeclaration of 'a'", 5, 8) // A ':='-destructured name is immutable, exactly as a single ':=' bind. try expectDiag("fn pair() (u8, u8) {\n return 1, 2\n}\nfn demo() void {\n a, b := pair()\n _ = b\n a = 3\n}", "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) {\n return 1, 2\n}\nfn demo() void {\n p := 1\n var q = 2\n p, q = pair()\n}", "cannot assign to immutable binding 'p'", 7, 5) } test "labeled loops check clean: break and continue resolve through nesting" { // `break :outer` from inside a nested loop resolves to the enclosing // labeled while; the labelled continue resolves to its own for. try expectClean("fn f(xs []u8) void {\n outer: while true {\n for x in xs {\n _ = x\n break :outer\n }\n }\n}") try expectClean("fn g(xs []u8) void {\n scan: for x in xs {\n if x == 0 {\n continue :scan\n }\n _ = x\n }\n}") // The existing labeled-block break is untouched by the unified stack. try expectClean("fn h() usize {\n v := blk: {\n break :blk 1\n }\n return v\n}") } test "an unknown break or continue label is reported at the label" { try expectDiag("fn f() void {\n while true {\n break :outer\n }\n}", "no enclosing loop or block is labeled 'outer'", 3, 16) try expectDiag("fn f() void {\n while true {\n continue :scan\n }\n}", "no enclosing loop is labeled 'scan'", 3, 19) } test "a loop label is not visible from the loop's else arm" { // The else arm runs after the loop — a break there cannot target it // (matching Zig); the label then also goes unused. try expectDiag("fn f(it Iter) void {\n outer: while it.next() |x| {\n _ = x\n } else {\n break :outer\n }\n}", "no enclosing loop or block is labeled 'outer'", 5, 16) } test "continuing a block label is rejected with a note at the declaration" { try expectDiag("fn f() usize {\n return blk: {\n continue :blk\n }\n}", "cannot continue the block label 'blk'", 3, 19) } test "an unused label is reported at its declaration" { try expectDiag("fn f() void {\n outer: while true {\n break\n }\n}", "unused loop label 'outer'", 2, 5) try expectDiag("fn f() void {\n blk: {\n _ = 1\n }\n}", "unused block label 'blk'", 2, 5) } test "a binding used only inside a type-position align is not flagged unused" { // PAGE is referenced only by the align qualifier inside the pointer // type — markType walks the align expression, so the use registers. try expectClean("fn f() {\n const PAGE usize = 4096\n var buf []align(PAGE) u8 = undefined\n _ = buf\n}") // The same through a sentinel form's shared payload. try expectClean("fn g() {\n const A usize = 16\n var s [:0]align(A) u8 = undefined\n _ = s\n}") }