Skip to content
← Back

src/syntax/process/case_exhaustiveness_checker.ghul

1
namespace Syntax.Process is
2
use Logging
3
4
use Semantic.Types.Type
5
6
// Exhaustiveness / redundancy check over a `case` statement or
7
// expression. The check runs once after every arm's pattern and
8
// statements have been compiled, so the scrutinee's type and each
9
// arm's classification are settled.
10
//
11
// The arms are read as rows of patterns and put to
12
// CASE_USEFULNESS, which answers both questions the construct
13
// needs: the arms are exhaustive when a row of wildcards matches
14
// nothing they leave open, and an arm is redundant when its own
15
// row matches nothing the arms before it left open. Whichever
16
// question is being asked, the answer comes back as the value
17
// shapes that escape, so a diagnostic can name them.
18
//
19
// A scrutinee whose values cannot be enumerated at all — `int`,
20
// `string`, an open class hierarchy — has no domain to cover, and
21
// takes the `case-needs-else` branch instead: a warning on the
22
// statement form and on an expression form with a defaultable
23
// expected type, an error otherwise.
24
class CASE_EXHAUSTIVENESS_CHECKER is
25
_logger: Logger
26
_classifier: CASE_DOMAIN_CLASSIFIER
27
_builder: CASE_PATTERN_BUILDER
28
_usefulness: CASE_USEFULNESS
29
30
// How many missing cases a diagnostic names before it stops.
31
// A sum type lists its uncovered alternatives and there are
32
// rarely many; a product can leave a great many combinations
33
// open, and listing them all would bury the point.
34
_MAX_REPORTED_WITNESSES: int static => 8
35
36
init(
37
logger: Logger,
38
innate_symbol_lookup: Semantic.Lookups.InnateSymbolLookup
39
) is
40
super.init()
41
42
_logger = logger
43
_classifier = CASE_DOMAIN_CLASSIFIER(innate_symbol_lookup)
44
_builder = CASE_PATTERN_BUILDER()
45
_usefulness = CASE_USEFULNESS()
46
si
47
48
check(`case: Trees.Statements.CASE) is
49
let expression_value = `case.expression?.value
50
51
if !expression_value? then
52
return
53
fi
54
55
let scrutinee_type = expression_value.type
56
57
if !scrutinee_type? \/ !scrutinee_type.is_settled then
58
return
59
fi
60
61
let domain = _classifier.classify(scrutinee_type)
62
63
if !domain? then
64
_check_open_domain(`case, scrutinee_type)
65
66
return
67
fi
68
69
let domains = Collections.LIST[PATTERN_DOMAIN?]()
70
71
domains.add(domain)
72
73
let rows = Collections.LIST[Collections.List[CASE_PATTERN]]()
74
let else_arm: Trees.Statements.CASE_MATCH? mut = null
75
76
for arm in `case.matches do
77
if _is_else_arm(arm) then
78
if !else_arm? then
79
else_arm = arm
80
fi
81
82
continue
83
fi
84
85
let arm_rows = _builder.build_rows(arm, domain)
86
87
// An arm whose shape could not be modelled covers
88
// nothing as far as this check is concerned, and is
89
// exempt from the redundancy report for the same
90
// reason a guarded arm is.
91
if !arm_rows? then
92
continue
93
fi
94
95
if !_adds_anything(rows, domains, arm_rows) then
96
_logger.warn(
97
arm.location,
98
"redundant-case-arm",
99
"this arm is already covered by a preceding arm"
100
)
101
fi
102
103
for row in arm_rows do
104
rows.add(row)
105
od
106
od
107
108
let witnesses =
109
_usefulness.find_witnesses(
110
rows,
111
domains,
112
CASE_PATTERN_BUILDER.wildcards(1),
113
_MAX_REPORTED_WITNESSES
114
)
115
116
let is_exhaustive = witnesses.count == 0
117
118
if is_exhaustive then
119
`case.case_state.is_exhaustive = true
120
fi
121
122
if else_arm? then
123
if is_exhaustive then
124
_logger.warn(
125
else_arm.location,
126
"dead-case-else",
127
"all cases are already covered before this else"
128
)
129
fi
130
131
return
132
fi
133
134
if is_exhaustive then
135
return
136
fi
137
138
let missing = _describe(witnesses)
139
140
// A product scrutinee reports through the same branch an
141
// unenumerable one does, naming the combinations it
142
// leaves open. Covering a product has never been required
143
// of an author, so a gap in one stays a fall-through
144
// rather than the error a gap in a sum is.
145
if _is_product(domain) then
146
_emit_needs_else(`case, scrutinee_type, missing)
147
148
return
149
fi
150
151
let message = "case is not exhaustive: missing {missing}"
152
let header = `case.location.start_position :: `case.expression.location
153
154
if `case.want_value then
155
_logger.error(
156
header,
157
message
158
)
159
else
160
_logger.warn(
161
header,
162
"non-exhaustive-case",
163
message
164
)
165
fi
166
si
167
168
// A scrutinee with no enumerable domain is covered only by an
169
// arm that cannot fail to match.
170
_check_open_domain(`case: Trees.Statements.CASE, scrutinee_type: Type) is
171
for arm in `case.matches do
172
if arm.guard? then
173
continue
174
fi
175
176
if !arm.expressions? /\ !arm.pattern? then
177
return
178
fi
179
180
if let pattern = arm.pattern then
181
if !pattern.is_explicit_type /\ !pattern.left.has_intrinsic_refutability then
182
return
183
fi
184
fi
185
od
186
187
_emit_needs_else(`case, scrutinee_type, null)
188
si
189
190
_emit_needs_else(`case: Trees.Statements.CASE, scrutinee_type: Type, missing: string?) is
191
let detail =
192
if let m = missing then
193
"missing {m}"
194
else
195
"{scrutinee_type} cannot be exhaustively matched"
196
fi
197
198
let header = `case.location.start_position :: `case.expression.location
199
200
if !`case.want_value then
201
_logger.warn(
202
header,
203
"case-needs-else",
204
"case without else may fall through: {detail}"
205
)
206
207
return
208
fi
209
210
let expected_type = `case.expected_type
211
212
if expected_type? /\ _is_defaultable(expected_type) then
213
_logger.warn(
214
header,
215
"case-needs-else",
216
"case without else may return default: {detail}"
217
)
218
219
`case.case_state.requires_default_fallthrough = true
220
221
return
222
fi
223
224
_logger.error(
225
header,
226
"else required: {detail}"
227
)
228
si
229
230
// True when the arm's rows reach a value none of the rows
231
// before it do.
232
_adds_anything(
233
rows: Collections.List[Collections.List[CASE_PATTERN]],
234
domains: Collections.List[PATTERN_DOMAIN?],
235
arm_rows: Collections.List[Collections.List[CASE_PATTERN]]
236
) -> bool is
237
for row in arm_rows do
238
if _usefulness.find_witnesses(rows, domains, row, 1).count > 0 then
239
return true
240
fi
241
od
242
243
return false
244
si
245
246
_describe(witnesses: Collections.List[Collections.List[CASE_PATTERN]]) -> string static =>
247
witnesses
248
|> Ghul.Pipes.map(w => w[0].describe())
249
|> Ghul.Pipes.join(", ")
250
251
_is_product(domain: PATTERN_DOMAIN) -> bool static =>
252
domain.constructors.count == 1 /\
253
domain.constructors[0].kind == ConstructorKind.PRODUCT
254
255
_is_defaultable(t: Type) -> bool static =>
256
t.is_value_type \/ t.is_optional
257
258
_is_else_arm(arm: Trees.Statements.CASE_MATCH) -> bool static =>
259
!arm.expressions? /\ !arm.pattern?
260
si
261
si