open Types
open Values
open Contexts
open Cps
open ValuesUtils
open TypesUtils
open InterpreterUtils
open InterpreterBorrowsCore
open InterpreterProjectors
open Errors

(** The local logger *)
let log = Logging.borrows_log

(** Auxiliary function to end borrows: lookup a borrow in the environment,
    update it (by returning an updated environment where the borrow has been
    replaced by {!Bottom})) if we can end the borrow (for instance, it is not
    an outer borrow...) or return the reason why we couldn't update the borrow.

    [end_borrow_aux] then simply performs a loop: as long as we need to end (outer)
    borrows, we end them, before finally ending the borrow we wanted to end in the
    first place.

    - [allowed_abs]: if not [None], allows to get a borrow in the given
      abstraction without ending the whole abstraction first. This parameter
      allows us to use {!end_borrow_aux} as an auxiliary function for
      {!end_abstraction_aux} (we end all the borrows in the abstraction one by one
      before removing the abstraction from the context).
    - [allow_inner_loans]: if [true], allow to end borrows containing inner
      loans. This is used to merge borrows with abstractions, to compute loop
      fixed points for instance.
*)
let end_borrow_get_borrow (span : Meta.span)
    (allowed_abs : AbstractionId.id option) (allow_inner_loans : bool)
    (l : BorrowId.id) (ctx : eval_ctx) :
    ( eval_ctx * (AbstractionId.id option * g_borrow_content) option,
      priority_borrows_or_abs )
    result =
  (* We use a reference to communicate the kind of borrow we found, if we
   * find one *)
  let replaced_bc : (AbstractionId.id option * g_borrow_content) option ref =
    ref None
  in
  let set_replaced_bc (abs_id : AbstractionId.id option) (bc : g_borrow_content)
      =
    sanity_check __FILE__ __LINE__ (Option.is_none !replaced_bc) span;
    replaced_bc := Some (abs_id, bc)
  in
  (* Raise an exception if:
   * - there are outer borrows
   * - if we are inside an abstraction
   * - there are inner loans
   * this exception is caught in a wrapper function *)
  let raise_if_priority (outer : AbstractionId.id option * borrow_ids option)
      (borrowed_value : typed_value option) =
    (* First, look for outer borrows or abstraction *)
    let outer_abs, outer_borrows = outer in
    (match outer_abs with
    | Some abs -> (
        if
          (* Check if we can end borrows inside this abstraction *)
          Some abs <> allowed_abs
        then raise (FoundPriority (OuterAbs abs))
        else
          match outer_borrows with
          | Some borrows -> raise (FoundPriority (OuterBorrows borrows))
          | None -> ())
    | None -> (
        match outer_borrows with
        | Some borrows -> raise (FoundPriority (OuterBorrows borrows))
        | None -> ()));
    (* Then check if there are inner loans *)
    if not allow_inner_loans then
      match borrowed_value with
      | None -> ()
      | Some v -> (
          match get_first_loan_in_value v with
          | None -> ()
          | Some c -> (
              match c with
              | VSharedLoan (bids, _) ->
                  raise (FoundPriority (InnerLoans (Borrows bids)))
              | VMutLoan bid -> raise (FoundPriority (InnerLoans (Borrow bid))))
          )
  in

  (* The environment is used to keep track of the outer loans *)
  let obj =
    object
      inherit [_] map_eval_ctx as super

      (** We reimplement {!visit_Loan} because we may have to update the
          outer borrows *)
      method! visit_VLoan (outer : AbstractionId.id option * borrow_ids option)
          lc =
        match lc with
        | VMutLoan bid -> VLoan (super#visit_VMutLoan outer bid)
        | VSharedLoan (bids, v) ->
            (* Update the outer borrows before diving into the shared value *)
            let outer = update_outer_borrows outer (Borrows bids) in
            VLoan (super#visit_VSharedLoan outer bids v)

      method! visit_VBorrow outer bc =
        match bc with
        | VSharedBorrow l' | VReservedMutBorrow l' ->
            (* Check if this is the borrow we are looking for *)
            if l = l' then (
              (* Check if there are outer borrows or if we are inside an abstraction *)
              raise_if_priority outer None;
              (* Register the update *)
              set_replaced_bc (fst outer) (Concrete bc);
              (* Update the value *)
              VBottom)
            else super#visit_VBorrow outer bc
        | VMutBorrow (l', bv) ->
            (* Check if this is the borrow we are looking for *)
            if l = l' then (
              (* Check if there are outer borrows or if we are inside an abstraction *)
              raise_if_priority outer (Some bv);
              (* Register the update *)
              set_replaced_bc (fst outer) (Concrete bc);
              (* Update the value *)
              VBottom)
            else
              (* Update the outer borrows before diving into the borrowed value *)
              let outer = update_outer_borrows outer (Borrow l') in
              VBorrow (super#visit_VMutBorrow outer l' bv)

      (** We reimplement {!visit_ALoan} because we may have to update the
          outer borrows *)
      method! visit_ALoan outer lc =
        (* Note that the children avalues are just other, independent loans,
         * so we don't need to update the outer borrows when diving in.
         * We keep track of the parents/children relationship only because we
         * need it to properly instantiate the backward functions when generating
         * the pure translation. *)
        match lc with
        | AMutLoan (pm, _, _) ->
            sanity_check __FILE__ __LINE__ (pm = PNone) span;
            (* Nothing special to do *)
            super#visit_ALoan outer lc
        | ASharedLoan (pm, bids, v, av) ->
            sanity_check __FILE__ __LINE__ (pm = PNone) span;
            (* Explore the shared value - we need to update the outer borrows *)
            let souter = update_outer_borrows outer (Borrows bids) in
            let v = super#visit_typed_value souter v in
            (* Explore the child avalue - we keep the same outer borrows *)
            let av = super#visit_typed_avalue outer av in
            (* Reconstruct *)
            ALoan (ASharedLoan (pm, bids, v, av))
        | AEndedMutLoan { given_back = _; child = _; given_back_span = _ }
        | AEndedSharedLoan _
        (* The loan has ended, so no need to update the outer borrows *)
        | AIgnoredMutLoan _ (* Nothing special to do *)
        | AEndedIgnoredMutLoan
            { given_back = _; child = _; given_back_span = _ }
        (* Nothing special to do *)
        | AIgnoredSharedLoan _ ->
            (* Nothing special to do *)
            super#visit_ALoan outer lc

      method! visit_ABorrow outer bc =
        match bc with
        | AMutBorrow (pm, bid, _) ->
            sanity_check __FILE__ __LINE__ (pm = PNone) span;
            (* Check if this is the borrow we are looking for *)
            if bid = l then (
              (* TODO: treat this case differently. We should not introduce
                 a bottom value. *)
              (* When ending a mut borrow, there are two cases:
               * - in the general case, we have to end the whole abstraction
               *   (and thus raise an exception to signal that to the caller)
               * - in some situations, the associated loan is inside the same
               *   abstraction as the borrow. In this situation, we can end
               *   the borrow without ending the whole abstraction, and we
               *   simply move the child avalue around.
               *)
              (* Check there are outer borrows, or if we need to end the whole
               * abstraction *)
              raise_if_priority outer None;
              (* Register the update *)
              set_replaced_bc (fst outer) (Abstract bc);
              (* Update the value - note that we are necessarily in the second
               * of the two cases described above.
               * Also note that, as we are moving the borrowed value inside the
               * abstraction (and not really giving the value back to the context)
               * we do not insert {!AEndedMutBorrow} but rather {!ABottom} *)
              craise __FILE__ __LINE__ span "Unimplemented"
              (* ABottom *))
            else
              (* Update the outer borrows before diving into the child avalue *)
              let outer = update_outer_borrows outer (Borrow bid) in
              super#visit_ABorrow outer bc
        | ASharedBorrow (pm, bid) ->
            sanity_check __FILE__ __LINE__ (pm = PNone) span;
            (* Check if this is the borrow we are looking for *)
            if bid = l then (
              (* Check there are outer borrows, or if we need to end the whole
               * abstraction *)
              raise_if_priority outer None;
              (* Register the update *)
              set_replaced_bc (fst outer) (Abstract bc);
              (* Update the value - note that we are necessarily in the second
               * of the two cases described above *)
              ABottom)
            else super#visit_ABorrow outer bc
        | AIgnoredMutBorrow (_, _)
        | AEndedMutBorrow _
        | AEndedIgnoredMutBorrow
            { given_back = _; child = _; given_back_span = _ }
        | AEndedSharedBorrow ->
            (* Nothing special to do *)
            super#visit_ABorrow outer bc
        | AProjSharedBorrow asb ->
            (* Check if the borrow we are looking for is in the asb *)
            if borrow_in_asb l asb then (
              (* Check there are outer borrows, or if we need to end the whole
               * abstraction *)
              raise_if_priority outer None;
              (* Register the update *)
              set_replaced_bc (fst outer) (Abstract bc);
              (* Update the value - note that we are necessarily in the second
               * of the two cases described above *)
              let asb = remove_borrow_from_asb span l asb in
              ABorrow (AProjSharedBorrow asb))
            else (* Nothing special to do *)
              super#visit_ABorrow outer bc

      method! visit_abs outer abs =
        (* Update the outer abs *)
        let outer_abs, outer_borrows = outer in
        sanity_check __FILE__ __LINE__ (Option.is_none outer_abs) span;
        sanity_check __FILE__ __LINE__ (Option.is_none outer_borrows) span;
        let outer = (Some abs.abs_id, None) in
        super#visit_abs outer abs
    end
  in
  (* Catch the exceptions - raised if there are outer borrows *)
  try
    let ctx = obj#visit_eval_ctx (None, None) ctx in
    Ok (ctx, !replaced_bc)
  with FoundPriority outers -> Error outers

(** Auxiliary function to end borrows. See {!give_back}.
    
    When we end a mutable borrow, we need to "give back" the value it contained
    to its original owner by reinserting it at the proper position.

    Note that this function checks that there is exactly one loan to which we
    give the value back.
    TODO: this was not the case before, so some sanity checks are not useful anymore.
 *)
let give_back_value (config : config) (span : Meta.span) (bid : BorrowId.id)
    (nv : typed_value) (ctx : eval_ctx) : eval_ctx =
  (* Sanity check *)
  exec_assert __FILE__ __LINE__
    (not (loans_in_value nv))
    span "Can not end a borrow because the value to give back contains bottom";
  exec_assert __FILE__ __LINE__
    (not (bottom_in_value ctx.ended_regions nv))
    span "Can not end a borrow because the value to give back contains bottom";
  (* Debug *)
  log#ldebug
    (lazy
      ("give_back_value:\n- bid: " ^ BorrowId.to_string bid ^ "\n- value: "
      ^ typed_value_to_string ~span:(Some span) ctx nv
      ^ "\n- context:\n"
      ^ eval_ctx_to_string ~span:(Some span) ctx
      ^ "\n"));
  (* We use a reference to check that we updated exactly one loan *)
  let replaced : bool ref = ref false in
  let set_replaced () =
    sanity_check __FILE__ __LINE__ (not !replaced) span;
    replaced := true
  in
  (* Whenever giving back symbolic values, they shouldn't contain already ended regions *)
  let check_symbolic_no_ended = true in
  (* We sometimes need to reborrow values while giving a value back due: prepare that *)
  let allow_reborrows = true in
  let fresh_reborrow, apply_registered_reborrows =
    prepare_reborrows config span allow_reborrows
  in
  (* The visitor to give back the values *)
  let obj =
    object (self)
      inherit [_] map_eval_ctx as super

      (** This is a bit annoying, but as we need the type of the value we
          are exploring, for sanity checks, we need to implement
          {!visit_typed_avalue} instead of
          overriding {!visit_ALoan} *)
      method! visit_typed_value opt_abs (v : typed_value) : typed_value =
        match v.value with
        | VLoan lc ->
            let value = self#visit_typed_Loan opt_abs v.ty lc in
            ({ v with value } : typed_value)
        | _ -> super#visit_typed_value opt_abs v

      method visit_typed_Loan opt_abs ty lc =
        match lc with
        | VSharedLoan (bids, v) ->
            (* We are giving back a value (i.e., the content of a *mutable*
             * borrow): nothing special to do *)
            VLoan (super#visit_VSharedLoan opt_abs bids v)
        | VMutLoan bid' ->
            (* Check if this is the loan we are looking for *)
            if bid' = bid then (
              (* Sanity check *)
              let expected_ty = ty in
              if nv.ty <> expected_ty then
                craise __FILE__ __LINE__ span
                  ("Value given back doesn't have the proper type:\n\
                    - expected: " ^ ty_to_string ctx ty ^ "\n- received: "
                 ^ ty_to_string ctx nv.ty);
              (* Replace *)
              set_replaced ();
              nv.value)
            else VLoan (super#visit_VMutLoan opt_abs bid')

      (** This is a bit annoying, but as we need the type of the avalue we
          are exploring, in order to be able to project the value we give
          back, we need to reimplement {!visit_typed_avalue} instead of
          {!visit_ALoan} *)
      method! visit_typed_avalue opt_abs (av : typed_avalue) : typed_avalue =
        match av.value with
        | ALoan lc ->
            let value = self#visit_typed_ALoan opt_abs av.ty lc in
            ({ av with value } : typed_avalue)
        | _ -> super#visit_typed_avalue opt_abs av

      (** We need to inspect ignored mutable borrows, to insert loan projectors
          if necessary.
       *)
      method! visit_ABorrow (opt_abs : abs option) (bc : aborrow_content)
          : avalue =
        match bc with
        | AIgnoredMutBorrow (bid', child) ->
            if bid' = Some bid then
              (* Insert a loans projector - note that if this case happens,
               * it is necessarily because we ended a parent abstraction,
               * and the given back value is thus a symbolic value *)
              match nv.value with
              | VSymbolic sv ->
                  let abs = Option.get opt_abs in
                  (* Remember the given back value as a meta-value
                   * TODO: it is a bit annoying to have to deconstruct
                   * the value... Think about a more elegant way. *)
                  let given_back_span = as_symbolic span nv.value in
                  (* The loan projector *)
                  let given_back =
                    mk_aproj_loans_value_from_symbolic_value abs.regions sv
                  in
                  (* Continue giving back in the child value *)
                  let child = super#visit_typed_avalue opt_abs child in
                  (* Return *)
                  ABorrow
                    (AEndedIgnoredMutBorrow
                       { given_back; child; given_back_span })
              | _ -> craise __FILE__ __LINE__ span "Unreachable"
            else
              (* Continue exploring *)
              ABorrow (super#visit_AIgnoredMutBorrow opt_abs bid' child)
        | _ ->
            (* Continue exploring *)
            super#visit_ABorrow opt_abs bc

      (** We are not specializing an already existing method, but adding a
          new method (for projections, we need type information) *)
      method visit_typed_ALoan (opt_abs : abs option) (ty : rty)
          (lc : aloan_content) : avalue =
        (* Preparing a bit *)
        let regions, ancestors_regions =
          match opt_abs with
          | None -> craise __FILE__ __LINE__ span "Unreachable"
          | Some abs -> (abs.regions, abs.ancestors_regions)
        in
        (* Rk.: there is a small issue with the types of the aloan values.
         * See the comment at the level of definition of {!typed_avalue} *)
        let borrowed_value_aty =
          let _, ty, _ = ty_get_ref ty in
          ty
        in
        match lc with
        | AMutLoan (pm, bid', child) ->
            sanity_check __FILE__ __LINE__ (pm = PNone) span;
            if bid' = bid then (
              (* This is the loan we are looking for: apply the projection to
               * the value we give back and replaced this mutable loan with
               * an ended loan *)
              (* Register the insertion *)
              set_replaced ();
              (* Remember the given back value as a meta-value *)
              let given_back_span = nv in
              (* Apply the projection *)
              let given_back =
                apply_proj_borrows span check_symbolic_no_ended ctx
                  fresh_reborrow regions ancestors_regions nv borrowed_value_aty
              in
              (* Continue giving back in the child value *)
              let child = super#visit_typed_avalue opt_abs child in
              (* Return the new value *)
              ALoan (AEndedMutLoan { child; given_back; given_back_span }))
            else (* Continue exploring *)
              super#visit_ALoan opt_abs lc
        | ASharedLoan (pm, _, _, _) ->
            sanity_check __FILE__ __LINE__ (pm = PNone) span;
            (* We are giving back a value to a *mutable* loan: nothing special to do *)
            super#visit_ALoan opt_abs lc
        | AEndedMutLoan { child = _; given_back = _; given_back_span = _ }
        | AEndedSharedLoan (_, _) ->
            (* Nothing special to do *)
            super#visit_ALoan opt_abs lc
        | AIgnoredMutLoan (opt_bid, child) ->
            (* This loan is ignored, but we may have to project on a subvalue
             * of the value which is given back *)
            if opt_bid = Some bid then
              (* Remember the given back value as a meta-value *)
              let given_back_span = nv in
              (* Note that we replace the ignored mut loan by an *ended* ignored
               * mut loan. Also, this is not the loan we are looking for *per se*:
               * we don't register the fact that we inserted the value somewhere
               * (i.e., we don't call {!set_replaced}) *)
              let given_back =
                apply_proj_borrows span check_symbolic_no_ended ctx
                  fresh_reborrow regions ancestors_regions nv borrowed_value_aty
              in
              (* Continue giving back in the child value *)
              let child = super#visit_typed_avalue opt_abs child in
              ALoan
                (AEndedIgnoredMutLoan { given_back; child; given_back_span })
            else super#visit_ALoan opt_abs lc
        | AEndedIgnoredMutLoan
            { given_back = _; child = _; given_back_span = _ }
        | AIgnoredSharedLoan _ ->
            (* Nothing special to do *)
            super#visit_ALoan opt_abs lc

      method! visit_EAbs opt_abs abs =
        (* We remember in which abstraction we are before diving -
         * this is necessary for projecting values: we need to know
         * over which regions to project *)
        sanity_check __FILE__ __LINE__ (Option.is_none opt_abs) span;
        super#visit_EAbs (Some abs) abs
    end
  in

  (* Explore the environment *)
  let ctx = obj#visit_eval_ctx None ctx in
  (* Check we gave back to exactly one loan *)
  cassert __FILE__ __LINE__ !replaced span "No loan updated";
  (* Apply the reborrows *)
  apply_registered_reborrows ctx

(** Give back a *modified* symbolic value. *)
let give_back_symbolic_value (_config : config) (span : Meta.span)
    (proj_regions : RegionId.Set.t) (proj_ty : rty) (sv : symbolic_value)
    (nsv : symbolic_value) (ctx : eval_ctx) : eval_ctx =
  (* Sanity checks *)
  sanity_check __FILE__ __LINE__
    (sv.sv_id <> nsv.sv_id && ty_is_rty proj_ty)
    span;
  (* Store the given-back value as a meta-value for synthesis purposes *)
  let mv = nsv in
  (* Substitution function, to replace the borrow projectors over symbolic values *)
  let subst (_abs : abs) local_given_back =
    (* See the below comments: there is something wrong here *)
    let _ = raise Utils.Unimplemented in
    (* Compute the projection over the given back value *)
    let child_proj =
      (* TODO: there is something wrong here.
         Consider this:
         {[
           abs0 {'a} { AProjLoans (s0 : &'a mut T) [] }
           abs1 {'b} { AProjBorrows (s0 : &'a mut T <: &'b mut T) }
         ]}

         Upon ending abs1, we give back some fresh symbolic value [s1],
         that we reinsert where the loan for [s0] is. However, the mutable
         borrow in the type [&'a mut T] was ended: we give back a value of
         type [T]! We thus *mustn't* introduce a projector here.
      *)
      (* AProjBorrows (nsv, sv.sv_ty) *)
      internal_error __FILE__ __LINE__ span
    in
    AProjLoans (sv, (mv, child_proj) :: local_given_back)
  in
  update_intersecting_aproj_loans span proj_regions proj_ty sv subst ctx

(** Auxiliary function to end borrows. See {!give_back}.

    This function is similar to {!give_back_value} but gives back an {!avalue}
    (coming from an abstraction).

    It is used when ending a borrow inside an abstraction, when the corresponding
    loan is inside the same abstraction (in which case we don't need to end the whole
    abstraction).
    
    REMARK: this function can't be used to give back the values borrowed by
    end abstraction when ending this abstraction. When doing this, we need
    to convert the {!avalue} to a {!type:value} by introducing the proper symbolic values.
 *)
let give_back_avalue_to_same_abstraction (_config : config) (span : Meta.span)
    (bid : BorrowId.id) (nv : typed_avalue) (nsv : typed_value) (ctx : eval_ctx)
    : eval_ctx =
  (* We use a reference to check that we updated exactly one loan *)
  let replaced : bool ref = ref false in
  let set_replaced () =
    cassert __FILE__ __LINE__ (not !replaced) span
      "Exacly one loan should be updated";
    replaced := true
  in
  let obj =
    object (self)
      inherit [_] map_eval_ctx as super

      (** This is a bit annoying, but as we need the type of the avalue we
          are exploring, in order to be able to project the value we give
          back, we need to reimplement {!visit_typed_avalue} instead of
          {!visit_ALoan}.

          TODO: it is possible to do this by remembering the type of the last
          typed avalue we entered.
       *)
      method! visit_typed_avalue opt_abs (av : typed_avalue) : typed_avalue =
        match av.value with
        | ALoan lc ->
            let value = self#visit_typed_ALoan opt_abs av.ty lc in
            ({ av with value } : typed_avalue)
        | _ -> super#visit_typed_avalue opt_abs av

      (** We are not specializing an already existing method, but adding a
          new method (for projections, we need type information).

          TODO: it is possible to do this by remembering the type of the last
          typed avalue we entered.
        *)
      method visit_typed_ALoan (opt_abs : abs option) (ty : rty)
          (lc : aloan_content) : avalue =
        match lc with
        | AMutLoan (pm, bid', child) ->
            sanity_check __FILE__ __LINE__ (pm = PNone) span;
            if bid' = bid then (
              (* Sanity check - about why we need to call {!ty_get_ref}
               * (and don't do the same thing as in {!give_back_value})
               * see the comment at the level of the definition of
               * {!typed_avalue} *)
              let _, expected_ty, _ = ty_get_ref ty in
              if nv.ty <> expected_ty then
                craise __FILE__ __LINE__ span
                  ("Value given back doesn't have the proper type:\n\
                    - expected: " ^ ty_to_string ctx ty ^ "\n- received: "
                 ^ ty_to_string ctx nv.ty);
              (* This is the loan we are looking for: apply the projection to
               * the value we give back and replaced this mutable loan with
               * an ended loan *)
              (* Register the insertion *)
              set_replaced ();
              (* Return the new value *)
              ALoan
                (AEndedMutLoan { given_back = nv; child; given_back_span = nsv }))
            else (* Continue exploring *)
              super#visit_ALoan opt_abs lc
        | ASharedLoan (PNone, _, _, _)
        (* We are giving back a value to a *mutable* loan: nothing special to do *)
        | AEndedMutLoan { given_back = _; child = _; given_back_span = _ }
        | AEndedSharedLoan (_, _) ->
            (* Nothing special to do *)
            super#visit_ALoan opt_abs lc
        | ASharedLoan (_, _, _, _) ->
            (* We get there if the projection marker is not [PNone] *)
            internal_error __FILE__ __LINE__ span
        | AIgnoredMutLoan (bid_opt, child) ->
            (* This loan is ignored, but we may have to project on a subvalue
             * of the value which is given back *)
            if bid_opt = Some bid then (
              (* Note that we replace the ignored mut loan by an *ended* ignored
               * mut loan. Also, this is not the loan we are looking for *per se*:
               * we don't register the fact that we inserted the value somewhere
               * (i.e., we don't call {!set_replaced}) *)
              (* Sanity check *)
              sanity_check __FILE__ __LINE__ (nv.ty = ty) span;
              ALoan
                (AEndedIgnoredMutLoan
                   { given_back = nv; child; given_back_span = nsv }))
            else super#visit_ALoan opt_abs lc
        | AEndedIgnoredMutLoan
            { given_back = _; child = _; given_back_span = _ }
        | AIgnoredSharedLoan _ ->
            (* Nothing special to do *)
            super#visit_ALoan opt_abs lc
    end
  in

  (* Explore the environment *)
  let ctx = obj#visit_eval_ctx None ctx in
  (* Check we gave back to exactly one loan *)
  cassert __FILE__ __LINE__ !replaced span "No loan updated";
  (* Return *)
  ctx

(** Auxiliary function to end borrows. See  {!give_back}.
    
    When we end a shared borrow, we need to remove the borrow id from the list
    of borrows to the shared value.

    Note that this function checks that there is exactly one shared loan that
    we update.
    TODO: this was not the case before, so some sanity checks are not useful anymore.
 *)
let give_back_shared _config (span : Meta.span) (bid : BorrowId.id)
    (ctx : eval_ctx) : eval_ctx =
  (* We use a reference to check that we updated exactly one loan *)
  let replaced : bool ref = ref false in
  let set_replaced () =
    cassert __FILE__ __LINE__ (not !replaced) span
      "Exactly one loan should be updated";
    replaced := true
  in
  let obj =
    object
      inherit [_] map_eval_ctx as super

      method! visit_VLoan opt_abs lc =
        match lc with
        | VSharedLoan (bids, shared_value) ->
            if BorrowId.Set.mem bid bids then (
              (* This is the loan we are looking for *)
              set_replaced ();
              (* If there remains exactly one borrow identifier, we need
               * to end the loan. Otherwise, we just remove the current
               * loan identifier *)
              if BorrowId.Set.cardinal bids = 1 then shared_value.value
              else
                VLoan (VSharedLoan (BorrowId.Set.remove bid bids, shared_value)))
            else
              (* Not the loan we are looking for: continue exploring *)
              VLoan (super#visit_VSharedLoan opt_abs bids shared_value)
        | VMutLoan bid' ->
            (* We are giving back a *shared* borrow: nothing special to do *)
            VLoan (super#visit_VMutLoan opt_abs bid')

      method! visit_ALoan opt_abs lc =
        match lc with
        | AMutLoan (pm, bid, av) ->
            sanity_check __FILE__ __LINE__ (pm = PNone) span;
            (* Nothing special to do (we are giving back a *shared* borrow) *)
            ALoan (super#visit_AMutLoan opt_abs pm bid av)
        | ASharedLoan (pm, bids, shared_value, child) ->
            sanity_check __FILE__ __LINE__ (pm = PNone) span;
            if BorrowId.Set.mem bid bids then (
              (* This is the loan we are looking for *)
              set_replaced ();
              (* If there remains exactly one borrow identifier, we need
               * to end the loan. Otherwise, we just remove the current
               * loan identifier *)
              if BorrowId.Set.cardinal bids = 1 then
                ALoan (AEndedSharedLoan (shared_value, child))
              else
                ALoan
                  (ASharedLoan
                     (pm, BorrowId.Set.remove bid bids, shared_value, child)))
            else
              (* Not the loan we are looking for: continue exploring *)
              super#visit_ALoan opt_abs lc
        | AEndedMutLoan { given_back = _; child = _; given_back_span = _ }
        (* Nothing special to do (the loan has ended) *)
        | AEndedSharedLoan (_, _)
        (* Nothing special to do (the loan has ended) *)
        | AIgnoredMutLoan (_, _)
        (* Nothing special to do (we are giving back a *shared* borrow) *)
        | AEndedIgnoredMutLoan
            { given_back = _; child = _; given_back_span = _ }
        (* Nothing special to do *)
        | AIgnoredSharedLoan _ ->
            (* Nothing special to do *)
            super#visit_ALoan opt_abs lc
    end
  in

  (* Explore the environment *)
  let ctx = obj#visit_eval_ctx None ctx in
  (* Check we gave back to exactly one loan *)
  cassert __FILE__ __LINE__ !replaced span "No loan updated";
  (* Return *)
  ctx

(** When copying values, we duplicate the shared borrows. This is tantamount
    to reborrowing the shared value. The following function applies this change
    to an environment by inserting a new borrow id in the set of borrows tracked
    by a shared value, referenced by the [original_bid] argument.
 *)
let reborrow_shared (span : Meta.span) (original_bid : BorrowId.id)
    (new_bid : BorrowId.id) (ctx : eval_ctx) : eval_ctx =
  (* Keep track of changes *)
  let r = ref false in
  let set_ref () =
    sanity_check __FILE__ __LINE__ (not !r) span;
    r := true
  in

  let obj =
    object
      inherit [_] map_env as super

      method! visit_VSharedLoan env bids sv =
        (* Shared loan: check if the borrow id we are looking for is in the
           set of borrow ids. If yes, insert the new borrow id, otherwise
           explore inside the shared value *)
        if BorrowId.Set.mem original_bid bids then (
          set_ref ();
          let bids' = BorrowId.Set.add new_bid bids in
          VSharedLoan (bids', sv))
        else super#visit_VSharedLoan env bids sv

      method! visit_ASharedLoan env pm bids v av =
        sanity_check __FILE__ __LINE__ (pm = PNone) span;
        (* This case is similar to the {!SharedLoan} case *)
        if BorrowId.Set.mem original_bid bids then (
          set_ref ();
          let bids' = BorrowId.Set.add new_bid bids in
          ASharedLoan (pm, bids', v, av))
        else super#visit_ASharedLoan env pm bids v av
    end
  in

  let env = obj#visit_env () ctx.env in
  (* Check that we reborrowed once *)
  sanity_check __FILE__ __LINE__ !r span;
  { ctx with env }

(** Convert an {!type:avalue} to a {!type:value}.

    This function is used when ending abstractions: whenever we end a borrow
    in an abstraction, we convert the borrowed {!avalue} to a fresh symbolic
    {!type:value}, then give back this {!type:value} to the context.

    Note that some regions may have ended in the symbolic value we generate.
    For instance, consider the following function signature:
    {[
      fn f<'a>(x : &'a mut &'a mut u32);
    ]}
    When ending the abstraction, the value given back for the outer borrow
    should be ⊥. In practice, we will give back a symbolic value which can't
    be expanded (because expanding this symbolic value would require expanding
    a reference whose region has already ended).
 *)
let convert_avalue_to_given_back_value (span : Meta.span) (av : typed_avalue) :
    symbolic_value =
  mk_fresh_symbolic_value span av.ty

(** Auxiliary function: see {!end_borrow_aux}.

    When we end a mutable borrow, we need to "give back" the value it contained
    to its original owner by reinserting it at the proper position.

    Rem.: this function is used when we end *one single* borrow (we don't
    end this borrow as member of the group of borrows belonging to an
    abstraction).
    If the borrow is an "abstract" borrow, it means we are ending a borrow
    inside an abstraction (we end a borrow whose corresponding loan is in
    the same abstraction - we are allowed to do so without ending the whole
    abstraction).
    TODO: we should not treat this case here, and should only consider internal
    borrows. This kind of internal reshuffling. should be similar to ending
    abstractions (it is tantamount to ending *sub*-abstractions).
 *)
let give_back (config : config) (span : Meta.span) (l : BorrowId.id)
    (bc : g_borrow_content) (ctx : eval_ctx) : eval_ctx =
  (* Debug *)
  log#ldebug
    (lazy
      (let bc =
         match bc with
         | Concrete bc -> borrow_content_to_string ~span:(Some span) ctx bc
         | Abstract bc -> aborrow_content_to_string ~span:(Some span) ctx bc
       in
       "give_back:\n- bid: " ^ BorrowId.to_string l ^ "\n- content: " ^ bc
       ^ "\n- context:\n"
       ^ eval_ctx_to_string ~span:(Some span) ctx
       ^ "\n"));
  (* This is used for sanity checks *)
  let sanity_ek =
    { enter_shared_loans = true; enter_mut_borrows = true; enter_abs = true }
  in
  match bc with
  | Concrete (VMutBorrow (l', tv)) ->
      (* Sanity check *)
      sanity_check __FILE__ __LINE__ (l' = l) span;
      sanity_check __FILE__ __LINE__ (not (loans_in_value tv)) span;
      (* Check that the corresponding loan is somewhere - purely a sanity check *)
      sanity_check __FILE__ __LINE__
        (Option.is_some (lookup_loan_opt span sanity_ek l ctx))
        span;
      (* Update the context *)
      give_back_value config span l tv ctx
  | Concrete (VSharedBorrow l' | VReservedMutBorrow l') ->
      (* Sanity check *)
      sanity_check __FILE__ __LINE__ (l' = l) span;
      (* Check that the borrow is somewhere - purely a sanity check *)
      sanity_check __FILE__ __LINE__
        (Option.is_some (lookup_loan_opt span sanity_ek l ctx))
        span;
      (* Update the context *)
      give_back_shared config span l ctx
  | Abstract (AMutBorrow (pm, l', av)) ->
      (* Sanity check *)
      sanity_check __FILE__ __LINE__ (pm = PNone) span;
      sanity_check __FILE__ __LINE__ (l' = l) span;
      (* Check that the corresponding loan is somewhere - purely a sanity check *)
      sanity_check __FILE__ __LINE__
        (Option.is_some (lookup_loan_opt span sanity_ek l ctx))
        span;
      (* Convert the avalue to a (fresh symbolic) value.

         Rem.: we shouldn't do this here. We should do this in a function
         which takes care of ending *sub*-abstractions.
      *)
      let sv = convert_avalue_to_given_back_value span av in
      (* Update the context *)
      give_back_avalue_to_same_abstraction config span l av
        (mk_typed_value_from_symbolic_value sv)
        ctx
  | Abstract (ASharedBorrow (pm, l')) ->
      (* Sanity check *)
      sanity_check __FILE__ __LINE__ (pm = PNone) span;
      sanity_check __FILE__ __LINE__ (l' = l) span;
      (* Check that the borrow is somewhere - purely a sanity check *)
      sanity_check __FILE__ __LINE__
        (Option.is_some (lookup_loan_opt span sanity_ek l ctx))
        span;
      (* Update the context *)
      give_back_shared config span l ctx
  | Abstract (AProjSharedBorrow asb) ->
      (* Sanity check *)
      sanity_check __FILE__ __LINE__ (borrow_in_asb l asb) span;
      (* Update the context *)
      give_back_shared config span l ctx
  | Abstract
      ( AEndedMutBorrow _ | AIgnoredMutBorrow _ | AEndedIgnoredMutBorrow _
      | AEndedSharedBorrow ) ->
      craise __FILE__ __LINE__ span "Unreachable"

let check_borrow_disappeared (span : Meta.span) (fun_name : string)
    (l : BorrowId.id) (ctx0 : eval_ctx) (ctx : eval_ctx) : unit =
  (match lookup_borrow_opt span ek_all l ctx with
  | None -> () (* Ok *)
  | Some _ ->
      log#ltrace
        (lazy
          (fun_name ^ ": " ^ BorrowId.to_string l
         ^ ": borrow didn't disappear:\n- original context:\n"
          ^ eval_ctx_to_string ~span:(Some span) ctx0
          ^ "\n\n- new context:\n"
          ^ eval_ctx_to_string ~span:(Some span) ctx));
      internal_error __FILE__ __LINE__ span);
  match lookup_loan_opt span ek_all l ctx with
  | None -> () (* Ok *)
  | Some _ ->
      log#ltrace
        (lazy
          (fun_name ^ ": " ^ BorrowId.to_string l
         ^ ": loan didn't disappear:\n- original context:\n"
          ^ eval_ctx_to_string ~span:(Some span) ctx0
          ^ "\n\n- new context:\n"
          ^ eval_ctx_to_string ~span:(Some span) ctx));
      internal_error __FILE__ __LINE__ span

(** End a borrow identified by its borrow id in a context.

    This function **preserves invariants** provided [allowed_abs] is [None]: if the
    borrow is inside another borrow/an abstraction, we end the outer borrow/abstraction
    first, etc.

    [allowed_abs]: see the comment for {!end_borrow_get_borrow}.

    [chain]: contains the list of borrows/abstraction ids on which {!end_borrow_aux}
    and {!end_abstraction_aux} were called, to remember the chain of calls. This is
    useful for debugging purposes, and also for sanity checks to detect cycles
    (which shouldn't happen if the environment is well-formed).

    Rk.: from now onwards, the functions are written in continuation passing style.
    The reason is that when ending borrows we may end abstractions, which requires
    generating code for the translation (and we do this in CPS).
    
    TODO: we should split this function in two: one function which doesn't
    perform anything smart and is trusted, and another function for the
    book-keeping.
 *)
let rec end_borrow_aux (config : config) (span : Meta.span)
    (chain : borrow_or_abs_ids) (allowed_abs : AbstractionId.id option)
    (l : BorrowId.id) : cm_fun =
 fun ctx ->
  (* Check that we don't loop *)
  let chain0 = chain in
  let chain =
    add_borrow_or_abs_id_to_chain span "end_borrow_aux: " (BorrowId l) chain
  in
  log#ldebug
    (lazy
      ("end borrow: " ^ BorrowId.to_string l ^ ":\n- original context:\n"
      ^ eval_ctx_to_string ~span:(Some span) ctx));

  (* Utility function for the sanity checks: check that the borrow disappeared
   * from the context *)
  let ctx0 = ctx in
  let check = check_borrow_disappeared span "end borrow" l ctx0 in
  (* Start by ending the borrow itself (we lookup it up and replace it with [Bottom] *)
  let allow_inner_loans = false in
  match end_borrow_get_borrow span allowed_abs allow_inner_loans l ctx with
  (* Two cases:
     - error: we found outer borrows (the borrow is inside a borrowed value) or
       inner loans (the borrow contains loans)
     - success: we didn't find outer borrows when updating (but maybe we actually
       didn't find the borrow we were looking for...). The borrow was successfully
       replaced with [Bottom], and we can proceed to ending the corresponding loan.

     Note that if [allowed_abs] is [Some abs_id] and the borrow is inside the
     abstraction identified by [abs_id], the abstraction is ignored (i.e.:
     {!end_borrow_get_borrow} won't return [Error] because of the abstraction
     itself).
  *)
  | Error priority -> (
      (* Debug *)
      log#ldebug
        (lazy
          ("end borrow: " ^ BorrowId.to_string l
         ^ ": found outer borrows/abs or inner loans:"
          ^ show_priority_borrows_or_abs priority));
      (* End the priority borrows, abstractions, then try again to end the target
       * borrow (if necessary) *)
      match priority with
      | OuterBorrows (Borrows bids) | InnerLoans (Borrows bids) ->
          (* Note that we might get there with [allowed_abs <> None]: we might
           * be trying to end a borrow inside an abstraction, but which is actually
           * inside another borrow *)
          let allowed_abs' = None in
          (* End the outer borrows *)
          let ctx, cc =
            end_borrows_aux config span chain allowed_abs' bids ctx
          in
          (* Retry to end the borrow *)
          let ctx, cc =
            comp cc (end_borrow_aux config span chain0 allowed_abs l ctx)
          in
          (* Check and continue *)
          check ctx;
          (ctx, cc)
      | OuterBorrows (Borrow bid) | InnerLoans (Borrow bid) ->
          let allowed_abs' = None in
          (* End the outer borrow *)
          let ctx, cc = end_borrow_aux config span chain allowed_abs' bid ctx in
          (* Retry to end the borrow *)
          let ctx, cc =
            comp cc (end_borrow_aux config span chain0 allowed_abs l ctx)
          in
          (* Check and continue *)
          check ctx;
          (ctx, cc)
      | OuterAbs abs_id ->
          (* The borrow is inside an abstraction: end the whole abstraction *)
          let ctx, end_abs = end_abstraction_aux config span chain abs_id ctx in
          (* Sanity check *)
          check ctx;
          (ctx, end_abs))
  | Ok (ctx, None) ->
      log#ldebug (lazy "End borrow: borrow not found");
      (* It is possible that we can't find a borrow in symbolic mode (ending
       * an abstraction may end several borrows at once *)
      sanity_check __FILE__ __LINE__ (config.mode = SymbolicMode) span;
      (* Do a sanity check and continue *)
      check ctx;
      (ctx, fun e -> e)
  (* We found a borrow and replaced it with [Bottom]: give it back (i.e., update
     the corresponding loan) *)
  | Ok (ctx, Some (_, bc)) ->
      (* Sanity check: the borrowed value shouldn't contain loans *)
      (match bc with
      | Concrete (VMutBorrow (_, bv)) ->
          sanity_check __FILE__ __LINE__
            (Option.is_none (get_first_loan_in_value bv))
            span
      | _ -> ());
      (* Give back the value *)
      let ctx = give_back config span l bc ctx in
      (* Do a sanity check and continue *)
      check ctx;
      (* Save a snapshot of the environment for the name generation *)
      let cc = SynthesizeSymbolic.save_snapshot ctx in
      (* Compose *)
      (ctx, cc)

and end_borrows_aux (config : config) (span : Meta.span)
    (chain : borrow_or_abs_ids) (allowed_abs : AbstractionId.id option)
    (lset : BorrowId.Set.t) : cm_fun =
 fun ctx ->
  (* This is not necessary, but we prefer to reorder the borrow ids,
     so that we actually end from the smallest id to the highest id - just
     a matter of taste, and may make debugging easier *)
  let ids = BorrowId.Set.fold (fun id ids -> id :: ids) lset [] in
  fold_left_apply_continuation
    (fun id ctx -> end_borrow_aux config span chain allowed_abs id ctx)
    ids ctx

and end_abstraction_aux (config : config) (span : Meta.span)
    (chain : borrow_or_abs_ids) (abs_id : AbstractionId.id) : cm_fun =
 fun ctx ->
  (* Check that we don't loop *)
  let chain =
    add_borrow_or_abs_id_to_chain span "end_abstraction_aux: " (AbsId abs_id)
      chain
  in
  (* Remember the original context for printing purposes *)
  let ctx0 = ctx in
  log#ldebug
    (lazy
      ("end_abstraction_aux: "
      ^ AbstractionId.to_string abs_id
      ^ "\n- original context:\n"
      ^ eval_ctx_to_string ~span:(Some span) ctx0));

  (* Lookup the abstraction - note that if we end a list of abstractions,
     ending one abstraction may lead to the current abstraction having
     preemptively been ended, so the abstraction might not be in the context
     anymore. *)
  match ctx_lookup_abs_opt ctx abs_id with
  | None ->
      log#ldebug
        (lazy
          ("abs not found (already ended): "
          ^ AbstractionId.to_string abs_id
          ^ "\n"));
      (ctx, fun e -> e)
  | Some abs ->
      (* Check that we can end the abstraction *)
      if abs.can_end then ()
      else
        craise __FILE__ __LINE__ span
          ("Can't end abstraction "
          ^ AbstractionId.to_string abs.abs_id
          ^ " as it is set as non-endable");

      (* End the parent abstractions first *)
      let ctx, cc = end_abstractions_aux config span chain abs.parents ctx in
      log#ldebug
        (lazy
          ("end_abstraction_aux: "
          ^ AbstractionId.to_string abs_id
          ^ "\n- context after parent abstractions ended:\n"
          ^ eval_ctx_to_string ~span:(Some span) ctx));

      (* End the loans inside the abstraction *)
      let ctx, cc =
        comp cc (end_abstraction_loans config span chain abs_id ctx)
      in
      log#ldebug
        (lazy
          ("end_abstraction_aux: "
          ^ AbstractionId.to_string abs_id
          ^ "\n- context after loans ended:\n"
          ^ eval_ctx_to_string ~span:(Some span) ctx));

      (* End the abstraction itself by redistributing the borrows it contains *)
      let ctx, cc =
        comp cc (end_abstraction_borrows config span chain abs_id ctx)
      in

      (* End the regions owned by the abstraction - note that we don't need to
         relookup the abstraction: the set of regions in an abstraction never
         changes... *)
      let ctx =
        let ended_regions = RegionId.Set.union ctx.ended_regions abs.regions in
        { ctx with ended_regions }
      in

      (* Remove all the references to the id of the current abstraction, and remove
         the abstraction itself.
         **Rk.**: this is where we synthesize the updated symbolic AST *)
      let ctx, cc =
        comp cc (end_abstraction_remove_from_context config span abs_id ctx)
      in

      (* Debugging *)
      log#ldebug
        (lazy
          ("end_abstraction_aux: "
          ^ AbstractionId.to_string abs_id
          ^ "\n- original context:\n"
          ^ eval_ctx_to_string ~span:(Some span) ctx0
          ^ "\n\n- new context:\n"
          ^ eval_ctx_to_string ~span:(Some span) ctx));

      (* Sanity check: ending an abstraction must preserve the invariants *)
      Invariants.check_invariants span ctx;

      (* Save a snapshot of the environment for the name generation *)
      let cc = cc_comp cc (SynthesizeSymbolic.save_snapshot ctx) in

      (* Return *)
      (ctx, cc)

and end_abstractions_aux (config : config) (span : Meta.span)
    (chain : borrow_or_abs_ids) (abs_ids : AbstractionId.Set.t) : cm_fun =
 fun ctx ->
  (* This is not necessary, but we prefer to reorder the abstraction ids,
   * so that we actually end from the smallest id to the highest id - just
   * a matter of taste, and may make debugging easier *)
  let abs_ids = AbstractionId.Set.fold (fun id ids -> id :: ids) abs_ids [] in
  fold_left_apply_continuation
    (fun id ctx -> end_abstraction_aux config span chain id ctx)
    abs_ids ctx

and end_abstraction_loans (config : config) (span : Meta.span)
    (chain : borrow_or_abs_ids) (abs_id : AbstractionId.id) : cm_fun =
 fun ctx ->
  (* Lookup the abstraction *)
  let abs = ctx_lookup_abs ctx abs_id in
  (* End the first loan we find.
   *
   * We ignore the "ignored mut/shared loans": as we should have already ended
   * the parent abstractions, they necessarily come from children. *)
  let opt_loan = get_first_non_ignored_aloan_in_abstraction span abs in
  match opt_loan with
  | None ->
      (* No loans: nothing to update *)
      (ctx, fun e -> e)
  | Some (BorrowIds bids) ->
      (* There are loans: end the corresponding borrows, then recheck *)
      let ctx, cc =
        match bids with
        | Borrow bid -> end_borrow_aux config span chain None bid ctx
        | Borrows bids -> end_borrows_aux config span chain None bids ctx
      in
      (* Reexplore, looking for loans *)
      comp cc (end_abstraction_loans config span chain abs_id ctx)
  | Some (SymbolicValue sv) ->
      (* There is a proj_loans over a symbolic value: end the proj_borrows
         which intersect this proj_loans, then end the proj_loans itself *)
      let ctx, cc =
        end_proj_loans_symbolic config span chain abs_id abs.regions sv ctx
      in
      (* Reexplore, looking for loans *)
      comp cc (end_abstraction_loans config span chain abs_id ctx)

and end_abstraction_borrows (config : config) (span : Meta.span)
    (chain : borrow_or_abs_ids) (abs_id : AbstractionId.id) : cm_fun =
 fun ctx ->
  log#ldebug
    (lazy
      ("end_abstraction_borrows: abs_id: " ^ AbstractionId.to_string abs_id));
  (* Note that the abstraction mustn't contain any loans *)
  (* We end the borrows, starting with the *inner* ones. This is important
     when considering nested borrows which have the same lifetime.
     TODO: is that really important? Initially, there was a concern about
     whether we should give back ⊥ or not, but everything is handled by
     the symbolic value expansion... Also, now we use the AEndedMutBorrow
     values to store the children avalues (which was not the case before - we
     initially replaced the ended mut borrows with ⊥).
  *)
  (* We explore in-depth and use exceptions. When exploring a borrow, if
   * the exploration didn't trigger an exception, it means there are no
   * inner borrows to end: we can thus trigger an exception for the current
   * borrow.
   *
   * TODO: there should be a function in InterpreterBorrowsCore which does just
   * that.
   *)
  let obj =
    object
      inherit [_] iter_abs as super

      method! visit_aborrow_content env bc =
        (* In-depth exploration *)
        super#visit_aborrow_content env bc;
        (* No exception was raise: we can raise an exception for the
         * current borrow *)
        match bc with
        | AMutBorrow _ | ASharedBorrow _ ->
            (* Raise an exception *)
            raise (FoundABorrowContent bc)
        | AProjSharedBorrow asb ->
            (* Raise an exception only if the asb contains borrows *)
            if
              List.exists
                (fun x -> match x with AsbBorrow _ -> true | _ -> false)
                asb
            then raise (FoundABorrowContent bc)
            else ()
        | AEndedMutBorrow _ | AIgnoredMutBorrow _ | AEndedIgnoredMutBorrow _
        | AEndedSharedBorrow ->
            (* Nothing to do for ignored borrows *)
            ()

      method! visit_aproj env sproj =
        (match sproj with
        | AProjLoans _ -> craise __FILE__ __LINE__ span "Unexpected"
        | AProjBorrows (sv, proj_ty) -> raise (FoundAProjBorrows (sv, proj_ty))
        | AEndedProjLoans _ | AEndedProjBorrows _ | AIgnoredProjBorrows -> ());
        super#visit_aproj env sproj

      (** We may need to end borrows in "regular" values, because of shared values *)
      method! visit_borrow_content _ bc =
        match bc with
        | VSharedBorrow _ | VMutBorrow (_, _) -> raise (FoundBorrowContent bc)
        | VReservedMutBorrow _ -> craise __FILE__ __LINE__ span "Unreachable"
    end
  in
  (* Lookup the abstraction *)
  let abs = ctx_lookup_abs ctx abs_id in
  try
    (* Explore the abstraction, looking for borrows *)
    obj#visit_abs () abs;
    (* No borrows: nothing to update *)
    (ctx, fun e -> e)
  with
  (* There are concrete (i.e., not symbolic) borrows: end them, then reexplore *)
  | FoundABorrowContent bc ->
      log#ldebug
        (lazy
          ("end_abstraction_borrows: found aborrow content: "
          ^ aborrow_content_to_string ~span:(Some span) ctx bc));
      let ctx =
        match bc with
        | AMutBorrow (pm, bid, av) ->
            sanity_check __FILE__ __LINE__ (pm = PNone) span;
            (* First, convert the avalue to a (fresh symbolic) value *)
            let sv = convert_avalue_to_given_back_value span av in
            (* Replace the mut borrow to register the fact that we ended
             * it and store with it the freshly generated given back value *)
            let ended_borrow = ABorrow (AEndedMutBorrow (sv, av)) in
            let ctx = update_aborrow span ek_all bid ended_borrow ctx in
            (* Give the value back *)
            let sv = mk_typed_value_from_symbolic_value sv in
            give_back_value config span bid sv ctx
        | ASharedBorrow (pm, bid) ->
            sanity_check __FILE__ __LINE__ (pm = PNone) span;
            (* Replace the shared borrow to account for the fact it ended *)
            let ended_borrow = ABorrow AEndedSharedBorrow in
            let ctx = update_aborrow span ek_all bid ended_borrow ctx in
            (* Give back *)
            give_back_shared config span bid ctx
        | AProjSharedBorrow asb ->
            (* Retrieve the borrow ids *)
            let bids =
              List.filter_map
                (fun asb ->
                  match asb with
                  | AsbBorrow bid -> Some bid
                  | AsbProjReborrows (_, _) -> None)
                asb
            in
            (* There should be at least one borrow identifier in the set, which we
             * can use to identify the whole set *)
            let repr_bid = List.hd bids in
            (* Replace the shared borrow with Bottom *)
            let ctx = update_aborrow span ek_all repr_bid ABottom ctx in
            (* Give back the shared borrows *)
            let ctx =
              List.fold_left
                (fun ctx bid -> give_back_shared config span bid ctx)
                ctx bids
            in
            (* Continue *)
            ctx
        | AEndedMutBorrow _ | AIgnoredMutBorrow _ | AEndedIgnoredMutBorrow _
        | AEndedSharedBorrow ->
            craise __FILE__ __LINE__ span "Unexpected"
      in
      (* Reexplore *)
      end_abstraction_borrows config span chain abs_id ctx
  (* There are symbolic borrows: end them, then reexplore *)
  | FoundAProjBorrows (sv, proj_ty) ->
      log#ldebug
        (lazy
          ("end_abstraction_borrows: found aproj borrows: "
          ^ aproj_to_string ctx (AProjBorrows (sv, proj_ty))));
      (* Generate a fresh symbolic value *)
      let nsv = mk_fresh_symbolic_value span proj_ty in
      (* Replace the proj_borrows - there should be exactly one *)
      let ended_borrow = AEndedProjBorrows nsv in
      let ctx = update_aproj_borrows span abs.abs_id sv ended_borrow ctx in
      (* Give back the symbolic value *)
      let ctx =
        give_back_symbolic_value config span abs.regions proj_ty sv nsv ctx
      in
      (* Reexplore *)
      end_abstraction_borrows config span chain abs_id ctx
  (* There are concrete (i.e., not symbolic) borrows in shared values: end them, then reexplore *)
  | FoundBorrowContent bc ->
      log#ldebug
        (lazy
          ("end_abstraction_borrows: found borrow content: "
          ^ borrow_content_to_string ~span:(Some span) ctx bc));
      let ctx =
        match bc with
        | VSharedBorrow bid -> (
            (* Replace the shared borrow with bottom *)
            let allow_inner_loans = false in
            match
              end_borrow_get_borrow span (Some abs_id) allow_inner_loans bid ctx
            with
            | Error _ -> craise __FILE__ __LINE__ span "Unreachable"
            | Ok (ctx, _) ->
                (* Give back *)
                give_back_shared config span bid ctx)
        | VMutBorrow (bid, v) -> (
            (* Replace the mut borrow with bottom *)
            let allow_inner_loans = false in
            match
              end_borrow_get_borrow span (Some abs_id) allow_inner_loans bid ctx
            with
            | Error _ -> craise __FILE__ __LINE__ span "Unreachable"
            | Ok (ctx, _) ->
                (* Give the value back - note that the mut borrow was below a
                 * shared borrow: the value is thus unchanged *)
                give_back_value config span bid v ctx)
        | VReservedMutBorrow _ -> craise __FILE__ __LINE__ span "Unreachable"
      in
      (* Reexplore *)
      end_abstraction_borrows config span chain abs_id ctx

(** Remove an abstraction from the context, as well as all its references *)
and end_abstraction_remove_from_context (_config : config) (span : Meta.span)
    (abs_id : AbstractionId.id) : cm_fun =
 fun ctx ->
  let ctx, abs = ctx_remove_abs span ctx abs_id in
  let abs = Option.get abs in
  (* Synthesize the symbolic AST *)
  (ctx, SynthesizeSymbolic.synthesize_end_abstraction ctx abs)

(** End a proj_loan over a symbolic value by ending the proj_borrows which
    intersect this proj_loans.
    
    Rk.:
    - if this symbolic value is primitively copiable, then:
      - either proj_borrows are only present in the concrete context
      - or there is only one intersecting proj_borrow present in an
        abstraction
    - otherwise, this symbolic value is not primitively copiable:
      - there may be proj_borrows_shared over this value
      - if we put aside the proj_borrows_shared, there should be exactly one
        intersecting proj_borrows, either in the concrete context or in an
        abstraction
*)
and end_proj_loans_symbolic (config : config) (span : Meta.span)
    (chain : borrow_or_abs_ids) (abs_id : AbstractionId.id)
    (regions : RegionId.Set.t) (sv : symbolic_value) : cm_fun =
 fun ctx ->
  (* Small helpers for sanity checks *)
  let check ctx = no_aproj_over_symbolic_in_context span sv ctx in
  (* Find the first proj_borrows which intersects the proj_loans *)
  let explore_shared = true in
  match
    lookup_intersecting_aproj_borrows_opt span explore_shared regions sv ctx
  with
  | None ->
      (* We couldn't find any in the context: it means that the symbolic value
       * is in the concrete environment (or that we dropped it, in which case
       * it is completely absent). We thus simply need to replace the loans
       * projector with an ended projector. *)
      let ctx = update_aproj_loans_to_ended span abs_id sv ctx in
      (* Sanity check *)
      check ctx;
      (* Continue *)
      (ctx, fun e -> e)
  | Some (SharedProjs projs) ->
      (* We found projectors over shared values - split between the projectors
         which belong to the current abstraction and the others.
         The context looks like this:
         {[
           abs'0 {
             // The loan was initially like this:
             // [shared_loan lids (s <: ...) [s]]
             // but if we get there it means it was already ended:
             ended_shared_loan (s <: ...) [s]
             proj_shared_borrows [...; (s <: ...); ...]
             proj_shared_borrows [...; (s <: ...); ...]
             ...
           }

           abs'1 [
             proj_shared_borrows [...; (s <: ...); ...]
             ...
           }

           ...

           // No [s] outside of abstractions

         ]}
      *)
      let _owned_projs, external_projs =
        List.partition (fun (abs_id', _) -> abs_id' = abs_id) projs
      in
      (* End the external borrow projectors (end their abstractions) *)
      let ctx, cc =
        let abs_ids = List.map fst external_projs in
        let abs_ids =
          List.fold_left
            (fun s id -> AbstractionId.Set.add id s)
            AbstractionId.Set.empty abs_ids
        in
        (* End the abstractions and continue *)
        end_abstractions_aux config span chain abs_ids ctx
      in
      (* End the internal borrows projectors and the loans projector *)
      let ctx =
        (* All the proj_borrows are owned: simply erase them *)
        let ctx =
          remove_intersecting_aproj_borrows_shared span regions sv ctx
        in
        (* End the loan itself *)
        update_aproj_loans_to_ended span abs_id sv ctx
      in
      (* Sanity check *)
      check ctx;
      (ctx, cc)
  | Some (NonSharedProj (abs_id', _proj_ty)) ->
      (* We found one projector of borrows in an abstraction: if it comes
       * from this abstraction, we can end it directly, otherwise we need
       * to end the abstraction where it came from first *)
      if abs_id' = abs_id then (
        (* Note that it happens when a function returns a [&mut ...] which gets
           expanded to [mut_borrow l s], and we end the borrow [l] (so [s] gets
           reinjected in the parent abstraction without having been modified).

           The context looks like this:
           {[
             abs'0 {
               [s <: ...]
               (s <: ...)
             }

             // Note that [s] can't appear in other abstractions or in the
             // regular environment (because we forbid the duplication of
             // symbolic values which contain borrows).
           ]}
        *)
        (* End the projector of borrows - TODO: not completely sure what to
         * replace it with... Maybe we should introduce an ABottomProj? *)
        let ctx = update_aproj_borrows span abs_id sv AIgnoredProjBorrows ctx in
        (* Sanity check: no other occurrence of an intersecting projector of borrows *)
        sanity_check __FILE__ __LINE__
          (Option.is_none
             (lookup_intersecting_aproj_borrows_opt span explore_shared regions
                sv ctx))
          span;
        (* End the projector of loans *)
        let ctx = update_aproj_loans_to_ended span abs_id sv ctx in
        (* Sanity check *)
        check ctx;
        (* Continue *)
        (ctx, fun e -> e))
      else
        (* The borrows proj comes from a different abstraction: end it. *)
        let ctx, cc = end_abstraction_aux config span chain abs_id' ctx in
        (* Retry ending the projector of loans *)
        let ctx, cc =
          comp cc
            (end_proj_loans_symbolic config span chain abs_id regions sv ctx)
        in
        (* Sanity check *)
        check ctx;
        (* Return *)
        (ctx, cc)

let end_borrow config (span : Meta.span) : BorrowId.id -> cm_fun =
  end_borrow_aux config span [] None

let end_borrows config (span : Meta.span) : BorrowId.Set.t -> cm_fun =
  end_borrows_aux config span [] None

let end_abstraction config span = end_abstraction_aux config span []
let end_abstractions config span = end_abstractions_aux config span []
let end_borrow_no_synth config span id ctx = fst (end_borrow config span id ctx)

let end_borrows_no_synth config span ids ctx =
  fst (end_borrows config span ids ctx)

let end_abstraction_no_synth config span id ctx =
  fst (end_abstraction config span id ctx)

let end_abstractions_no_synth config span ids ctx =
  fst (end_abstractions config span ids ctx)

(** Helper function: see {!activate_reserved_mut_borrow}.

    This function updates the shared loan to a mutable loan (we then update
    the borrow with another helper). Of course, the shared loan must contain
    exactly one borrow id (the one we give as parameter), otherwise we can't
    promote it. Also, the shared value mustn't contain any loan.

    The returned value (previously shared) is checked:
    - it mustn't contain loans
    - it mustn't contain {!Bottom}
    - it mustn't contain reserved borrows
    TODO: this kind of checks should be put in an auxiliary helper, because
    they are redundant.

    The loan to update mustn't be a borrowed value.
 *)
let promote_shared_loan_to_mut_loan (span : Meta.span) (l : BorrowId.id)
    (ctx : eval_ctx) : typed_value * eval_ctx =
  (* Debug *)
  log#ldebug
    (lazy
      ("promote_shared_loan_to_mut_loan:\n- loan: " ^ BorrowId.to_string l
     ^ "\n- context:\n"
      ^ eval_ctx_to_string ~span:(Some span) ctx
      ^ "\n"));
  (* Lookup the shared loan - note that we can't promote a shared loan
   * in a shared value, but we can do it in a mutably borrowed value.
   * This is important because we can do: [let y = &two-phase ( *x );]
   *)
  let ek =
    { enter_shared_loans = false; enter_mut_borrows = true; enter_abs = false }
  in
  match lookup_loan span ek l ctx with
  | _, Concrete (VMutLoan _) ->
      craise __FILE__ __LINE__ span "Expected a shared loan, found a mut loan"
  | _, Concrete (VSharedLoan (bids, sv)) ->
      (* Check that there is only one borrow id (l) and update the loan *)
      cassert __FILE__ __LINE__
        (BorrowId.Set.mem l bids && BorrowId.Set.cardinal bids = 1)
        span "There should only be one borrow id";
      (* We need to check that there aren't any loans in the value:
         we should have gotten rid of those already, but it is better
         to do a sanity check. *)
      sanity_check __FILE__ __LINE__ (not (loans_in_value sv)) span;
      (* Check there isn't {!Bottom} (this is actually an invariant *)
      cassert __FILE__ __LINE__
        (not (bottom_in_value ctx.ended_regions sv))
        span "There shouldn't be a bottom";
      (* Check there aren't reserved borrows *)
      cassert __FILE__ __LINE__
        (not (reserved_in_value sv))
        span "There shouldn't be reserved borrows";
      (* Update the loan content *)
      let ctx = update_loan span ek l (VMutLoan l) ctx in
      (* Return *)
      (sv, ctx)
  | _, Abstract _ ->
      (* I don't think it is possible to have two-phase borrows involving borrows
         returned by abstractions. I'm not sure how we could handle that anyway. *)
      craise __FILE__ __LINE__ span
        "Can't promote a shared loan to a mutable loan if the loan is inside a \
         region abstraction"

(** Helper function: see {!activate_reserved_mut_borrow}.

    This function updates a shared borrow to a mutable borrow (and that is
    all: it doesn't touch the corresponding loan).
 *)
let replace_reserved_borrow_with_mut_borrow (span : Meta.span) (l : BorrowId.id)
    (borrowed_value : typed_value) (ctx : eval_ctx) : eval_ctx =
  (* Lookup the reserved borrow - note that we don't go inside borrows/loans:
     there can't be reserved borrows inside other borrows/loans
  *)
  let ek =
    { enter_shared_loans = false; enter_mut_borrows = false; enter_abs = false }
  in
  match lookup_borrow span ek l ctx with
  | Concrete (VSharedBorrow _ | VMutBorrow (_, _)) ->
      craise __FILE__ __LINE__ span "Expected a reserved mutable borrow"
  | Concrete (VReservedMutBorrow _) ->
      (* Update it *)
      update_borrow span ek l (VMutBorrow (l, borrowed_value)) ctx
  | Abstract _ ->
      (* This can't happen for sure *)
      craise __FILE__ __LINE__ span
        "Can't promote a shared borrow to a mutable borrow if the borrow is \
         inside a region abstraction"

(** Promote a reserved mut borrow to a mut borrow. *)
let rec promote_reserved_mut_borrow (config : config) (span : Meta.span)
    (l : BorrowId.id) : cm_fun =
 fun ctx ->
  (* Lookup the value *)
  let ek =
    { enter_shared_loans = false; enter_mut_borrows = true; enter_abs = false }
  in
  match lookup_loan span ek l ctx with
  | _, Concrete (VMutLoan _) -> craise __FILE__ __LINE__ span "Unreachable"
  | _, Concrete (VSharedLoan (bids, sv)) -> (
      (* If there are loans inside the value, end them. Note that there can't be
         reserved borrows inside the value.
         If we perform an update, do a recursive call to lookup the updated value *)
      match get_first_loan_in_value sv with
      | Some lc ->
          (* End the loans *)
          let ctx, cc =
            match lc with
            | VSharedLoan (bids, _) -> end_borrows config span bids ctx
            | VMutLoan bid -> end_borrow config span bid ctx
          in
          (* Recursive call *)
          comp cc (promote_reserved_mut_borrow config span l ctx)
      | None ->
          (* No loan to end inside the value *)
          (* Some sanity checks *)
          log#ldebug
            (lazy
              ("activate_reserved_mut_borrow: resulting value:\n"
              ^ typed_value_to_string ~span:(Some span) ctx sv));
          sanity_check __FILE__ __LINE__ (not (loans_in_value sv)) span;
          sanity_check __FILE__ __LINE__
            (not (bottom_in_value ctx.ended_regions sv))
            span;
          sanity_check __FILE__ __LINE__ (not (reserved_in_value sv)) span;
          (* End the borrows which borrow from the value, at the exception of
             the borrow we want to promote *)
          let bids = BorrowId.Set.remove l bids in
          let ctx, cc = end_borrows config span bids ctx in
          (* Promote the loan - TODO: this will fail if the value contains
           * any loans. In practice, it shouldn't, but we could also
           * look for loans inside the value and end them before promoting
           * the borrow. *)
          let v, ctx = promote_shared_loan_to_mut_loan span l ctx in
          (* Promote the borrow - the value should have been checked by
             {!promote_shared_loan_to_mut_loan}
          *)
          let ctx = replace_reserved_borrow_with_mut_borrow span l v ctx in
          (* Return *)
          (ctx, cc))
  | _, Abstract _ ->
      (* I don't think it is possible to have two-phase borrows involving borrows
       * returned by abstractions. I'm not sure how we could handle that anyway. *)
      craise __FILE__ __LINE__ span
        "Can't activate a reserved mutable borrow referencing a loan inside\n\
        \         a region abstraction"

let destructure_abs (span : Meta.span) (abs_kind : abs_kind) (can_end : bool)
    (destructure_shared_values : bool) (ctx : eval_ctx) (abs0 : abs) : abs =
  (* Accumulator to store the destructured values *)
  let avalues = ref [] in
  (* Utility function to store a value in the accumulator *)
  let push_avalue av = avalues := List.append !avalues [ av ] in
  (* We use this function to make sure we never register values (i.e.,
     loans or borrows) when we shouldn't - see it as a sanity check:
     for now, we don't allow nested borrows, which means we should register
     values from children of borrows. In this condition, we could simply
     ignore the children altogether. Instead, we explore them and make sure
     we don't register values while doing so.
  *)
  let push_fail _ = craise __FILE__ __LINE__ span "Unreachable" in
  (* Function to explore an avalue and destructure it *)
  let rec list_avalues (allow_borrows : bool) (push : typed_avalue -> unit)
      (av : typed_avalue) : unit =
    let ty = av.ty in
    match av.value with
    | ABottom | AIgnored -> ()
    | AAdt adt ->
        (* Simply explore the children *)
        List.iter (list_avalues allow_borrows push) adt.field_values
    | ALoan lc -> (
        (* Explore the loan content *)
        match lc with
        | ASharedLoan (pm, bids, sv, child_av) ->
            (* We don't support nested borrows for now *)
            cassert __FILE__ __LINE__
              (not (value_has_borrows ctx sv.value))
              span "Nested borrows are not supported yet";
            (* Destructure the shared value *)
            let avl, sv =
              if destructure_shared_values then list_values sv else ([], sv)
            in
            (* Push a value *)
            let ignored = mk_aignored span child_av.ty in
            let value = ALoan (ASharedLoan (pm, bids, sv, ignored)) in
            push { value; ty };
            (* Explore the child *)
            list_avalues false push_fail child_av;
            (* Push the avalues introduced because we decomposed the inner loans -
               note that we pay attention to the order in which we introduce
               the avalues: we want to push them *after* the outer loan. If we
               didn't want that, we would have implemented [list_values] in
               exactly the same way as [list_avalues] (i.e., with a similar
               signature) *)
            List.iter push avl
        | AMutLoan (pm, bid, child_av) ->
            (* Explore the child *)
            list_avalues false push_fail child_av;
            (* Explore the whole loan *)
            let ignored = mk_aignored span child_av.ty in
            let value = ALoan (AMutLoan (pm, bid, ignored)) in
            push { value; ty }
        | AIgnoredMutLoan (opt_bid, child_av) ->
            (* We don't support nested borrows for now *)
            cassert __FILE__ __LINE__
              (not (ty_has_borrows ctx.type_ctx.type_infos child_av.ty))
              span "Nested borrows are not supported yet";
            sanity_check __FILE__ __LINE__ (opt_bid = None) span;
            (* Simply explore the child *)
            list_avalues false push_fail child_av
        | AEndedMutLoan
            { child = child_av; given_back = _; given_back_span = _ }
        | AEndedSharedLoan (_, child_av)
        | AEndedIgnoredMutLoan
            { child = child_av; given_back = _; given_back_span = _ }
        | AIgnoredSharedLoan child_av ->
            (* We don't support nested borrows for now *)
            cassert __FILE__ __LINE__
              (not (ty_has_borrows ctx.type_ctx.type_infos child_av.ty))
              span "Nested borrows are not supported yet";
            (* Simply explore the child *)
            list_avalues false push_fail child_av)
    | ABorrow bc -> (
        (* Sanity check - rem.: may be redundant with [push_fail] *)
        sanity_check __FILE__ __LINE__ allow_borrows span;
        (* Explore the borrow content *)
        match bc with
        | AMutBorrow (pm, bid, child_av) ->
            (* Explore the child *)
            list_avalues false push_fail child_av;
            (* Explore the borrow *)
            let ignored = mk_aignored span child_av.ty in
            let value = ABorrow (AMutBorrow (pm, bid, ignored)) in
            push { value; ty }
        | ASharedBorrow _ ->
            (* Nothing specific to do: keep the value as it is *)
            push av
        | AIgnoredMutBorrow (opt_bid, child_av) ->
            (* We don't support nested borrows for now *)
            cassert __FILE__ __LINE__
              (not (ty_has_borrows ctx.type_ctx.type_infos child_av.ty))
              span "Nested borrows are not supported yet";
            sanity_check __FILE__ __LINE__ (opt_bid = None) span;
            (* Just explore the child *)
            list_avalues false push_fail child_av
        | AEndedIgnoredMutBorrow
            { child = child_av; given_back = _; given_back_span = _ } ->
            (* We don't support nested borrows for now *)
            cassert __FILE__ __LINE__
              (not (ty_has_borrows ctx.type_ctx.type_infos child_av.ty))
              span "Nested borrows are not supported yet";
            (* Just explore the child *)
            list_avalues false push_fail child_av
        | AProjSharedBorrow asb ->
            (* We don't support nested borrows *)
            cassert __FILE__ __LINE__ (asb = []) span
              "Nested borrows are not supported yet";
            (* Nothing specific to do *)
            ()
        | AEndedMutBorrow _ | AEndedSharedBorrow ->
            (* If we get there it means the abstraction ended: it should not
               be in the context anymore (if we end *one* borrow in an abstraction,
               we have to end them all and remove the abstraction from the context)
            *)
            craise __FILE__ __LINE__ span "Unreachable")
    | ASymbolic _ ->
        (* For now, we fore all symbolic values containing borrows to be eagerly
           expanded *)
        sanity_check __FILE__ __LINE__
          (not (ty_has_borrows ctx.type_ctx.type_infos ty))
          span
  and list_values (v : typed_value) : typed_avalue list * typed_value =
    let ty = v.ty in
    match v.value with
    | VLiteral _ -> ([], v)
    | VAdt adt ->
        let avll, field_values =
          List.split (List.map list_values adt.field_values)
        in
        let avl = List.concat avll in
        let adt = { adt with field_values } in
        (avl, { v with value = VAdt adt })
    | VBottom -> craise __FILE__ __LINE__ span "Unreachable"
    | VBorrow _ ->
        (* We don't support nested borrows for now *)
        craise __FILE__ __LINE__ span "Unreachable"
    | VLoan lc -> (
        match lc with
        | VSharedLoan (bids, sv) ->
            let avl, sv = list_values sv in
            if destructure_shared_values then (
              (* Rem.: the shared value can't contain loans nor borrows *)
              cassert __FILE__ __LINE__ (ty_no_regions ty) span
                "Nested borrows are not supported yet";
              let av : typed_avalue =
                sanity_check __FILE__ __LINE__
                  (not (value_has_loans_or_borrows ctx sv.value))
                  span;
                (* We introduce fresh ids for the symbolic values *)
                let mk_value_with_fresh_sids (v : typed_value) : typed_value =
                  let visitor =
                    object
                      inherit [_] map_typed_avalue

                      method! visit_symbolic_value_id _ _ =
                        fresh_symbolic_value_id ()
                    end
                  in
                  visitor#visit_typed_value () v
                in
                let sv = mk_value_with_fresh_sids sv in
                (* Create the new avalue *)
                let value =
                  ALoan (ASharedLoan (PNone, bids, sv, mk_aignored span ty))
                in
                { value; ty }
              in
              let avl = List.append [ av ] avl in
              (avl, sv))
            else (avl, { v with value = VLoan (VSharedLoan (bids, sv)) })
        | VMutLoan _ -> craise __FILE__ __LINE__ span "Unreachable")
    | VSymbolic _ ->
        (* For now, we fore all symbolic values containing borrows to be eagerly
           expanded *)
        sanity_check __FILE__ __LINE__
          (not (ty_has_borrows ctx.type_ctx.type_infos ty))
          span;
        ([], v)
  in

  (* Destructure the avalues *)
  List.iter (list_avalues true push_avalue) abs0.avalues;
  let avalues = !avalues in
  (* Update *)
  { abs0 with avalues; kind = abs_kind; can_end }

let abs_is_destructured (span : Meta.span) (destructure_shared_values : bool)
    (ctx : eval_ctx) (abs : abs) : bool =
  let abs' =
    destructure_abs span abs.kind abs.can_end destructure_shared_values ctx abs
  in
  abs = abs'

let convert_value_to_abstractions (span : Meta.span) (abs_kind : abs_kind)
    (can_end : bool) (destructure_shared_values : bool) (ctx : eval_ctx)
    (v : typed_value) : abs list =
  (* Convert the value to a list of avalues *)
  let absl = ref [] in
  let push_abs (r_id : RegionId.id) (avalues : typed_avalue list) : unit =
    if avalues = [] then ()
    else
      (* Create the abs - note that we keep the order of the avalues as it is
         (unlike the environments) *)
      let abs =
        {
          abs_id = fresh_abstraction_id ();
          kind = abs_kind;
          can_end;
          parents = AbstractionId.Set.empty;
          original_parents = [];
          regions = RegionId.Set.singleton r_id;
          ancestors_regions = RegionId.Set.empty;
          avalues;
        }
      in
      (* Add to the list of abstractions *)
      absl := abs :: !absl
  in

  (* [group]: group in one abstraction (because we dived into a borrow/loan)

     We return one typed-value for the shared values: when we encounter a shared
     loan, we need to compute the value which will be shared. If [destructure_shared_values]
     is [true], this shared value will be stripped of its shared loans.
  *)
  let rec to_avalues (allow_borrows : bool) (inside_borrowed : bool)
      (group : bool) (r_id : RegionId.id) (v : typed_value) :
      typed_avalue list * typed_value =
    (* Debug *)
    log#ldebug
      (lazy
        ("convert_value_to_abstractions: to_avalues:\n- value: "
        ^ typed_value_to_string ~span:(Some span) ctx v));

    let ty = v.ty in
    match v.value with
    | VLiteral _ -> ([], v)
    | VBottom ->
        (* Can happen: we *do* convert dummy values to abstractions, and dummy
           values can contain bottoms *)
        ([], v)
    | VAdt adt ->
        (* Two cases, depending on whether we have to group all the borrows/loans
           inside one abstraction or not *)
        let avl, field_values =
          if group then
            (* Convert to avalues, and transmit to the parent *)
            let avl, field_values =
              List.split
                (List.map
                   (to_avalues allow_borrows inside_borrowed group r_id)
                   adt.field_values)
            in
            (List.concat avl, field_values)
          else
            (* Create one abstraction per field, and transmit nothing to the parent *)
            let field_values =
              List.map
                (fun fv ->
                  let r_id = fresh_region_id () in
                  let avl, fv =
                    to_avalues allow_borrows inside_borrowed group r_id fv
                  in
                  push_abs r_id avl;
                  fv)
                (* Slightly tricky: pay attention to the order in which the
                   abstractions are pushed (i.e.: the [List.rev] is important
                   to get a "good" environment, and a nice translation) *)
                (List.rev adt.field_values)
            in
            ([], field_values)
        in
        let adt = { adt with field_values } in
        (avl, { v with value = VAdt adt })
    | VBorrow bc -> (
        let _, ref_ty, kind = ty_as_ref ty in
        cassert __FILE__ __LINE__ (ty_no_regions ref_ty) span
          "Nested borrows are not supported yet";
        (* Sanity check *)
        sanity_check __FILE__ __LINE__ allow_borrows span;
        (* Convert the borrow content *)
        match bc with
        | VSharedBorrow bid ->
            cassert __FILE__ __LINE__ (ty_no_regions ref_ty) span
              "Nested borrows are not supported yet";
            let ty = TRef (RFVar r_id, ref_ty, kind) in
            let value = ABorrow (ASharedBorrow (PNone, bid)) in
            ([ { value; ty } ], v)
        | VMutBorrow (bid, bv) ->
            let r_id = if group then r_id else fresh_region_id () in
            (* We don't support nested borrows for now *)
            cassert __FILE__ __LINE__
              (not (value_has_borrows ctx bv.value))
              span "Nested borrows are not supported yet";
            (* Create an avalue to push - note that we use [AIgnore] for the inner avalue *)
            let ty = TRef (RFVar r_id, ref_ty, kind) in
            let ignored = mk_aignored span ref_ty in
            let av = ABorrow (AMutBorrow (PNone, bid, ignored)) in
            let av = { value = av; ty } in
            (* Continue exploring, looking for loans (and forbidding borrows,
               because we don't support nested borrows for now) *)
            let avl, bv = to_avalues false true true r_id bv in
            let value = { v with value = VBorrow (VMutBorrow (bid, bv)) } in
            (av :: avl, value)
        | VReservedMutBorrow _ ->
            (* This borrow should have been activated *)
            craise __FILE__ __LINE__ span "Unexpected")
    | VLoan lc -> (
        match lc with
        | VSharedLoan (bids, sv) ->
            let r_id = if group then r_id else fresh_region_id () in
            (* We don't support nested borrows for now *)
            cassert __FILE__ __LINE__
              (not (value_has_borrows ctx sv.value))
              span "Nested borrows are not supported yet";
            (* Push the avalue - note that we use [AIgnore] for the inner avalue *)
            (* For avalues, a loan has the borrow type *)
            cassert __FILE__ __LINE__ (ty_no_regions ty) span
              "Nested borrows are not supported yet";
            let ty = mk_ref_ty (RFVar r_id) ty RShared in
            let ignored = mk_aignored span ty in
            (* Rem.: the shared value might contain loans *)
            let avl, sv = to_avalues false true true r_id sv in
            let av = ALoan (ASharedLoan (PNone, bids, sv, ignored)) in
            let av = { value = av; ty } in
            (* Continue exploring, looking for loans (and forbidding borrows,
               because we don't support nested borrows for now) *)
            let value : value =
              if destructure_shared_values then sv.value
              else VLoan (VSharedLoan (bids, sv))
            in
            let value = { v with value } in
            (av :: avl, value)
        | VMutLoan bid ->
            (* Push the avalue - note that we use [AIgnore] for the inner avalue *)
            (* For avalues, a loan has the borrow type *)
            cassert __FILE__ __LINE__ (ty_no_regions ty) span
              "Nested borrows are not supported yet";
            let ty = mk_ref_ty (RFVar r_id) ty RMut in
            let ignored = mk_aignored span ty in
            let av = ALoan (AMutLoan (PNone, bid, ignored)) in
            let av = { value = av; ty } in
            ([ av ], v))
    | VSymbolic _ ->
        (* For now, we force all the symbolic values containing borrows to
           be eagerly expanded, and we don't support nested borrows *)
        cassert __FILE__ __LINE__
          (not (value_has_borrows ctx v.value))
          span "Nested borrows are not supported yet";
        (* Return nothing *)
        ([], v)
  in
  (* Generate the avalues *)
  let r_id = fresh_region_id () in
  let values, _ = to_avalues true false false r_id v in
  (* Introduce an abstraction for the returned values *)
  push_abs r_id values;
  (* Return *)
  List.rev !absl

type marker_borrow_or_loan_id =
  | BorrowId of proj_marker * borrow_id
  | LoanId of proj_marker * loan_id

type g_loan_content_with_ty =
  (ety * loan_content, rty * aloan_content) concrete_or_abs

type g_borrow_content_with_ty =
  (ety * borrow_content, rty * aborrow_content) concrete_or_abs

type merge_abstraction_info = {
  loans : MarkerBorrowId.Set.t;
  borrows : MarkerBorrowId.Set.t;
  borrows_loans : marker_borrow_or_loan_id list;
      (** We use a list to preserve the order in which the borrows were found *)
  loan_to_content : g_loan_content_with_ty MarkerBorrowId.Map.t;
  borrow_to_content : g_borrow_content_with_ty MarkerBorrowId.Map.t;
}

(** Small utility to help merging abstractions.

    We compute the list of loan/borrow contents, maps from borrow/loan ids
    to borrow/loan contents, etc.

    Note that this function is very specific to [merge_into_first_abstraction]: we
    have strong assumptions about the shape of the abstraction, and in
    particular that:
    - its values don't contain nested borrows
    - all the borrows are destructured (for instance, shared loans can't
      contain shared loans).
 *)
let compute_merge_abstraction_info (span : Meta.span) (ctx : eval_ctx)
    (avalues : typed_avalue list) : merge_abstraction_info =
  let loans : MarkerBorrowId.Set.t ref = ref MarkerBorrowId.Set.empty in
  let borrows : MarkerBorrowId.Set.t ref = ref MarkerBorrowId.Set.empty in
  let borrows_loans : marker_borrow_or_loan_id list ref = ref [] in
  let loan_to_content : g_loan_content_with_ty MarkerBorrowId.Map.t ref =
    ref MarkerBorrowId.Map.empty
  in
  let borrow_to_content : g_borrow_content_with_ty MarkerBorrowId.Map.t ref =
    ref MarkerBorrowId.Map.empty
  in

  let push_loan pm id (lc : g_loan_content_with_ty) : unit =
    sanity_check __FILE__ __LINE__
      (not (MarkerBorrowId.Set.mem (pm, id) !loans))
      span;
    loans := MarkerBorrowId.Set.add (pm, id) !loans;
    sanity_check __FILE__ __LINE__
      (not (MarkerBorrowId.Map.mem (pm, id) !loan_to_content))
      span;
    loan_to_content := MarkerBorrowId.Map.add (pm, id) lc !loan_to_content;
    borrows_loans := LoanId (pm, id) :: !borrows_loans
  in
  let push_loans pm ids lc : unit =
    BorrowId.Set.iter (fun id -> push_loan pm id lc) ids
  in
  let push_borrow pm id (bc : g_borrow_content_with_ty) : unit =
    sanity_check __FILE__ __LINE__
      (not (MarkerBorrowId.Set.mem (pm, id) !borrows))
      span;
    borrows := MarkerBorrowId.Set.add (pm, id) !borrows;
    sanity_check __FILE__ __LINE__
      (not (MarkerBorrowId.Map.mem (pm, id) !borrow_to_content))
      span;
    borrow_to_content := MarkerBorrowId.Map.add (pm, id) bc !borrow_to_content;
    borrows_loans := BorrowId (pm, id) :: !borrows_loans
  in

  let iter_avalues =
    object
      inherit [_] iter_typed_avalue as super

      (** We redefine this to track the types *)
      method! visit_typed_avalue _ v =
        super#visit_typed_avalue (Some (Abstract v.ty)) v

      (** We redefine this to track the types *)
      method! visit_typed_value _ (v : typed_value) =
        super#visit_typed_value (Some (Concrete v.ty)) v

      method! visit_loan_content _ _ =
        (* Could happen if we explore shared values whose sub-values are
           reborrowed. We use the fact that we destructure the nested shared
           loans before reducing a context or computing a join. *)
        craise __FILE__ __LINE__ span "Unreachable"

      method! visit_borrow_content _ _ =
        (* Can happen if we explore shared values which contain borrows -
           i.e., if we have nested borrows (we forbid this for now) *)
        craise __FILE__ __LINE__ span "Unreachable"

      method! visit_aloan_content env lc =
        let ty =
          match Option.get env with
          | Concrete _ -> craise __FILE__ __LINE__ span "Unreachable"
          | Abstract ty -> ty
        in
        (* Register the loans *)
        (match lc with
        | ASharedLoan (pm, bids, _, _) -> push_loans pm bids (Abstract (ty, lc))
        | AMutLoan (pm, bid, _) -> push_loan pm bid (Abstract (ty, lc))
        | AEndedMutLoan _ | AEndedSharedLoan _ | AIgnoredMutLoan _
        | AEndedIgnoredMutLoan _ | AIgnoredSharedLoan _ ->
            (* The abstraction has been destructured, so those shouldn't appear *)
            craise __FILE__ __LINE__ span "Unreachable");
        (* Continue *)
        super#visit_aloan_content env lc

      method! visit_aborrow_content env bc =
        let ty =
          match Option.get env with
          | Concrete _ -> craise __FILE__ __LINE__ span "Unreachable"
          | Abstract ty -> ty
        in
        (* Explore the borrow content *)
        (match bc with
        | AMutBorrow (pm, bid, _) | ASharedBorrow (pm, bid) ->
            push_borrow pm bid (Abstract (ty, bc))
        | AProjSharedBorrow asb ->
            let register asb =
              match asb with
              | AsbBorrow bid -> push_borrow PNone bid (Abstract (ty, bc))
              | AsbProjReborrows _ ->
                  (* Can only happen if the symbolic value (potentially) contains
                     borrows - i.e., we have nested borrows *)
                  craise __FILE__ __LINE__ span "Unreachable"
            in
            List.iter register asb
        | AIgnoredMutBorrow _ | AEndedIgnoredMutBorrow _ | AEndedMutBorrow _
        | AEndedSharedBorrow ->
            (* The abstraction has been destructured, so those shouldn't appear *)
            craise __FILE__ __LINE__ span "Unreachable");
        super#visit_aborrow_content env bc

      method! visit_symbolic_value _ sv =
        (* Sanity check: no borrows *)
        sanity_check __FILE__ __LINE__
          (not (symbolic_value_has_borrows ctx sv))
          span
    end
  in

  List.iter (iter_avalues#visit_typed_avalue None) avalues;

  {
    loans = !loans;
    borrows = !borrows;
    borrows_loans = List.rev !borrows_loans;
    loan_to_content = !loan_to_content;
    borrow_to_content = !borrow_to_content;
  }

type merge_duplicates_funcs = {
  merge_amut_borrows :
    borrow_id ->
    rty ->
    proj_marker ->
    typed_avalue ->
    rty ->
    proj_marker ->
    typed_avalue ->
    typed_avalue;
      (** Parameters:
          - [id]
          - [ty0]
          - [pm0]
          - [child0]
          - [ty1]
          - [pm1]
          - [child1]

          The children should be [AIgnored].
       *)
  merge_ashared_borrows :
    borrow_id -> rty -> proj_marker -> rty -> proj_marker -> typed_avalue;
      (** Parameters:
          - [id]
          - [ty0]
          - [pm0]
          - [ty1]
          - [pm1]
       *)
  merge_amut_loans :
    loan_id ->
    rty ->
    proj_marker ->
    typed_avalue ->
    rty ->
    proj_marker ->
    typed_avalue ->
    typed_avalue;
      (** Parameters:
          - [id]
          - [ty0]
          - [pm0]
          - [child0]
          - [ty1]
          - [pm1]
          - [child1]

          The children should be [AIgnored].
       *)
  merge_ashared_loans :
    loan_id_set ->
    rty ->
    proj_marker ->
    typed_value ->
    typed_avalue ->
    rty ->
    proj_marker ->
    typed_value ->
    typed_avalue ->
    typed_avalue;
      (** Parameters:
          - [ids]
          - [ty0]
          - [pm0]
          - [sv0]
          - [child0]
          - [ty1]
          - [pm1]
          - [sv1]
          - [child1]
       *)
}

(** Small utility: if a value doesn't have any marker, split it into two values
    with complementary markers. We use this for {!merge_abstractions}.

    We assume the value has been destructured (there are no nested loans,
    adts, the children are ignored, etc.).
 *)
let typed_avalue_split_marker (span : Meta.span) (ctx : eval_ctx)
    (av : typed_avalue) : typed_avalue list =
  let mk_split pm mk_value =
    if pm = PNone then [ mk_value PLeft; mk_value PRight ] else [ av ]
  in
  match av.value with
  | AAdt _ | ABottom | ASymbolic _ | AIgnored ->
      craise __FILE__ __LINE__ span "Unexpected"
  | ABorrow bc -> (
      match bc with
      | AMutBorrow (pm, bid, child) ->
          sanity_check __FILE__ __LINE__ (is_aignored child.value) span;
          let mk_value pm =
            { av with value = ABorrow (AMutBorrow (pm, bid, child)) }
          in
          mk_split pm mk_value
      | ASharedBorrow (pm, bid) ->
          let mk_value pm =
            { av with value = ABorrow (ASharedBorrow (pm, bid)) }
          in
          mk_split pm mk_value
      | _ -> craise __FILE__ __LINE__ span "Unsupported yet")
  | ALoan lc -> (
      match lc with
      | AMutLoan (pm, bid, child) ->
          sanity_check __FILE__ __LINE__ (is_aignored child.value) span;
          let mk_value pm =
            { av with value = ALoan (AMutLoan (pm, bid, child)) }
          in
          mk_split pm mk_value
      | ASharedLoan (pm, bids, sv, child) ->
          sanity_check __FILE__ __LINE__ (is_aignored child.value) span;
          sanity_check __FILE__ __LINE__
            (not (value_has_borrows ctx sv.value))
            span;
          let mk_value pm =
            { av with value = ALoan (ASharedLoan (pm, bids, sv, child)) }
          in
          mk_split pm mk_value
      | _ -> craise __FILE__ __LINE__ span "Unsupported yet")

let abs_split_markers (span : Meta.span) (ctx : eval_ctx) (abs : abs) : abs =
  {
    abs with
    avalues =
      List.concat (List.map (typed_avalue_split_marker span ctx) abs.avalues);
  }

(** Auxiliary function for {!merge_abstractions}.

    Phase 1 of the merge: we simplify all loan/borrow pairs, if a loan is
    in the left abstraction and its corresponding borrow is in the right
    abstraction.

    Important: this is asymmetric (the loan must be in the left abstraction).

    Example:
    {[
      abs0 { ML l0, MB l1 } |><| abs1 { MB l0 }
          ~~> abs1 { MB l1 }
    ]}

    But:
    {[
      abs1 { MB l0 } |><| abs0 { ML l0, MB l1 }
          ~~> abs1 { MB l0, ML l0, MB l1 }
    ]}

    We return the list of merged values.
 *)
let merge_abstractions_merge_loan_borrow_pairs (span : Meta.span)
    (merge_funs : merge_duplicates_funcs option) (ctx : eval_ctx) (abs0 : abs)
    (abs1 : abs) : typed_avalue list =
  log#ldebug (lazy "merge_abstractions_merge_loan_borrow_pairs");

  (* Split the markers inside the abstractions (if we allow using markers).

     We do so because it enables simplification later when we are in the following case:
     {[
       abs0 { ML l0 } |><| abs1 { |MB l0|, MB l1 }
     ]}

     If we split before merging we get:
     {[
       abs0 { |ML l0|, ︙ML l0︙ } |><| abs1 { |MB l0|, |MB l1|, ︙MB l1︙ }
           ~~> merge
       abs2 { ︙ML l0︙, |MB l1|, ︙MB l1︙ }
           ~~> simplify the complementary markers
       abs2 { ︙ML l0︙, MB l1 }
     ]}
  *)
  let abs0, abs1 =
    if merge_funs = None then (abs0, abs1)
    else (abs_split_markers span ctx abs0, abs_split_markers span ctx abs1)
  in

  (* Compute the relevant information *)
  let {
    loans = loans0;
    borrows = borrows0;
    borrows_loans = borrows_loans0;
    loan_to_content = loan_to_content0;
    borrow_to_content = borrow_to_content0;
  } =
    compute_merge_abstraction_info span ctx abs0.avalues
  in

  let {
    loans = loans1;
    borrows = borrows1;
    borrows_loans = borrows_loans1;
    loan_to_content = loan_to_content1;
    borrow_to_content = borrow_to_content1;
  } =
    compute_merge_abstraction_info span ctx abs1.avalues
  in

  (* Sanity check: no markers appear unless we allow merging duplicates *)
  if merge_funs = None then (
    sanity_check __FILE__ __LINE__
      (List.for_all
         (function LoanId (pm, _) | BorrowId (pm, _) -> pm = PNone)
         borrows_loans0)
      span;
    sanity_check __FILE__ __LINE__
      (List.for_all
         (function LoanId (pm, _) | BorrowId (pm, _) -> pm = PNone)
         borrows_loans1)
      span;
    sanity_check __FILE__ __LINE__
      (MarkerBorrowId.Set.disjoint borrows0 borrows1)
      span;
    sanity_check __FILE__ __LINE__
      (MarkerBorrowId.Set.disjoint loans0 loans1)
      span);

  (* Merge.
     There are several cases:
     - if a borrow/loan is only in one abstraction, we simply check if we need
       to filter it (because its associated loan/borrow is in the other
       abstraction).
     - if a borrow/loan is present in both abstractions, we need to merge its
       content.

     Note that we may need to merge strictly more than two avalues, because of
     shared loans. For instance, if we have:
     {[
       abs'0 { shared_loan { l0, l1 } ... }
       abs'1 { shared_loan { l0 } ..., shared_loan { l1 } ... }
     ]}

     We ignore this case for now: we check that whenever we merge two shared loans,
     then their sets of ids are equal, and fail if it is not the case.
     Remark: a way of solving this problem would be to destructure shared loans
     so that they always have exactly one id.
  *)
  let merged_borrows = ref MarkerBorrowId.Set.empty in
  let merged_loans = ref MarkerBorrowId.Set.empty in
  let avalues = ref [] in
  let push_avalue av =
    log#ldebug
      (lazy
        ("merge_abstractions_merge_loan_borrow_pairs: push_avalue: "
        ^ typed_avalue_to_string ~span:(Some span) ctx av));
    avalues := av :: !avalues
  in
  let push_opt_avalue av =
    match av with None -> () | Some av -> push_avalue av
  in

  (* Compute the intersection of:
     - the loans coming from the left abstraction
     - the borrows coming from the right abstraction *)
  let intersect = MarkerBorrowId.Set.inter loans0 borrows1 in

  (* This function is called when handling shared loans: we have to apply a projection
     marker to a set of borrow ids. *)
  let filter_bids (pm : proj_marker) (bids : BorrowId.Set.t) : BorrowId.Set.t =
    let bids =
      BorrowId.Set.to_seq bids
      |> Seq.map (fun x -> (pm, x))
      |> MarkerBorrowId.Set.of_seq
    in
    let bids = MarkerBorrowId.Set.diff bids intersect in
    sanity_check __FILE__ __LINE__ (not (MarkerBorrowId.Set.is_empty bids)) span;
    MarkerBorrowId.Set.to_seq bids |> Seq.map snd |> BorrowId.Set.of_seq
  in
  let filter_bid (bid : marker_borrow_id) : marker_borrow_id option =
    if MarkerBorrowId.Set.mem bid intersect then None else Some bid
  in

  let borrow_is_merged id = MarkerBorrowId.Set.mem id !merged_borrows in
  let set_borrow_as_merged id =
    merged_borrows := MarkerBorrowId.Set.add id !merged_borrows
  in
  let loan_is_merged id = MarkerBorrowId.Set.mem id !merged_loans in
  let set_loan_as_merged id =
    merged_loans := MarkerBorrowId.Set.add id !merged_loans
  in
  let set_loans_as_merged pm ids =
    BorrowId.Set.elements ids
    |> List.map (fun x -> (pm, x))
    |> List.iter set_loan_as_merged
  in

  (* Note that we first explore the borrows/loans of [abs0], because we
     want to merge *into* this abstraction, and as a consequence we want to
     preserve its structure as much as we can *)
  let borrows_loans = List.append borrows_loans0 borrows_loans1 in
  (* Iterate over all the borrows/loans ids found in the abstractions *)
  List.iter
    (fun bl ->
      match bl with
      | BorrowId (pm, bid) ->
          let bid = (pm, bid) in
          log#ldebug
            (lazy
              ("merge_abstractions: merging borrow "
              ^ MarkerBorrowId.to_string bid));

          (* Check if the borrow has already been merged - this can happen
             because we go through all the borrows/loans in [abs0] *then*
             all the borrows/loans in [abs1], and there may be duplicates
             between the two *)
          if borrow_is_merged bid then ()
          else (
            set_borrow_as_merged bid;
            (* Check if we need to filter it *)
            match filter_bid bid with
            | None -> ()
            | Some bid ->
                (* Lookup the contents *)
                let bc0 = MarkerBorrowId.Map.find_opt bid borrow_to_content0 in
                let bc1 = MarkerBorrowId.Map.find_opt bid borrow_to_content1 in
                (* Merge *)
                let av : typed_avalue =
                  match (bc0, bc1) with
                  | None, Some bc | Some bc, None -> (
                      match bc with
                      | Concrete (_, _) ->
                          (* This can happen only in case of nested borrows -
                             a concrete borrow can only happen inside a shared
                             loan
                          *)
                          craise __FILE__ __LINE__ span "Unreachable"
                      | Abstract (ty, bc) -> { value = ABorrow bc; ty })
                  | Some _, Some _ ->
                      (* Because of markers, the case where the same borrow is duplicated should
                         be unreachable. Note, this is due to all shared borrows currently
                         taking different ids, this will not be the case anymore when shared loans
                         will take a unique id instead of a set *)
                      craise __FILE__ __LINE__ span "Unreachable"
                  | None, None -> craise __FILE__ __LINE__ span "Unreachable"
                in
                push_avalue av)
      | LoanId (pm, bid) ->
          let bid = (pm, bid) in
          if
            (* Check if the loan has already been treated - it can happen
               for the same reason as for borrows, and also because shared
               loans contain sets of borrows (meaning that when taking care
               of one loan, we can merge several other loans at once).
            *)
            loan_is_merged bid
          then ()
          else (
            log#ldebug
              (lazy
                ("merge_abstractions: merging loan "
                ^ MarkerBorrowId.to_string bid));

            (* Check if we need to filter it *)
            match filter_bid bid with
            | None -> ()
            | Some bid ->
                (* Lookup the contents *)
                let lc0 = MarkerBorrowId.Map.find_opt bid loan_to_content0 in
                let lc1 = MarkerBorrowId.Map.find_opt bid loan_to_content1 in
                (* Merge *)
                let av : typed_avalue option =
                  match (lc0, lc1) with
                  | None, Some lc | Some lc, None -> (
                      match lc with
                      | Concrete _ ->
                          (* This shouldn't happen because the avalues should
                             have been destructured. *)
                          craise __FILE__ __LINE__ span "Unreachable"
                      | Abstract (ty, lc) -> (
                          match lc with
                          | ASharedLoan (pm, bids, sv, child) ->
                              let bids = filter_bids pm bids in
                              sanity_check __FILE__ __LINE__
                                (not (BorrowId.Set.is_empty bids))
                                span;
                              sanity_check __FILE__ __LINE__
                                (is_aignored child.value) span;
                              sanity_check __FILE__ __LINE__
                                (not (value_has_loans_or_borrows ctx sv.value))
                                span;
                              let lc = ASharedLoan (pm, bids, sv, child) in
                              set_loans_as_merged pm bids;
                              Some { value = ALoan lc; ty }
                          | AMutLoan _ ->
                              set_loan_as_merged bid;
                              Some { value = ALoan lc; ty }
                          | AEndedMutLoan _ | AEndedSharedLoan _
                          | AIgnoredMutLoan _ | AEndedIgnoredMutLoan _
                          | AIgnoredSharedLoan _ ->
                              (* The abstraction has been destructured, so those shouldn't appear *)
                              craise __FILE__ __LINE__ span "Unreachable"))
                  | Some _, Some _ ->
                      (* With projection markers, shared loans should not be duplicated *)
                      craise __FILE__ __LINE__ span "Unreachable"
                  | None, None -> craise __FILE__ __LINE__ span "Unreachable"
                in
                push_opt_avalue av))
    borrows_loans;

  (* Reverse the avalues (we visited the loans/borrows in order, but pushed
     new values at the beggining of the stack of avalues) *)
  List.rev !avalues

(** Auxiliary function for {!merge_abstractions}.

    Phase 2 of the merge: we remove markers, by merging pairs of the same
    element with different markers into one element without markers.

    Example:
    {[
      |MB l0|, MB l1, ︙MB l0︙
           ~~>
      MB l0, MB l1
    ]}
 *)
let merge_abstractions_merge_markers (span : Meta.span)
    (merge_funs : merge_duplicates_funcs option) (ctx : eval_ctx)
    (abs_values : typed_avalue list) : typed_avalue list =
  log#ldebug
    (lazy
      ("merge_abstractions_merge_markers:\n- avalues:\n"
      ^ String.concat ", " (List.map (typed_avalue_to_string ctx) abs_values)));

  (* We linearly traverse the list of avalues created through the first phase. *)

  (* Utilities to accumulate the list of values resulting from the merge *)
  let avalues = ref [] in
  let push_avalue av =
    log#ldebug
      (lazy
        ("merge_abstractions_merge_markers: push_avalue: "
        ^ typed_avalue_to_string ~span:(Some span) ctx av));
    avalues := av :: !avalues
  in

  (* Compute some relevant information *)
  let {
    loans = _;
    borrows = _;
    borrows_loans;
    loan_to_content;
    borrow_to_content;
  } =
    compute_merge_abstraction_info span ctx abs_values
  in

  (* We will merge elements with the same borrow/loan id, but with different markers.
     Hence, we only keep track of the id here: if [Borrow PLeft bid] has been merged
     and we see [Borrow PRight bid], we should ignore [Borrow PRight bid] (because
     when seeing [Borrow PLeft bid] we stored [Borrow PNone bid] into the list
     of values to insert in the resulting abstraction). *)
  let merged_borrows = ref BorrowId.Set.empty in
  let merged_loans = ref BorrowId.Set.empty in

  let borrow_is_merged id = BorrowId.Set.mem id !merged_borrows in
  let set_borrow_as_merged id =
    merged_borrows := BorrowId.Set.add id !merged_borrows
  in

  let loan_is_merged id = BorrowId.Set.mem id !merged_loans in
  let set_loan_as_merged id =
    merged_loans := BorrowId.Set.add id !merged_loans
  in
  let set_loans_as_merged ids = BorrowId.Set.iter set_loan_as_merged ids in

  (* Recreates an avalue from a borrow_content. *)
  let avalue_from_bc = function
    | Concrete (_, _) ->
        (* This can happen only in case of nested borrows, and should have been filtered during phase 1 *)
        craise __FILE__ __LINE__ span "Unreachable"
    | Abstract (ty, bc) -> { value = ABorrow bc; ty }
  in

  (* Recreates an avalue from a loan_content, and adds the set of loan ids as merged.
     See the comment in the loop below for a detailed explanation *)
  let avalue_from_lc = function
    | Concrete (_, _) ->
        (* This can happen only in case of nested borrows, and should have been filtered
           during phase 1 *)
        craise __FILE__ __LINE__ span "Unreachable"
    | Abstract (ty, bc) ->
        (match bc with
        | AMutLoan (_, id, _) -> set_loan_as_merged id
        | ASharedLoan (_, ids, _, _) -> set_loans_as_merged ids
        | _ -> craise __FILE__ __LINE__ span "Unreachable");
        { value = ALoan bc; ty }
  in

  let complementary_markers pm0 pm1 =
    (pm0 = PLeft && pm1 = PRight) || (pm0 = PRight && pm1 = PLeft)
  in

  (* Some utility functions *)
  (* Merge two aborrow contents - note that those contents must have the same id *)
  let merge_aborrow_contents (ty0 : rty) (bc0 : aborrow_content) (ty1 : rty)
      (bc1 : aborrow_content) : typed_avalue =
    match (bc0, bc1) with
    | AMutBorrow (pm0, id0, child0), AMutBorrow (pm1, id1, child1) ->
        (* Sanity-check of the precondition *)
        sanity_check __FILE__ __LINE__ (id0 = id1) span;
        sanity_check __FILE__ __LINE__ (complementary_markers pm0 pm1) span;
        (Option.get merge_funs).merge_amut_borrows id0 ty0 pm0 child0 ty1 pm1
          child1
    | ASharedBorrow (pm0, id0), ASharedBorrow (pm1, id1) ->
        (* Sanity-check of the precondition *)
        sanity_check __FILE__ __LINE__ (id0 = id1) span;
        sanity_check __FILE__ __LINE__ (complementary_markers pm0 pm1) span;
        (Option.get merge_funs).merge_ashared_borrows id0 ty0 pm0 ty1 pm1
    | AProjSharedBorrow _, AProjSharedBorrow _ ->
        (* Unreachable because requires nested borrows *)
        craise __FILE__ __LINE__ span "Unreachable"
    | _ ->
        (* Unreachable because those cases are ignored (ended/ignored borrows)
           or inconsistent *)
        craise __FILE__ __LINE__ span "Unreachable"
  in

  let merge_g_borrow_contents (bc0 : g_borrow_content_with_ty)
      (bc1 : g_borrow_content_with_ty) : typed_avalue =
    match (bc0, bc1) with
    | Concrete _, Concrete _ ->
        (* This can happen only in case of nested borrows - the borrow has
           to appear inside a shared loan. *)
        craise __FILE__ __LINE__ span "Unreachable"
    | Abstract (ty0, bc0), Abstract (ty1, bc1) ->
        merge_aborrow_contents ty0 bc0 ty1 bc1
    | Concrete _, Abstract _ | Abstract _, Concrete _ ->
        (* TODO: is it really unreachable? *)
        craise __FILE__ __LINE__ span "Unreachable"
  in

  let loan_content_to_ids (lc : g_loan_content_with_ty) : BorrowId.Set.t =
    match lc with
    | Abstract (_, lc) -> (
        match lc with
        | AMutLoan (_, id, _) -> BorrowId.Set.singleton id
        | ASharedLoan (_, ids, _, _) -> ids
        | _ ->
            (* Unreachable because those cases are ignored (ended/ignored borrows)
               or inconsistent *)
            craise __FILE__ __LINE__ span "Unreachable")
    | Concrete _ ->
        (* Can only happen with nested borrows *)
        craise __FILE__ __LINE__ span "Unreachable"
  in

  let merge_aloan_contents (ty0 : rty) (lc0 : aloan_content) (ty1 : rty)
      (lc1 : aloan_content) : typed_avalue =
    match (lc0, lc1) with
    | AMutLoan (pm0, id0, child0), AMutLoan (pm1, id1, child1) ->
        (* Sanity-check of the precondition *)
        sanity_check __FILE__ __LINE__ (id0 = id1) span;
        sanity_check __FILE__ __LINE__ (complementary_markers pm0 pm1) span;
        (* Merge *)
        (Option.get merge_funs).merge_amut_loans id0 ty0 pm0 child0 ty1 pm1
          child1
    | ASharedLoan (pm0, ids0, sv0, child0), ASharedLoan (pm1, ids1, sv1, child1)
      ->
        sanity_check __FILE__ __LINE__ (complementary_markers pm0 pm1) span;
        (* Check that the sets of ids are the same - if it is not the case, it
           means we actually need to merge more than 2 avalues: we ignore this
           case for now *)
        sanity_check __FILE__ __LINE__ (BorrowId.Set.equal ids0 ids1) span;
        let ids = ids0 in
        (* Merge *)
        (Option.get merge_funs).merge_ashared_loans ids ty0 pm0 sv0 child0 ty1
          pm1 sv1 child1
    | _ ->
        (* Unreachable because those cases are ignored (ended/ignored borrows)
           or inconsistent *)
        craise __FILE__ __LINE__ span "Unreachable"
  in

  (* Note that because we may filter ids from a set of id, this function has
     to register the merged loan ids: the caller doesn't do it (contrary to
     the borrow case) *)
  let merge_g_loan_contents (lc0 : g_loan_content_with_ty)
      (lc1 : g_loan_content_with_ty) : typed_avalue =
    match (lc0, lc1) with
    | Concrete _, Concrete _ ->
        (* This can not happen: the values should have been destructured *)
        craise __FILE__ __LINE__ span "Unreachable"
    | Abstract (ty0, lc0), Abstract (ty1, lc1) ->
        merge_aloan_contents ty0 lc0 ty1 lc1
    | Concrete _, Abstract _ | Abstract _, Concrete _ ->
        (* TODO: is it really unreachable? *)
        craise __FILE__ __LINE__ span "Unreachable"
  in

  let invert_proj_marker = function
    | PNone -> craise __FILE__ __LINE__ span "Unreachable"
    | PLeft -> PRight
    | PRight -> PLeft
  in

  (* We now iter over all the accumulated elements. For each element with a marker
     (i.e., not [PNone]), we attempt to find the dual element in the rest of the list. If so,
     we remove both elements, and insert the same element but with no marker.

     Importantly, attempting the merge when first seeing a marked element allows us to preserve
     the structure of the abstraction we are merging into (abs0). During phase 1, we traversed
     the borrow_loans of the abs 0 first, and hence these elements are at the top of the list *)
  List.iter
    (function
      | BorrowId (PNone, bid) ->
          sanity_check __FILE__ __LINE__ (not (borrow_is_merged bid)) span;
          (* This element has no marker. We do not filter it, hence we retrieve the
             contents and inject it into the avalues list *)
          let bc = MarkerBorrowId.Map.find (PNone, bid) borrow_to_content in
          push_avalue (avalue_from_bc bc);
          (* Setting the borrow as merged is not really necessary but we do it
             for consistency, and this allows us to do some sanity checks. *)
          set_borrow_as_merged bid
      | BorrowId (pm, bid) ->
          (* Check if the borrow has already been merged. If so, it means we already
             added the merged value to the avalues list, and we can thus skip it *)
          if borrow_is_merged bid then ()
          else (
            (* Not merged: set it as merged *)
            set_borrow_as_merged bid;
            (* Lookup the content of the borrow *)
            let bc0 = MarkerBorrowId.Map.find (pm, bid) borrow_to_content in
            (* Check if there exists the same borrow but with the complementary marker *)
            let obc1 =
              MarkerBorrowId.Map.find_opt
                (invert_proj_marker pm, bid)
                borrow_to_content
            in
            match obc1 with
            | None ->
                (* No dual element found, we keep the current one in the list of avalues,
                   with the same marker *)
                push_avalue (avalue_from_bc bc0)
            | Some bc1 ->
                (* We have borrows with left and right markers in the environment.
                   We merge their values, and push the result to the list of avalues.
                   The merge will also remove the projection marker *)
                push_avalue (merge_g_borrow_contents bc0 bc1))
      | LoanId (PNone, bid) ->
          (* Since we currently have a set of loan ids associated to a shared_borrow, we can
             have several loan ids associated to the same element. Hence, we need to ensure
             that we did not add the corresponding element previously.

             To do so, we use the loan id merged set for both marked and unmarked values.
             The assumption is that we should not have the same loan id for both an unmarked
             element and a marked element. It might be better to sanity-check this.

             Adding the loan id to the merged set will be done inside avalue_from_lc.

             Rem: Once we move to a single loan id per shared_loan, this should not be needed
             anymore.
          *)
          if loan_is_merged bid then ()
          else
            let lc = MarkerBorrowId.Map.find (PNone, bid) loan_to_content in
            push_avalue (avalue_from_lc lc);
            (* Mark as merged *)
            let ids = loan_content_to_ids lc in
            set_loans_as_merged ids
      | LoanId (pm, bid) -> (
          if
            (* Check if the loan has already been merged. If so, we skip it. *)
            loan_is_merged bid
          then ()
          else
            let lc0 = MarkerBorrowId.Map.find (pm, bid) loan_to_content in
            let olc1 =
              MarkerBorrowId.Map.find_opt
                (invert_proj_marker pm, bid)
                loan_to_content
            in
            (* Mark as merged *)
            let ids0 = loan_content_to_ids lc0 in
            set_loans_as_merged ids0;
            match olc1 with
            | None ->
                (* No dual element found, we keep the current one with the same marker *)
                push_avalue (avalue_from_lc lc0)
            | Some lc1 ->
                push_avalue (merge_g_loan_contents lc0 lc1);
                (* Mark as merged *)
                let ids1 = loan_content_to_ids lc1 in
                set_loans_as_merged ids1))
    borrows_loans;

  let avalues = List.rev !avalues in

  (* Reorder the avalues. We want the avalues to have the borrows first, then
     the loans (this structure is more stable when we merge abstractions together,
     meaning it is easier to find fixed points).
  *)
  let is_borrow (av : typed_avalue) : bool =
    match av.value with
    | ABorrow _ -> true
    | ALoan _ -> false
    | _ -> craise __FILE__ __LINE__ span "Unexpected"
  in
  let aborrows, aloans = List.partition is_borrow avalues in
  List.append aborrows aloans

(** Auxiliary function.

    Merge two abstractions into one, without updating the context.
 *)
let merge_abstractions (span : Meta.span) (abs_kind : abs_kind) (can_end : bool)
    (merge_funs : merge_duplicates_funcs option) (ctx : eval_ctx) (abs0 : abs)
    (abs1 : abs) : abs =
  log#ldebug
    (lazy
      ("merge_abstractions:\n- abs0:\n"
      ^ abs_to_string span ctx abs0
      ^ "\n\n- abs1:\n"
      ^ abs_to_string span ctx abs1));
  (* Sanity check: we can't merge an abstraction with itself *)
  sanity_check __FILE__ __LINE__ (abs0.abs_id <> abs1.abs_id) span;

  (* Check that the abstractions are destructured (i.e., there are no nested
     values, etc.) *)
  if !Config.sanity_checks then (
    let destructure_shared_values = true in
    sanity_check __FILE__ __LINE__
      (abs_is_destructured span destructure_shared_values ctx abs0)
      span;
    sanity_check __FILE__ __LINE__
      (abs_is_destructured span destructure_shared_values ctx abs1)
      span);

  (* Phase 1: simplify the loans coming from the left abstraction with
     the borrows coming from the right abstraction. *)
  let avalues =
    merge_abstractions_merge_loan_borrow_pairs span merge_funs ctx abs0 abs1
  in

  (* Phase 2: we now remove markers, by merging pairs of the same element with
     different markers into one element. To do so, we linearly traverse the list
     of avalues created through the first phase. *)
  let avalues = merge_abstractions_merge_markers span merge_funs ctx avalues in

  (* Create the new abstraction *)
  let abs_id = fresh_abstraction_id () in
  (* Note that one of the two abstractions might a parent of the other *)
  let parents =
    AbstractionId.Set.diff
      (AbstractionId.Set.union abs0.parents abs1.parents)
      (AbstractionId.Set.of_list [ abs0.abs_id; abs1.abs_id ])
  in
  let original_parents = AbstractionId.Set.elements parents in
  let regions = RegionId.Set.union abs0.regions abs1.regions in
  let ancestors_regions =
    RegionId.Set.diff (RegionId.Set.union abs0.regions abs1.regions) regions
  in
  let abs =
    {
      abs_id;
      kind = abs_kind;
      can_end;
      parents;
      original_parents;
      regions;
      ancestors_regions;
      avalues;
    }
  in

  (* Sanity check *)
  sanity_check __FILE__ __LINE__ (abs_is_destructured span true ctx abs) span;
  (* Return *)
  abs

(** Merge the regions in a context to a single region *)
let ctx_merge_regions (ctx : eval_ctx) (rid : RegionId.id)
    (rids : RegionId.Set.t) : eval_ctx =
  let rsubst x = if RegionId.Set.mem x rids then rid else x in
  let env = Substitute.env_subst_rids rsubst ctx.env in
  { ctx with env }

let merge_into_first_abstraction (span : Meta.span) (abs_kind : abs_kind)
    (can_end : bool) (merge_funs : merge_duplicates_funcs option)
    (ctx : eval_ctx) (abs_id0 : AbstractionId.id) (abs_id1 : AbstractionId.id) :
    eval_ctx * AbstractionId.id =
  (* Lookup the abstractions *)
  let abs0 = ctx_lookup_abs ctx abs_id0 in
  let abs1 = ctx_lookup_abs ctx abs_id1 in

  (* Merge them *)
  let nabs =
    merge_abstractions span abs_kind can_end merge_funs ctx abs0 abs1
  in

  (* Update the environment: replace the abstraction 0 with the result of the merge,
     remove the abstraction 1 *)
  let ctx = fst (ctx_subst_abs span ctx abs_id0 nabs) in
  let ctx = fst (ctx_remove_abs span ctx abs_id1) in

  (* Merge all the regions from the abstraction into one (the first - i.e., the
     one with the smallest id). Note that we need to do this in the whole
     environment, as those regions may be referenced as ancestor regions by
     the other abstractions, and may be present in symbolic values, etc. (this
     is not the case if there are no nested borrows, but we anticipate).
  *)
  let ctx =
    let regions = nabs.regions in
    (* Pick the first region id (this is the smallest) *)
    let rid = RegionId.Set.min_elt regions in
    (* If there is only one region, do nothing *)
    if RegionId.Set.cardinal regions = 1 then ctx
    else
      let rids = RegionId.Set.remove rid regions in
      ctx_merge_regions ctx rid rids
  in

  (* Return *)
  (ctx, nabs.abs_id)

let reorder_loans_borrows_in_fresh_abs (span : Meta.span) (allow_markers : bool)
    (old_abs_ids : AbstractionId.Set.t) (ctx : eval_ctx) : eval_ctx =
  let reorder_in_fresh_abs (abs : abs) : abs =
    (* Split between the loans and borrows *)
    let is_borrow (av : typed_avalue) : bool =
      match av.value with
      | ABorrow _ -> true
      | ALoan _ -> false
      | _ -> craise __FILE__ __LINE__ span "Unexpected"
    in
    let aborrows, aloans = List.partition is_borrow abs.avalues in

    (* Reoder the borrows, and the loans.

       After experimenting, it seems that a good way of reordering the loans
       and the borrows to find fixed points is simply to sort them by increasing
       order of id (taking the smallest id of a set of ids, in case of sets).

       This is actually not as arbitrary as it might seem, because the ids give
       us the order in which we introduced those borrows/loans.
    *)
    let get_borrow_id (av : typed_avalue) : BorrowId.id =
      match av.value with
      | ABorrow (AMutBorrow (pm, bid, _) | ASharedBorrow (pm, bid)) ->
          sanity_check __FILE__ __LINE__ (allow_markers || pm = PNone) span;
          bid
      | _ -> craise __FILE__ __LINE__ span "Unexpected"
    in
    let get_loan_id (av : typed_avalue) : BorrowId.id =
      match av.value with
      | ALoan (AMutLoan (pm, lid, _)) ->
          sanity_check __FILE__ __LINE__ (allow_markers || pm = PNone) span;
          lid
      | ALoan (ASharedLoan (pm, lids, _, _)) ->
          sanity_check __FILE__ __LINE__ (allow_markers || pm = PNone) span;
          BorrowId.Set.min_elt lids
      | _ -> craise __FILE__ __LINE__ span "Unexpected"
    in
    (* We use ordered maps to reorder the borrows and loans *)
    let reorder (get_bid : typed_avalue -> BorrowId.id)
        (values : typed_avalue list) : typed_avalue list =
      List.map snd
        (BorrowId.Map.bindings
           (BorrowId.Map.of_list (List.map (fun v -> (get_bid v, v)) values)))
    in
    let aborrows = reorder get_borrow_id aborrows in
    let aloans = reorder get_loan_id aloans in
    let avalues = List.append aborrows aloans in
    { abs with avalues }
  in

  let reorder_in_abs (abs : abs) =
    if AbstractionId.Set.mem abs.abs_id old_abs_ids then abs
    else reorder_in_fresh_abs abs
  in

  let env = env_map_abs reorder_in_abs ctx.env in

  { ctx with env }