%-----------------------------------------------------------------------------%
% vim: ft=mercury ts=4 sw=4 et
%-----------------------------------------------------------------------------%
% Copyright (C) 2002, 2005-2011 The University of Melbourne.
% Copyright (C) 2015-2024 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: term_constr_data.m.
% Main author: juliensf.
%
% This module defines data structures that are common to all modules in the
% second termination analyser.
%
% The main data structure defined here is the abstract representation (AR),
% which is an abstraction of a Mercury program in terms of linear arithmetic
% constraints on term sizes.
%
%-----------------------------------------------------------------------------%
%
% AR Goals.
%
% The AR has four kinds of goal:
%
% * primitives      - A set of primitive constraints representing the
%                     abstraction variable size relationships in some
%                     HLDS goal.
%
% * conjunction     - A conjunction of AR goals.
%
% * disjunction     - A disjunction of AR goals.
%
% * calls           - An abstraction of intra-SCC calls. Calls to procedures
%                     lower down the call-graph are abstracted as primitive
%                     AR goals.
%
% XXX In order to handle higher-order, we need to either modify the
% exiting AR call goal or add a new AR goal type.
%
%-----------------------------------------------------------------------------%
%
% Mapping the HLDS to the AR.
%
% 1. unification
%
% A HLDS unification of the form:
%
%       X = f(A, B, C)
%
% is converted to a AR primitive goal of the form:
%
%       { |X| = |A| + |B| + |C| + |f| }
%
% where |X| represents the size of the variable X (according to whatever
% measure we are using). There will also additional non-negativity
% constraints on any variables that have non-zero size type. Variables
% of that have zero size type are not included at all. Variables that
% represent polymorphic types are included. The code in term_constr_fixpoint.m
% and term_constr_pass2.m that processes calls is responsible for dealing with
% the situation where a polymorphic procedure is called with zero sized
% arguments.
%
% 2. conjunction and parallel conjunction
%
% A HLDS conjunction (A, B) is converted to an AR conjunction. Parallel
% conjunction is treated the same way.
%
% 3. disjunction and switches.
%
% A HLDS disjunction (A ; B) is converted to an AR disjunction. Switches
% are similar although we also have to add any constraints on the variable
% being switched on.
%
% 4. calls
%
% A HLDS call to a procedure lower down the call graph is abstracted as
% an AR primitive. A call to something in the same SCC becomes an AR call.
%
% 5. negation.
%
% A HLDS negation is abstracted as an AR primitive.
% The analyser tries to infer bounds upon the sizes of any input variables
% of the negated goal when it fails.
%
% 6. scopes
%
% Scope reasons, such as existential quantifications, are ignored unless
% they affect term size.
%
% 8. if-then-else.
%
% ( if Cond then Then else Else ) is abstracted as
%
%  disj(conj(|Cond|, |Then|), conj(neg(|Cond|), |Else|))
%
% (using |Goal| to represent the abstraction of Goal).
%
% 9. foreign_procs
%
% Currently these map onto a primitive whose variables are unconstrained.
% XXX Could do better with user supplied information.
%
% 10. generic call.
%
% XXX As above, need HO analysis to make these work.
%
%-----------------------------------------------------------------------------%

:- module transform_hlds.term_constr_data.

:- interface.

:- import_module hlds.
:- import_module hlds.hlds_module.
:- import_module hlds.hlds_pred.
:- import_module libs.
:- import_module libs.lp_rational.
:- import_module libs.polyhedron.
:- import_module parse_tree.
:- import_module parse_tree.prog_data.
:- import_module transform_hlds.term_constr_errors.

:- import_module bool.
:- import_module io.
:- import_module list.
:- import_module map.
:- import_module set.   % XXX We should experiment with different set
                        % implementations.

%-----------------------------------------------------------------------------%
%
% Types that are common to all parts of the termination analyser.
%

    % A size_var is a variable that represents the size (according
    % to some measure) of a program variable.
    %
:- type size_var    == lp_var.
:- type size_varset == lp_varset.

:- type size_term  == lp_term.

    % A map between prog_vars and their corresponding size_vars.
    %
:- type size_var_map == map(prog_var, size_var).

    % The widening strategy used in the fixpoint calculation.
    % (At present there is only one but we may add others in the future).
    %
:- type widening
    --->    after_fixed_cutoff(int).

    % The result of the argument size analysis.
    %
    % NOTE: this is just an indication that everything worked,
    % any argument size constraint derived will be stored in the
    % termination2_info structure.
    %
:- type arg_size_result
    --->    arg_size_ok
    ;       arg_size_error(list(term2_error)).

%-----------------------------------------------------------------------------%
%
% The abstract representation.
%

% XXX There should really be a representation for abstract SCCs as
% some of the data in the abstract_proc structure is actually information
% about the SCC; currently the relevant information is just duplicated
% amongst the abstract procs.

:- type abstract_scc == set(abstract_proc).

    % XXX This will need to be extended in order to handle HO calls and
    % intermodule mutual recursion.
    %
    % The idea here is that information about procedures from other
    % modules/HO information will be turned into `fake' abstract procs.
    % Using these fake procs we will then fill in the missing bits of
    % the SCCs that involve intermodule mutual recursion/HO calls, and
    % then run the analysis on them.
    %
    % This is the main reason that we try a eliminate, as much as
    % possible, dependencies between the AR and the HLDS.
    %
:- type abstract_ppid
    --->    real(pred_proc_id).

:- type abstract_proc
    --->    abstract_proc(
                % The procedure that this is an abstraction of.
                ap_ppid             :: abstract_ppid,

                % The context of the procedure.
                ap_context          :: prog_context,

                % The procedure's arguments (as size_vars).
                ap_head_vars        :: head_vars,

                % `yes' if the corresponding argument can be used
                % as part of a termination proof, `no' otherwise.
                ap_inputs           :: list(bool),

                % An abstraction of the body of the procedure.
                ap_body             :: abstract_goal,

                % Map from prog_vars to size_vars for the procedure.
                ap_size_var_map     :: size_var_map,

                % The varset from which the size_vars were allocated.
                % The linear solver needs this.
                ap_size_varset      :: size_varset,

                % The size_vars that have zero size.
                ap_zeros            :: zero_vars,

                % Is this procedure called from outside the SCC?
                ap_is_entry         :: bool,

                % The type of recursion present in the procedure.
                ap_recursion        :: recursion_type,

                % The number of calls made in the body of the procedure.
                % This is useful for short-circuiting pass 2.
                ap_num_calls        :: int,

                % A list of higher-order calls made by the procedure.
                % XXX Currently not used.
                ap_ho_calls         :: list(abstract_ho_call)
            ).

    % This is like an error message (and is treated as such at the moment).
    % It is here because we want to treat information regarding higher-order
    % constructs differently from other errors. In particular, higher-order
    % constructs will not always be errors (i.e. when we can analyse
    % them properly).
    %
:- type abstract_ho_call
    --->    ho_call(prog_context).

    % NOTE: the AR's notion of local/non-local variables may not correspond
    % directly to that in the HLDS because of various transformations
    % performed on the AR.
    %
:- type local_vars ==    list(size_var).
:- type nonlocal_vars == list(size_var).

:- type call_vars == list(size_var).
:- type head_vars == list(size_var).

    % `zero_vars' are those variables in a procedure that have
    % zero size type (as defined in term_norm.m).
    %
:- type zero_vars == set(size_var).

    % This is the representation of goals that the termination analyser
    % works with.
    %
:- type abstract_goal
    --->    term_disj(
                disj_goals     :: list(abstract_goal),
                % We keep track of the number of disjuncts for use
                % in heuristics that may speed up the convex hull calculation.
                disj_size      :: int,

                disj_locals    :: local_vars,
                disj_nonlocals :: nonlocal_vars
            )
    ;       term_conj(
                conj_goals     :: list(abstract_goal),
                conj_locals    :: local_vars,
                conj_nonlocals :: nonlocal_vars
            )
    ;       term_call(
                call_ppid      :: abstract_ppid,
                call_context   :: prog_context,
                call_vars      :: call_vars,
                call_zeros     :: zero_vars,
                call_locals    :: local_vars,
                call_nonlocals :: nonlocal_vars,
                call_constrs   :: polyhedron
            )
    ;       term_primitive(
                prim_constrs   :: polyhedron,
                prim_locals    :: local_vars,
                prim_nonlocals :: nonlocal_vars
            ).

    % This type is used to keep track of intramodule recursion during
    % the build pass.
    %
    % NOTE: if a procedure is (possibly) involved in intermodule recursion
    % we handle things differently.
    %
:- type recursion_type
    --->    none        % Procedure is not recursive.
    ;       direct_only % Only recursion is self-calls.
    ;       mutual_only % Only recursion is calls to other procs
                        % in the same SCC.
    ;       both.       % Both types of recursion.

%-----------------------------------------------------------------------------%
%
% Functions that operate on the AR.
%

    % Update the local and nonlocal variable sets associated with an
    % abstract goal.
    %
:- func update_local_and_nonlocal_vars(abstract_goal, local_vars,
    nonlocal_vars) = abstract_goal.

    % Succeeds iff the given SCC contains recursion.
    %
:- pred scc_contains_recursion(abstract_scc::in) is semidet.

    % Succeeds iff the given procedure is recursive (either directly
    % or otherwise).
    %
:- pred proc_is_recursive(abstract_proc::in) is semidet.

    % Returns the size_varset for this given SCC.
    %
:- func size_varset_from_abstract_scc(abstract_scc) = size_varset.

    % Succeeds iff the results of the analysis depend upon the values
    % of some higher-order variables.
    %
:- pred analysis_depends_on_ho(abstract_proc::in) is semidet.

    % For any two goals whose recursion types are known return the
    % recursion type of the conjunction of the two goals.
    %
:- func combine_recursion_types(recursion_type, recursion_type)
    = recursion_type.

    % Combines the constraints contained in two primitive goals into
    % a single primitive goal. It is an error to pass any other kind
    % of abstract goal as an argument to this function.
    %
:- func combine_primitive_goals(abstract_goal, abstract_goal) = abstract_goal.

    % Take a list of conjoined primitive goals and simplify them
    % so there is one large block of constraints.
    %
:- func simplify_abstract_rep(abstract_goal) = abstract_goal.
:- func simplify_conjuncts(list(abstract_goal)) = list(abstract_goal).

%-----------------------------------------------------------------------------%
%
% Predicates for printing out debugging traces, etc.
%

    % Dump a representation of the AR to stdout.
    %
:- pred dump_abstract_scc(io.text_output_stream::in, module_info::in,
    abstract_scc::in, io::di, io::uo) is det.

    % As above. The extra argument specifies the indentation level.
    %
:- pred dump_abstract_scc(io.text_output_stream::in, module_info::in,
    int::in, abstract_scc::in, io::di, io::uo) is det.

    % Write an abstract_proc to stdout.
    %
:- pred dump_abstract_proc(io.text_output_stream::in, module_info::in,
    int::in, abstract_proc::in, io::di, io::uo) is det.

    % Write an abstract_goal to stdout.
    %
:- pred dump_abstract_goal(io.text_output_stream::in, module_info::in,
    size_varset::in, int::in, abstract_goal::in, io::di, io::uo) is det.

%-----------------------------------------------------------------------------%
%-----------------------------------------------------------------------------%

:- implementation.

:- import_module hlds.hlds_out.
:- import_module hlds.hlds_out.hlds_out_util.

:- import_module int.
:- import_module require.
:- import_module string.
:- import_module term.
:- import_module varset.

%-----------------------------------------------------------------------------%
%
% Functions that operate on the AR.
%

update_local_and_nonlocal_vars(Goal0, Locals0, NonLocals0) = Goal :-
    (
        Goal0     = term_disj(Goals, Size, Locals1, NonLocals1),
        Locals    = Locals0 ++ Locals1,
        NonLocals = NonLocals0 ++ NonLocals1,
        Goal      = term_disj(Goals, Size, Locals,  NonLocals)
    ;
        Goal0     = term_conj(Goals, Locals1, NonLocals1),
        Locals    = Locals0 ++ Locals1,
        NonLocals = NonLocals0 ++ NonLocals1,
        Goal      = term_conj(Goals, Locals, NonLocals)
    ;
        Goal0     = term_call(PPId, Context, CallVars, Zeros, Locals1,
                        NonLocals1, Polyhedron),
        Locals    = Locals0 ++ Locals1,
        NonLocals = NonLocals0 ++ NonLocals1,
        Goal      = term_call(PPId, Context, CallVars, Zeros, Locals,
                        NonLocals, Polyhedron)
    ;
        Goal0     = term_primitive(Polyhedron, Locals1, NonLocals1),
        Locals    = Locals0 ++ Locals1,
        NonLocals = NonLocals0 ++ NonLocals1,
        Goal      = term_primitive(Polyhedron, Locals, NonLocals)
    ).

scc_contains_recursion(SCC) :-
    ( if set.remove_least(Proc, SCC, _) then
        Proc ^ ap_recursion \= none
    else
        unexpected($pred, "empty SCC")
    ).

proc_is_recursive(Proc) :-
    not Proc ^ ap_recursion = none.

size_varset_from_abstract_scc(SCC) = SizeVarSet :-
    ( if set.remove_least(Proc, SCC, _) then
        SizeVarSet = Proc ^ ap_size_varset
    else
        unexpected($pred, "empty SCC")
    ).

analysis_depends_on_ho(Proc) :-
    list.is_not_empty(Proc ^ ap_ho_calls).

%-----------------------------------------------------------------------------%
%
% Code for dealing with different types of recursion.
%

combine_recursion_types(none,        none)        = none.
combine_recursion_types(none,        direct_only) = direct_only.
combine_recursion_types(none,        mutual_only) = mutual_only.
combine_recursion_types(none,        both)        = both.
combine_recursion_types(direct_only, none)        = direct_only.
combine_recursion_types(direct_only, direct_only) = direct_only.
combine_recursion_types(direct_only, mutual_only) = both.
combine_recursion_types(direct_only, both)        = both.
combine_recursion_types(mutual_only, none)        = mutual_only.
combine_recursion_types(mutual_only, direct_only) = both.
combine_recursion_types(mutual_only, mutual_only) = mutual_only.
combine_recursion_types(mutual_only, both)        = both.
combine_recursion_types(both,        none)        = both.
combine_recursion_types(both,        direct_only) = both.
combine_recursion_types(both,        mutual_only) = both.
combine_recursion_types(both,        both)        = both.

combine_primitive_goals(GoalA, GoalB) = Goal :-
    ( if
        GoalA = term_primitive(PolyA, LocalsA, NonLocalsA),
        GoalB = term_primitive(PolyB, LocalsB, NonLocalsB)
    then
        Poly      = polyhedron.intersection(PolyA, PolyB),
        Locals    = LocalsA ++ LocalsB,
        NonLocals = NonLocalsA ++ NonLocalsB,
        Goal      = term_primitive(Poly, Locals, NonLocals)
    else
        unexpected($pred, "non-primitive goals")
    ).

%-----------------------------------------------------------------------------%
%
% Code for simplifying the abstract representation.
%

% XXX We should keep running the simplifications until we arrive at a fixpoint.

simplify_abstract_rep(Goal0) = Goal :-
    simplify_abstract_rep(Goal0, Goal).

:- pred simplify_abstract_rep(abstract_goal::in, abstract_goal::out) is det.

simplify_abstract_rep(Goal0, Goal) :-
    (
        Goal0 = term_disj(Disjuncts0, _Size0, Locals, NonLocals),
        % Begin by simplifying each disjunct.
        list.map(simplify_abstract_rep, Disjuncts0, Disjuncts),
        (
            Disjuncts = [] ,
            Goal = term_primitive(polyhedron.universe, [], [])
        ;
            Disjuncts = [Disjunct] ,
            % We need to merge the set of locals with the locals from the
            % disjunct otherwise we will end up throwing away the locals
            % from the enclosing goal.
            Goal = update_local_and_nonlocal_vars(Disjunct, Locals, NonLocals)
        ;
            Disjuncts = [_, _ | _] ,
            Size = list.length(Disjuncts),
            Goal = term_disj(Disjuncts, Size, Locals, NonLocals)
        )
    ;
        Goal0 = term_conj(Conjuncts0, Locals, NonLocals),
        some [!Conjuncts] (
            !:Conjuncts = Conjuncts0,
            list.map(simplify_abstract_rep, !Conjuncts),
            list.negated_filter(is_empty_primitive, !Conjuncts),
            flatten_conjuncts(!Conjuncts),
            list.negated_filter(is_empty_conj, !Conjuncts),
            Conjuncts = !.Conjuncts
        ),
        ( if Conjuncts = [Conjunct] then
            % The local/non-local var sets need to be updated for similar
            % reasons as we do with disjunctions.
            Goal = update_local_and_nonlocal_vars(Conjunct, Locals,
                NonLocals)
        else
            Goal = term_conj(Conjuncts, Locals, NonLocals)
        )
    ;
        ( Goal0 = term_primitive(_, _, _)
        ; Goal0 = term_call(_, _, _, _, _, _, _)
        ),
        Goal = Goal0
    ).

    % Given a conjunction of abstract goals take the intersection
    % of all consecutive primitive goals in the list of abstract goals.
    %
    % e.g if we have
    %
    %   [P1, P2, P3, NP1, NP2, P4, P5, NP3, P6, P7]
    %
    %   where Px is a primitive goal and NPx is a non-primitive goal,
    %
    % then simplify this to:
    %
    %   [(P1 /\ P2 /\ P3), NP1, NP2, (P4 /\ P5), NP3, (P6 /\ P7)]
    %
    %   where `/\' is the intersection of the primitive goals.
    %
    % Note: because intersection is commutative we could go further
    % and take the intersection of all the primitive goals in a conjunction,
    % but that unnecessarily increases the size of the edge labels in pass 2.
    %
:- pred flatten_conjuncts(list(abstract_goal)::in, list(abstract_goal)::out)
    is det.

flatten_conjuncts([], []).
flatten_conjuncts([Goal], [Goal]).
flatten_conjuncts(Goals0 @ [_, _ | _], Goals) :-
    flatten_conjuncts_2(Goals0, [], RevGoals),
    Goals = list.reverse(RevGoals).

:- pred flatten_conjuncts_2(list(abstract_goal)::in, list(abstract_goal)::in,
    list(abstract_goal)::out) is det.

flatten_conjuncts_2([], !RevGoals).
flatten_conjuncts_2([Goal0 | Goals0], !RevGoals) :-
    ( if Goal0 = term_primitive(_, _, _) then
        list.take_while(is_primitive, Goals0, Primitives, NextNonPrimitive),
        (
            Primitives = [],
            NewPrimitive = Goal0
        ;
            Primitives = [_ | _],
            NewPrimitive = list.foldl(combine_primitives, Primitives, Goal0)
        ),
        list.cons(NewPrimitive, !RevGoals)
    else
        list.cons(Goal0, !RevGoals),
        NextNonPrimitive = Goals0
    ),
    flatten_conjuncts_2(NextNonPrimitive, !RevGoals).

    % Test whether an abstract goal is a primitive.
    %
:- pred is_primitive(abstract_goal::in) is semidet.

is_primitive(term_primitive(_, _, _)).

:- func combine_primitives(abstract_goal, abstract_goal) = abstract_goal.

combine_primitives(GoalA, GoalB) = Goal :-
    ( if
        GoalA = term_primitive(PolyA, LocalsA, NonLocalsA),
        GoalB = term_primitive(PolyB, LocalsB, NonLocalsB)
    then
        Poly = polyhedron.intersection(PolyA, PolyB),
        Locals = LocalsA ++ LocalsB,
        NonLocals = NonLocalsA ++ NonLocalsB,
        Goal = term_primitive(Poly, Locals, NonLocals)
    else
        unexpected($pred, "non-primitive goals")
    ).

    % We end up with `empty' primitives by abstracting unifications
    % that involve variables that have zero size.
    %
:- pred is_empty_primitive(abstract_goal::in) is semidet.

is_empty_primitive(term_primitive(Poly, _, _)) :-
    polyhedron.is_universe(Poly).

    % We end up with `empty' conjunctions by abstracting conjunctions
    % that involve variables that have zero size.
    %
:- pred is_empty_conj(abstract_goal::in) is semidet.

is_empty_conj(term_conj([], _, _)).

%-----------------------------------------------------------------------------%
%
% Predicates for simplifying conjuncts.
%

% XXX Make this part of the other AR simplification predicates.

simplify_conjuncts(Goals0) = Goals :-
    simplify_conjuncts(Goals0, Goals).

:- pred simplify_conjuncts(list(abstract_goal)::in, list(abstract_goal)::out)
    is det.

simplify_conjuncts(Goals0, Goals) :-
    (
        Goals0 = [],
        Goals = []
    ;
        Goals0 = [Goal],
        Goals = [Goal]
    ;
        % If the list of conjuncts starts with two primitives
        % join them together into a single primitive.
        Goals0 = [GoalA, GoalB | OtherGoals],
        ( if
            GoalA = term_primitive(PolyA,  LocalsA, NonLocalsA),
            GoalB = term_primitive(PolyB,  LocalsB, NonLocalsB)
        then
            Poly = polyhedron.intersection(PolyA, PolyB),
            Locals = LocalsA ++ LocalsB,
            NonLocals = NonLocalsA ++ NonLocalsB,
            Goal = term_primitive(Poly, Locals, NonLocals),
            Goals1 = [Goal | OtherGoals],
            simplify_conjuncts(Goals1, Goals)
        else
            Goals = Goals0
        )
    ).

%-----------------------------------------------------------------------------%
%
% Predicates for printing out the abstract data structure.
% (These are for debugging only.)
%

dump_abstract_scc(Stream, ModuleInfo, SCC, !IO) :-
    dump_abstract_scc(Stream, ModuleInfo, 0, SCC, !IO).

dump_abstract_scc(Stream, ModuleInfo, Indent, SCC, !IO) :-
    set.foldl(dump_abstract_proc(Stream, ModuleInfo, Indent), SCC, !IO).

dump_abstract_proc(Stream, ModuleInfo, Indent, Proc, !IO) :-
    AbstractPPId = Proc ^ ap_ppid,
    HeadVars = Proc ^ ap_head_vars,
    Body = Proc ^ ap_body,
    SizeVarSet = Proc ^ ap_size_varset,
    AbstractPPId = real(PPId),
    PPIdStr = pred_proc_id_to_dev_string(ModuleInfo, PPId),
    HeadVarSizeStrs = list.map(size_var_to_string(SizeVarSet), HeadVars),
    HeadVarSizesStr = string.join_list(", ", HeadVarSizeStrs),
    indent_line(Stream, Indent, !IO),
    io.format(Stream, "%s : [ %s ] :- \n",
        [s(PPIdStr), s(HeadVarSizesStr)], !IO),
    dump_abstract_goal(Stream, ModuleInfo, SizeVarSet, Indent + 1, Body, !IO).

:- func size_var_to_string(size_varset, size_var) = string.

size_var_to_string(SizeVarSet, Var) = Str :-
    varset.lookup_name(SizeVarSet, Var, VarName),
    Str = string.format("%s[%d]", [s(VarName), i(term.var_to_int(Var))]).

:- func recursion_type_to_string(recursion_type) = string.
:- pragma consider_used(func(recursion_type_to_string/1)).

recursion_type_to_string(none)        = "none".
recursion_type_to_string(direct_only) = "direct recursion only".
recursion_type_to_string(mutual_only) = "mutual recursion only".
recursion_type_to_string(both)        = "mutual and direct recursion".

:- pred dump_abstract_disjuncts(io.text_output_stream::in, module_info::in,
    size_varset::in, int::in, list(abstract_goal)::in, io::di, io::uo) is det.

dump_abstract_disjuncts(_, _, _, _, [], !IO).
dump_abstract_disjuncts(Stream, ModuleInfo, VarSet, Indent,
        [Goal | Goals], !IO) :-
    dump_abstract_goal(Stream, ModuleInfo, VarSet, Indent + 1, Goal, !IO),
    (
        Goals = [_ | _],
        indent_line(Stream, Indent, !IO),
        io.write_string(Stream, ";\n", !IO)
    ;
        Goals = []
    ),
    dump_abstract_disjuncts(Stream, ModuleInfo, VarSet, Indent, Goals, !IO).

dump_abstract_goal(Stream, ModuleInfo, VarSet, Indent, AbstractGoal, !IO) :-
    (
        AbstractGoal = term_disj(Goals, Size, Locals, NonLocals),
        indent_line(Stream, Indent, !IO),
        io.format(Stream, "disj[%d](\n", [i(Size)], !IO),
        dump_abstract_disjuncts(Stream, ModuleInfo, VarSet, Indent,
            Goals, !IO),
        LocalVarNamesStr = var_names_to_string(VarSet, Locals),
        NonLocalVarNamesStr = var_names_to_string(VarSet, NonLocals),
        indent_line(Stream, Indent, !IO),
        io.format(Stream, " Locals: %s\n", [s(LocalVarNamesStr)], !IO),
        indent_line(Stream, Indent, !IO),
        io.format(Stream, " Non-Locals: %s\n", [s(NonLocalVarNamesStr)], !IO),
        indent_line(Stream, Indent, !IO),
        io.write_string(Stream, ")\n", !IO)
    ;
        AbstractGoal = term_conj(Goals, Locals, NonLocals),
        indent_line(Stream, Indent, !IO),
        io.write_string(Stream, "conj(\n", !IO),
        list.foldl(
            dump_abstract_goal(Stream, ModuleInfo, VarSet, Indent + 1),
            Goals, !IO),
        LocalVarNamesStr = var_names_to_string(VarSet, Locals),
        NonLocalVarNamesStr = var_names_to_string(VarSet, NonLocals),
        indent_line(Stream, Indent, !IO),
        io.format(Stream, " Locals: %s\n", [s(LocalVarNamesStr)], !IO),
        indent_line(Stream, Indent, !IO),
        io.format(Stream, " Non-Locals: %s\n", [s(NonLocalVarNamesStr)], !IO),
        indent_line(Stream, Indent, !IO),
        io.write_string(Stream, ")\n", !IO)
    ;
        AbstractGoal = term_call(PPId0, _, CallVars, _, _, _, CallPoly),
        PPId0 = real(PPId),
        PPIdStr = pred_proc_id_to_dev_string(ModuleInfo, PPId),
        CallVarNamesStr = var_names_to_string(VarSet, CallVars),
        indent_line(Stream, Indent, !IO),
        io.format(Stream, "call: %s : [%s]\n",
            [s(PPIdStr), s(CallVarNamesStr)], !IO),
        indent_line(Stream, Indent, !IO),
        io.write_string(Stream, "Other call constraints:[\n", !IO),
        polyhedron.write_polyhedron(Stream, VarSet, CallPoly, !IO),
        indent_line(Stream, Indent, !IO),
        io.write_string(Stream, "]\n", !IO)
    ;
        AbstractGoal = term_primitive(Poly, _, _),
        indent_line(Stream, Indent, !IO),
        io.write_string(Stream, "[\n", !IO),
        polyhedron.write_polyhedron(Stream, VarSet, Poly, !IO),
        indent_line(Stream, Indent, !IO),
        io.write_string(Stream, "]\n", !IO)
    ).

:- func var_names_to_string(size_varset, list(size_var)) = string.

var_names_to_string(VarSet, Vars) = Str :-
    list.map(varset.lookup_name(VarSet), Vars, VarNames),
    Str = string.join_list(", ", VarNames).

:- pred indent_line(io.text_output_stream::in, int::in, io::di, io::uo) is det.

indent_line(Stream, N, !IO) :-
    ( if N > 0 then
        io.write_string(Stream, "  ", !IO),
        indent_line(Stream, N - 1, !IO)
    else
        true
    ).

%-----------------------------------------------------------------------------%
:- end_module transform_hlds.term_constr_data.
%-----------------------------------------------------------------------------%
