Appearance
| 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 |