Remove non-MSRV implementation and interp-only options
[hiphop-php.git] / hphp / hack / src / hhbc / try_finally_rewriter.ml
blobd28a1a01c16dfba99b51d59a220f707b85e989a3
1 (**
2 * Copyright (c) 2017, Facebook, Inc.
3 * All rights reserved.
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the "hack" directory of this source tree.
8 *)
10 open Hhbc_ast
11 open Instruction_sequence
12 open Hh_core
14 module JT = Jump_targets
16 (* Collect list of Ret* and non rewritten Break/Continue instructions inside
17 try body. *)
18 let collect_jump_instructions instrseq env =
19 let jump_targets = Emit_env.get_jump_targets env in
20 let get_label_id ~is_break level =
21 match JT.get_target_for_level is_break level jump_targets with
22 | JT.ResolvedRegular (target_label, _)
23 | JT.ResolvedTryFinally { JT.target_label = target_label; _ } ->
24 JT.get_id_for_label target_label
25 | _ -> failwith "impossible"
27 let folder map i =
28 match i with
29 | ISpecialFlow Break l ->
30 IMap.add (get_label_id ~is_break:true l) i map
31 | ISpecialFlow Continue l ->
32 IMap.add (get_label_id ~is_break:false l) i map
33 | IContFlow (RetC | RetV | RetM _) ->
34 IMap.add (JT.get_id_for_return ()) i map
35 | ISpecialFlow Goto l ->
36 IMap.add (JT.get_id_for_label (Label.named l)) i map
37 | _ -> map
39 InstrSeq.fold_left instrseq ~init:IMap.empty ~f:folder
41 (* Delete Ret*, Break/Continue/Jmp(Named)/IterBreak(Named)
42 instructions from the try body *)
43 let cleanup_try_body instrseq =
44 let rewriter i =
45 match i with
46 | ISpecialFlow _ | IContFlow (RetC | RetV | RetM _) -> None
47 | _ -> Some i
49 InstrSeq.filter_map instrseq ~f:rewriter
51 let emit_jump_to_label l iters =
52 match iters with
53 | [] -> instr_jmp l
54 | iters -> instr_iter_break l iters
56 let emit_save_label_id id =
57 gather [
58 instr_int id;
59 instr_setl (Local.get_label_id_local ());
60 instr_popc;
63 let get_pos_for_error env =
64 let aux_pos p =
65 match p with
66 | None -> Pos.none
67 | Some p -> Pos.first_char_of_line p
69 let rec aux_scope = function
70 | Ast_scope.ScopeItem.Function fd :: _ ->
71 aux_pos @@ Some fd.Ast.f_span
72 (* For methods, it points to class not the method.. weird *)
73 | Ast_scope.ScopeItem.Class cd :: _ ->
74 aux_pos @@ Some cd.Ast.c_span
75 | Ast_scope.ScopeItem.Method _ :: scope
76 | Ast_scope.ScopeItem.Lambda :: scope
77 | Ast_scope.ScopeItem.LongLambda _ :: scope -> aux_scope scope
78 | _ -> aux_pos None
80 aux_scope @@ Emit_env.get_scope env
82 let fail_if_goto_from_try_to_finally try_block finally_block =
83 let find_gotos_in block =
84 let visitor =
85 object inherit [_] Ast_visitor.ast_visitor
86 method! on_goto goto_labels label = label :: goto_labels
87 end in
88 visitor#on_block [] block in
89 let goto_labels = find_gotos_in try_block in
90 let fail_if_find_any_label_in block goto_labels =
91 let visitor =
92 object inherit [_] A.iter
93 method! on_GotoLabel goto_labels (_, label) =
94 let label_opt = List.find goto_labels
95 (fun (_, label_in_list) -> label_in_list = label) in
96 match label_opt with
97 | Some (p, _label) -> Emit_fatal.raise_fatal_parse p
98 "'goto' into finally statement is disallowed"
99 | None -> ()
100 end in
101 visitor#on_block goto_labels block in
102 fail_if_find_any_label_in finally_block goto_labels
104 let emit_goto ~in_finally_epilogue env label =
105 let err_pos = get_pos_for_error env in
106 match SMap.get label @@ JT.get_labels_in_function () with
107 | None ->
108 Emit_fatal.raise_fatal_parse
109 err_pos @@ "'goto' to undefined label '" ^ label ^ "'"
110 | Some in_using ->
111 let named_label = Label.named label in
112 (* CONSIDER: we don't need to assign state id for label
113 for cases when it is not necessary, i.e. when jump target is in the same
114 scope. HHVM does not do this today, do the same for compatibility reasons *)
115 let label_id = JT.get_id_for_label named_label in
116 let jump_targets = Emit_env.get_jump_targets env in
117 begin match JT.find_goto_target jump_targets label with
118 | JT.ResolvedGoto_label iters ->
119 let preamble =
120 if not in_finally_epilogue then empty
121 else instr_unsetl @@ Local.get_label_id_local () in
122 gather [
123 preamble;
124 emit_jump_to_label named_label iters
126 | JT.ResolvedGoto_finally {
127 JT.rgf_finally_start_label;
128 JT.rgf_iterators_to_release;
129 } ->
130 let preamble =
131 if in_finally_epilogue then empty
132 else emit_save_label_id label_id in
133 gather [
134 preamble;
135 emit_jump_to_label rgf_finally_start_label rgf_iterators_to_release;
136 (* emit goto as an indicator for try/finally rewriter to generate
137 finally epilogue, try/finally rewriter will remove it. *)
138 instr_goto label;
140 | JT.ResolvedGoto_goto_from_finally ->
141 Emit_fatal.raise_fatal_runtime
142 err_pos
143 "Goto to a label outside a finally block is not supported"
144 | JT.ResolvedGoto_goto_invalid_label ->
145 let message =
146 if in_using
147 then "'goto' into or across using statement is disallowed"
148 else "'goto' into loop or switch statement is disallowed" in
149 Emit_fatal.raise_fatal_parse err_pos message
152 let emit_return
153 ~need_ref ~verify_return ~verify_out ~num_out ~in_finally_epilogue env =
154 let ret_instr = if need_ref then instr_retv else instr_retc in
155 (* check if there are try/finally region *)
156 let jump_targets = Emit_env.get_jump_targets env in
157 begin match JT.get_closest_enclosing_finally_label jump_targets with
158 (* no finally blocks, but there might be some iterators that should be
159 released before exit - do it *)
160 | None ->
161 let verify_return_instr =
162 if verify_return
163 then if need_ref then instr_verifyRetTypeV else instr_verifyRetTypeC
164 else empty
166 let release_iterators_instr =
167 let iterators_to_release = JT.collect_iterators jump_targets in
168 gather @@ List.map iterators_to_release ~f:(fun (is_mutable, it) ->
169 let iter_free = if is_mutable then MIterFree it else IterFree it in
170 instr (IIterator iter_free)
173 if in_finally_epilogue
174 then
175 let load_retval_instr =
176 if need_ref then instr_vgetl (Local.get_retval_local ())
177 else instr_cgetl (Local.get_retval_local ())
179 gather [
180 load_retval_instr;
181 verify_return_instr;
182 verify_out;
183 release_iterators_instr;
184 if num_out <> 0 then instr_retm (num_out + 1) else ret_instr
186 else gather [
187 verify_return_instr;
188 verify_out;
189 release_iterators_instr;
190 if num_out <> 0 then instr_retm (num_out + 1) else ret_instr
192 (* ret is in finally block and there might be iterators to release -
193 jump to finally block via Jmp/IterBreak *)
194 | Some (target_label, iterators_to_release) ->
195 let preamble =
196 if in_finally_epilogue then empty
197 else
198 let save_state = emit_save_label_id (JT.get_id_for_return ()) in
199 let save_retval =
200 if need_ref then gather [
201 instr_bindl (Local.get_retval_local ());
202 instr_popv;
204 else gather [
205 instr_setl (Local.get_retval_local ());
206 instr_popc;
209 gather [
210 save_state;
211 save_retval;
214 gather [
215 preamble;
216 emit_jump_to_label target_label iterators_to_release;
217 (* emit ret instr as an indicator for try/finally rewriter to generate
218 finally epilogue, try/finally rewriter will remove it. *)
219 ret_instr;
223 and emit_break_or_continue ~is_break ~in_finally_epilogue env pos level =
224 let jump_targets = Emit_env.get_jump_targets env in
225 match JT.get_target_for_level ~is_break level jump_targets with
226 | JT.NotFound -> Emit_fatal.emit_fatal_for_break_continue pos level
227 | JT.ResolvedRegular (target_label, iterators_to_release) ->
228 let preamble =
229 if in_finally_epilogue && level = 1 then instr_unsetl @@ Local.get_label_id_local ()
230 else empty
232 gather [
233 preamble;
234 Emit_pos.emit_pos pos;
235 emit_jump_to_label target_label iterators_to_release;
237 | JT.ResolvedTryFinally {
238 JT.target_label;
239 JT.finally_label;
240 JT.iterators_to_release;
241 JT.adjusted_level; } ->
242 let preamble =
243 if not in_finally_epilogue
244 then emit_save_label_id (JT.get_id_for_label target_label)
245 else empty
247 gather [
248 preamble;
249 emit_jump_to_label finally_label iterators_to_release;
250 Emit_pos.emit_pos pos;
251 (* emit break/continue instr as an indicator for try/finally rewriter
252 to generate finally epilogue - try/finally rewriter will remove it. *)
253 if is_break then instr_break adjusted_level else instr_continue adjusted_level
256 let emit_finally_epilogue
257 ~verify_return ~verify_out ~num_out env pos jump_instructions finally_end =
258 let emit_instr i =
259 match i with
260 | IContFlow RetM _
261 | IContFlow RetC ->
262 emit_return
263 ~need_ref:false ~verify_return ~verify_out ~num_out ~in_finally_epilogue:true env
264 | IContFlow RetV ->
265 emit_return
266 ~need_ref:true ~verify_return ~verify_out ~num_out ~in_finally_epilogue:true env
267 | ISpecialFlow (Break l) ->
268 emit_break_or_continue ~is_break:true ~in_finally_epilogue:true env pos l
269 | ISpecialFlow (Continue l) ->
270 emit_break_or_continue ~is_break:false ~in_finally_epilogue:true env pos l
271 | ISpecialFlow (Goto l) ->
272 emit_goto ~in_finally_epilogue:true env l
273 | _ -> failwith @@
274 "unexpected instruction: " ^
275 "only Ret* or Break/Continue/Jmp(Named)/IterBreak(Named) are expected"
277 match IMap.elements jump_instructions with
278 | [] -> empty
279 | [_, h] ->
280 gather [
281 Emit_pos.emit_pos pos;
282 instr_issetl (Local.get_label_id_local ());
283 instr_jmpz finally_end;
284 emit_instr h; ]
285 | (max_id, _) :: _ as lst ->
286 (* mimic HHVM behavior:
287 in some cases ids can be non-consequtive - this might happen i.e. return statement
288 appear in the block and it was assigned a high id before.
289 ((3, Return), (1, Break), (0, Continue))
290 In thid case generate switch as
291 switch (id) {
292 L0 -> handler for continue
293 L1 -> handler for break
294 FinallyEnd -> empty
295 L3 -> handler for return
298 (* This function builds a list of labels and jump targets for switch.
299 It is possible that cases ids are not consequtive
300 [L1,L2,L4]. Vector of labels in switch should be dense so we need to
301 fill holes with a label that points to the end of finally block
302 [End, L1, L2, End, L4]
304 let rec aux n instructions labels bodies =
305 match instructions with
306 | [] when n >= 0 ->
307 aux (n - 1) instructions (finally_end :: labels) (empty :: bodies)
308 | [] -> (labels, bodies)
309 | (id, instruction) :: t ->
310 if id = n then
311 let label = Label.next_regular () in
312 let body = gather [
313 instr_label label;
314 emit_instr instruction;]
316 aux (n - 1) t (label :: labels) (body :: bodies)
317 else
318 aux (n - 1) instructions (finally_end :: labels) (empty :: bodies)
320 (* lst is already sorted - IMap.elements took care of it *)
321 (* TODO: add is_sorted assert to make sure this behavior is preserved *)
322 let (labels, bodies) = aux max_id lst [] [] in
323 let labels = labels in
324 gather [
325 Emit_pos.emit_pos pos;
326 instr_issetl (Local.get_label_id_local ());
327 instr_jmpz finally_end;
328 instr_cgetl (Local.get_label_id_local ());
329 instr_switch labels;
330 gather bodies ]
332 TODO: This codegen is unnecessarily complex. Basically we are generating
334 IsSetL temp
335 JmpZ finally_end
336 CGetL temp
337 Switch Unbounded 0 <L4 L5>
339 UnsetL temp
340 Jmp LContinue
342 UnsetL temp
343 Jmp LBreak
345 Two problems immediately come to mind. First, why is the unset in every case,
346 instead of after the CGetL? Surely the unset doesn't modify the stack.
347 Second, now we have a jump-to-jump situation.
349 Would it not make more sense to instead say
351 IsSetL temp
352 JmpZ finally_end
353 CGetL temp
354 UnsetL temp
355 Switch Unbounded 0 <LBreak LContinue>