[closures preanalysis] [1/n] Adding first step of preanalysis to specialize methods with concrete closures they are called with
Summary: This preanalysis in general aims to create specialized clones of methods that have blocks as arguments and that are called with concrete closures, and then call these clone methods instead of the original ones. One complication is what happens with the captured variables in the closure. What we do is we add them to the formals of the cloned method and passed them through to the concrete blocks. We do this transformation in two steps: 1. Go through all the callers of methods with blocks as parameters, and create the clone methods. In this preanalysis we only create the attributes for the new method, not the code. We also update the call instructions in the callers to represent a call to the cloned method with updated arguments: we don't need to pass closures arguments anymore, we instead pass the captured variables as new arguments. 2. We add the corresponding code to the newly created clones: this means swapping the call to the block variable with a call to the corresponding block. Moreover, we add some of the new formals (that correspond to the captured variables) to the arguments of the call. This diff implements step 1 of the analysis. The next diff D23216021 implements step 2. Reviewed By: ngorogiannis Differential Revision: D23109204 fbshipit-source-id: 91a5eb16bmaster
parent
0a4af7754d
commit
892b16b8c0
@ -0,0 +1,172 @@
|
||||
(*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*)
|
||||
|
||||
open! IStd
|
||||
module F = Format
|
||||
|
||||
type formal_annot = {formal_type: Mangled.t * Typ.t; formal_annot: Annot.Item.t}
|
||||
[@@deriving compare]
|
||||
|
||||
type formal_actual = {formal: formal_annot; actual: Exp.t * Typ.t [@ignore]} [@@deriving compare]
|
||||
|
||||
let pp_formal_annot fmt formal =
|
||||
F.fprintf fmt "(formal_type=%a, formal_annot=%a)"
|
||||
(Pp.pair ~fst:Mangled.pp ~snd:(Typ.pp Pp.text))
|
||||
formal.formal_type Annot.Item.pp formal.formal_annot
|
||||
|
||||
|
||||
let pp_formal_actual fmt formal_actual =
|
||||
F.fprintf fmt "(formal=%a, actual=%a)" pp_formal_annot formal_actual.formal
|
||||
(Pp.pair ~fst:Exp.pp ~snd:(Typ.pp Pp.text))
|
||||
formal_actual.actual
|
||||
|
||||
|
||||
module FormalsMap = PrettyPrintable.MakePPMap (struct
|
||||
type t = formal_annot [@@deriving compare]
|
||||
|
||||
let pp = pp_formal_annot
|
||||
end)
|
||||
|
||||
module FormalsActualsSet = PrettyPrintable.MakePPSet (struct
|
||||
type t = formal_actual [@@deriving compare]
|
||||
|
||||
let pp = pp_formal_actual
|
||||
end)
|
||||
|
||||
let formals_actuals_map formals annotations actual_params =
|
||||
let rec formals_actuals_map_inner acc formals annotations actual_params =
|
||||
match (formals, annotations, actual_params) with
|
||||
| [], [], [] ->
|
||||
Some acc
|
||||
| fml :: fmls, an :: ans, act :: acts ->
|
||||
let acc = FormalsMap.add {formal_type= fml; formal_annot= an} act acc in
|
||||
formals_actuals_map_inner acc fmls ans acts
|
||||
| _, _, _ ->
|
||||
None
|
||||
in
|
||||
formals_actuals_map_inner FormalsMap.empty formals annotations actual_params
|
||||
|
||||
|
||||
let formals_actuals_new_set map =
|
||||
FormalsMap.fold
|
||||
(fun formal actual set ->
|
||||
match actual with
|
||||
| Exp.Closure closure, _ ->
|
||||
List.fold_left closure.Exp.captured_vars ~init:set ~f:(fun set (exp, var, typ, _) ->
|
||||
let formal_annot =
|
||||
{formal_type= (Pvar.build_formal_from_pvar var, typ); formal_annot= Annot.Item.empty}
|
||||
in
|
||||
let formal_actual = {formal= formal_annot; actual= (exp, typ)} in
|
||||
FormalsActualsSet.add formal_actual set )
|
||||
| actual ->
|
||||
FormalsActualsSet.add {formal; actual} set )
|
||||
map FormalsActualsSet.empty
|
||||
|
||||
|
||||
let formals_annots_actuals_lists new_formals_actuals =
|
||||
FormalsActualsSet.fold
|
||||
(fun {formal; actual} (fs, ans, acts) ->
|
||||
(formal.formal_type :: fs, formal.formal_annot :: ans, actual :: acts) )
|
||||
new_formals_actuals ([], [], [])
|
||||
|
||||
|
||||
let has_closure actual_params =
|
||||
List.exists actual_params ~f:(fun (exp, _) ->
|
||||
match exp with Exp.Closure c -> Procname.is_objc_block c.name | _ -> false )
|
||||
|
||||
|
||||
let should_specialize actual_params call_flags =
|
||||
let block_is_receiver actual_params =
|
||||
Int.equal (List.length actual_params) 1 && call_flags.CallFlags.cf_virtual
|
||||
in
|
||||
has_closure actual_params && not (block_is_receiver actual_params)
|
||||
|
||||
|
||||
(* name for the specialized method instantiated with closure arguments *)
|
||||
let pname_with_closure_args callee_pname actual_params =
|
||||
let block_name_args =
|
||||
List.filter_map actual_params ~f:(function
|
||||
| Exp.Closure cl, _ when Procname.is_objc_block cl.name ->
|
||||
Some (Procname.block_name_of_procname cl.name)
|
||||
| _ ->
|
||||
None )
|
||||
in
|
||||
Procname.with_block_parameters callee_pname block_name_args
|
||||
|
||||
|
||||
let formals_closures_map map =
|
||||
FormalsMap.fold
|
||||
(fun formal actual new_map ->
|
||||
match actual with
|
||||
| Exp.Closure closure, _ ->
|
||||
let captured_as_formals =
|
||||
List.map
|
||||
~f:(fun (_, var, typ, _) -> (Pvar.build_formal_from_pvar var, typ))
|
||||
closure.captured_vars
|
||||
in
|
||||
Mangled.Map.add (fst formal.formal_type) (closure.name, captured_as_formals) new_map
|
||||
| _ ->
|
||||
new_map )
|
||||
map Mangled.Map.empty
|
||||
|
||||
|
||||
let is_objc_setter proc_desc =
|
||||
let attributes = Procdesc.get_attributes proc_desc in
|
||||
match attributes.ProcAttributes.objc_accessor with Some (Objc_setter _) -> true | _ -> false
|
||||
|
||||
|
||||
let is_initializer proc_desc =
|
||||
let proc_name = Procdesc.get_proc_name proc_desc in
|
||||
Procname.is_constructor proc_name
|
||||
|
||||
|
||||
let replace_with_specialize_methods cfg _node instr =
|
||||
match instr with
|
||||
| Sil.Call (ret, Exp.Const (Const.Cfun callee_pname), actual_params, loc, flags)
|
||||
when should_specialize actual_params flags -> (
|
||||
match Procname.Hash.find_opt cfg callee_pname with
|
||||
(*TODO(T74127433): This specialization works well only when the we specialize methods that take a block
|
||||
parameter and then run the block. It doesn't work well when the block is instead stored in
|
||||
a field. This case will be left as future work, and we won't specialize common cases where this
|
||||
happens such as setters or initializers. *)
|
||||
| Some proc_desc when (not (is_objc_setter proc_desc)) && not (is_initializer proc_desc) -> (
|
||||
let callee_attributes = Procdesc.get_attributes proc_desc in
|
||||
match
|
||||
formals_actuals_map callee_attributes.formals callee_attributes.method_annotation.params
|
||||
actual_params
|
||||
with
|
||||
| Some map ->
|
||||
let set = formals_actuals_new_set map in
|
||||
let new_formals, new_annots, new_actuals = formals_annots_actuals_lists set in
|
||||
let annot = callee_attributes.method_annotation in
|
||||
let specialized_pname = pname_with_closure_args callee_pname actual_params in
|
||||
let new_attributes =
|
||||
{ callee_attributes with
|
||||
specialized_with_blocks_info=
|
||||
Some
|
||||
{ orig_proc= callee_pname
|
||||
; formals_to_procs_and_new_formals= formals_closures_map map }
|
||||
; is_defined= true
|
||||
; formals= new_formals
|
||||
; method_annotation= {annot with params= new_annots}
|
||||
; proc_name= specialized_pname }
|
||||
in
|
||||
Cfg.create_proc_desc cfg new_attributes |> ignore ;
|
||||
Sil.Call (ret, Exp.Const (Const.Cfun specialized_pname), new_actuals, loc, flags)
|
||||
| None ->
|
||||
instr )
|
||||
| _ ->
|
||||
instr )
|
||||
| _ ->
|
||||
instr
|
||||
|
||||
|
||||
let process cfg =
|
||||
let process_pdesc _proc_name proc_desc =
|
||||
Procdesc.replace_instrs proc_desc ~f:(replace_with_specialize_methods cfg) |> ignore
|
||||
in
|
||||
Procname.Hash.iter process_pdesc cfg
|
@ -0,0 +1,10 @@
|
||||
(*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*)
|
||||
|
||||
open! IStd
|
||||
|
||||
val process : Cfg.t -> unit
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
@interface BlockAsReceiver : NSObject
|
||||
|
||||
@end
|
||||
|
||||
@implementation BlockAsReceiver
|
||||
|
||||
static bool _isAppStartingUp() { return true; }
|
||||
|
||||
/* This program would crash if we would specialize NSObject.copy
|
||||
here because it would become a virtual call without receiver,
|
||||
as the receiver is the block parameter. Blocks as also objects! */
|
||||
static void setupTimerOk() {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
dispatch_block_t _timerBlock = [^{
|
||||
if (_isAppStartingUp()) {
|
||||
return;
|
||||
}
|
||||
} copy];
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
@interface A
|
||||
|
||||
typedef void (^MyBlock)(int x);
|
||||
|
||||
@end
|
||||
|
||||
int p() { return 0; }
|
||||
|
||||
foo(_Nullable MyBlock my_block1, _Nullable MyBlock my_block2, _Nonnull A* a) {
|
||||
my_block1(22);
|
||||
}
|
||||
|
||||
@implementation A {
|
||||
|
||||
int x;
|
||||
}
|
||||
|
||||
- (int)bar:(A*)a {
|
||||
int x = 0;
|
||||
foo(
|
||||
^(int i) {
|
||||
self->x = x;
|
||||
},
|
||||
^(int i) {
|
||||
self->x = i;
|
||||
},
|
||||
a);
|
||||
int y = p();
|
||||
return self->x;
|
||||
}
|
||||
|
||||
- (int)bar2 {
|
||||
foo(
|
||||
^(int i) {
|
||||
self->x = 5;
|
||||
},
|
||||
^(int i) {
|
||||
self->x = 5;
|
||||
},
|
||||
self);
|
||||
return self->x;
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,118 @@
|
||||
/* @generated */
|
||||
digraph cfg {
|
||||
"foo.acbd18db4cc2f85cedef654fccc4a4d8_1" [label="1: Start foo\nFormals: my_block1:_fn_(*) my_block2:_fn_(*) a:A*\nLocals: \nAnnotation: <> foo(<_Nullable> <_Nullable> <_Nonnull>) \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"foo.acbd18db4cc2f85cedef654fccc4a4d8_1" -> "foo.acbd18db4cc2f85cedef654fccc4a4d8_3" ;
|
||||
"foo.acbd18db4cc2f85cedef654fccc4a4d8_2" [label="2: Exit foo \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"foo.acbd18db4cc2f85cedef654fccc4a4d8_3" [label="3: Call n$0 \n n$0=*&my_block1:_fn_(*) [line 16, column 3]\n n$1=n$0(22:int) objc_block [line 16, column 3]\n " shape="box"]
|
||||
|
||||
|
||||
"foo.acbd18db4cc2f85cedef654fccc4a4d8_3" -> "foo.acbd18db4cc2f85cedef654fccc4a4d8_2" ;
|
||||
"p.83878c91171338902e0fe0fb97a8c47a_1" [label="1: Start p\nFormals: \nLocals: \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"p.83878c91171338902e0fe0fb97a8c47a_1" -> "p.83878c91171338902e0fe0fb97a8c47a_3" ;
|
||||
"p.83878c91171338902e0fe0fb97a8c47a_2" [label="2: Exit p \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"p.83878c91171338902e0fe0fb97a8c47a_3" [label="3: Return Stmt \n *&return:int=0 [line 13, column 11]\n " shape="box"]
|
||||
|
||||
|
||||
"p.83878c91171338902e0fe0fb97a8c47a_3" -> "p.83878c91171338902e0fe0fb97a8c47a_2" ;
|
||||
"objc_blockA.bar2_3(class A).d749ef9e4d7f0a45237d8fe9e40fc593_1" [label="1: Start objc_blockA.bar2_3\nFormals: self:A* i:int\nLocals: \nCaptured: [by ref]self:A* \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"objc_blockA.bar2_3(class A).d749ef9e4d7f0a45237d8fe9e40fc593_1" -> "objc_blockA.bar2_3(class A).d749ef9e4d7f0a45237d8fe9e40fc593_3" ;
|
||||
"objc_blockA.bar2_3(class A).d749ef9e4d7f0a45237d8fe9e40fc593_2" [label="2: Exit objc_blockA.bar2_3 \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"objc_blockA.bar2_3(class A).d749ef9e4d7f0a45237d8fe9e40fc593_3" [label="3: BinaryOperatorStmt: Assign \n n$15=*&self:A* [line 41, column 9]\n *n$15.x:int=5 [line 41, column 9]\n " shape="box"]
|
||||
|
||||
|
||||
"objc_blockA.bar2_3(class A).d749ef9e4d7f0a45237d8fe9e40fc593_3" -> "objc_blockA.bar2_3(class A).d749ef9e4d7f0a45237d8fe9e40fc593_2" ;
|
||||
"objc_blockA.bar2_4(class A).a4f29e420077ca6ce7e44776941a7430_1" [label="1: Start objc_blockA.bar2_4\nFormals: self:A* i:int\nLocals: \nCaptured: [by ref]self:A* \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"objc_blockA.bar2_4(class A).a4f29e420077ca6ce7e44776941a7430_1" -> "objc_blockA.bar2_4(class A).a4f29e420077ca6ce7e44776941a7430_3" ;
|
||||
"objc_blockA.bar2_4(class A).a4f29e420077ca6ce7e44776941a7430_2" [label="2: Exit objc_blockA.bar2_4 \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"objc_blockA.bar2_4(class A).a4f29e420077ca6ce7e44776941a7430_3" [label="3: BinaryOperatorStmt: Assign \n n$17=*&self:A* [line 44, column 9]\n *n$17.x:int=5 [line 44, column 9]\n " shape="box"]
|
||||
|
||||
|
||||
"objc_blockA.bar2_4(class A).a4f29e420077ca6ce7e44776941a7430_3" -> "objc_blockA.bar2_4(class A).a4f29e420077ca6ce7e44776941a7430_2" ;
|
||||
"objc_blockA.bar:_1(class A).3dfd8f5104e9624c9f972777203745e3_1" [label="1: Start objc_blockA.bar:_1\nFormals: self:A* x:int i:int\nLocals: \nCaptured: [by ref]self:A* [by ref]x:int \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"objc_blockA.bar:_1(class A).3dfd8f5104e9624c9f972777203745e3_1" -> "objc_blockA.bar:_1(class A).3dfd8f5104e9624c9f972777203745e3_3" ;
|
||||
"objc_blockA.bar:_1(class A).3dfd8f5104e9624c9f972777203745e3_2" [label="2: Exit objc_blockA.bar:_1 \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"objc_blockA.bar:_1(class A).3dfd8f5104e9624c9f972777203745e3_3" [label="3: BinaryOperatorStmt: Assign \n n$5=*&self:A* [line 28, column 9]\n n$6=*&x:int [line 28, column 19]\n *n$5.x:int=n$6 [line 28, column 9]\n " shape="box"]
|
||||
|
||||
|
||||
"objc_blockA.bar:_1(class A).3dfd8f5104e9624c9f972777203745e3_3" -> "objc_blockA.bar:_1(class A).3dfd8f5104e9624c9f972777203745e3_2" ;
|
||||
"objc_blockA.bar:_2(class A).714c02790d023adc163c946a9f0220cd_1" [label="1: Start objc_blockA.bar:_2\nFormals: self:A* i:int\nLocals: \nCaptured: [by ref]self:A* \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"objc_blockA.bar:_2(class A).714c02790d023adc163c946a9f0220cd_1" -> "objc_blockA.bar:_2(class A).714c02790d023adc163c946a9f0220cd_3" ;
|
||||
"objc_blockA.bar:_2(class A).714c02790d023adc163c946a9f0220cd_2" [label="2: Exit objc_blockA.bar:_2 \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"objc_blockA.bar:_2(class A).714c02790d023adc163c946a9f0220cd_3" [label="3: BinaryOperatorStmt: Assign \n n$8=*&self:A* [line 31, column 9]\n n$9=*&i:int [line 31, column 19]\n *n$8.x:int=n$9 [line 31, column 9]\n " shape="box"]
|
||||
|
||||
|
||||
"objc_blockA.bar:_2(class A).714c02790d023adc163c946a9f0220cd_3" -> "objc_blockA.bar:_2(class A).714c02790d023adc163c946a9f0220cd_2" ;
|
||||
"bar2#A#instance.413fa5106d6a23f2bf18df99659efb82_1" [label="1: Start A.bar2\nFormals: self:A*\nLocals: \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"bar2#A#instance.413fa5106d6a23f2bf18df99659efb82_1" -> "bar2#A#instance.413fa5106d6a23f2bf18df99659efb82_4" ;
|
||||
"bar2#A#instance.413fa5106d6a23f2bf18df99659efb82_2" [label="2: Exit A.bar2 \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"bar2#A#instance.413fa5106d6a23f2bf18df99659efb82_3" [label="3: Return Stmt \n n$12=*&self:A* [line 47, column 10]\n n$13=*n$12.x:int [line 47, column 10]\n *&return:int=n$13 [line 47, column 3]\n " shape="box"]
|
||||
|
||||
|
||||
"bar2#A#instance.413fa5106d6a23f2bf18df99659efb82_3" -> "bar2#A#instance.413fa5106d6a23f2bf18df99659efb82_2" ;
|
||||
"bar2#A#instance.413fa5106d6a23f2bf18df99659efb82_4" [label="4: Call _fun_foo \n n$14=*&self:A* [line 40, column 7]\n n$16=*&self:A* [line 43, column 7]\n n$18=*&self:A* [line 46, column 7]\n n$19=_fun_foo[objc_blockA.bar2_3^objc_blockA.bar2_4](n$14:A*,n$18:A*) block_params [line 39, column 3]\n " shape="box"]
|
||||
|
||||
|
||||
"bar2#A#instance.413fa5106d6a23f2bf18df99659efb82_4" -> "bar2#A#instance.413fa5106d6a23f2bf18df99659efb82_3" ;
|
||||
"bar:#A(class A)#instance.3e4a860660eb436d473f8ceeb9c1a72b_1" [label="1: Start A.bar:\nFormals: self:A* a:A*\nLocals: y:int x:int \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"bar:#A(class A)#instance.3e4a860660eb436d473f8ceeb9c1a72b_1" -> "bar:#A(class A)#instance.3e4a860660eb436d473f8ceeb9c1a72b_6" ;
|
||||
"bar:#A(class A)#instance.3e4a860660eb436d473f8ceeb9c1a72b_2" [label="2: Exit A.bar: \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"bar:#A(class A)#instance.3e4a860660eb436d473f8ceeb9c1a72b_3" [label="3: Return Stmt \n n$0=*&self:A* [line 35, column 10]\n n$1=*n$0.x:int [line 35, column 10]\n *&return:int=n$1 [line 35, column 3]\n " shape="box"]
|
||||
|
||||
|
||||
"bar:#A(class A)#instance.3e4a860660eb436d473f8ceeb9c1a72b_3" -> "bar:#A(class A)#instance.3e4a860660eb436d473f8ceeb9c1a72b_2" ;
|
||||
"bar:#A(class A)#instance.3e4a860660eb436d473f8ceeb9c1a72b_4" [label="4: DeclStmt \n VARIABLE_DECLARED(y:int); [line 34, column 3]\n n$2=_fun_p() [line 34, column 11]\n *&y:int=n$2 [line 34, column 3]\n " shape="box"]
|
||||
|
||||
|
||||
"bar:#A(class A)#instance.3e4a860660eb436d473f8ceeb9c1a72b_4" -> "bar:#A(class A)#instance.3e4a860660eb436d473f8ceeb9c1a72b_3" ;
|
||||
"bar:#A(class A)#instance.3e4a860660eb436d473f8ceeb9c1a72b_5" [label="5: Call _fun_foo \n n$3=*&self:A* [line 27, column 7]\n n$4=*&x:int [line 27, column 7]\n n$7=*&self:A* [line 30, column 7]\n n$10=*&a:A* [line 33, column 7]\n n$11=_fun_foo[objc_blockA.bar:_1^objc_blockA.bar:_2](n$4:int,n$3:A*,n$10:A*) block_params [line 26, column 3]\n " shape="box"]
|
||||
|
||||
|
||||
"bar:#A(class A)#instance.3e4a860660eb436d473f8ceeb9c1a72b_5" -> "bar:#A(class A)#instance.3e4a860660eb436d473f8ceeb9c1a72b_4" ;
|
||||
"bar:#A(class A)#instance.3e4a860660eb436d473f8ceeb9c1a72b_6" [label="6: DeclStmt \n VARIABLE_DECLARED(x:int); [line 25, column 3]\n *&x:int=0 [line 25, column 3]\n " shape="box"]
|
||||
|
||||
|
||||
"bar:#A(class A)#instance.3e4a860660eb436d473f8ceeb9c1a72b_6" -> "bar:#A(class A)#instance.3e4a860660eb436d473f8ceeb9c1a72b_5" ;
|
||||
"dealloc#A#instance.55ac864e91dcd5d484e8ab7d8eb94fcb_1" [label="1: Start A.dealloc\nFormals: self:A*\nLocals: \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"dealloc#A#instance.55ac864e91dcd5d484e8ab7d8eb94fcb_1" -> "dealloc#A#instance.55ac864e91dcd5d484e8ab7d8eb94fcb_3" ;
|
||||
"dealloc#A#instance.55ac864e91dcd5d484e8ab7d8eb94fcb_2" [label="2: Exit A.dealloc \n " color=yellow style=filled]
|
||||
|
||||
|
||||
"dealloc#A#instance.55ac864e91dcd5d484e8ab7d8eb94fcb_3" [label="3: Call dealloc \n " shape="box"]
|
||||
|
||||
|
||||
"dealloc#A#instance.55ac864e91dcd5d484e8ab7d8eb94fcb_3" -> "dealloc#A#instance.55ac864e91dcd5d484e8ab7d8eb94fcb_2" ;
|
||||
}
|
Loading…
Reference in new issue