const std = @import("std"); const ast = @import("../ast.zig"); const lower = @import("lower.zig"); const Node = ast.Node; const Lowering = lower.Lowering; /// The converged error-analysis facts lowering consumes (PLAN-ARCH A5.1): each /// pure-failable function's inferred error-tag set, and each bare-`!` closure /// SHAPE's inferred set. Backing maps currently live on `Lowering` (the facade /// writes `self.l.*`); `facts()` returns a view over them. pub const ErrorFacts = struct { inferred_error_sets: std.StringHashMap([]const u32), shape_inferred_sets: std.StringHashMap([]const u32), }; /// Whole-program error-set convergence (architecture phase A5.1), extracted /// from `Lowering`. Owns the fix-point traversals that converge inferred /// `!` error sets (`convergeInferredErrorSets`) and bare-`!` closure-shape sets /// (`convergeClosureShapeSets`), plus the AST collectors that feed them. /// /// A `*Lowering` facade (Principle 5, like `CallResolver`/`ProtocolResolver`): /// it reads the declaration map (`fn_ast_map`) + tag registry and writes the /// `inferred_error_sets` / `shape_inferred_sets` maps that still live on /// `Lowering` (consumers read them there). The per-closure-literal contribution /// (`recordClosureShape`) + its type/shape helpers stay in `Lowering`; this /// module calls back for that and reaches its own `collectErrorSites` via the /// facade. pub const ErrorAnalysis = struct { l: *Lowering, pub fn facts(self: ErrorAnalysis) ErrorFacts { return .{ .inferred_error_sets = self.l.inferred_error_sets, .shape_inferred_sets = self.l.shape_inferred_sets, }; } /// Collect the error TAGS raised + the `try`-call EDGES of a function body, /// for the inferred-set fix-point. Stops at nested function boundaries. pub fn collectErrorSites(self: ErrorAnalysis, node: *const Node, tags: *std.ArrayList(u32), edges: *std.ArrayList([]const u8)) void { switch (node.data) { .raise_stmt => |rs| { if (Lowering.isErrorTagLiteralNode(rs.tag)) { tags.append(self.l.alloc, self.l.module.types.internTag(rs.tag.data.field_access.field)) catch {}; } self.collectErrorSites(rs.tag, tags, edges); }, .try_expr => |te| { if (Lowering.callTargetName(te.operand)) |nm| edges.append(self.l.alloc, nm) catch {}; self.collectErrorSites(te.operand, tags, edges); }, .block => |b| for (b.stmts) |s| self.collectErrorSites(s, tags, edges), .if_expr => |ie| { self.collectErrorSites(ie.condition, tags, edges); self.collectErrorSites(ie.then_branch, tags, edges); if (ie.else_branch) |eb| self.collectErrorSites(eb, tags, edges); }, .while_expr => |w| { self.collectErrorSites(w.condition, tags, edges); self.collectErrorSites(w.body, tags, edges); }, .for_expr => |f| { for (f.iterables) |it| { self.collectErrorSites(it.expr, tags, edges); if (it.range_end) |re| self.collectErrorSites(re, tags, edges); } self.collectErrorSites(f.body, tags, edges); }, .return_stmt => |r| if (r.value) |v| self.collectErrorSites(v, tags, edges), .var_decl => |v| if (v.value) |val| self.collectErrorSites(val, tags, edges), .const_decl => |c| self.collectErrorSites(c.value, tags, edges), .destructure_decl => |d| self.collectErrorSites(d.value, tags, edges), .assignment => |a| { self.collectErrorSites(a.target, tags, edges); self.collectErrorSites(a.value, tags, edges); }, .multi_assign => |m| { for (m.targets) |t| self.collectErrorSites(t, tags, edges); for (m.values) |v| self.collectErrorSites(v, tags, edges); }, .call => |c| { self.collectErrorSites(c.callee, tags, edges); for (c.args) |a| self.collectErrorSites(a, tags, edges); }, .binary_op => |b| { self.collectErrorSites(b.lhs, tags, edges); self.collectErrorSites(b.rhs, tags, edges); }, .unary_op => |u| self.collectErrorSites(u.operand, tags, edges), .deref_expr => |d| self.collectErrorSites(d.operand, tags, edges), .force_unwrap => |fu| self.collectErrorSites(fu.operand, tags, edges), .null_coalesce => |nc| { self.collectErrorSites(nc.lhs, tags, edges); self.collectErrorSites(nc.rhs, tags, edges); }, .field_access => |fa| self.collectErrorSites(fa.object, tags, edges), .index_expr => |ix| { self.collectErrorSites(ix.object, tags, edges); self.collectErrorSites(ix.index, tags, edges); }, .spread_expr => |s| self.collectErrorSites(s.operand, tags, edges), .catch_expr => |ce| { self.collectErrorSites(ce.operand, tags, edges); self.collectErrorSites(ce.body, tags, edges); }, .defer_stmt => |d| self.collectErrorSites(d.expr, tags, edges), .push_stmt => |p| { self.collectErrorSites(p.context_expr, tags, edges); self.collectErrorSites(p.body, tags, edges); }, .array_literal => |al| for (al.elements) |el| self.collectErrorSites(el, tags, edges), .tuple_literal => |tl| for (tl.elements) |el| self.collectErrorSites(el.value, tags, edges), // Stop at nested function boundaries; leaves contribute nothing. else => {}, } } /// Whole-program fix-point that converges each top-level bare-`!` function's /// inferred error set (ERR E1.4b). Runs after `scanDecls` (ASTs + named /// error sets registered) and before body lowering, so `lowerTry`'s /// named-caller widening sees the converged callee sets. Also emits the /// empty-inferred warning. Scope: pure-failable functions (value-carrying /// raise/try aren't lowered yet — E2). pub fn convergeInferredErrorSets(self: ErrorAnalysis) void { const Node_ = struct { tags: std.ArrayList(u32), edges: std.ArrayList([]const u8), rt: ?*const Node, }; var work = std.StringHashMap(Node_).init(self.l.alloc); defer work.deinit(); // Seed each bare-`!` function with its direct escape sites. var it = self.l.program_index.fn_ast_map.iterator(); while (it.next()) |e| { const fd = e.value_ptr.*; if (!Lowering.astIsPureBareInferred(fd.return_type)) continue; var tags = std.ArrayList(u32).empty; var edges = std.ArrayList([]const u8).empty; self.collectErrorSites(fd.body, &tags, &edges); work.put(e.key_ptr.*, .{ .tags = tags, .edges = edges, .rt = fd.return_type }) catch {}; } // Union edge contributions until no set grows (monotone → terminates). var changed = true; while (changed) { changed = false; var wit = work.iterator(); while (wit.next()) |we| { for (we.value_ptr.edges.items) |callee| { const callee_tags: []const u32 = blk: { if (work.getPtr(callee)) |cc| break :blk cc.tags.items; if (self.l.program_index.fn_ast_map.get(callee)) |cfd| { if (Lowering.astPureNamedSet(cfd.return_type)) |nm| { break :blk self.l.namedSetTags(nm) orelse &.{}; } } break :blk &.{}; }; for (callee_tags) |t| { if (!Lowering.containsTag(we.value_ptr.tags.items, t)) { we.value_ptr.tags.append(self.l.alloc, t) catch {}; changed = true; } } } } } // Store the converged sets (sorted) and warn on empty inferred sets. var sit = work.iterator(); while (sit.next()) |se| { const sorted = self.l.alloc.dupe(u32, se.value_ptr.tags.items) catch continue; std.mem.sort(u32, sorted, {}, std.sort.asc(u32)); self.l.inferred_error_sets.put(se.key_ptr.*, sorted) catch {}; if (sorted.len == 0 and !std.mem.eql(u8, se.key_ptr.*, "main")) { if (self.l.diagnostics) |diags| { if (se.value_ptr.rt) |rt| { diags.addFmt(.warn, rt.span, "function '{s}' is declared `!` but never errors — drop the `!`", .{se.key_ptr.*}); } } } } } /// Whole-program union of each bare-`!` closure/fn-type SHAPE's escape set /// (ERR E5.1 sub-feature 2). Walks every function body for closure literals; /// each bare-`!` failable literal contributes its raises (+ `try named_fn()` /// edges, resolved against the name-keyed converged sets) to the node shared /// by all occurrences of its value-signature shape. A `try slot(x)` against /// any matching-shape slot then widens against this union. pub fn convergeClosureShapeSets(self: ErrorAnalysis) void { // Pin the visibility context to each fn's DEFINING module // (body.source_file, stamped by resolveImports) — a closure literal's // param/return annotations must resolve where the fn is written, not // against whatever module the previous pipeline phase happened to // leave as the ambient context (issue 0122). const saved = self.l.current_source_file; defer self.l.setCurrentSourceFile(saved); var it = self.l.program_index.fn_ast_map.iterator(); while (it.next()) |e| { self.l.setCurrentSourceFile(e.value_ptr.*.body.source_file orelse saved); self.collectClosureShapes(e.value_ptr.*.body); } } /// Recurse the AST collecting closure-literal shape contributions. Unlike /// `collectErrorSites`, this descends THROUGH lambda boundaries (a nested /// closure is its own shape, and may itself contain closures). The /// per-literal recording (`recordClosureShape`) stays in `Lowering`. fn collectClosureShapes(self: ErrorAnalysis, node: *const Node) void { switch (node.data) { .lambda => |lam| { self.l.recordClosureShape(&lam); self.collectClosureShapes(lam.body); }, .block => |b| for (b.stmts) |s| self.collectClosureShapes(s), .if_expr => |ie| { self.collectClosureShapes(ie.condition); self.collectClosureShapes(ie.then_branch); if (ie.else_branch) |eb| self.collectClosureShapes(eb); }, .while_expr => |w| { self.collectClosureShapes(w.condition); self.collectClosureShapes(w.body); }, .for_expr => |f| { for (f.iterables) |it| { self.collectClosureShapes(it.expr); if (it.range_end) |re| self.collectClosureShapes(re); } self.collectClosureShapes(f.body); }, .return_stmt => |r| if (r.value) |v| self.collectClosureShapes(v), .raise_stmt => |rs| self.collectClosureShapes(rs.tag), .var_decl => |v| if (v.value) |val| self.collectClosureShapes(val), .const_decl => |c| self.collectClosureShapes(c.value), .destructure_decl => |d| self.collectClosureShapes(d.value), .assignment => |a| { self.collectClosureShapes(a.target); self.collectClosureShapes(a.value); }, .multi_assign => |m| { for (m.targets) |t| self.collectClosureShapes(t); for (m.values) |v| self.collectClosureShapes(v); }, .call => |c| { self.collectClosureShapes(c.callee); for (c.args) |a| self.collectClosureShapes(a); }, .binary_op => |b| { self.collectClosureShapes(b.lhs); self.collectClosureShapes(b.rhs); }, .unary_op => |u| self.collectClosureShapes(u.operand), .deref_expr => |d| self.collectClosureShapes(d.operand), .force_unwrap => |fu| self.collectClosureShapes(fu.operand), .null_coalesce => |nc| { self.collectClosureShapes(nc.lhs); self.collectClosureShapes(nc.rhs); }, .field_access => |fa| self.collectClosureShapes(fa.object), .index_expr => |ix| { self.collectClosureShapes(ix.object); self.collectClosureShapes(ix.index); }, .spread_expr => |s| self.collectClosureShapes(s.operand), .try_expr => |te| self.collectClosureShapes(te.operand), .catch_expr => |ce| { self.collectClosureShapes(ce.operand); self.collectClosureShapes(ce.body); }, .defer_stmt => |d| self.collectClosureShapes(d.expr), .push_stmt => |p| { self.collectClosureShapes(p.context_expr); self.collectClosureShapes(p.body); }, .array_literal => |al| for (al.elements) |el| self.collectClosureShapes(el), .tuple_literal => |tl| for (tl.elements) |el| self.collectClosureShapes(el.value), else => {}, } } };