source/analysis/refinement.ml (368 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 Annotation module MapLattice = struct module type MapSignature = sig type key type 'data t val empty : 'data t val mem : 'data t -> key -> bool val set : 'data t -> key:key -> data:'data -> 'data t val find : 'data t -> key -> 'data option val remove : 'data t -> key -> 'data t val fold : 'data t -> init:'a -> f:(key:key -> data:'data -> 'a -> 'a) -> 'a val fold2 : 'data t -> 'data t -> init:'a -> f:(key:key -> data:[ `Both of 'data * 'data | `Left of 'data | `Right of 'data ] -> 'a -> 'a) -> 'a end module Make (Map : MapSignature) = struct include Map (** The keys of `right` have to be a subset of the keys of `left` for `left` to be less than or equal to `right`, since more keys = more restrictions = lower in the lattice *) let less_or_equal ~less_or_equal_one ~left ~right = let f ~key ~data:right_data sofar = sofar && match find left key with | Some left_data -> less_or_equal_one ~left:left_data ~right:right_data | None -> false in fold right ~init:true ~f let join ~join_one left right = let f ~key ~data sofar = match data with | `Both (left, right) -> set sofar ~key ~data:(join_one left right) | `Left _ | `Right _ -> sofar in fold2 left right ~init:empty ~f let meet ~meet_one left right = let f ~key ~data sofar = match data with | `Both (left, right) -> set sofar ~key ~data:(meet_one left right) | `Left data | `Right data -> set sofar ~key ~data in fold2 left right ~init:empty ~f (** Merge maps (take the union of all keys) *) let merge_with ~merge_one left right = let f ~key ~data sofar = match data with | `Both (left, right) -> set sofar ~key ~data:(merge_one left right) | `Left data | `Right data -> set sofar ~key ~data in fold2 left right ~init:empty ~f let update_existing ~old_map ~new_map = let update_unit ~key ~data map = if mem map key then set ~key ~data map else map in fold ~init:old_map ~f:update_unit new_map end end module IdentifierMap = MapLattice.Make (struct include Identifier.Map.Tree type key = Identifier.t end) module ReferenceMap = MapLattice.Make (struct include Reference.Map type key = Reference.t end) module Unit = struct type t = { base: Annotation.t option; attributes: t Identifier.Map.Tree.t; } [@@deriving eq] let empty = { base = None; attributes = IdentifierMap.empty } let create base = { empty with base = Some base } let create_mutable type_ = create (Annotation.create_mutable type_) let top = create (Annotation.create_mutable Type.Top) let rec pp format { base; attributes } = let attribute_map_entry (identifier, refinement_unit) = Format.asprintf "%a -> %a" Identifier.pp identifier pp refinement_unit in (match base with | Some base -> Format.fprintf format "[Base: %a; " Annotation.pp base | None -> Format.fprintf format "[Base: (); "); Map.Tree.to_alist attributes |> List.map ~f:attribute_map_entry |> String.concat ~sep:", " |> Format.fprintf format "Attributes: [%s]]" let show = Format.asprintf "%a" pp let find = Identifier.Map.Tree.find let base { base; _ } = base let set_base refinement_unit ~base = { refinement_unit with base = Some base } let set_base_if_none refinement_unit ~base = { refinement_unit with base = Option.first_some refinement_unit.base base } (** If `attribute_path` is empty, set the base annotation. Otherwise, find the appropriate attribute (traversing intermediate units and constructing new ones as needed) and set the base there. *) let set_annotation ?(wipe_subtree = false) ~attribute_path ~annotation refinement_unit = let rec recurse ~annotation ~identifiers ({ attributes; _ } as refinement_unit) = match identifiers with | [] -> if wipe_subtree then { empty with base = Some annotation } else { refinement_unit with base = Some annotation } | identifier :: identifiers -> { refinement_unit with attributes = attributes |> IdentifierMap.set ~key:identifier ~data: (find attributes identifier |> Option.value ~default:empty |> recurse ~annotation ~identifiers); } in recurse ~annotation ~identifiers:(attribute_path |> Reference.as_list) refinement_unit (** If `attribute_path` is empty, get the base annotation. Otherwise, find the appropriate attribute (traversing intermediate units until we finish or hit a dead end) and return the base found there, if any *) let get_annotation ~attribute_path refinement_unit = let rec recurse { base; attributes } ~identifiers = match identifiers with | [] -> base | identifier :: identifiers -> ( match find attributes identifier with | Some refinement_unit -> recurse refinement_unit ~identifiers | None -> None) in recurse refinement_unit ~identifiers:(attribute_path |> Reference.as_list) let rec less_or_equal ~global_resolution ~left ~right = let base_less_or_equal left_base right_base = match left_base, right_base with | Some left, Some right -> Annotation.less_or_equal ~type_less_or_equal:(GlobalResolution.less_or_equal global_resolution) ~left ~right | None, None -> true (* intermediate refinement units don't require computation *) | _ -> false in let less_or_equal_one = less_or_equal ~global_resolution in base_less_or_equal left.base right.base && IdentifierMap.less_or_equal ~less_or_equal_one ~left:left.attributes ~right:right.attributes let rec join ~global_resolution left right = if equal left top || equal right top then top else let should_recurse, base = match left.base, right.base with | Some left, Some right -> ( GlobalResolution.types_are_orderable global_resolution left.annotation right.annotation, Some (Annotation.join ~type_join:(GlobalResolution.join global_resolution) left right) ) | None, None -> (* you only want to continue the nested join should both attribute trees exist *) not (Map.Tree.is_empty left.attributes || Map.Tree.is_empty right.attributes), None | _ -> false, None in let attributes = if should_recurse then IdentifierMap.join ~join_one:(join ~global_resolution) left.attributes right.attributes else IdentifierMap.empty in { base; attributes } let rec meet ~global_resolution left right = let should_recurse, base = match left.base, right.base with | Some left, Some right -> ( GlobalResolution.types_are_orderable global_resolution left.annotation right.annotation, Some (Annotation.meet ~type_meet:(GlobalResolution.meet global_resolution) left right) ) | None, None -> (* you only want to continue the nested meet should at least one attribute tree exists *) not (Map.Tree.is_empty left.attributes && Map.Tree.is_empty right.attributes), None | _ -> false, None in let attributes = if should_recurse then IdentifierMap.meet ~meet_one:(meet ~global_resolution) left.attributes right.attributes else IdentifierMap.empty in { base; attributes } let widen ~global_resolution ~widening_threshold ~iteration left right = if iteration + 1 >= widening_threshold then create (Annotation.create_mutable Type.Top) else join ~global_resolution left right end module Store = struct type t = { annotations: Unit.t Reference.Map.t; temporary_annotations: Unit.t Reference.Map.t; } [@@deriving eq] let empty = { annotations = ReferenceMap.empty; temporary_annotations = ReferenceMap.empty } let pp format { annotations; temporary_annotations } = let show_annotation (reference, unit) = Format.asprintf "%a -> %a" Reference.pp reference Unit.pp unit in Map.to_alist annotations |> List.map ~f:show_annotation |> String.concat ~sep:", " |> Format.fprintf format "Annotations: [%s]\n"; Map.to_alist temporary_annotations |> List.map ~f:show_annotation |> String.concat ~sep:", " |> Format.fprintf format "Temporary Annotations: [%s]" let show = Format.asprintf "%a" pp let has_nontemporary_annotation ~name { annotations; _ } = ReferenceMap.mem annotations name let get_unit ?(include_temporary = true) ~name { annotations; temporary_annotations } = let temporary = if include_temporary then ReferenceMap.find temporary_annotations name else None in let found = match temporary with | Some _ -> temporary | None -> ReferenceMap.find annotations name in Option.value ~default:Unit.empty found (** Map an operation over what's at a given name. If there's nothing already existing, use `empty`. The way we handle temporary vs non-temporary is very particular: - If `temporary` is true we only apply this to `temporary_annotations` - Otherwise, we apply it to `annotations` and also apply it to any *existing* data in `temporary_annotations`, but we don't create any new `temporary_annotations`. - The idea here is to minimize the amount of duplicated data, but ensure that `annotations` and `temporary_annotations` always have a consistent view of (non-temporary) refinements. *) let map_over_name ~temporary ~name ~f { annotations; temporary_annotations } = let map_over_reference_map ~fallback reference_map = match Option.first_some (ReferenceMap.find reference_map name) fallback with | Some unit -> ReferenceMap.set ~key:name ~data:(f unit) reference_map | None -> reference_map in if temporary then { annotations; temporary_annotations = map_over_reference_map ~fallback:(Some Unit.empty) temporary_annotations; } else { annotations = map_over_reference_map ~fallback:(Some Unit.empty) annotations; temporary_annotations = map_over_reference_map ~fallback:None temporary_annotations; } let get_base ~name store = get_unit ~name store |> Unit.base let get_annotation ~name ~attribute_path store = get_unit ~name store |> Unit.get_annotation ~attribute_path let set_base ?(temporary = false) ~name ~base store = map_over_name ~temporary ~name ~f:(Unit.set_base ~base) store let new_as_base ?(temporary = false) ~name ~base { annotations; temporary_annotations } = if temporary then { annotations; temporary_annotations = ReferenceMap.set temporary_annotations ~key:name ~data:(Unit.create base); } else { annotations = ReferenceMap.set annotations ~key:name ~data:(Unit.create base); temporary_annotations = ReferenceMap.remove temporary_annotations name; } let set_annotation ?(temporary = false) ?(wipe_subtree = false) ~name ~attribute_path ~base ~annotation store = let set_unit_annotation unit = unit |> Unit.set_annotation ~wipe_subtree ~attribute_path ~annotation |> Unit.set_base_if_none ~base in map_over_name ~temporary ~name ~f:set_unit_annotation store let less_or_equal ~global_resolution ~left ~right = let less_or_equal_one = Unit.less_or_equal ~global_resolution in ReferenceMap.less_or_equal ~less_or_equal_one ~left:left.annotations ~right:right.annotations && ReferenceMap.less_or_equal ~less_or_equal_one ~left:left.temporary_annotations ~right:right.temporary_annotations (** Whenever we know for sure that right is pointwise less_or_equal to left, then we can save computation by only checking for equality pointwise, which doesn't require type ordering operations *) let less_or_equal_monotone ~left ~right = let less_or_equal_one ~left ~right = Unit.equal left right in ReferenceMap.less_or_equal ~less_or_equal_one ~left:left.annotations ~right:right.annotations && ReferenceMap.less_or_equal ~less_or_equal_one ~left:left.temporary_annotations ~right:right.temporary_annotations let meet ~global_resolution left right = let meet_one = Unit.meet ~global_resolution in { annotations = ReferenceMap.meet ~meet_one left.annotations right.annotations; temporary_annotations = ReferenceMap.meet ~meet_one left.temporary_annotations right.temporary_annotations; } (** Use an "outer" join to join or widen stores, which means we are strict about types (a proper join) but permissive about variables that might only be instantiated on one side. This can be done as either a join or a widen depending whether we set `widening_threshod`, which is applied at the `Refinement.Unit` level. *) let widen_or_join ~merge_one left right = { (* Newly-instantiated locals live in `annotations`, so we merge with join *) annotations = ReferenceMap.merge_with ~merge_one left.annotations right.annotations; (* `temporary_annotations` only has type info, so we do a proper join *) temporary_annotations = ReferenceMap.join ~join_one:merge_one left.temporary_annotations right.temporary_annotations; } let outer_join ~global_resolution = let merge_one = Unit.join ~global_resolution in widen_or_join ~merge_one let outer_widen ~global_resolution ~iteration ~widening_threshold = let merge_one = Unit.widen ~global_resolution ~iteration ~widening_threshold in widen_or_join ~merge_one let update_existing ~old_store ~new_store = { annotations = ReferenceMap.update_existing ~old_map:old_store.annotations ~new_map:new_store.annotations; temporary_annotations = ReferenceMap.update_existing ~old_map:old_store.temporary_annotations ~new_map:new_store.temporary_annotations; } let update_with_filter ~old_store ~new_store ~filter = let update_map old_map new_map = let f ~key ~data sofar = if Unit.base data |> Option.map ~f:(filter key) |> Option.value ~default:false then sofar |> ReferenceMap.set ~key ~data else sofar in ReferenceMap.fold ~init:old_map ~f new_map in { annotations = update_map old_store.annotations new_store.annotations; temporary_annotations = update_map old_store.temporary_annotations new_store.temporary_annotations; } end