(**************************************************************************)
(*                                                                        *)
(*                                 OCaml                                  *)
(*                                                                        *)
(*                       Pierre Chambart, OCamlPro                        *)
(*           Mark Shinwell and Leo White, Jane Street Europe              *)
(*                                                                        *)
(*   Copyright 2013--2016 OCamlPro SAS                                    *)
(*   Copyright 2014--2016 Jane Street Group LLC                           *)
(*                                                                        *)
(*   All rights reserved.  This file is distributed under the terms of    *)
(*   the GNU Lesser General Public License version 2.1, with the          *)
(*   special exception on linking described in the file LICENSE.          *)
(*                                                                        *)
(**************************************************************************)

[@@@ocaml.warning "+a-4-9-30-40-41-42-66"]
open! Int_replace_polymorphic_compare

module B = Inlining_cost.Benefit
module E = Inline_and_simplify_aux.Env
module R = Inline_and_simplify_aux.Result
module A = Simple_value_approx

let new_var name =
  Variable.create name
    ~current_compilation_unit:(Compilation_unit.get_current_exn ())

(** Fold over all variables bound by the given closure, which is bound to the
    variable [lhs_of_application], and corresponds to the given
    [function_decls].  Each variable bound by the closure is passed to the
    user-specified function as an [Flambda.named] value that projects the
    variable from its closure. *)
let fold_over_projections_of_vars_bound_by_closure ~closure_id_being_applied
      ~lhs_of_application ~bound_variables ~init ~f =
  Variable.Set.fold (fun var acc ->
      let expr : Flambda.named =
        Project_var {
          closure = lhs_of_application;
          closure_id = closure_id_being_applied;
          var = Var_within_closure.wrap var;
        }
      in
      f ~acc ~var ~expr)
    bound_variables
    init

let set_inline_attribute_on_all_apply body inline specialise =
  Flambda_iterators.map_toplevel_expr (function
      | Apply apply -> Apply { apply with inline; specialise }
      | expr -> expr)
    body

(** Assign fresh names for a function's parameters and rewrite the body to
    use these new names. *)
let copy_of_function's_body_with_freshened_params env
      ~(function_decl : A.function_declaration)
      ~(function_body : A.function_body) =
  let params = function_decl.params in
  let param_vars = Parameter.List.vars params in
  (* We cannot avoid the substitution in the case where we are inlining
     inside the function itself.  This can happen in two ways: either
     (a) we are inlining the function itself directly inside its declaration;
     or (b) we are inlining the function into an already-inlined copy.
     For (a) we cannot short-cut the substitution by freshening since the
     original [params] may still be referenced; for (b) we cannot do it
     either since the freshening may already be renaming the parameters for
     the first inlining of the function. *)
  if E.does_not_bind env param_vars
    && E.does_not_freshen env param_vars
  then
    params, function_body.body
  else
    let freshened_params = List.map (fun p -> Parameter.rename p) params in
    let subst =
      Variable.Map.of_list
        (List.combine param_vars (Parameter.List.vars freshened_params))
    in
    let body = Flambda_utils.toplevel_substitution subst function_body.body in
    freshened_params, body

(* CR-soon mshinwell: Add a note somewhere to explain why "bound by the closure"
   does not include the function identifiers for other functions in the same
   set of closures.
   mshinwell: The terminology may be used inconsistently. *)

(** Inline a function by copying its body into a context where it becomes
    closed.  That is to say, we bind the free variables of the body
    (= "variables bound by the closure"), and any function identifiers
    introduced by the corresponding set of closures. *)
let inline_by_copying_function_body ~env ~r
      ~lhs_of_application
      ~(inline_requested : Lambda.inline_attribute)
      ~(specialise_requested : Lambda.specialise_attribute)
      ~closure_id_being_applied
      ~(function_decl : A.function_declaration)
      ~(function_body : A.function_body)
      ~fun_vars
      ~args ~dbg ~simplify =
  assert (E.mem env lhs_of_application);
  assert (List.for_all (E.mem env) args);
  let r =
    if function_body.stub then r
    else R.map_benefit r B.remove_call
  in
  let freshened_params, body =
    copy_of_function's_body_with_freshened_params env
      ~function_decl ~function_body
  in
  let body =
    let default_inline =
      Lambda.equal_inline_attribute inline_requested Default_inline
    in
    let default_specialise =
      Lambda.equal_specialise_attribute specialise_requested Default_specialise
    in
    if function_body.stub
    && ((not default_inline) || (not default_specialise)) then
      (* When the function inlined function is a stub, the annotation
         is reported to the function applications inside the stub.
         This allows reporting the annotation to the application the
         original programmer really intended: the stub is not visible
         in the source. *)
      set_inline_attribute_on_all_apply body
        inline_requested specialise_requested
    else
      body
  in
  let bindings_for_params_to_args =
    (* Bind the function's parameters to the arguments from the call site. *)
    let args = List.map (fun arg -> Flambda.Expr (Var arg)) args in
    Flambda_utils.bind ~body
      ~bindings:(List.combine (Parameter.List.vars freshened_params) args)
  in
  (* Add bindings for the variables bound by the closure. *)
  let bindings_for_vars_bound_by_closure_and_params_to_args =
    let bound_variables =
      let params = Parameter.Set.vars function_decl.params in
      Variable.Set.diff
        (Variable.Set.diff function_body.free_variables params)
        fun_vars
    in
    fold_over_projections_of_vars_bound_by_closure ~closure_id_being_applied
      ~lhs_of_application ~bound_variables ~init:bindings_for_params_to_args
      ~f:(fun ~acc:body ~var ~expr -> Flambda.create_let var expr body)
  in
  (* Add bindings for variables corresponding to the functions introduced by
     the whole set of closures.  Each such variable will be bound to a closure;
     each such closure is in turn produced by moving from the closure being
     applied to another closure in the same set.
  *)
  let expr =
    Variable.Set.fold (fun another_closure_in_the_same_set expr ->
      let used =
        Variable.Set.mem another_closure_in_the_same_set
           function_body.free_variables
      in
      if used then
        Flambda.create_let another_closure_in_the_same_set
          (Move_within_set_of_closures {
            closure = lhs_of_application;
            start_from = closure_id_being_applied;
            move_to = Closure_id.wrap another_closure_in_the_same_set;
          })
          expr
      else expr)
      fun_vars
      bindings_for_vars_bound_by_closure_and_params_to_args
  in
  let env = E.set_never_inline env in
  let env = E.activate_freshening env in
  let env = E.set_inline_debuginfo ~dbg env in
  simplify env r expr

type state = {
  old_inside_to_new_inside : Variable.t Variable.Map.t;
    (* Map from old inner vars to new inner vars *)
  old_outside_to_new_outside : Variable.t Variable.Map.t;
    (* Map from old outer vars to new outer vars *)
  old_params_to_new_outside : Variable.t Variable.Map.t;
    (* Map from old parameters to new outer vars. These are params
       that should be specialised if they are copied to the new set of
       closures. *)
  old_fun_var_to_new_fun_var : Variable.t Variable.Map.t;
    (* Map from old fun vars to new fun vars. These are the functions
       that will be copied into the new set of closures *)
  let_bindings : (Variable.t * Flambda.named) list;
    (* Let bindings that will surround the definition of the new set
       of closures *)
  to_copy : Variable.t list;
    (* List of functions that still need to be copied to the new set
       of closures *)
  new_funs : Flambda.function_declaration Variable.Map.t;
    (* The function declarations for the new set of closures *)
  new_free_vars_with_old_projections : Flambda.specialised_to Variable.Map.t;
    (* The free variables for the new set of closures, but the projection
       fields still point to old free variables. *)
  new_specialised_args_with_old_projections :
    Flambda.specialised_to Variable.Map.t;
    (* The specialised parameters for the new set of closures, but the
       projection fields still point to old specialised parameters. *)
}

let empty_state =
  { to_copy = [];
    old_inside_to_new_inside   = Variable.Map.empty;
    old_outside_to_new_outside = Variable.Map.empty;
    old_params_to_new_outside = Variable.Map.empty;
    old_fun_var_to_new_fun_var = Variable.Map.empty;
    let_bindings = [];
    new_funs = Variable.Map.empty;
    new_free_vars_with_old_projections = Variable.Map.empty;
    new_specialised_args_with_old_projections = Variable.Map.empty; }

(* Add let bindings for the free vars in the set_of_closures and
   add them to [old_outside_to_new_outside] *)
let bind_free_vars ~lhs_of_application ~closure_id_being_applied
      ~state ~free_vars =
  Variable.Map.fold
    (fun free_var (spec : Flambda.specialised_to) state ->
       let var_clos = new_var Internal_variable_names.from_closure in
       let expr : Flambda.named =
         Project_var {
           closure = lhs_of_application;
           closure_id = closure_id_being_applied;
           var = Var_within_closure.wrap free_var;
         }
       in
       let let_bindings = (var_clos, expr) :: state.let_bindings in
       let old_outside_to_new_outside =
         Variable.Map.add spec.var var_clos state.old_outside_to_new_outside
       in
       { state with let_bindings; old_outside_to_new_outside })
    free_vars state

(* For arguments of specialised parameters:
   - Add them to [old_outside_to_new_outside]
   - Add them and their invariant aliases to [old_params_to_new_outside]
   For other arguments that are also worth specialising:
   - Add them and their invariant aliases to [old_params_to_new_outside] *)
let register_arguments ~specialised_args ~invariant_params
      ~state ~params ~args ~args_approxs =
  let rec loop ~state ~params ~args ~args_approxs =
    match params, args, args_approxs with
    | [], [], [] -> state
    | param :: params, arg :: args, arg_approx :: args_approxs -> begin
        let param = Parameter.var param in
        let worth_specialising, old_outside_to_new_outside =
          match Variable.Map.find_opt param specialised_args with
          | Some (spec : Flambda.specialised_to) ->
              let old_outside_to_new_outside =
                Variable.Map.add spec.var arg state.old_outside_to_new_outside
              in
              true, old_outside_to_new_outside
          | None ->
              let worth_specialising =
                A.useful arg_approx
                && Variable.Map.mem param (Lazy.force invariant_params)
              in
              worth_specialising, state.old_outside_to_new_outside
        in
        let old_params_to_new_outside =
          if worth_specialising then begin
            let old_params_to_new_outside =
              Variable.Map.add param arg state.old_params_to_new_outside
            in
            match Variable.Map.find_opt param (Lazy.force invariant_params) with
            | Some set ->
                Variable.Set.fold
                  (fun elem acc -> Variable.Map.add elem arg acc)
                  set old_params_to_new_outside
            | None ->
                old_params_to_new_outside
          end else begin
            state.old_params_to_new_outside
          end
        in
        let state =
          { state with old_outside_to_new_outside; old_params_to_new_outside }
        in
        loop ~state ~params ~args ~args_approxs
      end
    | _, _, _ -> assert false
  in
  loop ~state ~params ~args ~args_approxs

(* Add an old parameter to [old_inside_to_new_inside]. If it appears in
   [old_params_to_new_outside] then also add it to the new specialised args. *)
let add_param ~specialised_args ~state ~param =
  let param = Parameter.var param in
  let new_param = Variable.rename param in
  let old_inside_to_new_inside =
    Variable.Map.add param new_param state.old_inside_to_new_inside
  in
  let new_specialised_args_with_old_projections =
    match Variable.Map.find_opt param specialised_args with
    | Some (spec : Flambda.specialised_to) ->
        let new_outside_var =
          Variable.Map.find spec.var state.old_outside_to_new_outside
        in
        let new_spec : Flambda.specialised_to =
          { spec with var = new_outside_var }
        in
        Variable.Map.add new_param new_spec
          state.new_specialised_args_with_old_projections
    | None -> begin
        match Variable.Map.find_opt param state.old_params_to_new_outside with
        | None -> state.new_specialised_args_with_old_projections
        | Some new_outside_var ->
            let new_spec : Flambda.specialised_to =
              { var = new_outside_var; projection = None }
            in
            Variable.Map.add new_param new_spec
              state.new_specialised_args_with_old_projections
      end
  in
  let state =
    { state with old_inside_to_new_inside;
                 new_specialised_args_with_old_projections }
  in
  state, Parameter.wrap new_param

(* Add a let binding for an old fun_var, add it to the new free variables, and
   add it to [old_inside_to_new_inside] *)
let add_fun_var ~lhs_of_application ~closure_id_being_applied ~state ~fun_var =
  if Variable.Map.mem fun_var state.old_inside_to_new_inside then state
  else begin
    let inside_var = Variable.rename fun_var in
    let outside_var = Variable.create Internal_variable_names.closure in
    let expr =
      Flambda.Move_within_set_of_closures
        { closure    = lhs_of_application;
          start_from = closure_id_being_applied;
          move_to    = Closure_id.wrap fun_var; }
    in
    let let_bindings = (outside_var, expr) :: state.let_bindings in
    let spec : Flambda.specialised_to =
      { var = outside_var; projection = None; }
    in
    let new_free_vars_with_old_projections =
      Variable.Map.add inside_var spec state.new_free_vars_with_old_projections
    in
    let old_inside_to_new_inside =
      Variable.Map.add fun_var inside_var state.old_inside_to_new_inside
    in
    { state with
        old_inside_to_new_inside; let_bindings;
        new_free_vars_with_old_projections }
  end

(* Add an old free_var to the new free variables and add it to
   [old_inside_to_new_inside]. *)
let add_free_var ~free_vars ~state ~free_var =
  if Variable.Map.mem free_var state.old_inside_to_new_inside then state
  else begin
    let spec : Flambda.specialised_to = Variable.Map.find free_var free_vars in
    let outside_var = spec.var in
    let new_outside_var =
      Variable.Map.find outside_var state.old_outside_to_new_outside
    in
    let new_spec : Flambda.specialised_to =
      { spec with var = new_outside_var }
    in
    let new_inside_var = Variable.rename free_var in
    let new_free_vars_with_old_projections =
      Variable.Map.add new_inside_var new_spec
        state.new_free_vars_with_old_projections
    in
    let old_inside_to_new_inside =
      Variable.Map.add free_var new_inside_var state.old_inside_to_new_inside
    in
    { state with old_inside_to_new_inside; new_free_vars_with_old_projections }
  end

(* Add a function to the new set of closures iff:
   1) All its specialised parameters are available in
      [old_outside_to_new_outside]
   2) At least one more parameter will become specialised *)
let add_function ~specialised_args ~state ~fun_var ~function_decl =
  match function_decl.A.function_body with
  | None -> None
  | Some _ -> begin
    let rec loop worth_specialising = function
      | [] -> worth_specialising
      | param :: params -> begin
          let param = Parameter.var param in
          match Variable.Map.find_opt param specialised_args with
          | Some (spec : Flambda.specialised_to) ->
              Variable.Map.mem spec.var state.old_outside_to_new_outside
              && loop worth_specialising params
          | None ->
              let worth_specialising =
                worth_specialising
                || Variable.Map.mem param state.old_params_to_new_outside
              in
              loop worth_specialising params
        end
    in
    let worth_specialising = loop false function_decl.A.params in
    if not worth_specialising then None
    else begin
      let new_fun_var = Variable.rename fun_var in
      let old_fun_var_to_new_fun_var =
        Variable.Map.add fun_var new_fun_var state.old_fun_var_to_new_fun_var
      in
      let to_copy = fun_var :: state.to_copy in
      let state = { state with old_fun_var_to_new_fun_var; to_copy } in
      Some (state, new_fun_var)
    end
  end

(* Lookup a function in the new set of closures, trying to add it if
   necessary. *)
let lookup_function ~specialised_args ~state ~fun_var ~function_decl =
  match Variable.Map.find_opt fun_var state.old_fun_var_to_new_fun_var with
  | Some new_fun_var -> Some (state, new_fun_var)
  | None -> add_function ~specialised_args ~state ~fun_var ~function_decl

(* A direct call to a function in the new set of closures can be specialised
   if all the function's newly specialised parameters are passed arguments
   that are specialised to the same outside variable *)
let specialisable_call ~specialised_args ~state ~args ~params =
  List.for_all2
    (fun arg param ->
       let param = Parameter.var param in
       if Variable.Map.mem param specialised_args then true
       else begin
         let old_params_to_new_outside = state.old_params_to_new_outside in
         match Variable.Map.find_opt param old_params_to_new_outside with
         | None -> true
         | Some outside_var -> begin
             match Variable.Map.find_opt arg old_params_to_new_outside with
             | Some outside_var' ->
               Variable.equal outside_var outside_var'
             | None -> false
           end
       end)
    args params

(* Rewrite a call iff:
   1) It is to a function in the old set of closures that can be specialised
   2) All the newly specialised parameters of that function are passed values
      known to be equal to their new specialisation. *)
let rec rewrite_direct_call ~specialised_args ~funs ~direct_call_surrogates
      ~state ~closure_id ~(apply : Flambda.apply) =
  match Closure_id.Map.find_opt closure_id direct_call_surrogates with
  | Some closure_id ->
      rewrite_direct_call ~specialised_args ~funs ~direct_call_surrogates
        ~state ~closure_id ~apply
  | None -> begin
      let fun_var = Closure_id.unwrap closure_id in
      match Variable.Map.find_opt fun_var funs with
      | None -> None
      | Some function_decl -> begin
          match
            lookup_function ~specialised_args ~state ~fun_var ~function_decl
          with
          | None -> None
          | Some (state, new_fun_var) -> begin
              let args = apply.args in
              let params = function_decl.A.params in
              let specialisable =
                specialisable_call ~specialised_args ~state ~args ~params
              in
              if not specialisable then None
              else begin
                let kind = Flambda.Direct (Closure_id.wrap new_fun_var) in
                let apply = { apply with func = new_fun_var; kind } in
                Some (state, Flambda.Apply apply)
              end
            end
        end
    end

(* Rewrite the body a function declaration for use in the new set of
   closures. *)
let rewrite_function ~lhs_of_application ~closure_id_being_applied
      ~direct_call_surrogates ~specialised_args ~free_vars ~funs
      ~state fun_var =
  let function_decl : A.function_declaration =
    Variable.Map.find fun_var funs
  in
  let function_body =
    match function_decl.function_body with
    | None -> assert false
    | Some function_body -> function_body
  in
  let new_fun_var =
    Variable.Map.find fun_var state.old_fun_var_to_new_fun_var
  in
  let state, params =
    List.fold_right
      (fun param (state, params) ->
         let state, param = add_param ~specialised_args ~state ~param in
         (state, param :: params))
      function_decl.params (state, [])
  in
  let state =
    Variable.Set.fold
      (fun var state ->
         if Variable.Map.mem var funs then
           add_fun_var ~lhs_of_application ~closure_id_being_applied
             ~state ~fun_var:var
         else if Variable.Map.mem var free_vars then
           add_free_var ~free_vars ~state ~free_var:var
         else
           state)
      function_body.free_variables state
  in
  let state_ref = ref state in
  let body =
    Flambda_iterators.map_toplevel_expr
      (fun (expr : Flambda.t) ->
         match expr with
         | Apply ({ kind = Direct closure_id } as apply) -> begin
             match
               rewrite_direct_call ~specialised_args ~funs
                 ~direct_call_surrogates ~state:!state_ref ~closure_id ~apply
             with
             | None -> expr
             | Some (state, expr) ->
                 state_ref := state;
                 expr
           end
         | _ -> expr)
      function_body.body
  in
  let body =
    Flambda_utils.toplevel_substitution state.old_inside_to_new_inside body
  in
  let new_function_decl =
    Flambda.create_function_declaration
      ~params ~body
      ~stub:function_body.stub
      ~dbg:function_body.dbg
      ~inline:function_body.inline
      ~specialise:function_body.specialise
      ~is_a_functor:function_body.is_a_functor
      ~closure_origin:(Closure_origin.create (Closure_id.wrap new_fun_var))
      ~poll:function_body.poll
  in
  let new_funs =
    Variable.Map.add new_fun_var new_function_decl state.new_funs
  in
  let state = { !state_ref with new_funs } in
  state

let update_projections ~state projections =
  let old_to_new = state.old_inside_to_new_inside in
  Variable.Map.map
    (fun (spec_to : Flambda.specialised_to) ->
       let projection : Projection.t option =
         match spec_to.projection with
         | None -> None
         | Some (Project_var proj) -> begin
             match Variable.Map.find_opt proj.closure old_to_new with
             | None -> None
             | Some closure ->
                 let proj = { proj with closure } in
                 Some (Projection.Project_var proj)
           end
         | Some (Project_closure proj) -> begin
             match Variable.Map.find_opt proj.set_of_closures old_to_new with
             | None -> None
             | Some set_of_closures ->
                 let proj = { proj with set_of_closures } in
                 Some (Projection.Project_closure proj)
           end
         | Some (Move_within_set_of_closures proj) -> begin
             match Variable.Map.find_opt proj.closure old_to_new with
             | None -> None
             | Some closure ->
                 let proj = { proj with closure } in
                 Some (Projection.Move_within_set_of_closures proj)
           end
         | Some (Field (index, var)) -> begin
             match Variable.Map.find_opt var old_to_new with
             | None -> None
             | Some var -> Some (Projection.Field(index, var))
           end
      in
      { spec_to with projection })
    projections

let inline_by_copying_function_declaration
    ~(env : Inline_and_simplify_aux.Env.t)
    ~(r : Inline_and_simplify_aux.Result.t)
    ~(function_decls : A.function_declarations)
    ~(lhs_of_application : Variable.t)
    ~(inline_requested : Lambda.inline_attribute)
    ~(closure_id_being_applied : Closure_id.t)
    ~(function_decl : A.function_declaration)
    ~(args : Variable.t list)
    ~(args_approxs : A.t list)
    ~(invariant_params : Variable.Set.t Variable.Map.t lazy_t)
    ~(specialised_args : Flambda.specialised_to Variable.Map.t)
    ~(free_vars : Flambda.specialised_to Variable.Map.t)
    ~(direct_call_surrogates : Closure_id.t Closure_id.Map.t)
    ~(dbg : Debuginfo.t)
    ~(simplify : Inlining_decision_intf.simplify) =
  let state = empty_state in
  let state =
    bind_free_vars ~lhs_of_application ~closure_id_being_applied
      ~state ~free_vars
  in
  let params = function_decl.params in
  let state =
    register_arguments ~specialised_args ~invariant_params
      ~state ~params ~args ~args_approxs
  in
  let fun_var = Closure_id.unwrap closure_id_being_applied in
  match add_function ~specialised_args ~state ~fun_var ~function_decl with
  | None -> None
  | Some (state, new_fun_var) -> begin
      let funs = function_decls.funs in
      let rec loop state =
        match state.to_copy with
        | [] -> state
        | next :: rest ->
          let state = { state with to_copy = rest } in
          let state =
            rewrite_function ~lhs_of_application ~closure_id_being_applied
              ~direct_call_surrogates ~specialised_args ~free_vars ~funs
              ~state next
          in
          loop state
      in
      let state = loop state in
      let closure_id = Closure_id.wrap new_fun_var in
      let function_decls =
        Flambda.create_function_declarations_with_origin
          ~funs:state.new_funs
          ~set_of_closures_origin:function_decls.set_of_closures_origin
          ~is_classic_mode:function_decls.is_classic_mode
      in
      let free_vars =
        update_projections ~state
          state.new_free_vars_with_old_projections
      in
      let specialised_args =
        update_projections ~state
          state.new_specialised_args_with_old_projections
      in
      let direct_call_surrogates = Variable.Map.empty in
      let set_of_closures =
        Flambda.create_set_of_closures ~function_decls
          ~free_vars ~specialised_args ~direct_call_surrogates
      in
      let closure_var = new_var Internal_variable_names.dup_func in
      let set_of_closures_var =
        new_var Internal_variable_names.dup_set_of_closures
      in
      let project : Flambda.project_closure =
        {set_of_closures = set_of_closures_var; closure_id}
      in
      let apply : Flambda.apply =
        { func = closure_var; args; kind = Direct closure_id; dbg;
          inline = inline_requested; specialise = Default_specialise; }
      in
      let body =
        Flambda.create_let
          set_of_closures_var (Set_of_closures set_of_closures)
          (Flambda.create_let closure_var (Project_closure project)
             (Apply apply))
      in
      let expr = Flambda_utils.bind ~body ~bindings:state.let_bindings in
      let env = E.activate_freshening (E.set_never_inline env) in
      Some (simplify env r expr)
    end
