%-----------------------------------------------------------------------------% % 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. %-----------------------------------------------------------------------------%