Skip to content
← Back

src/syntax/process/compile_loops_and_exceptions.ghul

1
namespace Syntax.Process is
2
use Logging;
3
4
// Compiles `while` / `do` loops and `try` / `catch` statements,
5
// and the flow-sensitive narrowing around them. Split out of
6
// COMPILE_EXPRESSIONS, which delegates the matching pre / visit
7
// methods here. The `super.pre` / `super.visit` base-visitor
8
// calls stay in the visitor's thin stubs; the methods here are
9
// the enclosed logic.
10
//
11
// A loop drops narrows on variables its body writes (they cannot
12
// survive the back-edge) — see loop_kept_env, which the `for`
13
// loop on the visitor also uses. A `try` opens a TRY_FLOW_FRAME
14
// tracking whether the body / handlers can complete normally;
15
// narrowing established inside a try is discarded conservatively
16
// (an exception can leave the body anywhere).
17
class COMPILE_LOOPS_AND_EXCEPTIONS is
18
_logger: Logger;
19
_innate_symbol_lookup: Semantic.Lookups.InnateSymbolLookup;
20
_flow: NARROWING_FLOW;
21
_condition_analyzer: CONDITION_ANALYZER;
22
_conditionals: COMPILE_CONDITIONALS;
23
_visitor: COMPILE_EXPRESSIONS;
24
_try_flow_stack: Collections.LIST[TRY_FLOW_FRAME];
25
_loop_kept_stack: Collections.LIST[NARROW_ENV];
26
27
// Heap epoch captured at each pre_do, compared at visit_do:
28
// the kept environment was snapshot before the loop's
29
// condition and body walked, so a kill during either means
30
// its heap facts cannot be restored after the loop.
31
_loop_epoch_stack: Collections.LIST[int];
32
33
init(
34
logger: Logger,
35
innate_symbol_lookup: Semantic.Lookups.InnateSymbolLookup,
36
flow: NARROWING_FLOW,
37
condition_analyzer: CONDITION_ANALYZER,
38
conditionals: COMPILE_CONDITIONALS,
39
visitor: COMPILE_EXPRESSIONS,
40
try_flow_stack: Collections.LIST[TRY_FLOW_FRAME],
41
loop_kept_stack: Collections.LIST[NARROW_ENV]
42
) is
43
super.init();
44
45
_logger = logger;
46
_innate_symbol_lookup = innate_symbol_lookup;
47
_flow = flow;
48
_condition_analyzer = condition_analyzer;
49
_conditionals = conditionals;
50
_visitor = visitor;
51
_try_flow_stack = try_flow_stack;
52
_loop_kept_stack = loop_kept_stack;
53
_loop_epoch_stack = Collections.LIST[int]();
54
si
55
56
// The narrowing environment to use inside and after a loop
57
// whose body is `body`. A loop body may run any number of
58
// times, so a narrow on a variable the body *writes* cannot
59
// be assumed across the back-edge — those narrows are
60
// dropped. A narrow on a variable the body never writes is
61
// loop-invariant and survives. The body is scanned
62
// syntactically for assignment / `ref` targets.
63
loop_kept_env(body: Trees.Node) -> NARROW_ENV is
64
let env = _flow.current_env;
65
66
if env.is_bottom then
67
return env.copy();
68
fi
69
70
let collector = LOOP_ASSIGNMENT_COLLECTOR();
71
body.walk(collector);
72
73
let kept = NARROW_ENV();
74
75
for v in env.variables do
76
if let v_name = v.name, t = env.narrowed_type_of(v) then
77
if !collector.names.contains(v_name) then
78
kept.set_narrow(v, t);
79
fi
80
fi
81
od
82
83
// Definite-assignment is not subject to the kill-set: a
84
// variable assigned before the loop stays assigned
85
// regardless of what the body writes. Body-only
86
// assignments are dropped by restoring `kept` after the
87
// loop (a loop may run zero times).
88
for v in env.assigned_variables do
89
kept.set_assigned(v);
90
od
91
92
for v in env.non_null_variables do
93
if !collector.names.contains(v.name) then
94
kept.set_non_null(v);
95
fi
96
od
97
98
return kept;
99
si
100
101
pre_try(`try: Trees.Statements.TRY) -> bool is
102
// Record the pre-try environment. Its narrowing facts
103
// are discarded after the try (an exception can leave
104
// the body anywhere), but its definite-assignment facts
105
// survive — those assignments already happened.
106
_try_flow_stack.add(TRY_FLOW_FRAME(_flow.current_env.copy()));
107
108
return false;
109
si
110
111
// The frame for the `try` currently being walked, or null.
112
_try_flow_frame: TRY_FLOW_FRAME? =>
113
if _try_flow_stack.count > 0 then
114
_try_flow_stack[_try_flow_stack.count - 1]
115
else
116
null
117
fi;
118
119
visit_try(`try: Trees.Statements.TRY) is
120
let frame = _try_flow_frame;
121
122
if _try_flow_stack.count > 0 then
123
_try_flow_stack.remove_at(_try_flow_stack.count - 1);
124
fi
125
126
if frame? then
127
// A try with no catches never reached pre(CATCH), so
128
// the body exit is the current environment here.
129
if !frame.body_seen /\ !_flow.is_unreachable then
130
frame.can_complete = true;
131
fi
132
133
// Conservative: an exception can leave the try body
134
// at any point, so no narrowing survives the try —
135
// only the pre-try definite-assignment facts. When
136
// neither the body nor any handler can complete
137
// normally, control cannot fall through the try.
138
if frame.can_complete then
139
_flow.set_env(frame.entry.with_only_assigned());
140
else
141
_flow.set_unreachable();
142
fi
143
else
144
_flow.set_env(NARROW_ENV());
145
fi
146
147
if
148
`try.catches.count == 0 /\
149
!`try.`finally?
150
then
151
_logger.warn(`try.location, "try-without-handler", "try statement should have at least one catch clause and/or a finally clause");
152
fi
153
si
154
155
pre_catch(`catch: Trees.Statements.CATCH) -> bool is
156
let frame = _try_flow_frame;
157
158
// The first catch is reached right after the try body,
159
// so the current environment is the body's exit: record
160
// whether the body can complete normally.
161
if frame? /\ !frame.body_seen then
162
frame.body_seen = true;
163
164
if !_flow.is_unreachable then
165
frame.can_complete = true;
166
fi
167
fi
168
169
// A throw can land anywhere in the try body, so nothing
170
// narrowed there can be assumed in the handler — the
171
// catch body is walked with only the pre-try
172
// definite-assignment facts.
173
_flow.set_env(
174
if frame? then frame.entry.with_only_assigned() else NARROW_ENV() fi
175
);
176
177
return false;
178
si
179
180
visit_catch(`catch: Trees.Statements.CATCH) is
181
// A catch handler that completes normally is a path that
182
// reaches the end of the try statement.
183
let frame = _try_flow_frame;
184
185
if frame? /\ !_flow.is_unreachable then
186
frame.can_complete = true;
187
fi
188
189
if !`catch.variable? then
190
return;
191
fi
192
193
let variable = `catch.variable;
194
195
// Catch parameters must always have an explicit type —
196
// their whole purpose is to identify which exception
197
// shape the clause handles. Type inference (placeholder
198
// + LUB across writes) does not apply here, so the
199
// generalised let-without-type relaxation in
200
// pre(VARIABLE) doesn't extend to catch.
201
if isa Trees.TypeExpressions.INFER(variable.type_expression) then
202
_logger.error(variable.location, "catch variable must have an explicit type");
203
return;
204
fi
205
206
let type = variable.type_expression.type;
207
208
if !type? then
209
return;
210
fi
211
212
let exception_type = _innate_symbol_lookup.get_exception_type();
213
214
if !exception_type.is_assignable_from(type) then
215
_logger.error(variable.type_expression.location, "cannot catch {type} because it does not derive from System.Exception");
216
fi
217
si
218
219
pre_do(`do: Trees.Statements.DO) -> bool is
220
// Controlled walk that mirrors `pre_if_branch`: compute
221
// the loop's kill-set environment, walk the condition (or
222
// `while let` binding) under it, derive the body's
223
// narrowing environment from that, walk the body, and
224
// stash the kept environment for visit(DO) to restore
225
// after the loop. Per-iteration narrows established by the
226
// condition or binding flow into the body; the kill-set
227
// drops any narrow on a variable the loop writes so
228
// back-edge invariance is preserved.
229
let kept = loop_kept_env(`do);
230
_loop_kept_stack.add(kept);
231
_loop_epoch_stack.add(_flow.heap_epoch);
232
233
_flow.set_env(kept);
234
235
if `do.binding? then
236
_conditionals.check_refutable_binding(`do.binding);
237
elif `do.condition? then
238
let condition = `do.condition;
239
let epoch = _flow.heap_epoch;
240
241
condition.walk(_visitor);
242
243
let facts = _condition_analyzer.analyze_condition(condition, kept);
244
245
// The body environment derives from the pre-condition
246
// snapshot; a kill during the condition's own walk
247
// invalidates its heap facts (`x? /\ mutate()`).
248
if _flow.heap_killed_since(epoch) then
249
facts.then_env.drop_heap_facts();
250
fi
251
252
_flow.set_env(facts.then_env);
253
fi
254
255
`do.body.walk(_visitor);
256
257
return true;
258
si
259
260
visit_do(`do: Trees.Statements.DO) is
261
if _loop_kept_stack.count > 0 then
262
let kept = _loop_kept_stack[_loop_kept_stack.count - 1];
263
_loop_kept_stack.remove_at(_loop_kept_stack.count - 1);
264
265
let epoch = _loop_epoch_stack[_loop_epoch_stack.count - 1];
266
_loop_epoch_stack.remove_at(_loop_epoch_stack.count - 1);
267
268
if !`do.condition? /\ !`do.binding? /\ !_loop_body_has_break(`do.body) then
269
// An unconditional loop with no `break` cannot
270
// be exited — control never falls through it.
271
_flow.set_unreachable();
272
else
273
// The kept environment was snapshot before the
274
// loop walked; the assignment kill-set covers
275
// direct writes but not calls or member stores
276
// inside the loop, so a kill during the walk
277
// drops its heap facts before restoration.
278
if _flow.heap_killed_since(epoch) then
279
kept.drop_heap_facts();
280
fi
281
282
_flow.set_env(kept);
283
fi
284
fi
285
si
286
287
// True iff `body` contains a `break` targeting the loop it
288
// is the body of — a `break` inside a nested loop does not
289
// count (it targets that inner loop).
290
_loop_body_has_break(body: Trees.Statements.LIST) -> bool is
291
let finder = LOOP_BREAK_FINDER();
292
body.walk(finder);
293
294
return finder.found;
295
si
296
si
297
si