%-----------------------------------------------------------------------------% % vim: ft=mercury ts=4 sw=4 et %-----------------------------------------------------------------------------% % Copyright (C) 2017 The Mercury team. % This file may only be copied under the terms of the GNU General % Public License - see the file COPYING in the Mercury distribution. %-----------------------------------------------------------------------------% % % File: pre_quantification.m. % Main author: zs. % % This module adds implicit quantifications of some variables just inside % trace goal scopes in order to avoid a minor but annoying problem, which can % also be confusing for people who come across it the first time. % % Consider the following code: % % :- pred p(int::in, int::in, int::out) is det. % % p(A, B, X) :- % trace [io(!IO)] ( % ToPrint = A, % io.format("A = %d\n", [i(ToPrint)], !IO) % ), % trace [io(!IO)] ( % ToPrint = B, % io.format("B = %d\n", [i(ToPrint)], !IO) % ), % X = A + B, % trace [io(!IO)] ( % ToPrint = X, % io.format("X = %d\n", [i(ToPrint)], !IO) % ). % % Since the variable ToPrint is used inside more than one trace goal, % the compiler's usual scope rules consider it to be a nonlocal variable % in each trace goal. This is a problem, because the compiler does not allow % trace goals to bind variables that are not local to the trace goal. % It does this to preserve the ability of the program to work whether or not % the trace goal is there, since it can be deleted if either its compile_time % or run_time condition turns out to be false. % % However, in cases like this, where the nonlocal variable occurs *only* % inside trace goals, leaving ToPrint unbound if a trace goal is deleted % will not affect the execution of the program. Having it bound more than once % if the trace goals *aren't* deleted would be a problem, but this is easily % fixed by making ToPrint existentially quantified inside each trace goal; % that way, each trace goal will bind and use its own copy of ToPrint. % % The job of this module is to find out which variables in a clause body % are used only inside trace goals, and to insert the quantifications % into trace goals as necessary to ensure that all such variables % will end up being local to each trace goal scope. The part of the compiler % that ensures this is the quantification pass, which is invoked on the % goal we generate pretty much immediately after we return it. % % The overall effect of putting this pass before the quantification pass % is a minor tweak of the usual rules of quantification, a tweak that % makes the rules enforced by the compiler resemble the rules expected % by programmers more closely, helping the compiler approach closer to the % principle of least astonishment. % %-----------------------------------------------------------------------------% :- module hlds.pre_quantification. :- interface. :- import_module hlds.hlds_goal. %-----------------------------------------------------------------------------% :- pred separate_trace_goal_only_locals(hlds_goal::in, hlds_goal::out) is det. %-----------------------------------------------------------------------------% %-----------------------------------------------------------------------------% :- implementation. :- import_module parse_tree. :- import_module parse_tree.prog_data. :- import_module counter. :- import_module int. :- import_module list. :- import_module map. :- import_module maybe. :- import_module sparse_bitset. %-----------------------------------------------------------------------------% % % The pre-quantification algorithm is based on dividing the clause body % into one or more zones, each identified by a non-negative integer. % The zones are based on viewing the clause body as a tree: % % - with each goal being a node, % - atomic goals being leaf nodes, and % - each compound goal having a subtree for each subgoal. % % If lambda goals didn't exist, each zone would be identified by its top node. % % The top node of zone 0 is the root of the tree, while the top node of % every other zone is a trace goal, i.e. a scope goal whose scope_reason % is trace_goal. Every trace goal is the top node of a zone, with the % exception of trace goals that are contained inside other trace goals. % (We have no rule against nesting trace goals, though they don't make % much sense, and I (zs) have never seen such nesting.) % % The zone that each node belongs to is the zone whose top node you first find % when you traverse the tree upwards from the node. In other words, each zone % claims the nodes beneath it, except the ones that are claimed by another % zone whose top node is lower down. % % Since lambda goals do exist, we have to handle them. We do so by considering % the lambda goal to be effectively a separate predicate body inside this % clause (since the compiler will eventually turn it into that). So the % top level of a rhs_lambda_goal is also considered to be in zone 0. :- type zone == int. % We could make a zone a uint, but there are issues around the uint type's % membership of the enum typeclass, which is needed for the use of % sparse_bitset, as discussed on m-rev on 2017 july 3. :- func top_zone = zone. :- pragma inline(top_zone/0). top_zone = 0. %-----------------------------------------------------------------------------% % % The pre-quantification algorithm has three main steps. % % - The first step is to find out which variables are used in which zones. % This is done by build_vars_to_zones_in_*, which build the VarsToZones map. % % - The second step is finding out which variables occur only inside % trace goals, not outside them. These variables should be local to the % trace goals they appear in. If they occur in only one trace goal, % ordinary quantification will ensure this. We need to add explicit % existential quantifications for them only if they occur inside % more than one trace goal. In terms of zones, this means variables % that don't occur in zone 0 but do appear in more than one other zone. % (We call these variables the "duplicated" variables.) The job of % build_zones_to_dup_vars is to map each trace goal zone to the list % of variables that needs to have an existential quantifier added for it % in the scope of that trace goal, and to return that as ZonesToDupVars. % % - The third step is to go through the clause body and insert those % existential quantification scopes inside every trace goal scope % that contains one or more of these "duplicated" variables. % % Our parent ensures that we don't get called on clauses that % don't contain any trace goals. Most clauses that have trace goals % will have only a few of them, so the sparse_bitset(zone) will % typically contain only a single node. % :- type vars_to_zones == map(prog_var, sparse_bitset(zone)). :- type zones_to_dup_vars == map(zone, list(prog_var)). separate_trace_goal_only_locals(Goal0, Goal) :- map.init(VarsToZones0), counter.init(1, TraceCounter0), build_vars_to_zones_in_goal(top_zone, Goal0, TraceCounter0, _, VarsToZones0, VarsToZones), map.init(ZonesToDupVars0), map.foldl(build_zones_to_dup_vars, VarsToZones, ZonesToDupVars0, ZonesToDupVars), ( if map.is_empty(ZonesToDupVars) then Goal = Goal0 else add_exist_scopes_for_dup_vars_in_goal(top_zone, ZonesToDupVars, Goal0, Goal, TraceCounter0, _) ). %-----------------------------------------------------------------------------% % The first step described in the comment above % separate_trace_goal_only_locals. % :- pred build_vars_to_zones_in_goal(zone::in, hlds_goal::in, counter::in, counter::out, vars_to_zones::in, vars_to_zones::out) is det. build_vars_to_zones_in_goal(CurZone, Goal, !TraceCounter, !VarsToZones) :- Goal = hlds_goal(GoalExpr, _GoalInfo), ( GoalExpr = plain_call(_PredId, _ProcId, ArgVars, _Builtin, _MaybeUnifyContext, _SymName), record_vars_in_zone(CurZone, ArgVars, !VarsToZones) ; GoalExpr = generic_call(GenericCall, ArgVars, _Modes, _RegTypes, _Detism), record_vars_in_zone(CurZone, ArgVars, !VarsToZones), ( GenericCall = higher_order(ClosureVar, _Purity, _CallKind, _Arity), record_var_in_zone(CurZone, ClosureVar, !VarsToZones) ; GenericCall = class_method(TypeClassInfoVar, _MethodNum, _ClassId, _SimpleCallId), record_var_in_zone(CurZone, TypeClassInfoVar, !VarsToZones) ; GenericCall = event_call(_) ; GenericCall = cast(_) ) ; GoalExpr = call_foreign_proc(_Attrs, _PredId, _ProcId, ForeignArgs, ExtraArgs, _TraceCond, _Impl), record_vars_in_zone(CurZone, list.map(foreign_arg_var, ForeignArgs), !VarsToZones), record_vars_in_zone(CurZone, list.map(foreign_arg_var, ExtraArgs), !VarsToZones) ; GoalExpr = unify(LHSVar, RHS, _Mode, _Kind, _Context), record_var_in_zone(CurZone, LHSVar, !VarsToZones), ( RHS = rhs_var(RHSVar), record_var_in_zone(CurZone, RHSVar, !VarsToZones) ; RHS = rhs_functor(_ConsId, _IsExistConstr, RHSArgVars), record_vars_in_zone(CurZone, RHSArgVars, !VarsToZones) ; RHS = rhs_lambda_goal(_Purity, _Groundness, _PredOrFunc, _EvalMethod, _NonLocals, LambdaVars, _LambdaModes, _Detism, LambdaGoal), % The lambda goal is the one clause of the procedure that % we will construct from LambdaGoal. We therefore treat it % as we treat the top level goal. LambdaZone = top_zone, record_vars_in_zone(LambdaZone, LambdaVars, !VarsToZones), build_vars_to_zones_in_goal(LambdaZone, LambdaGoal, !TraceCounter, !VarsToZones) ) ; ( GoalExpr = conj(_ConjType, SubGoals) ; GoalExpr = disj(SubGoals) ), list.foldl2(build_vars_to_zones_in_goal(CurZone), SubGoals, !TraceCounter, !VarsToZones) ; GoalExpr = switch(Var, _CanFail, Cases), record_var_in_zone(CurZone, Var, !VarsToZones), list.foldl2(build_vars_to_zones_in_case(CurZone), Cases, !TraceCounter, !VarsToZones) ; GoalExpr = negation(SubGoal), build_vars_to_zones_in_goal(CurZone, SubGoal, !TraceCounter, !VarsToZones) ; GoalExpr = scope(Reason, SubGoal), ( if Reason = trace_goal(_, _, _, _, _), CurZone = top_zone then counter.allocate(NewZone, !TraceCounter), build_vars_to_zones_in_goal(NewZone, SubGoal, !TraceCounter, !VarsToZones) else build_vars_to_zones_in_goal(CurZone, SubGoal, !TraceCounter, !VarsToZones) ) ; GoalExpr = if_then_else(Vars, Cond, Then, Else), record_vars_in_zone(CurZone, Vars, !VarsToZones), build_vars_to_zones_in_goal(CurZone, Cond, !TraceCounter, !VarsToZones), build_vars_to_zones_in_goal(CurZone, Then, !TraceCounter, !VarsToZones), build_vars_to_zones_in_goal(CurZone, Else, !TraceCounter, !VarsToZones) ; GoalExpr = shorthand(ShortHand), ( ShortHand = bi_implication(SubGoalA, SubGoalB), build_vars_to_zones_in_goal(CurZone, SubGoalA, !TraceCounter, !VarsToZones), build_vars_to_zones_in_goal(CurZone, SubGoalB, !TraceCounter, !VarsToZones) ; ShortHand = atomic_goal(_GoalType, OuterVars, InnerVars, MaybeOutputVars, MainGoal, OrElseGoals, _OrElseInners), OuterVars = atomic_interface_vars(OVA, OVB), InnerVars = atomic_interface_vars(IVA, IVB), record_vars_in_zone(CurZone, [OVA, OVB, IVA, IVB], !VarsToZones), ( MaybeOutputVars = no ; MaybeOutputVars = yes(OutputVars), record_vars_in_zone(CurZone, OutputVars, !VarsToZones) ), build_vars_to_zones_in_goal(CurZone, MainGoal, !TraceCounter, !VarsToZones), list.foldl2(build_vars_to_zones_in_goal(CurZone), OrElseGoals, !TraceCounter, !VarsToZones) ; ShortHand = try_goal(MaybeIOStateVars, ResultVar, SubGoal), ( MaybeIOStateVars = no ; MaybeIOStateVars = yes(IOStateVars), IOStateVars = try_io_state_vars(ISVA, ISVB), record_vars_in_zone(CurZone, [ISVA, ISVB], !VarsToZones) ), record_var_in_zone(CurZone, ResultVar, !VarsToZones), build_vars_to_zones_in_goal(CurZone, SubGoal, !TraceCounter, !VarsToZones) ) ). :- pred build_vars_to_zones_in_case(zone::in, case::in, counter::in, counter::out, vars_to_zones::in, vars_to_zones::out) is det. build_vars_to_zones_in_case(CurZone, Case, !TraceCounter, !VarsToZones) :- Case = case(_MainConsId, _OtherConsIds, Goal), build_vars_to_zones_in_goal(CurZone, Goal, !TraceCounter, !VarsToZones). :- pred record_vars_in_zone(zone::in, list(prog_var)::in, vars_to_zones::in, vars_to_zones::out) is det. record_vars_in_zone(_Zone, [], !VarsToZones). record_vars_in_zone(Zone, [Var | Vars], !VarsToZones) :- record_var_in_zone(Zone, Var, !VarsToZones), record_vars_in_zone(Zone, Vars, !VarsToZones). :- pred record_var_in_zone(zone::in, prog_var::in, vars_to_zones::in, vars_to_zones::out) is det. record_var_in_zone(Zone, Var, !VarsToZones) :- ( if map.search(!.VarsToZones, Var, Zones0) then % Most variables will occur only in one zone, typically zone 0. % However, they will typically occur there more than once. % We don't want to set the bit of the zone more than once. ( if sparse_bitset.contains(Zones0, Zone) then true else sparse_bitset.insert(Zone, Zones0, Zones), map.det_update(Var, Zones, !VarsToZones) ) else Zones = sparse_bitset.make_singleton_set(Zone), map.det_insert(Var, Zones, !VarsToZones) ). %-----------------------------------------------------------------------------% % The second step described in the comment above % separate_trace_goal_only_locals. % :- pred build_zones_to_dup_vars(prog_var::in, sparse_bitset(zone)::in, zones_to_dup_vars::in, zones_to_dup_vars::out) is det. build_zones_to_dup_vars(Var, Zones, !ZonesToDupVars) :- ZoneList = sparse_bitset.to_sorted_list(Zones), ( if ZoneList = [FirstZone, _SecondZone | _], FirstZone \= top_zone then list.foldl(add_var_to_zone(Var), ZoneList, !ZonesToDupVars) else true ). :- pred add_var_to_zone(prog_var::in, zone::in, zones_to_dup_vars::in, zones_to_dup_vars::out) is det. add_var_to_zone(Var, Zone, !ZonesToDupVars) :- ( if map.search(!.ZonesToDupVars, Zone, DupVars0) then DupVars = [Var | DupVars0], map.det_update(Zone, DupVars, !ZonesToDupVars) else map.det_insert(Zone, [Var], !ZonesToDupVars) ). %-----------------------------------------------------------------------------% % The third step described in the comment above % separate_trace_goal_only_locals. % :- pred add_exist_scopes_for_dup_vars_in_goal(zone::in, zones_to_dup_vars::in, hlds_goal::in, hlds_goal::out, counter::in, counter::out) is det. add_exist_scopes_for_dup_vars_in_goal(CurZone, ZonesToDupVars, !Goal, !TraceCounter) :- !.Goal = hlds_goal(GoalExpr0, GoalInfo), ( ( GoalExpr0 = plain_call(_, _, _, _, _, _) ; GoalExpr0 = generic_call(_, _, _, _, _) ; GoalExpr0 = call_foreign_proc(_, _, _, _, _, _, _) ), GoalExpr = GoalExpr0 ; GoalExpr0 = unify(LHSVar, RHS0, Mode, Kind, Context), ( ( RHS0 = rhs_var(_) ; RHS0 = rhs_functor(_, _, _) ), GoalExpr = GoalExpr0 ; RHS0 = rhs_lambda_goal(Purity, Groundness, PredOrFunc, EvalMethod, NonLocals, LambdaVars, LambdaModes, Detism, LambdaGoal0), LambdaZone = top_zone, add_exist_scopes_for_dup_vars_in_goal(LambdaZone, ZonesToDupVars, LambdaGoal0, LambdaGoal, !TraceCounter), RHS = rhs_lambda_goal(Purity, Groundness, PredOrFunc, EvalMethod, NonLocals, LambdaVars, LambdaModes, Detism, LambdaGoal), GoalExpr = unify(LHSVar, RHS, Mode, Kind, Context) ) ; GoalExpr0 = conj(ConjType, SubGoals0), list.map_foldl( add_exist_scopes_for_dup_vars_in_goal(CurZone, ZonesToDupVars), SubGoals0, SubGoals, !TraceCounter), GoalExpr = conj(ConjType, SubGoals) ; GoalExpr0 = disj(SubGoals0), list.map_foldl( add_exist_scopes_for_dup_vars_in_goal(CurZone, ZonesToDupVars), SubGoals0, SubGoals, !TraceCounter), GoalExpr = disj(SubGoals) ; GoalExpr0 = switch(Var, CanFail, Cases0), list.map_foldl( add_exist_scopes_for_dup_vars_in_case(CurZone, ZonesToDupVars), Cases0, Cases, !TraceCounter), GoalExpr = switch(Var, CanFail, Cases) ; GoalExpr0 = negation(SubGoal0), add_exist_scopes_for_dup_vars_in_goal(CurZone, ZonesToDupVars, SubGoal0, SubGoal, !TraceCounter), GoalExpr = negation(SubGoal) ; GoalExpr0 = scope(Reason, SubGoal0), add_exist_scopes_for_dup_vars_in_goal(CurZone, ZonesToDupVars, SubGoal0, SubGoal1, !TraceCounter), ( if Reason = trace_goal(_, _, _, _, _), CurZone = top_zone then counter.allocate(NewZone, !TraceCounter), ( if map.search(ZonesToDupVars, NewZone, DupVars) then list.sort(DupVars, SortedDupVars), QuantReason = exist_quant(SortedDupVars), QuantExpr = scope(QuantReason, SubGoal1), SubGoal1 = hlds_goal(_, SubGoalInfo1), SubGoal = hlds_goal(QuantExpr, SubGoalInfo1) else SubGoal = SubGoal1 ) else SubGoal = SubGoal1 ), GoalExpr = scope(Reason, SubGoal) ; GoalExpr0 = if_then_else(Vars, Cond0, Then0, Else0), add_exist_scopes_for_dup_vars_in_goal(CurZone, ZonesToDupVars, Cond0, Cond, !TraceCounter), add_exist_scopes_for_dup_vars_in_goal(CurZone, ZonesToDupVars, Then0, Then, !TraceCounter), add_exist_scopes_for_dup_vars_in_goal(CurZone, ZonesToDupVars, Else0, Else, !TraceCounter), GoalExpr = if_then_else(Vars, Cond, Then, Else) ; GoalExpr0 = shorthand(ShortHand0), ( ShortHand0 = bi_implication(SubGoalA0, SubGoalB0), add_exist_scopes_for_dup_vars_in_goal(CurZone, ZonesToDupVars, SubGoalA0, SubGoalA, !TraceCounter), add_exist_scopes_for_dup_vars_in_goal(CurZone, ZonesToDupVars, SubGoalB0, SubGoalB, !TraceCounter), ShortHand = bi_implication(SubGoalA, SubGoalB) ; ShortHand0 = atomic_goal(GoalType, OuterVars, InnerVars, MaybeOutputVars, MainGoal0, OrElseGoals0, OrElseInners), add_exist_scopes_for_dup_vars_in_goal(CurZone, ZonesToDupVars, MainGoal0, MainGoal, !TraceCounter), list.map_foldl( add_exist_scopes_for_dup_vars_in_goal(CurZone, ZonesToDupVars), OrElseGoals0, OrElseGoals, !TraceCounter), ShortHand = atomic_goal(GoalType, OuterVars, InnerVars, MaybeOutputVars, MainGoal, OrElseGoals, OrElseInners) ; ShortHand0 = try_goal(MaybeIOStateVars, ResultVar, SubGoal0), add_exist_scopes_for_dup_vars_in_goal(CurZone, ZonesToDupVars, SubGoal0, SubGoal, !TraceCounter), ShortHand = try_goal(MaybeIOStateVars, ResultVar, SubGoal) ), GoalExpr = shorthand(ShortHand) ), !:Goal = hlds_goal(GoalExpr, GoalInfo). :- pred add_exist_scopes_for_dup_vars_in_case(zone::in, zones_to_dup_vars::in, case::in, case::out, counter::in, counter::out) is det. add_exist_scopes_for_dup_vars_in_case(CurZone, ZonesToDupVars, !Case, !TraceCounter) :- !.Case = case(MainConsId, OtherConsIds, Goal0), add_exist_scopes_for_dup_vars_in_goal(CurZone, ZonesToDupVars, Goal0, Goal, !TraceCounter), !:Case = case(MainConsId, OtherConsIds, Goal). %-----------------------------------------------------------------------------% :- end_module hlds.pre_quantification. %-----------------------------------------------------------------------------%