(***********************************************************************)
(*                                                                     *)
(*                                OCaml                                *)
(*                                                                     *)
(*            Xavier Leroy, projet Cristal, INRIA Rocquencourt         *)
(*                                                                     *)
(*  Copyright 1996 Institut National de Recherche en Informatique et   *)
(*  en Automatique.  All rights reserved.  This file is distributed    *)
(*  under the terms of the Q Public License version 1.0.               *)
(*                                                                     *)
(***********************************************************************)
(* Adapted for Javascript backend: Hongbo Zhang                        *)


(**
        [no_bounded_varaibles lambda]
        checks if [lambda] contains bounded variable, for
        example [Llet (str,id,arg,body) ] will fail such check.
        This is used to indicate such lambda expression if it is okay
        to inline directly since if it contains bounded variables it
        must be rebounded before inlining
*)
let rec 
  no_list args = Ext_list.for_all args no_bounded_variables 
  and no_list_snd : 'a. ('a * Lam.t ) list -> bool  = fun args ->
    Ext_list.for_all_snd  args no_bounded_variables
  and no_opt x =   
    match x with 
    | None -> true 
    | Some a -> no_bounded_variables a 
  and no_bounded_variables (l : Lam.t) =
  match l with
  | Lvar _ -> true
  | Lconst _ -> true
  | Lassign(_id, e) ->
    no_bounded_variables e
  | Lapply{ap_func; ap_args; _} ->
    no_bounded_variables ap_func && no_list ap_args
  | Lglobal_module _ -> true
  | Lprim {args; primitive = _ ; } ->
    no_list args
  | Lswitch(arg, sw) ->
    no_bounded_variables arg &&
    no_list_snd sw.sw_consts &&
    no_list_snd sw.sw_blocks &&
    no_opt sw.sw_failaction
  | Lstringswitch (arg,cases,default) ->
    no_bounded_variables arg &&
    no_list_snd cases && no_opt default
  | Lstaticraise (_,args) ->
    no_list args
  | Lifthenelse(e1, e2, e3) ->
    no_bounded_variables e1 && no_bounded_variables e2 && no_bounded_variables e3
  | Lsequence(e1, e2) ->
    no_bounded_variables e1 && no_bounded_variables e2
  | Lwhile(e1, e2) ->
    no_bounded_variables e1 && no_bounded_variables e2
  | Lsend (_k, met, obj, args, _) ->
    no_bounded_variables met  &&
    no_bounded_variables obj &&
    no_list args
  | Lstaticcatch(e1, (_,vars), e2) ->
    vars = [] && no_bounded_variables e1 &&  no_bounded_variables e2
  | Lfunction{body;params} ->
    params = [] && no_bounded_variables body;
  | Lfor _  -> false
  | Ltrywith _ -> false
  | Llet _ ->false
  | Lletrec(decl, body) -> decl = [] && no_bounded_variables body


(*
   TODO: 
   we should have a pass called, always inlinable
   as long as its length is smaller than [exit=exit_id], for example

   {[
     switch(box_name)
       {case "":exit=178;break;
        case "b":exit=178;break;
        case "h":box_type=/* Pp_hbox */0;break;
        case "hov":box_type=/* Pp_hovbox */3;break;
        case "hv":box_type=/* Pp_hvbox */2;break;
        case "v":box_type=/* Pp_vbox */1;break;
        default:box_type=invalid_box(/* () */0);}

       switch(exit){case 178:box_type=/* Pp_box */4;break}
   ]}
*)

(** The third argument is its occurrence,
  when do the substitution, if its occurence is > 1,
  we should refresh
 *)
type lam_subst = 
  | Id of Lam.t [@@unboxed]
  (* | Refresh of Lam.t *)

type subst_tbl = (Ident.t list * lam_subst ) Hash_int.t

let to_lam x = 
  match x with 
  | Id x -> x 
  (* | Refresh x -> Lam_bounded_vars.refresh x  *)

(**
   Simplify  ``catch body with (i ...) handler''
      - if (exit i ...) does not occur in body, suppress catch
      - if (exit i ...) occurs exactly once in body,
        substitute it with handler
      - If handler is a single variable, replace (exit i ..) with it


  Note:
    In ``catch body with (i x1 .. xn) handler''
     Substituted expression is
      let y1 = x1 and ... yn = xn in
      handler[x1 <- y1 ; ... ; xn <- yn]
     For the sake of preserving the uniqueness  of bound variables.
   ASKS: This documentation seems outdated
     (No alpha conversion of ``handler'' is presently needed, since
     substitution of several ``(exit i ...)''
     occurs only when ``handler'' is a variable.)
  Note that 
           for [query] result = 2, 
           the non-inline cost is 
           {[
             var exit ;

             exit = 11;
             exit = 11;

             switch(exit){
               case exit = 11 : body ; break
             }

           ]}
           the inline cost is 

           {[
             body;
             body;
           ]}

           when [i] is negative, we can not inline in general, 
           since the outer is a traditional [try .. catch] body, 
           if it is guaranteed to be non throw, then we can inline
        *)

(** TODO: better heuristics, also if we can group same exit code [j] 
       in a very early stage -- maybe we can define our enhanced [Lambda] 
       representation and counter can be more precise, for example [apply] 
       does not need patch from the compiler

       FIXME:   when inlining, need refresh local bound identifiers
       #1438 when the action containes bounded variable 
         to keep the invariant, everytime, we do an inlining,
         we need refresh, just refreshing once is not enough
    We need to decide whether inline or not based on post-simplification
       code, since when we do the substitution 
       we use the post-simplified expression, it is more consistent
       TODO: when we do the case merging on the js side, 
       the j is not very indicative                
*)             

let subst_helper (subst : subst_tbl) (query : int -> int) (lam : Lam.t) : Lam.t = 
  let rec simplif (lam : Lam.t) = 
    match lam with 
    | Lstaticcatch (l1,(i,xs),l2) ->      
        let i_occur = query i in 
        (match i_occur , l2 with
        | 0,_ -> simplif l1
        | ( _ , Lvar _
          | _, Lconst _) (* when i >= 0  # 2316 *) ->  
          Hash_int.add subst i (xs, Id (simplif l2)) ;
          simplif l1 (** l1 will inline *)
        | 1,_ when i >= 0 -> (** Ask: Note that we have predicate i >=0 *)
          Hash_int.add subst i (xs, Id (simplif l2)) ;
          simplif l1 (** l1 will inline *)
        |  _ ->
          let l2 = simplif l2 in 
          (* we only inline when [l2] does not contain bound variables
             no need to refresh
          *)
          let ok_to_inline = 
            i >=0 && 
            (no_bounded_variables l2) &&
            (let lam_size = Lam_analysis.size l2 in
             (i_occur <= 2 && lam_size < Lam_analysis.exit_inline_size   )
             || (lam_size < 5 ))
          in 
          if ok_to_inline 
          then 
            begin            
              Hash_int.add subst i (xs,  Id l2) ;
              simplif l1 
            end
          else Lam.staticcatch (simplif l1) (i,xs) l2)
    | Lstaticraise (i,[])  ->
      (match Hash_int.find_opt subst i with
       | Some (_,handler) -> to_lam handler
       | None -> lam)      
    | Lstaticraise (i,ls) ->
      let ls = Ext_list.map  ls simplif in
      (match Hash_int.find_opt subst i with
       | Some (xs, handler) -> 
         let handler = to_lam handler in 
         let ys = Ext_list.map xs Ident.rename in
         let env =
           Ext_list.fold_right2 xs ys Map_ident.empty 
             (fun x y t -> Map_ident.add t x (Lam.var y) ) in
         Ext_list.fold_right2 ys ls 
           (Lam_subst.subst  env  handler)
           (fun y l r -> Lam.let_ Strict y l r)
       | None -> Lam.staticraise i ls
      )
    | Lvar _|Lconst _  -> lam
    | Lapply {ap_func; ap_args;  ap_info } -> 
      Lam.apply (simplif ap_func) (Ext_list.map ap_args simplif) ap_info
    | Lfunction {arity; params; body; attr} -> 
      Lam.function_ ~arity  ~params ~body:(simplif body) ~attr
    | Llet (kind, v, l1, l2) -> 
      Lam.let_ kind v (simplif l1) (simplif l2)
    | Lletrec (bindings, body) ->
      Lam.letrec
        (Ext_list.map_snd  bindings simplif) 
        (simplif body)
    | Lglobal_module _ -> lam 
    | Lprim {primitive; args; loc} -> 
      let args = Ext_list.map args simplif in
      Lam.prim ~primitive ~args loc
    | Lswitch(l, sw) ->
      let new_l = simplif l in 
      let new_consts =  Ext_list.map_snd  sw.sw_consts simplif in 
      let new_blocks =  Ext_list.map_snd  sw.sw_blocks simplif in 
      let new_fail = Ext_option.map sw.sw_failaction simplif in       
      Lam.switch
        new_l
        { 
          sw with 
          sw_consts = new_consts ;
          sw_blocks = new_blocks; 
          sw_failaction = new_fail}
    | Lstringswitch(l,sw,d) ->
      Lam.stringswitch
        (simplif l) (Ext_list.map_snd  sw simplif)
        (Ext_option.map d simplif)
    | Ltrywith (l1, v, l2) -> 
      Lam.try_ (simplif l1) v (simplif l2)
    | Lifthenelse (l1, l2, l3) -> 
      Lam.if_ (simplif l1) (simplif l2) (simplif l3)
    | Lsequence (l1, l2) -> Lam.seq (simplif l1) (simplif l2)
    | Lwhile (l1, l2) -> Lam.while_ (simplif l1) (simplif l2)
    | Lfor (v, l1, l2, dir, l3) ->
      Lam.for_ v (simplif l1) (simplif l2) dir (simplif l3)
    | Lassign (v, l) -> 
      Lam.assign v (simplif l)
    | Lsend (k, m, o, ll, loc) ->
      Lam.send k (simplif m) (simplif o) (Ext_list.map ll simplif ) loc
  in 
  simplif lam 

let simplify_exits (lam : Lam.t) =
  let exits = Lam_exit_count.count_helper lam in
  subst_helper (Hash_int.create 17 ) (Lam_exit_count.count_exit exits) lam

(* Compile-time beta-reduction of functions immediately applied:
      Lapply(Lfunction(Curried, params, body), args, loc) ->
        let paramN = argN in ... let param1 = arg1 in body
      Lapply(Lfunction(Tupled, params, body), [Lprim(Pmakeblock(args))], loc) ->
        let paramN = argN in ... let param1 = arg1 in body
   Assumes |args| = |params|.
*)