(**************************************************************************)
(*                                                                        *)
(*                                 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

(** A variable in a closure can either be used by the closure itself
    or by an inlined version of the function. *)
let remove_unused_closure_variables ~remove_direct_call_surrogates program =
  let used_vars_within_closure, used_closure_ids =
    let used = Var_within_closure.Tbl.create 13 in
    let used_fun = Closure_id.Tbl.create 13 in
    let aux_named (named : Flambda.named) =
      match named with
      | Project_closure { set_of_closures = _; closure_id } ->
        Closure_id.Tbl.add used_fun closure_id ()
      | Project_var { closure_id; var } ->
        Var_within_closure.Tbl.add used var ();
        Closure_id.Tbl.add used_fun closure_id ()
      | Move_within_set_of_closures { closure = _; start_from; move_to } ->
        Closure_id.Tbl.add used_fun start_from ();
        Closure_id.Tbl.add used_fun move_to ()
      | Symbol _ | Const _ | Set_of_closures _ | Prim _ | Expr _
      | Allocated_const _ | Read_mutable _ | Read_symbol_field _ -> ()
    in
    Flambda_iterators.iter_named_of_program ~f:aux_named program;
    used, used_fun
  in
  let aux_named _ (named : Flambda.named) : Flambda.named =
    match named with
    | Set_of_closures ({ function_decls; free_vars; _ } as set_of_closures) ->
      let direct_call_surrogates =
        if remove_direct_call_surrogates then Variable.Set.empty
        else
          Variable.Set.of_list
            (Variable.Map.data set_of_closures.direct_call_surrogates)
      in
      let rec add_needed needed_funs remaining_funs free_vars_of_kept_funs =
        let new_needed_funs, remaining_funs =
          (* Keep a function if it is used either by the rest of the code,
             (in used_closure_ids), or by any other kept function
             (in free_vars_of_kept_funs) *)
          Variable.Map.partition (fun fun_id _ ->
              Variable.Set.mem fun_id free_vars_of_kept_funs
              || Closure_id.Tbl.mem used_closure_ids
                (Closure_id.wrap fun_id)
              || Variable.Set.mem fun_id direct_call_surrogates)
            remaining_funs
        in
        if Variable.Map.is_empty new_needed_funs then
          (* If no new function is needed, we reached fixpoint *)
          needed_funs, free_vars_of_kept_funs
        else begin
          let needed_funs =
            Variable.Map.disjoint_union needed_funs new_needed_funs
          in
          let free_vars_of_kept_funs =
            Variable.Map.fold (fun _ { Flambda. free_variables } acc ->
                Variable.Set.union free_variables acc)
              new_needed_funs
              free_vars_of_kept_funs
          in
          add_needed needed_funs remaining_funs free_vars_of_kept_funs
        end
      in
      let funs, free_vars_of_kept_funs =
        add_needed Variable.Map.empty function_decls.funs Variable.Set.empty
      in
      let free_vars =
        Variable.Map.filter (fun id _var ->
            Variable.Set.mem id free_vars_of_kept_funs
            || Var_within_closure.Tbl.mem
                 used_vars_within_closure
                 (Var_within_closure.wrap id))
          free_vars
      in
      let function_decls =
        Flambda.update_function_declarations function_decls ~funs
      in
      let specialised_args =
        (* Remove specialised args that are used by removed functions *)
        let all_remaining_arguments =
          Variable.Map.fold (fun _ { Flambda.params } set ->
              Variable.Set.union set (Parameter.Set.vars params))
            funs Variable.Set.empty
        in
        Variable.Map.filter (fun arg _ ->
            Variable.Set.mem arg all_remaining_arguments)
          set_of_closures.specialised_args
      in
      let free_vars =
        Flambda_utils.clean_projections ~which_variables:free_vars
      in
      let direct_call_surrogates =
        (* Remove direct call surrogates where either the existing function
           or the surrogate has been eliminated. *)
        Variable.Map.fold (fun existing surrogate surrogates ->
            if not (Variable.Map.mem existing funs)
              || not (Variable.Map.mem surrogate funs)
            then surrogates
            else Variable.Map.add existing surrogate surrogates)
          set_of_closures.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
      Set_of_closures set_of_closures
    | e -> e
  in
  Flambda_iterators.map_named_of_program ~f:aux_named program
