diff --git a/examples/1041-errors-failable-closure-shape-union.sx b/examples/1041-errors-failable-closure-shape-union.sx new file mode 100644 index 0000000..9de5ec2 --- /dev/null +++ b/examples/1041-errors-failable-closure-shape-union.sx @@ -0,0 +1,36 @@ +// Program-wide inferred-`!` union per closure shape (ERR E5.1 sub-feature 2). +// All occurrences of `Closure(s32) -> (s32, !)` share ONE inferred error set; +// every bare-`!` closure literal of that shape unions its raised tags in. A +// `try slot(x)` against any matching-shape slot widens against that union — so +// a caller whose named set covers { Negative, Other } type-checks, and the +// error channel actually carries each closure's own tag at runtime. + +#import "modules/std.sx"; + +All :: error { Negative, Other } + +// `h` is a bare-`!` Closure slot; the caller declares the union as `!All`. +dispatch :: (h: Closure(s32) -> (s32, !), x: s32) -> (s32, !All) { + return try h(x); +} + +main :: () -> s32 { + gpa := GPA.init(); + push Context.{ allocator = xx gpa } { + // Two literals of the SAME shape raising DIFFERENT tags both feed the + // one shared `Closure(s32)->(s32,!)` union node. + handlers : List(Closure(s32) -> (s32, !)) = .{}; + handlers.append(closure((x: s32) -> (s32, !) { if x < 0 { raise error.Negative; } return x * 2; })); + handlers.append(closure((x: s32) -> (s32, !) { if x == 0 { raise error.Other; } return x + 100; })); + + // success paths + print("ok0={}\n", dispatch(handlers.items[0], 5) catch e 0); // 10 + print("ok1={}\n", dispatch(handlers.items[1], 7) catch e 0); // 107 + + // failure paths: each closure raises its own tag, which propagates + // through `try` and is absorbed by the call-site `catch` fallback + print("err0={}\n", dispatch(handlers.items[0], -1) catch e -1); // raised Negative → -1 + print("err1={}\n", dispatch(handlers.items[1], 0) catch e -2); // raised Other → -2 + } + return 0; +} diff --git a/examples/1042-errors-failable-closure-shape-union-reject.sx b/examples/1042-errors-failable-closure-shape-union-reject.sx new file mode 100644 index 0000000..8b07637 --- /dev/null +++ b/examples/1042-errors-failable-closure-shape-union-reject.sx @@ -0,0 +1,25 @@ +// Program-wide closure-shape union — widening REJECTION (ERR E5.1 sub-feature 2). +// Two closure literals of shape `Closure(s32)->(s32,!)` raise `Negative` / +// `Other`; the shared inferred-`!` node for that shape is { Negative, Other }. +// A caller that `try`s a slot of this shape but declares only `!Small` (which +// omits both tags) is rejected — the union is checked against the caller's set +// even though the call goes through a slot with no static function name. + +#import "modules/std.sx"; + +Small :: error { Unrelated } + +reject :: (h: Closure(s32) -> (s32, !), x: s32) -> (s32, !Small) { + return try h(x); // Negative, Other ∉ Small → two diagnostics +} + +main :: () -> s32 { + gpa := GPA.init(); + push Context.{ allocator = xx gpa } { + handlers : List(Closure(s32) -> (s32, !)) = .{}; + handlers.append(closure((x: s32) -> (s32, !) { if x < 0 { raise error.Negative; } return x; })); + handlers.append(closure((x: s32) -> (s32, !) { if x == 0 { raise error.Other; } return x; })); + print("r={}\n", reject(handlers.items[0], 5) catch e 0); + } + return 0; +} diff --git a/examples/expected/1041-errors-failable-closure-shape-union.exit b/examples/expected/1041-errors-failable-closure-shape-union.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/1041-errors-failable-closure-shape-union.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/1041-errors-failable-closure-shape-union.stderr b/examples/expected/1041-errors-failable-closure-shape-union.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1041-errors-failable-closure-shape-union.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/1041-errors-failable-closure-shape-union.stdout b/examples/expected/1041-errors-failable-closure-shape-union.stdout new file mode 100644 index 0000000..bc469c4 --- /dev/null +++ b/examples/expected/1041-errors-failable-closure-shape-union.stdout @@ -0,0 +1,4 @@ +ok0=10 +ok1=107 +err0=-1 +err1=-2 diff --git a/examples/expected/1042-errors-failable-closure-shape-union-reject.exit b/examples/expected/1042-errors-failable-closure-shape-union-reject.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1042-errors-failable-closure-shape-union-reject.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1042-errors-failable-closure-shape-union-reject.stderr b/examples/expected/1042-errors-failable-closure-shape-union-reject.stderr new file mode 100644 index 0000000..6c4409b --- /dev/null +++ b/examples/expected/1042-errors-failable-closure-shape-union-reject.stderr @@ -0,0 +1,11 @@ +error: error tag 'error.Negative' is not in caller's error set 'Small' + --> /Users/agra/projects/sx/examples/1042-errors-failable-closure-shape-union-reject.sx:13:12 + | +13 | return try h(x); // Negative, Other ∉ Small → two diagnostics + | ^^^^^^^^ + +error: error tag 'error.Other' is not in caller's error set 'Small' + --> /Users/agra/projects/sx/examples/1042-errors-failable-closure-shape-union-reject.sx:13:12 + | +13 | return try h(x); // Negative, Other ∉ Small → two diagnostics + | ^^^^^^^^ diff --git a/examples/expected/1042-errors-failable-closure-shape-union-reject.stdout b/examples/expected/1042-errors-failable-closure-shape-union-reject.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1042-errors-failable-closure-shape-union-reject.stdout @@ -0,0 +1 @@ + diff --git a/src/ir/lower.zig b/src/ir/lower.zig index fad8aa2..d7c2418 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -215,6 +215,13 @@ pub const Lowering = struct { /// per-function sets (sidesteps the name-only error-set interning). Read by /// `lowerTry`'s named-caller widening and the empty-inferred warning. inferred_error_sets: std.StringHashMap([]const u32) = std.StringHashMap([]const u32).init(std.heap.page_allocator), + /// Whole-program-converged inferred error sets keyed by closure/function + /// VALUE-signature shape (ERR E5.1 sub-feature 2): every occurrence of + /// `Closure() -> (T, !)` with a structurally identical value-signature + /// shares one node; each bare-`!` closure literal of that shape unions its + /// escape tags in. Read by `checkEscapeWidening` when a `try` operand is a + /// closure/fn-type SLOT call (no static fn name). Key = `closureShapeKey`. + shape_inferred_sets: std.StringHashMap([]const u32) = std.StringHashMap([]const u32).init(std.heap.page_allocator), pub const ComptimeValue = union(enum) { int_val: i64, @@ -347,6 +354,12 @@ pub const Lowering = struct { // named-caller widening sees each bare-`!` callee's converged set; also // emits the empty-inferred warning. self.convergeInferredErrorSets(); + // Pass 1d': converge inferred (`bare !`) error sets per closure/fn-type + // SHAPE (ERR E5.1 sub-feature 2). Runs after the name-keyed pass so a + // closure's `try named_fn()` edge resolves against the converged + // top-level sets; before body lowering so `try slot(x)` widening sees + // the full per-shape union. + self.convergeClosureShapeSets(); // Pass 2: lower main (and comptime side-effects) self.lowerMainAndComptime(decls); // Pass 3: lower deferred functions (any_to_string etc.) now that all types are registered @@ -16187,14 +16200,25 @@ pub const Lowering = struct { /// Shared by `try` propagation and a failable `or` chain's final operand. fn checkEscapeWidening(self: *Lowering, callee_node: *const Node, callee_set: TypeId, caller_set: TypeId, span: ast.Span) void { if (self.isInferredErrorSet(caller_set)) return; - if (self.isInferredErrorSet(callee_set)) { - if (callTargetName(callee_node)) |nm| { - if (self.inferred_error_sets.get(nm)) |tags| { - self.diagTagsNotInSet(tags, caller_set, span); - } - } - } else { + if (!self.isInferredErrorSet(callee_set)) { self.checkErrorSetSubset(callee_set, caller_set, span); + return; + } + // Bare-`!` callee: either a named top-level function (its converged set + // is name-keyed) or a closure/fn-type SLOT (its set is shape-keyed, + // shared program-wide by value-signature). + if (callTargetName(callee_node)) |nm| { + if (self.inferred_error_sets.get(nm)) |tags| { + self.diagTagsNotInSet(tags, caller_set, span); + return; + } + } + if (self.shapeKeyOfCallee(callee_node)) |key| { + if (self.shape_inferred_sets.get(key)) |tags| { + self.diagTagsNotInSet(tags, caller_set, span); + } + // Empty union (no closure of this shape ever raises) → silently + // allowed: the slot's `!` resolves to ∅ (ERR E5.1 sub-feature 6). } } @@ -16576,6 +16600,194 @@ pub const Lowering = struct { return false; } + /// 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: *Lowering) void { + var it = self.fn_ast_map.iterator(); + while (it.next()) |e| { + 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). + fn collectClosureShapes(self: *Lowering, node: *const Node) void { + switch (node.data) { + .lambda => |lam| { + self.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| { + self.collectClosureShapes(f.iterable); + if (f.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 => {}, + } + } + + /// Record one closure literal's contribution to its value-signature shape's + /// inferred-`!` union. No-op unless the literal is a CONCRETE (non-generic) + /// bare-`!` failable closure; named-set / non-failable literals add no tags. + fn recordClosureShape(self: *Lowering, lam: *const ast.Lambda) void { + if (lam.type_params.len > 0) return; // generic shapes out of scope (sub-feature 8) + const rt_node = lam.return_type orelse return; // no annotation → non-failable infer + const ret = self.resolveType(rt_node); + const es = self.errorChannelOf(ret) orelse return; // not failable + if (!self.isInferredErrorSet(es)) return; // `!Named` → its own set, not the inferred union + + var ptys = std.ArrayList(TypeId).empty; + defer ptys.deinit(self.alloc); + for (lam.params) |p| { + if (p.is_variadic or p.is_pack or p.is_comptime) return; // not a plain fn-type slot + ptys.append(self.alloc, self.resolveType(p.type_expr)) catch return; + } + const key = self.closureShapeKey(ptys.items, self.returnValuePart(ret)); + + var tags = std.ArrayList(u32).empty; + defer tags.deinit(self.alloc); + var edges = std.ArrayList([]const u8).empty; + defer edges.deinit(self.alloc); + self.collectErrorSites(lam.body, &tags, &edges); + for (edges.items) |callee| { + for (self.calleeEscapeTags(callee)) |t| { + if (!containsTag(tags.items, t)) tags.append(self.alloc, t) catch {}; + } + } + self.unionShapeTags(key, tags.items); + } + + /// The escape tags of a callee referenced by name from a `try g()` edge: + /// a bare-`!` callee's converged set, or a `-> !Named` callee's declared set. + fn calleeEscapeTags(self: *Lowering, callee: []const u8) []const u32 { + if (self.inferred_error_sets.get(callee)) |t| return t; + if (self.fn_ast_map.get(callee)) |cfd| { + if (astPureNamedSet(cfd.return_type)) |nm| return self.namedSetTags(nm) orelse &.{}; + } + return &.{}; + } + + /// Merge `new_tags` into the shape node `key` (sorted, deduped). The map is + /// content-keyed (StringHashMap), so re-`put` with a fresh equal key string + /// overwrites the existing node's value in place. + fn unionShapeTags(self: *Lowering, key: []const u8, new_tags: []const u32) void { + var list = std.ArrayList(u32).empty; + defer list.deinit(self.alloc); + if (self.shape_inferred_sets.get(key)) |existing| list.appendSlice(self.alloc, existing) catch {}; + for (new_tags) |t| { + if (!containsTag(list.items, t)) list.append(self.alloc, t) catch {}; + } + const sorted = self.alloc.dupe(u32, list.items) catch return; + std.mem.sort(u32, sorted, {}, std.sort.asc(u32)); + self.shape_inferred_sets.put(key, sorted) catch {}; + } + + /// Canonical key for a callable VALUE-signature: param types + the value + /// part of the return (error slot excluded). Bare-`!` and non-failable + /// shapes of the same value-sig — and `.function` vs `.closure` of that + /// sig — collapse to one key, so all occurrences share one inferred node. + fn closureShapeKey(self: *Lowering, params: []const TypeId, value_ret: TypeId) []const u8 { + var buf = std.ArrayList(u8).empty; + buf.appendSlice(self.alloc, "shape") catch return "shape"; + for (params) |p| { + buf.append(self.alloc, '_') catch return "shape"; + buf.appendSlice(self.alloc, self.mangleTypeName(p)) catch return "shape"; + } + buf.appendSlice(self.alloc, "__") catch return "shape"; + buf.appendSlice(self.alloc, self.mangleTypeName(value_ret)) catch return "shape"; + return buf.items; + } + + /// The value part of a (possibly failable) return type, error slot dropped: + /// `(T, !)` → T (or a value-tuple); pure `-> !` → void; non-failable → self. + fn returnValuePart(self: *Lowering, ret: TypeId) TypeId { + const es = self.errorChannelOf(ret) orelse return ret; + if (ret == es) return .void; + return self.failableSuccessType(ret); + } + + /// Shape key of a call's callee expression when it's a closure/fn-type slot + /// (variable, field, index — anything with a `.closure`/`.function` type), + /// for the program-wide shape-union widening lookup. Null for non-callables. + fn shapeKeyOfCallee(self: *Lowering, node: *const Node) ?[]const u8 { + if (node.data != .call) return null; + const fty = self.inferExprType(node.data.call.callee); + if (fty.isBuiltin()) return null; + const info = self.module.types.get(fty); + const params: []const TypeId = switch (info) { + .closure => |c| c.params, + .function => |f| f.params, + else => return null, + }; + const ret: TypeId = switch (info) { + .closure => |c| c.ret, + .function => |f| f.ret, + else => return null, + }; + return self.closureShapeKey(params, self.returnValuePart(ret)); + } + fn binOpSymbol(op: ast.BinaryOp.Op) []const u8 { return switch (op) { .add => "+",