(**************************************************************************)
(*                                                                        *)
(*  This file is part of Frama-C.                                         *)
(*                                                                        *)
(*  Copyright (C) 2007-2020                                               *)
(*    CEA (Commissariat à l'énergie atomique et aux énergies              *)
(*         alternatives)                                                  *)
(*                                                                        *)
(*  you can redistribute it and/or modify it under the terms of the GNU   *)
(*  Lesser General Public License as published by the Free Software       *)
(*  Foundation, version 2.1.                                              *)
(*                                                                        *)
(*  It is distributed in the hope that it will be useful,                 *)
(*  but WITHOUT ANY WARRANTY; without even the implied warranty of        *)
(*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *)
(*  GNU Lesser General Public License for more details.                   *)
(*                                                                        *)
(*  See the GNU Lesser General Public License version 2.1                 *)
(*  for more details (enclosed in the file licenses/LGPLv2.1).            *)
(*                                                                        *)
(**************************************************************************)

open Cil_types
open Va_types
open Options
module Cil = Extends.Cil
module List = Extends.List
module Typ = Extends.Typ
module Build = Va_build


let params_types params =
  List.map (fun (_,typ,_) -> typ) params

let pp_prototype name fmt tparams =
  Format.fprintf fmt "%s(%a)"
    name
    (Pretty_utils.pp_list ~sep:", " Printer.pp_typ) tparams

let pp_overload name fmt l =
  let prototypes = List.map fst l in
  Pretty_utils.pp_list ~sep:"@\n" (pp_prototype name) fmt prototypes


let new_globals : (global list) ref = ref []


(* ************************************************************************ *)
(* Call translation                                                         *)
(* ************************************************************************ *)

exception Translate_call_exn

(* Extended integer types (e.g. int8_t, uint_least16_t, int_fast32_t)
   do not have their own character modifiers, but instead use macros that are
   converted into "standard" modifiers (e.g. "%hhd", "%hu", "%d", etc.).
   Therefore, we cannot enforce their types the same way as for e.g. size_t,
   which has its own modifier. We weaken the check, allowing a different name
   but still requiring same size and signedness. *)
let extended_integer_typenames =
  ["int8_t"; "uint8_t"; "int_least8_t"; "uint_least8_t";
   "int_fast8_t"; "uint_fast8_t";
   "int16_t"; "uint16_t"; "int_least16_t"; "uint_least16_t";
   "int_fast16_t"; "uint_fast16_t";
   "int32_t"; "uint32_t"; "int_least32_t"; "uint_least32_t";
   "int_fast32_t"; "uint_fast32_t";
   "int64_t"; "uint64_t"; "int_least64_t"; "uint_least64_t";
   "int_fast64_t"; "uint_fast64_t"]

let is_extended_integer_type t =
  match t with
  | TNamed (ti, _) -> List.mem ti.tname extended_integer_typenames
  | _ -> false

let integral_rep ikind =
  Cil.bitsSizeOfInt ikind, Cil.isSigned ikind

let expose t =
  Cil.type_remove_attributes_for_c_cast (Cil.unrollType t)

(* From most permissive to least permissive *)
type castability = Strict      (* strictly allowed by the C standard *)
                 | Tolerated   (* tolerated in practice *)
                 | NonPortable (* non-portable minor deviation *)
                 | NonStrict   (* only allowed in non-strict mode *)
                 | Never       (* never allowed *)

let can_cast given expected =
  match expose given, expose expected with
  | t1, t2 when Cil_datatype.Typ.equal t1 t2 -> Strict
  | (TInt (i1,a1) | TEnum({ekind=i1},a1)),
    (TInt (i2,a2) | TEnum({ekind=i2},a2)) ->
    if integral_rep i1 <> integral_rep i2 ||
       not (Cil_datatype.Attributes.equal a1 a2) then
      Never
    else if is_extended_integer_type given then
      Tolerated
    else if i1 = i2 then
      NonPortable
    else
      NonStrict
  | TPtr _, TPtr _ -> Strict
  | _, _ -> Never

let does_fit exp typ =
  match Cil.constFoldToInt exp, Cil.unrollType typ with
  | Some i, (TInt (ekind,_) | TEnum({ekind},_)) ->
    Cil.fitsInInt ekind i
  | _ -> false

(* Variant of [pp_typ] which details the underlying type for enums *)
let pretty_typ fmt t =
  match Cil.unrollType t with
  | TEnum (ei, _) ->
    Format.fprintf fmt "%a (%a)" Printer.pp_typ t
      Printer.pp_typ (TInt (ei.ekind, []))
  | _ -> Printer.pp_typ fmt t

(* cast the i-th argument exp to paramtyp *)
let cast_arg i paramtyp exp =
  let argtyp = Cil.typeOf exp in
  if not (does_fit exp paramtyp) then
    begin match can_cast argtyp paramtyp with
      | Strict | Tolerated -> ()
      | (NonPortable | NonStrict) when not (Strict.get ()) -> ()
      | NonPortable ->
        Self.warning ~current:true
          "Possible portability issues with enum type for argument %d \
           (use -variadic-no-strict to avoid this warning)."
          (i + 1)
      | NonStrict | Never ->
        Self.warning ~current:true
          "Incorrect type for argument %d. \
           The argument will be cast from %a to %a."
          (i + 1)
          pretty_typ argtyp pretty_typ paramtyp
    end;
  Cil.mkCast ~force:false ~e:exp ~newt:paramtyp


(* cast a list of args to the tparams list of types and remove unused args *)
let match_args tparams args =
  (* Remove unused arguments *)
  let paramcount = List.length tparams
  and argcount = List.length args in
  if argcount > paramcount  then
    Self.warning ~current:true
      "Too many arguments: expected %d, given %d. \
       Superfluous arguments will be removed."
      paramcount argcount
  else if argcount < paramcount then (
    Self.warning ~current:true
      "Not enough arguments: expected %d, given %d."
      paramcount argcount;
    raise Translate_call_exn
  );

  (* Translate params *)
  let new_args, unused_args = List.break paramcount args in
  List.mapi2 cast_arg tparams new_args, unused_args


(* translate a call by applying argument matching/pruning and changing
   callee *)
let match_call ~loc ~fundec scope mk_call new_callee new_tparams args =
  let new_args, unused_args = match_args new_tparams args in
  let call = mk_call (Cil.evar ~loc new_callee) new_args in
  let reads =
    List.map (fun e -> Cil.mkPureExprInstr ~fundec ~scope e) unused_args
  in
  reads @ [call]

(* ************************************************************************ *)
(* Aggregator calls                                                         *)
(* ************************************************************************ *)

let find_null exp_list =
  List.ifind (fun e -> Cil.isZero (Cil.constFold false e)) exp_list


let aggregator_call
    ~fundec ~ghost {a_target; a_pos; a_type; a_param} scope loc mk_call vf args =
  let name = vf.vf_decl.vorig_name
  and tparams = Typ.params_types a_target.vtype
  and pname, ptyp = a_param in

  (* Check argument count *)
  let argcount = List.length args
  and paramcount = List.length tparams in
  if argcount < paramcount then begin
    Self.warning ~current:true
      "Not enough arguments: expected %d, given %d."
      paramcount argcount;
    raise Translate_call_exn;
  end;

  (* Compute the size of the aggregation *)
  let size = match a_type with
    | EndedByNull ->
      begin try
          find_null (List.drop a_pos args) + 1
        with Not_found ->
          Self.warning ~current:true
            "Failed to find a sentinel (NULL pointer) in the argument list.";
          raise Translate_call_exn;
      end
  in

  (* Convert arguments *)
  let tparams_left = List.take a_pos tparams in
  let tparams_right = List.drop (a_pos + 1) tparams in
  let new_tparams = tparams_left @ List.make size ptyp @ tparams_right in
  let new_args, unused_args = match_args new_tparams args in

  (* Split the arguments *)
  let args_left, args_rem = List.break a_pos new_args in
  let args_middle, args_right = List.break size args_rem in

  (* Create the call code  *)
  Self.result ~current:true ~level:2
    "Translating call to %s to a call to %s."
    name a_target.vorig_name;
  let pname = if pname = "" then "param" else pname in
  let vaggr, assigns =
    Build.array_init ~loc fundec ~ghost scope pname ptyp args_middle
  in
  let new_arg = Cil.mkAddrOrStartOf ~loc (Cil.var vaggr) in
  let new_args = args_left @ [new_arg] @ args_right in
  let new_args,_ = match_args tparams new_args in
  let call = mk_call (Cil.evar ~loc a_target) new_args in
  let reads = List.map (Cil.mkPureExprInstr ~fundec ~scope ~loc) unused_args in
  assigns :: reads @ [call]

(* ************************************************************************ *)
(* Overloads calls                                                          *)
(* ************************************************************************ *)

let rec check_arg_matching expected given =
  match Cil.unrollType given, Cil.unrollType expected with
  | (TInt _ | TEnum _), (TInt _ | TEnum _) -> true
  | TPtr _, _ when Cil.isVoidPtrType expected -> true
  | TPtr (t1, _), TPtr (t2, _) -> check_arg_matching t1 t2
  | _, _ -> not (Cil.need_cast given expected)


let rec check_call_matching tparams targs =
  match tparams, targs with
  | [], [] -> true
  | [], _
  (* too many args: this is allowed by the standard (the extra arguments
     are ignored), but in practice this leads to disambiguation issues in
     some cases (e.g. last argument is 0 instead of NULL), so we prefer to
     be strict *)
  (* Not enough input args *)
  | _, [] -> false
  | a1 :: l1, a2 :: l2 ->
    check_arg_matching a1 a2 &&
    check_call_matching l1 l2


let filter_matching_prototypes overload args =
  (* Find suitable candidates for this call *)
  let targs = List.map Cil.typeOf args in
  let check (tparams, _vi) = check_call_matching tparams targs in
  List.filter check overload


let overloaded_call ~fundec overload block loc mk_call vf args =
  let name = vf.vf_decl.vorig_name in

  (* Find the matching prototype *)
  let tparams, new_callee =
    match filter_matching_prototypes overload args with
    | [] -> (* No matching prototype *)
      Self.warning ~current:true
        "@[No matching prototype found for this call to %s.@.\
         Expected candidates:@.\
         @[<v>       %a@]@.\
         Given arguments:@.\
         @[<v>       %a@]"
        name (pp_overload name) overload
        (pp_prototype name) (List.map Cil.typeOf args);
      raise Translate_call_exn;
    | [(tparams,vi)] -> (* Exactly one matching prototype *)
      tparams, vi
    | l -> (* Several matching prototypes *)
      Self.warning ~current:true
        "Ambiguous call to %s. Matching candidates are: \
         %a"
        name
        (pp_overload name) l;
      raise Translate_call_exn;
  in

  (* Store the translation *)
  Replacements.add new_callee vf.vf_decl;

  (* Rebuild the call *)
  Self.result ~current:true ~level:2
    "Translating call to the specialized version %a."
    (pp_prototype name) tparams;
  match_call ~loc ~fundec block mk_call new_callee tparams args



(* ************************************************************************ *)
(* Format functions calls                                                   *)
(* ************************************************************************ *)

(* --- Specification building --- *)

let rec static_string a = match a.enode with
  | Const (CStr s) -> Some (Format_string.String s)
  | Const (CWStr s) -> Some (Format_string.WString s)
  | CastE (_, e) -> static_string e
  | _ -> None

let find_global env name =
  try
    Some (Environment.find_global env name)
  with Not_found ->
    Self.warning ~once:true
      "Unable to locate global %s which should be in the Frama-C LibC. \
       Correct specifications can't be generated."
      name;
    None

let find_predicate name =
  match Logic_env.find_all_logic_functions name with
  | f :: _q -> Some f (* TODO: should we warn in case of overloading? *)
  | [] ->
    Self.warning ~once:true
      "Unable to locate ACSL predicate %s which should be in the Frama-C LibC. \
       Correct specifications can't be generated."
      name;
    None

let find_field env structname fieldname =
  try
    let compinfo = Environment.find_struct env structname in
    Some (Cil.getCompField compinfo fieldname)
  with Not_found ->
    Self.warning ~once:true
      "Unable to locate %s field %s."
      structname fieldname;
    None

let find_predicate_by_width typ narrow_name wide_name =
  match Cil.unrollTypeDeep typ with
  | TPtr (TInt(IChar, _), _) -> find_predicate narrow_name
  | TPtr (t, _) when
      (* drop attributes to remove 'const' qualifiers and fc_stdlib attributes *)
      Cil_datatype.Typ.equal
        (Cil.typeDeepDropAllAttributes (Cil.unrollTypeDeep t))
        Cil.theMachine.Cil.wcharType ->
    find_predicate wide_name
  | _ ->
    Self.warning ~current:true
      "expected single/wide character pointer type, got %a (%a, unrolled %a)"
      Printer.pp_typ typ Cil_types_debug.pp_typ typ Cil_types_debug.pp_typ (Cil.unrollTypeDeep typ);
    None

let build_fun_spec env loc vf format_fun tvparams formals =
  let open Format_types in
  let _ = () in
  let fixed_params_count = Typ.params_count vf.vf_original_type in
  let sformals, vformals = List.break fixed_params_count formals in
  let here = Logic_const.here_label in

  (* Spec *)
  let sources = ref []
  and dests = ref []
  and requires = ref []
  and ensures = ref [] in
  let iterm lval =
    Logic_const.new_identified_term (Build.tlval ~loc lval)
  and insert x t =
    t := x :: !t
  in
  let insert_source ?(indirect=false) lval =
    let itlval = iterm lval in
    let it_content = if indirect then
        { itlval.it_content with
          term_name = "indirect" :: itlval.it_content.term_name }
      else itlval.it_content
    in
    let itlval = { itlval with Cil_types.it_content } in
    insert itlval sources
  and insert_dest lval =
    insert (iterm lval) dests
  and insert_require pred =
    insert (Logic_const.new_predicate pred) requires
  and insert_ensure pred =
    insert (Normal, Logic_const.new_predicate pred) ensures
  in
  let add_lval ~indirect (lval,dir) =
    (* Add the lval to the list of sources/dests *)
    begin match dir with
      | (`ArgIn | `ArgInArray _) -> insert_source ~indirect lval
      | (`ArgOut | `ArgOutArray) -> insert_dest lval
      | `ArgInOut -> insert_source ~indirect lval; insert_dest lval
    end
  in
  let add_var ?pos (vi,dir) =
    (* Use the appropriate logical lval *)
    let lval = match dir with
      | `ArgIn -> Build.lvar vi
      | (`ArgInArray _ | `ArgOutArray) -> Build.trange_from_vi ~loc vi
      | (`ArgOut | `ArgInOut) -> Build.tvarmem ~loc vi
    in
    (* Build requires/ensures *)
    let term = Build.tvar ~loc vi in
    begin match dir with
      | `ArgInArray None ->
        let pred =
          find_predicate_by_width vi.vtype "valid_read_string" "valid_read_wstring"
        in
        begin match pred with
          | Some logic_info ->
            let labels = List.map (fun _ -> here) logic_info.l_labels in
            let p = Logic_const.papp ~loc (logic_info, labels, [term]) in
            insert_require p
          | None -> ()
        end

      | `ArgInArray (Some precision) ->
        assert (pos <> None);
        let pred =
          find_predicate_by_width vi.vtype "valid_read_nstring" "valid_read_nwstring"
        in
        begin match pred with
          | Some logic_info ->
            let labels = List.map (fun _ -> here) logic_info.l_labels in
            let nterm = match precision with
              | PStar ->
                let n_vi = List.nth vformals (Extlib.the pos) in
                Logic_utils.numeric_coerce Linteger (Build.tvar ~loc n_vi)
              | PInt n -> Cil.lconstant ~loc (Integer.of_int n)
            in
            let p = Logic_const.papp ~loc (logic_info, labels, [term; nterm]) in
            insert_require p
          | None -> ()
        end

      | `ArgOut ->
        insert_require (Logic_const.pvalid ~loc (here,term));
        insert_ensure (Logic_const.pinitialized ~loc (here,term))

      | _ -> ()
    end;
    (* Cil.hasAttribute "const" *)
    add_lval (lval,dir)
  in
  let make_indirect iterm =
    (* Add "indirect" to an identified term, if it isn't already *)
    if List.mem "indirect" iterm.it_content.term_name then iterm
    else
      let it_content =
        { iterm.it_content with
          term_name = "indirect" :: iterm.it_content.term_name }
      in
      { iterm with it_content }
  in

  (* Build variadic parameter source/dest list *)
  let dirs = List.map snd tvparams in
  let l = List.combine vformals dirs in
  let pos = ref (-1) in
  List.iter (incr pos; add_var ~indirect:false ~pos:!pos) l;

  (* Add format source and additional parameters *)
  let fmt_vi = List.nth sformals format_fun.f_format_pos in
  add_var ~indirect:true (fmt_vi, `ArgInArray None);

  (* Add buffer source/dest *)
  let add_stream vi =
    (* assigns stream->__fc_FILE_data
         \from stream->__fc_FILE_data, __fc_FILE_id *)
    begin match find_field env "__fc_FILE" "__fc_FILE_data" with
      | Some fieldinfo ->
        let varfield = Build.tvarfield ~loc vi fieldinfo in
        add_lval ~indirect:false (varfield, `ArgInOut)
      | None ->
        add_var ~indirect:false (vi, `ArgInOut)
    end;
    begin match find_field env "__fc_FILE" "__fc_FILE_id" with
      | Some fieldinfo ->
        let varfield = Build.tvarfield ~loc vi fieldinfo in
        add_lval ~indirect:true (varfield, `ArgIn)
      | None -> ()
    end
  in

  (* Add a bounded buffer *)
  let add_buffer vi_buffer vi_size =
    add_var ~indirect:true (vi_size, `ArgIn);
    (* this is an snprintf-like function; compute and add its precondition:
       \valid(s + (0..n-1)) || \valid(s + (0..format_length(format)-1)) *)
    let make_valid_range tvalid_length =
      let tvar = Build.tvar ~loc vi_buffer
      and tmin = Build.tzero ~loc
      and tmax = Build.tminus ~loc tvalid_length (Build.tone ~loc) in
      let toffs = Build.trange ~loc (Some tmin) (Some tmax) in
      let term = Build.tbinop ~loc PlusPI tvar toffs in
      Logic_const.pvalid ~loc (here, term)
    in
    let size_var = Build.tvar ~loc vi_size in
    let left_pred = make_valid_range size_var in
    let pred =
      find_predicate_by_width vi_buffer.vtype "format_length" "wformat_length"
    in
    match pred with
    | Some format_length ->
      let labels = List.map (fun _ -> here) format_length.l_labels in
      let fmt_var = Build.tvar ~loc fmt_vi in
      let flen_app =
        try Build.tapp ~loc format_length labels [fmt_var]
        with Build.NotAFunction ->
          Self.abort ~current:true
            "%a should be a logic function, not a predicate"
            Printer.pp_logic_var format_length.l_var_info
      in
      let right_pred = make_valid_range flen_app in
      let p = Logic_const.por ~loc (left_pred, right_pred) in
      insert_require p
    | None -> insert_require left_pred
  in

  begin match format_fun.f_buffer, format_fun.f_kind with
    | StdIO, ScanfLike ->
      begin match find_global env "__fc_stdin" with
        | Some vi -> add_stream vi
        | None -> ()
      end
    | StdIO, PrintfLike ->
      begin match find_global env "__fc_stdout" with
        | Some vi -> add_stream vi
        | None -> ()
      end
    | Arg (i, _), ScanfLike ->
      add_var ~indirect:true (List.nth sformals i, `ArgInArray None)
    | Arg (i, size_pos), PrintfLike ->
      add_var ~indirect:true (List.nth sformals i, `ArgOutArray);
      begin match size_pos with
        | Some n ->
          add_buffer (List.nth sformals i) (List.nth sformals n)
        | None -> ()
      end
    | Stream i, _ ->
      add_stream (List.nth sformals i)
    | File i, _ ->
      let file = List.nth sformals i in
      add_var ~indirect:true (file, `ArgIn);
    | Syslog, _ -> ()
  end;

  (* Build the assigns clause (without \result, for now; it will be added
     separately) *)
  let froms = List.map (fun iterm -> iterm, From !sources) !dests in

  (* Add return value dest: it is different from above since it is _indirectly_
     assigned from all sources *)
  let rettyp = Cil.getReturnType vf.vf_decl.vtype in
  let froms_for_result =
    if Cil.isVoidType rettyp then []
    else
      [iterm (Build.tresult rettyp),
       From (List.map make_indirect !sources)]
  in
  let assigns = Writes (froms_for_result @ froms) in

  (* Build the default behaviour *)
  let bhv = Cil.mk_behavior ~assigns
      ~requires:!requires ~post_cond:!ensures () in
  { (Cil.empty_funspec ()) with spec_behavior = [bhv] }


(* --- Call translation --- *)

let format_fun_call ~fundec env format_fun scope loc mk_call vf args =
  let name = vf.vf_decl.vorig_name
  and params = Typ.params vf.vf_decl.vtype in
  (* Remove the va_param parameter added during the declaration visit *)
  let fixed_params_count = Typ.params_count vf.vf_original_type in
  let sparams = List.take fixed_params_count params in

  (* Extract the format if possible *)
  let format =
    try
      let format_arg = List.nth args format_fun.f_format_pos in
      match static_string format_arg with
      | None ->
        Self.warning ~current:true
          "Call to function %s with non-static format argument:@ \
           no specification will be generated." name;
        raise Translate_call_exn (* No syntactic hint *)
      | Some s -> Format_parser.parse_format format_fun.f_kind s
    with
    | Format_parser.Invalid_format -> raise Translate_call_exn
  in

  (* Try to type expected parameters if possible *)
  let find_typedef = Environment.find_type env in
  let tvparams =
    try
      Format_typer.type_format ~find_typedef format
    with Format_typer.Type_not_found type_name ->
      Self.warning ~current:true
        "Unable to find type %s in the source code which should be used in \
         this call:@ no specification will be generated.@ \
         Note that due to cleanup, the type may have been defined in the \
         original code but not used anywhere."
        type_name;
      raise Translate_call_exn
  in
  let new_param i (typ,_dir) =
    let typ = if Cil.isIntegralType typ then
        Cil.integralPromotion typ
      else
        typ
    in
    "param" ^ string_of_int i, typ, []
  in
  let vparams = List.mapi new_param tvparams in
  let new_params = sparams @ vparams in

  (* Create the new callee *)
  vf.vf_specialization_count <- vf.vf_specialization_count + 1;
  let ret_typ, _, _, attributes = Cil.splitFunctionType vf.vf_decl.vtype in
  let new_callee_typ = TFun (ret_typ, Some new_params, false, attributes)
  and new_name = name ^ "_va_" ^ (string_of_int vf.vf_specialization_count)
  and mk_spec formals = build_fun_spec env loc vf format_fun tvparams formals
  in
  let new_callee, glob =
    Build.function_declaration ~vattr:[Attr ("fc_stdlib_generated", [])]
      ~loc:vf.vf_decl.vdecl name new_callee_typ mk_spec
  in
  new_callee.vname <- new_name;
  new_globals := glob :: !new_globals;

  (* Store the translation *)
  Replacements.add new_callee vf.vf_decl;

  (* Translate the call *)
  Self.result ~current:true ~level:2
    "Translating call to %s to a call to the specialized version %s."
    name new_callee.vname;
  let tparams = params_types new_params in
  match_call ~loc ~fundec scope mk_call new_callee tparams args
