source/analysis/cfg.ml (613 lines of code) (raw):

(* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. *) open Core open Ast open Pyre open Statement module MatchTranslate = struct open Expression let create_boolean_and ~location ~left ~right = Expression.BooleanOperator { left; operator = BooleanOperator.And; right } |> Node.create ~location let create_boolean_or ~location ~left ~right = Expression.BooleanOperator { left; operator = BooleanOperator.Or; right } |> Node.create ~location let create_constant ~location constant = Expression.Constant constant |> Node.create ~location let create_name ~location name = Expression.Name name |> Node.create ~location let create_attribute_name ~location ~base ~attribute = create_name ~location (Name.Attribute { base; attribute; special = false }) let create_identifier_name ~location name = create_name ~location (Name.Identifier name) let create_walrus ~location ~target ~value = Expression.WalrusOperator { target; value } |> Node.create ~location let create_comparison_equals ~location ~left ~right = Expression.ComparisonOperator { left; operator = ComparisonOperator.Equals; right } |> Node.create ~location let create_comparison_is ~location left right = Expression.ComparisonOperator { left; operator = ComparisonOperator.Is; right } |> Node.create ~location let create_call ~location ~callee ~arguments = Expression.Call { callee; arguments } |> Node.create ~location let create_getitem ~location ~container ~key = create_call ~location ~callee: (create_name ~location (Name.Attribute { base = container; attribute = "__getitem__"; special = true })) ~arguments:[{ Call.Argument.value = key; name = None }] let create_getitem_index ~location ~sequence ~index = create_getitem ~location ~container:sequence ~key:(create_constant ~location (Constant.Integer index)) let create_slice ~location ~lower ~upper = let to_constant = function | None -> Constant.NoneLiteral | Some index -> Constant.Integer index in let step = None in create_call ~location ~callee:(create_identifier_name ~location "slice") ~arguments: [ { Call.Argument.value = create_constant ~location (to_constant lower); name = None }; { Call.Argument.value = create_constant ~location (to_constant upper); name = None }; { Call.Argument.value = create_constant ~location (to_constant step); name = None }; ] let create_isinstance ~location object_expression type_expression = create_call ~location ~callee:(create_identifier_name ~location "isinstance") ~arguments: [ { Call.Argument.value = object_expression; name = None }; { Call.Argument.value = type_expression; name = None }; ] let create_getattr ~location base attribute = create_call ~location ~callee:(create_identifier_name ~location "getattr") ~arguments: [ { Call.Argument.value = base; name = None }; { Call.Argument.value = attribute; name = None }; ] let create_list ~location expression = create_call ~location ~callee:(create_identifier_name ~location "list") ~arguments:[{ Call.Argument.value = expression; name = None }] let create_dict ~location expression = create_call ~location ~callee:(create_identifier_name ~location "dict") ~arguments:[{ Call.Argument.value = expression; name = None }] let create_typing_sequence ~location = create_attribute_name ~location ~base:(create_identifier_name ~location "typing") ~attribute:"Sequence" let create_typing_mapping ~location = create_attribute_name ~location ~base:(create_identifier_name ~location "typing") ~attribute:"Mapping" let is_special_builtin_for_class_pattern cls = let is_special = function | ["bool"] | ["bytearray"] | ["bytes"] | ["dict"] | ["float"] | ["frozenset"] | ["int"] | ["list"] | ["set"] | ["str"] | ["tuple"] -> true | _ -> false in cls |> name_to_identifiers >>| is_special |> Option.value ~default:false let rec pattern_to_condition ~subject { Node.location; value = pattern } = let boolean_expression_capture ~location ~target ~value = (* Since we are creating boolean expression, we use walrus for capture, and add a trivial equality to make it a boolean expression that would always evaluate to True *) create_comparison_equals ~location ~left:(create_walrus ~location ~target ~value) ~right:target in match pattern with | Match.Pattern.MatchAs { pattern; name } -> ( let name = create_identifier_name ~location name in let capture = boolean_expression_capture ~location ~target:name ~value:subject in match pattern >>| pattern_to_condition ~subject:name with | Some condition -> create_boolean_and ~location ~left:capture ~right:condition | None -> capture) | MatchClass { class_name; patterns; keyword_attributes; keyword_patterns } -> let of_positional_pattern index = if index == 0 && is_special_builtin_for_class_pattern (Node.value class_name) then pattern_to_condition ~subject else let attribute = create_getitem_index ~location ~sequence: (create_attribute_name ~location ~base:subject ~attribute:"__match_args__") ~index in pattern_to_condition ~subject:(create_getattr ~location subject attribute) in let of_attribute_pattern attribute = pattern_to_condition ~subject:(create_attribute_name ~location ~base:subject ~attribute) in create_isinstance ~location subject (create_name ~location:(Node.location class_name) (Node.value class_name)) :: List.mapi ~f:of_positional_pattern patterns @ List.map2_exn ~f:of_attribute_pattern keyword_attributes keyword_patterns |> List.reduce_exn ~f:(fun left right -> create_boolean_and ~location ~left ~right) | MatchMapping { keys; patterns; rest } -> let of_key_pattern key = pattern_to_condition ~subject:(create_getitem ~location ~container:subject ~key) in let of_rest rest = let target = create_identifier_name ~location rest in (* Translation is not semantic here: in the runtime, "keys" that are matched would be removed from rest. We can skip doing that, as none of the current analyses are affected by it. *) let value = create_dict ~location subject in boolean_expression_capture ~location ~target ~value in create_isinstance ~location subject (create_typing_mapping ~location) :: List.map2_exn keys patterns ~f:of_key_pattern @ (Option.map rest ~f:of_rest |> Option.to_list) |> List.reduce_exn ~f:(fun left right -> create_boolean_and ~location ~left ~right) | MatchOr patterns -> List.map patterns ~f:(pattern_to_condition ~subject) |> List.reduce_exn ~f:(fun left right -> create_boolean_or ~location ~left ~right) | MatchSingleton constant -> create_comparison_is ~location subject (create_constant ~location constant) | MatchSequence patterns -> let prefix, rest, suffix = let is_not_star_pattern = function | { Ast.Node.value = Match.Pattern.MatchStar _; _ } -> false | _ -> true in let prefix, star_and_suffix = List.split_while patterns ~f:is_not_star_pattern in match star_and_suffix with | { Node.value = Match.Pattern.MatchStar rest; _ } :: suffix -> prefix, rest, suffix | _ -> prefix, None, [] in let prefix_length, suffix_length = List.length prefix, List.length suffix in let of_rest rest = let target = create_identifier_name ~location rest in let value = let lower = if prefix_length == 0 then None else Some prefix_length in let upper = if suffix_length == 0 then None else Some (-suffix_length) in create_getitem ~location ~container:subject ~key:(create_slice ~location ~lower ~upper) |> create_list ~location in boolean_expression_capture ~location ~target ~value in let of_prefix_pattern index = pattern_to_condition ~subject:(create_getitem_index ~location ~sequence:subject ~index) in let of_suffix_pattern index = pattern_to_condition ~subject: (create_getitem_index ~location ~sequence:subject ~index:(index - suffix_length)) in create_isinstance ~location subject (create_typing_sequence ~location) :: List.mapi prefix ~f:of_prefix_pattern @ (Option.map rest ~f:of_rest |> Option.to_list) @ List.mapi suffix ~f:of_suffix_pattern |> List.reduce_exn ~f:(fun left right -> create_boolean_and ~location ~left ~right) | MatchValue value -> create_comparison_equals ~location ~left:subject ~right:value | MatchWildcard -> Expression.Constant Constant.True |> Node.create ~location | _ -> Expression.Constant Constant.False |> Node.create ~location let to_condition ~subject ~case:{ Match.Case.pattern = { Node.location; _ } as pattern; guard; _ } = match pattern_to_condition ~subject pattern, guard with | pattern_condition, None -> pattern_condition | { Node.value = Expression.Constant Constant.True; _ }, Some guard -> guard | pattern_condition, Some guard -> create_boolean_and ~location ~left:pattern_condition ~right:guard end module Node = struct type kind = | Block of Ast.Statement.t list | Dispatch | Entry | Error | Normal | Final | For of For.t | If of If.t | Join | Try of Try.t | With of With.t | While of While.t [@@deriving compare, show, sexp] type t = { id: int; mutable kind: kind; mutable predecessors: Int.Set.t; mutable successors: Int.Set.t; } [@@deriving compare, sexp] let location_insensitive_equal left right = let equal_kind left right = let compare_equal compare left right = Int.equal (compare left right) 0 in match left, right with | Block left, Block right -> List.equal (compare_equal Statement.location_insensitive_compare) left right | For left, For right -> compare_equal For.location_insensitive_compare left right | If left, If right -> compare_equal If.location_insensitive_compare left right | Try left, Try right -> compare_equal Try.location_insensitive_compare left right | With left, With right -> compare_equal With.location_insensitive_compare left right | While left, While right -> compare_equal While.location_insensitive_compare left right | _ -> [%compare.equal: kind] left right in Int.equal left.id right.id && equal_kind left.kind right.kind && Int.Set.equal left.predecessors right.predecessors && Int.Set.equal left.successors right.successors let pp format node = Format.fprintf format "[Node %d]\n%a\nPredecessors: %a, Successors: %a\n" node.id pp_kind node.kind Sexp.pp [%message (Set.elements node.predecessors : int list)] Sexp.pp [%message (Set.elements node.successors : int list)] let create id kind predecessors successors = { id; kind; predecessors; successors } let node_count = ref 0 let reset_count () = node_count := 0 let empty graph kind = let node = { id = !node_count; kind; predecessors = Int.Set.empty; successors = Int.Set.empty } in Hashtbl.set graph ~key:!node_count ~data:node; node_count := !node_count + 1; node let id { id; _ } = id let statements { kind; _ } = match kind with | Block statements -> statements | _ -> [] let successors { successors; _ } = successors let predecessors { predecessors; _ } = predecessors let connect predecessor successor = predecessor.successors <- Set.add predecessor.successors successor.id; successor.predecessors <- Set.add successor.predecessors predecessor.id let connect_option predecessor successor = predecessor >>| (fun predecessor -> connect predecessor successor) |> ignore let has_predecessors { predecessors; _ } = not (Set.is_empty predecessors) let description { kind; _ } = let add_newlines string = if String.length string > 100 then String.split ~on:',' string |> String.concat ~sep:",\n" else string in let process_statement statement = Ast.Node.create_with_default_location statement |> show |> add_newlines in match kind with | Block statement_list -> List.map ~f:(fun statement -> show statement |> add_newlines) statement_list |> String.concat ~sep:"\n" | Dispatch -> "Dispatch" | Entry -> "Entry" | Error -> "Error" | Normal -> "Normal" | Final -> "Final" | For statement -> Statement.For statement |> process_statement | If statement -> Statement.If statement |> process_statement | Join -> "Join" | Try statement -> Statement.Try statement |> process_statement | With statement -> Statement.With statement |> process_statement | While statement -> Statement.While statement |> process_statement end type t = Node.t Int.Table.t type jumps = { break: Node.t; continue: Node.t; error: Node.t; normal: Node.t; } let match_cases_refutable cases = (* The parser guarantees irrefutable case will only appear at the last one. *) List.last cases >>| Match.Case.is_refutable |> Option.value ~default:true let pp format graph = let print_node index = Format.fprintf format "%a\n" Node.pp (Hashtbl.find_exn graph index) in Hashtbl.keys graph |> List.sort ~compare:Int.compare |> List.iter ~f:print_node let to_dot ?(precondition = fun _ -> "") ?(sort_labels = false) ?(single_line = false) graph = let newline_or_space = if single_line then " " else "\n" in let sorted_iteri table ~f = let map = Hashtbl.fold table ~init:Int.Map.empty ~f:(fun ~key ~data map -> Map.set ~key ~data map) in Map.iteri map ~f in let buffer = Buffer.create 10000 in Buffer.add_string buffer "digraph {"; Buffer.add_string buffer newline_or_space; let iteri = if sort_labels then sorted_iteri else Hashtbl.iteri in iteri ~f:(fun ~key ~data -> let label = Node.description data |> String.escaped in let label = Printf.sprintf " %d[label=\"%s\"]%s" key label newline_or_space in Buffer.add_string buffer label) graph; iteri ~f:(fun ~key ~data -> Set.iter ~f:(fun successor_id -> let edge = Printf.sprintf " %d -> %d [label=\"%s\", fontcolor=blue]%s" key successor_id (precondition successor_id) newline_or_space in Buffer.add_string buffer edge) (Node.successors data)) graph; Buffer.add_string buffer "}"; Buffer.contents buffer let show graph = Format.asprintf "%a" pp graph let entry_index = 0 let normal_index = 1 let error_index = 2 let exit_index = 3 let create define = Node.reset_count (); let graph = Int.Table.create () in let entry = Node.empty graph Node.Entry in assert (entry.Node.id = entry_index); let normal = Node.empty graph Node.Normal in assert (normal.Node.id = normal_index); let error = Node.empty graph Node.Error in assert (error.Node.id = error_index); let exit = Node.empty graph Node.Final in assert (exit.Node.id = exit_index); Node.connect normal exit; Node.connect error exit; (* Forward creation of the control flow for a list of statements. Returns the last node or `None` if the flow has ended in a jump. *) let rec create statements jumps predecessor = match statements with | { Ast.Node.value = Statement.For ({ For.body; orelse; _ } as loop); _ } :: statements -> (* ____________ * v | * -> [split] -> [preamble; body] * | \_________ * v v * [orelse] -> [join] -> *) let split = Node.empty graph (Node.For loop) in let join = Node.empty graph Node.Join in let loop_jumps = { jumps with break = join; continue = split } in Node.connect predecessor split; let preamble = For.preamble loop in let body = create (preamble :: body) loop_jumps split in Node.connect_option body split; let orelse = create orelse jumps split in Node.connect_option orelse join; Node.connect split join; create statements jumps join | { Ast.Node.value = Statement.If ({ If.test; body; orelse; _ } as conditional); _ } :: statements -> (* -> [split] -> [body] * | | * v v * [orelse] -> [join] -> *) let split = Node.empty graph (Node.If conditional) in let join = Node.empty graph Node.Join in Node.connect predecessor split; let body_node = let body_statements = let test = Expression.normalize test in Statement.assume ~origin:(Assert.Origin.If { true_branch = true }) test :: body in create body_statements jumps split in Node.connect_option body_node join; let orelse_statements = let test = Expression.negate test |> Expression.normalize in Statement.assume ~origin:(Assert.Origin.If { true_branch = false }) test :: orelse in let orelse = create orelse_statements jumps split in Node.connect_option orelse join; create statements jumps join | { Ast.Node.value = Match { subject; cases }; _ } :: statements -> (* The final else is optionally connected to the join node if it is refutable. * * predecessor -> [else 1] -> [else 2] -> ... -> [else n-1] -> [else n] * | | | | : * v v v v : * [case 1 [case 2 [case 3 ... [case n : * body] body] body] body] : * | | | | : * | | | | : * | \ \ \ v * \-----------------------------------------------------> [join] *) let join = Node.empty graph Node.Join in let from_case predecessor case = let test = MatchTranslate.to_condition ~subject ~case in let case_node = let test = Expression.normalize test in Node.empty graph (Node.Block [Statement.assume ~origin:Assert.Origin.Match test]) in Node.connect predecessor case_node; let case_node = create case.body jumps case_node in Node.connect_option case_node join; let else_node = let test = Expression.negate test |> Expression.normalize in Node.empty graph (Node.Block [Statement.assume ~origin:Assert.Origin.Match test]) in Node.connect predecessor else_node; else_node in let final_else = List.fold cases ~init:predecessor ~f:from_case in if match_cases_refutable cases then Node.connect final_else join; create statements jumps join | { Ast.Node.value = Try ({ Try.body; orelse; finally; handlers } as block); _ } :: statements -> (* We need to add edges to the "finally" block for all paths, because that block is always executed, regardless of the exit path (normal, return, or error), and we need to preserve the state that got us into the "finally". * -> [split] -> [body] -> [orelse] -> [finally_entry; finally_exit] -> next * | | | ^ statements * | -------/ | (on | * | | (on error) | error) | * V V | | * [dispatch] --> [handler1] -+-----------| * | --> [handler2] -+-----------/ * | | | * | <------------/ | * | (on error) | * | <--------------------/ * V * [global error exit] *) (* Scaffolding. *) let split = Node.empty graph (Node.Try block) in Node.connect predecessor split; let dispatch = Node.empty graph Node.Dispatch in Node.connect split dispatch; Node.connect dispatch jumps.error; let finally_entry = Node.empty graph (Node.Block []) in let finally_exit = create finally jumps finally_entry in (* Normal execution. *) let body_orelse = let body_jumps = { jumps with error = dispatch } in create body body_jumps split >>= create orelse jumps in Node.connect_option body_orelse finally_entry; (* Exception handling. *) let handler ({ Try.Handler.body; _ } as handler) = let preamble = Try.preamble handler in create (preamble @ body) jumps dispatch |> (Fn.flip Node.connect_option) finally_entry in List.iter handlers ~f:handler; if not (Node.has_predecessors finally_entry) then ( (* If the `finally` block has no predecessor, this must be because the body and all error handlers always return or raise exceptions. In that case, we add an edge from dispatch to `finally_entry` to make it reachable. We also assume `finally_exit` re-raises exceptions. * -> [split] -> [body] [finally_entry; finally_exit]-\ * | | ^ | * | -------/ | | * | | (on error) | | * V V | | * [dispatch] --------------------------/ | * | --> [handler1] | * | --> [handler2] | * | | | * | <------------/ | * | (on error) | * | <---------------------------------------------------------/ * V * [global error exit] *) Node.connect dispatch finally_entry; Node.connect_option finally_exit jumps.error; None) else (* `finally_exit` might legitimately not have an exit point if there is * a return in the `finally` clause. *) finally_exit >>= create statements jumps | { Ast.Node.value = With ({ With.body; _ } as block); _ } :: statements -> (* -> [split] -> [preamble; body] -> *) let split = Node.empty graph (Node.With block) in let preamble = With.preamble block in Node.connect predecessor split; create (preamble @ body) jumps split >>= create statements jumps | { Ast.Node.value = While ({ While.test; body; orelse } as loop); _ } :: statements -> (* _____________ * v | * -> [split] -> [body] * | : * v : * [orelse] -> [join] -> *) let split = Node.empty graph (Node.While loop) in let join = Node.empty graph Node.Join in let loop_jumps = { jumps with break = join; continue = split } in Node.connect predecessor split; let body = let body_statements = let test = Expression.normalize test in Statement.assume ~origin:(Assert.Origin.While { true_branch = true }) test :: body in create body_statements loop_jumps split in Node.connect_option body split; let orelse = let orelse_statements = let test = Expression.negate test |> Expression.normalize in Statement.assume ~origin:(Assert.Origin.While { true_branch = false }) test :: orelse in create orelse_statements jumps split in Node.connect_option orelse join; if Set.is_empty join.predecessors then Some split else create statements jumps join | statement :: statements -> ( (* -> [statement] -> * | * ? * [break | continue | error | normal ] *) let node = match predecessor.Node.kind with | Node.Block preceding_statements -> predecessor.Node.kind <- Node.Block (preceding_statements @ [statement]); predecessor | _ -> let node = Node.empty graph (Node.Block [statement]) in Node.connect predecessor node; node in match statement with | { Ast.Node.value = Assert { Assert.test; origin = Assert.Origin.Assertion; _ }; _ } when Expression.is_false test -> Node.connect node jumps.error; None | { Ast.Node.value = Assert { Assert.test; origin = Assert.Origin.While _; _ }; _ } when Expression.is_false test -> None | { Ast.Node.value = Break; _ } -> Node.connect node jumps.break; None | { Ast.Node.value = Continue; _ } -> Node.connect node jumps.continue; None | { Ast.Node.value = Raise _; _ } -> Node.connect node jumps.error; None | { Ast.Node.value = Return _; _ } -> Node.connect node jumps.normal; None | _ -> create statements jumps node) | [] -> Some predecessor in let jumps = { break = normal; continue = normal; error; normal } in let node = create define.Define.body jumps entry in Node.connect_option node normal; graph let node cfg ~id = Hashtbl.find_exn cfg id