(*
 * Operations:
 *    1. Check that all return/export operations are in legal
 *       places.
 *    2. Add final operations to sequences that don't already
 *       have them.
 *    3. Add return handlers to functions that have nontrivial
 *       return statements.
 *)

include Omake_pos.Make (struct let name = "Omake_ir_semant" end)


(*
 * Synthesized attributes.
 *    renv_has_return : is there a non-tail return?
 *    renv_is_final : code after this point is never executed
 *)
type renv =
   { renv_has_return : bool;
     renv_is_final   : bool;
     renv_is_value   : bool
   }

(*
 * Inherited attributes.
 *    env_in_function : currently within a function body
 *    env_in_cond     : currently within a conditional
 *    env_is_tail     : the current expression is in final position
 *)
type env =
   { env_warnings      : Lm_location.t option ref;
     env_in_function   : Omake_ir.return_id option;
     env_in_cond       : bool;
     env_section_tail  : bool;
     env_function_tail : bool
   }

(*
 * Return environments.
 *)
let renv_empty =
   { renv_has_return = false;
     renv_is_final   = false;
     renv_is_value   = false
   }

let renv_value =
   { renv_has_return   = false;
     renv_is_final     = false;
     renv_is_value     = true
   }

let renv_final =
   { renv_has_return   = false;
     renv_is_final     = true;
     renv_is_value     = true
   }

let renv_return =
   { renv_has_return   = true;
     renv_is_final     = true;
     renv_is_value     = true
   }

(*
 * Normal environment, not in a function.
 *)
let env_empty () =
   { env_warnings      = ref None;
     env_in_function   = None;
     env_in_cond       = false;
     env_section_tail  = false;
     env_function_tail = false
   }

let env_object env =
   { env_warnings      = env.env_warnings;
     env_in_function   = None;
     env_in_cond       = false;
     env_section_tail  = false;
     env_function_tail = false
   }

let env_object_tail env =
   { env_warnings      = env.env_warnings;
     env_in_function   = None;
     env_in_cond       = false;
     env_section_tail  = true;
     env_function_tail = true
   }

(*
 * Fresh environment for a function body.
 *)
let new_return_id loc v =
   let _, v = Omake_ir_util.var_of_var_info v in
      loc, Lm_symbol.to_string v

let env_fun env id =
   { env_warnings      = env.env_warnings;
     env_in_function   = Some id;
     env_in_cond       = false;
     env_section_tail  = true;
     env_function_tail = true
   }

let env_anon_fun env =
   { env with env_in_cond       = true;
              env_section_tail  = true
   }

let update_return renv has_return =
   { renv with renv_has_return = renv.renv_has_return || has_return }

(*
 * Error checkers.
 *)
let check_return_placement env loc =
   match env.env_in_function with
      None ->
         let pos = string_pos "check_in_function" (loc_exp_pos loc) in
         let print_error buf =
            Format.fprintf buf "@[<v 0>Misplaced return statement.";
            Format.fprintf buf "@ The return is not within a function.@]"
         in
            raise (Omake_value_type.OmakeException (pos, LazyError print_error))
    | Some id ->
         if not (env.env_function_tail || env.env_section_tail && env.env_in_cond) then begin
            Lm_printf.eprintf "@[<v 3>*** omake warning: %a@ statements after this return are not reached@]@." Lm_location.pp_print_location loc;
            env.env_warnings := Some loc
         end;
         id

(* let check_section_tail env loc = *)
(*    if not env.env_section_tail then *)
(*       let pos = string_pos "check_section_tail" (loc_exp_pos loc) in *)
(*          raise (Omake_value_type.OmakeException (pos, StringError "This should be the last expression in the section.")) *)

let check_object_tail env loc =
   if env.env_in_function <> None || not env.env_section_tail then
      let pos = string_pos "check_object_tail" (loc_exp_pos loc) in
         raise (Omake_value_type.OmakeException (pos, StringError "This should be the last expression in the object."))

(*
 * Convert a string expression.
 *)
let rec build_string env s =
  let env = { env with env_function_tail = false } in
  match s with
    Omake_ir.NoneString _
  | IntString _
  | FloatString _
  | WhiteString _
  | ConstString _
  | ThisString _
  | KeyApplyString _
  | VarString _ ->
    false, s
  | FunString (loc, opt_params, params, e, export) ->
    (* Returns propagate -through- anonymous functions *)
    let renv, e = build_sequence_exp (env_anon_fun env) e in
    let has_return, opt_params = build_keyword_param_list env opt_params in
    renv.renv_has_return || has_return, FunString (loc, opt_params, params, e, export)
  | ApplyString (loc, v, args, kargs) ->
    let has_return1, args = build_string_list env args in
    let has_return2, kargs = build_keyword_string_list env kargs in
    has_return1 || has_return2, ApplyString (loc, v, args, kargs)
  | SuperApplyString (loc, v1, v2, args, kargs) ->
    let has_return1, args = build_string_list env args in
    let has_return2, kargs = build_keyword_string_list env kargs in
    has_return1 || has_return2, SuperApplyString (loc, v1, v2, args, kargs)
  | MethodApplyString (loc, v, vl, args, kargs) ->
    let has_return1, args = build_string_list env args in
    let has_return2, kargs = build_keyword_string_list env kargs in
    has_return1 || has_return2, MethodApplyString (loc, v, vl, args, kargs)
  | SequenceString (loc, sl) ->
    let has_return, sl = build_string_list env sl in
    has_return, SequenceString (loc, sl)
  | ArrayString (loc, sl) ->
    let has_return, sl = build_string_list env sl in
    has_return, ArrayString (loc, sl)
  | ArrayOfString (loc, s) ->
    let has_return, s = build_string env s in
    has_return, ArrayOfString (loc, s)
  | QuoteString (loc, sl) ->
    let has_return, sl = build_string_list env sl in
    has_return, QuoteString (loc, sl)
  | QuoteStringString (loc, c, sl) ->
    let has_return, sl = build_string_list env sl in
    has_return, QuoteStringString (loc, c, sl)
  | ObjectString (loc, el, export) ->
    let el = build_object_exp env el in
    (* XXX: we should handle the case when an object contains a return *)
    false, ObjectString (loc, el, export)
  | BodyString (loc, el, export) ->
    let renv, el = build_sequence_exp env el in
    renv.renv_has_return, BodyString (loc, el, export)
  | ExpString (loc, el, export) ->
    let renv, el = build_sequence_exp env el in
    renv.renv_has_return, ExpString (loc, el, export)
  | CasesString (loc, cases) ->
    let env = { env with env_in_cond = true } in
    let has_return, cases =
      List.fold_left (fun (has_return, cases) (v, s, el, export) ->
        let has_return2, s = build_string env s in
        let renv, e = build_sequence_exp env el in
        let has_return = has_return || has_return2 || renv.renv_has_return in
        has_return, (v, s, e, export) :: cases) (false, []) cases
    in
    has_return, CasesString (loc, List.rev cases)
  | LazyString (loc, s) ->
    let has_return, s = build_string env s in
    has_return, LazyString (loc, s)
  | LetVarString (loc, v, s1, s2) ->
    let has_return1, s1 = build_string env s1 in
    let has_return2, s2 = build_string env s2 in
    has_return1 || has_return2, LetVarString (loc, v, s1, s2)

and build_string_list env sl =
  let has_return, sl =
    List.fold_left (fun (has_return, sl) s ->
      let has_return2, s = build_string env s in
      has_return || has_return2, s :: sl) (false, []) sl
  in
  has_return, List.rev sl

and build_keyword_string_list env kargs =
  let has_return, kargs =
    List.fold_left (fun (has_return, sl) (v, s) ->
      let has_return2, s = build_string env s in
      has_return || has_return2, (v, s) :: sl) (false, []) kargs
  in
  has_return, List.rev kargs

and build_keyword_param_list env kargs =
  let has_return, kargs =
    List.fold_left (fun (has_return, sl) (v, v_info, s_opt) ->
      match s_opt with
        Some s ->
        let has_return2, s = build_string env s in
        has_return || has_return2, (v, v_info, Some s) :: sl
      | None ->
        has_return, (v, v_info, None) :: sl) (false, []) kargs
  in
  has_return, List.rev kargs

(*
 * Convert the current expression.
 *)
and build_exp env e =
  match e with
    Omake_ir.LetFunExp (loc, v, vl, curry, opt_params, vars, el, export) ->
    let id = new_return_id loc v in
    let renv, el = build_sequence_exp (env_fun env id) el in
    let el =
      if renv.renv_has_return then
        Omake_ir.[ReturnBodyExp (loc, el, id)]
      else
        el
    in
    let has_return, opt_params = build_keyword_param_list env opt_params in
    let e = Omake_ir.LetFunExp (loc, v, vl, curry, opt_params, vars, el, export) in
    update_return renv_empty has_return, e
  | LetObjectExp (loc, v, vl, s, el, export) ->
    let el = build_object_exp env el in
    let has_return, s = build_string env s in
    let e = Omake_ir.LetObjectExp (loc, v, vl, s, el, export) in
    update_return renv_empty has_return, e
  | StaticExp (loc, node, v, el) ->
    let el = build_object_exp env el in
    let e = Omake_ir.StaticExp (loc, node, v, el) in
    renv_empty, e
  | IfExp (loc, cases) ->
    let renv, cases = build_cases_exp env cases in
    let e = Omake_ir.IfExp (loc, cases) in
    renv, e
  | SequenceExp (loc, el) ->
    let renv, el = build_sequence_exp env el in
    let e = Omake_ir.SequenceExp (loc, el) in
    renv, e
  | SectionExp (loc, s, el, export) ->
    let has_return, s = build_string env s in
    let renv, el = build_sequence_exp env el in
    let e = Omake_ir.SectionExp (loc, s, el, export) in
    update_return renv has_return, e
  | ReturnBodyExp (loc, el, id) ->
    let renv, el = build_sequence_exp env el in
    let el = Omake_ir.ReturnBodyExp (loc, el, id) in
    renv, el
  | LetVarExp (loc, v, vl, kind, s) ->
    let has_return, s = build_string env s in
    let e = Omake_ir.LetVarExp (loc, v, vl, kind, s) in
    update_return renv_empty has_return, e
  | IncludeExp (loc, s, sl) ->
    let has_return1, s = build_string env s in
    let has_return2, sl = build_string_list env sl in
    let e = Omake_ir.IncludeExp (loc, s, sl) in
    update_return renv_empty (has_return1 || has_return2), e
  | ApplyExp (loc, v, args, kargs) ->
    let has_return1, args = build_string_list env args in
    let has_return2, kargs = build_keyword_string_list env kargs in
    let e = Omake_ir.ApplyExp (loc, v, args, kargs) in
    update_return renv_empty (has_return1 || has_return2), e
  | SuperApplyExp (loc, v1, v2, args, kargs) ->
    let has_return1, args = build_string_list env args in
    let has_return2, kargs = build_keyword_string_list env kargs in
    let e = Omake_ir.SuperApplyExp (loc, v1, v2, args, kargs) in
    update_return renv_empty (has_return1 || has_return2), e
  | MethodApplyExp (loc, v, vl, args, kargs) ->
    let has_return1, args = build_string_list env args in
    let has_return2, kargs = build_keyword_string_list env kargs in
    let e = Omake_ir.MethodApplyExp (loc, v, vl, args, kargs) in
    update_return renv_empty (has_return1 || has_return2), e
  | LetKeyExp (loc, v, kind, s) ->
    let has_return, s = build_string env s in
    let e = Omake_ir.LetKeyExp (loc, v, kind, s) in
    update_return renv_empty has_return, e
  | LetThisExp (loc, s) ->
    let has_return, s = build_string env s in
    let e = Omake_ir.LetThisExp (loc, s) in
    update_return renv_empty has_return, e
  | ShellExp (loc, s) ->
    let has_return, s = build_string env s in
    let e = Omake_ir.ShellExp (loc, s) in
    update_return renv_value has_return, e
  | KeyExp _
  | OpenExp _ ->
    renv_empty, e
  | StringExp (loc, s) ->
    let has_return, s = build_string env s in
    let e = Omake_ir.StringExp (loc, s) in
    update_return renv_value has_return, e
  | ReturnExp (loc, s, _) ->
    let id = check_return_placement env loc in
    let has_return, s = build_string env s in
    if env.env_function_tail then
      update_return renv_final has_return, Omake_ir.StringExp (loc, s)
    else
      renv_return, Omake_ir.ReturnExp (loc, s, id)
  | ReturnObjectExp (loc, _)
  | ReturnSaveExp loc ->
    check_object_tail env loc;
    renv_final, e

(*
 * An object expression is an expression sequence,
 * but it is not in a function.
 *)
and build_object_exp_aux env el =
  match el with
    [e] ->
    let _, e = build_exp (env_object_tail env) e in
    [e]
  | e :: el ->
    let _, e = build_exp env e in
    let el = build_object_exp_aux env el in
    e :: el
  | [] ->
    []

and build_object_exp env el =
  build_object_exp_aux (env_object env) el

(*
 * A new sequence expression.
 * It should be terminated with a final statement.
 *)
and build_sequence_exp env el =
  let env_non_tail =
    { env with env_section_tail = false;
      env_function_tail = false
    }
  in
  let rec build_sequence_core has_return rel el =
    match el with
      [e] ->
      let env_tail = { env with env_section_tail = true } in
      let renv, e = build_exp env_tail e in
      let rel = e :: rel in
      update_return renv has_return, rel
    | e :: el ->
      let renv, e = build_exp env_non_tail e in
      let has_return = has_return || renv.renv_has_return in
      let rel = e :: rel in
      build_sequence_core has_return rel el
    | [] ->
      renv_empty, []
  in
  let renv, rel = build_sequence_core false [] el in
  renv, List.rev rel

(*
 * Cases are slightly different from sequences because
 * returns are always allowed.  Note that the completeness is
 * not checked, so even if all cases end in a return,
 * evaluation may continue from here.
 *)
and build_cases_exp env cases =
  let env =
    { env with env_in_cond = true;
      env_section_tail = true
    }
  in
  let has_return, cases =
    List.fold_left (fun (has_return, cases) (s, el, export) ->
      let renv, el = build_sequence_exp env el in
      let has_return = has_return || renv.renv_has_return in
      has_return, (s, el, export) :: cases) (false, []) cases
  in
  let cases = List.rev cases in
  let renv =
    { renv_is_final   = false;
      renv_is_value   = true;
      renv_has_return = has_return
    }
  in
  renv, cases

(************************************************************************
 * Main function
*)
let build_prog venv e =
  let env = env_empty () in
  let _, e = build_exp env e in
  let count = !(env.env_warnings) in
  let () =
    match count with
    |Some loc when Omake_options.opt_warn_error (Omake_env.venv_options venv) ->
      raise (Omake_value_type.OmakeException (loc_exp_pos loc, StringError "warnings treated as errors"))
    | _ -> () in
  e
