ERR/E5.1: program-wide inferred-! union per closure/fn shape
All occurrences of Closure(<sig>) -> (T, !) with a structurally identical value-signature now share one inferred error-set node; every bare-! closure literal of that shape unions its escape tags in, and a `try slot(x)` against any matching-shape slot widens the caller's named set against that union. This closes the gap where a slot call (no static function name) skipped the widening check entirely. - shape_inferred_sets keyed by closureShapeKey (params + value-return via mangleTypeName, error slot excluded) so bare-!, non-failable, .function and .closure of one value-sig collapse to a single key. - convergeClosureShapeSets pre-pass (lowerRoot Pass 1d', after the name-keyed convergeInferredErrorSets): collectClosureShapes walks fn bodies through lambda boundaries; recordClosureShape resolves each concrete bare-! literal's shape and unions its raises (+ try named_fn() edges via calleeEscapeTags) into the shape node. - checkEscapeWidening falls back to shapeKeyOfCallee for bare-! slot calls (computed from the callee expr's .function/.closure type). Empty union is silently allowed (sub-feature 6). Scope: concrete shapes only (generic lambdas skipped); closure-to-closure try edges are not fix-pointed (under-approximation = a missed diagnostic, never a miscompile). Tests: 1041 (positive — union composes, runs), 1042 (reject — two widening diagnostics, exit 1).
This commit is contained in:
226
src/ir/lower.zig
226
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(<sig>) -> (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 => "+",
|
||||
|
||||
Reference in New Issue
Block a user