test(ir): lock error-set convergence before A5.1 extraction (A5.1 scaffolding step 1)
Test-first scaffolding ahead of extracting src/ir/error_analysis.zig — no code
change to the convergence targets (convergeInferredErrorSets /
convergeClosureShapeSets / collectErrorSites / collectClosureShapes).
Adds 2 unit tests via the already-pub convergence functions (no new exposure):
- convergeInferredErrorSets transitive/SCC: a `caller :: () -> ! { try raiser(); }`
with no direct raise converges to raiser's {Foo} across the try edge — the
whole-program fixpoint A5.1 must preserve. (Today's E1.4b test only covered a
direct raiser + the empty-set warning.)
- convergeClosureShapeSets: a bare-`!` closure literal `() -> ! { raise error.Bar }`
inside a host fn unions {Bar} into one shape_inferred_sets entry.
Adds 2 .ir snapshots (first .ir for these error forms), vetted clean
(idempotent, path-free, no #run): 1006-errors-inferred-error-sets (inferred-set
error-channel shapes) and 1009-errors-catch (catch lowering). 1004-errors-try
was already pinned.
PLAN-ERR is complete/idle, so the A5 overlap risk is low (the target functions
are stable, not in-flight). The sub-step-2 module will be named
src/ir/error_analysis.zig, NOT errors.zig (src/errors.zig is the DiagnosticList).
zig build, zig build test, tests/run_examples.sh (357/0) all green.
This commit is contained in:
3269
examples/expected/1006-errors-inferred-error-sets.ir
Normal file
3269
examples/expected/1006-errors-inferred-error-sets.ir
Normal file
File diff suppressed because it is too large
Load Diff
3361
examples/expected/1009-errors-catch.ir
Normal file
3361
examples/expected/1009-errors-catch.ir
Normal file
File diff suppressed because it is too large
Load Diff
@@ -903,3 +903,96 @@ test "conversions: optionalOfFlattened wraps once, flattening a nested optional"
|
||||
// Contrast: the plain wrap does NOT flatten — ?T -> ??T (distinct type).
|
||||
try std.testing.expect(module.types.optionalOf(opt_s64) != opt_s64);
|
||||
}
|
||||
|
||||
// ── A5.1 test-first scaffolding: error-set convergence ───────────────
|
||||
// Lock the whole-program inferred-error-set + closure-shape convergence
|
||||
// (`convergeInferredErrorSets` / `convergeClosureShapeSets`, both already pub)
|
||||
// before they move to `src/ir/error_analysis.zig`. The transitive-convergence
|
||||
// case (a caller inheriting a callee's set across a `try` edge) is the SCC
|
||||
// fixpoint A5.1 must preserve; today's E1.4b test only covers a direct raiser.
|
||||
|
||||
test "errors: convergeInferredErrorSets propagates a callee set across a try edge" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var lowering = Lowering.init(&module);
|
||||
|
||||
const mk = struct {
|
||||
fn node(a: std.mem.Allocator, data: ast.Node.Data) *Node {
|
||||
const n = a.create(Node) catch unreachable;
|
||||
n.* = .{ .span = .{ .start = 0, .end = 0 }, .data = data };
|
||||
return n;
|
||||
}
|
||||
};
|
||||
|
||||
// raiser :: () -> ! { raise error.Foo; }
|
||||
const r_rt = mk.node(alloc, .{ .error_type_expr = .{ .name = null } });
|
||||
const r_err = mk.node(alloc, .{ .identifier = .{ .name = "error" } });
|
||||
const r_fa = mk.node(alloc, .{ .field_access = .{ .object = r_err, .field = "Foo" } });
|
||||
const r_raise = mk.node(alloc, .{ .raise_stmt = .{ .tag = r_fa } });
|
||||
const r_body = mk.node(alloc, .{ .block = .{ .stmts = &[_]*Node{r_raise} } });
|
||||
const raiser_fd = ast.FnDecl{ .name = "raiser", .params = &.{}, .return_type = r_rt, .body = r_body };
|
||||
|
||||
// caller :: () -> ! { try raiser(); } — no direct raise; inherits {Foo}.
|
||||
const c_rt = mk.node(alloc, .{ .error_type_expr = .{ .name = null } });
|
||||
const c_callee = mk.node(alloc, .{ .identifier = .{ .name = "raiser" } });
|
||||
const c_call = mk.node(alloc, .{ .call = .{ .callee = c_callee, .args = &.{} } });
|
||||
const c_try = mk.node(alloc, .{ .try_expr = .{ .operand = c_call } });
|
||||
const c_body = mk.node(alloc, .{ .block = .{ .stmts = &[_]*Node{c_try} } });
|
||||
const caller_fd = ast.FnDecl{ .name = "caller", .params = &.{}, .return_type = c_rt, .body = c_body };
|
||||
|
||||
lowering.program_index.fn_ast_map.put("raiser", &raiser_fd) catch unreachable;
|
||||
lowering.program_index.fn_ast_map.put("caller", &caller_fd) catch unreachable;
|
||||
|
||||
lowering.convergeInferredErrorSets();
|
||||
|
||||
const foo = module.types.internTag("Foo");
|
||||
const raiser_set = lowering.inferred_error_sets.get("raiser") orelse unreachable;
|
||||
try std.testing.expectEqual(@as(usize, 1), raiser_set.len);
|
||||
try std.testing.expectEqual(foo, raiser_set[0]);
|
||||
// The caller raises nothing directly but converges to {Foo} via the edge.
|
||||
const caller_set = lowering.inferred_error_sets.get("caller") orelse unreachable;
|
||||
try std.testing.expectEqual(@as(usize, 1), caller_set.len);
|
||||
try std.testing.expectEqual(foo, caller_set[0]);
|
||||
}
|
||||
|
||||
test "errors: convergeClosureShapeSets unions a bare-! closure literal's raises" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var lowering = Lowering.init(&module);
|
||||
|
||||
const mk = struct {
|
||||
fn node(a: std.mem.Allocator, data: ast.Node.Data) *Node {
|
||||
const n = a.create(Node) catch unreachable;
|
||||
n.* = .{ .span = .{ .start = 0, .end = 0 }, .data = data };
|
||||
return n;
|
||||
}
|
||||
};
|
||||
|
||||
// host :: () { () -> ! { raise error.Bar; }; } — a bare-`!` closure literal
|
||||
// sitting in `host`'s body; its raises union into the shape set.
|
||||
const lam_rt = mk.node(alloc, .{ .error_type_expr = .{ .name = null } });
|
||||
const l_err = mk.node(alloc, .{ .identifier = .{ .name = "error" } });
|
||||
const l_fa = mk.node(alloc, .{ .field_access = .{ .object = l_err, .field = "Bar" } });
|
||||
const l_raise = mk.node(alloc, .{ .raise_stmt = .{ .tag = l_fa } });
|
||||
const lam_body = mk.node(alloc, .{ .block = .{ .stmts = &[_]*Node{l_raise} } });
|
||||
const lambda = mk.node(alloc, .{ .lambda = .{ .params = &.{}, .return_type = lam_rt, .body = lam_body } });
|
||||
const host_body = mk.node(alloc, .{ .block = .{ .stmts = &[_]*Node{lambda} } });
|
||||
const host_fd = ast.FnDecl{ .name = "host", .params = &.{}, .return_type = null, .body = host_body };
|
||||
|
||||
lowering.program_index.fn_ast_map.put("host", &host_fd) catch unreachable;
|
||||
|
||||
lowering.convergeClosureShapeSets();
|
||||
|
||||
// Exactly one closure shape recorded, carrying {Bar}.
|
||||
try std.testing.expectEqual(@as(u32, 1), lowering.shape_inferred_sets.count());
|
||||
var it = lowering.shape_inferred_sets.valueIterator();
|
||||
const tags = it.next().?.*;
|
||||
try std.testing.expectEqual(@as(usize, 1), tags.len);
|
||||
try std.testing.expectEqual(module.types.internTag("Bar"), tags[0]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user