(********************************************************************)
(*                                                                  *)
(*  The Why3 Verification Platform   /   The Why3 Development Team  *)
(*  Copyright 2010-2022 --  Inria - CNRS - Paris-Saclay University  *)
(*                                                                  *)
(*  This software is distributed under the terms of the GNU Lesser  *)
(*  General Public License version 2.1, with the special exception  *)
(*  on linking described in file LICENSE.                           *)
(*                                                                  *)
(********************************************************************)

open Wstdlib
open Ident
open Ty
open Term
open Decl
open Theory
open Ity
open Expr
open Pdecl

(** *)

type prog_symbol =
  | PV of pvsymbol
  | RS of rsymbol
  | OO of Srs.t

type namespace = {
  ns_ts : itysymbol   Mstr.t;  (* type symbols *)
  ns_ps : prog_symbol Mstr.t;  (* program symbols *)
  ns_xs : xsymbol     Mstr.t;  (* exception symbols *)
  ns_ns : namespace   Mstr.t;  (* inner namespaces *)
}

let empty_ns = {
  ns_ts = Mstr.empty;
  ns_ps = Mstr.empty;
  ns_xs = Mstr.empty;
  ns_ns = Mstr.empty;
}

let ns_replace eq chk x vo vn =
  if not chk then vn else
  if eq vo vn then vo else
  raise (ClashSymbol x)

let merge_ts = ns_replace its_equal
let merge_xs = ns_replace xs_equal

type overload =
  | FixedRes of ity (* t -> t -> ... -> T *)
  | SameType        (* t -> t -> ... -> t *)
  | NoOver          (* neither *)

let overload_of_rs {rs_cty = cty} =
  if cty.cty_effect.eff_ghost then NoOver else
  if cty.cty_mask <> MaskVisible then NoOver else
  let same_type ity a = not a.pv_ghost && ity_equal a.pv_ity ity in
  match cty.cty_args with
  | a::al when not a.pv_ghost && List.for_all (same_type a.pv_ity) al ->
      let res = cty.cty_result in
      if ity_equal res a.pv_ity then SameType else
      if ity_closed res && res.ity_pure then FixedRes res else NoOver
  | _ -> NoOver

let same_overload r1 r2 =
  List.length r1.rs_cty.cty_args = List.length r2.rs_cty.cty_args &&
  match overload_of_rs r1, overload_of_rs r2 with
  | SameType, SameType -> true
  | FixedRes t1, FixedRes t2 -> ity_equal t1 t2
  | _ -> false (* two NoOver's are not the same *)

let ref_attr = Ident.create_attribute "mlw:reference_var"

let has_ref s =
  let has v = Sattr.mem ref_attr v.pv_vs.vs_name.id_attrs in
  List.exists has s.rs_cty.cty_args

exception IncompatibleNotation of string

let merge_ps chk x vo vn =
  let fsty rs = (List.hd rs.rs_cty.cty_args).pv_ity in
  if chk then match vo, vn with (* export namespace *)
    (* currently, we have no way to export notation *)
    | _, OO _ | OO _, _ -> assert false
    | PV v1, PV v2 when pv_equal v1 v2 -> vo
    | RS r1, RS r2 when rs_equal r1 r2 -> vo
    | _ -> raise (ClashSymbol x)
  else match vo, vn with (* import namespace *)
    (* once again, no way to export notation *)
    | _, OO _ -> assert false
    (* but we can merge two compatible symbols *)
    | RS r1, RS r2 ->
        if rs_equal r1 r2 then vo else
        if has_ref r1 || has_ref r2 then vn else
        if not (same_overload r1 r2) then vn else
        if ity_equal (fsty r1) (fsty r2) then vn else
        OO (Srs.add r2 (Srs.singleton r1))
    (* or add a compatible symbol to notation *)
    | OO s1, RS r2 ->
        if Srs.mem r2 s1 then vo else
        if has_ref r2 then vn else
        let r1 = Srs.choose s1 in
        if not (same_overload r1 r2) then vn else
        let ty = fsty r2 in
        let confl r = not (ity_equal (fsty r) ty) in
        let s1 = Srs.filter confl s1 in
        if Srs.is_empty s1 then vn else
        OO (Srs.add r2 s1)
    | _ -> vn

let rec merge_ns chk _ no nn =
  if no == nn then no else
  let union merge o n =
    let merge x vo vn = Some (merge chk x vo vn) in
    if o == n then o else Mstr.union merge o n in
  { ns_ts = union merge_ts no.ns_ts nn.ns_ts;
    ns_ps = union merge_ps no.ns_ps nn.ns_ps;
    ns_xs = union merge_xs no.ns_xs nn.ns_xs;
    ns_ns = union merge_ns no.ns_ns nn.ns_ns }

let ns_add merge chk x vn m = Mstr.change (function
  | Some vo -> Some (merge chk x vo vn)
  | None    -> Some vn) x m

let add_ts chk x ts ns = { ns with ns_ts = ns_add merge_ts chk x ts ns.ns_ts }
let add_ps chk x ps ns = { ns with ns_ps = ns_add merge_ps chk x ps ns.ns_ps }
let add_xs chk x xs ns = { ns with ns_xs = ns_add merge_xs chk x xs ns.ns_xs }
let add_ns chk x nn ns = { ns with ns_ns = ns_add merge_ns chk x nn ns.ns_ns }

let merge_ns chk nn no = merge_ns chk "" no nn (* swap arguments *)

let rec ns_find get_map ns = function
  | []   -> assert false
  | [a]  -> Mstr.find a (get_map ns)
  | a::l -> ns_find get_map (Mstr.find a ns.ns_ns) l

let ns_find_its = ns_find (fun ns -> ns.ns_ts)
let ns_find_xs  = ns_find (fun ns -> ns.ns_xs)
let ns_find_ns  = ns_find (fun ns -> ns.ns_ns)

let ns_find_prog_symbol ns s =
  let ps = ns_find (fun ns -> ns.ns_ps) ns s in
  match ps with
  | RS _ | PV _ -> ps
  | OO ss ->
      let rs1 = Expr.Srs.min_elt ss in
      let rs2 = Expr.Srs.max_elt ss in
      if Expr.rs_equal rs1 rs2 then RS rs1 else ps

let ns_find_pv ns s = match ns_find_prog_symbol ns s with
  | PV pv -> pv | _ -> raise Not_found

let ns_find_rs ns s = match ns_find_prog_symbol ns s with
  | RS rs -> rs | _ -> raise Not_found

(** {2 Module} *)

type pmodule = {
  mod_theory : theory;        (* pure theory *)
  mod_units  : mod_unit list; (* module declarations *)
  mod_export : namespace;     (* exported namespace *)
  mod_known  : known_map;     (* known identifiers *)
  mod_local  : Sid.t;         (* locally declared idents *)
  mod_used   : Sid.t;         (* used modules *)
}

and mod_unit =
  | Udecl  of pdecl
  | Uuse   of pmodule
  | Uclone of mod_inst
  | Umeta  of meta * meta_arg list
  | Uscope of string * mod_unit list

and mod_inst = {
  mi_mod : pmodule;
  mi_ty  : ity Mts.t;
  mi_ts  : itysymbol Mts.t;
  mi_ls  : lsymbol Mls.t;
  mi_pr  : prsymbol Mpr.t;
  mi_pk  : prop_kind Mpr.t;
  mi_pv  : pvsymbol Mvs.t;
  mi_rs  : rsymbol Mrs.t;
  mi_xs  : xsymbol Mxs.t;
  mi_df  : prop_kind;
}

let empty_mod_inst m = {
  mi_mod = m;
  mi_ty  = Mts.empty;
  mi_ts  = Mts.empty;
  mi_ls  = Mls.empty;
  mi_pr  = Mpr.empty;
  mi_pk  = Mpr.empty;
  mi_pv  = Mvs.empty;
  mi_rs  = Mrs.empty;
  mi_xs  = Mxs.empty;
  mi_df  = Plemma;
}

(** {2 Module under construction} *)

type pmodule_uc = {
  muc_theory : theory_uc;
  muc_units  : mod_unit list;
  muc_import : namespace list;
  muc_export : namespace list;
  muc_known  : known_map;
  muc_local  : Sid.t;
  muc_used   : Sid.t;
  muc_env    : Env.env;
}

let empty_module env n p = {
  muc_theory = create_theory ~path:p n;
  muc_units  = [];
  muc_import = [empty_ns];
  muc_export = [empty_ns];
  muc_known  = Mid.empty;
  muc_local  = Sid.empty;
  muc_used   = Sid.empty;
  muc_env    = env;
}

let close_module, restore_module =
  let h = Hid.create 17 in
  (fun uc ->
     let th = close_theory uc.muc_theory in (* catches errors *)
     let m = { mod_theory = th;
               mod_units  = List.rev uc.muc_units;
               mod_export = List.hd uc.muc_export;
               mod_known  = uc.muc_known;
               mod_local  = uc.muc_local;
               mod_used   = uc.muc_used; } in
     Hid.add h th.th_name m;
     m),
  (fun th -> Hid.find h th.th_name)

let open_scope uc s = match uc.muc_import with
  | ns :: _ -> { uc with
      muc_theory = Theory.open_scope uc.muc_theory s;
      muc_units  = [Uscope (s, uc.muc_units)];
      muc_import =       ns :: uc.muc_import;
      muc_export = empty_ns :: uc.muc_export; }
  | [] -> assert false

let close_scope uc ~import =
  let th = Theory.close_scope uc.muc_theory ~import in
  match List.rev uc.muc_units, uc.muc_import, uc.muc_export with
  | [Uscope (_,ul1)], _ :: sti, _ :: ste -> (* empty scope *)
      { uc with muc_theory = th;  muc_units  = ul1;
                muc_import = sti; muc_export = ste; }
  | Uscope (s,ul1) :: ul0, _ :: i1 :: sti, e0 :: e1 :: ste ->
      let i1 = if import then merge_ns false e0 i1 else i1 in
      let i1 = add_ns false s e0 i1 in
      let e1 = add_ns true  s e0 e1 in
      { uc with
          muc_theory = th;
          muc_units  = Uscope (s,ul0) :: ul1;
          muc_import = i1 :: sti;
          muc_export = e1 :: ste; }
  | _ -> assert false

let import_scope uc ql = match uc.muc_import with
  | i1 :: sti ->
      let th = Theory.import_scope uc.muc_theory ql in
      let i1 = merge_ns false (ns_find_ns i1 ql) i1 in
      { uc with muc_theory = th; muc_import = i1::sti }
  | _ -> assert false

let use_export uc ({mod_theory = mth} as m) =
  let th = Theory.use_export uc.muc_theory mth in
  let uc = if Sid.mem mth.th_name uc.muc_used then uc
    else { uc with
      muc_known = merge_known uc.muc_known m.mod_known;
      muc_used  = Sid.add mth.th_name uc.muc_used } in
  match uc.muc_import, uc.muc_export with
  | i0 :: sti, e0 :: ste -> { uc with
      muc_theory = th;
      muc_units  = Uuse m :: uc.muc_units;
      muc_import = merge_ns false m.mod_export i0 :: sti;
      muc_export = merge_ns true  m.mod_export e0 :: ste; }
  | _ -> assert false

let store_path, store_module, restore_path, restore_module_id =
  let id_to_path = Wid.create 17 in
  let id_to_pmod = Wid.create 17 in
  let store_path {muc_theory = uc} path id =
    (* this symbol already belongs to some theory *)
    if Wid.mem id_to_path id then () else
    let prefix = List.rev (id.id_string :: path @ uc.uc_prefix) in
    Wid.set id_to_path id (uc.uc_path, uc.uc_name.id_string, prefix) in
  let store_module pmod  =
    let th = pmod.mod_theory in
    let id = th.th_name in
    (* this symbol is already a module *)
    if Wid.mem id_to_path id then () else begin
        Wid.set id_to_path id (th.th_path, id.id_string, []);
        Sid.iter (fun id -> Wid.set id_to_pmod id pmod) pmod.mod_local;
        Wid.set id_to_pmod id pmod;
      end
  in
  let restore_path id = Wid.find id_to_path id in
  let restore_module_id id = Wid.find id_to_pmod id in
  store_path, store_module, restore_path, restore_module_id

let close_module uc =
  let m = close_module uc in
  store_module m;
  m

(*
let count_regions {muc_known = kn} {pv_ity = ity} mr =
  let add_reg r mr = Mreg.change (fun n -> Some (n <> None)) r mr in
  let meet mr1 mr2 = Mreg.union (fun _ x y -> Some (x || y)) mr1 mr2 in
  let join mr1 mr2 = Mreg.union (fun _ _ _ -> Some true) mr1 mr2 in
  let rec down mr ity = if ity.ity_imm then mr else
    match ity.ity_node with
    | Ityreg r -> fields (add_reg r mr) r.reg_its r.reg_args r.reg_regs
    | Ityapp (s,tl,rl) -> fields mr s tl rl
    | Ityvar _ -> assert false
  and fields mr s tl rl =
    if s.its_private && rl = [] then mr else
    let isb = its_match_regs s tl rl in
    let add_ity mr ity = down mr (ity_full_inst isb ity) in
    let add_proj mr f = add_ity mr f.rs_cty.cty_result in
    let add_field mr v = add_ity mr v.pv_ity in
    let d = find_its_defn kn s in
    if d.itd_constructors = [] then
      List.fold_left add_proj mr d.itd_fields
    else
      join mr (List.fold_left (fun mr c ->
        meet mr (List.fold_left add_field Mreg.empty c.rs_cty.cty_args))
        Mreg.empty d.itd_constructors) in
  down mr ity
*)

let add_symbol add id v uc =
  store_path uc [] id;
  match uc.muc_import, uc.muc_export with
  | i0 :: sti, e0 :: ste -> { uc with
      muc_import = add false id.id_string v i0 :: sti;
      muc_export = add true  id.id_string v e0 :: ste }
  | _ -> assert false

let add_pdecl_no_logic uc d =
  let uc = { uc with
    muc_units = Udecl d :: uc.muc_units;
    muc_known = known_add_decl uc.muc_known d;
    muc_local = Sid.union uc.muc_local d.pd_news } in
  let add_rs uc s = add_symbol add_ps s.rs_name (RS s) uc in
  let add_rd uc d = add_rs uc d.rec_sym in
  match d.pd_node with
  | PDtype tdl ->
      let add uc d =
        let uc = List.fold_left add_rs uc d.itd_fields in
        let uc = List.fold_left add_rs uc d.itd_constructors in
        add_symbol add_ts d.itd_its.its_ts.ts_name d.itd_its uc in
      List.fold_left add uc tdl
  | PDlet (LDvar (v,_)) -> add_symbol add_ps v.pv_vs.vs_name (PV v) uc
  | PDlet (LDsym (s,_)) -> add_rs uc s
  | PDlet (LDrec l) -> List.fold_left add_rd uc l
  | PDexn xs -> add_symbol add_xs xs.xs_name xs uc
  | PDpure -> uc

let add_pdecl_raw ?(warn=true) uc d =
  let uc = add_pdecl_no_logic uc d in
  let th = List.fold_left (add_decl ~warn) uc.muc_theory d.pd_pure in
  let add_meta th (m,l) = Theory.add_meta th m l in
  let th = List.fold_left add_meta th d.pd_meta in
  { uc with muc_theory = th }

(** {2 Builtin symbols} *)

let dummy_env = Env.create_env []

let builtin_module =
  let uc = empty_module dummy_env (id_fresh "BuiltIn") ["why3";"BuiltIn"] in
  let uc = add_pdecl_no_logic uc pd_int in
  let uc = add_pdecl_no_logic uc pd_real in
  let uc = add_pdecl_no_logic uc pd_str in
  let uc = add_pdecl_no_logic uc pd_equ in
  let m = close_module uc in
  { m with mod_theory = builtin_theory }

let bool_module =
  let uc = empty_module dummy_env (id_fresh "Bool") ["why3";"Bool"] in
  let uc = add_pdecl_no_logic uc pd_bool in
  let m = close_module uc in
  { m with mod_theory = bool_theory }

let highord_module =
  let uc = empty_module dummy_env (id_fresh "HighOrd") ["why3";"HighOrd"] in
  let uc = add_pdecl_no_logic uc pd_func in
  let uc = add_pdecl_no_logic uc pd_func_app in
  let m = close_module uc in
  { m with mod_theory = highord_theory }

let tuple_module = Hint.memo 17 (fun n ->
  let nm = "Tuple" ^ string_of_int n in
  let uc = empty_module dummy_env (id_fresh nm) ["why3";nm] in
  let uc = add_pdecl_no_logic uc (pd_tuple n) in
  let m = close_module uc in
  { m with mod_theory = tuple_theory n })

let unit_module =
  let uc = empty_module dummy_env (id_fresh "Unit") ["why3";"Unit"] in
  let uc = use_export uc (tuple_module 0) in
  let add uc d = add_pdecl_raw ~warn:false uc d in
  let td = create_alias_decl (id_fresh "unit") [] ity_unit in
  close_module (List.fold_left add uc (create_type_decl [td]))

let itd_ref =
  let tv = create_tvsymbol (id_fresh "a") in
  let attrs = Sattr.singleton is_field_id_attr in
  let id_contents = id_fresh ~attrs "contents" in
  let pj = create_pvsymbol id_contents (ity_var tv) in
  create_plain_record_decl ~priv:false ~mut:true (id_fresh "ref")
                                            [tv] [true, pj] [] None

let its_ref = itd_ref.itd_its
let rs_ref_cons = List.hd itd_ref.itd_constructors
let rs_ref_proj = List.hd itd_ref.itd_fields

let ts_ref = its_ref.its_ts
let ls_ref_cons = ls_of_rs rs_ref_cons
let ls_ref_proj = ls_of_rs rs_ref_proj

let rs_ref_ld, rs_ref =
  let cty = rs_ref_cons.rs_cty in
  let ityl = List.map (fun v -> v.pv_ity) cty.cty_args in
  let_sym (id_fresh "ref") (c_app rs_ref_cons [] ityl cty.cty_result)

let ref_module =
  let add uc d = add_pdecl_raw ~warn:false uc d in
  let uc = empty_module dummy_env (id_fresh "Ref") ["why3";"Ref"] in
  let uc = List.fold_left add uc (create_type_decl [itd_ref]) in
  let uc = use_export uc builtin_module (* needed for "=" *) in
  close_module (add uc (create_let_decl rs_ref_ld))

let create_module env ?(path=[]) n =
  let m = empty_module env n path in
  let m = use_export m builtin_module in
  let m = use_export m bool_module in
  let m = use_export m unit_module in
  m

let add_use uc syms = Sid.fold (fun id uc ->
  if id_equal id ts_func.ts_name then
    use_export uc highord_module
  else if id_equal id ts_ref.ts_name then
    use_export uc ref_module
  else match is_ts_tuple_id id with
  | Some n -> use_export uc (tuple_module n)
  | None -> uc) (Mid.set_diff syms uc.muc_known) uc

let mk_vc uc d = Vc.vc uc.muc_env uc.muc_known uc.muc_theory d

let add_pdecl ?(warn=true) ~vc uc d =
  let uc = add_use uc d.pd_syms in
  let dl = if vc then mk_vc uc d else [] in
  (* verification conditions must not add additional dependencies
     on built-in theories like TupleN or HighOrd. Also, we expect
     int.Int or any other library theory to be in the context:
     importing them automatically seems to be too invasive. *)
  add_pdecl_raw ~warn (List.fold_left (add_pdecl_raw ~warn) uc dl) d

let syms_of_ts s ts = Sid.add ts.ts_name s
let syms_of_ty s ty = ty_s_fold syms_of_ts s ty
let syms_of_ls s ls = let s = Sid.add ls.ls_name s in
                      let s = Opt.fold syms_of_ty s ls.ls_value in
                      List.fold_left syms_of_ty s ls.ls_args

let add_meta uc m al =
  let add s = function
    | MAts ts -> syms_of_ts s ts
    | MAty ty -> syms_of_ty s ty
    | MAls ls -> syms_of_ls s ls
    | _ -> s in
  let uc = add_use uc (List.fold_left add Sid.empty al) in
  { uc with muc_theory = Theory.add_meta uc.muc_theory m al;
            muc_units  = Umeta (m,al) :: uc.muc_units; }

(** {2 Cloning} *)

type clones = {
  cl_local : Sid.t;
  mutable ty_table : ity Mts.t;
  mutable ts_table : itysymbol Mts.t;
  mutable ls_table : lsymbol Mls.t;
  mutable pr_table : prsymbol Mpr.t;
  mutable rn_table : region Mreg.t;
  mutable fd_table : pvsymbol Mpv.t;
  mutable pv_table : pvsymbol Mvs.t;
  mutable rs_table : rsymbol Mrs.t;
  mutable xs_table : xsymbol Mxs.t;
}

let empty_clones m = {
  cl_local = m.mod_local;
  ty_table = Mts.empty;
  ts_table = Mts.empty;
  ls_table = Mls.empty;
  pr_table = Mpr.empty;
  rn_table = Mreg.empty;
  fd_table = Mpv.empty;
  pv_table = Mvs.empty;
  rs_table = Mrs.empty;
  xs_table = Mxs.empty;
}

(* populate the clone structure *)

let rec sm_trans_ty tym tsm ty = match ty.ty_node with
  | Tyapp (s, tl) ->
      let tl = List.map (sm_trans_ty tym tsm) tl in
      begin match Mts.find_opt s tsm with
      | Some its -> ty_app its.its_ts tl
      | None -> begin match Mts.find_opt s tym with
      | Some ity -> ty_inst (ts_match_args s tl) (ty_of_ity ity)
      | None -> ty_app s tl
      end end
  | Tyvar _ -> ty

let clone_ty cl ty = sm_trans_ty cl.ty_table cl.ts_table ty

let cl_find_its cl its =
  if not (Sid.mem its.its_ts.ts_name cl.cl_local) then its
  else Mts.find its.its_ts cl.ts_table

let cl_find_ts cl ts =
  if not (Sid.mem ts.ts_name cl.cl_local) then ts
  else (Mts.find ts cl.ts_table).its_ts

let rec clone_ity cl ity = match ity.ity_node with
  | Ityreg r ->
      ity_reg (clone_reg cl r)
  | Ityapp (s, tl, rl) ->
      let tl' = List.map (clone_ity cl) tl in
      begin match Mts.find_opt s.its_ts cl.ts_table with
      | Some s' -> ity_app_pure s' tl' (* pure to pure *)
                     (if List.for_all (fun r -> r.ity_pure) rl
                        then [] else clone_regs cl s tl rl s')
      | None -> (* creative indentation *)
      begin match Mts.find_opt s.its_ts cl.ty_table with
      | Some ity -> ity_full_inst (its_match_regs s tl' []) ity
      | None -> ity_app_pure s tl' (List.map (clone_ity cl) rl)
      end end
  | Ityvar _ -> ity

and clone_reg cl reg =
  try Mreg.find reg cl.rn_table with Not_found ->
  let tl' = List.map (clone_ity cl) reg.reg_args in
  let s',rl' = match Mts.find_opt reg.reg_its.its_ts cl.ts_table with
    | Some s' -> s', clone_regs cl reg.reg_its reg.reg_args reg.reg_regs s'
    | None -> reg.reg_its, List.map (clone_ity cl) reg.reg_regs in
  let r = create_region (id_clone reg.reg_name) s' tl' rl' in
  cl.rn_table <- Mreg.add reg r cl.rn_table;
  r

and clone_regs cl s tl rl s' =
  if rl = [] then [] else
  let base = its_match_regs s tl rl in
  let add sbs pj =
    let ity = clone_ity cl (ity_full_inst base pj.pv_ity) in
    ity_match sbs (Mpv.find pj cl.fd_table).pv_ity ity in
  let sbs = List.fold_left add isb_empty s.its_mfields in
  let sbs = List.fold_left add sbs s.its_ofields in
  let rec inst ity =
    if ity.ity_pure then ity_full_inst sbs ity else
    match ity.ity_node with
    | Ityreg r when Mreg.mem r sbs.isb_reg ->
        Mreg.find r sbs.isb_reg
    | Ityreg {reg_its = s; reg_args = tl; reg_regs = rl} ->
        ity_app s (List.map inst tl) (List.map inst rl)
    | Ityapp (s,tl,rl) ->
        ity_app_pure s (List.map inst tl) (List.map inst rl)
    | Ityvar v ->
        Mtv.find v sbs.isb_var in
  List.map (fun reg -> inst (ity_reg reg)) s'.its_regions

let cl_find_ls cl ls =
  if not (Sid.mem ls.ls_name cl.cl_local) then ls
  else Mls.find ls cl.ls_table

let clone_term cl mv t = t_gen_map (clone_ty cl) (cl_find_ls cl) mv t

let clone_fmla cl f = clone_term cl Mvs.empty f (* for closed terms *)

let cl_find_pr cl pr =
  if not (Sid.mem pr.pr_name cl.cl_local) then pr
  else Mpr.find pr cl.pr_table

let cl_find_pv cl pv =
  if not (Sid.mem pv.pv_vs.vs_name cl.cl_local) then pv
  else Mvs.find pv.pv_vs cl.pv_table

let cl_find_rs cl rs =
  if not (Sid.mem rs.rs_name cl.cl_local) then rs
  else Mrs.find rs cl.rs_table

let cl_find_xs cl xs =
  if not (Sid.mem xs.xs_name cl.cl_local) then xs
  else Mxs.find xs cl.xs_table

let cl_init m inst =
  let cl = empty_clones m in
  let non_local id =
    if not (Sid.mem id cl.cl_local) then raise (NonLocal id) in
  Mts.iter (fun ts _ -> non_local ts.ts_name) inst.mi_ts;
  Mts.iter (fun ts _ -> non_local ts.ts_name) inst.mi_ty;
  let check_ls ls _ =
    non_local ls.ls_name;
    try ignore (restore_rs ls);
        raise (BadInstance (BadI_ls_rs ls))
    with Not_found -> () in
  Mls.iter check_ls inst.mi_ls;
  let check_rs rs _ =
    non_local rs.rs_name;
    match (Mid.find rs.rs_name m.mod_known).pd_node with
    | PDtype _ -> raise (CannotInstantiate rs.rs_name)
    | PDlet  _ | PDexn  _ | PDpure -> () in
  Mrs.iter check_rs inst.mi_rs;
  Mvs.iter (fun vs _ -> non_local vs.vs_name) inst.mi_pv;
  Mxs.iter (fun xs _ -> non_local xs.xs_name) inst.mi_xs;
  let check_pk pr _ =
    non_local pr.pr_name;
    match (Mid.find pr.pr_name m.mod_known).pd_node with
    | PDtype _ | PDlet  _ | PDexn  _ ->
        raise (CannotInstantiate pr.pr_name)
    | PDpure -> () in
  Mpr.iter check_pk inst.mi_pk;
  Mpr.iter (fun pr _ -> (* only through bad API use *)
    raise (BadInstance (BadI pr.pr_name))) inst.mi_pr;
  cl

(* clone declarations *)

let clone_ls cl ls =
  let constr = ls.ls_constr in
  let id = id_clone ls.ls_name in
  let at = List.map (clone_ty cl) ls.ls_args in
  let vt = Opt.map (clone_ty cl) ls.ls_value in
  let ls' = create_lsymbol ~constr id at vt in
  cl.ls_table <- Mls.add ls ls' cl.ls_table;
  ls'

let clone_decl inst cl uc d = match d.d_node with
  | Dtype _ | Ddata _ -> assert false (* impossible *)
  | Dparam ls when Mls.mem ls inst.mi_ls ->
      let ls' = Mls.find ls inst.mi_ls in
      let mtch sb ty ty' = try ty_match sb ty' (clone_ty cl ty) with
        | Ty.TypeMismatch (ty1,ty2) -> raise (BadInstance (BadI_ls_type (ls,ty1,ty2))) in
      let sbs = match ls.ls_value,ls'.ls_value with
        | Some ty, Some ty' -> mtch Mtv.empty ty ty'
        | None, None -> Mtv.empty
        | _ -> raise (BadInstance (BadI_ls_kind ls)) in
      ignore (try List.fold_left2 mtch sbs ls.ls_args ls'.ls_args
        with Invalid_argument _ ->
          raise (BadInstance (BadI_ls_arity ls)));
      cl.ls_table <- Mls.add ls ls' cl.ls_table;
      uc
  | Dparam ls ->
      let d = create_param_decl (clone_ls cl ls) in
      add_pdecl ~warn:false ~vc:false uc (create_pure_decl d)
  | Dlogic ldl ->
      List.iter (fun (ls,_) ->
        if Mls.mem ls inst.mi_ls then raise (CannotInstantiate ls.ls_name);
        ignore (clone_ls cl ls)) ldl;
      let get_logic (_,ld) =
        Opt.get (ls_defn_of_axiom (clone_fmla cl (ls_defn_axiom ld))) in
      let d = create_logic_decl (List.map get_logic ldl) in
      add_pdecl ~warn:false ~vc:false uc (create_pure_decl d)
  | Dind (s, idl) ->
      let lls = List.map (fun (ls,_) ->
        if Mls.mem ls inst.mi_ls then raise (CannotInstantiate ls.ls_name);
        clone_ls cl ls) idl in
      let get_case (pr,f) =
        if Mpr.mem pr inst.mi_pk then raise (CannotInstantiate pr.pr_name);
        let pr' = create_prsymbol (id_clone pr.pr_name) in
        cl.pr_table <- Mpr.add pr pr' cl.pr_table;
        pr', clone_fmla cl f in
      let get_ind ls (_,la) = ls, List.map get_case la in
      let d = create_ind_decl s (List.map2 get_ind lls idl) in
      add_pdecl ~warn:false ~vc:false uc (create_pure_decl d)
  | Dprop (k,pr,f) ->
      let skip, k' = match k, Mpr.find_opt pr inst.mi_pk with
        | Pgoal, _ -> true, Pgoal
        | Plemma, Some Pgoal -> true, Pgoal
        | Plemma, _ -> false, Plemma
        | Paxiom, Some k -> false, k
        | Paxiom, None -> false, inst.mi_df in
      if skip then uc else
      let attr = Sattr.remove Ident.useraxiom_attr pr.pr_name.id_attrs in
      let pr' = create_prsymbol (id_attr pr.pr_name attr) in
      cl.pr_table <- Mpr.add pr pr' cl.pr_table;
      let d = create_prop_decl k' pr' (clone_fmla cl f) in
      add_pdecl ~warn:false ~vc:false uc (create_pure_decl d)

let cl_save_ls cl s s' =
  cl.ls_table <- Mls.add_new (CannotInstantiate s.ls_name) s s' cl.ls_table

let cl_save_rs cl s s' =
  cl.rs_table <- Mrs.add s s' cl.rs_table;
  begin match s.rs_field, s'.rs_field with
  | Some v, Some v' -> cl.fd_table <- Mpv.add v v' cl.fd_table
  | None, _ -> () (* can instantiate a non-field with a field *)
  | _ -> raise (CannotInstantiate s.rs_name) (* but not vice versa *)
  end;
  match s.rs_logic, s'.rs_logic with
  | RLls s, RLls s' -> cl_save_ls cl s s'
  | RLnone, (RLnone | RLls _ | RLlemma) -> ()
  | RLlemma, RLlemma -> ()
  | _ -> raise (BadInstance (BadI_rs_kind s.rs_name))

type smap = {
  sm_vs : vsymbol Mvs.t;
  sm_pv : pvsymbol Mvs.t;
  sm_xs : xsymbol Mxs.t;
  sm_rs : rsymbol Mrs.t;
}

let sm_of_cl cl = {
  sm_vs = Mvs.map (fun v -> v.pv_vs) cl.pv_table;
  sm_pv = cl.pv_table;
  sm_xs = cl.xs_table;
  sm_rs = cl.rs_table }

let sm_save_vs sm v v' = {
  sm_vs = Mvs.add v v'.pv_vs sm.sm_vs;
  sm_pv = Mvs.add v v' sm.sm_pv;
  sm_xs = sm.sm_xs;
  sm_rs = sm.sm_rs }

let sm_save_pv sm v v' = {
  sm_vs = Mvs.add v.pv_vs v'.pv_vs sm.sm_vs;
  sm_pv = Mvs.add v.pv_vs v' sm.sm_pv;
  sm_xs = sm.sm_xs;
  sm_rs = sm.sm_rs }

let sm_save_xs sm s s' =
  { sm with sm_xs = Mxs.add s s' sm.sm_xs }

let sm_save_rs cl sm s s' =
  let sm = { sm with sm_rs = Mrs.add s s' sm.sm_rs } in
  match s.rs_logic, s'.rs_logic with
  | RLls s, RLls s' -> cl_save_ls cl s s'; sm
  | RLpv v, RLpv v' -> sm_save_pv sm v v'
  | _ -> sm

let sm_find_pv sm v = Mvs.find_def v v.pv_vs sm.sm_pv
  (* non-instantiated global variables are not in sm *)

let sm_find_xs sm xs = Mxs.find_def xs xs sm.sm_xs

let sm_find_rs sm rs = Mrs.find_def rs rs sm.sm_rs

let clone_pv cl {pv_vs = vs; pv_ity = ity; pv_ghost = ghost} =
  create_pvsymbol (id_clone vs.vs_name) ~ghost (clone_ity cl ity)

let clone_invl cl sm invl =
  List.map (fun t -> clone_term cl sm.sm_vs t) invl

let clone_varl cl sm varl = List.map (fun (t,r) ->
  clone_term cl sm.sm_vs t, Opt.map (cl_find_ls cl) r) varl

let clone_cty cl sm ?(drop_decr=false) cty =
  let res = clone_ity cl cty.cty_result in
  let args = List.map (clone_pv cl) cty.cty_args in
  let sm_args = List.fold_left2 sm_save_pv sm cty.cty_args args in
  let add_old o n (sm, olds) = let o' = clone_pv cl o in
    sm_save_pv sm o o', Mpv.add o' (sm_find_pv sm_args n) olds in
  let sm_olds, olds = Mpv.fold add_old cty.cty_oldies (sm_args, Mpv.empty) in
  let pre = if drop_decr then List.tl cty.cty_pre else cty.cty_pre in
  let pre = clone_invl cl sm_args pre in
  let post = clone_invl cl sm_olds cty.cty_post in
  let xpost = Mxs.fold (fun xs fl q ->
    let xs = sm_find_xs sm xs in
    let fl = clone_invl cl sm_olds fl in
    Mxs.add xs fl q) cty.cty_xpost Mxs.empty in
  let add_read v s = Spv.add (sm_find_pv sm_args v) s in
  let reads = Spv.fold add_read (cty_reads cty) Spv.empty in
  let reads = List.fold_right add_read cty.cty_args reads in
  let reads = Spv.union reads (Mpv.domain olds) in
  let add_frz v frz = ity_frz_regs frz v.pv_ity in
  let frz = Spv.fold add_frz reads Sreg.empty in
  let add_write reg fs m =
    if Spv.is_empty fs && not reg.reg_its.its_private then m else
    Mreg.add reg fs m in
  let add_write reg fs m =
    let add_fd fd s = Spv.add (Mpv.find_def fd fd cl.fd_table) s in
    (* add old mutable fields to the effect *)
    let fs = Spv.fold add_fd fs Spv.empty in
    (* add new mutable fields to the effect *)
    let ({reg_its = s'} as reg') = clone_reg cl reg in
    let smf_reg' = Spv.of_list s'.its_mfields in
    let smf_reg = Spv.of_list reg.reg_its.its_mfields in
    let smf_ref = Spv.fold add_fd smf_reg Spv.empty in
    let fs = Spv.union fs (Spv.diff smf_reg' smf_ref) in
    let m = add_write reg' fs m in
    (* add new non-mutable fields to the effect *)
    let sof_reg' = Spv.of_list s'.its_ofields in
    let sof_reg = Spv.of_list reg.reg_its.its_ofields in
    let sof_ref = Spv.fold add_fd sof_reg Spv.empty in
    let rec add_ofds r ofds m = if Spv.is_empty ofds then m else
      let sbs = its_match_regs r.reg_its r.reg_args r.reg_regs in
      let add_ofd f m =
        ity_exp_fold add_ofd_reg m (ity_full_inst sbs f.pv_ity) in
      Spv.fold add_ofd ofds m
    and add_ofd_reg m r = if Sreg.mem r frz then m else
      add_ofds r (Spv.of_list r.reg_its.its_ofields)
        (add_write r (Spv.of_list r.reg_its.its_mfields) m) in
    add_ofds reg' (Spv.diff sof_reg' sof_ref) m in
  let writes = Mreg.fold add_write cty.cty_effect.eff_writes Mreg.empty in
  let add_reset reg s = Sreg.add (clone_reg cl reg) s in
  let resets = Sreg.fold add_reset cty.cty_effect.eff_resets Sreg.empty in
  let eff = eff_reset (eff_reset_overwritten (eff_write reads writes)) resets in
  let add_spoil tv eff = eff_spoil eff (ity_var tv) in
  let eff = Stv.fold add_spoil cty.cty_effect.eff_spoils eff in
  let add_raise xs eff = eff_raise eff (sm_find_xs sm xs) in
  let eff = Sxs.fold add_raise cty.cty_effect.eff_raises eff in
  let eff = if partial cty.cty_effect.eff_oneway then eff_partial eff else eff in
  let eff = if diverges cty.cty_effect.eff_oneway then eff_diverge eff else eff in
  let cty = create_cty_defensive ~mask:cty.cty_mask args pre post xpost olds eff res in
  cty_ghostify (cty_ghost cty) cty

let sm_save_args sm c c' =
  List.fold_left2 sm_save_pv sm c.cty_args c'.cty_args

let sm_save_olds sm c c' =
  let revs = Mpv.fold (fun o n m -> Mpv.add n o m) c'.cty_oldies Mpv.empty in
  let add_old o n sm = sm_save_pv sm o (Mpv.find (sm_find_pv sm n) revs) in
  Mpv.fold add_old c.cty_oldies sm

let clone_ppat cl sm pp mask =
  let rec conv_pat p = match p.pat_node with
    | Term.Pwild -> PPwild
    | Term.Pvar v -> PPvar (id_clone v.vs_name, (restore_pv v).pv_ghost)
    | Term.Pas (p,v) ->
        PPas (conv_pat p, id_clone v.vs_name, (restore_pv v).pv_ghost)
    | Term.Por (p1,p2) -> PPor (conv_pat p1, conv_pat p2)
    | Term.Papp (s,pl) ->
        PPapp (restore_rs (cl_find_ls cl s), List.map conv_pat pl) in
  let pre = conv_pat pp.pp_pat in
  let vm, pp' = create_prog_pattern pre (clone_ity cl pp.pp_ity) mask in
  let save v sm = sm_save_vs sm v (Mstr.find v.vs_name.id_string vm) in
  Svs.fold save pp.pp_pat.pat_vars sm, pp'

let rec clone_expr cl sm e = e_attr_copy e (match e.e_node with
  | Evar v -> e_var (sm_find_pv sm v)
  | Econst c -> e_const c (clone_ity cl e.e_ity)
  | Eexec (c,_) -> e_exec (clone_cexp cl sm c)
  | Eassign asl ->
      let conv (r,f,v) =
        e_var (sm_find_pv sm r), cl_find_rs cl f, e_var (sm_find_pv sm v) in
      e_assign (List.map conv asl)
  | Elet (ld, e) ->
      let sm, ld = clone_let_defn None cl sm ld in
      e_let ld (clone_expr cl sm e)
  | Eif (e1, e2, e3) ->
      e_if (clone_expr cl sm e1) (clone_expr cl sm e2) (clone_expr cl sm e3)
  | Ematch (d, bl, xl) ->
      let d = clone_expr cl sm d in
      let conv_rbr (pp, e) =
        let sm, pp = clone_ppat cl sm pp d.e_mask in
        pp, clone_expr cl sm e in
      let conv_xbr xs (vl, e) m =
        let vl' = List.map (clone_pv cl) vl in
        let sm = List.fold_left2 sm_save_pv sm vl vl' in
        Mxs.add (sm_find_xs sm xs) (vl', clone_expr cl sm e) m in
      e_match d (List.map conv_rbr bl) (Mxs.fold conv_xbr xl Mxs.empty)
  | Ewhile (c,invl,varl,e) ->
      e_while (clone_expr cl sm c) (clone_invl cl sm invl)
              (clone_varl cl sm varl) (clone_expr cl sm e)
  | Efor (v, (f,dir,t), i, invl, e) ->
      let v' = clone_pv cl v in
      let ism = sm_save_pv sm v v' in
      let i' = if pv_equal v i then v' else clone_pv cl i in
      let ism = if pv_equal v i then ism else sm_save_pv ism i i' in
      e_for v'
        (e_var (sm_find_pv sm f)) dir (e_var (sm_find_pv sm t))
        i' (clone_invl cl ism invl) (clone_expr cl ism e)
  | Eraise (xs, e1) ->
      e_raise (sm_find_xs sm xs) (clone_expr cl sm e1) (clone_ity cl e.e_ity)
  | Eexn ({xs_name = id; xs_mask = mask; xs_ity = ity} as xs, e) ->
      let xs' = create_xsymbol (id_clone id) ~mask (clone_ity cl ity) in
      e_exn xs' (clone_expr cl (sm_save_xs sm xs xs') e)
  | Eassert (k, f) ->
      e_assert k (clone_term cl sm.sm_vs f)
  | Eghost e ->
      e_ghostify true (clone_expr cl sm e)
  | Epure t ->
      e_pure (clone_term cl sm.sm_vs t)
  | Eabsurd -> e_absurd (clone_ity cl e.e_ity))

and clone_cexp cl sm c = match c.c_node with
  | Capp (s,vl) ->
      let vl = List.map (fun v -> sm_find_pv sm v) vl in
      let al = List.map (fun v -> clone_ity cl v.pv_ity) c.c_cty.cty_args in
      let res = clone_ity cl c.c_cty.cty_result in
      c_app (sm_find_rs sm s) vl al res
  | Cpur (s,vl) ->
      let vl = List.map (fun v -> sm_find_pv sm v) vl in
      let al = List.map (fun v -> clone_ity cl v.pv_ity) c.c_cty.cty_args in
      let res = clone_ity cl c.c_cty.cty_result in
      c_pur (cl_find_ls cl s) vl al res
  | Cfun e ->
      let cty = clone_cty cl sm c.c_cty in
      let sm = sm_save_args sm c.c_cty cty in
      let sm = sm_save_olds sm c.c_cty cty in
      c_fun ~mask:cty.cty_mask cty.cty_args cty.cty_pre
        cty.cty_post cty.cty_xpost cty.cty_oldies (clone_expr cl sm e)
  | Cany ->
      c_any (clone_cty cl sm c.c_cty)

and clone_let_defn loc cl sm ld = match ld with
  | LDvar (v,e) ->
      let e' = clone_expr cl sm e in
      let id = id_clone ?loc v.pv_vs.vs_name in
      let ld, v' = let_var id ~ghost:v.pv_ghost e' in
      sm_save_pv sm v v', ld
  | LDsym (s,c) ->
      let c' = clone_cexp cl sm c in
      let ld, s' = let_sym (id_clone ?loc s.rs_name)
        ~ghost:(rs_ghost s) ~kind:(rs_kind s) c' in
      sm_save_rs cl sm s s', ld
  | LDrec rdl ->
      let conv_rs mrs {rec_rsym = {rs_name = id} as rs} =
        let cty = clone_cty cl sm ~drop_decr:true rs.rs_cty in
        let rs' = create_rsymbol (id_clone ?loc id) ~ghost:(rs_ghost rs) cty in
        Mrs.add rs rs' mrs, rs' in
      let mrs, rsyml = Lists.map_fold_left conv_rs sm.sm_rs rdl in
      let rsm = { sm with sm_rs = mrs } in
      let conv_rd ({rec_fun = c} as rd) ({rs_cty = cty} as rs) =
        let rsm = sm_save_args rsm c.c_cty cty in
        let varl = clone_varl cl rsm rd.rec_varl in
        let rsm = sm_save_olds rsm c.c_cty cty in
        let e = match c.c_node with
          | Cfun e -> clone_expr cl rsm e
          | _ -> assert false (* can't be *) in
        let c = c_fun ~mask:c.c_cty.cty_mask cty.cty_args
          cty.cty_pre cty.cty_post cty.cty_xpost cty.cty_oldies e in
        rs, c, varl, rs_kind rd.rec_sym in
      let ld, rdl' = let_rec (List.map2 conv_rd rdl rsyml) in
      let sm = List.fold_left2 (fun sm d d' ->
        sm_save_rs cl sm d.rec_sym d'.rec_sym) sm rdl rdl' in
      sm, ld

let clone_type_record cl s d s' d' =
  let fields' = Hstr.create 16 in
  let matched_fields' = Hpv.create 16 in
  let add_field' rs' = let pj' = fd_of_rs rs' in
    Hstr.add fields' pj'.pv_vs.vs_name.id_string rs' in
  List.iter add_field' d'.itd_fields;
  (* refinement preserves (im)mutability *)
  if not s.its_mutable && s'.its_mutable then
    raise (BadInstance (BadI_ty_mut_rhs s.its_ts));
  if s.its_mutable && not s'.its_mutable then
    raise (BadInstance (BadI_ty_mut_lhs s.its_ts));
  (* the fields from the old type must appear in the new type *)
  let match_pj (bsb,fsb) rs =
    let pj = fd_of_rs rs in
    let pj_str = pj.pv_vs.vs_name.id_string in
    let pj_ity = clone_ity cl pj.pv_ity in
    let pj_ght = pj.pv_ghost in
    let pj_mut = List.exists (pv_equal pj) s.its_mfields in
    let rs' = try Hstr.find fields' pj_str with Not_found ->
        raise (BadInstance (BadI_field (s.its_ts, pj.pv_vs))) in
    let pj' = fd_of_rs rs' in
    let pj'_ity = pj'.pv_ity in
    let pj'_ght = pj'.pv_ghost in
    let pj'_mut = List.exists (pv_equal pj') s'.its_mfields in
    let bsb = try ity_match bsb pj'_ity pj_ity with TypeMismatch _ ->
      raise (BadInstance (BadI_field_type (s.its_ts, pj.pv_vs))) in
    let fsb = try ity_match fsb pj_ity pj'_ity with TypeMismatch _ ->
      raise (BadInstance (BadI_field_type (s.its_ts, pj.pv_vs))) in
    if not (pj_ght || not pj'_ght) then
      raise (BadInstance (BadI_field_ghost (s.its_ts, pj.pv_vs)));
    if not (pj_mut = pj'_mut) then
      raise (BadInstance (BadI_field_mut (s.its_ts, pj.pv_vs)));
    let ls, ls' = ls_of_rs rs, ls_of_rs rs' in
    cl.ls_table <- Mls.add ls ls' cl.ls_table;
    cl.rs_table <- Mrs.add rs rs' cl.rs_table;
    cl.fd_table <- Mpv.add pj pj' cl.fd_table;
    Hpv.add matched_fields' pj' pj;
    bsb, fsb in
  ignore (List.fold_left match_pj (isb_empty,isb_empty) d.itd_fields);
  (* the new fields must be separated from the old ones *)
  let check_npj' (op,np) rs' =
    let pj' = fd_of_rs rs' in
    if Hpv.mem matched_fields' pj' then ity_freeze op pj'.pv_ity, np
                                   else op, ity_freeze np pj'.pv_ity in
  let op,np = List.fold_left check_npj' (isb_empty,isb_empty) d'.itd_fields in
  if not (Mreg.set_disjoint op.isb_reg np.isb_reg) then
    raise (BadInstance (BadI_ty_alias s.its_ts));
  (* if we refine a mutable type, then all new regions in the invariant
     come from the new fields (no invariant strengthening on the old ones) *)
  let pj_occurs pj f = t_v_occurs pj.pv_vs f > 0 in
  let check_opj' pj' pj =
    if not pj'.pv_ity.ity_pure &&
       List.exists (pj_occurs pj') d'.itd_invariant &&
       not (List.exists (pj_occurs pj) d.itd_invariant) then
    raise (BadInstance (BadI_field_inv (s.its_ts, pj.pv_vs))) in
  if s.its_mutable then Hpv.iter check_opj' matched_fields';
  (* validate the refinement *)
  cl.ts_table <- Mts.add s.its_ts s' cl.ts_table

let clone_type_decl inst cl tdl kn =
  let def =
    List.fold_left (fun m d -> Mits.add d.itd_its d m) Mits.empty tdl in
  let htd = Hits.create 5 in
  let vcs = ref ([] : (itysymbol * term) list) in
  let rec visit alg ({its_ts = {ts_name = id} as ts} as s) d =
    if not (Hits.mem htd s) then
    let alg = Sits.add s alg in
    let id' = id_clone id in
    let cloned = Mts.mem ts inst.mi_ts || Mts.mem ts inst.mi_ty in
    let conv_pj v = create_pvsymbol
      (id_clone v.pv_vs.vs_name) ~ghost:v.pv_ghost (conv_ity alg v.pv_ity) in
    let save_itd itd =
      List.iter2 (cl_save_rs cl) d.itd_constructors itd.itd_constructors;
      List.iter2 (cl_save_rs cl) d.itd_fields itd.itd_fields;
      Hits.add htd s (Some itd) in
    (* alias *)
    if s.its_def <> NoDef then begin
      if cloned then raise (CannotInstantiate id);
      let itd = match s.its_def with
        | Alias ty -> create_alias_decl id' ts.ts_args (conv_ity alg ty)
        | Range ir -> create_range_decl id' ir
        | Float ff -> create_float_decl id' ff
        | NoDef -> assert false (* never *) in
      cl.ts_table <- Mts.add ts itd.itd_its cl.ts_table;
      save_itd itd
    end else
    (* abstract *)
    if s.its_private && cloned then begin
      begin match Mts.find_opt ts inst.mi_ts with
      | Some s' ->
          if not (List.length ts.ts_args = List.length s'.its_ts.ts_args) then
            raise (BadInstance (BadI_ty_arity ts));
          let pd' = Mid.find s'.its_ts.ts_name kn in
          let d' = match pd'.pd_node with
            | PDtype [d'] -> d'
            (* FIXME? we could refine with mutual types *)
            | PDtype _ -> raise (BadInstance (BadI_ty_rec ts))
            | PDlet _ | PDexn _ | PDpure -> assert false in
          clone_type_record cl s d s' d'; (* clone record fields *)
          (* generate and add VC for type invariant implication *)
          if d.itd_invariant <> [] then begin
            let inv = axiom_of_invariant d in
            let invl = clone_invl cl (sm_of_cl cl) [inv] in
            let add_vc inv = vcs := (d.itd_its, inv) :: !vcs in
            List.iter add_vc invl end
      | None -> begin match Mts.find_opt ts inst.mi_ty with
      | Some ity -> (* creative indentation *)
          let stv = Stv.of_list ts.ts_args in
          if not (Stv.subset (ity_freevars Stv.empty ity) stv) then
            raise (BadInstance (BadI_ty_vars ts));
          if not (d.itd_fields = [] && d.itd_invariant = []) then
            raise (BadInstance (BadI_ty_ner ts));
          if not (its_pure s && ity.ity_pure) then
            raise (BadInstance (BadI_ty_impure ts));
          cl.ty_table <- Mts.add ts ity cl.ty_table
      | None -> assert false end end;
      Hits.add htd s None;
    end else
    (* variant *)
    if not s.its_mutable && d.itd_constructors <> [] &&
                            d.itd_invariant = [] then begin
      if cloned then raise (CannotInstantiate id);
      let conv_fd m fd =
        let v = fd_of_rs fd in Mpv.add v (conv_pj v) m in
      let fldm = List.fold_left conv_fd Mpv.empty d.itd_fields in
      let conv_pj pj = match Mpv.find_opt pj fldm with
        | Some pj' -> true, pj' | None -> false, conv_pj pj in
      let conv_cs cs =
        id_clone cs.rs_name, List.map conv_pj cs.rs_cty.cty_args in
      let csl = List.map conv_cs d.itd_constructors in
      match Mts.find_opt ts cl.ts_table with
      | Some s' ->
          let itd = create_rec_variant_decl s' csl in
          save_itd itd
      | None ->
          let itd = create_plain_variant_decl id' ts.ts_args csl in
          cl.ts_table <- Mts.add ts itd.itd_its cl.ts_table;
          save_itd itd
    end else begin
    (* flat record *)
      if cloned then raise (CannotInstantiate id);
      let mfld = Spv.of_list s.its_mfields in
      let pjl = List.map fd_of_rs d.itd_fields in
      let fdl = List.map (fun v -> Spv.mem v mfld, conv_pj v) pjl in
      let inv =
        if d.itd_invariant = [] then [] else
        let add mv u (_,v) = Mvs.add u.pv_vs v.pv_vs mv in
        let mv = List.fold_left2 add Mvs.empty pjl fdl in
        List.map (clone_term cl mv) d.itd_invariant in
      let clone_wit = clone_expr cl (sm_of_cl cl) in
      let wit = Opt.map clone_wit d.itd_witness in
      let itd = create_plain_record_decl id' ts.ts_args
        ~priv:s.its_private ~mut:s.its_mutable fdl inv wit in
      cl.ts_table <- Mts.add ts itd.itd_its cl.ts_table;
      save_itd itd
    end

  and conv_ity alg ity =
    let rec down ity = match ity.ity_node with
      | Ityreg {reg_its = s; reg_args = tl}
      | Ityapp (s,tl,_) ->
          if Sits.mem s alg then begin
            if not (Mts.mem s.its_ts cl.ts_table) then
            let id = id_clone s.its_ts.ts_name in
            let s' = create_rec_itysymbol id s.its_ts.ts_args in
            cl.ts_table <- Mts.add s.its_ts s' cl.ts_table
          end else Opt.iter (visit alg s) (Mits.find_opt s def);
          List.iter down tl
      | Ityvar _ -> () in
    down ity;
    clone_ity cl ity in

  Mits.iter (visit Sits.empty) def;
  Lists.map_filter (fun d -> Hits.find htd d.itd_its) tdl,
  !vcs

let add_vc uc (its, f) =
  let {id_string = nm; id_loc = loc} = its.its_ts.ts_name in
  let attrs = Sattr.singleton (Ident.create_attribute ("expl:VC for " ^ nm)) in
  let pr = create_prsymbol (id_fresh ~attrs ?loc (nm ^ "'vc")) in
  let d = create_pure_decl (create_prop_decl Pgoal pr f) in
  add_pdecl ~warn:false ~vc:false uc d

let freeze_foreign cl reads =
  let frz = Spv.fold (fun v isb ->
    if Sid.mem v.pv_vs.vs_name cl.cl_local then isb
    else ity_freeze isb v.pv_ity) reads isb_empty in
  let frz = Mreg.map (fun ity -> match ity.ity_node with
    | Ityreg r -> r | _ -> assert false) frz.isb_reg in
  cl.rn_table <- Mreg.set_union cl.rn_table frz

let clone_pdecl loc inst cl uc d = match d.pd_node with
  | PDtype tdl ->
      let add_e spv e = Spv.union spv e.e_effect.eff_reads in
      let add_d spv d = Opt.fold add_e spv d.itd_witness in
      freeze_foreign cl (List.fold_left add_d Spv.empty tdl);
      let ndl, vcl = clone_type_decl inst cl tdl uc.muc_known in
      let uc = List.fold_left add_vc uc vcl in
      let dl = if ndl <> [] then create_type_decl ndl else [] in
      let save_special_ls d d' = match d.d_node, d'.d_node with
        | Dparam ls, Dparam ls' | Dlogic [ls,_], Dlogic [ls',_] ->
            cl.ls_table <- Mls.add ls ls' cl.ls_table;
        | Dtype _, Dtype _ -> ()
        | _ -> assert false in
      begin match tdl, dl with
      | [{itd_its = {its_def = (Range _|Float _)}}], [d'] ->
          List.iter2 save_special_ls d.pd_pure d'.pd_pure
      | _ -> () end;
      let add uc d = add_pdecl ~warn:false ~vc:false uc d in
      List.fold_left add uc dl
  | PDlet (LDsym (rs, c)) when Mrs.mem rs inst.mi_rs ->
      freeze_foreign cl (cty_reads c.c_cty);
      (* refine only [val] symbols *)
      let cty = match c.c_node with (* cty for [val constant] is not c.c_cty *)
        | Cany -> c.c_cty
        | Cfun {e_node = Eexec ({c_node = Cany}, cty)} -> cty
        | _ -> raise (CannotInstantiate rs.rs_name) in
      let cty = clone_cty cl (sm_of_cl cl) cty in
      let rs' = Mrs.find rs inst.mi_rs in
      cl_save_rs cl rs rs';
      if List.length cty.cty_args <> List.length rs'.rs_cty.cty_args then
        raise (BadInstance (BadI_rs_arity rs.rs_name));
      let e = try e_exec (c_app rs' cty.cty_args [] cty.cty_result) with
        | exn -> raise (BadInstance (BadI_rs_type (rs.rs_name, exn))) in
      let cexp = c_fun ~mask:cty.cty_mask cty.cty_args cty.cty_pre
        cty.cty_post cty.cty_xpost cty.cty_oldies e in
      let id = id_derive (rs.rs_name.id_string ^ "'refn") rs.rs_name in
      let ld, ss = let_sym id ~ghost:(rs_ghost rs) ~kind:(rs_kind rs) cexp in
      if cty_ghost ss.rs_cty <> cty_ghost rs.rs_cty then
        raise (BadInstance (BadI_rs_ghost rs.rs_name));
      if mask_spill ss.rs_cty.cty_mask rs.rs_cty.cty_mask then
        raise (BadInstance (BadI_rs_mask rs.rs_name));
      let eff = eff_ghostify (rs_ghost rs) cty.cty_effect in
      let eff' = eff_fusion eff ss.rs_cty.cty_effect in
      if not (eff_equal eff eff') then begin
        (* Format.eprintf "@[%a@]@\n" print_cty cty; *)
        (* Format.eprintf "@[%a@]@\n" print_cty ss.rs_cty; *)
        let find_vars regs =
          let reads = Spv.union eff.eff_reads (Spv.of_list cty.cty_args) in
          Mreg.fold (fun r _ svs -> Spv.fold (fun v svs ->
            if ity_r_reachable r v.pv_ity
            then Svs.add v.pv_vs svs else svs) reads svs) regs Svs.empty in
        if not (Spv.equal eff.eff_reads eff'.eff_reads) then
          raise (BadInstance (BadI_rs_reads (rs.rs_name,
            Spv.fold (fun v svs -> Svs.add v.pv_vs svs)
              (Spv.diff eff'.eff_reads eff.eff_reads) Svs.empty)));
        (* extra resets may lead to fewer writes, so we check them first *)
        if not (Sreg.subset eff.eff_covers eff'.eff_covers) then
          raise (BadInstance (BadI_rs_resets (rs.rs_name,
            find_vars (Sreg.diff eff.eff_covers eff'.eff_covers))));
        if not (Mreg.equal Spv.equal eff.eff_writes eff'.eff_writes) then
          raise (BadInstance (BadI_rs_writes (rs.rs_name,
            find_vars (Mreg.filter (fun r fd ->
              try not (Spv.equal fd (Mreg.find r eff.eff_writes))
              with Not_found -> true) eff'.eff_writes))));
        if not (Sreg.equal eff.eff_taints eff'.eff_taints) then
          raise (BadInstance (BadI_rs_taints (rs.rs_name,
            find_vars (Sreg.diff eff'.eff_taints eff.eff_taints))));
        if not (Sreg.equal eff.eff_covers eff'.eff_covers) then
          raise (BadInstance (BadI_rs_covers (rs.rs_name,
            find_vars (Sreg.diff eff'.eff_covers eff.eff_covers))));
        if not (Sreg.equal eff.eff_resets eff'.eff_resets) then
          raise (BadInstance (BadI_rs_resets (rs.rs_name,
            find_vars (Sreg.diff eff'.eff_resets eff.eff_resets))));
        if not (Sxs.equal eff.eff_raises eff'.eff_raises) then
          raise (BadInstance (BadI_rs_raises (rs.rs_name,
            Sxs.fold (fun xs sid -> Sid.add xs.xs_name sid)
              (Sxs.diff eff'.eff_raises eff.eff_raises) Sid.empty)));
        if not (Stv.equal eff.eff_spoils eff'.eff_spoils) then
          raise (BadInstance (BadI_rs_spoils (rs.rs_name,
              Stv.diff eff'.eff_spoils eff.eff_spoils)));
        if eff.eff_oneway <> eff'.eff_oneway then
          raise (BadInstance (BadI_rs_oneway rs.rs_name));
        if eff.eff_ghost <> eff'.eff_ghost then
          raise (BadInstance (BadI_rs_ghost rs.rs_name));
        raise (BadInstance (BadI rs.rs_name));
      end;
      (* FIXME add correspondance for "let lemma" to cl.pr_table *)
      let dl = mk_vc uc (create_let_decl ld) in
      List.fold_left (add_pdecl_raw ~warn:false) uc dl
  | PDlet ld ->
      begin match ld with
        | LDvar ({pv_vs=vs}, _) when Mvs.mem vs inst.mi_pv ->
            raise (CannotInstantiate vs.vs_name)
        | LDrec rdl ->
            let no_inst {rec_sym = rs} =
              if Mrs.mem rs inst.mi_rs then
                raise (CannotInstantiate rs.rs_name) in
            List.iter no_inst rdl
        | _ -> () end;
      let reads = match ld with
        | LDvar (_,e) -> e.e_effect.eff_reads
        | LDsym (_,c) -> cty_reads c.c_cty
        | LDrec rdl -> List.fold_left (fun spv {rec_rsym = s} ->
            Spv.union spv (cty_reads s.rs_cty)) Spv.empty rdl in
      freeze_foreign cl reads;
      let sm, ld = clone_let_defn loc cl (sm_of_cl cl) ld in
      cl.pv_table <- sm.sm_pv; cl.rs_table <- sm.sm_rs;
      add_pdecl ~warn:false ~vc:false uc (create_let_decl ld)
  | PDexn ({xs_name = id} as xs) when Mxs.mem xs inst.mi_xs ->
      let xs' = Mxs.find xs inst.mi_xs in
      begin try let ity = clone_ity cl xs.xs_ity in
                ignore (ity_match isb_empty xs'.xs_ity ity)
        with TypeMismatch _ -> raise (BadInstance (BadI_xs_type id)) end;
      if mask_spill xs'.xs_mask xs.xs_mask then
        raise (BadInstance (BadI_xs_mask id));
      cl.xs_table <- Mxs.add xs xs' cl.xs_table;
      uc
  | PDexn xs ->
      let id = id_clone xs.xs_name in
      let ity = clone_ity cl xs.xs_ity in
      let xs' = create_xsymbol id ~mask:xs.xs_mask ity in
      cl.xs_table <- Mxs.add xs xs' cl.xs_table;
      add_pdecl ~warn:false ~vc:false uc (create_exn_decl xs')
  | PDpure ->
      let uc = List.fold_left (clone_decl inst cl) uc d.pd_pure in
      assert (d.pd_meta = []); (* pure decls do not produce metas *)
      uc

let theory_add_clone = Theory.add_clone_internal ()

let add_clone uc mi =
  let sm = {
    sm_ty = Mts.map ty_of_ity mi.mi_ty;
    sm_ts = Mts.map (fun s -> s.its_ts) mi.mi_ts;
    sm_ls = mi.mi_ls;
    sm_pr = mi.mi_pr } in
  let uc = add_use uc (Sid.empty |>
    Mts.fold (fun _ ts s -> syms_of_ts s ts) sm.sm_ts |>
    Mts.fold (fun _ ty s -> syms_of_ty s ty) sm.sm_ty |>
    Mls.fold (fun _ ls s -> syms_of_ls s ls) sm.sm_ls) in
  { uc with
      muc_theory = theory_add_clone uc.muc_theory mi.mi_mod.mod_theory sm;
      muc_units  = Uclone mi :: uc.muc_units }

let clone_export ?loc uc m inst =
  let cl = cl_init m inst in
  let rec add_unit uc u = match u with
    | Udecl d -> clone_pdecl loc inst cl uc d
    | Uuse m -> use_export uc m
    | Uclone mi ->
        begin try add_clone uc { mi_mod = mi.mi_mod;
          mi_ty = Mts.map (clone_ity cl) mi.mi_ty;
          mi_ts = Mts.map (cl_find_its cl) mi.mi_ts;
          mi_ls = Mls.map (cl_find_ls cl) mi.mi_ls;
          mi_pr = Mpr.map (cl_find_pr cl) mi.mi_pr;
          mi_pk = mi.mi_pk;
          mi_pv = Mvs.map (cl_find_pv cl) mi.mi_pv;
          mi_rs = Mrs.map (cl_find_rs cl) mi.mi_rs;
          mi_xs = Mxs.map (cl_find_xs cl) mi.mi_xs;
          mi_df = mi.mi_df}
        with Not_found -> uc end
    | Umeta (m,al) ->
        begin try add_meta uc m (List.map (function
          | MAty ty -> MAty (clone_ty cl ty)
          | MAts ts -> MAts (cl_find_ts cl ts)
          | MAls ls -> MAls (cl_find_ls cl ls)
          | MApr pr -> MApr (cl_find_pr cl pr)
          | a -> a) al)
        with Not_found -> uc end
    | Uscope (n,ul) ->
        let uc = open_scope uc n in
        let uc = List.fold_left add_unit uc ul in
        close_scope ~import:false uc in
  let uc = List.fold_left add_unit uc m.mod_units in
  let mi = {
    mi_mod = m;
    mi_ty  = cl.ty_table;
    mi_ts  = cl.ts_table;
    mi_ls  = cl.ls_table;
    mi_pr  = cl.pr_table;
    mi_pk  = inst.mi_pk;
    mi_pv  = cl.pv_table;
    mi_rs  = cl.rs_table;
    mi_xs  = cl.xs_table;
    mi_df  = inst.mi_df} in
  add_clone uc mi

(** {2 WhyML language} *)

type mlw_file = pmodule Mstr.t

let convert mm =
  let convert m = m.mod_theory in
  if Mstr.is_empty mm then Mstr.empty else
  match (snd (Mstr.choose mm)).mod_theory.th_path with
  | "why3" :: path ->
      begin try Env.base_language_builtin path
      with Not_found -> Mstr.map convert mm end
  | _ -> Mstr.map convert mm

let mlw_language = Env.register_language Env.base_language convert

module Hpath = Exthtbl.Make(struct
  type t = Env.pathname
  let hash = Hashtbl.hash
  let equal = (=)
end)

let mlw_language_builtin =
  let builtin s =
    if s = unit_module.mod_theory.th_name.id_string then unit_module else
    if s = ref_module.mod_theory.th_name.id_string then ref_module else
    if s = builtin_theory.th_name.id_string then builtin_module else
    if s = highord_theory.th_name.id_string then highord_module else
    if s = bool_theory.th_name.id_string then bool_module else
    match tuple_theory_name s with
    | Some n -> tuple_module n
    | None -> raise Not_found in
  Hpath.memo 7 (function
    | [s] -> Mstr.singleton s (builtin s)
    | _   -> raise Not_found)

let () = Env.add_builtin mlw_language mlw_language_builtin

exception ModuleNotFound of Env.pathname * string

let read_module env path s =
  let path = if path = [] then ["why3"; s] else path in
  let mm = Env.read_library mlw_language env path in
  Mstr.find_exn (ModuleNotFound (path,s)) s mm

(* pretty-printing *)

let print_path fmt sl =
  Pp.print_list (Pp.constant_string ".") Format.pp_print_string fmt sl

let print_mname fmt {mod_theory = th} =
  List.iter (fun s ->
    Format.pp_print_string fmt s;
    Format.pp_print_char fmt '.') th.th_path;
  Format.pp_print_string fmt th.th_name.id_string

let rec print_unit fmt = function
  | Udecl d -> Pdecl.print_pdecl fmt d
  | Uuse m -> Format.fprintf fmt "use export %a" print_mname m
  | Uclone mi -> Format.fprintf fmt "clone export %a with ..."
      print_mname mi.mi_mod
  | Umeta (m,al) -> Format.fprintf fmt "@[<hov 2>meta %s %a@]"
      m.meta_name (Pp.print_list Pp.comma Pretty.print_meta_arg) al
  | Uscope (s,[Uuse m]) -> Format.fprintf fmt "use %a%s" print_mname m
      (if s = m.mod_theory.th_name.id_string then "" else " as " ^ s)
  | Uscope (s,[Uclone mi]) -> Format.fprintf fmt "clone %a%s with ..."
      print_mname mi.mi_mod
      (if s = mi.mi_mod.mod_theory.th_name.id_string then "" else " as " ^ s)
  | Uscope (s,ul) -> Format.fprintf fmt "@[<hov 2>scope %s@\n%a@]@\nend"
      s (Pp.print_list Pp.newline2 print_unit) ul

let print_module fmt m = Format.fprintf fmt
  "@[<hov 2>module %s@\n%a@]@\nend" m.mod_theory.th_name.id_string
  (Pp.print_list Pp.newline2 print_unit) m.mod_units

let () = Exn_printer.register (fun fmt e -> match e with
  | IncompatibleNotation nm -> Format.fprintf fmt
      "Incombatible type signatures for notation '%a'" Ident.print_decoded nm
  | ModuleNotFound (sl,s) -> Format.fprintf fmt
      "Module %s not found in library %a" s print_path sl
  | _ -> raise e)
