Skip to content

Commit f45c270

Browse files
authored
Merge pull request #9642 from roc-lang/comptime-exhaustiveness
Implement comptime exhaustiveness checking
2 parents 19fd252 + 782a533 commit f45c270

47 files changed

Lines changed: 1274 additions & 175 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/backend/dev/LirCodeGen.zig

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5428,7 +5428,8 @@ pub fn LirCodeGen(comptime target: RocTarget) type {
54285428
try locals.put(localKey(expect_stmt.condition), expect_stmt.condition);
54295429
try stack.append(sa, expect_stmt.next);
54305430
},
5431-
.runtime_error => {},
5431+
.comptime_branch_taken => |marker| try stack.append(sa, marker.next),
5432+
.runtime_error, .comptime_exhaustiveness_failed => {},
54325433
.incref => |inc| {
54335434
try locals.put(localKey(inc.value), inc.value);
54345435
try stack.append(sa, inc.next);
@@ -5549,7 +5550,8 @@ pub fn LirCodeGen(comptime target: RocTarget) type {
55495550
try locals.put(localKey(expect_stmt.condition), expect_stmt.condition);
55505551
try stack.append(sa, expect_stmt.next);
55515552
},
5552-
.runtime_error => {},
5553+
.comptime_branch_taken => |marker| try stack.append(sa, marker.next),
5554+
.runtime_error, .comptime_exhaustiveness_failed => {},
55535555
.incref => |inc| {
55545556
try locals.put(localKey(inc.value), inc.value);
55555557
try stack.append(sa, inc.next);
@@ -14048,6 +14050,15 @@ pub fn LirCodeGen(comptime target: RocTarget) type {
1404814050
try self.emitTrap();
1404914051
},
1405014052

14053+
.comptime_exhaustiveness_failed => {
14054+
try self.emitRocCrash("compile-time exhaustiveness failure reached runtime code");
14055+
try self.emitTrap();
14056+
},
14057+
14058+
.comptime_branch_taken => |marker| {
14059+
try work.append(wa, .{ .node = marker.next });
14060+
},
14061+
1405114062
.join => |j| {
1405214063
const jp_key = @intFromEnum(j.id);
1405314064
try self.setupJoinPointParams(j.id, j.params);

src/backend/llvm/MonoLlvmCodeGen.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,6 +1800,12 @@ pub const MonoLlvmCodeGen = struct {
18001800
.runtime_error => {
18011801
try self.emitCrashBytes("hit a runtime error");
18021802
},
1803+
.comptime_exhaustiveness_failed => {
1804+
try self.emitCrashBytes("compile-time exhaustiveness failure reached runtime code");
1805+
},
1806+
.comptime_branch_taken => |marker| {
1807+
try work.append(wa, .{ .node = marker.next });
1808+
},
18031809
.incref => |inc| {
18041810
try self.emitRcForLocal(.incref, inc.value, inc.count, inc.atomicity);
18051811
try work.append(wa, .{ .node = inc.next });

src/backend/wasm/WasmCodeGen.zig

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3123,7 +3123,8 @@ fn collectProcLocals(
31233123
try recordProcLocal(locals, expect_stmt.condition);
31243124
try work.append(wa, expect_stmt.next);
31253125
},
3126-
.runtime_error => {},
3126+
.comptime_branch_taken => |marker| try work.append(wa, marker.next),
3127+
.runtime_error, .comptime_exhaustiveness_failed => {},
31273128
.switch_stmt => |switch_stmt| {
31283129
try recordProcLocal(locals, switch_stmt.cond);
31293130
for (self.store.getCFSwitchBranches(switch_stmt.branches)) |branch| {
@@ -7284,6 +7285,13 @@ fn generateCFStmtNode(self: *Self, work: *std.ArrayList(StmtWork), wa: Allocator
72847285
try self.emitRocStaticStringCall(wasm_roc_ops_crashed_offset, msg);
72857286
self.currentCode().append(self.allocator, Op.@"unreachable") catch return error.OutOfMemory;
72867287
},
7288+
.comptime_exhaustiveness_failed => {
7289+
try self.emitRocStaticStringCall(wasm_roc_ops_crashed_offset, "compile-time exhaustiveness failure reached runtime code");
7290+
self.currentCode().append(self.allocator, Op.@"unreachable") catch return error.OutOfMemory;
7291+
},
7292+
.comptime_branch_taken => |marker| {
7293+
try work.append(wa, .{ .node = .{ .stmt_id = marker.next, .stop = stop } });
7294+
},
72877295
.crash => |crash| {
72887296
const msg_bytes = self.store.getString(crash.msg);
72897297
try self.emitRocStaticStringCall(wasm_roc_ops_crashed_offset, msg_bytes);

src/canonicalize/Can.zig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10129,6 +10129,7 @@ fn runExprKernel(
1012910129
const if_expr_idx = try self.env.addExpr(Expr{ .e_if = .{
1013010130
.branches = branches_span,
1013110131
.final_else = final_else,
10132+
.warn_unused_branches = false,
1013210133
} }, state.region);
1013310134

1013410135
const if_free_vars = self.scratch_free_vars.spanFrom(state.free_vars_start);
@@ -10802,6 +10803,7 @@ fn runExprKernel(
1080210803
.e_if = .{
1080310804
.branches = branches_span,
1080410805
.final_else = can_else.idx,
10806+
.warn_unused_branches = true,
1080510807
},
1080610808
}, state.region);
1080710809

@@ -10868,6 +10870,7 @@ fn runExprKernel(
1086810870
.e_if = .{
1086910871
.branches = branches_span,
1087010872
.final_else = empty_record_idx,
10873+
.warn_unused_branches = true,
1087110874
},
1087210875
}, state.region);
1087310876

src/canonicalize/Expression.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ pub const Expr = union(enum) {
201201
e_if: struct {
202202
branches: IfBranch.Span,
203203
final_else: Expr.Idx,
204+
warn_unused_branches: bool,
204205
},
205206
/// This is *only* for calling functions, not for tag application.
206207
/// The Tag variant contains any applied values inside it.

src/canonicalize/Node.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ pub const Payload = extern union {
575575
};
576576

577577
pub const ExprIfThenElse = extern struct {
578-
branches_else_idx: u32, // Index into span_with_node_data: (branches.start, branches.len, final_else)
578+
branches_else_idx: u32, // Index into if_data
579579
_padding: [8]u8 = .{ 0, 0, 0, 0, 0, 0, 0, 0 },
580580
};
581581

src/canonicalize/NodeStore.zig

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ span2_data: collections.SafeList(Span2), // Typed storage for (start, len) span
2828
span_with_node_data: collections.SafeList(SpanWithNode), // Typed storage for (start, len, node) triples
2929
method_call_data: collections.SafeList(MethodCallData), // Typed storage for method args plus method-token source region
3030
match_data: collections.SafeList(MatchData), // Typed storage for match expression data
31+
if_data: collections.SafeList(IfData), // Typed storage for if expression data
3132
match_branch_data: collections.SafeList(MatchBranchData), // Typed storage for match branch data
3233
closure_data: collections.SafeList(ClosureData), // Typed storage for closure expressions
3334
zero_arg_tag_data: collections.SafeList(ZeroArgTagData), // Typed storage for zero-argument tags
@@ -73,6 +74,15 @@ pub const MatchData = extern struct {
7374
skip_exhaustiveness: u32,
7475
};
7576

77+
/// If expression data.
78+
/// Stores branches span, final else, and whether to warn for untaken compile-time branches.
79+
pub const IfData = extern struct {
80+
branches_start: u32,
81+
branches_len: u32,
82+
final_else: u32,
83+
warn_unused_branches: u32,
84+
};
85+
7686
/// Match branch data.
7787
/// Stores patterns span, value, guard, and redundant flag.
7888
pub const MatchBranchData = extern struct {
@@ -257,6 +267,8 @@ pub fn initCapacity(gpa: Allocator, capacity: usize) Allocator.Error!NodeStore {
257267
errdefer method_call_data.deinit(gpa);
258268
var match_data = try collections.SafeList(MatchData).initCapacity(gpa, capacity / 8);
259269
errdefer match_data.deinit(gpa);
270+
var if_data = try collections.SafeList(IfData).initCapacity(gpa, capacity / 8);
271+
errdefer if_data.deinit(gpa);
260272
var match_branch_data = try collections.SafeList(MatchBranchData).initCapacity(gpa, capacity / 8);
261273
errdefer match_branch_data.deinit(gpa);
262274
var closure_data = try collections.SafeList(ClosureData).initCapacity(gpa, capacity / 16);
@@ -285,6 +297,7 @@ pub fn initCapacity(gpa: Allocator, capacity: usize) Allocator.Error!NodeStore {
285297
.span_with_node_data = span_with_node_data,
286298
.method_call_data = method_call_data,
287299
.match_data = match_data,
300+
.if_data = if_data,
288301
.match_branch_data = match_branch_data,
289302
.closure_data = closure_data,
290303
.zero_arg_tag_data = zero_arg_tag_data,
@@ -308,6 +321,7 @@ pub fn clone(self: *const NodeStore, gpa: Allocator) Allocator.Error!NodeStore {
308321
.span_with_node_data = try self.span_with_node_data.clone(gpa),
309322
.method_call_data = try self.method_call_data.clone(gpa),
310323
.match_data = try self.match_data.clone(gpa),
324+
.if_data = try self.if_data.clone(gpa),
311325
.match_branch_data = try self.match_branch_data.clone(gpa),
312326
.closure_data = try self.closure_data.clone(gpa),
313327
.zero_arg_tag_data = try self.zero_arg_tag_data.clone(gpa),
@@ -331,6 +345,7 @@ pub fn deinit(store: *NodeStore) void {
331345
store.span_with_node_data.deinit(store.gpa);
332346
store.method_call_data.deinit(store.gpa);
333347
store.match_data.deinit(store.gpa);
348+
store.if_data.deinit(store.gpa);
334349
store.match_branch_data.deinit(store.gpa);
335350
store.closure_data.deinit(store.gpa);
336351
store.zero_arg_tag_data.deinit(store.gpa);
@@ -354,6 +369,7 @@ pub fn relocate(store: *NodeStore, offset: isize) void {
354369
store.span_with_node_data.relocate(offset);
355370
store.method_call_data.relocate(offset);
356371
store.match_data.relocate(offset);
372+
store.if_data.relocate(offset);
357373
store.match_branch_data.relocate(offset);
358374
store.closure_data.relocate(offset);
359375
store.zero_arg_tag_data.relocate(offset);
@@ -1034,12 +1050,12 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr {
10341050
},
10351051
.expr_if_then_else => {
10361052
const p = payload.expr_if_then_else;
1037-
// Retrieve branches span and final_else from span_with_node_data
1038-
const branches_else = store.span_with_node_data.items.items[p.branches_else_idx];
1053+
const if_data = store.if_data.items.items[p.branches_else_idx];
10391054

10401055
return CIR.Expr{ .e_if = .{
1041-
.branches = .{ .span = .{ .start = branches_else.start, .len = branches_else.len } },
1042-
.final_else = @enumFromInt(branches_else.node),
1056+
.branches = .{ .span = .{ .start = if_data.branches_start, .len = if_data.branches_len } },
1057+
.final_else = @enumFromInt(if_data.final_else),
1058+
.warn_unused_branches = if_data.warn_unused_branches != 0,
10431059
} };
10441060
},
10451061
.expr_field_access => {
@@ -2454,15 +2470,16 @@ pub fn addExpr(store: *NodeStore, expr: CIR.Expr, region: base.Region) Allocator
24542470
},
24552471
.e_if => |e| {
24562472
node.tag = .expr_if_then_else;
2457-
const branches_else_idx: u32 = @intCast(store.span_with_node_data.len());
2458-
_ = try store.span_with_node_data.append(store.gpa, .{
2459-
.start = e.branches.span.start,
2460-
.len = e.branches.span.len,
2461-
.node = @intFromEnum(e.final_else),
2473+
const if_data_idx: u32 = @intCast(store.if_data.len());
2474+
_ = try store.if_data.append(store.gpa, .{
2475+
.branches_start = e.branches.span.start,
2476+
.branches_len = e.branches.span.len,
2477+
.final_else = @intFromEnum(e.final_else),
2478+
.warn_unused_branches = @intFromBool(e.warn_unused_branches),
24622479
});
24632480

24642481
node.setPayload(.{ .expr_if_then_else = .{
2465-
.branches_else_idx = branches_else_idx,
2482+
.branches_else_idx = if_data_idx,
24662483
} });
24672484
},
24682485
.e_call => |e| {
@@ -4603,6 +4620,7 @@ pub const Serialized = extern struct {
46034620
span_with_node_data: collections.SafeList(SpanWithNode).Serialized,
46044621
method_call_data: collections.SafeList(MethodCallData).Serialized,
46054622
match_data: collections.SafeList(MatchData).Serialized,
4623+
if_data: collections.SafeList(IfData).Serialized,
46064624
match_branch_data: collections.SafeList(MatchBranchData).Serialized,
46074625
closure_data: collections.SafeList(ClosureData).Serialized,
46084626
zero_arg_tag_data: collections.SafeList(ZeroArgTagData).Serialized,
@@ -4634,6 +4652,8 @@ pub const Serialized = extern struct {
46344652
try self.method_call_data.serialize(&store.method_call_data, allocator, writer);
46354653
// Serialize match_data
46364654
try self.match_data.serialize(&store.match_data, allocator, writer);
4655+
// Serialize if_data
4656+
try self.if_data.serialize(&store.if_data, allocator, writer);
46374657
// Serialize match_branch_data
46384658
try self.match_branch_data.serialize(&store.match_branch_data, allocator, writer);
46394659
// Serialize closure_data
@@ -4666,6 +4686,7 @@ pub const Serialized = extern struct {
46664686
.span_with_node_data = self.span_with_node_data.deserializeInto(base_addr),
46674687
.method_call_data = self.method_call_data.deserializeInto(base_addr),
46684688
.match_data = self.match_data.deserializeInto(base_addr),
4689+
.if_data = self.if_data.deserializeInto(base_addr),
46694690
.match_branch_data = self.match_branch_data.deserializeInto(base_addr),
46704691
.closure_data = self.closure_data.deserializeInto(base_addr),
46714692
.zero_arg_tag_data = self.zero_arg_tag_data.deserializeInto(base_addr),
@@ -4691,6 +4712,7 @@ pub const Serialized = extern struct {
46914712
.span_with_node_data = self.span_with_node_data.deserializeInto(base_addr),
46924713
.method_call_data = self.method_call_data.deserializeInto(base_addr),
46934714
.match_data = self.match_data.deserializeInto(base_addr),
4715+
.if_data = self.if_data.deserializeInto(base_addr),
46944716
.match_branch_data = self.match_branch_data.deserializeInto(base_addr),
46954717
.closure_data = self.closure_data.deserializeInto(base_addr),
46964718
.zero_arg_tag_data = self.zero_arg_tag_data.deserializeInto(base_addr),

src/canonicalize/test/node_store_test.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ test "NodeStore round trip - Expressions" {
314314
.e_if = .{
315315
.branches = CIR.Expr.IfBranch.Span{ .span = rand_span() },
316316
.final_else = rand_idx(CIR.Expr.Idx),
317+
.warn_unused_branches = true,
317318
},
318319
});
319320
try expressions.append(gpa, CIR.Expr{

src/check/Check.zig

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,11 @@ checking_call_arg: bool = false,
186186
/// treatment of a variable binding. Deliberately NOT set for mutable `var`
187187
/// bindings, which must never generalize.
188188
checking_binding_rhs: bool = false,
189+
/// Nonzero while checking source that constructs a compile-time value. Static
190+
/// exhaustiveness diagnostics under this depth are empirical candidates: the
191+
/// compile-time finalizer either observes them executing, reports their generated
192+
/// miss path, or discards them if the source was not reached.
193+
empirical_exhaustiveness_depth: u32 = 0,
189194
/// Deferred def-level unifications (def_var = ptrn_var = expr_var).
190195
/// These must happen AFTER generalization to avoid lowering expr_var's rank
191196
/// before generalization can process it, but BEFORE eql constraint resolution
@@ -3422,6 +3427,9 @@ fn checkDef(self: *Self, def_idx: CIR.Def.Idx, env: *Env) std.mem.Allocator.Erro
34223427
}
34233428
};
34243429

3430+
self.empirical_exhaustiveness_depth += 1;
3431+
defer self.empirical_exhaustiveness_depth -= 1;
3432+
34253433
// Infer types for the body, checking against the instantiated annotation
34263434
self.checking_binding_rhs = true;
34273435
const def_does_fx = try self.checkExpr(def.expr, env, expectation);
@@ -6417,6 +6425,10 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected)
64176425

64186426
// Check the the body of the expr
64196427
// If we have an expected function, use that as the expr's expected type
6428+
const saved_empirical_exhaustiveness_depth = self.empirical_exhaustiveness_depth;
6429+
self.empirical_exhaustiveness_depth = 0;
6430+
defer self.empirical_exhaustiveness_depth = saved_empirical_exhaustiveness_depth;
6431+
64206432
const body_does_fx = if (mb_anno_func) |expected_func| blk: {
64216433
const lambda_body_does_fx = try self.checkExpr(lambda.body, env, Expected.none().withBranchResult(expected_func.ret));
64226434
try self.closeAbsentConstructedPayloadVars(lambda.body, body_var);
@@ -7779,6 +7791,10 @@ fn closeAbsentConstructedPayloadVars(
77797791
}
77807792
}
77817793

7794+
fn pendingExhaustivenessMode(self: *const Self) ProblemStore.PendingStaticExhaustivenessMode {
7795+
return if (self.empirical_exhaustiveness_depth == 0) .static else .empirical;
7796+
}
7797+
77827798
fn checkDestructureExhaustiveness(
77837799
self: *Self,
77847800
pattern_idx: CIR.Pattern.Idx,
@@ -7829,7 +7845,7 @@ fn checkDestructureExhaustiveness(
78297845
.count = self.problems.missing_patterns_backing.items.len - missing_patterns_start,
78307846
};
78317847

7832-
_ = try self.problems.appendProblem(self.gpa, .{ .non_exhaustive_destructure = .{
7848+
try self.problems.appendPendingStaticExhaustiveness(self.gpa, .destructure, self.pendingExhaustivenessMode(), region, .{ .non_exhaustive_destructure = .{
78337849
.pattern = pattern_idx,
78347850
.value_snapshot = value_snapshot,
78357851
.missing_patterns = missing_patterns_range,
@@ -8587,7 +8603,7 @@ fn checkMatchExpr(
85878603
.count = self.problems.missing_patterns_backing.items.len - missing_patterns_start,
85888604
};
85898605

8590-
_ = try self.problems.appendProblem(self.gpa, .{ .non_exhaustive_match = .{
8606+
try self.problems.appendPendingStaticExhaustiveness(self.gpa, .match, self.pendingExhaustivenessMode(), match_region, .{ .non_exhaustive_match = .{
85918607
.match_expr = expr_idx,
85928608
.condition_snapshot = condition_snapshot,
85938609
.missing_patterns = missing_patterns_range,

src/check/checked_artifact.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4987,6 +4987,7 @@ pub const CheckedExprData = union(enum) {
49874987
if_: struct {
49884988
branches: []const CheckedIfBranch,
49894989
final_else: CheckedExprId,
4990+
warn_unused_branches: bool,
49904991
},
49914992
call: struct {
49924993
func: CheckedExprId,
@@ -6567,6 +6568,7 @@ const CheckedBodyPayloadCopier = struct {
65676568
.e_if => |if_| .{ .if_ = .{
65686569
.branches = try self.copyIfBranches(if_.branches),
65696570
.final_else = self.checkedExpr(if_.final_else),
6571+
.warn_unused_branches = if_.warn_unused_branches,
65706572
} },
65716573
.e_call => |call| .{ .call = .{
65726574
.func = self.checkedExpr(call.func),

0 commit comments

Comments
 (0)