diff options
author | Son Ho | 2023-01-08 13:10:05 +0100 |
---|---|---|
committer | Son HO | 2023-02-03 11:21:46 +0100 |
commit | b6dbf55f8190374b45faa20414984dc8ac195105 (patch) | |
tree | f6e90e6fd270ccc989aca07a774a089d3af7744f | |
parent | 1302f2830905dc63f294aad00d78d03486e13d73 (diff) |
Split InterpreterLoops into several files
-rw-r--r-- | compiler/Driver.ml | 3 | ||||
-rw-r--r-- | compiler/InterpreterLoops.ml | 4101 | ||||
-rw-r--r-- | compiler/InterpreterLoops.mli | 62 | ||||
-rw-r--r-- | compiler/InterpreterLoopsCore.ml | 386 | ||||
-rw-r--r-- | compiler/InterpreterLoopsFixedPoint.ml | 965 | ||||
-rw-r--r-- | compiler/InterpreterLoopsFixedPoint.mli | 166 | ||||
-rw-r--r-- | compiler/InterpreterLoopsJoinCtxs.ml | 719 | ||||
-rw-r--r-- | compiler/InterpreterLoopsJoinCtxs.mli | 120 | ||||
-rw-r--r-- | compiler/InterpreterLoopsMatchCtxs.ml | 1591 | ||||
-rw-r--r-- | compiler/InterpreterLoopsMatchCtxs.mli | 301 | ||||
-rw-r--r-- | compiler/Logging.ml | 9 | ||||
-rw-r--r-- | compiler/dune | 4 |
12 files changed, 4329 insertions, 4098 deletions
diff --git a/compiler/Driver.ml b/compiler/Driver.ml index 3d2e84ad..dfe4e908 100644 --- a/compiler/Driver.ml +++ b/compiler/Driver.ml @@ -24,6 +24,9 @@ let _ = pre_passes_log#set_level EL.Info; interpreter_log#set_level EL.Info; statements_log#set_level EL.Info; + loops_match_ctxs_log#set_level EL.Info; + loops_join_ctxs_log#set_level EL.Info; + loops_fixed_point_log#set_level EL.Info; loops_log#set_level EL.Info; paths_log#set_level EL.Info; expressions_log#set_level EL.Info; diff --git a/compiler/InterpreterLoops.ml b/compiler/InterpreterLoops.ml index dd0cfc3f..5b170ac5 100644 --- a/compiler/InterpreterLoops.ml +++ b/compiler/InterpreterLoops.ml @@ -1,61 +1,3 @@ -(** This module implements support for loops. - - Throughout the module, we will use the following code as example to - illustrate what the functions do (this function simply returns a mutable - borrow to the nth element of a list): - {[ - pub fn list_nth_mut<'a, T>(mut ls: &'a mut List<T>, mut i: u32) -> &'a mut T { - loop { - match ls { - List::Nil => { - panic!() - } - List::Cons(x, tl) => { - if i == 0 { - return x; - } else { - ls = tl; - i -= 1; - } - } - } - } - } - ]} - - Upon reaching the loop entry, the environment is as follows (ignoring the - dummy variables): - {[ - abs@0 { ML l0 } - ls -> MB l0 (s2 : loops::List<T>) - i -> s1 : u32 - ]} - - Upon reaching the [continue] at the end of the first iteration, the environment - is as follows: - {[ - abs@0 { ML l0 } - ls -> MB l4 (s@6 : loops::List<T>) - i -> s@7 : u32 - _@1 -> MB l0 (loops::List::Cons (ML l1, ML l2)) - _@2 -> MB l2 (@Box (ML l4)) // tail - _@3 -> MB l1 (s@3 : T) // hd - ]} - - The fixed point we compute is: - {[ - abs@0 { ML l0 } - ls -> MB l1 (s@3 : loops::List<T>) - i -> s@4 : u32 - abs@fp { // fp: fixed-point - MB l0 - ML l1 - } - ]} - - From here, we deduce that [abs@fp { MB l0, ML l1}] is the loop abstraction. - *) - module T = Types module PV = PrimitiveValues module V = Values @@ -64,3931 +6,18 @@ module C = Contexts module Subst = Substitute module A = LlbcAst module L = Logging -open TypesUtils open ValuesUtils module Inv = Invariants module S = SynthesizeSymbolic -module UF = UnionFind open Cps open InterpreterUtils -open InterpreterBorrows -open InterpreterExpressions +open InterpreterLoopsCore +open InterpreterLoopsMatchCtxs +open InterpreterLoopsFixedPoint (** The local logger *) let log = L.loops_log -type updt_env_kind = - | AbsInLeft of V.AbstractionId.id - | LoanInLeft of V.BorrowId.id - | LoansInLeft of V.BorrowId.Set.t - | AbsInRight of V.AbstractionId.id - | LoanInRight of V.BorrowId.id - | LoansInRight of V.BorrowId.Set.t - -(** Utility exception *) -exception ValueMatchFailure of updt_env_kind - -(** Utility exception *) -exception Distinct of string - -type ctx_or_update = (C.eval_ctx, updt_env_kind) result - -(** Union Find *) -module UnionFind = UF.Make (UF.StoreMap) - -(** A small utility - - - Rem.: some environments may be ill-formed (they may contain several times - the same loan or borrow - this happens for instance when merging - environments). This is the reason why we use sets in some places (for - instance, [borrow_to_abs] maps to a *set* of ids). -*) -type abs_borrows_loans_maps = { - abs_ids : V.AbstractionId.id list; - abs_to_borrows : V.BorrowId.Set.t V.AbstractionId.Map.t; - abs_to_loans : V.BorrowId.Set.t V.AbstractionId.Map.t; - abs_to_borrows_loans : V.BorrowId.Set.t V.AbstractionId.Map.t; - borrow_to_abs : V.AbstractionId.Set.t V.BorrowId.Map.t; - loan_to_abs : V.AbstractionId.Set.t V.BorrowId.Map.t; - borrow_loan_to_abs : V.AbstractionId.Set.t V.BorrowId.Map.t; -} - -(** Compute various maps linking the abstractions and the borrows/loans they contain. - - The [explore] function is used to filter abstractions. - - [no_duplicates] checks that borrows/loans are not referenced more than once - (see the documentation for {!abs_borrows_loans_maps}). - *) -let compute_abs_borrows_loans_maps (no_duplicates : bool) - (explore : V.abs -> bool) (env : C.env) : abs_borrows_loans_maps = - let abs_ids = ref [] in - let abs_to_borrows = ref V.AbstractionId.Map.empty in - let abs_to_loans = ref V.AbstractionId.Map.empty in - let abs_to_borrows_loans = ref V.AbstractionId.Map.empty in - let borrow_to_abs = ref V.BorrowId.Map.empty in - let loan_to_abs = ref V.BorrowId.Map.empty in - let borrow_loan_to_abs = ref V.BorrowId.Map.empty in - - let module R (Id0 : Identifiers.Id) (Id1 : Identifiers.Id) = struct - (* - [check_singleton_sets]: check that the mapping maps to a singletong. - [check_not_already_registered]: check if the mapping was not already registered. - *) - let register_mapping (check_singleton_sets : bool) - (check_not_already_registered : bool) (map : Id1.Set.t Id0.Map.t ref) - (id0 : Id0.id) (id1 : Id1.id) : unit = - (* Sanity check *) - (if check_singleton_sets || check_not_already_registered then - match Id0.Map.find_opt id0 !map with - | None -> () - | Some set -> - assert ( - (not check_not_already_registered) || not (Id1.Set.mem id1 set))); - (* Update the mapping *) - map := - Id0.Map.update id0 - (fun ids -> - match ids with - | None -> Some (Id1.Set.singleton id1) - | Some ids -> - (* Sanity check *) - assert (not check_singleton_sets); - assert ( - (not check_not_already_registered) - || not (Id1.Set.mem id1 ids)); - (* Update *) - Some (Id1.Set.add id1 ids)) - !map - end in - let module RAbsBorrow = R (V.AbstractionId) (V.BorrowId) in - let module RBorrowAbs = R (V.BorrowId) (V.AbstractionId) in - let register_borrow_id abs_id bid = - RAbsBorrow.register_mapping false no_duplicates abs_to_borrows abs_id bid; - RAbsBorrow.register_mapping false false abs_to_borrows_loans abs_id bid; - RBorrowAbs.register_mapping no_duplicates no_duplicates borrow_to_abs bid - abs_id; - RBorrowAbs.register_mapping false false borrow_loan_to_abs bid abs_id - in - - let register_loan_id abs_id bid = - RAbsBorrow.register_mapping false no_duplicates abs_to_loans abs_id bid; - RAbsBorrow.register_mapping false false abs_to_borrows_loans abs_id bid; - RBorrowAbs.register_mapping no_duplicates no_duplicates loan_to_abs bid - abs_id; - RBorrowAbs.register_mapping false false borrow_loan_to_abs bid abs_id - in - - let explore_abs = - object (self : 'self) - inherit [_] V.iter_typed_avalue as super - - (** Make sure we don't register the ignored ids *) - method! visit_aloan_content abs_id lc = - match lc with - | AMutLoan _ | ASharedLoan _ -> - (* Process those normally *) - super#visit_aloan_content abs_id lc - | AIgnoredMutLoan (_, child) - | AEndedIgnoredMutLoan { child; given_back = _; given_back_meta = _ } - | AIgnoredSharedLoan child -> - (* Ignore the id of the loan, if there is *) - self#visit_typed_avalue abs_id child - | AEndedMutLoan _ | AEndedSharedLoan _ -> raise (Failure "Unreachable") - - (** Make sure we don't register the ignored ids *) - method! visit_aborrow_content abs_id bc = - match bc with - | AMutBorrow _ | ASharedBorrow _ | AProjSharedBorrow _ -> - (* Process those normally *) - super#visit_aborrow_content abs_id bc - | AIgnoredMutBorrow (_, child) - | AEndedIgnoredMutBorrow { child; given_back = _; given_back_meta = _ } - -> - (* Ignore the id of the borrow, if there is *) - self#visit_typed_avalue abs_id child - | AEndedMutBorrow _ | AEndedSharedBorrow -> - raise (Failure "Unreachable") - - method! visit_borrow_id abs_id bid = register_borrow_id abs_id bid - method! visit_loan_id abs_id lid = register_loan_id abs_id lid - end - in - - C.env_iter_abs - (fun abs -> - let abs_id = abs.abs_id in - if explore abs then ( - abs_to_borrows := - V.AbstractionId.Map.add abs_id V.BorrowId.Set.empty !abs_to_borrows; - abs_to_loans := - V.AbstractionId.Map.add abs_id V.BorrowId.Set.empty !abs_to_loans; - abs_ids := abs.abs_id :: !abs_ids; - List.iter (explore_abs#visit_typed_avalue abs.abs_id) abs.avalues) - else ()) - env; - - (* Rem.: there is no need to reverse the abs ids, because we explored the environment - starting with the freshest values and abstractions *) - { - abs_ids = !abs_ids; - abs_to_borrows = !abs_to_borrows; - abs_to_loans = !abs_to_loans; - abs_to_borrows_loans = !abs_to_borrows_loans; - borrow_to_abs = !borrow_to_abs; - loan_to_abs = !loan_to_abs; - borrow_loan_to_abs = !borrow_loan_to_abs; - } - -(** Refresh the ids of the fresh abstractions. - - We do this because {!prepare_ashared_loans} introduces some non-fixed - abstractions in contexts which are later joined: we have to make sure two - contexts we join don't have non-fixed abstractions with the same ids. - *) -let refresh_abs (old_abs : V.AbstractionId.Set.t) (ctx : C.eval_ctx) : - C.eval_ctx = - let ids, _ = compute_context_ids ctx in - let abs_to_refresh = V.AbstractionId.Set.diff ids.aids old_abs in - let aids_subst = - List.map - (fun id -> (id, C.fresh_abstraction_id ())) - (V.AbstractionId.Set.elements abs_to_refresh) - in - let aids_subst = V.AbstractionId.Map.of_list aids_subst in - let subst id = - match V.AbstractionId.Map.find_opt id aids_subst with - | None -> id - | Some id -> id - in - let env = - Subst.env_subst_ids - (fun x -> x) - (fun x -> x) - (fun x -> x) - (fun x -> x) - (fun x -> x) - subst ctx.env - in - { ctx with C.env } - -(** Reorder the loans and borrows in the fresh abstractions. - - We do this in order to enforce some structure in the environments: this - allows us to find fixed-points. Note that this function needs to be - called typically after we merge abstractions together (see {!collapse_ctx} - for instance). - *) -let reorder_loans_borrows_in_fresh_abs (old_abs_ids : V.AbstractionId.Set.t) - (ctx : C.eval_ctx) : C.eval_ctx = - let reorder_in_fresh_abs (abs : V.abs) : V.abs = - (* Split between the loans and borrows *) - let is_borrow (av : V.typed_avalue) : bool = - match av.V.value with - | ABorrow _ -> true - | ALoan _ -> false - | _ -> raise (Failure "Unexpected") - in - let aborrows, aloans = List.partition is_borrow abs.V.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). - *) - let get_borrow_id (av : V.typed_avalue) : V.BorrowId.id = - match av.V.value with - | V.ABorrow (V.AMutBorrow (bid, _) | V.ASharedBorrow bid) -> bid - | _ -> raise (Failure "Unexpected") - in - let get_loan_id (av : V.typed_avalue) : V.BorrowId.id = - match av.V.value with - | V.ALoan (V.AMutLoan (lid, _)) -> lid - | V.ALoan (V.ASharedLoan (lids, _, _)) -> V.BorrowId.Set.min_elt lids - | _ -> raise (Failure "Unexpected") - in - (* We use ordered maps to reorder the borrows and loans *) - let reorder (get_bid : V.typed_avalue -> V.BorrowId.id) - (values : V.typed_avalue list) : V.typed_avalue list = - List.map snd - (V.BorrowId.Map.bindings - (V.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 V.avalues } - in - - let reorder_in_abs (abs : V.abs) = - if V.AbstractionId.Set.mem abs.abs_id old_abs_ids then abs - else reorder_in_fresh_abs abs - in - - let env = C.env_map_abs reorder_in_abs ctx.env in - - { ctx with C.env } - -(** Destructure all the new abstractions *) -let destructure_new_abs (loop_id : V.LoopId.id) - (old_abs_ids : V.AbstractionId.Set.t) (ctx : C.eval_ctx) : C.eval_ctx = - let abs_kind = V.Loop (loop_id, None, V.LoopSynthInput) in - let can_end = true in - let destructure_shared_values = true in - let is_fresh_abs_id (id : V.AbstractionId.id) : bool = - not (V.AbstractionId.Set.mem id old_abs_ids) - in - let env = - C.env_map_abs - (fun abs -> - if is_fresh_abs_id abs.abs_id then - let abs = - destructure_abs abs_kind can_end destructure_shared_values ctx abs - in - abs - else abs) - ctx.env - in - { ctx with env } - -(** Decompose the shared avalues, so as not to have nested shared loans - inside of abstractions. - - For instance: - {[ - abs'0 { shared_aloan ls0 (true, shared_loan ls1 s0) _ } - - ~~> - - abs'0 { - shared_aloan ls0 (true, s0) _ - shared_aloan ls1 s1 _ - } - ]} - - Note that we decompose only in the "fresh" abstractions (this is controled - by the [old_aids] parameter). - - TODO: how to factorize with {!InterpreterBorrows.destructure_abs}? - *) -let decompose_shared_avalues (loop_id : V.LoopId.id) - (old_aids : V.AbstractionId.Set.t) (ctx : C.eval_ctx) : C.eval_ctx = - (* Decompose the shared avalues in a fresh abstraction. We only decompose - in fresh (i.e., non-fixed) abstractions: {!decompose_in_abs} below - ignores the fixed abstractions and delegates to this function *) - let decompose_in_fresh_abs (abs : V.abs) : V.abs = - let new_loans = ref [] in - (* When decomposing, we need to duplicate the symbolic values (we - simply introduce fresh ids) *) - let mk_value_with_fresh_sids (v : V.typed_value) : V.typed_value = - let visitor = - object - inherit [_] V.map_typed_avalue - method! visit_symbolic_value_id _ _ = C.fresh_symbolic_value_id () - end - in - visitor#visit_typed_value () v - in - (* Visit the values: whenever we enter a shared loan (which is necessarily - a strict subvalue of a shared aloan, because we only explore abstractions) - we move it outside (we remove the shared loan, register a shared value - to insert in the abstraction, and use fresh symbolic values in this new - shared value). - *) - let loan_visitor = - object (self : 'self) - inherit [_] V.map_typed_avalue as super - - method! visit_typed_value env v = - match v.V.value with - | V.Loan (V.SharedLoan (lids, sv)) -> - let sv = self#visit_typed_value env sv in - - (* Introduce fresh symbolic values for the new loan *) - let nsv = mk_value_with_fresh_sids sv in - new_loans := List.append !new_loans [ (lids, nsv) ]; - - (* Return *) - sv - | _ -> super#visit_typed_value env v - end - in - let rid = T.RegionId.Set.min_elt abs.regions in - - (* We registered new loans to add in the abstraction: actually create those - loans, and insert them in the abstraction *) - let mk_ashared_loan (lids : V.BorrowId.Set.t) (sv : V.typed_value) : - V.typed_avalue = - (* There shouldn't be nested borrows *) - let sv_ty = ety_no_regions_to_rty sv.V.ty in - let ty = T.Ref (T.Var rid, sv_ty, T.Shared) in - let child_av = mk_aignored sv_ty in - let value = V.ALoan (V.ASharedLoan (lids, sv, child_av)) in - { V.value; ty } - in - let avalues = - List.map - (fun av -> - new_loans := []; - let av = loan_visitor#visit_typed_avalue () av in - let new_loans = - List.map (fun (lids, sv) -> mk_ashared_loan lids sv) !new_loans - in - av :: List.rev new_loans) - abs.V.avalues - in - let avalues = List.concat avalues in - let abs_id = C.fresh_abstraction_id () in - let kind = V.Loop (loop_id, None, V.LoopSynthInput) in - let can_end = true in - let abs = { abs with V.abs_id; kind; can_end; avalues } in - abs - in - - (* Decompose only in the fresh abstractions *) - let decompose_in_abs abs = - if V.AbstractionId.Set.mem abs.V.abs_id old_aids then abs - else decompose_in_fresh_abs abs - in - - (* Apply in the environment *) - let env = C.env_map_abs decompose_in_abs ctx.env in - { ctx with C.env } - -(** Collapse an environment. - - We do this to simplify an environment, for the purpose of finding a loop - fixed point. - - We do the following: - - we look for all the *new* dummy values (we use sets of old ids to decide - wether a value is new or not) and convert them into abstractions - - whenever there is a new abstraction in the context, and some of its - its borrows are associated to loans in another new abstraction, we - merge them. - In effect, this allows us to merge newly introduced abstractions/borrows - with their parent abstractions. - - For instance, when evaluating the first loop iteration, we start in the - following environment: - {[ - abs@0 { ML l0 } - ls -> MB l0 (s2 : loops::List<T>) - i -> s1 : u32 - ]} - - and get the following environment upon reaching the [Continue] statement: - {[ - abs@0 { ML l0 } - ls -> MB l4 (s@6 : loops::List<T>) - i -> s@7 : u32 - _@1 -> MB l0 (loops::List::Cons (ML l1, ML l2)) - _@2 -> MB l2 (@Box (ML l4)) // tail - _@3 -> MB l1 (s@3 : T) // hd - ]} - - In this new environment, the dummy variables [_@1], [_@2] and [_@3] are - considered as new. - - We first convert the new dummy values to abstractions. It gives: - {[ - abs@0 { ML l0 } - ls -> MB l4 (s@6 : loops::List<T>) - i -> s@7 : u32 - abs@1 { MB l0, ML l1, ML l2 } - abs@2 { MB l2, ML l4 } - abs@3 { MB l1 } - ]} - - We finally merge the new abstractions together. It gives: - {[ - abs@0 { ML l0 } - ls -> MB l4 (s@6 : loops::List<T>) - i -> s@7 : u32 - abs@4 { MB l0, ML l4 } - ]} - - [merge_funs]: those are used to merge loans or borrows which appear in both - abstractions (rem.: here we mean that, for instance, both abstractions - contain a shared loan with id l0). - This can happen when merging environments (note that such environments are not well-formed - - they become well formed again after collapsing). - *) -let collapse_ctx (loop_id : V.LoopId.id) - (merge_funs : merge_duplicates_funcs option) (old_ids : ids_sets) - (ctx0 : C.eval_ctx) : C.eval_ctx = - (* Debug *) - log#ldebug - (lazy - ("collapse_ctx:\n\n- fixed_ids:\n" ^ show_ids_sets old_ids - ^ "\n\n- ctx0:\n" ^ eval_ctx_to_string ctx0 ^ "\n\n")); - - let abs_kind = V.Loop (loop_id, None, LoopSynthInput) in - let can_end = true in - let destructure_shared_values = true in - let is_fresh_abs_id (id : V.AbstractionId.id) : bool = - not (V.AbstractionId.Set.mem id old_ids.aids) - in - let is_fresh_did (id : C.DummyVarId.id) : bool = - not (C.DummyVarId.Set.mem id old_ids.dids) - in - (* Convert the dummy values to abstractions (note that when we convert - values to abstractions, the resulting abstraction should be destructured) *) - (* Note that we preserve the order of the dummy values: we replace them with - abstractions in place - this makes matching easier *) - let env = - List.concat - (List.map - (fun ee -> - match ee with - | C.Abs _ | C.Frame | C.Var (VarBinder _, _) -> [ ee ] - | C.Var (DummyBinder id, v) -> - if is_fresh_did id then - let absl = - convert_value_to_abstractions abs_kind can_end - destructure_shared_values ctx0 v - in - List.map (fun abs -> C.Abs abs) absl - else [ ee ]) - ctx0.env) - in - let ctx = { ctx0 with C.env } in - log#ldebug - (lazy - ("collapse_ctx: after converting values to abstractions:\n" - ^ show_ids_sets old_ids ^ "\n\n- ctx:\n" ^ eval_ctx_to_string ctx ^ "\n\n" - )); - - log#ldebug - (lazy - ("collapse_ctx: after decomposing the shared values in the abstractions:\n" - ^ show_ids_sets old_ids ^ "\n\n- ctx:\n" ^ eval_ctx_to_string ctx ^ "\n\n" - )); - - (* Explore all the *new* abstractions, and compute various maps *) - let explore (abs : V.abs) = is_fresh_abs_id abs.abs_id in - let ids_maps = - compute_abs_borrows_loans_maps (merge_funs = None) explore env - in - let { - abs_ids; - abs_to_borrows; - abs_to_loans = _; - abs_to_borrows_loans; - borrow_to_abs = _; - loan_to_abs; - borrow_loan_to_abs; - } = - ids_maps - in - - (* Change the merging behaviour depending on the input parameters *) - let abs_to_borrows, loan_to_abs = - if merge_funs <> None then (abs_to_borrows_loans, borrow_loan_to_abs) - else (abs_to_borrows, loan_to_abs) - in - - (* Merge the abstractions together *) - let merged_abs : V.AbstractionId.id UF.elem V.AbstractionId.Map.t = - V.AbstractionId.Map.of_list (List.map (fun id -> (id, UF.make id)) abs_ids) - in - - let ctx = ref ctx in - - (* Merge all the mergeable abs. - - We iterate over the abstractions, then over the borrows in the abstractions. - We do this because we want to control the order in which abstractions - are merged (the ids are iterated in increasing order). Otherwise, we - could simply iterate over all the borrows in [borrow_to_abs]... - *) - List.iter - (fun abs_id0 -> - let bids = V.AbstractionId.Map.find abs_id0 abs_to_borrows in - let bids = V.BorrowId.Set.elements bids in - List.iter - (fun bid -> - match V.BorrowId.Map.find_opt bid loan_to_abs with - | None -> (* Nothing to do *) () - | Some abs_ids1 -> - V.AbstractionId.Set.iter - (fun abs_id1 -> - (* We need to merge - unless we have already merged *) - (* First, find the representatives for the two abstractions (the - representative is the abstraction into which we merged) *) - let abs_ref0 = - UF.find (V.AbstractionId.Map.find abs_id0 merged_abs) - in - let abs_id0 = UF.get abs_ref0 in - let abs_ref1 = - UF.find (V.AbstractionId.Map.find abs_id1 merged_abs) - in - let abs_id1 = UF.get abs_ref1 in - (* If the two ids are the same, it means the abstractions were already merged *) - if abs_id0 = abs_id1 then () - else ( - (* We actually need to merge the abstractions *) - - (* Debug *) - log#ldebug - (lazy - ("collapse_ctx: merging abstraction " - ^ V.AbstractionId.to_string abs_id1 - ^ " into " - ^ V.AbstractionId.to_string abs_id0 - ^ ":\n\n" ^ eval_ctx_to_string !ctx)); - - (* Update the environment - pay attention to the order: we - we merge [abs_id1] *into* [abs_id0] *) - let nctx, abs_id = - merge_into_abstraction abs_kind can_end merge_funs !ctx - abs_id1 abs_id0 - in - ctx := nctx; - - (* Update the union find *) - let abs_ref_merged = UF.union abs_ref0 abs_ref1 in - UF.set abs_ref_merged abs_id)) - abs_ids1) - bids) - abs_ids; - - log#ldebug - (lazy - ("collapse_ctx:\n\n- fixed_ids:\n" ^ show_ids_sets old_ids - ^ "\n\n- after collapse:\n" ^ eval_ctx_to_string !ctx ^ "\n\n")); - - (* Reorder the loans and borrows in the fresh abstractions *) - let ctx = reorder_loans_borrows_in_fresh_abs old_ids.aids !ctx in - - log#ldebug - (lazy - ("collapse_ctx:\n\n- fixed_ids:\n" ^ show_ids_sets old_ids - ^ "\n\n- after collapse and reorder borrows/loans:\n" - ^ eval_ctx_to_string ctx ^ "\n\n")); - - (* Return the new context *) - ctx - -(** Match two types during a join. *) -let rec match_types (match_distinct_types : 'r T.ty -> 'r T.ty -> 'r T.ty) - (match_regions : 'r -> 'r -> 'r) (ty0 : 'r T.ty) (ty1 : 'r T.ty) : 'r T.ty = - let match_rec = match_types match_distinct_types match_regions in - match (ty0, ty1) with - | Adt (id0, regions0, tys0), Adt (id1, regions1, tys1) -> - assert (id0 = id1); - let id = id0 in - let regions = - List.map - (fun (id0, id1) -> match_regions id0 id1) - (List.combine regions0 regions1) - in - let tys = - List.map (fun (ty0, ty1) -> match_rec ty0 ty1) (List.combine tys0 tys1) - in - Adt (id, regions, tys) - | TypeVar vid0, TypeVar vid1 -> - assert (vid0 = vid1); - let vid = vid0 in - TypeVar vid - | Bool, Bool | Char, Char | Never, Never | Str, Str -> ty0 - | Integer int_ty0, Integer int_ty1 -> - assert (int_ty0 = int_ty1); - ty0 - | Array ty0, Array ty1 | Slice ty0, Slice ty1 -> match_rec ty0 ty1 - | Ref (r0, ty0, k0), Ref (r1, ty1, k1) -> - let r = match_regions r0 r1 in - let ty = match_rec ty0 ty1 in - assert (k0 = k1); - let k = k0 in - Ref (r, ty, k) - | _ -> match_distinct_types ty0 ty1 - -(** See {!Match}. - - This module contains specialized match functions to instantiate the generic - {!Match} functor. - *) -module type Matcher = sig - val match_etys : T.ety -> T.ety -> T.ety - val match_rtys : T.rty -> T.rty -> T.rty - - (** The input primitive values are not equal *) - val match_distinct_primitive_values : - T.ety -> V.primitive_value -> V.primitive_value -> V.typed_value - - (** The input ADTs don't have the same variant *) - val match_distinct_adts : T.ety -> V.adt_value -> V.adt_value -> V.typed_value - - (** The meta-value is the result of a match. - - We take an additional function as input, which acts as a matcher over - typed values, to be able to lookup the shared values and match them. - We do this for shared borrows (and not, e.g., mutable borrows) because - shared borrows introduce indirections, while mutable borrows carry the - borrowed values with them: we might want to explore and match those - borrowed values, in which case we have to manually look them up before - calling the match function. - *) - val match_shared_borrows : - (V.typed_value -> V.typed_value -> V.typed_value) -> - T.ety -> - V.borrow_id -> - V.borrow_id -> - V.borrow_id - - (** The input parameters are: - - [ty] - - [bid0]: first borrow id - - [bv0]: first borrowed value - - [bid1] - - [bv1] - - [bv]: the result of matching [bv0] with [bv1] - *) - val match_mut_borrows : - T.ety -> - V.borrow_id -> - V.typed_value -> - V.borrow_id -> - V.typed_value -> - V.typed_value -> - V.borrow_id * V.typed_value - - (** Parameters: - [ty] - [ids0] - [ids1] - [v]: the result of matching the shared values coming from the two loans - *) - val match_shared_loans : - T.ety -> - V.loan_id_set -> - V.loan_id_set -> - V.typed_value -> - V.loan_id_set * V.typed_value - - val match_mut_loans : T.ety -> V.loan_id -> V.loan_id -> V.loan_id - - (** There are no constraints on the input symbolic values *) - val match_symbolic_values : - V.symbolic_value -> V.symbolic_value -> V.symbolic_value - - (** Match a symbolic value with a value which is not symbolic. - - If the boolean is [true], it means the symbolic value comes from the - *left* environment. Otherwise it comes from the right environment (it - is important when throwing exceptions, for instance when we need to - end loans in one of the two environments). - *) - val match_symbolic_with_other : - bool -> V.symbolic_value -> V.typed_value -> V.typed_value - - (** Match a bottom value with a value which is not bottom. - - If the boolean is [true], it means the bottom value comes from the - *left* environment. Otherwise it comes from the right environment (it - is important when throwing exceptions, for instance when we need to - end loans in one of the two environments). - *) - val match_bottom_with_other : bool -> V.typed_value -> V.typed_value - - (** The input ADTs don't have the same variant *) - val match_distinct_aadts : - T.rty -> V.adt_avalue -> T.rty -> V.adt_avalue -> T.rty -> V.typed_avalue - - (** Parameters: - [ty0] - [bid0] - [ty1] - [bid1] - [ty]: result of matching ty0 and ty1 - *) - val match_ashared_borrows : - T.rty -> V.borrow_id -> T.rty -> V.borrow_id -> T.rty -> V.typed_avalue - - (** Parameters: - [ty0] - [bid0] - [av0] - [ty1] - [bid1] - [av1] - [ty]: result of matching ty0 and ty1 - [av]: result of matching av0 and av1 - *) - val match_amut_borrows : - T.rty -> - V.borrow_id -> - V.typed_avalue -> - T.rty -> - V.borrow_id -> - V.typed_avalue -> - T.rty -> - V.typed_avalue -> - V.typed_avalue - - (** Parameters: - [ty0] - [ids0] - [v0] - [av0] - [ty1] - [ids1] - [v1] - [av1] - [ty]: result of matching ty0 and ty1 - [v]: result of matching v0 and v1 - [av]: result of matching av0 and av1 - *) - val match_ashared_loans : - T.rty -> - V.loan_id_set -> - V.typed_value -> - V.typed_avalue -> - T.rty -> - V.loan_id_set -> - V.typed_value -> - V.typed_avalue -> - T.rty -> - V.typed_value -> - V.typed_avalue -> - V.typed_avalue - - (** Parameters: - [ty0] - [id0] - [av0] - [ty1] - [id1] - [av1] - [ty]: result of matching ty0 and ty1 - [av]: result of matching av0 and av1 - *) - val match_amut_loans : - T.rty -> - V.borrow_id -> - V.typed_avalue -> - T.rty -> - V.borrow_id -> - V.typed_avalue -> - T.rty -> - V.typed_avalue -> - V.typed_avalue - - (** Match two arbitrary avalues whose constructors don't match (this function - is typically used to raise the proper exception). - *) - val match_avalues : V.typed_avalue -> V.typed_avalue -> V.typed_avalue -end - -(** Generic functor to implement matching functions between values, environments, - etc. - - We use it for joins, to check if two environments are convertible, etc. - See for instance {!MakeJoinMatcher} and {!MakeCheckEquivMatcher}. - - The functor is parameterized by a {!Matcher}, which implements the - non-generic part of the match. More precisely, the role of {!Match} is two - provide generic functions which recursively match two values (by recursively - matching the fields of ADT values for instance). When it does need to match - values in a non-trivial manner (if two ADT values don't have the same - variant for instance) it calls the corresponding specialized function from - {!Matcher}. - *) -module Match (M : Matcher) = struct - (** Match two values. - - Rem.: this function raises exceptions of type {!ValueMatchFailure}. - *) - let rec match_typed_values (ctx : C.eval_ctx) (v0 : V.typed_value) - (v1 : V.typed_value) : V.typed_value = - let match_rec = match_typed_values ctx in - let ty = M.match_etys v0.V.ty v1.V.ty in - match (v0.V.value, v1.V.value) with - | V.Primitive pv0, V.Primitive pv1 -> - if pv0 = pv1 then v1 else M.match_distinct_primitive_values ty pv0 pv1 - | V.Adt av0, V.Adt av1 -> - if av0.variant_id = av1.variant_id then - let fields = List.combine av0.field_values av1.field_values in - let field_values = - List.map (fun (f0, f1) -> match_rec f0 f1) fields - in - let value : V.value = - V.Adt { variant_id = av0.variant_id; field_values } - in - { V.value; ty = v1.V.ty } - else ( - (* For now, we don't merge ADTs which contain borrows *) - assert (not (value_has_borrows ctx v0.V.value)); - assert (not (value_has_borrows ctx v1.V.value)); - (* Merge *) - M.match_distinct_adts ty av0 av1) - | Bottom, Bottom -> v0 - | Borrow bc0, Borrow bc1 -> - let bc = - match (bc0, bc1) with - | SharedBorrow bid0, SharedBorrow bid1 -> - let bid = M.match_shared_borrows match_rec ty bid0 bid1 in - V.SharedBorrow bid - | MutBorrow (bid0, bv0), MutBorrow (bid1, bv1) -> - let bv = match_rec bv0 bv1 in - assert (not (value_has_borrows ctx bv.V.value)); - let bid, bv = M.match_mut_borrows ty bid0 bv0 bid1 bv1 bv in - V.MutBorrow (bid, bv) - | ReservedMutBorrow _, _ - | _, ReservedMutBorrow _ - | SharedBorrow _, MutBorrow _ - | MutBorrow _, SharedBorrow _ -> - (* If we get here, either there is a typing inconsistency, or we are - trying to match a reserved borrow, which shouldn't happen because - reserved borrow should be eliminated very quickly - they are introduced - just before function calls which activate them *) - raise (Failure "Unexpected") - in - { V.value = V.Borrow bc; ty } - | Loan lc0, Loan lc1 -> - (* TODO: maybe we should enforce that the ids are always exactly the same - - without matching *) - let lc = - match (lc0, lc1) with - | SharedLoan (ids0, sv0), SharedLoan (ids1, sv1) -> - let sv = match_rec sv0 sv1 in - assert (not (value_has_borrows ctx sv.V.value)); - let ids, sv = M.match_shared_loans ty ids0 ids1 sv in - V.SharedLoan (ids, sv) - | MutLoan id0, MutLoan id1 -> - let id = M.match_mut_loans ty id0 id1 in - V.MutLoan id - | SharedLoan _, MutLoan _ | MutLoan _, SharedLoan _ -> - raise (Failure "Unreachable") - in - { V.value = Loan lc; ty = v1.V.ty } - | Symbolic sv0, Symbolic sv1 -> - (* For now, we force all the symbolic values containing borrows to - be eagerly expanded, and we don't support nested borrows *) - assert (not (value_has_borrows ctx v0.V.value)); - assert (not (value_has_borrows ctx v1.V.value)); - (* Match *) - let sv = M.match_symbolic_values sv0 sv1 in - { v1 with V.value = V.Symbolic sv } - | Loan lc, _ -> ( - match lc with - | SharedLoan (ids, _) -> raise (ValueMatchFailure (LoansInLeft ids)) - | MutLoan id -> raise (ValueMatchFailure (LoanInLeft id))) - | _, Loan lc -> ( - match lc with - | SharedLoan (ids, _) -> raise (ValueMatchFailure (LoansInRight ids)) - | MutLoan id -> raise (ValueMatchFailure (LoanInRight id))) - | Symbolic sv, _ -> M.match_symbolic_with_other true sv v1 - | _, Symbolic sv -> M.match_symbolic_with_other false sv v0 - | Bottom, _ -> M.match_bottom_with_other true v1 - | _, Bottom -> M.match_bottom_with_other false v0 - | _ -> - log#ldebug - (lazy - ("Unexpected match case:\n- value0: " - ^ typed_value_to_string ctx v0 - ^ "\n- value1: " - ^ typed_value_to_string ctx v1)); - raise (Failure "Unexpected match case") - - (** Match two avalues. - - Rem.: this function raises exceptions of type {!ValueMatchFailure}. - *) - and match_typed_avalues (ctx : C.eval_ctx) (v0 : V.typed_avalue) - (v1 : V.typed_avalue) : V.typed_avalue = - log#ldebug - (lazy - ("match_typed_avalues:\n- value0: " - ^ typed_avalue_to_string ctx v0 - ^ "\n- value1: " - ^ typed_avalue_to_string ctx v1)); - - let match_rec = match_typed_values ctx in - let match_arec = match_typed_avalues ctx in - let ty = M.match_rtys v0.V.ty v1.V.ty in - match (v0.V.value, v1.V.value) with - | V.AAdt av0, V.AAdt av1 -> - if av0.variant_id = av1.variant_id then - let fields = List.combine av0.field_values av1.field_values in - let field_values = - List.map (fun (f0, f1) -> match_arec f0 f1) fields - in - let value : V.avalue = - V.AAdt { variant_id = av0.variant_id; field_values } - in - { V.value; ty } - else (* Merge *) - M.match_distinct_aadts v0.V.ty av0 v1.V.ty av1 ty - | ABottom, ABottom -> mk_abottom ty - | AIgnored, AIgnored -> mk_aignored ty - | ABorrow bc0, ABorrow bc1 -> ( - log#ldebug (lazy "match_typed_avalues: borrows"); - match (bc0, bc1) with - | ASharedBorrow bid0, ASharedBorrow bid1 -> - log#ldebug (lazy "match_typed_avalues: shared borrows"); - M.match_ashared_borrows v0.V.ty bid0 v1.V.ty bid1 ty - | AMutBorrow (bid0, av0), AMutBorrow (bid1, av1) -> - log#ldebug (lazy "match_typed_avalues: mut borrows"); - log#ldebug - (lazy - "match_typed_avalues: mut borrows: matching children values"); - let av = match_arec av0 av1 in - log#ldebug - (lazy "match_typed_avalues: mut borrows: matched children values"); - M.match_amut_borrows v0.V.ty bid0 av0 v1.V.ty bid1 av1 ty av - | AIgnoredMutBorrow _, AIgnoredMutBorrow _ -> - (* The abstractions are destructured: we shouldn't get there *) - raise (Failure "Unexpected") - | AProjSharedBorrow asb0, AProjSharedBorrow asb1 -> ( - match (asb0, asb1) with - | [], [] -> - (* This case actually stands for ignored shared borrows, when - there are no nested borrows *) - v0 - | _ -> - (* We should get there only if there are nested borrows *) - raise (Failure "Unexpected")) - | _ -> - (* TODO: getting there is not necessarily inconsistent (it may - just be because the environments don't match) so we may want - to call a specific function (which could raise the proper - exception). - Rem.: we shouldn't get to the ended borrow cases, because - an abstraction should never contain ended borrows unless - we are *currently* ending it, in which case we need - to completely end it before continuing. - *) - raise (Failure "Unexpected")) - | ALoan lc0, ALoan lc1 -> ( - log#ldebug (lazy "match_typed_avalues: loans"); - (* TODO: maybe we should enforce that the ids are always exactly the same - - without matching *) - match (lc0, lc1) with - | ASharedLoan (ids0, sv0, av0), ASharedLoan (ids1, sv1, av1) -> - log#ldebug (lazy "match_typed_avalues: shared loans"); - let sv = match_rec sv0 sv1 in - let av = match_arec av0 av1 in - assert (not (value_has_borrows ctx sv.V.value)); - M.match_ashared_loans v0.V.ty ids0 sv0 av0 v1.V.ty ids1 sv1 av1 ty - sv av - | AMutLoan (id0, av0), AMutLoan (id1, av1) -> - log#ldebug (lazy "match_typed_avalues: mut loans"); - log#ldebug - (lazy "match_typed_avalues: mut loans: matching children values"); - let av = match_arec av0 av1 in - log#ldebug - (lazy "match_typed_avalues: mut loans: matched children values"); - M.match_amut_loans v0.V.ty id0 av0 v1.V.ty id1 av1 ty av - | AIgnoredMutLoan _, AIgnoredMutLoan _ - | AIgnoredSharedLoan _, AIgnoredSharedLoan _ -> - (* Those should have been filtered when destructuring the abstractions - - they are necessary only when there are nested borrows *) - raise (Failure "Unreachable") - | _ -> raise (Failure "Unreachable")) - | ASymbolic _, ASymbolic _ -> - (* For now, we force all the symbolic values containing borrows to - be eagerly expanded, and we don't support nested borrows *) - raise (Failure "Unreachable") - | _ -> M.match_avalues v0 v1 -end - -(* Very annoying: functors only take modules as inputs... *) -module type MatchJoinState = sig - (** The current context *) - val ctx : C.eval_ctx - - (** The current loop *) - val loop_id : V.LoopId.id - - (** The abstractions introduced when performing the matches *) - val nabs : V.abs list ref -end - -(** A matcher for joins (we use joins to compute loop fixed points). - - See the explanations for {!Match}. - - In case of loop joins: - - we match *concrete* values - - we check that the "fixed" abstractions (the abstractions to be framed - - i.e., the abstractions which are the same in the two environments to - join) are equal - - we keep the abstractions which are not in the frame, then merge those - together (if they have borrows/loans in common) later - The join matcher is used to match the *concrete* values only. For this - reason, we fail on the functions which match avalues. - *) -module MakeJoinMatcher (S : MatchJoinState) : Matcher = struct - (** Small utility *) - let push_abs (abs : V.abs) : unit = S.nabs := abs :: !S.nabs - - let push_absl (absl : V.abs list) : unit = List.iter push_abs absl - - let match_etys ty0 ty1 = - assert (ty0 = ty1); - ty0 - - let match_rtys ty0 ty1 = - (* The types must be equal - in effect, this forbids to match symbolic - values containing borrows *) - assert (ty0 = ty1); - ty0 - - let match_distinct_primitive_values (ty : T.ety) (_ : V.primitive_value) - (_ : V.primitive_value) : V.typed_value = - mk_fresh_symbolic_typed_value_from_ety V.LoopJoin ty - - let match_distinct_adts (ty : T.ety) (adt0 : V.adt_value) (adt1 : V.adt_value) - : V.typed_value = - (* Check that the ADTs don't contain borrows - this is redundant with checks - performed by the caller, but we prefer to be safe with regards to future - updates - *) - let check_no_borrows (v : V.typed_value) = - assert (not (value_has_borrows S.ctx v.V.value)) - in - List.iter check_no_borrows adt0.field_values; - List.iter check_no_borrows adt1.field_values; - - (* Check if there are loans: we request to end them *) - let check_loans (left : bool) (fields : V.typed_value list) : unit = - match InterpreterBorrowsCore.get_first_loan_in_values fields with - | Some (V.SharedLoan (ids, _)) -> - if left then raise (ValueMatchFailure (LoansInLeft ids)) - else raise (ValueMatchFailure (LoansInRight ids)) - | Some (V.MutLoan id) -> - if left then raise (ValueMatchFailure (LoanInLeft id)) - else raise (ValueMatchFailure (LoanInRight id)) - | None -> () - in - check_loans true adt0.field_values; - check_loans false adt1.field_values; - - (* No borrows, no loans: we can introduce a symbolic value *) - mk_fresh_symbolic_typed_value_from_ety V.LoopJoin ty - - let match_shared_borrows _ (ty : T.ety) (bid0 : V.borrow_id) - (bid1 : V.borrow_id) : V.borrow_id = - if bid0 = bid1 then bid0 - else - (* We replace bid0 and bid1 with a fresh borrow id, and introduce - an abstraction which links all of them: - {[ - { SB bid0, SB bid1, SL {bid2} } - ]} - *) - let rid = C.fresh_region_id () in - let bid2 = C.fresh_borrow_id () in - - (* Generate a fresh symbolic value for the shared value *) - let _, bv_ty, kind = ty_as_ref ty in - let sv = mk_fresh_symbolic_typed_value_from_ety V.LoopJoin bv_ty in - - let borrow_ty = - mk_ref_ty (T.Var rid) (ety_no_regions_to_rty bv_ty) kind - in - - (* Generate the avalues for the abstraction *) - let mk_aborrow (bid : V.borrow_id) : V.typed_avalue = - let value = V.ABorrow (V.ASharedBorrow bid) in - { V.value; ty = borrow_ty } - in - let borrows = [ mk_aborrow bid0; mk_aborrow bid1 ] in - - let loan = - V.ASharedLoan - ( V.BorrowId.Set.singleton bid2, - sv, - mk_aignored (ety_no_regions_to_rty bv_ty) ) - in - (* Note that an aloan has a borrow type *) - let loan = { V.value = V.ALoan loan; ty = borrow_ty } in - - let avalues = List.append borrows [ loan ] in - - (* Generate the abstraction *) - let abs = - { - V.abs_id = C.fresh_abstraction_id (); - kind = V.Loop (S.loop_id, None, LoopSynthInput); - can_end = true; - parents = V.AbstractionId.Set.empty; - original_parents = []; - regions = T.RegionId.Set.singleton rid; - ancestors_regions = T.RegionId.Set.empty; - avalues; - } - in - push_abs abs; - - (* Return the new borrow *) - bid2 - - let match_mut_borrows (ty : T.ety) (bid0 : V.borrow_id) (bv0 : V.typed_value) - (bid1 : V.borrow_id) (bv1 : V.typed_value) (bv : V.typed_value) : - V.borrow_id * V.typed_value = - if bid0 = bid1 then ( - (* If the merged value is not the same as the original value, we introduce - an abstraction: - - {[ - { MB bid0, ML nbid } // where nbid fresh - ]} - - and we use bid' for the borrow id that we return. - - We do this because we want to make sure that, whenever a mutably - borrowed value is modified in a loop iteration, then there is - a fresh abstraction between this borrowed value and the fixed - abstractions. - *) - assert (not (value_has_borrows S.ctx bv.V.value)); - - if bv0 = bv1 then ( - assert (bv0 = bv); - (bid0, bv)) - else - let rid = C.fresh_region_id () in - let nbid = C.fresh_borrow_id () in - - let kind = T.Mut in - let bv_ty = ety_no_regions_to_rty bv.V.ty in - let borrow_ty = mk_ref_ty (T.Var rid) bv_ty kind in - - let borrow_av = - let ty = borrow_ty in - let value = V.ABorrow (V.AMutBorrow (bid0, mk_aignored bv_ty)) in - mk_typed_avalue ty value - in - - let loan_av = - let ty = borrow_ty in - let value = V.ALoan (V.AMutLoan (nbid, mk_aignored bv_ty)) in - mk_typed_avalue ty value - in - - let avalues = [ borrow_av; loan_av ] in - - (* Generate the abstraction *) - let abs = - { - V.abs_id = C.fresh_abstraction_id (); - kind = V.Loop (S.loop_id, None, LoopSynthInput); - can_end = true; - parents = V.AbstractionId.Set.empty; - original_parents = []; - regions = T.RegionId.Set.singleton rid; - ancestors_regions = T.RegionId.Set.empty; - avalues; - } - in - push_abs abs; - - (* Return the new borrow *) - (nbid, bv)) - else - (* We replace bid0 and bid1 with a fresh borrow id, and introduce - an abstraction which links all of them: - {[ - { MB bid0, MB bid1, ML bid2 } - ]} - *) - let rid = C.fresh_region_id () in - let bid2 = C.fresh_borrow_id () in - - (* Generate a fresh symbolic value for the borrowed value *) - let _, bv_ty, kind = ty_as_ref ty in - let sv = mk_fresh_symbolic_typed_value_from_ety V.LoopJoin bv_ty in - - let borrow_ty = - mk_ref_ty (T.Var rid) (ety_no_regions_to_rty bv_ty) kind - in - - (* Generate the avalues for the abstraction *) - let mk_aborrow (bid : V.borrow_id) (bv : V.typed_value) : V.typed_avalue = - let bv_ty = ety_no_regions_to_rty bv.V.ty in - let value = V.ABorrow (V.AMutBorrow (bid, mk_aignored bv_ty)) in - { V.value; ty = borrow_ty } - in - let borrows = [ mk_aborrow bid0 bv0; mk_aborrow bid1 bv1 ] in - - let loan = V.AMutLoan (bid2, mk_aignored (ety_no_regions_to_rty bv_ty)) in - (* Note that an aloan has a borrow type *) - let loan = { V.value = V.ALoan loan; ty = borrow_ty } in - - let avalues = List.append borrows [ loan ] in - - (* Generate the abstraction *) - let abs = - { - V.abs_id = C.fresh_abstraction_id (); - kind = V.Loop (S.loop_id, None, LoopSynthInput); - can_end = true; - parents = V.AbstractionId.Set.empty; - original_parents = []; - regions = T.RegionId.Set.singleton rid; - ancestors_regions = T.RegionId.Set.empty; - avalues; - } - in - push_abs abs; - - (* Return the new borrow *) - (bid2, sv) - - let match_shared_loans (_ : T.ety) (ids0 : V.loan_id_set) - (ids1 : V.loan_id_set) (sv : V.typed_value) : - V.loan_id_set * V.typed_value = - (* Check if the ids are the same - Rem.: we forbid the sets of loans - to be different. However, if we dive inside data-structures (by - using a shared borrow) the shared values might themselves contain - shared loans, which need to be matched. For this reason, we destructure - the shared values (see {!destructure_abs}). - *) - let extra_ids_left = V.BorrowId.Set.diff ids0 ids1 in - let extra_ids_right = V.BorrowId.Set.diff ids1 ids0 in - if not (V.BorrowId.Set.is_empty extra_ids_left) then - raise (ValueMatchFailure (LoansInLeft extra_ids_left)); - if not (V.BorrowId.Set.is_empty extra_ids_right) then - raise (ValueMatchFailure (LoansInRight extra_ids_right)); - - (* This should always be true if we get here *) - assert (ids0 = ids1); - let ids = ids0 in - - (* Return *) - (ids, sv) - - let match_mut_loans (_ : T.ety) (id0 : V.loan_id) (id1 : V.loan_id) : - V.loan_id = - if id0 = id1 then id0 - else - (* We forbid this case for now: if we get there, we force to end - both borrows *) - raise (ValueMatchFailure (LoanInLeft id0)) - - let match_symbolic_values (sv0 : V.symbolic_value) (sv1 : V.symbolic_value) : - V.symbolic_value = - let id0 = sv0.sv_id in - let id1 = sv1.sv_id in - if id0 = id1 then ( - (* Sanity check *) - assert (sv0 = sv1); - (* Return *) - sv0) - else ( - (* The caller should have checked that the symbolic values don't contain - borrows *) - assert (not (ty_has_borrows S.ctx.type_context.type_infos sv0.sv_ty)); - (* We simply introduce a fresh symbolic value *) - mk_fresh_symbolic_value V.LoopJoin sv0.sv_ty) - - let match_symbolic_with_other (left : bool) (sv : V.symbolic_value) - (v : V.typed_value) : V.typed_value = - (* Check that: - - there are no borrows in the symbolic value - - there are no borrows in the "regular" value - If there are loans in the regular value, raise an exception. - *) - assert (not (ty_has_borrows S.ctx.type_context.type_infos sv.sv_ty)); - assert (not (value_has_borrows S.ctx v.V.value)); - let value_is_left = not left in - (match InterpreterBorrowsCore.get_first_loan_in_value v with - | None -> () - | Some (SharedLoan (ids, _)) -> - if value_is_left then raise (ValueMatchFailure (LoansInLeft ids)) - else raise (ValueMatchFailure (LoansInRight ids)) - | Some (MutLoan id) -> - if value_is_left then raise (ValueMatchFailure (LoanInLeft id)) - else raise (ValueMatchFailure (LoanInRight id))); - (* Return a fresh symbolic value *) - mk_fresh_symbolic_typed_value V.LoopJoin sv.sv_ty - - let match_bottom_with_other (left : bool) (v : V.typed_value) : V.typed_value - = - (* If there are outer loans in the non-bottom value, raise an exception. - Otherwise, convert it to an abstraction and return [Bottom]. - *) - let with_borrows = false in - let value_is_left = not left in - match - InterpreterBorrowsCore.get_first_outer_loan_or_borrow_in_value - with_borrows v - with - | Some (BorrowContent _) -> raise (Failure "Unreachable") - | Some (LoanContent lc) -> ( - match lc with - | V.SharedLoan (ids, _) -> - if value_is_left then raise (ValueMatchFailure (LoansInLeft ids)) - else raise (ValueMatchFailure (LoansInRight ids)) - | V.MutLoan id -> - if value_is_left then raise (ValueMatchFailure (LoanInLeft id)) - else raise (ValueMatchFailure (LoanInRight id))) - | None -> - (* Convert the value to an abstraction *) - let abs_kind = V.Loop (S.loop_id, None, LoopSynthInput) in - let can_end = true in - let destructure_shared_values = true in - let absl = - convert_value_to_abstractions abs_kind can_end - destructure_shared_values S.ctx v - in - push_absl absl; - (* Return [Bottom] *) - mk_bottom v.V.ty - - (* As explained in comments: we don't use the join matcher to join avalues, - only concrete values *) - - let match_distinct_aadts _ _ _ _ _ = raise (Failure "Unreachable") - let match_ashared_borrows _ _ _ _ = raise (Failure "Unreachable") - let match_amut_borrows _ _ _ _ _ _ _ _ = raise (Failure "Unreachable") - let match_ashared_loans _ _ _ _ _ _ _ _ _ _ _ = raise (Failure "Unreachable") - let match_amut_loans _ _ _ _ _ _ _ _ = raise (Failure "Unreachable") - let match_avalues _ _ = raise (Failure "Unreachable") -end - -let mk_collapse_ctx_merge_duplicate_funs (loop_id : V.LoopId.id) - (ctx : C.eval_ctx) : merge_duplicates_funcs = - (* Rem.: the merge functions raise exceptions (that we catch). *) - let module S : MatchJoinState = struct - let ctx = ctx - let loop_id = loop_id - let nabs = ref [] - end in - let module JM = MakeJoinMatcher (S) in - let module M = Match (JM) in - (* Functions to match avalues (see {!merge_duplicates_funcs}). - - Those functions are used to merge borrows/loans with the *same ids*. - - They will always be called on destructured avalues (whose children are - [AIgnored] - we enforce that through sanity checks). We rely on the join - matcher [JM] to match the concrete values (for shared loans for instance). - Note that the join matcher doesn't implement match functions for avalues - (see the comments in {!MakeJoinMatcher}. - *) - let merge_amut_borrows id ty0 child0 _ty1 child1 = - (* Sanity checks *) - assert (is_aignored child0.V.value); - assert (is_aignored child1.V.value); - - (* We need to pick a type for the avalue. The types on the left and on the - right may use different regions: it doesn't really matter (here, we pick - the one from the left), because we will merge those regions together - anyway (see the comments for {!merge_into_abstraction}). - *) - let ty = ty0 in - let child = child0 in - let value = V.ABorrow (V.AMutBorrow (id, child)) in - { V.value; ty } - in - - let merge_ashared_borrows id ty0 ty1 = - (* Sanity checks *) - let _ = - let _, ty0, _ = ty_as_ref ty0 in - let _, ty1, _ = ty_as_ref ty1 in - assert (not (ty_has_borrows ctx.type_context.type_infos ty0)); - assert (not (ty_has_borrows ctx.type_context.type_infos ty1)) - in - - (* Same remarks as for [merge_amut_borrows] *) - let ty = ty0 in - let value = V.ABorrow (V.ASharedBorrow id) in - { V.value; ty } - in - - let merge_amut_loans id ty0 child0 _ty1 child1 = - (* Sanity checks *) - assert (is_aignored child0.V.value); - assert (is_aignored child1.V.value); - (* Same remarks as for [merge_amut_borrows] *) - let ty = ty0 in - let child = child0 in - let value = V.ALoan (V.AMutLoan (id, child)) in - { V.value; ty } - in - let merge_ashared_loans ids ty0 (sv0 : V.typed_value) child0 _ty1 - (sv1 : V.typed_value) child1 = - (* Sanity checks *) - assert (is_aignored child0.V.value); - assert (is_aignored child1.V.value); - (* Same remarks as for [merge_amut_borrows]. - - This time we need to also merge the shared values. We rely on the - join matcher [JM] to do so. - *) - assert (not (value_has_loans_or_borrows ctx sv0.V.value)); - assert (not (value_has_loans_or_borrows ctx sv1.V.value)); - let ty = ty0 in - let child = child0 in - let sv = M.match_typed_values ctx sv0 sv1 in - let value = V.ALoan (V.ASharedLoan (ids, sv, child)) in - { V.value; ty } - in - { - merge_amut_borrows; - merge_ashared_borrows; - merge_amut_loans; - merge_ashared_loans; - } - -let merge_into_abstraction (loop_id : V.LoopId.id) (abs_kind : V.abs_kind) - (can_end : bool) (ctx : C.eval_ctx) (aid0 : V.AbstractionId.id) - (aid1 : V.AbstractionId.id) : C.eval_ctx * V.AbstractionId.id = - let merge_funs = mk_collapse_ctx_merge_duplicate_funs loop_id ctx in - merge_into_abstraction abs_kind can_end (Some merge_funs) ctx aid0 aid1 - -(** Collapse an environment, merging the duplicated borrows/loans. - - This function simply calls {!collapse_ctx} with the proper merging functions. - - We do this because when we join environments, we may introduce duplicated - loans and borrows. See the explanations for {!join_ctxs}. - *) -let collapse_ctx_with_merge (loop_id : V.LoopId.id) (old_ids : ids_sets) - (ctx : C.eval_ctx) : C.eval_ctx = - let merge_funs = mk_collapse_ctx_merge_duplicate_funs loop_id ctx in - try collapse_ctx loop_id (Some merge_funs) old_ids ctx - with ValueMatchFailure _ -> raise (Failure "Unexpected") - -(** Join two contexts. - - We use this to join the environments at loop (re-)entry to progressively - compute a fixed point. - - We make the hypothesis (and check it) that the environments have the same - prefixes (same variable ids, same abstractions, etc.). The prefix of - variable and abstraction ids is given by the [fixed_ids] identifier sets. We - check that those prefixes are the same (the dummy variables are the same, - the abstractions are the same), match the values mapped to by the variables - which are not dummy, then group the additional dummy variables/abstractions - together. In a sense, the [fixed_ids] define a frame (in a separation logic - sense). - - Note that when joining the values mapped to by the non-dummy variables, we - may introduce duplicated borrows. Also, we don't match the abstractions - which are not in the prefix, and this can also lead to borrow - duplications. For this reason, the environment needs to be collapsed - afterwards to get rid of those duplicated loans/borrows. - - For instance, if we have: - {[ - fixed = { abs0 } - - env0 = { - abs0 { ML l0 } - l -> MB l0 s0 - } - - env1 = { - abs0 { ML l0 } - l -> MB l1 s1 - abs1 { MB l0, ML l1 } - } - ]} - - We get: - {[ - join env0 env1 = { - abs0 { ML l0 } (* abs0 is fixed: we simply check it is equal in env0 and env1 *) - l -> MB l2 s2 - abs1 { MB l0, ML l1 } (* abs1 is new: we keep it unchanged *) - abs2 { MB l0, MB l1, ML l2 } (* Introduced when joining on the "l" variable *) - } - ]} - - Rem.: in practice, this join works because we take care of pushing new values - and abstractions *at the end* of the environments, meaning the environment - prefixes keep the same structure. - - Rem.: assuming that the environment has some structure poses *no soundness - issue*. It can only make the join fail if the environments actually don't have - this structure: this is a *completeness issue*. - *) -let join_ctxs (loop_id : V.LoopId.id) (fixed_ids : ids_sets) (ctx0 : C.eval_ctx) - (ctx1 : C.eval_ctx) : ctx_or_update = - (* Debug *) - log#ldebug - (lazy - ("join_ctxs:\n\n- fixed_ids:\n" ^ show_ids_sets fixed_ids - ^ "\n\n- ctx0:\n" - ^ eval_ctx_to_string_no_filter ctx0 - ^ "\n\n- ctx1:\n" - ^ eval_ctx_to_string_no_filter ctx1 - ^ "\n\n")); - - let env0 = List.rev ctx0.env in - let env1 = List.rev ctx1.env in - - (* We need to pick a context for some functions like [match_typed_values]: - the context is only used to lookup module data, so we can pick whichever - we want. - TODO: this is not very clean. Maybe we should just carry this data around. - *) - let ctx = ctx0 in - - let nabs = ref [] in - - (* Explore the environments. *) - let join_suffixes (env0 : C.env) (env1 : C.env) : C.env = - (* Debug *) - log#ldebug - (lazy - ("join_suffixes:\n\n- fixed_ids:\n" ^ show_ids_sets fixed_ids - ^ "\n\n- ctx0:\n" - ^ eval_ctx_to_string_no_filter { ctx0 with env = List.rev env0 } - ^ "\n\n- ctx1:\n" - ^ eval_ctx_to_string_no_filter { ctx1 with env = List.rev env1 } - ^ "\n\n")); - - (* Sanity check: there are no values/abstractions which should be in the prefix *) - let check_valid (ee : C.env_elem) : unit = - match ee with - | C.Var (C.VarBinder _, _) -> - (* Variables are necessarily in the prefix *) - raise (Failure "Unreachable") - | Var (C.DummyBinder did, _) -> - assert (not (C.DummyVarId.Set.mem did fixed_ids.dids)) - | Abs abs -> - assert (not (V.AbstractionId.Set.mem abs.abs_id fixed_ids.aids)) - | Frame -> - (* This should have been eliminated *) - raise (Failure "Unreachable") - in - List.iter check_valid env0; - List.iter check_valid env1; - (* Concatenate the suffixes and append the abstractions introduced while - joining the prefixes *) - let absl = List.map (fun abs -> C.Abs abs) (List.rev !nabs) in - List.concat [ env0; env1; absl ] - in - - let module S : MatchJoinState = struct - (* The context is only used to lookup module data: we can pick whichever we want *) - let ctx = ctx - let loop_id = loop_id - let nabs = nabs - end in - let module JM = MakeJoinMatcher (S) in - let module M = Match (JM) in - (* Rem.: this function raises exceptions *) - let rec join_prefixes (env0 : C.env) (env1 : C.env) : C.env = - match (env0, env1) with - | ( (C.Var (C.DummyBinder b0, v0) as var0) :: env0', - (C.Var (C.DummyBinder b1, v1) as var1) :: env1' ) -> - (* Debug *) - log#ldebug - (lazy - ("join_prefixes: DummyBinders:\n\n- fixed_ids:\n" ^ "\n" - ^ show_ids_sets fixed_ids ^ "\n\n- value0:\n" - ^ env_elem_to_string ctx var0 - ^ "\n\n- value1:\n" - ^ env_elem_to_string ctx var1 - ^ "\n\n")); - - (* Two cases: the dummy value is an old value, in which case the bindings - must be the same and we must join their values. Otherwise, it means we - are not in the prefix anymore *) - if C.DummyVarId.Set.mem b0 fixed_ids.dids then ( - (* Still in the prefix: match the values *) - assert (b0 = b1); - let b = b0 in - let v = M.match_typed_values ctx v0 v1 in - let var = C.Var (C.DummyBinder b, v) in - (* Continue *) - var :: join_prefixes env0' env1') - else (* Not in the prefix anymore *) - join_suffixes env0 env1 - | ( (C.Var (C.VarBinder b0, v0) as var0) :: env0', - (C.Var (C.VarBinder b1, v1) as var1) :: env1' ) -> - (* Debug *) - log#ldebug - (lazy - ("join_prefixes: VarBinders:\n\n- fixed_ids:\n" ^ "\n" - ^ show_ids_sets fixed_ids ^ "\n\n- value0:\n" - ^ env_elem_to_string ctx var0 - ^ "\n\n- value1:\n" - ^ env_elem_to_string ctx var1 - ^ "\n\n")); - - (* Variable bindings *must* be in the prefix and consequently their - ids must be the same *) - assert (b0 = b1); - (* Match the values *) - let b = b0 in - let v = M.match_typed_values ctx v0 v1 in - let var = C.Var (C.VarBinder b, v) in - (* Continue *) - var :: join_prefixes env0' env1' - | (C.Abs abs0 as abs) :: env0', C.Abs abs1 :: env1' -> - (* Debug *) - log#ldebug - (lazy - ("join_prefixes: Abs:\n\n- fixed_ids:\n" ^ "\n" - ^ show_ids_sets fixed_ids ^ "\n\n- abs0:\n" ^ abs_to_string ctx abs0 - ^ "\n\n- abs1:\n" ^ abs_to_string ctx abs1 ^ "\n\n")); - - (* Same as for the dummy values: there are two cases *) - if V.AbstractionId.Set.mem abs0.abs_id fixed_ids.aids then ( - (* Still in the prefix: the abstractions must be the same *) - assert (abs0 = abs1); - (* Continue *) - abs :: join_prefixes env0' env1') - else (* Not in the prefix anymore *) - join_suffixes env0 env1 - | _ -> - (* The elements don't match: we are not in the prefix anymore *) - join_suffixes env0 env1 - in - - try - (* Remove the frame delimiter (the first element of an environment is a frame delimiter) *) - let env0, env1 = - match (env0, env1) with - | C.Frame :: env0, C.Frame :: env1 -> (env0, env1) - | _ -> raise (Failure "Unreachable") - in - - log#ldebug - (lazy - ("- env0:\n" ^ C.show_env env0 ^ "\n\n- env1:\n" ^ C.show_env env1 - ^ "\n\n")); - - let env = List.rev (C.Frame :: join_prefixes env0 env1) in - - (* Construct the joined context - of course, the type, fun, etc. contexts - * should be the same in the two contexts *) - let { - C.type_context; - fun_context; - global_context; - region_groups; - type_vars; - env = _; - ended_regions = ended_regions0; - } = - ctx0 - in - let { - C.type_context = _; - fun_context = _; - global_context = _; - region_groups = _; - type_vars = _; - env = _; - ended_regions = ended_regions1; - } = - ctx1 - in - let ended_regions = T.RegionId.Set.union ended_regions0 ended_regions1 in - Ok - { - C.type_context; - fun_context; - global_context; - region_groups; - type_vars; - env; - ended_regions; - } - with ValueMatchFailure e -> Error e - -(** See {!MakeCheckEquivMatcher}. - - Very annoying: functors only take modules as inputs... - *) -module type MatchCheckEquivState = sig - (** [true] if we check equivalence between contexts, [false] if we match - a source context with a target context. *) - val check_equiv : bool - - val ctx : C.eval_ctx - val rid_map : T.RegionId.InjSubst.t ref - - (** Substitution for the loan and borrow ids - used only if [check_equiv] is true *) - val blid_map : V.BorrowId.InjSubst.t ref - - (** Substitution for the borrow ids - used only if [check_equiv] is false *) - val borrow_id_map : V.BorrowId.InjSubst.t ref - - (** Substitution for the loans ids - used only if [check_equiv] is false *) - val loan_id_map : V.BorrowId.InjSubst.t ref - - val sid_map : V.SymbolicValueId.InjSubst.t ref - val sid_to_value_map : V.typed_value V.SymbolicValueId.Map.t ref - val aid_map : V.AbstractionId.InjSubst.t ref - val lookup_shared_value_in_ctx0 : V.BorrowId.id -> V.typed_value - val lookup_shared_value_in_ctx1 : V.BorrowId.id -> V.typed_value -end - -(** An auxiliary matcher that we use for two purposes: - - to check if two contexts are equivalent modulo id substitution (i.e., - alpha equivalent) (see {!ctxs_are_equivalent}). - - to compute a mapping between the borrows and the symbolic values in a - target context to the values and borrows in a source context (see - {!match_ctx_with_target}). - - TODO: rename - *) -module MakeCheckEquivMatcher (S : MatchCheckEquivState) = struct - module MkGetSetM (Id : Identifiers.Id) = struct - module Inj = Id.InjSubst - - let add (msg : string) (m : Inj.t ref) (k0 : Id.id) (k1 : Id.id) = - (* Check if k0 is already registered as a key *) - match Inj.find_opt k0 !m with - | None -> - (* Not registered: check if k1 is in the set of values, - otherwise add the binding *) - if Inj.Set.mem k1 (Inj.elements !m) then - raise - (Distinct - (msg ^ "adding [k0=" ^ Id.to_string k0 ^ " -> k1=" - ^ Id.to_string k1 ^ " ]: k1 already in the set of elements")) - else ( - m := Inj.add k0 k1 !m; - k1) - | Some k1' -> - (* It is: check that the bindings are consistent *) - if k1 <> k1' then raise (Distinct (msg ^ "already a binding for k0")) - else k1 - - let match_e (msg : string) (m : Inj.t ref) (k0 : Id.id) (k1 : Id.id) : Id.id - = - (* TODO: merge the add and merge functions *) - add msg m k0 k1 - - let match_el (msg : string) (m : Inj.t ref) (kl0 : Id.id list) - (kl1 : Id.id list) : Id.id list = - List.map (fun (k0, k1) -> match_e msg m k0 k1) (List.combine kl0 kl1) - - (** Figuring out mappings between sets of ids is hard in all generality... - We use the fact that the fresh ids should have been generated - the same way (i.e., in the same order) and match the ids two by - two in increasing order. - *) - let match_es (msg : string) (m : Inj.t ref) (ks0 : Id.Set.t) - (ks1 : Id.Set.t) : Id.Set.t = - Id.Set.of_list - (match_el msg m (Id.Set.elements ks0) (Id.Set.elements ks1)) - end - - module GetSetRid = MkGetSetM (T.RegionId) - - let match_rid = GetSetRid.match_e "match_rid: " S.rid_map - - (* let match_ridl = GetSetRid.match_el S.rid_map *) - let match_rids = GetSetRid.match_es "match_rids: " S.rid_map - - module GetSetBid = MkGetSetM (V.BorrowId) - - let match_blid msg = GetSetBid.match_e msg S.blid_map - let match_blidl msg = GetSetBid.match_el msg S.blid_map - let match_blids msg = GetSetBid.match_es msg S.blid_map - - let match_borrow_id = - if S.check_equiv then match_blid "match_borrow_id: " - else GetSetBid.match_e "match_borrow_id: " S.borrow_id_map - - let match_borrow_idl = - if S.check_equiv then match_blidl "match_borrow_idl: " - else GetSetBid.match_el "match_borrow_idl: " S.borrow_id_map - - let match_borrow_ids = - if S.check_equiv then match_blids "match_borrow_ids: " - else GetSetBid.match_es "match_borrow_ids: " S.borrow_id_map - - let match_loan_id = - if S.check_equiv then match_blid "match_loan_id: " - else GetSetBid.match_e "match_loan_id: " S.loan_id_map - - let match_loan_idl = - if S.check_equiv then match_blidl "match_loan_idl: " - else GetSetBid.match_el "match_loan_idl: " S.loan_id_map - - let match_loan_ids = - if S.check_equiv then match_blids "match_loan_ids: " - else GetSetBid.match_es "match_loan_ids: " S.loan_id_map - - module GetSetSid = MkGetSetM (V.SymbolicValueId) - module GetSetAid = MkGetSetM (V.AbstractionId) - - let match_aid = GetSetAid.match_e "match_aid: " S.aid_map - let match_aidl = GetSetAid.match_el "match_aidl: " S.aid_map - let match_aids = GetSetAid.match_es "match_aids: " S.aid_map - - (** *) - let match_etys ty0 ty1 = - if ty0 <> ty1 then raise (Distinct "match_etys") else ty0 - - let match_rtys ty0 ty1 = - let match_distinct_types _ _ = raise (Distinct "match_rtys") in - let match_regions r0 r1 = - match (r0, r1) with - | T.Static, T.Static -> r1 - | Var rid0, Var rid1 -> - let rid = match_rid rid0 rid1 in - Var rid - | _ -> raise (Distinct "match_rtys") - in - match_types match_distinct_types match_regions ty0 ty1 - - let match_distinct_primitive_values (ty : T.ety) (_ : V.primitive_value) - (_ : V.primitive_value) : V.typed_value = - mk_fresh_symbolic_typed_value_from_ety V.LoopJoin ty - - let match_distinct_adts (_ty : T.ety) (_adt0 : V.adt_value) - (_adt1 : V.adt_value) : V.typed_value = - raise (Distinct "match_distinct_adts") - - let match_shared_borrows - (match_typed_values : V.typed_value -> V.typed_value -> V.typed_value) - (_ty : T.ety) (bid0 : V.borrow_id) (bid1 : V.borrow_id) : V.borrow_id = - log#ldebug - (lazy - ("MakeCheckEquivMatcher: match_shared_borrows: " ^ "bid0: " - ^ V.BorrowId.to_string bid0 ^ ", bid1: " ^ V.BorrowId.to_string bid1)); - - let bid = match_borrow_id bid0 bid1 in - (* If we don't check for equivalence (i.e., we apply a fixed-point), - we lookup the shared values and match them. - *) - let _ = - if S.check_equiv then () - else - let v0 = S.lookup_shared_value_in_ctx0 bid0 in - let v1 = S.lookup_shared_value_in_ctx1 bid1 in - log#ldebug - (lazy - ("MakeCheckEquivMatcher: match_shared_borrows: looked up values:" - ^ "sv0: " - ^ typed_value_to_string S.ctx v0 - ^ ", sv1: " - ^ typed_value_to_string S.ctx v1)); - - let _ = match_typed_values v0 v1 in - () - in - bid - - let match_mut_borrows (_ty : T.ety) (bid0 : V.borrow_id) - (_bv0 : V.typed_value) (bid1 : V.borrow_id) (_bv1 : V.typed_value) - (bv : V.typed_value) : V.borrow_id * V.typed_value = - let bid = match_borrow_id bid0 bid1 in - (bid, bv) - - let match_shared_loans (_ : T.ety) (ids0 : V.loan_id_set) - (ids1 : V.loan_id_set) (sv : V.typed_value) : - V.loan_id_set * V.typed_value = - let ids = match_loan_ids ids0 ids1 in - (ids, sv) - - let match_mut_loans (_ : T.ety) (bid0 : V.loan_id) (bid1 : V.loan_id) : - V.loan_id = - match_loan_id bid0 bid1 - - let match_symbolic_values (sv0 : V.symbolic_value) (sv1 : V.symbolic_value) : - V.symbolic_value = - let id0 = sv0.sv_id in - let id1 = sv1.sv_id in - - log#ldebug - (lazy - ("MakeCheckEquivMatcher: match_symbolic_values: " ^ "sv0: " - ^ V.SymbolicValueId.to_string id0 - ^ ", sv1: " - ^ V.SymbolicValueId.to_string id1)); - - (* If we don't check for equivalence, we also update the map from sids - to values *) - if S.check_equiv then - (* Create the joined symbolic value *) - let sv_id = - GetSetSid.match_e "match_symbolic_values: ids: " S.sid_map id0 id1 - in - let sv_ty = match_rtys sv0.V.sv_ty sv1.V.sv_ty in - let sv_kind = - if sv0.V.sv_kind = sv1.V.sv_kind then sv0.V.sv_kind - else raise (Distinct "match_symbolic_values: sv_kind") - in - let sv = { V.sv_id; sv_ty; sv_kind } in - sv - else ( - (* Check: fixed values are fixed *) - assert (id0 = id1 || not (V.SymbolicValueId.InjSubst.mem id0 !S.sid_map)); - - (* Update the symbolic value mapping *) - let sv1 = mk_typed_value_from_symbolic_value sv1 in - - (* Update the symbolic value mapping *) - S.sid_to_value_map := - V.SymbolicValueId.Map.add_strict id0 sv1 !S.sid_to_value_map; - - (* Return - the returned value is not used: we can return whatever - we want *) - sv0) - - let match_symbolic_with_other (left : bool) (sv : V.symbolic_value) - (v : V.typed_value) : V.typed_value = - if S.check_equiv then raise (Distinct "match_symbolic_with_other") - else ( - assert left; - let id = sv.sv_id in - (* Check: fixed values are fixed *) - assert (not (V.SymbolicValueId.InjSubst.mem id !S.sid_map)); - (* Update the binding for the target symbolic value *) - S.sid_to_value_map := - V.SymbolicValueId.Map.add_strict id v !S.sid_to_value_map; - (* Return - the returned value is not used, so we can return whatever we want *) - v) - - let match_bottom_with_other (left : bool) (v : V.typed_value) : V.typed_value - = - (* It can happen that some variables get initialized in some branches - and not in some others, which causes problems when matching. *) - (* TODO: the returned value is not used, while it should: in generality it - should be ok to match a fixed-point with the environment we get at - a continue, where the fixed point contains some bottom values. *) - if left && not (value_has_loans_or_borrows S.ctx v.V.value) then - mk_bottom v.V.ty - else raise (Distinct "match_bottom_with_other") - - let match_distinct_aadts _ _ _ _ _ = raise (Distinct "match_distinct_adts") - - let match_ashared_borrows _ty0 bid0 _ty1 bid1 ty = - let bid = match_borrow_id bid0 bid1 in - let value = V.ABorrow (V.ASharedBorrow bid) in - { V.value; ty } - - let match_amut_borrows _ty0 bid0 _av0 _ty1 bid1 _av1 ty av = - let bid = match_borrow_id bid0 bid1 in - let value = V.ABorrow (V.AMutBorrow (bid, av)) in - { V.value; ty } - - let match_ashared_loans _ty0 ids0 _v0 _av0 _ty1 ids1 _v1 _av1 ty v av = - let bids = match_loan_ids ids0 ids1 in - let value = V.ALoan (V.ASharedLoan (bids, v, av)) in - { V.value; ty } - - let match_amut_loans _ty0 id0 _av0 _ty1 id1 _av1 ty av = - log#ldebug - (lazy - ("MakeCheckEquivMatcher:match_amut_loans:" ^ "\n- id0: " - ^ V.BorrowId.to_string id0 ^ "\n- id1: " ^ V.BorrowId.to_string id1 - ^ "\n- ty: " ^ rty_to_string S.ctx ty ^ "\n- av: " - ^ typed_avalue_to_string S.ctx av)); - - let id = match_loan_id id0 id1 in - let value = V.ALoan (V.AMutLoan (id, av)) in - { V.value; ty } - - let match_avalues v0 v1 = - log#ldebug - (lazy - ("avalues don't match:\n- v0: " - ^ typed_avalue_to_string S.ctx v0 - ^ "\n- v1: " - ^ typed_avalue_to_string S.ctx v1)); - raise (Distinct "match_avalues") -end - -(** See {!match_ctxs} *) -type ids_maps = { - aid_map : V.AbstractionId.InjSubst.t; - blid_map : V.BorrowId.InjSubst.t; - (** Substitution for the loan and borrow ids *) - borrow_id_map : V.BorrowId.InjSubst.t; (** Substitution for the borrow ids *) - loan_id_map : V.BorrowId.InjSubst.t; (** Substitution for the loan ids *) - rid_map : T.RegionId.InjSubst.t; - sid_map : V.SymbolicValueId.InjSubst.t; - sid_to_value_map : V.typed_value V.SymbolicValueId.Map.t; -} -[@@deriving show] - -(** Compute whether two contexts are equivalent modulo an identifier substitution. - - [fixed_ids]: ids for which we force the mapping to be the identity. - - We also take advantage of the structure of the environments, which should - have the same prefixes (we check it). See the explanations for {!join_ctxs}. - - TODO: explanations. - - [check_equiv]: if [true], check if the two contexts are equivalent. - If [false], compute a mapping from the first context to the second context, - in the sense of [match_ctx_with_target]. - - The lookup functions are used to match the shared values when [check_equiv] - is [false] (we sometimes use [match_ctxs] on partial environments, meaning - we have to lookup the shared values in the complete environment, otherwise - we miss some mappings). - - We return an optional ids map: [Some] if the match succeeded, [None] otherwise. - *) -let match_ctxs (check_equiv : bool) (fixed_ids : ids_sets) - (lookup_shared_value_in_ctx0 : V.BorrowId.id -> V.typed_value) - (lookup_shared_value_in_ctx1 : V.BorrowId.id -> V.typed_value) - (ctx0 : C.eval_ctx) (ctx1 : C.eval_ctx) : ids_maps option = - log#ldebug - (lazy - ("match_ctxs:\n\n- fixed_ids:\n" ^ show_ids_sets fixed_ids - ^ "\n\n- ctx0:\n" - ^ eval_ctx_to_string_no_filter ctx0 - ^ "\n\n- ctx1:\n" - ^ eval_ctx_to_string_no_filter ctx1 - ^ "\n\n")); - - (* Initialize the maps and instantiate the matcher *) - let module IdMap (Id : Identifiers.Id) = struct - let mk_map_ref (ids : Id.Set.t) : Id.InjSubst.t ref = - ref - (Id.InjSubst.of_list (List.map (fun x -> (x, x)) (Id.Set.elements ids))) - end in - let rid_map = - let module IdMap = IdMap (T.RegionId) in - IdMap.mk_map_ref fixed_ids.rids - in - let blid_map = - let module IdMap = IdMap (V.BorrowId) in - IdMap.mk_map_ref fixed_ids.blids - in - let borrow_id_map = - let module IdMap = IdMap (V.BorrowId) in - IdMap.mk_map_ref fixed_ids.borrow_ids - in - let loan_id_map = - let module IdMap = IdMap (V.BorrowId) in - IdMap.mk_map_ref fixed_ids.loan_ids - in - let aid_map = - let module IdMap = IdMap (V.AbstractionId) in - IdMap.mk_map_ref fixed_ids.aids - in - let sid_map = - let module IdMap = IdMap (V.SymbolicValueId) in - IdMap.mk_map_ref fixed_ids.sids - in - (* In case we don't try to check equivalence but want to compute a mapping - from a source context to a target context, we use a map from symbolic - value ids to values (rather than to ids). - *) - let sid_to_value_map : V.typed_value V.SymbolicValueId.Map.t ref = - ref V.SymbolicValueId.Map.empty - in - - let module S : MatchCheckEquivState = struct - let check_equiv = check_equiv - let ctx = ctx0 - let rid_map = rid_map - let blid_map = blid_map - let borrow_id_map = borrow_id_map - let loan_id_map = loan_id_map - let sid_map = sid_map - let sid_to_value_map = sid_to_value_map - let aid_map = aid_map - let lookup_shared_value_in_ctx0 = lookup_shared_value_in_ctx0 - let lookup_shared_value_in_ctx1 = lookup_shared_value_in_ctx1 - end in - let module CEM = MakeCheckEquivMatcher (S) in - let module M = Match (CEM) in - (* Match the environments - we assume that they have the same structure - (and fail if they don't) *) - - (* Small utility: check that ids are fixed/mapped to themselves *) - let ids_are_fixed (ids : ids_sets) : bool = - let { aids; blids = _; borrow_ids; loan_ids; dids; rids; sids } = ids in - V.AbstractionId.Set.subset aids fixed_ids.aids - && V.BorrowId.Set.subset borrow_ids fixed_ids.borrow_ids - && V.BorrowId.Set.subset loan_ids fixed_ids.loan_ids - && C.DummyVarId.Set.subset dids fixed_ids.dids - && T.RegionId.Set.subset rids fixed_ids.rids - && V.SymbolicValueId.Set.subset sids fixed_ids.sids - in - - (* We need to pick a context for some functions like [match_typed_values]: - the context is only used to lookup module data, so we can pick whichever - we want. - TODO: this is not very clean. Maybe we should just carry the relevant data - (i.e.e, not the whole context) around. - *) - let ctx = ctx0 in - - (* Rem.: this function raises exceptions of type [Distinct] *) - let match_abstractions (abs0 : V.abs) (abs1 : V.abs) : unit = - let { - V.abs_id = abs_id0; - kind = kind0; - can_end = can_end0; - parents = parents0; - original_parents = original_parents0; - regions = regions0; - ancestors_regions = ancestors_regions0; - avalues = avalues0; - } = - abs0 - in - - let { - V.abs_id = abs_id1; - kind = kind1; - can_end = can_end1; - parents = parents1; - original_parents = original_parents1; - regions = regions1; - ancestors_regions = ancestors_regions1; - avalues = avalues1; - } = - abs1 - in - - let _ = CEM.match_aid abs_id0 abs_id1 in - if kind0 <> kind1 || can_end0 <> can_end1 then - raise (Distinct "match_abstractions: kind or can_end"); - let _ = CEM.match_aids parents0 parents1 in - let _ = CEM.match_aidl original_parents0 original_parents1 in - let _ = CEM.match_rids regions0 regions1 in - let _ = CEM.match_rids ancestors_regions0 ancestors_regions1 in - - log#ldebug (lazy "match_abstractions: matching values"); - let _ = - List.map - (fun (v0, v1) -> M.match_typed_avalues ctx v0 v1) - (List.combine avalues0 avalues1) - in - log#ldebug (lazy "match_abstractions: values matched OK"); - () - in - - (* Rem.: this function raises exceptions of type [Distinct] *) - let rec match_envs (env0 : C.env) (env1 : C.env) : unit = - log#ldebug - (lazy - ("match_ctxs: match_envs:\n\n- fixed_ids:\n" ^ show_ids_sets fixed_ids - ^ "\n\n- rid_map: " - ^ T.RegionId.InjSubst.show_t !rid_map - ^ "\n- blid_map: " - ^ V.BorrowId.InjSubst.show_t !blid_map - ^ "\n- sid_map: " - ^ V.SymbolicValueId.InjSubst.show_t !sid_map - ^ "\n- aid_map: " - ^ V.AbstractionId.InjSubst.show_t !aid_map - ^ "\n\n- ctx0:\n" - ^ eval_ctx_to_string_no_filter { ctx0 with env = List.rev env0 } - ^ "\n\n- ctx1:\n" - ^ eval_ctx_to_string_no_filter { ctx1 with env = List.rev env1 } - ^ "\n\n")); - - match (env0, env1) with - | ( C.Var (C.DummyBinder b0, v0) :: env0', - C.Var (C.DummyBinder b1, v1) :: env1' ) -> - (* Sanity check: if the dummy value is an old value, the bindings must - be the same and their values equal (and the borrows/loans/symbolic *) - if C.DummyVarId.Set.mem b0 fixed_ids.dids then ( - (* Fixed values: the values must be equal *) - assert (b0 = b1); - assert (v0 = v1); - (* The ids present in the left value must be fixed *) - let ids, _ = compute_typed_value_ids v0 in - assert ((not S.check_equiv) || ids_are_fixed ids)); - (* We still match the values - allows to compute mappings (which - are the identity actually) *) - let _ = M.match_typed_values ctx v0 v1 in - match_envs env0' env1' - | C.Var (C.VarBinder b0, v0) :: env0', C.Var (C.VarBinder b1, v1) :: env1' - -> - assert (b0 = b1); - (* Match the values *) - let _ = M.match_typed_values ctx v0 v1 in - (* Continue *) - match_envs env0' env1' - | C.Abs abs0 :: env0', C.Abs abs1 :: env1' -> - log#ldebug (lazy "match_ctxs: match_envs: matching abs"); - (* Same as for the dummy values: there are two cases *) - if V.AbstractionId.Set.mem abs0.abs_id fixed_ids.aids then ( - log#ldebug (lazy "match_ctxs: match_envs: matching abs: fixed abs"); - (* Still in the prefix: the abstractions must be the same *) - assert (abs0 = abs1); - (* Their ids must be fixed *) - let ids, _ = compute_abs_ids abs0 in - assert ((not S.check_equiv) || ids_are_fixed ids); - (* Continue *) - match_envs env0' env1') - else ( - log#ldebug - (lazy "match_ctxs: match_envs: matching abs: not fixed abs"); - (* Match the values *) - match_abstractions abs0 abs1; - (* Continue *) - match_envs env0' env1') - | [], [] -> - (* Done *) - () - | _ -> - (* The elements don't match *) - raise (Distinct "match_ctxs: match_envs: env elements don't match") - in - - (* Match the environments. - - Rem.: we don't match the ended regions (would it make any sense actually?) *) - try - (* Remove the frame delimiter (the first element of an environment is a frame delimiter) *) - let env0 = List.rev ctx0.env in - let env1 = List.rev ctx1.env in - let env0, env1 = - match (env0, env1) with - | C.Frame :: env0, C.Frame :: env1 -> (env0, env1) - | _ -> raise (Failure "Unreachable") - in - - match_envs env0 env1; - let maps = - { - aid_map = !aid_map; - blid_map = !blid_map; - borrow_id_map = !borrow_id_map; - loan_id_map = !loan_id_map; - rid_map = !rid_map; - sid_map = !sid_map; - sid_to_value_map = !sid_to_value_map; - } - in - Some maps - with Distinct msg -> - log#ldebug (lazy ("match_ctxs: distinct: " ^ msg)); - None - -(** Compute whether two contexts are equivalent modulo an identifier substitution. - - [fixed_ids]: ids for which we force the mapping to be the identity. - - We also take advantage of the structure of the environments, which should - have the same prefixes (we check it). See the explanations for {!join_ctxs}. - - For instance, the following environments are equivalent: - {[ - env0 = { - abs@0 { ML l0 } - ls -> MB l1 (s2 : loops::List<T>) - i -> s1 : u32 - abs@1 { MB l0, ML l1 } - } - - env1 = { - abs@0 { ML l0 } - ls -> MB l2 (s4 : loops::List<T>) - i -> s3 : u32 - abs@2 { MB l0, ML l2 } - } - ]} - - We can go from [env0] to [env1] with the substitution: - {[ - abs_id_subst: { abs@1 -> abs@2 } - borrow_id_subst: { l1 -> l2 } - symbolic_id_subst: { s1 -> s3, s2 -> s4 } - ]} - *) -let ctxs_are_equivalent (fixed_ids : ids_sets) (ctx0 : C.eval_ctx) - (ctx1 : C.eval_ctx) : bool = - let check_equivalent = true in - let lookup_shared_value _ = raise (Failure "Unreachable") in - Option.is_some - (match_ctxs check_equivalent fixed_ids lookup_shared_value - lookup_shared_value ctx0 ctx1) - -(** Join the context at the entry of the loop with the contexts upon reentry - (upon reaching the [Continue] statement - the goal is to compute a fixed - point for the loop entry). - - As we may have to end loans in the environments before doing the join, - we return those updated environments, and the joined environment. - *) -let loop_join_origin_with_continue_ctxs (config : C.config) - (loop_id : V.LoopId.id) (fixed_ids : ids_sets) (old_ctx : C.eval_ctx) - (ctxl : C.eval_ctx list) : (C.eval_ctx * C.eval_ctx list) * C.eval_ctx = - (* # Join with the new contexts, one by one - - For every context, we repeteadly attempt to join it with the current - result of the join: if we fail (because we need to end loans for instance), - we update the context and retry. - Rem.: we should never have to end loans in the aggregated context, only - in the one we are trying to add to the join. - *) - let joined_ctx = ref old_ctx in - let rec join_one_aux (ctx : C.eval_ctx) : C.eval_ctx = - match join_ctxs loop_id fixed_ids !joined_ctx ctx with - | Ok nctx -> - joined_ctx := nctx; - ctx - | Error err -> - let ctx = - match err with - | LoanInRight bid -> - InterpreterBorrows.end_borrow_no_synth config bid ctx - | LoansInRight bids -> - InterpreterBorrows.end_borrows_no_synth config bids ctx - | AbsInRight _ | AbsInLeft _ | LoanInLeft _ | LoansInLeft _ -> - raise (Failure "Unexpected") - in - join_one_aux ctx - in - let join_one (ctx : C.eval_ctx) : C.eval_ctx = - log#ldebug - (lazy - ("loop_join_origin_with_continue_ctxs:join_one: initial ctx:\n" - ^ eval_ctx_to_string ctx)); - - (* Destructure the abstractions introduced in the new context *) - let ctx = destructure_new_abs loop_id fixed_ids.aids ctx in - log#ldebug - (lazy - ("loop_join_origin_with_continue_ctxs:join_one: after destructure:\n" - ^ eval_ctx_to_string ctx)); - - (* Collapse the context we want to add to the join *) - let ctx = collapse_ctx loop_id None fixed_ids ctx in - log#ldebug - (lazy - ("loop_join_origin_with_continue_ctxs:join_one: after collapse:\n" - ^ eval_ctx_to_string ctx)); - - (* Refresh the fresh abstractions *) - let ctx = refresh_abs fixed_ids.aids ctx in - - (* Join the two contexts *) - let ctx1 = join_one_aux ctx in - log#ldebug - (lazy - ("loop_join_origin_with_continue_ctxs:join_one: after join:\n" - ^ eval_ctx_to_string ctx1)); - - (* Collapse again - the join might have introduce abstractions we want - to merge with the others (note that those abstractions may actually - lead to borrows/loans duplications) *) - joined_ctx := collapse_ctx_with_merge loop_id fixed_ids !joined_ctx; - log#ldebug - (lazy - ("loop_join_origin_with_continue_ctxs:join_one: after join-collapse:\n" - ^ eval_ctx_to_string !joined_ctx)); - - (* Sanity check *) - if !Config.check_invariants then Invariants.check_invariants !joined_ctx; - (* Return *) - ctx1 - in - - let ctxl = List.map join_one ctxl in - - (* # Return *) - ((old_ctx, ctxl), !joined_ctx) - -(** Prepare the shared loans in the abstractions by moving them to fresh - abstractions. - - We use this to prepare an evaluation context before computing a fixed point. - - Because a loop iteration might lead to symbolic value expansions and create - shared loans in shared values inside the *fixed* abstractions, which we want - to leave unchanged, we introduce some reborrows in the following way: - - {[ - abs'0 { SL {l0, l1} s0 } - l0 -> SB l0 - l1 -> SB l1 - - ~~> - - abs'0 { SL {l0, l1} s0 } - l0 -> SB l2 - l1 -> SB l3 - abs'2 { SB l0, SL {l2} s2 } - abs'3 { SB l1, SL {l3} s3 } - ]} - - This is sound but leads to information loss. This way, the fixed abstraction - [abs'0] is never modified because [s0] is never accessed (and thus never - expanded). - - We do this because it makes it easier to compute joins and fixed points. - - **REMARK**: - As a side note, we only reborrow the loan ids whose corresponding borrows - appear in values (i.e., not in abstractions). - - For instance, if we have: - {[ - abs'0 { - SL {l0} s0 - SL {l1} s1 - } - abs'1 { SB l0 } - x -> SB l1 - ]} - - we only introduce a fresh abstraction for [l1]. - *) -let prepare_ashared_loans (loop_id : V.LoopId.id option) : cm_fun = - fun cf ctx0 -> - let ctx = ctx0 in - (* Compute the set of borrows which appear in the abstractions, so that - we can filter the borrows that we reborrow. - *) - let absl = - List.filter_map - (function C.Var _ | C.Frame -> None | C.Abs abs -> Some abs) - ctx.env - in - let absl_ids, absl_id_maps = compute_absl_ids absl in - let abs_borrow_ids = absl_ids.borrow_ids in - - (* Map from the fresh sids to the original symbolic values *) - let sid_subst = ref [] in - - (* Return the same value but where: - - the shared loans have been removed - - the symbolic values have been replaced with fresh symbolic values - - the region ids found in the value and belonging to the set [rids] have - been substituted with [nrid] - *) - let mk_value_with_fresh_sids_no_shared_loans (rids : T.RegionId.Set.t) - (nrid : T.RegionId.id) (v : V.typed_value) : V.typed_value = - (* Remove the shared loans *) - let v = value_remove_shared_loans v in - (* Substitute the symbolic values and the region *) - Subst.typed_value_subst_ids - (fun r -> if T.RegionId.Set.mem r rids then nrid else r) - (fun x -> x) - (fun x -> x) - (fun id -> - let nid = C.fresh_symbolic_value_id () in - let sv = V.SymbolicValueId.Map.find id absl_id_maps.sids_to_values in - sid_subst := (nid, sv) :: !sid_subst; - nid) - (fun x -> x) - v - in - - let borrow_substs = ref [] in - let fresh_absl = ref [] in - - (* Auxiliary function to create a new abstraction for a shared value found in - an abstraction. - - Example: - ======== - When exploring: - {[ - abs'0 { SL {l0, l1} s0 } - ]} - - we find the shared value: - - {[ - SL {l0, l1} s0 - ]} - - and introduce the corresponding abstraction: - {[ - abs'2 { SB l0, SL {l2} s2 } - ]} - *) - let push_abs_for_shared_value (abs : V.abs) (sv : V.typed_value) - (lid : V.BorrowId.id) : unit = - (* Create a fresh borrow (for the reborrow) *) - let nlid = C.fresh_borrow_id () in - - (* We need a fresh region for the new abstraction *) - let nrid = C.fresh_region_id () in - - (* Prepare the shared value *) - let nsv = mk_value_with_fresh_sids_no_shared_loans abs.regions nrid sv in - - (* Save the borrow substitution, to apply it to the context later *) - borrow_substs := (lid, nlid) :: !borrow_substs; - - (* Rem.: the below sanity checks are not really necessary *) - assert (V.AbstractionId.Set.is_empty abs.parents); - assert (abs.original_parents = []); - assert (T.RegionId.Set.is_empty abs.ancestors_regions); - - (* Introduce the new abstraction for the shared values *) - let rty = ety_no_regions_to_rty sv.V.ty in - - (* Create the shared loan child *) - let child_rty = rty in - let child_av = mk_aignored child_rty in - - (* Create the shared loan *) - let loan_rty = T.Ref (T.Var nrid, rty, T.Shared) in - let loan_value = - V.ALoan (V.ASharedLoan (V.BorrowId.Set.singleton nlid, nsv, child_av)) - in - let loan_value = mk_typed_avalue loan_rty loan_value in - - (* Create the shared borrow *) - let borrow_rty = loan_rty in - let borrow_value = V.ABorrow (V.ASharedBorrow lid) in - let borrow_value = mk_typed_avalue borrow_rty borrow_value in - - (* Create the abstraction *) - let avalues = [ borrow_value; loan_value ] in - let kind = - match loop_id with - | Some loop_id -> V.Loop (loop_id, None, V.LoopSynthInput) - | None -> V.Identity - in - let can_end = true in - let fresh_abs = - { - V.abs_id = C.fresh_abstraction_id (); - kind; - can_end; - parents = V.AbstractionId.Set.empty; - original_parents = []; - regions = T.RegionId.Set.singleton nrid; - ancestors_regions = T.RegionId.Set.empty; - avalues; - } - in - fresh_absl := fresh_abs :: !fresh_absl - in - - (* Explore the shared values in the context abstractions, and introduce - fresh abstractions with reborrows for those shared values. - - We simply explore the context and call {!push_abs_for_shared_value} - when necessary. - *) - let collect_shared_values_in_abs (abs : V.abs) : unit = - let collect_shared_value lids (sv : V.typed_value) = - (* Sanity check: we don't support nested borrows for now *) - assert (not (value_has_borrows ctx sv.V.value)); - - (* Filter the loan ids whose corresponding borrows appear in abstractions - (see the documentation of the function) *) - let lids = V.BorrowId.Set.diff lids abs_borrow_ids in - - (* Generate fresh borrows and values *) - V.BorrowId.Set.iter (push_abs_for_shared_value abs sv) lids - in - - let visit_avalue = - object - inherit [_] V.iter_typed_avalue as super - - method! visit_SharedLoan env lids sv = - collect_shared_value lids sv; - - (* Continue the exploration *) - super#visit_SharedLoan env lids sv - - method! visit_ASharedLoan env lids sv _ = - collect_shared_value lids sv; - - (* Continue the exploration *) - super#visit_SharedLoan env lids sv - - (** Check that there are no symbolic values with *borrows* inside the - abstraction - shouldn't happen if the symbolic values are greedily - expanded. - We do this because those values could contain shared borrows: - if it is the case, we need to duplicate them too. - TODO: implement this more general behavior. - *) - method! visit_symbolic_value env sv = - assert (not (symbolic_value_has_borrows ctx sv)); - super#visit_symbolic_value env sv - end - in - List.iter (visit_avalue#visit_typed_avalue None) abs.avalues - in - C.env_iter_abs collect_shared_values_in_abs ctx.env; - - (* Update the borrow ids in the environment. - - Example: - ======== - If we start with environment: - {[ - abs'0 { SL {l0, l1} s0 } - l0 -> SB l0 - l1 -> SB l1 - ]} - - We introduce the following abstractions: - {[ - abs'2 { SB l0, SL {l2} s2 } - abs'3 { SB l1, SL {l3} s3 } - ]} - - While doing so, we registered the fact that we introduced [l2] for [l0] - and [l3] for [l1]: we now need to perform the proper substitutions in - the values [l0] and [l1]. This gives: - - {[ - l0 -> SB l0 - l1 -> SB l1 - - ~~> - - l0 -> SB l2 - l1 -> SB l3 - ]} - *) - let env = - let bmap = V.BorrowId.Map.of_list !borrow_substs in - let bsusbt bid = - match V.BorrowId.Map.find_opt bid bmap with - | None -> bid - | Some bid -> bid - in - - let visitor = - object - inherit [_] C.map_env - method! visit_borrow_id _ bid = bsusbt bid - end - in - visitor#visit_env () ctx.env - in - - (* Add the abstractions *) - let fresh_absl = List.map (fun abs -> C.Abs abs) !fresh_absl in - let env = List.append fresh_absl env in - let ctx = { ctx with env } in - - let _, new_ctx_ids_map = compute_context_ids ctx in - - (* Synthesize *) - match cf ctx with - | None -> None - | Some e -> - (* Add the let-bindings which introduce the fresh symbolic values *) - Some - (List.fold_left - (fun e (sid, v) -> - let v = mk_typed_value_from_symbolic_value v in - let sv = - V.SymbolicValueId.Map.find sid new_ctx_ids_map.sids_to_values - in - SymbolicAst.IntroSymbolic (ctx, None, sv, v, e)) - e !sid_subst) - -let prepare_ashared_loans_no_synth (loop_id : V.LoopId.id) (ctx : C.eval_ctx) : - C.eval_ctx = - get_cf_ctx_no_synth (prepare_ashared_loans (Some loop_id)) ctx - -(** Compute a fixed-point for the context at the entry of the loop. - We also return: - - the sets of fixed ids - - the map from region group id to the corresponding abstraction appearing - in the fixed point (this is useful to compute the return type of the loop - backward functions for instance). - - Rem.: the list of symbolic values should be computable by simply exploring - the fixed point environment and listing all the symbolic values we find. - In the future, we might want to do something more precise, by listing only - the values which are read or modified (some symbolic values may be ignored). - *) -let compute_loop_entry_fixed_point (config : C.config) (loop_id : V.LoopId.id) - (eval_loop_body : st_cm_fun) (ctx0 : C.eval_ctx) : - C.eval_ctx * ids_sets * V.abs T.RegionGroupId.Map.t = - (* The continuation for when we exit the loop - we register the - environments upon loop *reentry*, and synthesize nothing by - returning [None] - *) - let ctxs = ref [] in - let register_ctx ctx = ctxs := ctx :: !ctxs in - - (* Introduce "reborrows" for the shared values in the abstractions, so that - the shared values in the fixed abstractions never get modified (technically, - they are immutable, but in practice we can introduce more shared loans, or - expand symbolic values). - - For more details, see the comments for {!prepare_ashared_loans} - *) - let ctx = prepare_ashared_loans_no_synth loop_id ctx0 in - - (* Debug *) - log#ldebug - (lazy - ("compute_loop_entry_fixed_point: after prepare_ashared_loans:" - ^ "\n\n- ctx0:\n" - ^ eval_ctx_to_string_no_filter ctx0 - ^ "\n\n- ctx1:\n" - ^ eval_ctx_to_string_no_filter ctx - ^ "\n\n")); - - let cf_exit_loop_body (res : statement_eval_res) : m_fun = - fun ctx -> - match res with - | Return | Panic | Break _ -> None - | Unit -> - (* See the comment in {!eval_loop} *) - raise (Failure "Unreachable") - | Continue i -> - (* For now we don't support continues to outer loops *) - assert (i = 0); - register_ctx ctx; - None - | LoopReturn _ | EndEnterLoop _ | EndContinue _ -> - (* We don't support nested loops for now *) - raise (Failure "Nested loops are not supported for now") - in - - (* The fixed ids. They are the ids of the original ctx, after we ended - the borrows/loans which end during the first loop iteration (we do - one loop iteration, then set it to [Some]. - *) - let fixed_ids : ids_sets option ref = ref None in - - (* Join the contexts at the loop entry - ctx1 is the current joined - context (the context at the loop entry, after we called - {!prepare_ashared_loans}, if this is the first iteration) *) - let join_ctxs (ctx1 : C.eval_ctx) : C.eval_ctx = - (* If this is the first iteration, end the borrows/loans/abs which - appear in ctx1 and not in the other contexts, then compute the - set of fixed ids. This means those borrows/loans have to end - in the loop, and we rather end them *before* the loop. *) - let ctx1 = - match !fixed_ids with - | Some _ -> ctx1 - | None -> - let old_ids, _ = compute_context_ids ctx1 in - let new_ids, _ = compute_contexts_ids !ctxs in - let blids = V.BorrowId.Set.diff old_ids.blids new_ids.blids in - let aids = V.AbstractionId.Set.diff old_ids.aids new_ids.aids in - (* End those borrows and abstractions *) - let end_borrows_abs blids aids ctx = - let ctx = - InterpreterBorrows.end_borrows_no_synth config blids ctx - in - let ctx = - InterpreterBorrows.end_abstractions_no_synth config aids ctx - in - ctx - in - (* End the borrows/abs in [ctx1] *) - let ctx1 = end_borrows_abs blids aids ctx1 in - (* We can also do the same in the contexts [ctxs]: if there are - several contexts, maybe one of them ended some borrows and some - others didn't. As we need to end those borrows anyway (the join - will detect them and ask to end them) we do it preemptively. - *) - ctxs := List.map (end_borrows_abs blids aids) !ctxs; - (* Note that the fixed ids are given by the original context, from *before* - we introduce fresh abstractions/reborrows for the shared values *) - fixed_ids := Some (fst (compute_context_ids ctx0)); - ctx1 - in - - let fixed_ids = Option.get !fixed_ids in - let (_, _), ctx2 = - loop_join_origin_with_continue_ctxs config loop_id fixed_ids ctx1 !ctxs - in - ctxs := []; - ctx2 - in - (* Compute the set of fixed ids - for the symbolic ids, we compute the - intersection of ids between the original environment and the list - of new environments *) - let compute_fixed_ids (ctxl : C.eval_ctx list) : ids_sets = - let fixed_ids, _ = compute_context_ids ctx0 in - let { aids; blids; borrow_ids; loan_ids; dids; rids; sids } = fixed_ids in - let sids = ref sids in - List.iter - (fun ctx -> - let fixed_ids, _ = compute_context_ids ctx in - sids := V.SymbolicValueId.Set.inter !sids fixed_ids.sids) - ctxl; - let sids = !sids in - let fixed_ids = { aids; blids; borrow_ids; loan_ids; dids; rids; sids } in - fixed_ids - in - (* Check if two contexts are equivalent - modulo alpha conversion on the - existentially quantified borrows/abstractions/symbolic values. - *) - let equiv_ctxs (ctx1 : C.eval_ctx) (ctx2 : C.eval_ctx) : bool = - let fixed_ids = compute_fixed_ids [ ctx1; ctx2 ] in - let check_equivalent = true in - let lookup_shared_value _ = raise (Failure "Unreachable") in - Option.is_some - (match_ctxs check_equivalent fixed_ids lookup_shared_value - lookup_shared_value ctx1 ctx2) - in - let max_num_iter = Config.loop_fixed_point_max_num_iters in - let rec compute_fixed_point (ctx : C.eval_ctx) (i0 : int) (i : int) : - C.eval_ctx = - if i = 0 then - raise - (Failure - ("Could not compute a loop fixed point in " ^ string_of_int i0 - ^ " iterations")) - else - (* Evaluate the loop body to register the different contexts upon reentry *) - let _ = eval_loop_body cf_exit_loop_body ctx in - (* Compute the join between the original contexts and the contexts computed - upon reentry *) - let ctx1 = join_ctxs ctx in - - (* Debug *) - log#ldebug - (lazy - ("compute_fixed_point:" ^ "\n\n- ctx0:\n" - ^ eval_ctx_to_string_no_filter ctx - ^ "\n\n- ctx1:\n" - ^ eval_ctx_to_string_no_filter ctx1 - ^ "\n\n")); - - (* Check if we reached a fixed point: if not, iterate *) - if equiv_ctxs ctx ctx1 then ctx1 else compute_fixed_point ctx1 i0 (i - 1) - in - let fp = compute_fixed_point ctx max_num_iter max_num_iter in - - (* Debug *) - log#ldebug - (lazy - ("compute_fixed_point: fixed point computed before matching with input \ - region groups:" ^ "\n\n- fp:\n" - ^ eval_ctx_to_string_no_filter fp - ^ "\n\n")); - - (* Make sure we have exactly one loop abstraction per function region (merge - abstractions accordingly). - - Rem.: this shouldn't impact the set of symbolic value ids (because we - already merged abstractions "vertically" and are now merging them - "horizontally": the symbolic values contained in the abstractions (typically - the shared values) will be preserved. - *) - let fp, rg_to_abs = - (* List the loop abstractions in the fixed-point *) - let fp_aids, add_aid, _mem_aid = V.AbstractionId.Set.mk_stateful_set () in - - let list_loop_abstractions = - object - inherit [_] C.map_eval_ctx - - method! visit_abs _ abs = - match abs.kind with - | Loop (loop_id', _, kind) -> - assert (loop_id' = loop_id); - assert (kind = V.LoopSynthInput); - (* The abstractions introduced so far should be endable *) - assert (abs.can_end = true); - add_aid abs.abs_id; - abs - | _ -> abs - end - in - let fp = list_loop_abstractions#visit_eval_ctx () fp in - - (* For every input region group: - * - evaluate until we get to a [return] - * - end the input abstraction corresponding to the input region group - * - find which loop abstractions end at that moment - * - * [fp_ended_aids] links region groups to sets of ended abstractions. - *) - let fp_ended_aids = ref T.RegionGroupId.Map.empty in - let add_ended_aids (rg_id : T.RegionGroupId.id) - (aids : V.AbstractionId.Set.t) : unit = - match T.RegionGroupId.Map.find_opt rg_id !fp_ended_aids with - | None -> - fp_ended_aids := T.RegionGroupId.Map.add rg_id aids !fp_ended_aids - | Some aids' -> - let aids = V.AbstractionId.Set.union aids aids' in - fp_ended_aids := T.RegionGroupId.Map.add rg_id aids !fp_ended_aids - in - let cf_loop : st_m_fun = - fun res ctx -> - match res with - | Continue _ | Panic -> - (* We don't want to generate anything *) - None - | Break _ -> - (* We enforce that we can't get there: see {!PrePasses.remove_loop_breaks} *) - raise (Failure "Unreachable") - | Unit | LoopReturn _ | EndEnterLoop _ | EndContinue _ -> - (* For why we can't get [Unit], see the comments inside {!eval_loop_concrete}. - For [EndEnterLoop] and [EndContinue]: we don't support nested loops for now. - *) - raise (Failure "Unreachable") - | Return -> - (* Should we consume the return value and pop the frame? - * If we check in [Interpreter] that the loop abstraction we end is - * indeed the correct one, I think it is sound to under-approximate here - * (and it shouldn't make any difference). - *) - let _ = - List.iter - (fun rg_id -> - (* Lookup the input abstraction - we use the fact that the - abstractions should have been introduced in a specific - order (and we check that it is indeed the case) *) - let abs_id = - V.AbstractionId.of_int (T.RegionGroupId.to_int rg_id) - in - (* By default, the [SynthInput] abs can't end *) - let ctx = C.ctx_set_abs_can_end ctx abs_id true in - assert ( - let abs = C.ctx_lookup_abs ctx abs_id in - abs.kind = V.SynthInput rg_id); - (* End this abstraction *) - let ctx = - InterpreterBorrows.end_abstraction_no_synth config abs_id ctx - in - (* Explore the context, and check which abstractions are not there anymore *) - let ids, _ = compute_context_ids ctx in - let ended_ids = V.AbstractionId.Set.diff !fp_aids ids.aids in - add_ended_aids rg_id ended_ids) - ctx.region_groups - in - (* We don't want to generate anything *) - None - in - let _ = eval_loop_body cf_loop fp in - - (* Check that the sets of abstractions we need to end per region group are pairwise - * disjoint *) - let aids_union = ref V.AbstractionId.Set.empty in - let _ = - T.RegionGroupId.Map.iter - (fun _ ids -> - assert (V.AbstractionId.Set.disjoint !aids_union ids); - aids_union := V.AbstractionId.Set.union ids !aids_union) - !fp_ended_aids - in - - (* We also check that all the regions need to end - this is not necessary per - se, but if it doesn't happen it is bizarre and worth investigating... *) - assert (V.AbstractionId.Set.equal !aids_union !fp_aids); - - (* Merge the abstractions which need to be merged, and compute the map from - region id to abstraction id *) - let fp = ref fp in - let rg_to_abs = ref T.RegionGroupId.Map.empty in - let _ = - T.RegionGroupId.Map.iter - (fun rg_id ids -> - let ids = V.AbstractionId.Set.elements ids in - (* Retrieve the first id of the group *) - match ids with - | [] -> - (* We shouldn't get there: we actually introduce reborrows with - {!prepare_ashared_loans} and in the [match_mut_borrows] function - of {!MakeJoinMatcher} to introduce some fresh abstractions for - this purpose. - *) - raise (Failure "Unexpected") - | id0 :: ids -> - let id0 = ref id0 in - (* Add the proper region group into the abstraction *) - let abs_kind = V.Loop (loop_id, Some rg_id, V.LoopSynthInput) in - let abs = C.ctx_lookup_abs !fp !id0 in - let abs = { abs with V.kind = abs_kind } in - let fp', _ = C.ctx_subst_abs !fp !id0 abs in - fp := fp'; - (* Merge all the abstractions into this one *) - List.iter - (fun id -> - try - log#ldebug - (lazy - ("compute_loop_entry_fixed_point: merge FP \ - abstraction: " - ^ V.AbstractionId.to_string id - ^ " into " - ^ V.AbstractionId.to_string !id0)); - (* Note that we merge *into* [id0] *) - let fp', id0' = - merge_into_abstraction loop_id abs_kind false !fp id !id0 - in - fp := fp'; - id0 := id0'; - () - with ValueMatchFailure _ -> raise (Failure "Unexpected")) - ids; - (* Register the mapping *) - let abs = C.ctx_lookup_abs !fp !id0 in - rg_to_abs := T.RegionGroupId.Map.add_strict rg_id abs !rg_to_abs) - !fp_ended_aids - in - let rg_to_abs = !rg_to_abs in - - (* Reorder the loans and borrows in the fresh abstractions in the fixed-point *) - let fp = - reorder_loans_borrows_in_fresh_abs (Option.get !fixed_ids).aids !fp - in - - (* Update the abstraction's [can_end] field and their kinds. - - Note that if [remove_rg_id] is [true], we set the region id to [None] - and set the abstractions as endable: this is so that we can check that - we have a fixed point (so far in the fixed point the loop abstractions had - no region group, and we set them as endable just above). - - If [remove_rg_id] is [false], we simply set the abstractions as non-endable - to freeze them (we will use the fixed point as starting point for the - symbolic execution of the loop body, and we have to make sure the input - abstractions are frozen). - *) - let update_loop_abstractions (remove_rg_id : bool) = - object - inherit [_] C.map_eval_ctx - - method! visit_abs _ abs = - match abs.kind with - | Loop (loop_id', _, kind) -> - assert (loop_id' = loop_id); - assert (kind = V.LoopSynthInput); - let kind = - if remove_rg_id then V.Loop (loop_id, None, V.LoopSynthInput) - else abs.kind - in - { abs with can_end = remove_rg_id; kind } - | _ -> abs - end - in - let update_kinds_can_end (remove_rg_id : bool) ctx = - (update_loop_abstractions remove_rg_id)#visit_eval_ctx () ctx - in - let fp = update_kinds_can_end false fp in - - (* Sanity check: we still have a fixed point - we simply call [compute_fixed_point] - while allowing exactly one iteration to see if it fails *) - let _ = - let fp_test = update_kinds_can_end true fp in - log#ldebug - (lazy - ("compute_fixed_point: fixed point after matching with the function \ - region groups:\n" - ^ eval_ctx_to_string_no_filter fp_test)); - compute_fixed_point fp_test 1 1 - in - - (* Return *) - (fp, rg_to_abs) - in - let fixed_ids = compute_fixed_ids [ fp ] in - - (* Return *) - (fp, fixed_ids, rg_to_abs) - -(** Split an environment between the fixed abstractions, values, etc. and - the new abstractions, values, etc. - - Returns: (fixed, new abs, new dummies) - *) -let ctx_split_fixed_new (fixed_ids : ids_sets) (ctx : C.eval_ctx) : - C.env * V.abs list * V.typed_value list = - let is_fresh_did (id : C.DummyVarId.id) : bool = - not (C.DummyVarId.Set.mem id fixed_ids.dids) - in - let is_fresh_abs_id (id : V.AbstractionId.id) : bool = - not (V.AbstractionId.Set.mem id fixed_ids.aids) - in - (* Filter the new abstractions and dummy variables (there shouldn't be any new dummy variable - though) in the target context *) - let is_fresh (ee : C.env_elem) : bool = - match ee with - | C.Var (VarBinder _, _) | C.Frame -> false - | C.Var (DummyBinder bv, _) -> is_fresh_did bv - | C.Abs abs -> is_fresh_abs_id abs.abs_id - in - let new_eel, filt_env = List.partition is_fresh ctx.env in - let is_abs ee = match ee with C.Abs _ -> true | _ -> false in - let new_absl, new_dummyl = List.partition is_abs new_eel in - let new_absl = - List.map - (fun ee -> - match ee with C.Abs abs -> abs | _ -> raise (Failure "Unreachable")) - new_absl - in - let new_dummyl = - List.map - (fun ee -> - match ee with - | C.Var (DummyBinder _, v) -> v - | _ -> raise (Failure "Unreachable")) - new_dummyl - in - (filt_env, new_absl, new_dummyl) - -type borrow_loan_corresp = { - borrow_to_loan_id_map : V.BorrowId.InjSubst.t; - loan_to_borrow_id_map : V.BorrowId.InjSubst.t; -} -[@@deriving show] - -let ids_sets_empty_borrows_loans (ids : ids_sets) : ids_sets = - let { aids; blids = _; borrow_ids = _; loan_ids = _; dids; rids; sids } = - ids - in - let empty = V.BorrowId.Set.empty in - let ids = - { - aids; - blids = empty; - borrow_ids = empty; - loan_ids = empty; - dids; - rids; - sids; - } - in - ids - -(** Utility *) -type loans_borrows_pair = { - loans : V.BorrowId.Set.t; - borrows : V.BorrowId.Set.t; -} -[@@deriving show, ord] - -(** For the abstractions in the fixed point, compute the correspondance between - the borrows ids and the loans ids, if we want to introduce equivalent - identity abstractions (i.e., abstractions which do nothing - the input - borrows are exactly the output loans). - - **Context:** - ============ - When we (re-enter) the loop, we want to introduce identity abstractions - (i.e., abstractions which actually only introduce fresh identifiers for - some borrows, to abstract away a bit the borrow graph) which have the same - shape as the abstractions introduced for the fixed point (see the explanations - for [match_ctx_with_target]). This allows us to transform the environment - into a fixed point (again, see the explanations for [match_ctx_with_target]). - - In order to introduce those identity abstractions, we need to figure out, - for those abstractions, which loans should be linked to which borrows. - We do this in the following way. - - We match the fixed point environment with the environment upon first entry - in the loop, and exploit the fact that the fixed point was derived by also - joining this first entry environment: because of that, the borrows in the - abstractions introduced for the fixed-point actually exist in this first - environment (they are not fresh). For [list_nth_mut] (see the explanations - at the top of the file) we have the following: - - {[ - // Environment upon first entry in the loop - env0 = { - abs@0 { ML l0 } - ls -> MB l0 (s2 : loops::List<T>) - i -> s1 : u32 - } - - // Fixed-point environment - env_fp = { - abs@0 { ML l0 } - ls -> MB l1 (s3 : loops::List<T>) - i -> s4 : u32 - abs@fp { - MB l0 // this borrow appears in [env0] - ML l1 - } - } - ]} - - We filter those environments to remove the non-fixed dummy variables, - abstractions, etc. in a manner similar to [match_ctx_with_target]. We - get: - - {[ - filtered_env0 = { - abs@0 { ML l0 } - ls -> MB l0 (s2 : loops::List<T>) - i -> s1 : u32 - } - - filtered_env_fp = { - abs@0 { ML l0 } - ls -> MB l1 (s3 : loops::List<T>) - i -> s@ : u32 - // removed abs@fp - } - ]} - - We then match [filtered_env_fp] with [filtered_env0], taking care to not - consider loans and borrows in a disjoint manner, and ignoring the fixed - values, abstractions, loans, etc. We get: - {[ - borrows_map: { l1 -> l0 } // because we matched [MB l1 ...] with [MB l0 ...] in [ls] - loans_map: {} // we ignore abs@0, which is "fixed" - ]} - - From there we deduce that, if we want to introduce an identity abstraction with the - shape of [abs@fp], we should link [l1] to [l0]. In other words, the value retrieved - through the loan [l1] is actually the value which has to be given back to [l0]. - *) -let compute_fixed_point_id_correspondance (fixed_ids : ids_sets) - (src_ctx : C.eval_ctx) (tgt_ctx : C.eval_ctx) : borrow_loan_corresp = - log#ldebug - (lazy - ("compute_fixed_point_id_correspondance:\n\n- fixed_ids:\n" - ^ show_ids_sets fixed_ids ^ "\n\n- src_ctx:\n" ^ eval_ctx_to_string src_ctx - ^ "\n\n- tgt_ctx:\n" ^ eval_ctx_to_string tgt_ctx ^ "\n\n")); - - let filt_src_env, _, _ = ctx_split_fixed_new fixed_ids src_ctx in - let filt_src_ctx = { src_ctx with env = filt_src_env } in - let filt_tgt_env, new_absl, _ = ctx_split_fixed_new fixed_ids tgt_ctx in - let filt_tgt_ctx = { tgt_ctx with env = filt_tgt_env } in - - log#ldebug - (lazy - ("compute_fixed_point_id_correspondance:\n\n- fixed_ids:\n" - ^ show_ids_sets fixed_ids ^ "\n\n- filt_src_ctx:\n" - ^ eval_ctx_to_string filt_src_ctx - ^ "\n\n- filt_tgt_ctx:\n" - ^ eval_ctx_to_string filt_tgt_ctx - ^ "\n\n")); - - (* Match the source context and the filtered target context *) - let maps = - let check_equiv = false in - let fixed_ids = ids_sets_empty_borrows_loans fixed_ids in - let open InterpreterBorrowsCore in - let lookup_shared_loan lid ctx : V.typed_value = - match snd (lookup_loan ek_all lid ctx) with - | Concrete (V.SharedLoan (_, v)) -> v - | Abstract (V.ASharedLoan (_, v, _)) -> v - | _ -> raise (Failure "Unreachable") - in - let lookup_in_tgt id = lookup_shared_loan id tgt_ctx in - let lookup_in_src id = lookup_shared_loan id src_ctx in - Option.get - (match_ctxs check_equiv fixed_ids lookup_in_tgt lookup_in_src filt_tgt_ctx - filt_src_ctx) - in - - log#ldebug - (lazy - ("compute_fixed_point_id_correspondance:\n\n- tgt_to_src_maps:\n" - ^ show_ids_maps maps ^ "\n\n")); - - let src_to_tgt_borrow_map = - V.BorrowId.Map.of_list - (List.map - (fun (x, y) -> (y, x)) - (V.BorrowId.InjSubst.bindings maps.borrow_id_map)) - in - - (* Sanity check: for every abstraction, the target loans and borrows are mapped - to the same set of source loans and borrows. - - For instance, if we map the [env_fp] to [env0] (only looking at the bindings, - ignoring the abstractions) below: - {[ - env0 = { - abs@0 { ML l0 } - ls -> MB l0 (s2 : loops::List<T>) - i -> s1 : u32 - } - - env_fp = { - abs@0 { ML l0 } - ls -> MB l1 (s3 : loops::List<T>) - i -> s4 : u32 - abs@fp { - MB l0 - ML l1 - } - } - ]} - - We get that l1 is mapped to l0. From there, we see that abs@fp consumes - the same borrows that it gives: it is indeed an identity function. - - TODO: we should also check the mappings for the shared values (to - make sure the abstractions are indeed the identity)... - *) - List.iter - (fun abs -> - let ids, _ = compute_abs_ids abs in - (* Map the *loan* ids (we just match the corresponding *loans* ) *) - let loan_ids = - V.BorrowId.Set.map - (fun x -> V.BorrowId.InjSubst.find x maps.borrow_id_map) - ids.loan_ids - in - (* Check that the loan and borrows are related *) - assert (V.BorrowId.Set.equal ids.borrow_ids loan_ids)) - new_absl; - - (* For every target abstraction (going back to the [list_nth_mut] example, - we have to visit [abs@fp { ML l0, MB l1 }]): - - go through the tgt borrows ([l1]) - - for every tgt borrow, find the corresponding src borrow ([l0], because - we have: [borrows_map: { l1 -> l0 }]) - - from there, find the corresponding tgt loan ([l0]) - - Note that this borrow does not necessarily appear in the src_to_tgt_borrow_map, - if it actually corresponds to a borrows introduced when decomposing the - abstractions to move the shared values out of the source context abstractions. - *) - let tgt_borrow_to_loan = ref V.BorrowId.InjSubst.empty in - let visit_tgt = - object - inherit [_] V.iter_abs - - method! visit_borrow_id _ id = - (* Find the target borrow *) - let tgt_borrow_id = V.BorrowId.Map.find id src_to_tgt_borrow_map in - (* Update the map *) - tgt_borrow_to_loan := - V.BorrowId.InjSubst.add id tgt_borrow_id !tgt_borrow_to_loan - end - in - List.iter (visit_tgt#visit_abs ()) new_absl; - - (* Compute the map from loan to borrows *) - let tgt_loan_to_borrow = - V.BorrowId.InjSubst.of_list - (List.map - (fun (x, y) -> (y, x)) - (V.BorrowId.InjSubst.bindings !tgt_borrow_to_loan)) - in - - (* Return *) - { - borrow_to_loan_id_map = !tgt_borrow_to_loan; - loan_to_borrow_id_map = tgt_loan_to_borrow; - } - -(** Match a context with a target context. - - This is used to compute application of loop translations: we use this - to introduce "identity" abstractions upon (re-)entering the loop. - - For instance, the fixed point for [list_nth_mut] (see the top of the file) - is: - {[ - env_fp = { - abs@0 { ML l0 } - ls -> MB l1 (s@3 : loops::List<T>) - i -> s@4 : u32 - abs@fp { - MB l0 - ML l1 - } - } - ]} - - Upon re-entering the loop, starting from the fixed point, we get the - following environment: - {[ - env = { - abs@0 { ML l0 } - ls -> MB l5 (s@6 : loops::List<T>) - i -> s@7 : u32 - abs@1 { MB l0, ML l1 } - _@1 -> MB l1 (loops::List::Cons (ML l2, ML l3)) - _@2 -> MB l3 (@Box (ML l5)) // tail - _@3 -> MB l2 (s@3 : T) // hd - } - ]} - - We want to introduce an abstraction [abs@2], which has the same shape as [abs@fp] - above (the fixed-point abstraction), and which is actually the identity. If we do so, - we get an environment which is actually also a fixed point (we can collapse - the dummy variables and [abs@1] to actually retrieve the fixed point we - computed, and we use the fact that those values and abstractions can't be - *directly* manipulated unless we end this newly introduced [abs@2], which we - forbid). - - We match the *fixed point context* with the context upon entering the loop - by doing the following. - - 1. We filter [env_fp] and [env] to remove the newly introduced dummy variables - and abstractions. We get: - - {[ - filtered_env_fp = { - abs@0 { ML l0 } - ls -> MB l1 (s@3 : loops::List<T>) - i -> s@4 : u32 - // removed abs@fp - } - - filtered_env = { - abs@0 { ML l0 } - ls -> MB l5 (s@6 : loops::List<T>) - i -> s@7 : u32 - // removed abs@1, _@1, etc. - } - ]} - - 2. We match [filtered_env_fp] with [filtered_env] to compute a map from - the FP borrows/loans to the current borrows/loans (and also from symbolic values to - values). Note that we take care to *consider loans and borrows separately*, - and we ignore the "fixed" abstractions (which are unchanged - we checked that - when computing the fixed point). - We get: - {[ - borrows_map: { l1 -> l5 } // because we matched [MB l1 ...] with [MB l5 ...] - loans_map: {} // we ignore abs@0, which is "fixed" - ]} - - 3. We want to introduce an instance of [abs@fp] which is actually the - identity. From [compute_fixed_point_id_correspondance] and looking at - [abs@fp], we know we should link the instantiation of loan [l1] with the - instantiation of loan [l0]. We substitute [l0] with [l5] (following step 2.) - and introduce a fresh borrow [l6] for [l5] that we use to instantiate [l1]. - We get the following environment: - - {[ - env = { - abs@0 { ML l0 } - ls -> MB l6 (s@6 : loops::List<T>) - i -> s@7 : u32 - abs@1 { MB l0, ML l1 } - _@1 -> MB l1 (loops::List::Cons (ML l2, ML l3)) - _@2 -> MB l3 (@Box (ML l5)) // tail - _@3 -> MB l2 (s@3 : T) // hd - abs@2 { MB l5, ML l6 } // this is actually the identity: l6 = l5 - } - ]} - - 4. As we now have a fixed point (see above comments), we can consider than - [abs@2] links the current iteration to the last one before we exit. What we - are interested in is that: - - upon inserting [abs@2] we re-entered the loop, meaning in the translation - we need to insert a recursive call to the loop forward function - - upon ending [abs@2] we need to insert a call to the loop backward function - - Because we want to ignore them, we end the loans in the newly introduced - [abs@2] abstraction (i.e., [l6]). We get: - {[ - env = { - abs@0 { ML l0 } - ls -> ⊥ - i -> s@7 : u32 - abs@1 { MB l0, ML l1 } - _@1 -> MB l1 (loops::List::Cons (ML l2, ML l3)) - _@2 -> MB l3 (@Box (ML l5)) // tail - _@3 -> MB l2 (s@3 : T) // hd - abs@2 { MB l5 } - } - ]} - - TODO: we shouldn't need to end the loans, we should actually remove them - before inserting the new abstractions (we may have issues with the symbolic - values, if they contain borrows - above i points to [s@7], but it should - be a different symbolic value...). - - Finally, we use the map from symbolic values to values to compute the list of - input values of the loop: we simply list the values, by order of increasing - symbolic value id. We *do* use the fixed values (though they are in the frame) - because they may be *read* inside the loop. - - We can then proceed to finishing the symbolic execution and doing the - synthesis. - - Rem.: we might reorganize the [tgt_ctx] by ending loans for instance. - - **Parameters**: - [is_loop_entry]: [true] if first entry into the loop, [false] if re-entry - (i.e., continue). - [fp_input_svalues]: the list of symbolic values appearing in the fixed - point and which must be instantiated during the match (this is the list - of input parameters of the loop). - *) -let match_ctx_with_target (config : C.config) (loop_id : V.LoopId.id) - (is_loop_entry : bool) (fp_bl_maps : borrow_loan_corresp) - (fp_input_svalues : V.SymbolicValueId.id list) (fixed_ids : ids_sets) - (src_ctx : C.eval_ctx) : st_cm_fun = - fun cf tgt_ctx -> - (* Debug *) - log#ldebug - (lazy - ("match_ctx_with_target:\n" ^ "\n- fixed_ids: " ^ show_ids_sets fixed_ids - ^ "\n" ^ "\n- src_ctx: " ^ eval_ctx_to_string src_ctx ^ "\n- tgt_ctx: " - ^ eval_ctx_to_string tgt_ctx)); - - (* We first reorganize [tgt_ctx] so that we can match [src_ctx] with it (by - ending loans for instance - remember that the [src_ctx] is the fixed point - context, which results from joins during which we ended the loans which - were introduced during the loop iterations) - *) - (* End the loans which lead to mismatches when joining *) - let rec cf_reorganize_join_tgt : cm_fun = - fun cf tgt_ctx -> - (* Collect fixed values in the source and target contexts: end the loans in the - source context which don't appear in the target context *) - let filt_src_env, _, _ = ctx_split_fixed_new fixed_ids src_ctx in - let filt_tgt_env, _, _ = ctx_split_fixed_new fixed_ids tgt_ctx in - - log#ldebug - (lazy - ("match_ctx_with_target:\n" ^ "\n- fixed_ids: " - ^ show_ids_sets fixed_ids ^ "\n" ^ "\n- filt_src_ctx: " - ^ env_to_string src_ctx filt_src_env - ^ "\n- filt_tgt_ctx: " - ^ env_to_string tgt_ctx filt_tgt_env)); - - (* Remove the abstractions *) - let filter (ee : C.env_elem) : bool = - match ee with Var _ -> true | Abs _ | Frame -> false - in - let filt_src_env = List.filter filter filt_src_env in - let filt_tgt_env = List.filter filter filt_tgt_env in - - (* Match the values to check if there are loans to eliminate *) - - (* We need to pick a context for some functions like [match_typed_values]: - the context is only used to lookup module data, so we can pick whichever - we want. - TODO: this is not very clean. Maybe we should just carry this data around. - *) - let ctx = tgt_ctx in - - let nabs = ref [] in - - let module S : MatchJoinState = struct - (* The context is only used to lookup module data: we can pick whichever we want *) - let ctx = ctx - let loop_id = loop_id - let nabs = nabs - end in - let module JM = MakeJoinMatcher (S) in - let module M = Match (JM) in - try - let _ = - List.iter - (fun (var0, var1) -> - match (var0, var1) with - | C.Var (C.DummyBinder b0, v0), C.Var (C.DummyBinder b1, v1) -> - assert (b0 = b1); - let _ = M.match_typed_values ctx v0 v1 in - () - | C.Var (C.VarBinder b0, v0), C.Var (C.VarBinder b1, v1) -> - assert (b0 = b1); - let _ = M.match_typed_values ctx v0 v1 in - () - | _ -> raise (Failure "Unexpected")) - (List.combine filt_src_env filt_tgt_env) - in - (* No exception was thrown: continue *) - cf tgt_ctx - with ValueMatchFailure e -> - (* Exception: end the corresponding borrows, and continue *) - let cc = - match e with - | LoanInRight bid -> InterpreterBorrows.end_borrow config bid - | LoansInRight bids -> InterpreterBorrows.end_borrows config bids - | AbsInRight _ | AbsInLeft _ | LoanInLeft _ | LoansInLeft _ -> - raise (Failure "Unexpected") - in - comp cc cf_reorganize_join_tgt cf tgt_ctx - in - - (* Introduce the "identity" abstractions for the loop reentry. - - Match the target context with the source context so as to compute how to - map the borrows from the target context (i.e., the fixed point context) - to the borrows in the source context. - - Substitute the *loans* in the abstractions introduced by the target context - (the abstractions of the fixed point) to properly link those abstraction: - we introduce *identity* abstractions (the loans are equal to the borrows): - we substitute the loans and introduce fresh ids for the borrows, symbolic - values, etc. About the *identity abstractions*, see the comments for - [compute_fixed_point_id_correspondance]. - - TODO: this whole thing is very technical and error-prone. - We should rely on a more primitive and safer function - [add_identity_abs] to add the identity abstractions one by one. - *) - let cf_introduce_loop_fp_abs : m_fun = - fun tgt_ctx -> - (* Match the source and target contexts *) - let filt_tgt_env, _, _ = ctx_split_fixed_new fixed_ids tgt_ctx in - let filt_src_env, new_absl, new_dummyl = - ctx_split_fixed_new fixed_ids src_ctx - in - assert (new_dummyl = []); - let filt_tgt_ctx = { tgt_ctx with env = filt_tgt_env } in - let filt_src_ctx = { src_ctx with env = filt_src_env } in - - let src_to_tgt_maps = - let check_equiv = false in - let fixed_ids = ids_sets_empty_borrows_loans fixed_ids in - let open InterpreterBorrowsCore in - let lookup_shared_loan lid ctx : V.typed_value = - match snd (lookup_loan ek_all lid ctx) with - | Concrete (V.SharedLoan (_, v)) -> v - | Abstract (V.ASharedLoan (_, v, _)) -> v - | _ -> raise (Failure "Unreachable") - in - let lookup_in_src id = lookup_shared_loan id src_ctx in - let lookup_in_tgt id = lookup_shared_loan id tgt_ctx in - (* Match *) - Option.get - (match_ctxs check_equiv fixed_ids lookup_in_src lookup_in_tgt - filt_src_ctx filt_tgt_ctx) - in - let tgt_to_src_borrow_map = - V.BorrowId.Map.of_list - (List.map - (fun (x, y) -> (y, x)) - (V.BorrowId.InjSubst.bindings src_to_tgt_maps.borrow_id_map)) - in - - (* Debug *) - log#ldebug - (lazy - ("match_ctx_with_target: cf_introduce_loop_fp_abs:" ^ "\n\n- tgt_ctx: " - ^ eval_ctx_to_string tgt_ctx ^ "\n\n- src_ctx: " - ^ eval_ctx_to_string src_ctx ^ "\n\n- filt_tgt_ctx: " - ^ eval_ctx_to_string_no_filter filt_tgt_ctx - ^ "\n\n- filt_src_ctx: " - ^ eval_ctx_to_string_no_filter filt_src_ctx - ^ "\n\n- new_absl:\n" - ^ eval_ctx_to_string - { src_ctx with C.env = List.map (fun abs -> C.Abs abs) new_absl } - ^ "\n\n- fixed_ids:\n" ^ show_ids_sets fixed_ids ^ "\n\n- fp_bl_maps:\n" - ^ show_borrow_loan_corresp fp_bl_maps - ^ "\n\n- src_to_tgt_maps: " - ^ show_ids_maps src_to_tgt_maps)); - - (* Update the borrows and symbolic ids in the source context. - - Going back to the [list_nth_mut_example], the original environment upon - re-entering the loop is: - - {[ - abs@0 { ML l0 } - ls -> MB l5 (s@6 : loops::List<T>) - i -> s@7 : u32 - _@1 -> MB l0 (loops::List::Cons (ML l1, ML l2)) - _@2 -> MB l2 (@Box (ML l4)) // tail - _@3 -> MB l1 (s@3 : T) // hd - abs@1 { MB l4, ML l5 } - ]} - - The fixed-point environment is: - {[ - env_fp = { - abs@0 { ML l0 } - ls -> MB l1 (s3 : loops::List<T>) - i -> s4 : u32 - abs@fp { - MB l0 // this borrow appears in [env0] - ML l1 - } - } - ]} - - Through matching, we detect that in [env_fp], [l1] is matched - to [l5]. We introduce a fresh borrow [l6] for [l1], and remember - in the map [src_fresh_borrows_map] that: [{ l1 -> l6}]. - - We get: - {[ - abs@0 { ML l0 } - ls -> MB l6 (s@6 : loops::List<T>) // l6 is fresh and doesn't have a corresponding loan - i -> s@7 : u32 - _@1 -> MB l0 (loops::List::Cons (ML l1, ML l2)) - _@2 -> MB l2 (@Box (ML l4)) // tail - _@3 -> MB l1 (s@3 : T) // hd - abs@1 { MB l4, ML l5 } - ]} - - Later, we will introduce the identity abstraction: - {[ - abs@2 { MB l5, ML l6 } - ]} - *) - (* First, compute the set of borrows which appear in the fresh abstractions - of the fixed-point: we want to introduce fresh ids only for those. *) - let new_absl_ids, _ = compute_absl_ids new_absl in - let src_fresh_borrows_map = ref V.BorrowId.Map.empty in - let visit_tgt = - object - inherit [_] C.map_eval_ctx - - method! visit_borrow_id _ id = - (* Map the borrow, if it needs to be mapped *) - if - (* We map the borrows for which we computed a mapping *) - V.BorrowId.InjSubst.Set.mem id - (V.BorrowId.InjSubst.elements src_to_tgt_maps.borrow_id_map) - (* And which have corresponding loans in the fresh fixed-point abstractions *) - && V.BorrowId.Set.mem - (V.BorrowId.Map.find id tgt_to_src_borrow_map) - new_absl_ids.loan_ids - then ( - let src_id = V.BorrowId.Map.find id tgt_to_src_borrow_map in - let nid = C.fresh_borrow_id () in - src_fresh_borrows_map := - V.BorrowId.Map.add src_id nid !src_fresh_borrows_map; - nid) - else id - end - in - let tgt_ctx = visit_tgt#visit_eval_ctx () tgt_ctx in - - log#ldebug - (lazy - ("match_ctx_with_target: cf_introduce_loop_fp_abs: \ - src_fresh_borrows_map:\n" - ^ V.BorrowId.Map.show V.BorrowId.to_string !src_fresh_borrows_map - ^ "\n")); - - (* Rem.: we don't update the symbolic values. It is not necessary - because there shouldn't be any symbolic value containing borrows. - - Rem.: we will need to do something about the symbolic values in the - abstractions and in the *variable bindings* once we allow symbolic - values containing borrows to not be eagerly expanded. - *) - assert Config.greedy_expand_symbolics_with_borrows; - - (* Update the borrows and loans in the abstractions of the target context. - - Going back to the [list_nth_mut] example and by using [src_fresh_borrows_map], - we instantiate the fixed-point abstractions that we will insert into the - context. - The abstraction is [abs { MB l0, ML l1 }]. - Because of [src_fresh_borrows_map], we substitute [l1] with [l6]. - Because of the match between the contexts, we substitute [l0] with [l5]. - We get: - {[ - abs@2 { MB l5, ML l6 } - ]} - *) - let region_id_map = ref T.RegionId.Map.empty in - let get_rid rid = - match T.RegionId.Map.find_opt rid !region_id_map with - | Some rid -> rid - | None -> - let nid = C.fresh_region_id () in - region_id_map := T.RegionId.Map.add rid nid !region_id_map; - nid - in - let visit_src = - object - inherit [_] C.map_eval_ctx as super - - method! visit_borrow_id _ bid = - log#ldebug - (lazy - ("match_ctx_with_target: cf_introduce_loop_fp_abs: \ - visit_borrow_id: " ^ V.BorrowId.to_string bid ^ "\n")); - - (* Lookup the id of the loan corresponding to this borrow *) - let src_lid = - V.BorrowId.InjSubst.find bid fp_bl_maps.borrow_to_loan_id_map - in - - log#ldebug - (lazy - ("match_ctx_with_target: cf_introduce_loop_fp_abs: looked up \ - src_lid: " - ^ V.BorrowId.to_string src_lid - ^ "\n")); - - (* Lookup the tgt borrow id to which this borrow was mapped *) - let tgt_bid = - V.BorrowId.InjSubst.find src_lid src_to_tgt_maps.borrow_id_map - in - - log#ldebug - (lazy - ("match_ctx_with_target: cf_introduce_loop_fp_abs: looked up \ - tgt_bid: " - ^ V.BorrowId.to_string tgt_bid - ^ "\n")); - - tgt_bid - - method! visit_loan_id _ id = - log#ldebug - (lazy - ("match_ctx_with_target: cf_introduce_loop_fp_abs: \ - visit_loan_id: " ^ V.BorrowId.to_string id ^ "\n")); - (* Map the borrow - rem.: we mapped the borrows *in the values*, - meaning we know how to map the *corresponding loans in the - abstractions* *) - match V.BorrowId.Map.find_opt id !src_fresh_borrows_map with - | None -> - (* No mapping: this means that the borrow was mapped when - we matched values (it doesn't come from a fresh abstraction) - and because of this, it should actually be mapped to itself *) - assert ( - V.BorrowId.InjSubst.find id src_to_tgt_maps.borrow_id_map = id); - id - | Some id -> id - - method! visit_symbolic_value_id _ _ = C.fresh_symbolic_value_id () - method! visit_abstraction_id _ _ = C.fresh_abstraction_id () - method! visit_region_id _ id = get_rid id - - (** We also need to change the abstraction kind *) - method! visit_abs env abs = - match abs.kind with - | V.Loop (loop_id', rg_id, kind) -> - assert (loop_id' = loop_id); - assert (kind = V.LoopSynthInput); - let can_end = false in - let kind = V.Loop (loop_id, rg_id, V.LoopCall) in - let abs = { abs with kind; can_end } in - super#visit_abs env abs - | _ -> super#visit_abs env abs - end - in - let new_absl = List.map (visit_src#visit_abs ()) new_absl in - let new_absl = List.map (fun abs -> C.Abs abs) new_absl in - - (* Add the abstractions from the target context to the source context *) - let nenv = List.append new_absl tgt_ctx.env in - let tgt_ctx = { tgt_ctx with env = nenv } in - - log#ldebug - (lazy - ("match_ctx_with_target:cf_introduce_loop_fp_abs:\n- result ctx:\n" - ^ eval_ctx_to_string tgt_ctx)); - - (* Sanity check *) - if !Config.check_invariants then - Invariants.check_borrowed_values_invariant tgt_ctx; - - (* End all the borrows which appear in the *new* abstractions *) - let new_borrows = - V.BorrowId.Set.of_list - (List.map snd (V.BorrowId.Map.bindings !src_fresh_borrows_map)) - in - let cc = InterpreterBorrows.end_borrows config new_borrows in - - (* Compute the loop input values *) - let input_values = - V.SymbolicValueId.Map.of_list - (List.map - (fun sid -> - ( sid, - V.SymbolicValueId.Map.find sid src_to_tgt_maps.sid_to_value_map - )) - fp_input_svalues) - in - - (* Continue *) - cc - (cf - (if is_loop_entry then EndEnterLoop (loop_id, input_values) - else EndContinue (loop_id, input_values))) - tgt_ctx - in - - (* Compose and continue *) - cf_reorganize_join_tgt cf_introduce_loop_fp_abs tgt_ctx - (** Evaluate a loop in concrete mode *) let eval_loop_concrete (eval_loop_body : st_cm_fun) : st_cm_fun = fun cf ctx -> @@ -4036,129 +65,6 @@ let eval_loop_concrete (eval_loop_body : st_cm_fun) : st_cm_fun = (* Apply *) eval_loop_body reeval_loop_body ctx -(** Compute the set of "quantified" symbolic value ids in a fixed-point context. - - We compute: - - the set of symbolic value ids that are freshly introduced - - the list of input symbolic values - *) -let compute_fp_ctx_symbolic_values (ctx : C.eval_ctx) (fp_ctx : C.eval_ctx) : - V.SymbolicValueId.Set.t * V.symbolic_value list = - let old_ids, _ = compute_context_ids ctx in - let fp_ids, fp_ids_maps = compute_context_ids fp_ctx in - let fresh_sids = V.SymbolicValueId.Set.diff fp_ids.sids old_ids.sids in - - (* Compute the set of symbolic values which appear in shared values inside - *fixed* abstractions: because we introduce fresh abstractions and reborrows - with {!prepare_ashared_loans}, those values are never accessed directly - inside the loop iterations: we can ignore them (and should, because - otherwise it leads to a very ugly translation with duplicated, unused - values) *) - let shared_sids_in_fixed_abs = - let fixed_absl = - List.filter - (fun (ee : C.env_elem) -> - match ee with - | C.Var _ | C.Frame -> false - | Abs abs -> V.AbstractionId.Set.mem abs.abs_id old_ids.aids) - ctx.env - in - - (* Rem.: as we greedily expand the symbolic values containing borrows, and - in particular the (mutable/shared) borrows, we could simply list the - symbolic values appearing in the abstractions: those are necessarily - shared values. We prefer to be more general, in prevision of later - changes. - *) - let sids = ref V.SymbolicValueId.Set.empty in - let visitor = - object (self) - inherit [_] C.iter_env - - method! visit_ASharedLoan inside_shared _ sv child_av = - self#visit_typed_value true sv; - self#visit_typed_avalue inside_shared child_av - - method! visit_symbolic_value_id inside_shared sid = - if inside_shared then sids := V.SymbolicValueId.Set.add sid !sids - end - in - visitor#visit_env false fixed_absl; - !sids - in - - (* Remove the shared symbolic values present in the fixed abstractions - - see comments for [shared_sids_in_fixed_abs]. *) - let sids_to_values = fp_ids_maps.sids_to_values in - - log#ldebug - (lazy - ("compute_fp_ctx_symbolic_values:" ^ "\n- shared_sids_in_fixed_abs:" - ^ V.SymbolicValueId.Set.show shared_sids_in_fixed_abs - ^ "\n- all_sids_to_values: " - ^ V.SymbolicValueId.Map.show (symbolic_value_to_string ctx) sids_to_values - ^ "\n")); - - let sids_to_values = - V.SymbolicValueId.Map.filter - (fun sid _ -> - not (V.SymbolicValueId.Set.mem sid shared_sids_in_fixed_abs)) - sids_to_values - in - - (* List the input symbolic values in proper order. - - We explore the environment, and order the symbolic values in the order - in which they are found - this way, the symbolic values found in a - variable [x] which appears before [y] are listed first, for instance. - *) - let input_svalues = - let found_sids = ref V.SymbolicValueId.Set.empty in - let ordered_sids = ref [] in - - let visitor = - object (self) - inherit [_] C.iter_env - - (** We lookup the shared values *) - method! visit_SharedBorrow env bid = - let open InterpreterBorrowsCore in - let v = - match snd (lookup_loan ek_all bid fp_ctx) with - | Concrete (V.SharedLoan (_, v)) -> v - | Abstract (V.ASharedLoan (_, v, _)) -> v - | _ -> raise (Failure "Unreachable") - in - self#visit_typed_value env v - - method! visit_symbolic_value_id _ id = - if not (V.SymbolicValueId.Set.mem id !found_sids) then ( - found_sids := V.SymbolicValueId.Set.add id !found_sids; - ordered_sids := id :: !ordered_sids) - end - in - - List.iter (visitor#visit_env_elem ()) (List.rev fp_ctx.env); - - List.filter_map - (fun id -> V.SymbolicValueId.Map.find_opt id sids_to_values) - (List.rev !ordered_sids) - in - - log#ldebug - (lazy - ("compute_fp_ctx_symbolic_values:" ^ "\n- src context:\n" - ^ eval_ctx_to_string_no_filter ctx - ^ "\n- fixed point:\n" - ^ eval_ctx_to_string_no_filter fp_ctx - ^ "\n- fresh_sids: " - ^ V.SymbolicValueId.Set.show fresh_sids - ^ "\n- input_svalues: " - ^ Print.list_to_string (symbolic_value_to_string ctx) input_svalues - ^ "\n\n")); - - (fresh_sids, input_svalues) - (** Evaluate a loop in symbolic mode *) let eval_loop_symbolic (config : C.config) (eval_loop_body : st_cm_fun) : st_cm_fun = @@ -4307,7 +213,6 @@ let eval_loop_symbolic (config : C.config) (eval_loop_body : st_cm_fun) : S.synthesize_loop loop_id input_svalues fresh_sids rg_to_given_back end_expr loop_expr -(** Evaluate a loop *) let eval_loop (config : C.config) (eval_loop_body : st_cm_fun) : st_cm_fun = fun cf ctx -> match config.C.mode with diff --git a/compiler/InterpreterLoops.mli b/compiler/InterpreterLoops.mli new file mode 100644 index 00000000..7395739b --- /dev/null +++ b/compiler/InterpreterLoops.mli @@ -0,0 +1,62 @@ +(** This module implements support for loops. + + Throughout the module, we will use the following code as example to + illustrate what the functions do (this function simply returns a mutable + borrow to the nth element of a list): + {[ + pub fn list_nth_mut<'a, T>(mut ls: &'a mut List<T>, mut i: u32) -> &'a mut T { + loop { + match ls { + List::Nil => { + panic!() + } + List::Cons(x, tl) => { + if i == 0 { + return x; + } else { + ls = tl; + i -= 1; + } + } + } + } + } + ]} + + Upon reaching the loop entry, the environment is as follows (ignoring the + dummy variables): + {[ + abs@0 { ML l0 } + ls -> MB l0 (s2 : loops::List<T>) + i -> s1 : u32 + ]} + + Upon reaching the [continue] at the end of the first iteration, the environment + is as follows: + {[ + abs@0 { ML l0 } + ls -> MB l4 (s@6 : loops::List<T>) + i -> s@7 : u32 + _@1 -> MB l0 (loops::List::Cons (ML l1, ML l2)) + _@2 -> MB l2 (@Box (ML l4)) // tail + _@3 -> MB l1 (s@3 : T) // hd + ]} + + The fixed point we compute is: + {[ + abs@0 { ML l0 } + ls -> MB l1 (s@3 : loops::List<T>) + i -> s@4 : u32 + abs@fp { // fp: fixed-point + MB l0 + ML l1 + } + ]} + + From here, we deduce that [abs@fp { MB l0, ML l1}] is the loop abstraction. + *) + +module C = Contexts + +(** Evaluate a loop *) +val eval_loop : C.config -> Cps.st_cm_fun -> Cps.st_cm_fun diff --git a/compiler/InterpreterLoopsCore.ml b/compiler/InterpreterLoopsCore.ml new file mode 100644 index 00000000..209fce1c --- /dev/null +++ b/compiler/InterpreterLoopsCore.ml @@ -0,0 +1,386 @@ +(** Core definitions for the [IntepreterLoops*] *) + +module T = Types +module PV = PrimitiveValues +module V = Values +module E = Expressions +module C = Contexts +module Subst = Substitute +module A = LlbcAst +module L = Logging +module Inv = Invariants +module S = SynthesizeSymbolic +module UF = UnionFind +open InterpreterUtils +open InterpreterExpressions + +type updt_env_kind = + | AbsInLeft of V.AbstractionId.id + | LoanInLeft of V.BorrowId.id + | LoansInLeft of V.BorrowId.Set.t + | AbsInRight of V.AbstractionId.id + | LoanInRight of V.BorrowId.id + | LoansInRight of V.BorrowId.Set.t + +(** Utility exception *) +exception ValueMatchFailure of updt_env_kind + +(** Utility exception *) +exception Distinct of string + +type ctx_or_update = (C.eval_ctx, updt_env_kind) result + +(** Union Find *) +module UnionFind = UF.Make (UF.StoreMap) + +(** A small utility - + + Rem.: some environments may be ill-formed (they may contain several times + the same loan or borrow - this happens for instance when merging + environments). This is the reason why we use sets in some places (for + instance, [borrow_to_abs] maps to a *set* of ids). +*) +type abs_borrows_loans_maps = { + abs_ids : V.AbstractionId.id list; + abs_to_borrows : V.BorrowId.Set.t V.AbstractionId.Map.t; + abs_to_loans : V.BorrowId.Set.t V.AbstractionId.Map.t; + abs_to_borrows_loans : V.BorrowId.Set.t V.AbstractionId.Map.t; + borrow_to_abs : V.AbstractionId.Set.t V.BorrowId.Map.t; + loan_to_abs : V.AbstractionId.Set.t V.BorrowId.Map.t; + borrow_loan_to_abs : V.AbstractionId.Set.t V.BorrowId.Map.t; +} + +(** See {!InterpreterLoopsMatchCtxs.MakeMatcher} and {!InterpreterLoopsCore.Matcher}. + + This module contains primitive match functions to instantiate the generic + {!InterpreterLoopsMatchCtxs.MakeMatcher} functor. + *) +module type PrimMatcher = sig + val match_etys : T.ety -> T.ety -> T.ety + val match_rtys : T.rty -> T.rty -> T.rty + + (** The input primitive values are not equal *) + val match_distinct_primitive_values : + T.ety -> V.primitive_value -> V.primitive_value -> V.typed_value + + (** The input ADTs don't have the same variant *) + val match_distinct_adts : T.ety -> V.adt_value -> V.adt_value -> V.typed_value + + (** The meta-value is the result of a match. + + We take an additional function as input, which acts as a matcher over + typed values, to be able to lookup the shared values and match them. + We do this for shared borrows (and not, e.g., mutable borrows) because + shared borrows introduce indirections, while mutable borrows carry the + borrowed values with them: we might want to explore and match those + borrowed values, in which case we have to manually look them up before + calling the match function. + *) + val match_shared_borrows : + (V.typed_value -> V.typed_value -> V.typed_value) -> + T.ety -> + V.borrow_id -> + V.borrow_id -> + V.borrow_id + + (** The input parameters are: + - [ty] + - [bid0]: first borrow id + - [bv0]: first borrowed value + - [bid1] + - [bv1] + - [bv]: the result of matching [bv0] with [bv1] + *) + val match_mut_borrows : + T.ety -> + V.borrow_id -> + V.typed_value -> + V.borrow_id -> + V.typed_value -> + V.typed_value -> + V.borrow_id * V.typed_value + + (** Parameters: + [ty] + [ids0] + [ids1] + [v]: the result of matching the shared values coming from the two loans + *) + val match_shared_loans : + T.ety -> + V.loan_id_set -> + V.loan_id_set -> + V.typed_value -> + V.loan_id_set * V.typed_value + + val match_mut_loans : T.ety -> V.loan_id -> V.loan_id -> V.loan_id + + (** There are no constraints on the input symbolic values *) + val match_symbolic_values : + V.symbolic_value -> V.symbolic_value -> V.symbolic_value + + (** Match a symbolic value with a value which is not symbolic. + + If the boolean is [true], it means the symbolic value comes from the + *left* environment. Otherwise it comes from the right environment (it + is important when throwing exceptions, for instance when we need to + end loans in one of the two environments). + *) + val match_symbolic_with_other : + bool -> V.symbolic_value -> V.typed_value -> V.typed_value + + (** Match a bottom value with a value which is not bottom. + + If the boolean is [true], it means the bottom value comes from the + *left* environment. Otherwise it comes from the right environment (it + is important when throwing exceptions, for instance when we need to + end loans in one of the two environments). + *) + val match_bottom_with_other : bool -> V.typed_value -> V.typed_value + + (** The input ADTs don't have the same variant *) + val match_distinct_aadts : + T.rty -> V.adt_avalue -> T.rty -> V.adt_avalue -> T.rty -> V.typed_avalue + + (** Parameters: + [ty0] + [bid0] + [ty1] + [bid1] + [ty]: result of matching ty0 and ty1 + *) + val match_ashared_borrows : + T.rty -> V.borrow_id -> T.rty -> V.borrow_id -> T.rty -> V.typed_avalue + + (** Parameters: + [ty0] + [bid0] + [av0] + [ty1] + [bid1] + [av1] + [ty]: result of matching ty0 and ty1 + [av]: result of matching av0 and av1 + *) + val match_amut_borrows : + T.rty -> + V.borrow_id -> + V.typed_avalue -> + T.rty -> + V.borrow_id -> + V.typed_avalue -> + T.rty -> + V.typed_avalue -> + V.typed_avalue + + (** Parameters: + [ty0] + [ids0] + [v0] + [av0] + [ty1] + [ids1] + [v1] + [av1] + [ty]: result of matching ty0 and ty1 + [v]: result of matching v0 and v1 + [av]: result of matching av0 and av1 + *) + val match_ashared_loans : + T.rty -> + V.loan_id_set -> + V.typed_value -> + V.typed_avalue -> + T.rty -> + V.loan_id_set -> + V.typed_value -> + V.typed_avalue -> + T.rty -> + V.typed_value -> + V.typed_avalue -> + V.typed_avalue + + (** Parameters: + [ty0] + [id0] + [av0] + [ty1] + [id1] + [av1] + [ty]: result of matching ty0 and ty1 + [av]: result of matching av0 and av1 + *) + val match_amut_loans : + T.rty -> + V.borrow_id -> + V.typed_avalue -> + T.rty -> + V.borrow_id -> + V.typed_avalue -> + T.rty -> + V.typed_avalue -> + V.typed_avalue + + (** Match two arbitrary avalues whose constructors don't match (this function + is typically used to raise the proper exception). + *) + val match_avalues : V.typed_avalue -> V.typed_avalue -> V.typed_avalue +end + +module type Matcher = sig + (** Match two values. + + Rem.: this function raises exceptions of type {!Aeneas.InterpreterLoopsCore.ValueMatchFailure}. + *) + val match_typed_values : + C.eval_ctx -> V.typed_value -> V.typed_value -> V.typed_value + + (** Match two avalues. + + Rem.: this function raises exceptions of type {!Aeneas.InterpreterLoopsCore.ValueMatchFailure}. + *) + val match_typed_avalues : + C.eval_ctx -> V.typed_avalue -> V.typed_avalue -> V.typed_avalue +end + +(** See {!InterpreterLoopsMatchCtxs.MakeCheckEquivMatcher} and + {!InterpreterLoopsCore.CheckEquivMatcher}. + + Very annoying: functors only take modules as inputs... + *) +module type MatchCheckEquivState = sig + (** [true] if we check equivalence between contexts, [false] if we match + a source context with a target context. *) + val check_equiv : bool + + val ctx : C.eval_ctx + val rid_map : T.RegionId.InjSubst.t ref + + (** Substitution for the loan and borrow ids - used only if [check_equiv] is true *) + val blid_map : V.BorrowId.InjSubst.t ref + + (** Substitution for the borrow ids - used only if [check_equiv] is false *) + val borrow_id_map : V.BorrowId.InjSubst.t ref + + (** Substitution for the loans ids - used only if [check_equiv] is false *) + val loan_id_map : V.BorrowId.InjSubst.t ref + + val sid_map : V.SymbolicValueId.InjSubst.t ref + val sid_to_value_map : V.typed_value V.SymbolicValueId.Map.t ref + val aid_map : V.AbstractionId.InjSubst.t ref + val lookup_shared_value_in_ctx0 : V.BorrowId.id -> V.typed_value + val lookup_shared_value_in_ctx1 : V.BorrowId.id -> V.typed_value +end + +module type CheckEquivMatcher = sig + include PrimMatcher + + val match_aid : V.abstraction_id -> V.abstraction_id -> V.abstraction_id + + val match_aidl : + V.abstraction_id list -> V.abstraction_id list -> V.abstraction_id list + + val match_aids : + V.abstraction_id_set -> V.abstraction_id_set -> V.abstraction_id_set + + val match_rid : V.region_id -> V.region_id -> V.region_id + val match_rids : V.region_id_set -> V.region_id_set -> V.region_id_set + val match_borrow_id : V.borrow_id -> V.borrow_id -> V.borrow_id + + val match_borrow_idl : + V.borrow_id list -> V.borrow_id list -> V.borrow_id list + + val match_borrow_ids : V.borrow_id_set -> V.borrow_id_set -> V.borrow_id_set + val match_loan_id : V.loan_id -> V.loan_id -> V.loan_id + val match_loan_idl : V.loan_id list -> V.loan_id list -> V.loan_id list + val match_loan_ids : V.loan_id_set -> V.loan_id_set -> V.loan_id_set +end + +(** See {!InterpreterLoopsMatchCtxs.match_ctxs} *) +type ids_maps = { + aid_map : V.AbstractionId.InjSubst.t; + blid_map : V.BorrowId.InjSubst.t; + (** Substitution for the loan and borrow ids *) + borrow_id_map : V.BorrowId.InjSubst.t; (** Substitution for the borrow ids *) + loan_id_map : V.BorrowId.InjSubst.t; (** Substitution for the loan ids *) + rid_map : T.RegionId.InjSubst.t; + sid_map : V.SymbolicValueId.InjSubst.t; + sid_to_value_map : V.typed_value V.SymbolicValueId.Map.t; +} +[@@deriving show] + +type borrow_loan_corresp = { + borrow_to_loan_id_map : V.BorrowId.InjSubst.t; + loan_to_borrow_id_map : V.BorrowId.InjSubst.t; +} +[@@deriving show] + +(* Very annoying: functors only take modules as inputs... *) +module type MatchJoinState = sig + (** The current context *) + val ctx : C.eval_ctx + + (** The current loop *) + val loop_id : V.LoopId.id + + (** The abstractions introduced when performing the matches *) + val nabs : V.abs list ref +end + +(** Split an environment between the fixed abstractions, values, etc. and + the new abstractions, values, etc. + + Returns: (fixed, new abs, new dummies) + *) +let ctx_split_fixed_new (fixed_ids : ids_sets) (ctx : C.eval_ctx) : + C.env * V.abs list * V.typed_value list = + let is_fresh_did (id : C.DummyVarId.id) : bool = + not (C.DummyVarId.Set.mem id fixed_ids.dids) + in + let is_fresh_abs_id (id : V.AbstractionId.id) : bool = + not (V.AbstractionId.Set.mem id fixed_ids.aids) + in + (* Filter the new abstractions and dummy variables (there shouldn't be any new dummy variable + though) in the target context *) + let is_fresh (ee : C.env_elem) : bool = + match ee with + | C.Var (VarBinder _, _) | C.Frame -> false + | C.Var (DummyBinder bv, _) -> is_fresh_did bv + | C.Abs abs -> is_fresh_abs_id abs.abs_id + in + let new_eel, filt_env = List.partition is_fresh ctx.env in + let is_abs ee = match ee with C.Abs _ -> true | _ -> false in + let new_absl, new_dummyl = List.partition is_abs new_eel in + let new_absl = + List.map + (fun ee -> + match ee with C.Abs abs -> abs | _ -> raise (Failure "Unreachable")) + new_absl + in + let new_dummyl = + List.map + (fun ee -> + match ee with + | C.Var (DummyBinder _, v) -> v + | _ -> raise (Failure "Unreachable")) + new_dummyl + in + (filt_env, new_absl, new_dummyl) + +let ids_sets_empty_borrows_loans (ids : ids_sets) : ids_sets = + let { aids; blids = _; borrow_ids = _; loan_ids = _; dids; rids; sids } = + ids + in + let empty = V.BorrowId.Set.empty in + let ids = + { + aids; + blids = empty; + borrow_ids = empty; + loan_ids = empty; + dids; + rids; + sids; + } + in + ids diff --git a/compiler/InterpreterLoopsFixedPoint.ml b/compiler/InterpreterLoopsFixedPoint.ml new file mode 100644 index 00000000..aff8f3fe --- /dev/null +++ b/compiler/InterpreterLoopsFixedPoint.ml @@ -0,0 +1,965 @@ +module T = Types +module PV = PrimitiveValues +module V = Values +module E = Expressions +module C = Contexts +module Subst = Substitute +module A = LlbcAst +module L = Logging +open TypesUtils +open ValuesUtils +module Inv = Invariants +module S = SynthesizeSymbolic +open Cps +open InterpreterUtils +open InterpreterLoopsCore +open InterpreterLoopsMatchCtxs +open InterpreterLoopsJoinCtxs + +(** The local logger *) +let log = L.loops_fixed_point_log + +(** Reorder the loans and borrows in the fresh abstractions. + + We do this in order to enforce some structure in the environments: this + allows us to find fixed-points. Note that this function needs to be + called typically after we merge abstractions together (see {!collapse_ctx} + for instance). + *) +let reorder_loans_borrows_in_fresh_abs (old_abs_ids : V.AbstractionId.Set.t) + (ctx : C.eval_ctx) : C.eval_ctx = + let reorder_in_fresh_abs (abs : V.abs) : V.abs = + (* Split between the loans and borrows *) + let is_borrow (av : V.typed_avalue) : bool = + match av.V.value with + | ABorrow _ -> true + | ALoan _ -> false + | _ -> raise (Failure "Unexpected") + in + let aborrows, aloans = List.partition is_borrow abs.V.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). + *) + let get_borrow_id (av : V.typed_avalue) : V.BorrowId.id = + match av.V.value with + | V.ABorrow (V.AMutBorrow (bid, _) | V.ASharedBorrow bid) -> bid + | _ -> raise (Failure "Unexpected") + in + let get_loan_id (av : V.typed_avalue) : V.BorrowId.id = + match av.V.value with + | V.ALoan (V.AMutLoan (lid, _)) -> lid + | V.ALoan (V.ASharedLoan (lids, _, _)) -> V.BorrowId.Set.min_elt lids + | _ -> raise (Failure "Unexpected") + in + (* We use ordered maps to reorder the borrows and loans *) + let reorder (get_bid : V.typed_avalue -> V.BorrowId.id) + (values : V.typed_avalue list) : V.typed_avalue list = + List.map snd + (V.BorrowId.Map.bindings + (V.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 V.avalues } + in + + let reorder_in_abs (abs : V.abs) = + if V.AbstractionId.Set.mem abs.abs_id old_abs_ids then abs + else reorder_in_fresh_abs abs + in + + let env = C.env_map_abs reorder_in_abs ctx.env in + + { ctx with C.env } + +let prepare_ashared_loans (loop_id : V.LoopId.id option) : cm_fun = + fun cf ctx0 -> + let ctx = ctx0 in + (* Compute the set of borrows which appear in the abstractions, so that + we can filter the borrows that we reborrow. + *) + let absl = + List.filter_map + (function C.Var _ | C.Frame -> None | C.Abs abs -> Some abs) + ctx.env + in + let absl_ids, absl_id_maps = compute_absl_ids absl in + let abs_borrow_ids = absl_ids.borrow_ids in + + (* Map from the fresh sids to the original symbolic values *) + let sid_subst = ref [] in + + (* Return the same value but where: + - the shared loans have been removed + - the symbolic values have been replaced with fresh symbolic values + - the region ids found in the value and belonging to the set [rids] have + been substituted with [nrid] + *) + let mk_value_with_fresh_sids_no_shared_loans (rids : T.RegionId.Set.t) + (nrid : T.RegionId.id) (v : V.typed_value) : V.typed_value = + (* Remove the shared loans *) + let v = value_remove_shared_loans v in + (* Substitute the symbolic values and the region *) + Subst.typed_value_subst_ids + (fun r -> if T.RegionId.Set.mem r rids then nrid else r) + (fun x -> x) + (fun x -> x) + (fun id -> + let nid = C.fresh_symbolic_value_id () in + let sv = V.SymbolicValueId.Map.find id absl_id_maps.sids_to_values in + sid_subst := (nid, sv) :: !sid_subst; + nid) + (fun x -> x) + v + in + + let borrow_substs = ref [] in + let fresh_absl = ref [] in + + (* Auxiliary function to create a new abstraction for a shared value found in + an abstraction. + + Example: + ======== + When exploring: + {[ + abs'0 { SL {l0, l1} s0 } + ]} + + we find the shared value: + + {[ + SL {l0, l1} s0 + ]} + + and introduce the corresponding abstraction: + {[ + abs'2 { SB l0, SL {l2} s2 } + ]} + *) + let push_abs_for_shared_value (abs : V.abs) (sv : V.typed_value) + (lid : V.BorrowId.id) : unit = + (* Create a fresh borrow (for the reborrow) *) + let nlid = C.fresh_borrow_id () in + + (* We need a fresh region for the new abstraction *) + let nrid = C.fresh_region_id () in + + (* Prepare the shared value *) + let nsv = mk_value_with_fresh_sids_no_shared_loans abs.regions nrid sv in + + (* Save the borrow substitution, to apply it to the context later *) + borrow_substs := (lid, nlid) :: !borrow_substs; + + (* Rem.: the below sanity checks are not really necessary *) + assert (V.AbstractionId.Set.is_empty abs.parents); + assert (abs.original_parents = []); + assert (T.RegionId.Set.is_empty abs.ancestors_regions); + + (* Introduce the new abstraction for the shared values *) + let rty = ety_no_regions_to_rty sv.V.ty in + + (* Create the shared loan child *) + let child_rty = rty in + let child_av = mk_aignored child_rty in + + (* Create the shared loan *) + let loan_rty = T.Ref (T.Var nrid, rty, T.Shared) in + let loan_value = + V.ALoan (V.ASharedLoan (V.BorrowId.Set.singleton nlid, nsv, child_av)) + in + let loan_value = mk_typed_avalue loan_rty loan_value in + + (* Create the shared borrow *) + let borrow_rty = loan_rty in + let borrow_value = V.ABorrow (V.ASharedBorrow lid) in + let borrow_value = mk_typed_avalue borrow_rty borrow_value in + + (* Create the abstraction *) + let avalues = [ borrow_value; loan_value ] in + let kind = + match loop_id with + | Some loop_id -> V.Loop (loop_id, None, V.LoopSynthInput) + | None -> V.Identity + in + let can_end = true in + let fresh_abs = + { + V.abs_id = C.fresh_abstraction_id (); + kind; + can_end; + parents = V.AbstractionId.Set.empty; + original_parents = []; + regions = T.RegionId.Set.singleton nrid; + ancestors_regions = T.RegionId.Set.empty; + avalues; + } + in + fresh_absl := fresh_abs :: !fresh_absl + in + + (* Explore the shared values in the context abstractions, and introduce + fresh abstractions with reborrows for those shared values. + + We simply explore the context and call {!push_abs_for_shared_value} + when necessary. + *) + let collect_shared_values_in_abs (abs : V.abs) : unit = + let collect_shared_value lids (sv : V.typed_value) = + (* Sanity check: we don't support nested borrows for now *) + assert (not (value_has_borrows ctx sv.V.value)); + + (* Filter the loan ids whose corresponding borrows appear in abstractions + (see the documentation of the function) *) + let lids = V.BorrowId.Set.diff lids abs_borrow_ids in + + (* Generate fresh borrows and values *) + V.BorrowId.Set.iter (push_abs_for_shared_value abs sv) lids + in + + let visit_avalue = + object + inherit [_] V.iter_typed_avalue as super + + method! visit_SharedLoan env lids sv = + collect_shared_value lids sv; + + (* Continue the exploration *) + super#visit_SharedLoan env lids sv + + method! visit_ASharedLoan env lids sv _ = + collect_shared_value lids sv; + + (* Continue the exploration *) + super#visit_SharedLoan env lids sv + + (** Check that there are no symbolic values with *borrows* inside the + abstraction - shouldn't happen if the symbolic values are greedily + expanded. + We do this because those values could contain shared borrows: + if it is the case, we need to duplicate them too. + TODO: implement this more general behavior. + *) + method! visit_symbolic_value env sv = + assert (not (symbolic_value_has_borrows ctx sv)); + super#visit_symbolic_value env sv + end + in + List.iter (visit_avalue#visit_typed_avalue None) abs.avalues + in + C.env_iter_abs collect_shared_values_in_abs ctx.env; + + (* Update the borrow ids in the environment. + + Example: + ======== + If we start with environment: + {[ + abs'0 { SL {l0, l1} s0 } + l0 -> SB l0 + l1 -> SB l1 + ]} + + We introduce the following abstractions: + {[ + abs'2 { SB l0, SL {l2} s2 } + abs'3 { SB l1, SL {l3} s3 } + ]} + + While doing so, we registered the fact that we introduced [l2] for [l0] + and [l3] for [l1]: we now need to perform the proper substitutions in + the values [l0] and [l1]. This gives: + + {[ + l0 -> SB l0 + l1 -> SB l1 + + ~~> + + l0 -> SB l2 + l1 -> SB l3 + ]} + *) + let env = + let bmap = V.BorrowId.Map.of_list !borrow_substs in + let bsusbt bid = + match V.BorrowId.Map.find_opt bid bmap with + | None -> bid + | Some bid -> bid + in + + let visitor = + object + inherit [_] C.map_env + method! visit_borrow_id _ bid = bsusbt bid + end + in + visitor#visit_env () ctx.env + in + + (* Add the abstractions *) + let fresh_absl = List.map (fun abs -> C.Abs abs) !fresh_absl in + let env = List.append fresh_absl env in + let ctx = { ctx with env } in + + let _, new_ctx_ids_map = compute_context_ids ctx in + + (* Synthesize *) + match cf ctx with + | None -> None + | Some e -> + (* Add the let-bindings which introduce the fresh symbolic values *) + Some + (List.fold_left + (fun e (sid, v) -> + let v = mk_typed_value_from_symbolic_value v in + let sv = + V.SymbolicValueId.Map.find sid new_ctx_ids_map.sids_to_values + in + SymbolicAst.IntroSymbolic (ctx, None, sv, v, e)) + e !sid_subst) + +let prepare_ashared_loans_no_synth (loop_id : V.LoopId.id) (ctx : C.eval_ctx) : + C.eval_ctx = + get_cf_ctx_no_synth (prepare_ashared_loans (Some loop_id)) ctx + +let compute_loop_entry_fixed_point (config : C.config) (loop_id : V.LoopId.id) + (eval_loop_body : st_cm_fun) (ctx0 : C.eval_ctx) : + C.eval_ctx * ids_sets * V.abs T.RegionGroupId.Map.t = + (* The continuation for when we exit the loop - we register the + environments upon loop *reentry*, and synthesize nothing by + returning [None] + *) + let ctxs = ref [] in + let register_ctx ctx = ctxs := ctx :: !ctxs in + + (* Introduce "reborrows" for the shared values in the abstractions, so that + the shared values in the fixed abstractions never get modified (technically, + they are immutable, but in practice we can introduce more shared loans, or + expand symbolic values). + + For more details, see the comments for {!prepare_ashared_loans} + *) + let ctx = prepare_ashared_loans_no_synth loop_id ctx0 in + + (* Debug *) + log#ldebug + (lazy + ("compute_loop_entry_fixed_point: after prepare_ashared_loans:" + ^ "\n\n- ctx0:\n" + ^ eval_ctx_to_string_no_filter ctx0 + ^ "\n\n- ctx1:\n" + ^ eval_ctx_to_string_no_filter ctx + ^ "\n\n")); + + let cf_exit_loop_body (res : statement_eval_res) : m_fun = + fun ctx -> + match res with + | Return | Panic | Break _ -> None + | Unit -> + (* See the comment in {!eval_loop} *) + raise (Failure "Unreachable") + | Continue i -> + (* For now we don't support continues to outer loops *) + assert (i = 0); + register_ctx ctx; + None + | LoopReturn _ | EndEnterLoop _ | EndContinue _ -> + (* We don't support nested loops for now *) + raise (Failure "Nested loops are not supported for now") + in + + (* The fixed ids. They are the ids of the original ctx, after we ended + the borrows/loans which end during the first loop iteration (we do + one loop iteration, then set it to [Some]. + *) + let fixed_ids : ids_sets option ref = ref None in + + (* Join the contexts at the loop entry - ctx1 is the current joined + context (the context at the loop entry, after we called + {!prepare_ashared_loans}, if this is the first iteration) *) + let join_ctxs (ctx1 : C.eval_ctx) : C.eval_ctx = + (* If this is the first iteration, end the borrows/loans/abs which + appear in ctx1 and not in the other contexts, then compute the + set of fixed ids. This means those borrows/loans have to end + in the loop, and we rather end them *before* the loop. *) + let ctx1 = + match !fixed_ids with + | Some _ -> ctx1 + | None -> + let old_ids, _ = compute_context_ids ctx1 in + let new_ids, _ = compute_contexts_ids !ctxs in + let blids = V.BorrowId.Set.diff old_ids.blids new_ids.blids in + let aids = V.AbstractionId.Set.diff old_ids.aids new_ids.aids in + (* End those borrows and abstractions *) + let end_borrows_abs blids aids ctx = + let ctx = + InterpreterBorrows.end_borrows_no_synth config blids ctx + in + let ctx = + InterpreterBorrows.end_abstractions_no_synth config aids ctx + in + ctx + in + (* End the borrows/abs in [ctx1] *) + let ctx1 = end_borrows_abs blids aids ctx1 in + (* We can also do the same in the contexts [ctxs]: if there are + several contexts, maybe one of them ended some borrows and some + others didn't. As we need to end those borrows anyway (the join + will detect them and ask to end them) we do it preemptively. + *) + ctxs := List.map (end_borrows_abs blids aids) !ctxs; + (* Note that the fixed ids are given by the original context, from *before* + we introduce fresh abstractions/reborrows for the shared values *) + fixed_ids := Some (fst (compute_context_ids ctx0)); + ctx1 + in + + let fixed_ids = Option.get !fixed_ids in + let (_, _), ctx2 = + loop_join_origin_with_continue_ctxs config loop_id fixed_ids ctx1 !ctxs + in + ctxs := []; + ctx2 + in + (* Compute the set of fixed ids - for the symbolic ids, we compute the + intersection of ids between the original environment and the list + of new environments *) + let compute_fixed_ids (ctxl : C.eval_ctx list) : ids_sets = + let fixed_ids, _ = compute_context_ids ctx0 in + let { aids; blids; borrow_ids; loan_ids; dids; rids; sids } = fixed_ids in + let sids = ref sids in + List.iter + (fun ctx -> + let fixed_ids, _ = compute_context_ids ctx in + sids := V.SymbolicValueId.Set.inter !sids fixed_ids.sids) + ctxl; + let sids = !sids in + let fixed_ids = { aids; blids; borrow_ids; loan_ids; dids; rids; sids } in + fixed_ids + in + (* Check if two contexts are equivalent - modulo alpha conversion on the + existentially quantified borrows/abstractions/symbolic values. + *) + let equiv_ctxs (ctx1 : C.eval_ctx) (ctx2 : C.eval_ctx) : bool = + let fixed_ids = compute_fixed_ids [ ctx1; ctx2 ] in + let check_equivalent = true in + let lookup_shared_value _ = raise (Failure "Unreachable") in + Option.is_some + (match_ctxs check_equivalent fixed_ids lookup_shared_value + lookup_shared_value ctx1 ctx2) + in + let max_num_iter = Config.loop_fixed_point_max_num_iters in + let rec compute_fixed_point (ctx : C.eval_ctx) (i0 : int) (i : int) : + C.eval_ctx = + if i = 0 then + raise + (Failure + ("Could not compute a loop fixed point in " ^ string_of_int i0 + ^ " iterations")) + else + (* Evaluate the loop body to register the different contexts upon reentry *) + let _ = eval_loop_body cf_exit_loop_body ctx in + (* Compute the join between the original contexts and the contexts computed + upon reentry *) + let ctx1 = join_ctxs ctx in + + (* Debug *) + log#ldebug + (lazy + ("compute_fixed_point:" ^ "\n\n- ctx0:\n" + ^ eval_ctx_to_string_no_filter ctx + ^ "\n\n- ctx1:\n" + ^ eval_ctx_to_string_no_filter ctx1 + ^ "\n\n")); + + (* Check if we reached a fixed point: if not, iterate *) + if equiv_ctxs ctx ctx1 then ctx1 else compute_fixed_point ctx1 i0 (i - 1) + in + let fp = compute_fixed_point ctx max_num_iter max_num_iter in + + (* Debug *) + log#ldebug + (lazy + ("compute_fixed_point: fixed point computed before matching with input \ + region groups:" ^ "\n\n- fp:\n" + ^ eval_ctx_to_string_no_filter fp + ^ "\n\n")); + + (* Make sure we have exactly one loop abstraction per function region (merge + abstractions accordingly). + + Rem.: this shouldn't impact the set of symbolic value ids (because we + already merged abstractions "vertically" and are now merging them + "horizontally": the symbolic values contained in the abstractions (typically + the shared values) will be preserved. + *) + let fp, rg_to_abs = + (* List the loop abstractions in the fixed-point *) + let fp_aids, add_aid, _mem_aid = V.AbstractionId.Set.mk_stateful_set () in + + let list_loop_abstractions = + object + inherit [_] C.map_eval_ctx + + method! visit_abs _ abs = + match abs.kind with + | Loop (loop_id', _, kind) -> + assert (loop_id' = loop_id); + assert (kind = V.LoopSynthInput); + (* The abstractions introduced so far should be endable *) + assert (abs.can_end = true); + add_aid abs.abs_id; + abs + | _ -> abs + end + in + let fp = list_loop_abstractions#visit_eval_ctx () fp in + + (* For every input region group: + * - evaluate until we get to a [return] + * - end the input abstraction corresponding to the input region group + * - find which loop abstractions end at that moment + * + * [fp_ended_aids] links region groups to sets of ended abstractions. + *) + let fp_ended_aids = ref T.RegionGroupId.Map.empty in + let add_ended_aids (rg_id : T.RegionGroupId.id) + (aids : V.AbstractionId.Set.t) : unit = + match T.RegionGroupId.Map.find_opt rg_id !fp_ended_aids with + | None -> + fp_ended_aids := T.RegionGroupId.Map.add rg_id aids !fp_ended_aids + | Some aids' -> + let aids = V.AbstractionId.Set.union aids aids' in + fp_ended_aids := T.RegionGroupId.Map.add rg_id aids !fp_ended_aids + in + let cf_loop : st_m_fun = + fun res ctx -> + match res with + | Continue _ | Panic -> + (* We don't want to generate anything *) + None + | Break _ -> + (* We enforce that we can't get there: see {!PrePasses.remove_loop_breaks} *) + raise (Failure "Unreachable") + | Unit | LoopReturn _ | EndEnterLoop _ | EndContinue _ -> + (* For why we can't get [Unit], see the comments inside {!eval_loop_concrete}. + For [EndEnterLoop] and [EndContinue]: we don't support nested loops for now. + *) + raise (Failure "Unreachable") + | Return -> + (* Should we consume the return value and pop the frame? + * If we check in [Interpreter] that the loop abstraction we end is + * indeed the correct one, I think it is sound to under-approximate here + * (and it shouldn't make any difference). + *) + let _ = + List.iter + (fun rg_id -> + (* Lookup the input abstraction - we use the fact that the + abstractions should have been introduced in a specific + order (and we check that it is indeed the case) *) + let abs_id = + V.AbstractionId.of_int (T.RegionGroupId.to_int rg_id) + in + (* By default, the [SynthInput] abs can't end *) + let ctx = C.ctx_set_abs_can_end ctx abs_id true in + assert ( + let abs = C.ctx_lookup_abs ctx abs_id in + abs.kind = V.SynthInput rg_id); + (* End this abstraction *) + let ctx = + InterpreterBorrows.end_abstraction_no_synth config abs_id ctx + in + (* Explore the context, and check which abstractions are not there anymore *) + let ids, _ = compute_context_ids ctx in + let ended_ids = V.AbstractionId.Set.diff !fp_aids ids.aids in + add_ended_aids rg_id ended_ids) + ctx.region_groups + in + (* We don't want to generate anything *) + None + in + let _ = eval_loop_body cf_loop fp in + + (* Check that the sets of abstractions we need to end per region group are pairwise + * disjoint *) + let aids_union = ref V.AbstractionId.Set.empty in + let _ = + T.RegionGroupId.Map.iter + (fun _ ids -> + assert (V.AbstractionId.Set.disjoint !aids_union ids); + aids_union := V.AbstractionId.Set.union ids !aids_union) + !fp_ended_aids + in + + (* We also check that all the regions need to end - this is not necessary per + se, but if it doesn't happen it is bizarre and worth investigating... *) + assert (V.AbstractionId.Set.equal !aids_union !fp_aids); + + (* Merge the abstractions which need to be merged, and compute the map from + region id to abstraction id *) + let fp = ref fp in + let rg_to_abs = ref T.RegionGroupId.Map.empty in + let _ = + T.RegionGroupId.Map.iter + (fun rg_id ids -> + let ids = V.AbstractionId.Set.elements ids in + (* Retrieve the first id of the group *) + match ids with + | [] -> + (* We shouldn't get there: we actually introduce reborrows with + {!prepare_ashared_loans} and in the [match_mut_borrows] function + of {!MakeJoinMatcher} to introduce some fresh abstractions for + this purpose. + *) + raise (Failure "Unexpected") + | id0 :: ids -> + let id0 = ref id0 in + (* Add the proper region group into the abstraction *) + let abs_kind = V.Loop (loop_id, Some rg_id, V.LoopSynthInput) in + let abs = C.ctx_lookup_abs !fp !id0 in + let abs = { abs with V.kind = abs_kind } in + let fp', _ = C.ctx_subst_abs !fp !id0 abs in + fp := fp'; + (* Merge all the abstractions into this one *) + List.iter + (fun id -> + try + log#ldebug + (lazy + ("compute_loop_entry_fixed_point: merge FP \ + abstraction: " + ^ V.AbstractionId.to_string id + ^ " into " + ^ V.AbstractionId.to_string !id0)); + (* Note that we merge *into* [id0] *) + let fp', id0' = + merge_into_abstraction loop_id abs_kind false !fp id !id0 + in + fp := fp'; + id0 := id0'; + () + with ValueMatchFailure _ -> raise (Failure "Unexpected")) + ids; + (* Register the mapping *) + let abs = C.ctx_lookup_abs !fp !id0 in + rg_to_abs := T.RegionGroupId.Map.add_strict rg_id abs !rg_to_abs) + !fp_ended_aids + in + let rg_to_abs = !rg_to_abs in + + (* Reorder the loans and borrows in the fresh abstractions in the fixed-point *) + let fp = + reorder_loans_borrows_in_fresh_abs (Option.get !fixed_ids).aids !fp + in + + (* Update the abstraction's [can_end] field and their kinds. + + Note that if [remove_rg_id] is [true], we set the region id to [None] + and set the abstractions as endable: this is so that we can check that + we have a fixed point (so far in the fixed point the loop abstractions had + no region group, and we set them as endable just above). + + If [remove_rg_id] is [false], we simply set the abstractions as non-endable + to freeze them (we will use the fixed point as starting point for the + symbolic execution of the loop body, and we have to make sure the input + abstractions are frozen). + *) + let update_loop_abstractions (remove_rg_id : bool) = + object + inherit [_] C.map_eval_ctx + + method! visit_abs _ abs = + match abs.kind with + | Loop (loop_id', _, kind) -> + assert (loop_id' = loop_id); + assert (kind = V.LoopSynthInput); + let kind = + if remove_rg_id then V.Loop (loop_id, None, V.LoopSynthInput) + else abs.kind + in + { abs with can_end = remove_rg_id; kind } + | _ -> abs + end + in + let update_kinds_can_end (remove_rg_id : bool) ctx = + (update_loop_abstractions remove_rg_id)#visit_eval_ctx () ctx + in + let fp = update_kinds_can_end false fp in + + (* Sanity check: we still have a fixed point - we simply call [compute_fixed_point] + while allowing exactly one iteration to see if it fails *) + let _ = + let fp_test = update_kinds_can_end true fp in + log#ldebug + (lazy + ("compute_fixed_point: fixed point after matching with the function \ + region groups:\n" + ^ eval_ctx_to_string_no_filter fp_test)); + compute_fixed_point fp_test 1 1 + in + + (* Return *) + (fp, rg_to_abs) + in + let fixed_ids = compute_fixed_ids [ fp ] in + + (* Return *) + (fp, fixed_ids, rg_to_abs) + +let compute_fixed_point_id_correspondance (fixed_ids : ids_sets) + (src_ctx : C.eval_ctx) (tgt_ctx : C.eval_ctx) : borrow_loan_corresp = + log#ldebug + (lazy + ("compute_fixed_point_id_correspondance:\n\n- fixed_ids:\n" + ^ show_ids_sets fixed_ids ^ "\n\n- src_ctx:\n" ^ eval_ctx_to_string src_ctx + ^ "\n\n- tgt_ctx:\n" ^ eval_ctx_to_string tgt_ctx ^ "\n\n")); + + let filt_src_env, _, _ = ctx_split_fixed_new fixed_ids src_ctx in + let filt_src_ctx = { src_ctx with env = filt_src_env } in + let filt_tgt_env, new_absl, _ = ctx_split_fixed_new fixed_ids tgt_ctx in + let filt_tgt_ctx = { tgt_ctx with env = filt_tgt_env } in + + log#ldebug + (lazy + ("compute_fixed_point_id_correspondance:\n\n- fixed_ids:\n" + ^ show_ids_sets fixed_ids ^ "\n\n- filt_src_ctx:\n" + ^ eval_ctx_to_string filt_src_ctx + ^ "\n\n- filt_tgt_ctx:\n" + ^ eval_ctx_to_string filt_tgt_ctx + ^ "\n\n")); + + (* Match the source context and the filtered target context *) + let maps = + let check_equiv = false in + let fixed_ids = ids_sets_empty_borrows_loans fixed_ids in + let open InterpreterBorrowsCore in + let lookup_shared_loan lid ctx : V.typed_value = + match snd (lookup_loan ek_all lid ctx) with + | Concrete (V.SharedLoan (_, v)) -> v + | Abstract (V.ASharedLoan (_, v, _)) -> v + | _ -> raise (Failure "Unreachable") + in + let lookup_in_tgt id = lookup_shared_loan id tgt_ctx in + let lookup_in_src id = lookup_shared_loan id src_ctx in + Option.get + (match_ctxs check_equiv fixed_ids lookup_in_tgt lookup_in_src filt_tgt_ctx + filt_src_ctx) + in + + log#ldebug + (lazy + ("compute_fixed_point_id_correspondance:\n\n- tgt_to_src_maps:\n" + ^ show_ids_maps maps ^ "\n\n")); + + let src_to_tgt_borrow_map = + V.BorrowId.Map.of_list + (List.map + (fun (x, y) -> (y, x)) + (V.BorrowId.InjSubst.bindings maps.borrow_id_map)) + in + + (* Sanity check: for every abstraction, the target loans and borrows are mapped + to the same set of source loans and borrows. + + For instance, if we map the [env_fp] to [env0] (only looking at the bindings, + ignoring the abstractions) below: + {[ + env0 = { + abs@0 { ML l0 } + ls -> MB l0 (s2 : loops::List<T>) + i -> s1 : u32 + } + + env_fp = { + abs@0 { ML l0 } + ls -> MB l1 (s3 : loops::List<T>) + i -> s4 : u32 + abs@fp { + MB l0 + ML l1 + } + } + ]} + + We get that l1 is mapped to l0. From there, we see that abs@fp consumes + the same borrows that it gives: it is indeed an identity function. + + TODO: we should also check the mappings for the shared values (to + make sure the abstractions are indeed the identity)... + *) + List.iter + (fun abs -> + let ids, _ = compute_abs_ids abs in + (* Map the *loan* ids (we just match the corresponding *loans* ) *) + let loan_ids = + V.BorrowId.Set.map + (fun x -> V.BorrowId.InjSubst.find x maps.borrow_id_map) + ids.loan_ids + in + (* Check that the loan and borrows are related *) + assert (V.BorrowId.Set.equal ids.borrow_ids loan_ids)) + new_absl; + + (* For every target abstraction (going back to the [list_nth_mut] example, + we have to visit [abs@fp { ML l0, MB l1 }]): + - go through the tgt borrows ([l1]) + - for every tgt borrow, find the corresponding src borrow ([l0], because + we have: [borrows_map: { l1 -> l0 }]) + - from there, find the corresponding tgt loan ([l0]) + + Note that this borrow does not necessarily appear in the src_to_tgt_borrow_map, + if it actually corresponds to a borrows introduced when decomposing the + abstractions to move the shared values out of the source context abstractions. + *) + let tgt_borrow_to_loan = ref V.BorrowId.InjSubst.empty in + let visit_tgt = + object + inherit [_] V.iter_abs + + method! visit_borrow_id _ id = + (* Find the target borrow *) + let tgt_borrow_id = V.BorrowId.Map.find id src_to_tgt_borrow_map in + (* Update the map *) + tgt_borrow_to_loan := + V.BorrowId.InjSubst.add id tgt_borrow_id !tgt_borrow_to_loan + end + in + List.iter (visit_tgt#visit_abs ()) new_absl; + + (* Compute the map from loan to borrows *) + let tgt_loan_to_borrow = + V.BorrowId.InjSubst.of_list + (List.map + (fun (x, y) -> (y, x)) + (V.BorrowId.InjSubst.bindings !tgt_borrow_to_loan)) + in + + (* Return *) + { + borrow_to_loan_id_map = !tgt_borrow_to_loan; + loan_to_borrow_id_map = tgt_loan_to_borrow; + } + +let compute_fp_ctx_symbolic_values (ctx : C.eval_ctx) (fp_ctx : C.eval_ctx) : + V.SymbolicValueId.Set.t * V.symbolic_value list = + let old_ids, _ = compute_context_ids ctx in + let fp_ids, fp_ids_maps = compute_context_ids fp_ctx in + let fresh_sids = V.SymbolicValueId.Set.diff fp_ids.sids old_ids.sids in + + (* Compute the set of symbolic values which appear in shared values inside + *fixed* abstractions: because we introduce fresh abstractions and reborrows + with {!prepare_ashared_loans}, those values are never accessed directly + inside the loop iterations: we can ignore them (and should, because + otherwise it leads to a very ugly translation with duplicated, unused + values) *) + let shared_sids_in_fixed_abs = + let fixed_absl = + List.filter + (fun (ee : C.env_elem) -> + match ee with + | C.Var _ | C.Frame -> false + | Abs abs -> V.AbstractionId.Set.mem abs.abs_id old_ids.aids) + ctx.env + in + + (* Rem.: as we greedily expand the symbolic values containing borrows, and + in particular the (mutable/shared) borrows, we could simply list the + symbolic values appearing in the abstractions: those are necessarily + shared values. We prefer to be more general, in prevision of later + changes. + *) + let sids = ref V.SymbolicValueId.Set.empty in + let visitor = + object (self) + inherit [_] C.iter_env + + method! visit_ASharedLoan inside_shared _ sv child_av = + self#visit_typed_value true sv; + self#visit_typed_avalue inside_shared child_av + + method! visit_symbolic_value_id inside_shared sid = + if inside_shared then sids := V.SymbolicValueId.Set.add sid !sids + end + in + visitor#visit_env false fixed_absl; + !sids + in + + (* Remove the shared symbolic values present in the fixed abstractions - + see comments for [shared_sids_in_fixed_abs]. *) + let sids_to_values = fp_ids_maps.sids_to_values in + + log#ldebug + (lazy + ("compute_fp_ctx_symbolic_values:" ^ "\n- shared_sids_in_fixed_abs:" + ^ V.SymbolicValueId.Set.show shared_sids_in_fixed_abs + ^ "\n- all_sids_to_values: " + ^ V.SymbolicValueId.Map.show (symbolic_value_to_string ctx) sids_to_values + ^ "\n")); + + let sids_to_values = + V.SymbolicValueId.Map.filter + (fun sid _ -> + not (V.SymbolicValueId.Set.mem sid shared_sids_in_fixed_abs)) + sids_to_values + in + + (* List the input symbolic values in proper order. + + We explore the environment, and order the symbolic values in the order + in which they are found - this way, the symbolic values found in a + variable [x] which appears before [y] are listed first, for instance. + *) + let input_svalues = + let found_sids = ref V.SymbolicValueId.Set.empty in + let ordered_sids = ref [] in + + let visitor = + object (self) + inherit [_] C.iter_env + + (** We lookup the shared values *) + method! visit_SharedBorrow env bid = + let open InterpreterBorrowsCore in + let v = + match snd (lookup_loan ek_all bid fp_ctx) with + | Concrete (V.SharedLoan (_, v)) -> v + | Abstract (V.ASharedLoan (_, v, _)) -> v + | _ -> raise (Failure "Unreachable") + in + self#visit_typed_value env v + + method! visit_symbolic_value_id _ id = + if not (V.SymbolicValueId.Set.mem id !found_sids) then ( + found_sids := V.SymbolicValueId.Set.add id !found_sids; + ordered_sids := id :: !ordered_sids) + end + in + + List.iter (visitor#visit_env_elem ()) (List.rev fp_ctx.env); + + List.filter_map + (fun id -> V.SymbolicValueId.Map.find_opt id sids_to_values) + (List.rev !ordered_sids) + in + + log#ldebug + (lazy + ("compute_fp_ctx_symbolic_values:" ^ "\n- src context:\n" + ^ eval_ctx_to_string_no_filter ctx + ^ "\n- fixed point:\n" + ^ eval_ctx_to_string_no_filter fp_ctx + ^ "\n- fresh_sids: " + ^ V.SymbolicValueId.Set.show fresh_sids + ^ "\n- input_svalues: " + ^ Print.list_to_string (symbolic_value_to_string ctx) input_svalues + ^ "\n\n")); + + (fresh_sids, input_svalues) diff --git a/compiler/InterpreterLoopsFixedPoint.mli b/compiler/InterpreterLoopsFixedPoint.mli new file mode 100644 index 00000000..cb03bc9e --- /dev/null +++ b/compiler/InterpreterLoopsFixedPoint.mli @@ -0,0 +1,166 @@ +module T = Types +module PV = PrimitiveValues +module V = Values +module E = Expressions +module C = Contexts +module Subst = Substitute +module A = LlbcAst +module L = Logging +module Inv = Invariants +module S = SynthesizeSymbolic +open InterpreterUtils +open InterpreterLoopsCore + +(** Prepare the shared loans in the abstractions by moving them to fresh + abstractions. + + We use this to prepare an evaluation context before computing a fixed point. + + Because a loop iteration might lead to symbolic value expansions and create + shared loans in shared values inside the *fixed* abstractions, which we want + to leave unchanged, we introduce some reborrows in the following way: + + {[ + abs'0 { SL {l0, l1} s0 } + l0 -> SB l0 + l1 -> SB l1 + + ~~> + + abs'0 { SL {l0, l1} s0 } + l0 -> SB l2 + l1 -> SB l3 + abs'2 { SB l0, SL {l2} s2 } + abs'3 { SB l1, SL {l3} s3 } + ]} + + This is sound but leads to information loss. This way, the fixed abstraction + [abs'0] is never modified because [s0] is never accessed (and thus never + expanded). + + We do this because it makes it easier to compute joins and fixed points. + + **REMARK**: + As a side note, we only reborrow the loan ids whose corresponding borrows + appear in values (i.e., not in abstractions). + + For instance, if we have: + {[ + abs'0 { + SL {l0} s0 + SL {l1} s1 + } + abs'1 { SB l0 } + x -> SB l1 + ]} + + we only introduce a fresh abstraction for [l1]. + *) +val prepare_ashared_loans : V.loop_id option -> Cps.cm_fun + +(** Compute a fixed-point for the context at the entry of the loop. + We also return: + - the sets of fixed ids + - the map from region group id to the corresponding abstraction appearing + in the fixed point (this is useful to compute the return type of the loop + backward functions for instance). + + Rem.: the list of symbolic values should be computable by simply exploring + the fixed point environment and listing all the symbolic values we find. + In the future, we might want to do something more precise, by listing only + the values which are read or modified (some symbolic values may be ignored). + *) +val compute_loop_entry_fixed_point : + C.config -> + V.loop_id -> + Cps.st_cm_fun -> + C.eval_ctx -> + C.eval_ctx * ids_sets * V.abs SymbolicAst.region_group_id_map + +(** For the abstractions in the fixed point, compute the correspondance between + the borrows ids and the loans ids, if we want to introduce equivalent + identity abstractions (i.e., abstractions which do nothing - the input + borrows are exactly the output loans). + + **Context:** + ============ + When we (re-enter) the loop, we want to introduce identity abstractions + (i.e., abstractions which actually only introduce fresh identifiers for + some borrows, to abstract away a bit the borrow graph) which have the same + shape as the abstractions introduced for the fixed point (see the explanations + for [match_ctx_with_target]). This allows us to transform the environment + into a fixed point (again, see the explanations for [match_ctx_with_target]). + + In order to introduce those identity abstractions, we need to figure out, + for those abstractions, which loans should be linked to which borrows. + We do this in the following way. + + We match the fixed point environment with the environment upon first entry + in the loop, and exploit the fact that the fixed point was derived by also + joining this first entry environment: because of that, the borrows in the + abstractions introduced for the fixed-point actually exist in this first + environment (they are not fresh). For [list_nth_mut] (see the explanations + at the top of the file) we have the following: + + {[ + // Environment upon first entry in the loop + env0 = { + abs@0 { ML l0 } + ls -> MB l0 (s2 : loops::List<T>) + i -> s1 : u32 + } + + // Fixed-point environment + env_fp = { + abs@0 { ML l0 } + ls -> MB l1 (s3 : loops::List<T>) + i -> s4 : u32 + abs@fp { + MB l0 // this borrow appears in [env0] + ML l1 + } + } + ]} + + We filter those environments to remove the non-fixed dummy variables, + abstractions, etc. in a manner similar to [match_ctx_with_target]. We + get: + + {[ + filtered_env0 = { + abs@0 { ML l0 } + ls -> MB l0 (s2 : loops::List<T>) + i -> s1 : u32 + } + + filtered_env_fp = { + abs@0 { ML l0 } + ls -> MB l1 (s3 : loops::List<T>) + i -> s@ : u32 + // removed abs@fp + } + ]} + + We then match [filtered_env_fp] with [filtered_env0], taking care to not + consider loans and borrows in a disjoint manner, and ignoring the fixed + values, abstractions, loans, etc. We get: + {[ + borrows_map: { l1 -> l0 } // because we matched [MB l1 ...] with [MB l0 ...] in [ls] + loans_map: {} // we ignore abs@0, which is "fixed" + ]} + + From there we deduce that, if we want to introduce an identity abstraction with the + shape of [abs@fp], we should link [l1] to [l0]. In other words, the value retrieved + through the loan [l1] is actually the value which has to be given back to [l0]. + *) +val compute_fixed_point_id_correspondance : + ids_sets -> C.eval_ctx -> C.eval_ctx -> borrow_loan_corresp + +(** Compute the set of "quantified" symbolic value ids in a fixed-point context. + + We compute: + - the set of symbolic value ids that are freshly introduced + - the list of input symbolic values + *) +val compute_fp_ctx_symbolic_values : + C.eval_ctx -> C.eval_ctx -> V.symbolic_value_id_set * V.symbolic_value list diff --git a/compiler/InterpreterLoopsJoinCtxs.ml b/compiler/InterpreterLoopsJoinCtxs.ml new file mode 100644 index 00000000..6fb0449d --- /dev/null +++ b/compiler/InterpreterLoopsJoinCtxs.ml @@ -0,0 +1,719 @@ +module T = Types +module PV = PrimitiveValues +module V = Values +module E = Expressions +module C = Contexts +module Subst = Substitute +module A = LlbcAst +module L = Logging +open TypesUtils +open ValuesUtils +module Inv = Invariants +module S = SynthesizeSymbolic +module UF = UnionFind +open InterpreterUtils +open InterpreterBorrows +open InterpreterLoopsCore +open InterpreterLoopsMatchCtxs + +(** The local logger *) +let log = L.loops_join_ctxs_log + +(** Reorder the loans and borrows in the fresh abstractions. + + We do this in order to enforce some structure in the environments: this + allows us to find fixed-points. Note that this function needs to be + called typically after we merge abstractions together (see {!collapse_ctx} + for instance). + *) +let reorder_loans_borrows_in_fresh_abs (old_abs_ids : V.AbstractionId.Set.t) + (ctx : C.eval_ctx) : C.eval_ctx = + let reorder_in_fresh_abs (abs : V.abs) : V.abs = + (* Split between the loans and borrows *) + let is_borrow (av : V.typed_avalue) : bool = + match av.V.value with + | ABorrow _ -> true + | ALoan _ -> false + | _ -> raise (Failure "Unexpected") + in + let aborrows, aloans = List.partition is_borrow abs.V.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). + *) + let get_borrow_id (av : V.typed_avalue) : V.BorrowId.id = + match av.V.value with + | V.ABorrow (V.AMutBorrow (bid, _) | V.ASharedBorrow bid) -> bid + | _ -> raise (Failure "Unexpected") + in + let get_loan_id (av : V.typed_avalue) : V.BorrowId.id = + match av.V.value with + | V.ALoan (V.AMutLoan (lid, _)) -> lid + | V.ALoan (V.ASharedLoan (lids, _, _)) -> V.BorrowId.Set.min_elt lids + | _ -> raise (Failure "Unexpected") + in + (* We use ordered maps to reorder the borrows and loans *) + let reorder (get_bid : V.typed_avalue -> V.BorrowId.id) + (values : V.typed_avalue list) : V.typed_avalue list = + List.map snd + (V.BorrowId.Map.bindings + (V.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 V.avalues } + in + + let reorder_in_abs (abs : V.abs) = + if V.AbstractionId.Set.mem abs.abs_id old_abs_ids then abs + else reorder_in_fresh_abs abs + in + + let env = C.env_map_abs reorder_in_abs ctx.env in + + { ctx with C.env } + +(** Collapse an environment. + + We do this to simplify an environment, for the purpose of finding a loop + fixed point. + + We do the following: + - we look for all the *new* dummy values (we use sets of old ids to decide + wether a value is new or not) and convert them into abstractions + - whenever there is a new abstraction in the context, and some of its + its borrows are associated to loans in another new abstraction, we + merge them. + In effect, this allows us to merge newly introduced abstractions/borrows + with their parent abstractions. + + For instance, when evaluating the first loop iteration, we start in the + following environment: + {[ + abs@0 { ML l0 } + ls -> MB l0 (s2 : loops::List<T>) + i -> s1 : u32 + ]} + + and get the following environment upon reaching the [Continue] statement: + {[ + abs@0 { ML l0 } + ls -> MB l4 (s@6 : loops::List<T>) + i -> s@7 : u32 + _@1 -> MB l0 (loops::List::Cons (ML l1, ML l2)) + _@2 -> MB l2 (@Box (ML l4)) // tail + _@3 -> MB l1 (s@3 : T) // hd + ]} + + In this new environment, the dummy variables [_@1], [_@2] and [_@3] are + considered as new. + + We first convert the new dummy values to abstractions. It gives: + {[ + abs@0 { ML l0 } + ls -> MB l4 (s@6 : loops::List<T>) + i -> s@7 : u32 + abs@1 { MB l0, ML l1, ML l2 } + abs@2 { MB l2, ML l4 } + abs@3 { MB l1 } + ]} + + We finally merge the new abstractions together. It gives: + {[ + abs@0 { ML l0 } + ls -> MB l4 (s@6 : loops::List<T>) + i -> s@7 : u32 + abs@4 { MB l0, ML l4 } + ]} + + [merge_funs]: those are used to merge loans or borrows which appear in both + abstractions (rem.: here we mean that, for instance, both abstractions + contain a shared loan with id l0). + This can happen when merging environments (note that such environments are not well-formed - + they become well formed again after collapsing). + *) +let collapse_ctx (loop_id : V.LoopId.id) + (merge_funs : merge_duplicates_funcs option) (old_ids : ids_sets) + (ctx0 : C.eval_ctx) : C.eval_ctx = + (* Debug *) + log#ldebug + (lazy + ("collapse_ctx:\n\n- fixed_ids:\n" ^ show_ids_sets old_ids + ^ "\n\n- ctx0:\n" ^ eval_ctx_to_string ctx0 ^ "\n\n")); + + let abs_kind = V.Loop (loop_id, None, LoopSynthInput) in + let can_end = true in + let destructure_shared_values = true in + let is_fresh_abs_id (id : V.AbstractionId.id) : bool = + not (V.AbstractionId.Set.mem id old_ids.aids) + in + let is_fresh_did (id : C.DummyVarId.id) : bool = + not (C.DummyVarId.Set.mem id old_ids.dids) + in + (* Convert the dummy values to abstractions (note that when we convert + values to abstractions, the resulting abstraction should be destructured) *) + (* Note that we preserve the order of the dummy values: we replace them with + abstractions in place - this makes matching easier *) + let env = + List.concat + (List.map + (fun ee -> + match ee with + | C.Abs _ | C.Frame | C.Var (VarBinder _, _) -> [ ee ] + | C.Var (DummyBinder id, v) -> + if is_fresh_did id then + let absl = + convert_value_to_abstractions abs_kind can_end + destructure_shared_values ctx0 v + in + List.map (fun abs -> C.Abs abs) absl + else [ ee ]) + ctx0.env) + in + let ctx = { ctx0 with C.env } in + log#ldebug + (lazy + ("collapse_ctx: after converting values to abstractions:\n" + ^ show_ids_sets old_ids ^ "\n\n- ctx:\n" ^ eval_ctx_to_string ctx ^ "\n\n" + )); + + log#ldebug + (lazy + ("collapse_ctx: after decomposing the shared values in the abstractions:\n" + ^ show_ids_sets old_ids ^ "\n\n- ctx:\n" ^ eval_ctx_to_string ctx ^ "\n\n" + )); + + (* Explore all the *new* abstractions, and compute various maps *) + let explore (abs : V.abs) = is_fresh_abs_id abs.abs_id in + let ids_maps = + compute_abs_borrows_loans_maps (merge_funs = None) explore env + in + let { + abs_ids; + abs_to_borrows; + abs_to_loans = _; + abs_to_borrows_loans; + borrow_to_abs = _; + loan_to_abs; + borrow_loan_to_abs; + } = + ids_maps + in + + (* Change the merging behaviour depending on the input parameters *) + let abs_to_borrows, loan_to_abs = + if merge_funs <> None then (abs_to_borrows_loans, borrow_loan_to_abs) + else (abs_to_borrows, loan_to_abs) + in + + (* Merge the abstractions together *) + let merged_abs : V.AbstractionId.id UF.elem V.AbstractionId.Map.t = + V.AbstractionId.Map.of_list (List.map (fun id -> (id, UF.make id)) abs_ids) + in + + let ctx = ref ctx in + + (* Merge all the mergeable abs. + + We iterate over the abstractions, then over the borrows in the abstractions. + We do this because we want to control the order in which abstractions + are merged (the ids are iterated in increasing order). Otherwise, we + could simply iterate over all the borrows in [borrow_to_abs]... + *) + List.iter + (fun abs_id0 -> + let bids = V.AbstractionId.Map.find abs_id0 abs_to_borrows in + let bids = V.BorrowId.Set.elements bids in + List.iter + (fun bid -> + match V.BorrowId.Map.find_opt bid loan_to_abs with + | None -> (* Nothing to do *) () + | Some abs_ids1 -> + V.AbstractionId.Set.iter + (fun abs_id1 -> + (* We need to merge - unless we have already merged *) + (* First, find the representatives for the two abstractions (the + representative is the abstraction into which we merged) *) + let abs_ref0 = + UF.find (V.AbstractionId.Map.find abs_id0 merged_abs) + in + let abs_id0 = UF.get abs_ref0 in + let abs_ref1 = + UF.find (V.AbstractionId.Map.find abs_id1 merged_abs) + in + let abs_id1 = UF.get abs_ref1 in + (* If the two ids are the same, it means the abstractions were already merged *) + if abs_id0 = abs_id1 then () + else ( + (* We actually need to merge the abstractions *) + + (* Debug *) + log#ldebug + (lazy + ("collapse_ctx: merging abstraction " + ^ V.AbstractionId.to_string abs_id1 + ^ " into " + ^ V.AbstractionId.to_string abs_id0 + ^ ":\n\n" ^ eval_ctx_to_string !ctx)); + + (* Update the environment - pay attention to the order: we + we merge [abs_id1] *into* [abs_id0] *) + let nctx, abs_id = + merge_into_abstraction abs_kind can_end merge_funs !ctx + abs_id1 abs_id0 + in + ctx := nctx; + + (* Update the union find *) + let abs_ref_merged = UF.union abs_ref0 abs_ref1 in + UF.set abs_ref_merged abs_id)) + abs_ids1) + bids) + abs_ids; + + log#ldebug + (lazy + ("collapse_ctx:\n\n- fixed_ids:\n" ^ show_ids_sets old_ids + ^ "\n\n- after collapse:\n" ^ eval_ctx_to_string !ctx ^ "\n\n")); + + (* Reorder the loans and borrows in the fresh abstractions *) + let ctx = reorder_loans_borrows_in_fresh_abs old_ids.aids !ctx in + + log#ldebug + (lazy + ("collapse_ctx:\n\n- fixed_ids:\n" ^ show_ids_sets old_ids + ^ "\n\n- after collapse and reorder borrows/loans:\n" + ^ eval_ctx_to_string ctx ^ "\n\n")); + + (* Return the new context *) + ctx + +let mk_collapse_ctx_merge_duplicate_funs (loop_id : V.LoopId.id) + (ctx : C.eval_ctx) : merge_duplicates_funcs = + (* Rem.: the merge functions raise exceptions (that we catch). *) + let module S : MatchJoinState = struct + let ctx = ctx + let loop_id = loop_id + let nabs = ref [] + end in + let module JM = MakeJoinMatcher (S) in + let module M = MakeMatcher (JM) in + (* Functions to match avalues (see {!merge_duplicates_funcs}). + + Those functions are used to merge borrows/loans with the *same ids*. + + They will always be called on destructured avalues (whose children are + [AIgnored] - we enforce that through sanity checks). We rely on the join + matcher [JM] to match the concrete values (for shared loans for instance). + Note that the join matcher doesn't implement match functions for avalues + (see the comments in {!MakeJoinMatcher}. + *) + let merge_amut_borrows id ty0 child0 _ty1 child1 = + (* Sanity checks *) + assert (is_aignored child0.V.value); + assert (is_aignored child1.V.value); + + (* We need to pick a type for the avalue. The types on the left and on the + right may use different regions: it doesn't really matter (here, we pick + the one from the left), because we will merge those regions together + anyway (see the comments for {!merge_into_abstraction}). + *) + let ty = ty0 in + let child = child0 in + let value = V.ABorrow (V.AMutBorrow (id, child)) in + { V.value; ty } + in + + let merge_ashared_borrows id ty0 ty1 = + (* Sanity checks *) + let _ = + let _, ty0, _ = ty_as_ref ty0 in + let _, ty1, _ = ty_as_ref ty1 in + assert (not (ty_has_borrows ctx.type_context.type_infos ty0)); + assert (not (ty_has_borrows ctx.type_context.type_infos ty1)) + in + + (* Same remarks as for [merge_amut_borrows] *) + let ty = ty0 in + let value = V.ABorrow (V.ASharedBorrow id) in + { V.value; ty } + in + + let merge_amut_loans id ty0 child0 _ty1 child1 = + (* Sanity checks *) + assert (is_aignored child0.V.value); + assert (is_aignored child1.V.value); + (* Same remarks as for [merge_amut_borrows] *) + let ty = ty0 in + let child = child0 in + let value = V.ALoan (V.AMutLoan (id, child)) in + { V.value; ty } + in + let merge_ashared_loans ids ty0 (sv0 : V.typed_value) child0 _ty1 + (sv1 : V.typed_value) child1 = + (* Sanity checks *) + assert (is_aignored child0.V.value); + assert (is_aignored child1.V.value); + (* Same remarks as for [merge_amut_borrows]. + + This time we need to also merge the shared values. We rely on the + join matcher [JM] to do so. + *) + assert (not (value_has_loans_or_borrows ctx sv0.V.value)); + assert (not (value_has_loans_or_borrows ctx sv1.V.value)); + let ty = ty0 in + let child = child0 in + let sv = M.match_typed_values ctx sv0 sv1 in + let value = V.ALoan (V.ASharedLoan (ids, sv, child)) in + { V.value; ty } + in + { + merge_amut_borrows; + merge_ashared_borrows; + merge_amut_loans; + merge_ashared_loans; + } + +let merge_into_abstraction (loop_id : V.LoopId.id) (abs_kind : V.abs_kind) + (can_end : bool) (ctx : C.eval_ctx) (aid0 : V.AbstractionId.id) + (aid1 : V.AbstractionId.id) : C.eval_ctx * V.AbstractionId.id = + let merge_funs = mk_collapse_ctx_merge_duplicate_funs loop_id ctx in + merge_into_abstraction abs_kind can_end (Some merge_funs) ctx aid0 aid1 + +(** Collapse an environment, merging the duplicated borrows/loans. + + This function simply calls {!collapse_ctx} with the proper merging functions. + + We do this because when we join environments, we may introduce duplicated + loans and borrows. See the explanations for {!join_ctxs}. + *) +let collapse_ctx_with_merge (loop_id : V.LoopId.id) (old_ids : ids_sets) + (ctx : C.eval_ctx) : C.eval_ctx = + let merge_funs = mk_collapse_ctx_merge_duplicate_funs loop_id ctx in + try collapse_ctx loop_id (Some merge_funs) old_ids ctx + with ValueMatchFailure _ -> raise (Failure "Unexpected") + +let join_ctxs (loop_id : V.LoopId.id) (fixed_ids : ids_sets) (ctx0 : C.eval_ctx) + (ctx1 : C.eval_ctx) : ctx_or_update = + (* Debug *) + log#ldebug + (lazy + ("join_ctxs:\n\n- fixed_ids:\n" ^ show_ids_sets fixed_ids + ^ "\n\n- ctx0:\n" + ^ eval_ctx_to_string_no_filter ctx0 + ^ "\n\n- ctx1:\n" + ^ eval_ctx_to_string_no_filter ctx1 + ^ "\n\n")); + + let env0 = List.rev ctx0.env in + let env1 = List.rev ctx1.env in + + (* We need to pick a context for some functions like [match_typed_values]: + the context is only used to lookup module data, so we can pick whichever + we want. + TODO: this is not very clean. Maybe we should just carry this data around. + *) + let ctx = ctx0 in + + let nabs = ref [] in + + (* Explore the environments. *) + let join_suffixes (env0 : C.env) (env1 : C.env) : C.env = + (* Debug *) + log#ldebug + (lazy + ("join_suffixes:\n\n- fixed_ids:\n" ^ show_ids_sets fixed_ids + ^ "\n\n- ctx0:\n" + ^ eval_ctx_to_string_no_filter { ctx0 with env = List.rev env0 } + ^ "\n\n- ctx1:\n" + ^ eval_ctx_to_string_no_filter { ctx1 with env = List.rev env1 } + ^ "\n\n")); + + (* Sanity check: there are no values/abstractions which should be in the prefix *) + let check_valid (ee : C.env_elem) : unit = + match ee with + | C.Var (C.VarBinder _, _) -> + (* Variables are necessarily in the prefix *) + raise (Failure "Unreachable") + | Var (C.DummyBinder did, _) -> + assert (not (C.DummyVarId.Set.mem did fixed_ids.dids)) + | Abs abs -> + assert (not (V.AbstractionId.Set.mem abs.abs_id fixed_ids.aids)) + | Frame -> + (* This should have been eliminated *) + raise (Failure "Unreachable") + in + List.iter check_valid env0; + List.iter check_valid env1; + (* Concatenate the suffixes and append the abstractions introduced while + joining the prefixes *) + let absl = List.map (fun abs -> C.Abs abs) (List.rev !nabs) in + List.concat [ env0; env1; absl ] + in + + let module S : MatchJoinState = struct + (* The context is only used to lookup module data: we can pick whichever we want *) + let ctx = ctx + let loop_id = loop_id + let nabs = nabs + end in + let module JM = MakeJoinMatcher (S) in + let module M = MakeMatcher (JM) in + (* Rem.: this function raises exceptions *) + let rec join_prefixes (env0 : C.env) (env1 : C.env) : C.env = + match (env0, env1) with + | ( (C.Var (C.DummyBinder b0, v0) as var0) :: env0', + (C.Var (C.DummyBinder b1, v1) as var1) :: env1' ) -> + (* Debug *) + log#ldebug + (lazy + ("join_prefixes: DummyBinders:\n\n- fixed_ids:\n" ^ "\n" + ^ show_ids_sets fixed_ids ^ "\n\n- value0:\n" + ^ env_elem_to_string ctx var0 + ^ "\n\n- value1:\n" + ^ env_elem_to_string ctx var1 + ^ "\n\n")); + + (* Two cases: the dummy value is an old value, in which case the bindings + must be the same and we must join their values. Otherwise, it means we + are not in the prefix anymore *) + if C.DummyVarId.Set.mem b0 fixed_ids.dids then ( + (* Still in the prefix: match the values *) + assert (b0 = b1); + let b = b0 in + let v = M.match_typed_values ctx v0 v1 in + let var = C.Var (C.DummyBinder b, v) in + (* Continue *) + var :: join_prefixes env0' env1') + else (* Not in the prefix anymore *) + join_suffixes env0 env1 + | ( (C.Var (C.VarBinder b0, v0) as var0) :: env0', + (C.Var (C.VarBinder b1, v1) as var1) :: env1' ) -> + (* Debug *) + log#ldebug + (lazy + ("join_prefixes: VarBinders:\n\n- fixed_ids:\n" ^ "\n" + ^ show_ids_sets fixed_ids ^ "\n\n- value0:\n" + ^ env_elem_to_string ctx var0 + ^ "\n\n- value1:\n" + ^ env_elem_to_string ctx var1 + ^ "\n\n")); + + (* Variable bindings *must* be in the prefix and consequently their + ids must be the same *) + assert (b0 = b1); + (* Match the values *) + let b = b0 in + let v = M.match_typed_values ctx v0 v1 in + let var = C.Var (C.VarBinder b, v) in + (* Continue *) + var :: join_prefixes env0' env1' + | (C.Abs abs0 as abs) :: env0', C.Abs abs1 :: env1' -> + (* Debug *) + log#ldebug + (lazy + ("join_prefixes: Abs:\n\n- fixed_ids:\n" ^ "\n" + ^ show_ids_sets fixed_ids ^ "\n\n- abs0:\n" ^ abs_to_string ctx abs0 + ^ "\n\n- abs1:\n" ^ abs_to_string ctx abs1 ^ "\n\n")); + + (* Same as for the dummy values: there are two cases *) + if V.AbstractionId.Set.mem abs0.abs_id fixed_ids.aids then ( + (* Still in the prefix: the abstractions must be the same *) + assert (abs0 = abs1); + (* Continue *) + abs :: join_prefixes env0' env1') + else (* Not in the prefix anymore *) + join_suffixes env0 env1 + | _ -> + (* The elements don't match: we are not in the prefix anymore *) + join_suffixes env0 env1 + in + + try + (* Remove the frame delimiter (the first element of an environment is a frame delimiter) *) + let env0, env1 = + match (env0, env1) with + | C.Frame :: env0, C.Frame :: env1 -> (env0, env1) + | _ -> raise (Failure "Unreachable") + in + + log#ldebug + (lazy + ("- env0:\n" ^ C.show_env env0 ^ "\n\n- env1:\n" ^ C.show_env env1 + ^ "\n\n")); + + let env = List.rev (C.Frame :: join_prefixes env0 env1) in + + (* Construct the joined context - of course, the type, fun, etc. contexts + * should be the same in the two contexts *) + let { + C.type_context; + fun_context; + global_context; + region_groups; + type_vars; + env = _; + ended_regions = ended_regions0; + } = + ctx0 + in + let { + C.type_context = _; + fun_context = _; + global_context = _; + region_groups = _; + type_vars = _; + env = _; + ended_regions = ended_regions1; + } = + ctx1 + in + let ended_regions = T.RegionId.Set.union ended_regions0 ended_regions1 in + Ok + { + C.type_context; + fun_context; + global_context; + region_groups; + type_vars; + env; + ended_regions; + } + with ValueMatchFailure e -> Error e + +(** Destructure all the new abstractions *) +let destructure_new_abs (loop_id : V.LoopId.id) + (old_abs_ids : V.AbstractionId.Set.t) (ctx : C.eval_ctx) : C.eval_ctx = + let abs_kind = V.Loop (loop_id, None, V.LoopSynthInput) in + let can_end = true in + let destructure_shared_values = true in + let is_fresh_abs_id (id : V.AbstractionId.id) : bool = + not (V.AbstractionId.Set.mem id old_abs_ids) + in + let env = + C.env_map_abs + (fun abs -> + if is_fresh_abs_id abs.abs_id then + let abs = + destructure_abs abs_kind can_end destructure_shared_values ctx abs + in + abs + else abs) + ctx.env + in + { ctx with env } + +(** Refresh the ids of the fresh abstractions. + + We do this because {!prepare_ashared_loans} introduces some non-fixed + abstractions in contexts which are later joined: we have to make sure two + contexts we join don't have non-fixed abstractions with the same ids. + *) +let refresh_abs (old_abs : V.AbstractionId.Set.t) (ctx : C.eval_ctx) : + C.eval_ctx = + let ids, _ = compute_context_ids ctx in + let abs_to_refresh = V.AbstractionId.Set.diff ids.aids old_abs in + let aids_subst = + List.map + (fun id -> (id, C.fresh_abstraction_id ())) + (V.AbstractionId.Set.elements abs_to_refresh) + in + let aids_subst = V.AbstractionId.Map.of_list aids_subst in + let subst id = + match V.AbstractionId.Map.find_opt id aids_subst with + | None -> id + | Some id -> id + in + let env = + Subst.env_subst_ids + (fun x -> x) + (fun x -> x) + (fun x -> x) + (fun x -> x) + (fun x -> x) + subst ctx.env + in + { ctx with C.env } + +let loop_join_origin_with_continue_ctxs (config : C.config) + (loop_id : V.LoopId.id) (fixed_ids : ids_sets) (old_ctx : C.eval_ctx) + (ctxl : C.eval_ctx list) : (C.eval_ctx * C.eval_ctx list) * C.eval_ctx = + (* # Join with the new contexts, one by one + + For every context, we repeteadly attempt to join it with the current + result of the join: if we fail (because we need to end loans for instance), + we update the context and retry. + Rem.: we should never have to end loans in the aggregated context, only + in the one we are trying to add to the join. + *) + let joined_ctx = ref old_ctx in + let rec join_one_aux (ctx : C.eval_ctx) : C.eval_ctx = + match join_ctxs loop_id fixed_ids !joined_ctx ctx with + | Ok nctx -> + joined_ctx := nctx; + ctx + | Error err -> + let ctx = + match err with + | LoanInRight bid -> + InterpreterBorrows.end_borrow_no_synth config bid ctx + | LoansInRight bids -> + InterpreterBorrows.end_borrows_no_synth config bids ctx + | AbsInRight _ | AbsInLeft _ | LoanInLeft _ | LoansInLeft _ -> + raise (Failure "Unexpected") + in + join_one_aux ctx + in + let join_one (ctx : C.eval_ctx) : C.eval_ctx = + log#ldebug + (lazy + ("loop_join_origin_with_continue_ctxs:join_one: initial ctx:\n" + ^ eval_ctx_to_string ctx)); + + (* Destructure the abstractions introduced in the new context *) + let ctx = destructure_new_abs loop_id fixed_ids.aids ctx in + log#ldebug + (lazy + ("loop_join_origin_with_continue_ctxs:join_one: after destructure:\n" + ^ eval_ctx_to_string ctx)); + + (* Collapse the context we want to add to the join *) + let ctx = collapse_ctx loop_id None fixed_ids ctx in + log#ldebug + (lazy + ("loop_join_origin_with_continue_ctxs:join_one: after collapse:\n" + ^ eval_ctx_to_string ctx)); + + (* Refresh the fresh abstractions *) + let ctx = refresh_abs fixed_ids.aids ctx in + + (* Join the two contexts *) + let ctx1 = join_one_aux ctx in + log#ldebug + (lazy + ("loop_join_origin_with_continue_ctxs:join_one: after join:\n" + ^ eval_ctx_to_string ctx1)); + + (* Collapse again - the join might have introduce abstractions we want + to merge with the others (note that those abstractions may actually + lead to borrows/loans duplications) *) + joined_ctx := collapse_ctx_with_merge loop_id fixed_ids !joined_ctx; + log#ldebug + (lazy + ("loop_join_origin_with_continue_ctxs:join_one: after join-collapse:\n" + ^ eval_ctx_to_string !joined_ctx)); + + (* Sanity check *) + if !Config.check_invariants then Invariants.check_invariants !joined_ctx; + (* Return *) + ctx1 + in + + let ctxl = List.map join_one ctxl in + + (* # Return *) + ((old_ctx, ctxl), !joined_ctx) diff --git a/compiler/InterpreterLoopsJoinCtxs.mli b/compiler/InterpreterLoopsJoinCtxs.mli new file mode 100644 index 00000000..ae655fb8 --- /dev/null +++ b/compiler/InterpreterLoopsJoinCtxs.mli @@ -0,0 +1,120 @@ +module T = Types +module PV = PrimitiveValues +module V = Values +module E = Expressions +module C = Contexts +module Subst = Substitute +module A = LlbcAst +module L = Logging +module Inv = Invariants +module S = SynthesizeSymbolic +open InterpreterUtils +open InterpreterLoopsCore + +(** Merge an abstraction into another abstraction in a context. + + This function is similar to {!InterpreterBorrows.merge_into_abstraction}. + + Parameters: + - [loop_id] + - [abs_kind] + - [can_end] + - [ctx] + - [aid0] + - [aid1] + *) +val merge_into_abstraction : + V.loop_id -> + V.abs_kind -> + bool -> + C.eval_ctx -> + V.abstraction_id -> + V.abstraction_id -> + C.eval_ctx * V.abstraction_id + +(** Join two contexts. + + We use this to join the environments at loop (re-)entry to progressively + compute a fixed point. + + We make the hypothesis (and check it) that the environments have the same + prefixes (same variable ids, same abstractions, etc.). The prefix of + variable and abstraction ids is given by the [fixed_ids] identifier sets. We + check that those prefixes are the same (the dummy variables are the same, + the abstractions are the same), match the values mapped to by the variables + which are not dummy, then group the additional dummy variables/abstractions + together. In a sense, the [fixed_ids] define a frame (in a separation logic + sense). + + Note that when joining the values mapped to by the non-dummy variables, we + may introduce duplicated borrows. Also, we don't match the abstractions + which are not in the prefix, and this can also lead to borrow + duplications. For this reason, the environment needs to be collapsed + afterwards to get rid of those duplicated loans/borrows. + + For instance, if we have: + {[ + fixed = { abs0 } + + env0 = { + abs0 { ML l0 } + l -> MB l0 s0 + } + + env1 = { + abs0 { ML l0 } + l -> MB l1 s1 + abs1 { MB l0, ML l1 } + } + ]} + + We get: + {[ + join env0 env1 = { + abs0 { ML l0 } (* abs0 is fixed: we simply check it is equal in env0 and env1 *) + l -> MB l2 s2 + abs1 { MB l0, ML l1 } (* abs1 is new: we keep it unchanged *) + abs2 { MB l0, MB l1, ML l2 } (* Introduced when joining on the "l" variable *) + } + ]} + + Rem.: in practice, this join works because we take care of pushing new values + and abstractions *at the end* of the environments, meaning the environment + prefixes keep the same structure. + + Rem.: assuming that the environment has some structure poses *no soundness + issue*. It can only make the join fail if the environments actually don't have + this structure: this is a *completeness issue*. + + Parameters: + - [loop_id] + - [fixed_ids] + - [ctx0] + - [ctx1] + *) +val join_ctxs : + V.loop_id -> ids_sets -> C.eval_ctx -> C.eval_ctx -> ctx_or_update + +(** Join the context at the entry of the loop with the contexts upon reentry + (upon reaching the [Continue] statement - the goal is to compute a fixed + point for the loop entry). + + As we may have to end loans in the environments before doing the join, + we return those updated environments, and the joined environment. + + This function is mostly built on top of {!join_ctxs}. + + Parameters: + - [config] + - [loop_id] + - [fixed_ids] + - [old_ctx] + - [ctxl] + *) +val loop_join_origin_with_continue_ctxs : + C.config -> + V.loop_id -> + ids_sets -> + C.eval_ctx -> + C.eval_ctx list -> + (C.eval_ctx * C.eval_ctx list) * C.eval_ctx diff --git a/compiler/InterpreterLoopsMatchCtxs.ml b/compiler/InterpreterLoopsMatchCtxs.ml new file mode 100644 index 00000000..f9d45f20 --- /dev/null +++ b/compiler/InterpreterLoopsMatchCtxs.ml @@ -0,0 +1,1591 @@ +(** This module implements support to match contexts for loops. + + The matching functions are used for instance to compute joins, or + to check if two contexts are equivalent (modulo conversion). + *) + +module T = Types +module PV = PrimitiveValues +module V = Values +module E = Expressions +module C = Contexts +module Subst = Substitute +module A = LlbcAst +module L = Logging +open TypesUtils +open ValuesUtils +module Inv = Invariants +module S = SynthesizeSymbolic +open Cps +open InterpreterUtils +open InterpreterBorrows +open InterpreterLoopsCore + +(** The local logger *) +let log = L.loops_match_ctxs_log + +let compute_abs_borrows_loans_maps (no_duplicates : bool) + (explore : V.abs -> bool) (env : C.env) : abs_borrows_loans_maps = + let abs_ids = ref [] in + let abs_to_borrows = ref V.AbstractionId.Map.empty in + let abs_to_loans = ref V.AbstractionId.Map.empty in + let abs_to_borrows_loans = ref V.AbstractionId.Map.empty in + let borrow_to_abs = ref V.BorrowId.Map.empty in + let loan_to_abs = ref V.BorrowId.Map.empty in + let borrow_loan_to_abs = ref V.BorrowId.Map.empty in + + let module R (Id0 : Identifiers.Id) (Id1 : Identifiers.Id) = struct + (* + [check_singleton_sets]: check that the mapping maps to a singletong. + [check_not_already_registered]: check if the mapping was not already registered. + *) + let register_mapping (check_singleton_sets : bool) + (check_not_already_registered : bool) (map : Id1.Set.t Id0.Map.t ref) + (id0 : Id0.id) (id1 : Id1.id) : unit = + (* Sanity check *) + (if check_singleton_sets || check_not_already_registered then + match Id0.Map.find_opt id0 !map with + | None -> () + | Some set -> + assert ( + (not check_not_already_registered) || not (Id1.Set.mem id1 set))); + (* Update the mapping *) + map := + Id0.Map.update id0 + (fun ids -> + match ids with + | None -> Some (Id1.Set.singleton id1) + | Some ids -> + (* Sanity check *) + assert (not check_singleton_sets); + assert ( + (not check_not_already_registered) + || not (Id1.Set.mem id1 ids)); + (* Update *) + Some (Id1.Set.add id1 ids)) + !map + end in + let module RAbsBorrow = R (V.AbstractionId) (V.BorrowId) in + let module RBorrowAbs = R (V.BorrowId) (V.AbstractionId) in + let register_borrow_id abs_id bid = + RAbsBorrow.register_mapping false no_duplicates abs_to_borrows abs_id bid; + RAbsBorrow.register_mapping false false abs_to_borrows_loans abs_id bid; + RBorrowAbs.register_mapping no_duplicates no_duplicates borrow_to_abs bid + abs_id; + RBorrowAbs.register_mapping false false borrow_loan_to_abs bid abs_id + in + + let register_loan_id abs_id bid = + RAbsBorrow.register_mapping false no_duplicates abs_to_loans abs_id bid; + RAbsBorrow.register_mapping false false abs_to_borrows_loans abs_id bid; + RBorrowAbs.register_mapping no_duplicates no_duplicates loan_to_abs bid + abs_id; + RBorrowAbs.register_mapping false false borrow_loan_to_abs bid abs_id + in + + let explore_abs = + object (self : 'self) + inherit [_] V.iter_typed_avalue as super + + (** Make sure we don't register the ignored ids *) + method! visit_aloan_content abs_id lc = + match lc with + | AMutLoan _ | ASharedLoan _ -> + (* Process those normally *) + super#visit_aloan_content abs_id lc + | AIgnoredMutLoan (_, child) + | AEndedIgnoredMutLoan { child; given_back = _; given_back_meta = _ } + | AIgnoredSharedLoan child -> + (* Ignore the id of the loan, if there is *) + self#visit_typed_avalue abs_id child + | AEndedMutLoan _ | AEndedSharedLoan _ -> raise (Failure "Unreachable") + + (** Make sure we don't register the ignored ids *) + method! visit_aborrow_content abs_id bc = + match bc with + | AMutBorrow _ | ASharedBorrow _ | AProjSharedBorrow _ -> + (* Process those normally *) + super#visit_aborrow_content abs_id bc + | AIgnoredMutBorrow (_, child) + | AEndedIgnoredMutBorrow { child; given_back = _; given_back_meta = _ } + -> + (* Ignore the id of the borrow, if there is *) + self#visit_typed_avalue abs_id child + | AEndedMutBorrow _ | AEndedSharedBorrow -> + raise (Failure "Unreachable") + + method! visit_borrow_id abs_id bid = register_borrow_id abs_id bid + method! visit_loan_id abs_id lid = register_loan_id abs_id lid + end + in + + C.env_iter_abs + (fun abs -> + let abs_id = abs.abs_id in + if explore abs then ( + abs_to_borrows := + V.AbstractionId.Map.add abs_id V.BorrowId.Set.empty !abs_to_borrows; + abs_to_loans := + V.AbstractionId.Map.add abs_id V.BorrowId.Set.empty !abs_to_loans; + abs_ids := abs.abs_id :: !abs_ids; + List.iter (explore_abs#visit_typed_avalue abs.abs_id) abs.avalues) + else ()) + env; + + (* Rem.: there is no need to reverse the abs ids, because we explored the environment + starting with the freshest values and abstractions *) + { + abs_ids = !abs_ids; + abs_to_borrows = !abs_to_borrows; + abs_to_loans = !abs_to_loans; + abs_to_borrows_loans = !abs_to_borrows_loans; + borrow_to_abs = !borrow_to_abs; + loan_to_abs = !loan_to_abs; + borrow_loan_to_abs = !borrow_loan_to_abs; + } + +(** Match two types during a join. *) +let rec match_types (match_distinct_types : 'r T.ty -> 'r T.ty -> 'r T.ty) + (match_regions : 'r -> 'r -> 'r) (ty0 : 'r T.ty) (ty1 : 'r T.ty) : 'r T.ty = + let match_rec = match_types match_distinct_types match_regions in + match (ty0, ty1) with + | Adt (id0, regions0, tys0), Adt (id1, regions1, tys1) -> + assert (id0 = id1); + let id = id0 in + let regions = + List.map + (fun (id0, id1) -> match_regions id0 id1) + (List.combine regions0 regions1) + in + let tys = + List.map (fun (ty0, ty1) -> match_rec ty0 ty1) (List.combine tys0 tys1) + in + Adt (id, regions, tys) + | TypeVar vid0, TypeVar vid1 -> + assert (vid0 = vid1); + let vid = vid0 in + TypeVar vid + | Bool, Bool | Char, Char | Never, Never | Str, Str -> ty0 + | Integer int_ty0, Integer int_ty1 -> + assert (int_ty0 = int_ty1); + ty0 + | Array ty0, Array ty1 | Slice ty0, Slice ty1 -> match_rec ty0 ty1 + | Ref (r0, ty0, k0), Ref (r1, ty1, k1) -> + let r = match_regions r0 r1 in + let ty = match_rec ty0 ty1 in + assert (k0 = k1); + let k = k0 in + Ref (r, ty, k) + | _ -> match_distinct_types ty0 ty1 + +module MakeMatcher (M : PrimMatcher) : Matcher = struct + let rec match_typed_values (ctx : C.eval_ctx) (v0 : V.typed_value) + (v1 : V.typed_value) : V.typed_value = + let match_rec = match_typed_values ctx in + let ty = M.match_etys v0.V.ty v1.V.ty in + match (v0.V.value, v1.V.value) with + | V.Primitive pv0, V.Primitive pv1 -> + if pv0 = pv1 then v1 else M.match_distinct_primitive_values ty pv0 pv1 + | V.Adt av0, V.Adt av1 -> + if av0.variant_id = av1.variant_id then + let fields = List.combine av0.field_values av1.field_values in + let field_values = + List.map (fun (f0, f1) -> match_rec f0 f1) fields + in + let value : V.value = + V.Adt { variant_id = av0.variant_id; field_values } + in + { V.value; ty = v1.V.ty } + else ( + (* For now, we don't merge ADTs which contain borrows *) + assert (not (value_has_borrows ctx v0.V.value)); + assert (not (value_has_borrows ctx v1.V.value)); + (* Merge *) + M.match_distinct_adts ty av0 av1) + | Bottom, Bottom -> v0 + | Borrow bc0, Borrow bc1 -> + let bc = + match (bc0, bc1) with + | SharedBorrow bid0, SharedBorrow bid1 -> + let bid = M.match_shared_borrows match_rec ty bid0 bid1 in + V.SharedBorrow bid + | MutBorrow (bid0, bv0), MutBorrow (bid1, bv1) -> + let bv = match_rec bv0 bv1 in + assert (not (value_has_borrows ctx bv.V.value)); + let bid, bv = M.match_mut_borrows ty bid0 bv0 bid1 bv1 bv in + V.MutBorrow (bid, bv) + | ReservedMutBorrow _, _ + | _, ReservedMutBorrow _ + | SharedBorrow _, MutBorrow _ + | MutBorrow _, SharedBorrow _ -> + (* If we get here, either there is a typing inconsistency, or we are + trying to match a reserved borrow, which shouldn't happen because + reserved borrow should be eliminated very quickly - they are introduced + just before function calls which activate them *) + raise (Failure "Unexpected") + in + { V.value = V.Borrow bc; ty } + | Loan lc0, Loan lc1 -> + (* TODO: maybe we should enforce that the ids are always exactly the same - + without matching *) + let lc = + match (lc0, lc1) with + | SharedLoan (ids0, sv0), SharedLoan (ids1, sv1) -> + let sv = match_rec sv0 sv1 in + assert (not (value_has_borrows ctx sv.V.value)); + let ids, sv = M.match_shared_loans ty ids0 ids1 sv in + V.SharedLoan (ids, sv) + | MutLoan id0, MutLoan id1 -> + let id = M.match_mut_loans ty id0 id1 in + V.MutLoan id + | SharedLoan _, MutLoan _ | MutLoan _, SharedLoan _ -> + raise (Failure "Unreachable") + in + { V.value = Loan lc; ty = v1.V.ty } + | Symbolic sv0, Symbolic sv1 -> + (* For now, we force all the symbolic values containing borrows to + be eagerly expanded, and we don't support nested borrows *) + assert (not (value_has_borrows ctx v0.V.value)); + assert (not (value_has_borrows ctx v1.V.value)); + (* Match *) + let sv = M.match_symbolic_values sv0 sv1 in + { v1 with V.value = V.Symbolic sv } + | Loan lc, _ -> ( + match lc with + | SharedLoan (ids, _) -> raise (ValueMatchFailure (LoansInLeft ids)) + | MutLoan id -> raise (ValueMatchFailure (LoanInLeft id))) + | _, Loan lc -> ( + match lc with + | SharedLoan (ids, _) -> raise (ValueMatchFailure (LoansInRight ids)) + | MutLoan id -> raise (ValueMatchFailure (LoanInRight id))) + | Symbolic sv, _ -> M.match_symbolic_with_other true sv v1 + | _, Symbolic sv -> M.match_symbolic_with_other false sv v0 + | Bottom, _ -> M.match_bottom_with_other true v1 + | _, Bottom -> M.match_bottom_with_other false v0 + | _ -> + log#ldebug + (lazy + ("Unexpected match case:\n- value0: " + ^ typed_value_to_string ctx v0 + ^ "\n- value1: " + ^ typed_value_to_string ctx v1)); + raise (Failure "Unexpected match case") + + and match_typed_avalues (ctx : C.eval_ctx) (v0 : V.typed_avalue) + (v1 : V.typed_avalue) : V.typed_avalue = + log#ldebug + (lazy + ("match_typed_avalues:\n- value0: " + ^ typed_avalue_to_string ctx v0 + ^ "\n- value1: " + ^ typed_avalue_to_string ctx v1)); + + let match_rec = match_typed_values ctx in + let match_arec = match_typed_avalues ctx in + let ty = M.match_rtys v0.V.ty v1.V.ty in + match (v0.V.value, v1.V.value) with + | V.AAdt av0, V.AAdt av1 -> + if av0.variant_id = av1.variant_id then + let fields = List.combine av0.field_values av1.field_values in + let field_values = + List.map (fun (f0, f1) -> match_arec f0 f1) fields + in + let value : V.avalue = + V.AAdt { variant_id = av0.variant_id; field_values } + in + { V.value; ty } + else (* Merge *) + M.match_distinct_aadts v0.V.ty av0 v1.V.ty av1 ty + | ABottom, ABottom -> mk_abottom ty + | AIgnored, AIgnored -> mk_aignored ty + | ABorrow bc0, ABorrow bc1 -> ( + log#ldebug (lazy "match_typed_avalues: borrows"); + match (bc0, bc1) with + | ASharedBorrow bid0, ASharedBorrow bid1 -> + log#ldebug (lazy "match_typed_avalues: shared borrows"); + M.match_ashared_borrows v0.V.ty bid0 v1.V.ty bid1 ty + | AMutBorrow (bid0, av0), AMutBorrow (bid1, av1) -> + log#ldebug (lazy "match_typed_avalues: mut borrows"); + log#ldebug + (lazy + "match_typed_avalues: mut borrows: matching children values"); + let av = match_arec av0 av1 in + log#ldebug + (lazy "match_typed_avalues: mut borrows: matched children values"); + M.match_amut_borrows v0.V.ty bid0 av0 v1.V.ty bid1 av1 ty av + | AIgnoredMutBorrow _, AIgnoredMutBorrow _ -> + (* The abstractions are destructured: we shouldn't get there *) + raise (Failure "Unexpected") + | AProjSharedBorrow asb0, AProjSharedBorrow asb1 -> ( + match (asb0, asb1) with + | [], [] -> + (* This case actually stands for ignored shared borrows, when + there are no nested borrows *) + v0 + | _ -> + (* We should get there only if there are nested borrows *) + raise (Failure "Unexpected")) + | _ -> + (* TODO: getting there is not necessarily inconsistent (it may + just be because the environments don't match) so we may want + to call a specific function (which could raise the proper + exception). + Rem.: we shouldn't get to the ended borrow cases, because + an abstraction should never contain ended borrows unless + we are *currently* ending it, in which case we need + to completely end it before continuing. + *) + raise (Failure "Unexpected")) + | ALoan lc0, ALoan lc1 -> ( + log#ldebug (lazy "match_typed_avalues: loans"); + (* TODO: maybe we should enforce that the ids are always exactly the same - + without matching *) + match (lc0, lc1) with + | ASharedLoan (ids0, sv0, av0), ASharedLoan (ids1, sv1, av1) -> + log#ldebug (lazy "match_typed_avalues: shared loans"); + let sv = match_rec sv0 sv1 in + let av = match_arec av0 av1 in + assert (not (value_has_borrows ctx sv.V.value)); + M.match_ashared_loans v0.V.ty ids0 sv0 av0 v1.V.ty ids1 sv1 av1 ty + sv av + | AMutLoan (id0, av0), AMutLoan (id1, av1) -> + log#ldebug (lazy "match_typed_avalues: mut loans"); + log#ldebug + (lazy "match_typed_avalues: mut loans: matching children values"); + let av = match_arec av0 av1 in + log#ldebug + (lazy "match_typed_avalues: mut loans: matched children values"); + M.match_amut_loans v0.V.ty id0 av0 v1.V.ty id1 av1 ty av + | AIgnoredMutLoan _, AIgnoredMutLoan _ + | AIgnoredSharedLoan _, AIgnoredSharedLoan _ -> + (* Those should have been filtered when destructuring the abstractions - + they are necessary only when there are nested borrows *) + raise (Failure "Unreachable") + | _ -> raise (Failure "Unreachable")) + | ASymbolic _, ASymbolic _ -> + (* For now, we force all the symbolic values containing borrows to + be eagerly expanded, and we don't support nested borrows *) + raise (Failure "Unreachable") + | _ -> M.match_avalues v0 v1 +end + +module MakeJoinMatcher (S : MatchJoinState) : PrimMatcher = struct + (** Small utility *) + let push_abs (abs : V.abs) : unit = S.nabs := abs :: !S.nabs + + let push_absl (absl : V.abs list) : unit = List.iter push_abs absl + + let match_etys ty0 ty1 = + assert (ty0 = ty1); + ty0 + + let match_rtys ty0 ty1 = + (* The types must be equal - in effect, this forbids to match symbolic + values containing borrows *) + assert (ty0 = ty1); + ty0 + + let match_distinct_primitive_values (ty : T.ety) (_ : V.primitive_value) + (_ : V.primitive_value) : V.typed_value = + mk_fresh_symbolic_typed_value_from_ety V.LoopJoin ty + + let match_distinct_adts (ty : T.ety) (adt0 : V.adt_value) (adt1 : V.adt_value) + : V.typed_value = + (* Check that the ADTs don't contain borrows - this is redundant with checks + performed by the caller, but we prefer to be safe with regards to future + updates + *) + let check_no_borrows (v : V.typed_value) = + assert (not (value_has_borrows S.ctx v.V.value)) + in + List.iter check_no_borrows adt0.field_values; + List.iter check_no_borrows adt1.field_values; + + (* Check if there are loans: we request to end them *) + let check_loans (left : bool) (fields : V.typed_value list) : unit = + match InterpreterBorrowsCore.get_first_loan_in_values fields with + | Some (V.SharedLoan (ids, _)) -> + if left then raise (ValueMatchFailure (LoansInLeft ids)) + else raise (ValueMatchFailure (LoansInRight ids)) + | Some (V.MutLoan id) -> + if left then raise (ValueMatchFailure (LoanInLeft id)) + else raise (ValueMatchFailure (LoanInRight id)) + | None -> () + in + check_loans true adt0.field_values; + check_loans false adt1.field_values; + + (* No borrows, no loans: we can introduce a symbolic value *) + mk_fresh_symbolic_typed_value_from_ety V.LoopJoin ty + + let match_shared_borrows _ (ty : T.ety) (bid0 : V.borrow_id) + (bid1 : V.borrow_id) : V.borrow_id = + if bid0 = bid1 then bid0 + else + (* We replace bid0 and bid1 with a fresh borrow id, and introduce + an abstraction which links all of them: + {[ + { SB bid0, SB bid1, SL {bid2} } + ]} + *) + let rid = C.fresh_region_id () in + let bid2 = C.fresh_borrow_id () in + + (* Generate a fresh symbolic value for the shared value *) + let _, bv_ty, kind = ty_as_ref ty in + let sv = mk_fresh_symbolic_typed_value_from_ety V.LoopJoin bv_ty in + + let borrow_ty = + mk_ref_ty (T.Var rid) (ety_no_regions_to_rty bv_ty) kind + in + + (* Generate the avalues for the abstraction *) + let mk_aborrow (bid : V.borrow_id) : V.typed_avalue = + let value = V.ABorrow (V.ASharedBorrow bid) in + { V.value; ty = borrow_ty } + in + let borrows = [ mk_aborrow bid0; mk_aborrow bid1 ] in + + let loan = + V.ASharedLoan + ( V.BorrowId.Set.singleton bid2, + sv, + mk_aignored (ety_no_regions_to_rty bv_ty) ) + in + (* Note that an aloan has a borrow type *) + let loan = { V.value = V.ALoan loan; ty = borrow_ty } in + + let avalues = List.append borrows [ loan ] in + + (* Generate the abstraction *) + let abs = + { + V.abs_id = C.fresh_abstraction_id (); + kind = V.Loop (S.loop_id, None, LoopSynthInput); + can_end = true; + parents = V.AbstractionId.Set.empty; + original_parents = []; + regions = T.RegionId.Set.singleton rid; + ancestors_regions = T.RegionId.Set.empty; + avalues; + } + in + push_abs abs; + + (* Return the new borrow *) + bid2 + + let match_mut_borrows (ty : T.ety) (bid0 : V.borrow_id) (bv0 : V.typed_value) + (bid1 : V.borrow_id) (bv1 : V.typed_value) (bv : V.typed_value) : + V.borrow_id * V.typed_value = + if bid0 = bid1 then ( + (* If the merged value is not the same as the original value, we introduce + an abstraction: + + {[ + { MB bid0, ML nbid } // where nbid fresh + ]} + + and we use bid' for the borrow id that we return. + + We do this because we want to make sure that, whenever a mutably + borrowed value is modified in a loop iteration, then there is + a fresh abstraction between this borrowed value and the fixed + abstractions. + *) + assert (not (value_has_borrows S.ctx bv.V.value)); + + if bv0 = bv1 then ( + assert (bv0 = bv); + (bid0, bv)) + else + let rid = C.fresh_region_id () in + let nbid = C.fresh_borrow_id () in + + let kind = T.Mut in + let bv_ty = ety_no_regions_to_rty bv.V.ty in + let borrow_ty = mk_ref_ty (T.Var rid) bv_ty kind in + + let borrow_av = + let ty = borrow_ty in + let value = V.ABorrow (V.AMutBorrow (bid0, mk_aignored bv_ty)) in + mk_typed_avalue ty value + in + + let loan_av = + let ty = borrow_ty in + let value = V.ALoan (V.AMutLoan (nbid, mk_aignored bv_ty)) in + mk_typed_avalue ty value + in + + let avalues = [ borrow_av; loan_av ] in + + (* Generate the abstraction *) + let abs = + { + V.abs_id = C.fresh_abstraction_id (); + kind = V.Loop (S.loop_id, None, LoopSynthInput); + can_end = true; + parents = V.AbstractionId.Set.empty; + original_parents = []; + regions = T.RegionId.Set.singleton rid; + ancestors_regions = T.RegionId.Set.empty; + avalues; + } + in + push_abs abs; + + (* Return the new borrow *) + (nbid, bv)) + else + (* We replace bid0 and bid1 with a fresh borrow id, and introduce + an abstraction which links all of them: + {[ + { MB bid0, MB bid1, ML bid2 } + ]} + *) + let rid = C.fresh_region_id () in + let bid2 = C.fresh_borrow_id () in + + (* Generate a fresh symbolic value for the borrowed value *) + let _, bv_ty, kind = ty_as_ref ty in + let sv = mk_fresh_symbolic_typed_value_from_ety V.LoopJoin bv_ty in + + let borrow_ty = + mk_ref_ty (T.Var rid) (ety_no_regions_to_rty bv_ty) kind + in + + (* Generate the avalues for the abstraction *) + let mk_aborrow (bid : V.borrow_id) (bv : V.typed_value) : V.typed_avalue = + let bv_ty = ety_no_regions_to_rty bv.V.ty in + let value = V.ABorrow (V.AMutBorrow (bid, mk_aignored bv_ty)) in + { V.value; ty = borrow_ty } + in + let borrows = [ mk_aborrow bid0 bv0; mk_aborrow bid1 bv1 ] in + + let loan = V.AMutLoan (bid2, mk_aignored (ety_no_regions_to_rty bv_ty)) in + (* Note that an aloan has a borrow type *) + let loan = { V.value = V.ALoan loan; ty = borrow_ty } in + + let avalues = List.append borrows [ loan ] in + + (* Generate the abstraction *) + let abs = + { + V.abs_id = C.fresh_abstraction_id (); + kind = V.Loop (S.loop_id, None, LoopSynthInput); + can_end = true; + parents = V.AbstractionId.Set.empty; + original_parents = []; + regions = T.RegionId.Set.singleton rid; + ancestors_regions = T.RegionId.Set.empty; + avalues; + } + in + push_abs abs; + + (* Return the new borrow *) + (bid2, sv) + + let match_shared_loans (_ : T.ety) (ids0 : V.loan_id_set) + (ids1 : V.loan_id_set) (sv : V.typed_value) : + V.loan_id_set * V.typed_value = + (* Check if the ids are the same - Rem.: we forbid the sets of loans + to be different. However, if we dive inside data-structures (by + using a shared borrow) the shared values might themselves contain + shared loans, which need to be matched. For this reason, we destructure + the shared values (see {!destructure_abs}). + *) + let extra_ids_left = V.BorrowId.Set.diff ids0 ids1 in + let extra_ids_right = V.BorrowId.Set.diff ids1 ids0 in + if not (V.BorrowId.Set.is_empty extra_ids_left) then + raise (ValueMatchFailure (LoansInLeft extra_ids_left)); + if not (V.BorrowId.Set.is_empty extra_ids_right) then + raise (ValueMatchFailure (LoansInRight extra_ids_right)); + + (* This should always be true if we get here *) + assert (ids0 = ids1); + let ids = ids0 in + + (* Return *) + (ids, sv) + + let match_mut_loans (_ : T.ety) (id0 : V.loan_id) (id1 : V.loan_id) : + V.loan_id = + if id0 = id1 then id0 + else + (* We forbid this case for now: if we get there, we force to end + both borrows *) + raise (ValueMatchFailure (LoanInLeft id0)) + + let match_symbolic_values (sv0 : V.symbolic_value) (sv1 : V.symbolic_value) : + V.symbolic_value = + let id0 = sv0.sv_id in + let id1 = sv1.sv_id in + if id0 = id1 then ( + (* Sanity check *) + assert (sv0 = sv1); + (* Return *) + sv0) + else ( + (* The caller should have checked that the symbolic values don't contain + borrows *) + assert (not (ty_has_borrows S.ctx.type_context.type_infos sv0.sv_ty)); + (* We simply introduce a fresh symbolic value *) + mk_fresh_symbolic_value V.LoopJoin sv0.sv_ty) + + let match_symbolic_with_other (left : bool) (sv : V.symbolic_value) + (v : V.typed_value) : V.typed_value = + (* Check that: + - there are no borrows in the symbolic value + - there are no borrows in the "regular" value + If there are loans in the regular value, raise an exception. + *) + assert (not (ty_has_borrows S.ctx.type_context.type_infos sv.sv_ty)); + assert (not (value_has_borrows S.ctx v.V.value)); + let value_is_left = not left in + (match InterpreterBorrowsCore.get_first_loan_in_value v with + | None -> () + | Some (SharedLoan (ids, _)) -> + if value_is_left then raise (ValueMatchFailure (LoansInLeft ids)) + else raise (ValueMatchFailure (LoansInRight ids)) + | Some (MutLoan id) -> + if value_is_left then raise (ValueMatchFailure (LoanInLeft id)) + else raise (ValueMatchFailure (LoanInRight id))); + (* Return a fresh symbolic value *) + mk_fresh_symbolic_typed_value V.LoopJoin sv.sv_ty + + let match_bottom_with_other (left : bool) (v : V.typed_value) : V.typed_value + = + (* If there are outer loans in the non-bottom value, raise an exception. + Otherwise, convert it to an abstraction and return [Bottom]. + *) + let with_borrows = false in + let value_is_left = not left in + match + InterpreterBorrowsCore.get_first_outer_loan_or_borrow_in_value + with_borrows v + with + | Some (BorrowContent _) -> raise (Failure "Unreachable") + | Some (LoanContent lc) -> ( + match lc with + | V.SharedLoan (ids, _) -> + if value_is_left then raise (ValueMatchFailure (LoansInLeft ids)) + else raise (ValueMatchFailure (LoansInRight ids)) + | V.MutLoan id -> + if value_is_left then raise (ValueMatchFailure (LoanInLeft id)) + else raise (ValueMatchFailure (LoanInRight id))) + | None -> + (* Convert the value to an abstraction *) + let abs_kind = V.Loop (S.loop_id, None, LoopSynthInput) in + let can_end = true in + let destructure_shared_values = true in + let absl = + convert_value_to_abstractions abs_kind can_end + destructure_shared_values S.ctx v + in + push_absl absl; + (* Return [Bottom] *) + mk_bottom v.V.ty + + (* As explained in comments: we don't use the join matcher to join avalues, + only concrete values *) + + let match_distinct_aadts _ _ _ _ _ = raise (Failure "Unreachable") + let match_ashared_borrows _ _ _ _ = raise (Failure "Unreachable") + let match_amut_borrows _ _ _ _ _ _ _ _ = raise (Failure "Unreachable") + let match_ashared_loans _ _ _ _ _ _ _ _ _ _ _ = raise (Failure "Unreachable") + let match_amut_loans _ _ _ _ _ _ _ _ = raise (Failure "Unreachable") + let match_avalues _ _ = raise (Failure "Unreachable") +end + +module MakeCheckEquivMatcher (S : MatchCheckEquivState) : CheckEquivMatcher = +struct + module MkGetSetM (Id : Identifiers.Id) = struct + module Inj = Id.InjSubst + + let add (msg : string) (m : Inj.t ref) (k0 : Id.id) (k1 : Id.id) = + (* Check if k0 is already registered as a key *) + match Inj.find_opt k0 !m with + | None -> + (* Not registered: check if k1 is in the set of values, + otherwise add the binding *) + if Inj.Set.mem k1 (Inj.elements !m) then + raise + (Distinct + (msg ^ "adding [k0=" ^ Id.to_string k0 ^ " -> k1=" + ^ Id.to_string k1 ^ " ]: k1 already in the set of elements")) + else ( + m := Inj.add k0 k1 !m; + k1) + | Some k1' -> + (* It is: check that the bindings are consistent *) + if k1 <> k1' then raise (Distinct (msg ^ "already a binding for k0")) + else k1 + + let match_e (msg : string) (m : Inj.t ref) (k0 : Id.id) (k1 : Id.id) : Id.id + = + (* TODO: merge the add and merge functions *) + add msg m k0 k1 + + let match_el (msg : string) (m : Inj.t ref) (kl0 : Id.id list) + (kl1 : Id.id list) : Id.id list = + List.map (fun (k0, k1) -> match_e msg m k0 k1) (List.combine kl0 kl1) + + (** Figuring out mappings between sets of ids is hard in all generality... + We use the fact that the fresh ids should have been generated + the same way (i.e., in the same order) and match the ids two by + two in increasing order. + *) + let match_es (msg : string) (m : Inj.t ref) (ks0 : Id.Set.t) + (ks1 : Id.Set.t) : Id.Set.t = + Id.Set.of_list + (match_el msg m (Id.Set.elements ks0) (Id.Set.elements ks1)) + end + + module GetSetRid = MkGetSetM (T.RegionId) + + let match_rid = GetSetRid.match_e "match_rid: " S.rid_map + let match_rids = GetSetRid.match_es "match_rids: " S.rid_map + + module GetSetBid = MkGetSetM (V.BorrowId) + + let match_blid msg = GetSetBid.match_e msg S.blid_map + let match_blidl msg = GetSetBid.match_el msg S.blid_map + let match_blids msg = GetSetBid.match_es msg S.blid_map + + let match_borrow_id = + if S.check_equiv then match_blid "match_borrow_id: " + else GetSetBid.match_e "match_borrow_id: " S.borrow_id_map + + let match_borrow_idl = + if S.check_equiv then match_blidl "match_borrow_idl: " + else GetSetBid.match_el "match_borrow_idl: " S.borrow_id_map + + let match_borrow_ids = + if S.check_equiv then match_blids "match_borrow_ids: " + else GetSetBid.match_es "match_borrow_ids: " S.borrow_id_map + + let match_loan_id = + if S.check_equiv then match_blid "match_loan_id: " + else GetSetBid.match_e "match_loan_id: " S.loan_id_map + + let match_loan_idl = + if S.check_equiv then match_blidl "match_loan_idl: " + else GetSetBid.match_el "match_loan_idl: " S.loan_id_map + + let match_loan_ids = + if S.check_equiv then match_blids "match_loan_ids: " + else GetSetBid.match_es "match_loan_ids: " S.loan_id_map + + module GetSetSid = MkGetSetM (V.SymbolicValueId) + module GetSetAid = MkGetSetM (V.AbstractionId) + + let match_aid = GetSetAid.match_e "match_aid: " S.aid_map + let match_aidl = GetSetAid.match_el "match_aidl: " S.aid_map + let match_aids = GetSetAid.match_es "match_aids: " S.aid_map + + (** *) + let match_etys ty0 ty1 = + if ty0 <> ty1 then raise (Distinct "match_etys") else ty0 + + let match_rtys ty0 ty1 = + let match_distinct_types _ _ = raise (Distinct "match_rtys") in + let match_regions r0 r1 = + match (r0, r1) with + | T.Static, T.Static -> r1 + | Var rid0, Var rid1 -> + let rid = match_rid rid0 rid1 in + Var rid + | _ -> raise (Distinct "match_rtys") + in + match_types match_distinct_types match_regions ty0 ty1 + + let match_distinct_primitive_values (ty : T.ety) (_ : V.primitive_value) + (_ : V.primitive_value) : V.typed_value = + mk_fresh_symbolic_typed_value_from_ety V.LoopJoin ty + + let match_distinct_adts (_ty : T.ety) (_adt0 : V.adt_value) + (_adt1 : V.adt_value) : V.typed_value = + raise (Distinct "match_distinct_adts") + + let match_shared_borrows + (match_typed_values : V.typed_value -> V.typed_value -> V.typed_value) + (_ty : T.ety) (bid0 : V.borrow_id) (bid1 : V.borrow_id) : V.borrow_id = + log#ldebug + (lazy + ("MakeCheckEquivMatcher: match_shared_borrows: " ^ "bid0: " + ^ V.BorrowId.to_string bid0 ^ ", bid1: " ^ V.BorrowId.to_string bid1)); + + let bid = match_borrow_id bid0 bid1 in + (* If we don't check for equivalence (i.e., we apply a fixed-point), + we lookup the shared values and match them. + *) + let _ = + if S.check_equiv then () + else + let v0 = S.lookup_shared_value_in_ctx0 bid0 in + let v1 = S.lookup_shared_value_in_ctx1 bid1 in + log#ldebug + (lazy + ("MakeCheckEquivMatcher: match_shared_borrows: looked up values:" + ^ "sv0: " + ^ typed_value_to_string S.ctx v0 + ^ ", sv1: " + ^ typed_value_to_string S.ctx v1)); + + let _ = match_typed_values v0 v1 in + () + in + bid + + let match_mut_borrows (_ty : T.ety) (bid0 : V.borrow_id) + (_bv0 : V.typed_value) (bid1 : V.borrow_id) (_bv1 : V.typed_value) + (bv : V.typed_value) : V.borrow_id * V.typed_value = + let bid = match_borrow_id bid0 bid1 in + (bid, bv) + + let match_shared_loans (_ : T.ety) (ids0 : V.loan_id_set) + (ids1 : V.loan_id_set) (sv : V.typed_value) : + V.loan_id_set * V.typed_value = + let ids = match_loan_ids ids0 ids1 in + (ids, sv) + + let match_mut_loans (_ : T.ety) (bid0 : V.loan_id) (bid1 : V.loan_id) : + V.loan_id = + match_loan_id bid0 bid1 + + let match_symbolic_values (sv0 : V.symbolic_value) (sv1 : V.symbolic_value) : + V.symbolic_value = + let id0 = sv0.sv_id in + let id1 = sv1.sv_id in + + log#ldebug + (lazy + ("MakeCheckEquivMatcher: match_symbolic_values: " ^ "sv0: " + ^ V.SymbolicValueId.to_string id0 + ^ ", sv1: " + ^ V.SymbolicValueId.to_string id1)); + + (* If we don't check for equivalence, we also update the map from sids + to values *) + if S.check_equiv then + (* Create the joined symbolic value *) + let sv_id = + GetSetSid.match_e "match_symbolic_values: ids: " S.sid_map id0 id1 + in + let sv_ty = match_rtys sv0.V.sv_ty sv1.V.sv_ty in + let sv_kind = + if sv0.V.sv_kind = sv1.V.sv_kind then sv0.V.sv_kind + else raise (Distinct "match_symbolic_values: sv_kind") + in + let sv = { V.sv_id; sv_ty; sv_kind } in + sv + else ( + (* Check: fixed values are fixed *) + assert (id0 = id1 || not (V.SymbolicValueId.InjSubst.mem id0 !S.sid_map)); + + (* Update the symbolic value mapping *) + let sv1 = mk_typed_value_from_symbolic_value sv1 in + + (* Update the symbolic value mapping *) + S.sid_to_value_map := + V.SymbolicValueId.Map.add_strict id0 sv1 !S.sid_to_value_map; + + (* Return - the returned value is not used: we can return whatever + we want *) + sv0) + + let match_symbolic_with_other (left : bool) (sv : V.symbolic_value) + (v : V.typed_value) : V.typed_value = + if S.check_equiv then raise (Distinct "match_symbolic_with_other") + else ( + assert left; + let id = sv.sv_id in + (* Check: fixed values are fixed *) + assert (not (V.SymbolicValueId.InjSubst.mem id !S.sid_map)); + (* Update the binding for the target symbolic value *) + S.sid_to_value_map := + V.SymbolicValueId.Map.add_strict id v !S.sid_to_value_map; + (* Return - the returned value is not used, so we can return whatever we want *) + v) + + let match_bottom_with_other (left : bool) (v : V.typed_value) : V.typed_value + = + (* It can happen that some variables get initialized in some branches + and not in some others, which causes problems when matching. *) + (* TODO: the returned value is not used, while it should: in generality it + should be ok to match a fixed-point with the environment we get at + a continue, where the fixed point contains some bottom values. *) + if left && not (value_has_loans_or_borrows S.ctx v.V.value) then + mk_bottom v.V.ty + else raise (Distinct "match_bottom_with_other") + + let match_distinct_aadts _ _ _ _ _ = raise (Distinct "match_distinct_adts") + + let match_ashared_borrows _ty0 bid0 _ty1 bid1 ty = + let bid = match_borrow_id bid0 bid1 in + let value = V.ABorrow (V.ASharedBorrow bid) in + { V.value; ty } + + let match_amut_borrows _ty0 bid0 _av0 _ty1 bid1 _av1 ty av = + let bid = match_borrow_id bid0 bid1 in + let value = V.ABorrow (V.AMutBorrow (bid, av)) in + { V.value; ty } + + let match_ashared_loans _ty0 ids0 _v0 _av0 _ty1 ids1 _v1 _av1 ty v av = + let bids = match_loan_ids ids0 ids1 in + let value = V.ALoan (V.ASharedLoan (bids, v, av)) in + { V.value; ty } + + let match_amut_loans _ty0 id0 _av0 _ty1 id1 _av1 ty av = + log#ldebug + (lazy + ("MakeCheckEquivMatcher:match_amut_loans:" ^ "\n- id0: " + ^ V.BorrowId.to_string id0 ^ "\n- id1: " ^ V.BorrowId.to_string id1 + ^ "\n- ty: " ^ rty_to_string S.ctx ty ^ "\n- av: " + ^ typed_avalue_to_string S.ctx av)); + + let id = match_loan_id id0 id1 in + let value = V.ALoan (V.AMutLoan (id, av)) in + { V.value; ty } + + let match_avalues v0 v1 = + log#ldebug + (lazy + ("avalues don't match:\n- v0: " + ^ typed_avalue_to_string S.ctx v0 + ^ "\n- v1: " + ^ typed_avalue_to_string S.ctx v1)); + raise (Distinct "match_avalues") +end + +let match_ctxs (check_equiv : bool) (fixed_ids : ids_sets) + (lookup_shared_value_in_ctx0 : V.BorrowId.id -> V.typed_value) + (lookup_shared_value_in_ctx1 : V.BorrowId.id -> V.typed_value) + (ctx0 : C.eval_ctx) (ctx1 : C.eval_ctx) : ids_maps option = + log#ldebug + (lazy + ("match_ctxs:\n\n- fixed_ids:\n" ^ show_ids_sets fixed_ids + ^ "\n\n- ctx0:\n" + ^ eval_ctx_to_string_no_filter ctx0 + ^ "\n\n- ctx1:\n" + ^ eval_ctx_to_string_no_filter ctx1 + ^ "\n\n")); + + (* Initialize the maps and instantiate the matcher *) + let module IdMap (Id : Identifiers.Id) = struct + let mk_map_ref (ids : Id.Set.t) : Id.InjSubst.t ref = + ref + (Id.InjSubst.of_list (List.map (fun x -> (x, x)) (Id.Set.elements ids))) + end in + let rid_map = + let module IdMap = IdMap (T.RegionId) in + IdMap.mk_map_ref fixed_ids.rids + in + let blid_map = + let module IdMap = IdMap (V.BorrowId) in + IdMap.mk_map_ref fixed_ids.blids + in + let borrow_id_map = + let module IdMap = IdMap (V.BorrowId) in + IdMap.mk_map_ref fixed_ids.borrow_ids + in + let loan_id_map = + let module IdMap = IdMap (V.BorrowId) in + IdMap.mk_map_ref fixed_ids.loan_ids + in + let aid_map = + let module IdMap = IdMap (V.AbstractionId) in + IdMap.mk_map_ref fixed_ids.aids + in + let sid_map = + let module IdMap = IdMap (V.SymbolicValueId) in + IdMap.mk_map_ref fixed_ids.sids + in + (* In case we don't try to check equivalence but want to compute a mapping + from a source context to a target context, we use a map from symbolic + value ids to values (rather than to ids). + *) + let sid_to_value_map : V.typed_value V.SymbolicValueId.Map.t ref = + ref V.SymbolicValueId.Map.empty + in + + let module S : MatchCheckEquivState = struct + let check_equiv = check_equiv + let ctx = ctx0 + let rid_map = rid_map + let blid_map = blid_map + let borrow_id_map = borrow_id_map + let loan_id_map = loan_id_map + let sid_map = sid_map + let sid_to_value_map = sid_to_value_map + let aid_map = aid_map + let lookup_shared_value_in_ctx0 = lookup_shared_value_in_ctx0 + let lookup_shared_value_in_ctx1 = lookup_shared_value_in_ctx1 + end in + let module CEM = MakeCheckEquivMatcher (S) in + let module M = MakeMatcher (CEM) in + (* Match the environments - we assume that they have the same structure + (and fail if they don't) *) + + (* Small utility: check that ids are fixed/mapped to themselves *) + let ids_are_fixed (ids : ids_sets) : bool = + let { aids; blids = _; borrow_ids; loan_ids; dids; rids; sids } = ids in + V.AbstractionId.Set.subset aids fixed_ids.aids + && V.BorrowId.Set.subset borrow_ids fixed_ids.borrow_ids + && V.BorrowId.Set.subset loan_ids fixed_ids.loan_ids + && C.DummyVarId.Set.subset dids fixed_ids.dids + && T.RegionId.Set.subset rids fixed_ids.rids + && V.SymbolicValueId.Set.subset sids fixed_ids.sids + in + + (* We need to pick a context for some functions like [match_typed_values]: + the context is only used to lookup module data, so we can pick whichever + we want. + TODO: this is not very clean. Maybe we should just carry the relevant data + (i.e.e, not the whole context) around. + *) + let ctx = ctx0 in + + (* Rem.: this function raises exceptions of type [Distinct] *) + let match_abstractions (abs0 : V.abs) (abs1 : V.abs) : unit = + let { + V.abs_id = abs_id0; + kind = kind0; + can_end = can_end0; + parents = parents0; + original_parents = original_parents0; + regions = regions0; + ancestors_regions = ancestors_regions0; + avalues = avalues0; + } = + abs0 + in + + let { + V.abs_id = abs_id1; + kind = kind1; + can_end = can_end1; + parents = parents1; + original_parents = original_parents1; + regions = regions1; + ancestors_regions = ancestors_regions1; + avalues = avalues1; + } = + abs1 + in + + let _ = CEM.match_aid abs_id0 abs_id1 in + if kind0 <> kind1 || can_end0 <> can_end1 then + raise (Distinct "match_abstractions: kind or can_end"); + let _ = CEM.match_aids parents0 parents1 in + let _ = CEM.match_aidl original_parents0 original_parents1 in + let _ = CEM.match_rids regions0 regions1 in + let _ = CEM.match_rids ancestors_regions0 ancestors_regions1 in + + log#ldebug (lazy "match_abstractions: matching values"); + let _ = + List.map + (fun (v0, v1) -> M.match_typed_avalues ctx v0 v1) + (List.combine avalues0 avalues1) + in + log#ldebug (lazy "match_abstractions: values matched OK"); + () + in + + (* Rem.: this function raises exceptions of type [Distinct] *) + let rec match_envs (env0 : C.env) (env1 : C.env) : unit = + log#ldebug + (lazy + ("match_ctxs: match_envs:\n\n- fixed_ids:\n" ^ show_ids_sets fixed_ids + ^ "\n\n- rid_map: " + ^ T.RegionId.InjSubst.show_t !rid_map + ^ "\n- blid_map: " + ^ V.BorrowId.InjSubst.show_t !blid_map + ^ "\n- sid_map: " + ^ V.SymbolicValueId.InjSubst.show_t !sid_map + ^ "\n- aid_map: " + ^ V.AbstractionId.InjSubst.show_t !aid_map + ^ "\n\n- ctx0:\n" + ^ eval_ctx_to_string_no_filter { ctx0 with env = List.rev env0 } + ^ "\n\n- ctx1:\n" + ^ eval_ctx_to_string_no_filter { ctx1 with env = List.rev env1 } + ^ "\n\n")); + + match (env0, env1) with + | ( C.Var (C.DummyBinder b0, v0) :: env0', + C.Var (C.DummyBinder b1, v1) :: env1' ) -> + (* Sanity check: if the dummy value is an old value, the bindings must + be the same and their values equal (and the borrows/loans/symbolic *) + if C.DummyVarId.Set.mem b0 fixed_ids.dids then ( + (* Fixed values: the values must be equal *) + assert (b0 = b1); + assert (v0 = v1); + (* The ids present in the left value must be fixed *) + let ids, _ = compute_typed_value_ids v0 in + assert ((not S.check_equiv) || ids_are_fixed ids)); + (* We still match the values - allows to compute mappings (which + are the identity actually) *) + let _ = M.match_typed_values ctx v0 v1 in + match_envs env0' env1' + | C.Var (C.VarBinder b0, v0) :: env0', C.Var (C.VarBinder b1, v1) :: env1' + -> + assert (b0 = b1); + (* Match the values *) + let _ = M.match_typed_values ctx v0 v1 in + (* Continue *) + match_envs env0' env1' + | C.Abs abs0 :: env0', C.Abs abs1 :: env1' -> + log#ldebug (lazy "match_ctxs: match_envs: matching abs"); + (* Same as for the dummy values: there are two cases *) + if V.AbstractionId.Set.mem abs0.abs_id fixed_ids.aids then ( + log#ldebug (lazy "match_ctxs: match_envs: matching abs: fixed abs"); + (* Still in the prefix: the abstractions must be the same *) + assert (abs0 = abs1); + (* Their ids must be fixed *) + let ids, _ = compute_abs_ids abs0 in + assert ((not S.check_equiv) || ids_are_fixed ids); + (* Continue *) + match_envs env0' env1') + else ( + log#ldebug + (lazy "match_ctxs: match_envs: matching abs: not fixed abs"); + (* Match the values *) + match_abstractions abs0 abs1; + (* Continue *) + match_envs env0' env1') + | [], [] -> + (* Done *) + () + | _ -> + (* The elements don't match *) + raise (Distinct "match_ctxs: match_envs: env elements don't match") + in + + (* Match the environments. + + Rem.: we don't match the ended regions (would it make any sense actually?) *) + try + (* Remove the frame delimiter (the first element of an environment is a frame delimiter) *) + let env0 = List.rev ctx0.env in + let env1 = List.rev ctx1.env in + let env0, env1 = + match (env0, env1) with + | C.Frame :: env0, C.Frame :: env1 -> (env0, env1) + | _ -> raise (Failure "Unreachable") + in + + match_envs env0 env1; + let maps = + { + aid_map = !aid_map; + blid_map = !blid_map; + borrow_id_map = !borrow_id_map; + loan_id_map = !loan_id_map; + rid_map = !rid_map; + sid_map = !sid_map; + sid_to_value_map = !sid_to_value_map; + } + in + Some maps + with Distinct msg -> + log#ldebug (lazy ("match_ctxs: distinct: " ^ msg)); + None + +let ctxs_are_equivalent (fixed_ids : ids_sets) (ctx0 : C.eval_ctx) + (ctx1 : C.eval_ctx) : bool = + let check_equivalent = true in + let lookup_shared_value _ = raise (Failure "Unreachable") in + Option.is_some + (match_ctxs check_equivalent fixed_ids lookup_shared_value + lookup_shared_value ctx0 ctx1) + +let match_ctx_with_target (config : C.config) (loop_id : V.LoopId.id) + (is_loop_entry : bool) (fp_bl_maps : borrow_loan_corresp) + (fp_input_svalues : V.SymbolicValueId.id list) (fixed_ids : ids_sets) + (src_ctx : C.eval_ctx) : st_cm_fun = + fun cf tgt_ctx -> + (* Debug *) + log#ldebug + (lazy + ("match_ctx_with_target:\n" ^ "\n- fixed_ids: " ^ show_ids_sets fixed_ids + ^ "\n" ^ "\n- src_ctx: " ^ eval_ctx_to_string src_ctx ^ "\n- tgt_ctx: " + ^ eval_ctx_to_string tgt_ctx)); + + (* We first reorganize [tgt_ctx] so that we can match [src_ctx] with it (by + ending loans for instance - remember that the [src_ctx] is the fixed point + context, which results from joins during which we ended the loans which + were introduced during the loop iterations) + *) + (* End the loans which lead to mismatches when joining *) + let rec cf_reorganize_join_tgt : cm_fun = + fun cf tgt_ctx -> + (* Collect fixed values in the source and target contexts: end the loans in the + source context which don't appear in the target context *) + let filt_src_env, _, _ = ctx_split_fixed_new fixed_ids src_ctx in + let filt_tgt_env, _, _ = ctx_split_fixed_new fixed_ids tgt_ctx in + + log#ldebug + (lazy + ("match_ctx_with_target:\n" ^ "\n- fixed_ids: " + ^ show_ids_sets fixed_ids ^ "\n" ^ "\n- filt_src_ctx: " + ^ env_to_string src_ctx filt_src_env + ^ "\n- filt_tgt_ctx: " + ^ env_to_string tgt_ctx filt_tgt_env)); + + (* Remove the abstractions *) + let filter (ee : C.env_elem) : bool = + match ee with Var _ -> true | Abs _ | Frame -> false + in + let filt_src_env = List.filter filter filt_src_env in + let filt_tgt_env = List.filter filter filt_tgt_env in + + (* Match the values to check if there are loans to eliminate *) + + (* We need to pick a context for some functions like [match_typed_values]: + the context is only used to lookup module data, so we can pick whichever + we want. + TODO: this is not very clean. Maybe we should just carry this data around. + *) + let ctx = tgt_ctx in + + let nabs = ref [] in + + let module S : MatchJoinState = struct + (* The context is only used to lookup module data: we can pick whichever we want *) + let ctx = ctx + let loop_id = loop_id + let nabs = nabs + end in + let module JM = MakeJoinMatcher (S) in + let module M = MakeMatcher (JM) in + try + let _ = + List.iter + (fun (var0, var1) -> + match (var0, var1) with + | C.Var (C.DummyBinder b0, v0), C.Var (C.DummyBinder b1, v1) -> + assert (b0 = b1); + let _ = M.match_typed_values ctx v0 v1 in + () + | C.Var (C.VarBinder b0, v0), C.Var (C.VarBinder b1, v1) -> + assert (b0 = b1); + let _ = M.match_typed_values ctx v0 v1 in + () + | _ -> raise (Failure "Unexpected")) + (List.combine filt_src_env filt_tgt_env) + in + (* No exception was thrown: continue *) + cf tgt_ctx + with ValueMatchFailure e -> + (* Exception: end the corresponding borrows, and continue *) + let cc = + match e with + | LoanInRight bid -> InterpreterBorrows.end_borrow config bid + | LoansInRight bids -> InterpreterBorrows.end_borrows config bids + | AbsInRight _ | AbsInLeft _ | LoanInLeft _ | LoansInLeft _ -> + raise (Failure "Unexpected") + in + comp cc cf_reorganize_join_tgt cf tgt_ctx + in + + (* Introduce the "identity" abstractions for the loop reentry. + + Match the target context with the source context so as to compute how to + map the borrows from the target context (i.e., the fixed point context) + to the borrows in the source context. + + Substitute the *loans* in the abstractions introduced by the target context + (the abstractions of the fixed point) to properly link those abstraction: + we introduce *identity* abstractions (the loans are equal to the borrows): + we substitute the loans and introduce fresh ids for the borrows, symbolic + values, etc. About the *identity abstractions*, see the comments for + [compute_fixed_point_id_correspondance]. + + TODO: this whole thing is very technical and error-prone. + We should rely on a more primitive and safer function + [add_identity_abs] to add the identity abstractions one by one. + *) + let cf_introduce_loop_fp_abs : m_fun = + fun tgt_ctx -> + (* Match the source and target contexts *) + let filt_tgt_env, _, _ = ctx_split_fixed_new fixed_ids tgt_ctx in + let filt_src_env, new_absl, new_dummyl = + ctx_split_fixed_new fixed_ids src_ctx + in + assert (new_dummyl = []); + let filt_tgt_ctx = { tgt_ctx with env = filt_tgt_env } in + let filt_src_ctx = { src_ctx with env = filt_src_env } in + + let src_to_tgt_maps = + let check_equiv = false in + let fixed_ids = ids_sets_empty_borrows_loans fixed_ids in + let open InterpreterBorrowsCore in + let lookup_shared_loan lid ctx : V.typed_value = + match snd (lookup_loan ek_all lid ctx) with + | Concrete (V.SharedLoan (_, v)) -> v + | Abstract (V.ASharedLoan (_, v, _)) -> v + | _ -> raise (Failure "Unreachable") + in + let lookup_in_src id = lookup_shared_loan id src_ctx in + let lookup_in_tgt id = lookup_shared_loan id tgt_ctx in + (* Match *) + Option.get + (match_ctxs check_equiv fixed_ids lookup_in_src lookup_in_tgt + filt_src_ctx filt_tgt_ctx) + in + let tgt_to_src_borrow_map = + V.BorrowId.Map.of_list + (List.map + (fun (x, y) -> (y, x)) + (V.BorrowId.InjSubst.bindings src_to_tgt_maps.borrow_id_map)) + in + + (* Debug *) + log#ldebug + (lazy + ("match_ctx_with_target: cf_introduce_loop_fp_abs:" ^ "\n\n- tgt_ctx: " + ^ eval_ctx_to_string tgt_ctx ^ "\n\n- src_ctx: " + ^ eval_ctx_to_string src_ctx ^ "\n\n- filt_tgt_ctx: " + ^ eval_ctx_to_string_no_filter filt_tgt_ctx + ^ "\n\n- filt_src_ctx: " + ^ eval_ctx_to_string_no_filter filt_src_ctx + ^ "\n\n- new_absl:\n" + ^ eval_ctx_to_string + { src_ctx with C.env = List.map (fun abs -> C.Abs abs) new_absl } + ^ "\n\n- fixed_ids:\n" ^ show_ids_sets fixed_ids ^ "\n\n- fp_bl_maps:\n" + ^ show_borrow_loan_corresp fp_bl_maps + ^ "\n\n- src_to_tgt_maps: " + ^ show_ids_maps src_to_tgt_maps)); + + (* Update the borrows and symbolic ids in the source context. + + Going back to the [list_nth_mut_example], the original environment upon + re-entering the loop is: + + {[ + abs@0 { ML l0 } + ls -> MB l5 (s@6 : loops::List<T>) + i -> s@7 : u32 + _@1 -> MB l0 (loops::List::Cons (ML l1, ML l2)) + _@2 -> MB l2 (@Box (ML l4)) // tail + _@3 -> MB l1 (s@3 : T) // hd + abs@1 { MB l4, ML l5 } + ]} + + The fixed-point environment is: + {[ + env_fp = { + abs@0 { ML l0 } + ls -> MB l1 (s3 : loops::List<T>) + i -> s4 : u32 + abs@fp { + MB l0 // this borrow appears in [env0] + ML l1 + } + } + ]} + + Through matching, we detect that in [env_fp], [l1] is matched + to [l5]. We introduce a fresh borrow [l6] for [l1], and remember + in the map [src_fresh_borrows_map] that: [{ l1 -> l6}]. + + We get: + {[ + abs@0 { ML l0 } + ls -> MB l6 (s@6 : loops::List<T>) // l6 is fresh and doesn't have a corresponding loan + i -> s@7 : u32 + _@1 -> MB l0 (loops::List::Cons (ML l1, ML l2)) + _@2 -> MB l2 (@Box (ML l4)) // tail + _@3 -> MB l1 (s@3 : T) // hd + abs@1 { MB l4, ML l5 } + ]} + + Later, we will introduce the identity abstraction: + {[ + abs@2 { MB l5, ML l6 } + ]} + *) + (* First, compute the set of borrows which appear in the fresh abstractions + of the fixed-point: we want to introduce fresh ids only for those. *) + let new_absl_ids, _ = compute_absl_ids new_absl in + let src_fresh_borrows_map = ref V.BorrowId.Map.empty in + let visit_tgt = + object + inherit [_] C.map_eval_ctx + + method! visit_borrow_id _ id = + (* Map the borrow, if it needs to be mapped *) + if + (* We map the borrows for which we computed a mapping *) + V.BorrowId.InjSubst.Set.mem id + (V.BorrowId.InjSubst.elements src_to_tgt_maps.borrow_id_map) + (* And which have corresponding loans in the fresh fixed-point abstractions *) + && V.BorrowId.Set.mem + (V.BorrowId.Map.find id tgt_to_src_borrow_map) + new_absl_ids.loan_ids + then ( + let src_id = V.BorrowId.Map.find id tgt_to_src_borrow_map in + let nid = C.fresh_borrow_id () in + src_fresh_borrows_map := + V.BorrowId.Map.add src_id nid !src_fresh_borrows_map; + nid) + else id + end + in + let tgt_ctx = visit_tgt#visit_eval_ctx () tgt_ctx in + + log#ldebug + (lazy + ("match_ctx_with_target: cf_introduce_loop_fp_abs: \ + src_fresh_borrows_map:\n" + ^ V.BorrowId.Map.show V.BorrowId.to_string !src_fresh_borrows_map + ^ "\n")); + + (* Rem.: we don't update the symbolic values. It is not necessary + because there shouldn't be any symbolic value containing borrows. + + Rem.: we will need to do something about the symbolic values in the + abstractions and in the *variable bindings* once we allow symbolic + values containing borrows to not be eagerly expanded. + *) + assert Config.greedy_expand_symbolics_with_borrows; + + (* Update the borrows and loans in the abstractions of the target context. + + Going back to the [list_nth_mut] example and by using [src_fresh_borrows_map], + we instantiate the fixed-point abstractions that we will insert into the + context. + The abstraction is [abs { MB l0, ML l1 }]. + Because of [src_fresh_borrows_map], we substitute [l1] with [l6]. + Because of the match between the contexts, we substitute [l0] with [l5]. + We get: + {[ + abs@2 { MB l5, ML l6 } + ]} + *) + let region_id_map = ref T.RegionId.Map.empty in + let get_rid rid = + match T.RegionId.Map.find_opt rid !region_id_map with + | Some rid -> rid + | None -> + let nid = C.fresh_region_id () in + region_id_map := T.RegionId.Map.add rid nid !region_id_map; + nid + in + let visit_src = + object + inherit [_] C.map_eval_ctx as super + + method! visit_borrow_id _ bid = + log#ldebug + (lazy + ("match_ctx_with_target: cf_introduce_loop_fp_abs: \ + visit_borrow_id: " ^ V.BorrowId.to_string bid ^ "\n")); + + (* Lookup the id of the loan corresponding to this borrow *) + let src_lid = + V.BorrowId.InjSubst.find bid fp_bl_maps.borrow_to_loan_id_map + in + + log#ldebug + (lazy + ("match_ctx_with_target: cf_introduce_loop_fp_abs: looked up \ + src_lid: " + ^ V.BorrowId.to_string src_lid + ^ "\n")); + + (* Lookup the tgt borrow id to which this borrow was mapped *) + let tgt_bid = + V.BorrowId.InjSubst.find src_lid src_to_tgt_maps.borrow_id_map + in + + log#ldebug + (lazy + ("match_ctx_with_target: cf_introduce_loop_fp_abs: looked up \ + tgt_bid: " + ^ V.BorrowId.to_string tgt_bid + ^ "\n")); + + tgt_bid + + method! visit_loan_id _ id = + log#ldebug + (lazy + ("match_ctx_with_target: cf_introduce_loop_fp_abs: \ + visit_loan_id: " ^ V.BorrowId.to_string id ^ "\n")); + (* Map the borrow - rem.: we mapped the borrows *in the values*, + meaning we know how to map the *corresponding loans in the + abstractions* *) + match V.BorrowId.Map.find_opt id !src_fresh_borrows_map with + | None -> + (* No mapping: this means that the borrow was mapped when + we matched values (it doesn't come from a fresh abstraction) + and because of this, it should actually be mapped to itself *) + assert ( + V.BorrowId.InjSubst.find id src_to_tgt_maps.borrow_id_map = id); + id + | Some id -> id + + method! visit_symbolic_value_id _ _ = C.fresh_symbolic_value_id () + method! visit_abstraction_id _ _ = C.fresh_abstraction_id () + method! visit_region_id _ id = get_rid id + + (** We also need to change the abstraction kind *) + method! visit_abs env abs = + match abs.kind with + | V.Loop (loop_id', rg_id, kind) -> + assert (loop_id' = loop_id); + assert (kind = V.LoopSynthInput); + let can_end = false in + let kind = V.Loop (loop_id, rg_id, V.LoopCall) in + let abs = { abs with kind; can_end } in + super#visit_abs env abs + | _ -> super#visit_abs env abs + end + in + let new_absl = List.map (visit_src#visit_abs ()) new_absl in + let new_absl = List.map (fun abs -> C.Abs abs) new_absl in + + (* Add the abstractions from the target context to the source context *) + let nenv = List.append new_absl tgt_ctx.env in + let tgt_ctx = { tgt_ctx with env = nenv } in + + log#ldebug + (lazy + ("match_ctx_with_target:cf_introduce_loop_fp_abs:\n- result ctx:\n" + ^ eval_ctx_to_string tgt_ctx)); + + (* Sanity check *) + if !Config.check_invariants then + Invariants.check_borrowed_values_invariant tgt_ctx; + + (* End all the borrows which appear in the *new* abstractions *) + let new_borrows = + V.BorrowId.Set.of_list + (List.map snd (V.BorrowId.Map.bindings !src_fresh_borrows_map)) + in + let cc = InterpreterBorrows.end_borrows config new_borrows in + + (* Compute the loop input values *) + let input_values = + V.SymbolicValueId.Map.of_list + (List.map + (fun sid -> + ( sid, + V.SymbolicValueId.Map.find sid src_to_tgt_maps.sid_to_value_map + )) + fp_input_svalues) + in + + (* Continue *) + cc + (cf + (if is_loop_entry then EndEnterLoop (loop_id, input_values) + else EndContinue (loop_id, input_values))) + tgt_ctx + in + + (* Compose and continue *) + cf_reorganize_join_tgt cf_introduce_loop_fp_abs tgt_ctx diff --git a/compiler/InterpreterLoopsMatchCtxs.mli b/compiler/InterpreterLoopsMatchCtxs.mli new file mode 100644 index 00000000..7e585dd6 --- /dev/null +++ b/compiler/InterpreterLoopsMatchCtxs.mli @@ -0,0 +1,301 @@ +(** This module implements support to match contexts for loops. + + The matching functions are used for instance to compute joins, or + to check if two contexts are equivalent (modulo conversion). + *) + +module T = Types +module PV = PrimitiveValues +module V = Values +module E = Expressions +module C = Contexts +module Subst = Substitute +module A = LlbcAst +module Inv = Invariants +module S = SynthesizeSymbolic +open Cps +open InterpreterUtils +open InterpreterLoopsCore + +(** Compute various maps linking the abstractions and the borrows/loans they contain. + + Parameters: + - [no_duplicates]: checks that borrows/loans are not referenced more than once + (see the documentation for {!type:InterpreterLoopsCore.abs_borrows_loans_maps}). + - [explore]: this function is used to filter abstractions. + - [env] + *) +val compute_abs_borrows_loans_maps : + bool -> (V.abs -> bool) -> C.env -> abs_borrows_loans_maps + +(** Generic functor to implement matching functions between values, environments, + etc. + + We use it for joins, to check if two environments are convertible, etc. + See for instance {!MakeJoinMatcher} and {!MakeCheckEquivMatcher}. + + The functor is parameterized by a {!InterpreterLoopsCore.PrimMatcher}, which implements the + non-generic part of the match. More precisely, the role of {!InterpreterLoopsCore.PrimMatcher} is two + provide generic functions which recursively match two values (by recursively + matching the fields of ADT values for instance). When it does need to match + values in a non-trivial manner (if two ADT values don't have the same + variant for instance) it calls the corresponding specialized function from + {!InterpreterLoopsCore.PrimMatcher}. + *) +module MakeMatcher : functor (PM : PrimMatcher) -> Matcher + +(** A matcher for joins (we use joins to compute loop fixed points). + + See the explanations for {!MakeMatcher}. + + In case of loop joins: + - we match *concrete* values + - we check that the "fixed" abstractions (the abstractions to be framed + - i.e., the abstractions which are the same in the two environments to + join) are equal + - we keep the abstractions which are not in the frame, then merge those + together (if they have borrows/loans in common) later + The join matcher is used to match the *concrete* values only. For this + reason, we fail on the functions which match avalues. + *) +module MakeJoinMatcher : functor (S : MatchJoinState) -> PrimMatcher + +(** An auxiliary matcher that we use for two purposes: + - to check if two contexts are equivalent modulo id substitution (i.e., + alpha equivalent) (see {!ctxs_are_equivalent}). + - to compute a mapping between the borrows and the symbolic values in a + target context to the values and borrows in a source context (see + {!match_ctx_with_target}). + *) +module MakeCheckEquivMatcher : functor (S : MatchCheckEquivState) -> + CheckEquivMatcher + +(** Compute whether two contexts are equivalent modulo an identifier substitution. + + [fixed_ids]: ids for which we force the mapping to be the identity. + + We also take advantage of the structure of the environments, which should + have the same prefixes (we check it). See the explanations for {!InterpreterLoopsJoinCtxs.join_ctxs}. + + TODO: explanations. + + The input parameters are: + - [check_equiv]: if [true], check if the two contexts are equivalent. + If [false], compute a mapping from the first context to the second context, + in the sense of [match_ctx_with_target]. + + - [fixed_ids] + + - [lookup_shared_value_in_ctx0], [lookup_shared_value_in_ctx1]: + The lookup functions are used to match the shared values when [check_equiv] + is [false] (we sometimes use [match_ctxs] on partial environments, meaning + we have to lookup the shared values in the complete environment, otherwise + we miss some mappings). + + - [ctx0] + - [ctx1] + + We return an optional ids map: [Some] if the match succeeded, [None] otherwise. + *) +val match_ctxs : + bool -> + ids_sets -> + (V.loan_id -> V.typed_value) -> + (V.loan_id -> V.typed_value) -> + C.eval_ctx -> + C.eval_ctx -> + ids_maps option + +(** Compute whether two contexts are equivalent modulo an identifier substitution. + + We also take advantage of the structure of the environments, which should + have the same prefixes (we check it). See the explanations for + {!InterpreterLoopsJoinCtxs.join_ctxs}. + + For instance, the following environments are equivalent: + {[ + env0 = { + abs@0 { ML l0 } + ls -> MB l1 (s2 : loops::List<T>) + i -> s1 : u32 + abs@1 { MB l0, ML l1 } + } + + env1 = { + abs@0 { ML l0 } + ls -> MB l2 (s4 : loops::List<T>) + i -> s3 : u32 + abs@2 { MB l0, ML l2 } + } + ]} + + We can go from [env0] to [env1] with the substitution: + {[ + abs_id_subst: { abs@1 -> abs@2 } + borrow_id_subst: { l1 -> l2 } + symbolic_id_subst: { s1 -> s3, s2 -> s4 } + ]} + + + Parameters: + - [fixed_ids]: ids for which we force the mapping to be the identity. + - [ctx0] + - [ctx1] + *) +val ctxs_are_equivalent : ids_sets -> C.eval_ctx -> C.eval_ctx -> bool + +(** Match a context with a target context. + + This is used to compute application of loop translations: we use this + to introduce "identity" abstractions upon (re-)entering the loop. + + For instance, the fixed point for [list_nth_mut] (see the top of the file) + is: + {[ + env_fp = { + abs@0 { ML l0 } + ls -> MB l1 (s@3 : loops::List<T>) + i -> s@4 : u32 + abs@fp { + MB l0 + ML l1 + } + } + ]} + + Upon re-entering the loop, starting from the fixed point, we get the + following environment: + {[ + env = { + abs@0 { ML l0 } + ls -> MB l5 (s@6 : loops::List<T>) + i -> s@7 : u32 + abs@1 { MB l0, ML l1 } + _@1 -> MB l1 (loops::List::Cons (ML l2, ML l3)) + _@2 -> MB l3 (@Box (ML l5)) // tail + _@3 -> MB l2 (s@3 : T) // hd + } + ]} + + We want to introduce an abstraction [abs@2], which has the same shape as [abs@fp] + above (the fixed-point abstraction), and which is actually the identity. If we do so, + we get an environment which is actually also a fixed point (we can collapse + the dummy variables and [abs@1] to actually retrieve the fixed point we + computed, and we use the fact that those values and abstractions can't be + *directly* manipulated unless we end this newly introduced [abs@2], which we + forbid). + + We match the *fixed point context* with the context upon entering the loop + by doing the following. + + 1. We filter [env_fp] and [env] to remove the newly introduced dummy variables + and abstractions. We get: + + {[ + filtered_env_fp = { + abs@0 { ML l0 } + ls -> MB l1 (s@3 : loops::List<T>) + i -> s@4 : u32 + // removed abs@fp + } + + filtered_env = { + abs@0 { ML l0 } + ls -> MB l5 (s@6 : loops::List<T>) + i -> s@7 : u32 + // removed abs@1, _@1, etc. + } + ]} + + 2. We match [filtered_env_fp] with [filtered_env] to compute a map from + the FP borrows/loans to the current borrows/loans (and also from symbolic values to + values). Note that we take care to *consider loans and borrows separately*, + and we ignore the "fixed" abstractions (which are unchanged - we checked that + when computing the fixed point). + We get: + {[ + borrows_map: { l1 -> l5 } // because we matched [MB l1 ...] with [MB l5 ...] + loans_map: {} // we ignore abs@0, which is "fixed" + ]} + + 3. We want to introduce an instance of [abs@fp] which is actually the + identity. From [compute_fixed_point_id_correspondance] and looking at + [abs@fp], we know we should link the instantiation of loan [l1] with the + instantiation of loan [l0]. We substitute [l0] with [l5] (following step 2.) + and introduce a fresh borrow [l6] for [l5] that we use to instantiate [l1]. + We get the following environment: + + {[ + env = { + abs@0 { ML l0 } + ls -> MB l6 (s@6 : loops::List<T>) + i -> s@7 : u32 + abs@1 { MB l0, ML l1 } + _@1 -> MB l1 (loops::List::Cons (ML l2, ML l3)) + _@2 -> MB l3 (@Box (ML l5)) // tail + _@3 -> MB l2 (s@3 : T) // hd + abs@2 { MB l5, ML l6 } // this is actually the identity: l6 = l5 + } + ]} + + 4. As we now have a fixed point (see above comments), we can consider than + [abs@2] links the current iteration to the last one before we exit. What we + are interested in is that: + - upon inserting [abs@2] we re-entered the loop, meaning in the translation + we need to insert a recursive call to the loop forward function + - upon ending [abs@2] we need to insert a call to the loop backward function + + Because we want to ignore them, we end the loans in the newly introduced + [abs@2] abstraction (i.e., [l6]). We get: + {[ + env = { + abs@0 { ML l0 } + ls -> ⊥ + i -> s@7 : u32 + abs@1 { MB l0, ML l1 } + _@1 -> MB l1 (loops::List::Cons (ML l2, ML l3)) + _@2 -> MB l3 (@Box (ML l5)) // tail + _@3 -> MB l2 (s@3 : T) // hd + abs@2 { MB l5 } + } + ]} + + TODO: we shouldn't need to end the loans, we should actually remove them + before inserting the new abstractions (we may have issues with the symbolic + values, if they contain borrows - above i points to [s@7], but it should + be a different symbolic value...). + + Finally, we use the map from symbolic values to values to compute the list of + input values of the loop: we simply list the values, by order of increasing + symbolic value id. We *do* use the fixed values (though they are in the frame) + because they may be *read* inside the loop. + + We can then proceed to finishing the symbolic execution and doing the + synthesis. + + Rem.: we might reorganize the [tgt_ctx] by ending loans for instance. + + **Parameters**: + - [config] + - [loop_id] + - [is_loop_entry]: [true] if first entry into the loop, [false] if re-entry + (i.e., continue). + - [fp_bl_maps]: for the abstractions in the fixed-point (the source context), + the maps from loans to borrows and borrows to loans, if those abstractions + are seen as identity abstractions (for every of those abstractions, there + must be a bijection between the borrows and the loans) + - [fp_input_svalues]: the list of symbolic values appearing in the fixed + point (the source context) and which must be instantiated during the match + (this is the list of input parameters of the loop). + - [fixed_ids] + - [src_ctx] + *) +val match_ctx_with_target : + C.config -> + V.loop_id -> + bool -> + borrow_loan_corresp -> + V.symbolic_value_id list -> + ids_sets -> + C.eval_ctx -> + st_cm_fun diff --git a/compiler/Logging.ml b/compiler/Logging.ml index 706ea5cb..9dc1f5e3 100644 --- a/compiler/Logging.ml +++ b/compiler/Logging.ml @@ -24,6 +24,15 @@ let pure_to_extract_log = L.get_logger "MainLogger.ExtractBase" (** Logger for Interpreter *) let interpreter_log = L.get_logger "MainLogger.Interpreter" +(** Logger for InterpreterLoopsMatchCtxs *) +let loops_match_ctxs_log = L.get_logger "MainLogger.Interpreter.LoopsMatchCtxs" + +(** Logger for InterpreterLoopsJoinCtxs *) +let loops_join_ctxs_log = L.get_logger "MainLogger.Interpreter.LoopsJoinCtxs" + +(** Logger for InterpreterLoopsFixedPoint *) +let loops_fixed_point_log = L.get_logger "MainLogger.Interpreter.FixedPoint" + (** Logger for InterpreterLoops *) let loops_log = L.get_logger "MainLogger.Interpreter.Loops" diff --git a/compiler/dune b/compiler/dune index 0d899ecf..ae9cef04 100644 --- a/compiler/dune +++ b/compiler/dune @@ -29,6 +29,10 @@ InterpreterExpansion InterpreterExpressions Interpreter + InterpreterLoopsCore + InterpreterLoopsMatchCtxs + InterpreterLoopsJoinCtxs + InterpreterLoopsFixedPoint InterpreterLoops InterpreterPaths InterpreterProjectors |