(*
 * Copyright (c) 2017-present, Facebook, Inc.
 *
 * 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 type HilConfig = sig
  val include_array_indexes : bool
end

module DefaultConfig : HilConfig = struct
  let include_array_indexes = false
end

(** HIL adds a map from temporary variables to access paths to each domain *)
module MakeHILDomain (Domain : AbstractDomain.S) = struct
  include AbstractDomain.Pair (Domain) (Bindings)

  (** hides HIL implementation details *)
  let pp fmt (astate, id_map) =
    if Config.debug_level_analysis >= 3 then
      Format.fprintf fmt "Bindings: @[<h>%a@]@\n" Bindings.pp id_map ;
    Domain.pp fmt astate
end

module Make (TransferFunctions : TransferFunctions.HIL) (HilConfig : HilConfig) = struct
  module CFG = TransferFunctions.CFG
  module Domain = MakeHILDomain (TransferFunctions.Domain)

  type extras = TransferFunctions.extras

  let pp_session_name = TransferFunctions.pp_session_name

  let is_java_unlock pname actuals =
    (* would check is_java, but we want to include builtins too *)
    (not (Typ.Procname.is_c_method pname))
    && match ConcurrencyModels.get_lock_effect pname actuals with Unlock _ -> true | _ -> false


  let exec_instr_actual extras bindings node hil_instr actual_state =
    match (hil_instr : HilInstr.t) with
    | Call (_, Direct callee_pname, actuals, _, loc) as hil_instr
      when is_java_unlock callee_pname actuals ->
        (* need to be careful not to move reads/writes out of a critical section due to odd
           temporaries introduced in our translation of try/synchronized in Java. to ensure this,
           "dump" all of the temporaries out of the id map, then execute the unlock instruction. *)
        let actual_state' =
          Bindings.fold bindings ~init:actual_state ~f:(fun id access_expr astate_acc ->
              let lhs_access_path = HilExp.AccessExpression.base (id, Typ.mk Typ.Tvoid) in
              let dummy_assign =
                HilInstr.Assign (lhs_access_path, HilExp.AccessExpression access_expr, loc)
              in
              TransferFunctions.exec_instr astate_acc extras node dummy_assign )
        in
        (TransferFunctions.exec_instr actual_state' extras node hil_instr, Bindings.empty)
    | hil_instr ->
        (TransferFunctions.exec_instr actual_state extras node hil_instr, bindings)


  let append_bindings = IList.append_no_duplicates ~cmp:Var.compare |> Staged.unstage

  let hil_instr_of_sil bindings instr =
    let hil_translation =
      let f_resolve_id = Bindings.resolve bindings in
      HilInstr.of_sil ~include_array_indexes:HilConfig.include_array_indexes ~f_resolve_id instr
    in
    match hil_translation with
    | Bind (id, access_path) ->
        (None, Bindings.add id access_path bindings)
    | Instr (Metadata (ExitScope (vars, loc))) ->
        let bindings, vars =
          List.fold vars ~init:(bindings, []) ~f:(fun (bindings, vars) var ->
              let bindings, vars' = Bindings.exit_scope var bindings in
              (bindings, append_bindings vars vars') )
        in
        let instr =
          if List.is_empty vars then None else Some (HilInstr.Metadata (ExitScope (vars, loc)))
        in
        (instr, bindings)
    | Instr instr ->
        (Some instr, bindings)


  let exec_instr ((actual_state, bindings) as astate) extras node instr =
    let actual_state', bindings' =
      match hil_instr_of_sil bindings instr with
      | None, bindings ->
          (actual_state, bindings)
      | Some hil_instr, bindings ->
          exec_instr_actual extras bindings node hil_instr actual_state
    in
    if phys_equal bindings bindings' && phys_equal actual_state actual_state' then astate
    else (actual_state', bindings')
end

module type S = sig
  module Interpreter : AbstractInterpreter.S

  type domain

  val compute_post :
    Interpreter.TransferFunctions.extras ProcData.t -> initial:domain -> domain option
end

module MakeAbstractInterpreterWithConfig
    (MakeAbstractInterpreter : AbstractInterpreter.Make)
    (HilConfig : HilConfig)
    (TransferFunctions : TransferFunctions.HIL) :
  S
  with type domain = TransferFunctions.Domain.t
   and module Interpreter = MakeAbstractInterpreter(Make(TransferFunctions)(HilConfig)) = struct
  module LowerHilInterpreter = Make (TransferFunctions) (HilConfig)
  module Interpreter = MakeAbstractInterpreter (LowerHilInterpreter)

  type domain = TransferFunctions.Domain.t

  let compute_post ({ProcData.pdesc; tenv} as proc_data) ~initial =
    Preanal.do_preanalysis pdesc tenv ;
    let initial' = (initial, Bindings.empty) in
    let pp_instr (_, bindings) instr =
      match LowerHilInterpreter.hil_instr_of_sil bindings instr with
      | Some hil_instr, _ ->
          Some (fun f -> Format.fprintf f "@[<h>%a@];@;" HilInstr.pp hil_instr)
      | None, _ ->
          None
    in
    Interpreter.compute_post ~pp_instr proc_data ~initial:initial' |> Option.map ~f:fst
end

module MakeAbstractInterpreter (TransferFunctions : TransferFunctions.HIL) =
  MakeAbstractInterpreterWithConfig (AbstractInterpreter.MakeRPO) (DefaultConfig)
    (TransferFunctions)
