%---------------------------------------------------------------------------%
% vim: ft=mercury ts=4 sw=4 et
%---------------------------------------------------------------------------%
% Copyright (C) 2005-2011 The University of Melbourne.
% Copyright (C) 2014-2016, 2018-2026 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: state_var.m.
% Main author of original version: rafe.
% Main author of the current version, rewritten in 2011: zs.
%
%---------------------------------------------------------------------------%

:- module hlds.make_hlds.state_var.
:- interface.

:- import_module hlds.hlds_clauses.
:- import_module hlds.hlds_goal.
:- import_module hlds.hlds_module.
:- import_module hlds.make_hlds.goal_expr_to_goal.
:- import_module hlds.make_hlds.qual_info.
:- import_module mdbcomp.
:- import_module mdbcomp.prim_data.
:- import_module parse_tree.
:- import_module parse_tree.error_spec.
:- import_module parse_tree.prog_data.
:- import_module parse_tree.prog_item.

:- import_module list.
:- import_module map.
:- import_module one_or_more.

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

    % This type describes the state of the code that converts goals
    % from their parse tree form to their HLDS form. Almost all the code
    % in all the modules of the make_hlds package that handle goals
    % pass around values of this type as effectively global state,
    % with persistent updates. (The state of the state var transformation
    % itself is threaded through that code in a different manner;
    % see the definition of the svar_state type below.)
    %
    % With one exception, all of the fields are writeable.
:- type unravel_info
    --->    unravel_info(
                % The module_info, which we use for several purposes.
                % Most uses are readonly, including getting the globals
                % for option lookup, and the module name for creating
                % debug output streams.
                %
                % The only situation in which we update the module_info field
                % is when processing disable_warning scopes that disable
                % the warning for occurs check violations. In that case,
                % we set the option controlling that warning to "no"
                % while processing the goal in the scope, and reset it
                % afterwards. Such scopes are rare enough that storing the
                % value of that option as a separate field in this structure
                % would not be worthwhile.
                ui_module_info      :: module_info,

                % The value of the from_ground_term_threshold option.
                % This field duplicates the value stored in the globals
                % structure, but it is needed often enough that a separate
                % fast-access copy is worthwhile.
                % This field is read-only.
                ui_fgt_threshold    :: int,

                % The store where we record information about what entities
                % imported from other modules are used. We use that info
                % to generate warnings about unused imports.
                ui_qual_info        :: qual_info,

                % The varset of the clause whose goal we are converting.
                % New instances of state variables are allocated from here.
                ui_varset           :: prog_varset,

                % The part of the state of the state var transformation
                % that is updated persistently (meaning, that once we create
                % a new version, we don't go back to look at previous
                % versions.)
                ui_state_var_store  :: svar_store,

                % The errors and warnings that we definitely want to print.
                % (The svar_store also contains error_specs, but we print those
                % only as hints *if and when* we later find certain other kinds
                % of errors.)
                ui_error_specs      :: list(error_spec)
            ).

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

:- pred create_new_unravel_var(prog_var::out,
    unravel_info::in, unravel_info::out) is det.

:- pred create_new_named_unravel_var(string::in, prog_var::out,
    unravel_info::in, unravel_info::out) is det.

:- pred record_unravel_found_syntax_error(
    unravel_info::in, unravel_info::out) is det.

:- pred add_unravel_spec(error_spec::in,
    unravel_info::in, unravel_info::out) is det.
:- pred add_unravel_specs(list(error_spec)::in,
    unravel_info::in, unravel_info::out) is det.
:- pred add_unravel_oom_specs(one_or_more(error_spec)::in,
    unravel_info::in, unravel_info::out) is det.

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

    % This synonym improves code legibility. The intention is that we use
    % svar instead of prog_var in pred type declarations for any variables X
    % that represent state variables !X.
:- type svar == prog_var.

    % When collecting the arms of a disjunction, we also need to collect
    % the resulting svar_states.
:- type hlds_goal_svar_state
    --->    hlds_goal_svar_state(hlds_goal, svar_state).

    % The state of the currently visible state variables. The state gets
    % updated differently along differently execution paths. When execution
    % paths rejoin, you need to create the state after the rejoin from the
    % states being rejoined (which is what we use hlds_goal_svar_state for)
    % using their last common ancestor state as a basis.
:- type svar_state.

    % The persistent information needed by the state variable transformation.
    % The store should always be threaded straight through all computations
    % involved in the translation of the parse tree to the HLDS, with all
    % updates being permanent.
:- type svar_store.

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

    % Given the user-given name of a state variable, return the name
    % of the program variable representing its initial version.
    %
:- func initial_state_var_name(string) = string.

    % is_prog_var_for_some_state_var(VarSet, ProgVar, StateVarName):
    %
    % Succeed if and only if ProgVar's name indicates that it represents
    % a version of a state variable named StateVarName. Succeed for
    % any version: initial, middle, or final.
    %
:- pred is_prog_var_for_some_state_var(prog_varset::in, prog_var::in,
    string::out) is semidet.

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

    % Replace !X args with two args !.X, !:X in that order.
    %
:- pred expand_bang_state_pairs_in_terms(list(prog_term)::in,
    list(prog_term)::out) is det.
:- pred expand_bang_state_pairs_in_instance_method(instance_method::in,
    instance_method::out) is det.

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

:- type new_statevar_map.

    % Prepare for processing a clause by processing its head.
    % If the head contains any references to !.S or !:S or both,
    % make state variable S known in the body of the clause.
    % (The head should not contain any references to !S; those should
    % have been expanded out by calling expand_bang_state_pairs BEFORE calling
    % this predicate.)
    %
    % Given the original list of args, we return a version in which state
    % variable references have been replaced. Since we don't yet know what
    % the final values of the state variables will be, we create prog_vars
    % to represent these values, and return a mapping from the state vars
    % to these designated-final-value prog_vars.
    %
:- pred svar_prepare_for_clause_head(module_info::in, qual_info::in,
    prog_varset::in, list(prog_term)::in, list(prog_term)::out,
    map(svar, prog_var)::out, new_statevar_map::out,
    svar_state::out, unravel_info::out) is det.

    % Prepare for processing a lambda expression by processing its head.
    %
    % In most ways, this is very similar to processing the head of a clause,
    % but we also need to handle state variables which are visible in the scope
    % that encloses the lambda expression. We make those state vars read-only
    % within the lambda expression.
    %
:- pred svar_prepare_for_lambda_head(prog_context::in,
    list(prog_term)::in, list(prog_term)::out,
    map(svar, prog_var)::out, new_statevar_map::out,
    svar_state::in, svar_state::out,
    unravel_info::in, unravel_info::out) is det.

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

    % Finish processing a clause. Make the final values of the clause's state
    % vars match the mapping we decided on when processing the head.
    %
:- pred svar_finish_clause_body(prog_context::in, new_statevar_map::in,
    map(svar, prog_var)::in, svar_state::in, svar_state::in,
    hlds_goal::in, hlds_goal::in, hlds_goal::out,
    list(error_spec)::out, unused_statevar_arg_map::out,
    unravel_info::in, unravel_info::out) is det.

    % Finish processing a lambda expression.
    %
:- pred svar_finish_lambda_body(prog_context::in, list(mer_mode)::in,
    new_statevar_map::in, map(svar, prog_var)::in, goal::in,
    list(hlds_goal)::in, hlds_goal::out, svar_state::in, svar_state::in,
    unravel_info::in, unravel_info::out) is det.

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

    % Finish the execution of an atomic goal. If this goal was not inside
    % another atomic goal, then make any updates to state variables performed
    % by the atomic goal take effect: make the value assigned to !:S inside
    % the goal the new !.S.
    %
:- pred svar_finish_atomic_goal(loc_kind::in, svar_state::in, svar_state::out)
    is det.

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

    % Add some local state variables.
    %
:- pred svar_prepare_for_local_state_vars(prog_context::in, list(svar)::in,
    svar_state::in, svar_state::out,
    unravel_info::in, unravel_info::out) is det.

    % Remove some local state variables.
    %
:- pred svar_finish_local_state_vars(unravel_info::in,
    list(svar)::in, svar_state::in, svar_state::in, svar_state::out) is det.

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

    % Make sure that all arms of a disjunction produce the same state variable
    % bindings, by adding unifiers as necessary.
    %
:- pred svar_finish_disjunction(list(hlds_goal_svar_state)::in,
    list(hlds_goal)::out, svar_state::in, svar_state::out,
    unravel_info::in, unravel_info::out) is det.

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

    % Add unifiers to the Then and Else arms of an if-then-else as needed
    % to ensure that all the state variables match up.
    %
    % We also add unifiers to the Then arm for any new state variable
    % mappings produced in the condition.
    %
:- pred svar_finish_if_then_else(loc_kind::in, prog_context::in,
    list(svar)::in,
    hlds_goal::in, hlds_goal::out, hlds_goal::in, hlds_goal::out,
    svar_state::in, svar_state::in, svar_state::in, svar_state::in,
    svar_state::out,
    unravel_info::in, unravel_info::out) is det.

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

:- type svar_outer_atomic_scope_info.
:- type svar_inner_atomic_scope_info.

    % svar_start_outer_atomic_scope(Context, OuterStateVar, OuterDI, OuterUO,
    %   OuterScopeInfo, !SVarState, !UrInfo):
    %
    % This predicate converts a !OuterStateVar specification in an atomic scope
    % to a pair of outer state variables, OuterDI and OuterUO. Since
    % !OuterStateVar should *not* be accessible inside the atomic scope,
    % we delete it, but record it in OuterScopeInfo. The accessibility of
    % !OuterStateVar will be restored when you call svar_finish_atomic_scope
    % with OuterScopeInfo.
    %
:- pred svar_start_outer_atomic_scope(prog_context::in, prog_var::in,
    prog_var::out, prog_var::out, svar_outer_atomic_scope_info::out,
    svar_state::in, svar_state::out,
    unravel_info::in, unravel_info::out) is det.

    % svar_finish_outer_atomic_scope(OuterScopeInfo, !SInfo):
    %
    % Restore the accessibility of !OuterStateVar that was disabled by
    % svar_start_atomic_scope.
    %
:- pred svar_finish_outer_atomic_scope(svar_outer_atomic_scope_info::in,
    svar_state::in, svar_state::out) is det.

    % svar_start_inner_atomic_scope(Context, InnerStateVar, InnerScopeInfo,
    %   !SVarState, !UrInfo):
    %
    % This predicate prepares for an atomic scope with an !InnerStateVar
    % specification by making that state var available.
    %
:- pred svar_start_inner_atomic_scope(prog_context::in, prog_var::in,
    svar_inner_atomic_scope_info::out,
    svar_state::in, svar_state::out,
    unravel_info::in, unravel_info::out) is det.

    % svar_finish_inner_atomic_scope(Context, InnerScopeInfo, InnerDI, InnerUO,
    %   !SVarState, !UrInfo):
    %
    % This predicate ends an atomic scope with an !InnerStateVar
    % specification by making that state var unavailable, and returning
    % the two variables InnerDI and InnerUO representing the initial and final
    % states of this state variable.
    %
:- pred svar_finish_inner_atomic_scope(prog_context::in,
    svar_inner_atomic_scope_info::in, prog_var::out, prog_var::out,
    svar_state::in, svar_state::out) is det.

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

    % Given a list of argument terms, replace !.X and !:X with the
    % ordinary variables corresponding to them, updating the svar_state
    % as appropriate. Any occurrence of !X should already have been
    % expanded into a <!.X, !:X> pair by a call to expand_bang_state_pairs.
    %
:- pred replace_any_dot_colon_state_var_in_terms(
    list(prog_term)::in, list(prog_term)::out,
    svar_state::in, svar_state::out,
    unravel_info::in, unravel_info::out) is det.

    % Same as replace_any_dot_colon_state_var_in_terms, but for only one term.
    %
:- pred replace_any_dot_colon_state_var_in_term(prog_term::in, prog_term::out,
    svar_state::in, svar_state::out,
    unravel_info::in, unravel_info::out) is det.

    % Look up the prog_var that represents the current state of the given
    % state variable.
    %
:- pred lookup_dot_state_var(prog_context::in, svar::in, prog_var::out,
    svar_state::in, svar_state::out,
    unravel_info::in, unravel_info::out) is det.

    % Look up the prog_var that represents the next state of the given
    % state variable.
    %
:- pred lookup_colon_state_var(prog_context::in, svar::in, prog_var::out,
    svar_state::in, svar_state::out,
    unravel_info::in, unravel_info::out) is det.

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

    % Flatten a conjunction while preserving the invariants that the state
    % variable transformation cares about.
    %
:- pred svar_flatten_conj(prog_context::in,
    list(hlds_goal)::in, hlds_goal::out,
    unravel_info::in, unravel_info::out) is det.

    % Flatten a goal into a conjunction while preserving the invariants that
    % the state variable transformation cares about.
    %
:- pred svar_goal_to_conj_list(hlds_goal::in, list(hlds_goal)::out,
    unravel_info::in, unravel_info::out) is det.

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

    % Does the given argument list have a function result term
    % that tries to use state var notation to refer to *two* terms?
    %
    % If yes, return the state variable involved, and the context of the
    % reference.
    %
:- pred illegal_state_var_func_result(pred_or_func::in, list(prog_term)::in,
    svar::out, prog_context::out) is semidet.

    % Does the given term have the form a !X, i.e. does it represent
    % *two* arguments? This is not acceptable in some contexts, such as
    % function results and lambda expression arguments.
    %
    % If yes, return the state variable involved, and the context of the
    % reference.
    %
:- pred is_term_a_bang_state_pair(prog_term::in,
    svar::out, prog_context::out) is semidet.

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

:- pred report_illegal_state_var_update(prog_context::in,
    string::in, prog_context::in, svar::in,
    unravel_info::in, unravel_info::out) is det.

:- pred report_illegal_func_svar_result(prog_context::in, svar::in,
    unravel_info::in, unravel_info::out) is det.
:- func report_illegal_func_svar_result_raw(prog_context,
    prog_varset, svar) = error_spec.

:- pred report_illegal_bang_svar_lambda_arg(prog_context::in, svar::in,
    unravel_info::in, unravel_info::out) is det.
:- func report_illegal_bang_svar_lambda_arg_raw(prog_context,
    prog_varset, svar) = error_spec.

:- pred report_svar_unify_error(prog_context::in, svar::in,
    svar_state::in, svar_state::out,
    unravel_info::in, unravel_info::out) is det.

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

:- implementation.

:- import_module hlds.goal_vars.
:- import_module hlds.hlds_markers.
:- import_module hlds.make_goal.
:- import_module hlds.mode_util.
:- import_module libs.
:- import_module libs.globals.
:- import_module libs.optimization_options.
:- import_module libs.options.
:- import_module mdbcomp.goal_path.
:- import_module mdbcomp.sym_name.
:- import_module parse_tree.prog_util.
:- import_module parse_tree.set_of_var.

:- import_module assoc_list.
:- import_module bool.
:- import_module cord.
:- import_module counter.
:- import_module io.
:- import_module maybe.
:- import_module one_or_more_map.
:- import_module pair.
:- import_module require.
:- import_module string.
:- import_module term.
:- import_module term_context.
:- import_module uint.
:- import_module varset.

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

create_new_unravel_var(Var, !UrInfo) :-
    VarSet0 = !.UrInfo ^ ui_varset,
    varset.new_var(Var, VarSet0, VarSet),
    !UrInfo ^ ui_varset := VarSet.

create_new_named_unravel_var(Name, Var, !UrInfo) :-
    VarSet0 = !.UrInfo ^ ui_varset,
    varset.new_named_var(Name, Var, VarSet0, VarSet),
    !UrInfo ^ ui_varset := VarSet.

record_unravel_found_syntax_error(!UrInfo) :-
    QualInfo0 = !.UrInfo ^ ui_qual_info,
    qual_info_set_found_syntax_error(yes, QualInfo0, QualInfo),
    !UrInfo ^ ui_qual_info := QualInfo.

add_unravel_spec(NewSpec, !UrInfo) :-
    Specs0 = !.UrInfo ^ ui_error_specs,
    Specs = [NewSpec | Specs0],
    !UrInfo ^ ui_error_specs := Specs.

add_unravel_specs(NewSpecs, !UrInfo) :-
    (
        NewSpecs = []
    ;
        NewSpecs = [_ | _],
        Specs0 = !.UrInfo ^ ui_error_specs,
        Specs = NewSpecs ++ Specs0,
        !UrInfo ^ ui_error_specs := Specs
    ).

add_unravel_oom_specs(one_or_more(HeadSpec, TailSpecs), !UrInfo) :-
    Specs0 = !.UrInfo ^ ui_error_specs,
    Specs = [HeadSpec | TailSpecs] ++ Specs0,
    !UrInfo ^ ui_error_specs := Specs.

%---------------------------------------------------------------------------%
%
% Define the main data structures used by the implementation of state vars.
%

    % State vars defined outside a lambda goal become readonly when we move
    % inside the lambda goal. Inside the lambda goal, it makes sense to access
    % the current value of such a state var, but it does not make sense
    % to try to update its value.
    %
    % We should make negations behave similarly: it should not be possible
    % to update an outside state var inside a negation. However, for now,
    % the language reference manual allows such updates. This type is here
    % in case that changes.
:- type readonly_context_kind
    --->    roc_lambda.

:- type svar_status
    % The two updated statuses may legally be present in a status map
    % only DURING the processing of an atomic goal. At the end of each
    % atomic goal, such statuses are always reset to status_known.
    --->    status_unknown
            % We are in a scope that allows use of this state var,
            % but it has not been given a value yet. This could be because
            % the scope of the state var was established with !:S, not !S
            % or !.S, in a clause head, or because it was established
            % in a `some [!S]' scope.

    ;       status_unknown_updated(prog_var)
            % Before this atomic goal, this state var was status_unknown,
            % but it was initialized by the current atomic goal to the given
            % prog_var.

    ;       status_known_ro(prog_var, readonly_context_kind, prog_context)
            % The given prog_var is the current version of this state var,
            % but the variable is readonly (ro); the program CANNOT create
            % new versions of the state var. The second argument says WHY
            % new versions cannot be created, and the third says where
            % the construct named by the second argument occurs.

    ;       status_known(prog_var)
            % The given prog_var is the current version of this state var;
            % the program can create new versions of the state var,
            % but has not done so yet.

    ;       status_known_updated(prog_var, prog_var).
            % The first prog_var is the current version of this state var,
            % and the second is the new, updated version, which will become
            % the current version when we finish executing the current
            % atomic goal.

:- type svar_state
    --->    svar_state(
                state_status_map    ::  map(svar, svar_status)
            ).

:- type last_id_map == map(prog_var, uint).

:- type svar_store
    --->    svar_store(
                store_next_goal_id          ::  ucounter,
                store_final_remap           ::  incremental_rename_map,

                % This maps each state variable that has had N name_middle
                % versions created to N, *provided* that N > 0. We use this
                % for two purposes:
                %
                % - Giving a name to the variables representing the middle
                %   (meaning non-initial and non-final) versions of each
                %   state variable that is affected only by references to
                %   that state variable. We used to use as the variable
                %   id assigned by the current varset, but this meant that
                %   changes in the number and/or handling of non-state
                %   variables in the clause could affect the variable names
                %   reported by error messages, requiring annoying updates to
                %   test cases .err_exp* files.
                %
                % - We use the absence of an entry for a state variable
                %   as an indication that this state variable has not been
                %   updated. This can seem a bit strange, since it is possible
                %   for a clause body to invoke a predicate that takes the
                %   state variable from its initial state to its final state
                %   directly, but even if that ends up being the case,
                %   it will not have started out that way. Instead, when
                %   processing the call, we will allocate a new middle version
                %   for the state var, and only when we finish the processing
                %   of the clause as a whole will we replace that middle
                %   version with the final version. And when we do, the
                %   record of the allocated-but-substitured-out middle version
                %   will remain in this field.
                store_last_id_map           ::  last_id_map,

                % We use this slot to store informational messages that
                % we want to print *only* in the presence of mode errors,
                % because
                %
                % - in the presence of mode errors, they can sometimes
                %   pinpoint the *cause* of some of those errors, but
                % - in the absence of mode errors, the messages would be
                %   only clutter.
                %
                % As of 2025 feb 20, these messages are all about situations
                % in which a disjunction or an if-then-else initialises
                % a state variable in some branches but not in others.
                store_missing_init_specs    ::  list(error_spec)
            ).

    % Create a new svar_state/store set up to start processing a clause head.
    %
:- func new_svar_state = svar_state.
:- func new_svar_store = svar_store.

new_svar_state = svar_state(map.init).
new_svar_store = svar_store(counter.uinit(1u), map.init, map.init, []).

:- type state_var_name_source
    --->    name_initial
    ;       name_middle
    ;       name_final.

:- pred new_state_var_instance(svar::in, state_var_name_source::in,
    prog_var::out, unravel_info::in, unravel_info::out) is det.

new_state_var_instance(StateVar, NameSource, Var, !UrInfo) :-
    VarSet0 = !.UrInfo ^ ui_varset,
    SVarName = varset.lookup_name(VarSet0, StateVar),
    (
        NameSource = name_initial,
        ProgVarName = initial_state_var_name(SVarName),
        varset.new_named_var(ProgVarName, Var, VarSet0, VarSet)
    ;
        NameSource = name_middle,
        SVarState0 = !.UrInfo ^ ui_state_var_store,
        LastIdMap0 = SVarState0 ^ store_last_id_map,
        ( if map.search(LastIdMap0, StateVar, LastId0) then
            CurId = LastId0 + 1u,
            map.det_update(StateVar, CurId, LastIdMap0, LastIdMap)
        else
            CurId = 1u,
            map.det_insert(StateVar, 1u, LastIdMap0, LastIdMap)
        ),
        SVarState = SVarState0 ^ store_last_id_map := LastIdMap,
        !UrInfo ^ ui_state_var_store := SVarState,
        ProgVarName = string.format("STATE_VARIABLE_%s_%u",
            [s(SVarName), u(CurId)]),
        varset.new_named_var(ProgVarName, Var, VarSet0, VarSet)
    ;
        NameSource = name_final,
        ProgVarName = string.format("STATE_VARIABLE_%s", [s(SVarName)]),
        varset.new_named_var(ProgVarName, Var, VarSet0, VarSet)
    ),
    !UrInfo ^ ui_varset := VarSet.

initial_state_var_name(SVarName) = ProgVarName :-
    ProgVarName = string.format("STATE_VARIABLE_%s_0", [s(SVarName)]).

is_prog_var_for_some_state_var(VarSet, Var, SVarName) :-
    % All prog_vars representing state vars are named.
    varset.search_name(VarSet, Var, VarName),
    string.remove_prefix("STATE_VARIABLE_", VarName, AfterStdPrefix),
    UnderscoreSeparatedPieces = string.split_at_char('_', AfterStdPrefix),
    (
        UnderscoreSeparatedPieces = [],
        % There is no underscore, so there can be no numerical suffix.
        SVarName = AfterStdPrefix
    ;
        UnderscoreSeparatedPieces = [_ | _],
        list.det_last(UnderscoreSeparatedPieces, LastPiece),
        string.to_int(LastPiece, _N),
        % This is either the initial or a middle version of SVarName.
        % (Initial if _N is zero, and middle otherwise)
        NumericalSuffix = "_" ++ LastPiece,
        SVarName = string.det_remove_suffix(AfterStdPrefix, NumericalSuffix)
    ).

    % is_prog_var_for_state_var(VarSet, StateVarName, ProgVar):
    %
    % Succeed if and only if ProgVar's name indicates that it represents
    % a version of the state variable named !StateVarName. Succeed for
    % any version: initial, middle, or final.
    %
    % This was a first attempt at solving the problem that the predicate above
    % is also addressing, before I realized that collecting all referenced
    % state variables permits a simpler design in our caller than having to
    % repeat the test (on *every* variable in a clauses_info's varset) for
    % every state var of interest to the code generating unneeded state
    % warnings.
    %
:- pred is_prog_var_for_state_var(prog_varset::in, string::in, prog_var::in)
   is semidet.
:- pragma consider_used(pred(is_prog_var_for_state_var/3)).

is_prog_var_for_state_var(VarSet, SVarName, Var) :-
    % All prog_vars representing state vars are named.
    varset.search_name(VarSet, Var, VarName),
    string.remove_prefix("STATE_VARIABLE_", VarName, AfterStdPrefix),
    string.remove_prefix(SVarName, AfterStdPrefix, Suffix),
    (
        Suffix = ""
        % This is the final version of  SVarName.
    ;
        string.remove_prefix("_", Suffix, SuffixAfterUnderscore),
        string.to_int(SuffixAfterUnderscore, _N)
        % This is either the initial or a middle version of SVarName.
        % (Initial if _N is zero, and middle otherwise)
    ).

%---------------------------------------------------------------------------%
%
% Expand !S into !.S, !:S pairs.
%

expand_bang_state_pairs_in_terms([], []).
expand_bang_state_pairs_in_terms([HeadArg0 | TailArgs0], Args) :-
    expand_bang_state_pairs_in_terms(TailArgs0, TailArgs),
    (
        HeadArg0 = variable(_, _),
        Args = [HeadArg0 | TailArgs]
    ;
        HeadArg0 = functor(Const, FunctorArgs, Context),
        ( if
            Const = atom("!"),
            FunctorArgs = [variable(_StateVar, _)]
        then
            HeadArg1 = functor(atom("!."), FunctorArgs, Context),
            HeadArg2 = functor(atom("!:"), FunctorArgs, Context),
            Args = [HeadArg1, HeadArg2 | TailArgs]
        else
            Args = [HeadArg0 | TailArgs]
        )
    ).

expand_bang_state_pairs_in_instance_method(IM0, IM) :-
    IM0 = instance_method(MethodId0, ProcDef0, Context),
    MethodId0 = pred_pf_name_arity(PredOrFunc, MethodSymName, _UserArity0),
    (
        ProcDef0 = instance_proc_def_name(_),
        IM = IM0
    ;
        ProcDef0 = instance_proc_def_clauses(ItemClausesCord0),
        cord.map_pred(expand_bang_state_pairs_in_clause,
            ItemClausesCord0, ItemClausesCord),
        % Note that ItemClausesCord0 should never be empty...
        ( if cord.head(ItemClausesCord, ItemClause) then
            Args = ItemClause ^ cl_head_args,
            PredFormArity = arg_list_arity(Args),
            user_arity_pred_form_arity(PredOrFunc, UserArity, PredFormArity),
            MethodId = pred_pf_name_arity(PredOrFunc, MethodSymName, UserArity)
        else
            MethodId = MethodId0
        ),
        ProcDef = instance_proc_def_clauses(ItemClausesCord),
        IM = instance_method(MethodId, ProcDef, Context)
    ).

:- pred expand_bang_state_pairs_in_clause(item_clause_info::in,
    item_clause_info::out) is det.

expand_bang_state_pairs_in_clause(ItemClause0, ItemClause) :-
    ItemClause0 = item_clause_info(PredOrFunc, SymName, Args0, VarSet,
        MaybeBody, Context, SeqNum),
    expand_bang_state_pairs_in_terms(Args0, Args),
    ItemClause = item_clause_info(PredOrFunc, SymName, Args, VarSet,
        MaybeBody, Context, SeqNum).

%---------------------------------------------------------------------------%
%
% Handle the start of processing a clause.
%

:- type maybe_statevar_arg_pos
    --->    arg_old(uint)
    ;       arg_new(uint)
    ;       non_arg.

:- type new_statevar_map ==
    one_or_more_map(prog_var, maybe_statevar_arg_pos).

:- func init_new_statevar_map = new_statevar_map.

init_new_statevar_map = one_or_more_map.init.

svar_prepare_for_clause_head(ModuleInfo0, QualInfo0, VarSet0,
        Args0, Args, FinalMap, NewSVars, !:SVarState, !:UrInfo) :-
    module_info_get_globals(ModuleInfo0, Globals),
    globals.get_opt_tuple(Globals, OptTuple),
    Threshold = OptTuple ^ ot_from_ground_term_threshold,
    !:SVarState = new_svar_state,
    SVarStore0 = new_svar_store,
    Specs0 = [],
    !:UrInfo = unravel_info(ModuleInfo0, Threshold, QualInfo0, VarSet0,
        SVarStore0, Specs0),
    svar_prepare_head_terms(0u, 1u, Args0, Args, map.init, FinalMap,
        !SVarState, init_new_statevar_map, NewSVars, !UrInfo).

:- pred svar_prepare_head_terms(uint::in, uint::in,
    list(prog_term)::in, list(prog_term)::out,
    map(svar, prog_var)::in, map(svar, prog_var)::out,
    svar_state::in, svar_state::out,
    new_statevar_map::in, new_statevar_map::out,
    unravel_info::in, unravel_info::out) is det.

svar_prepare_head_terms(_, _, [], [],
        !FinalMap, !SVarState, !NewSVars, !UrInfo).
svar_prepare_head_terms(CurDepth, CurArgNum, [Term0 | Terms0], [Term | Terms],
        !FinalMap, !SVarState, !NewSVars, !UrInfo) :-
    svar_prepare_head_term(CurDepth, CurArgNum, Term0, Term,
        !FinalMap, !SVarState, !NewSVars, !UrInfo),
    svar_prepare_head_terms(CurDepth, CurArgNum + 1u, Terms0, Terms,
        !FinalMap, !SVarState, !NewSVars, !UrInfo).

:- pred svar_prepare_head_term(uint::in, uint::in,
    prog_term::in, prog_term::out,
    map(svar, prog_var)::in, map(svar, prog_var)::out,
    svar_state::in, svar_state::out,
    new_statevar_map::in, new_statevar_map::out,
    unravel_info::in, unravel_info::out) is det.

svar_prepare_head_term(CurDepth, CurArgNum, Term0, Term,
        !FinalMap, !SVarState, !NewSVars, !UrInfo) :-
    (
        Term0 = variable(_, _),
        Term = Term0
    ;
        Term0 = functor(Functor, SubTerms0, Context),
        ( if
            Functor = atom("!."),
            SubTerms0 = [variable(StateVar, _)]
        then
            !.SVarState = svar_state(StatusMap0),
            ( if map.search(StatusMap0, StateVar, OldStatus) then
                (
                    OldStatus = status_unknown,
                    % !:S happened to precede !.S in the head, which is ok.
                    new_state_var_instance(StateVar, name_initial, Var,
                        !UrInfo),
                    Term = variable(Var, Context),
                    Status = status_known(Var),
                    map.det_update(StateVar, Status, StatusMap0, StatusMap)
                ;
                    OldStatus = status_known(Var),
                    Term = variable(Var, Context),
                    StatusMap = StatusMap0
                ;
                    OldStatus = status_unknown_updated(_),
                    unexpected($pred, "status_unknown_updated for !.")
                ;
                    OldStatus = status_known_updated(_, _),
                    unexpected($pred, "status_known_updated for !.")
                ;
                    OldStatus = status_known_ro(_, _, _),
                    % This can happen if the context outside a lambda
                    % expression has a state variable named StateVar,
                    % which make_svars_read_only has given this status,
                    % and the lambda expression itself also has !.StateVar.
                    new_state_var_instance(StateVar, name_initial, Var,
                        !UrInfo),
                    Term = variable(Var, Context),
                    Status = status_known(Var),
                    map.det_update(StateVar, Status, StatusMap0, StatusMap)
                )
            else
                new_state_var_instance(StateVar, name_initial, Var, !UrInfo),
                Term = variable(Var, Context),
                Status = status_known(Var),
                map.det_insert(StateVar, Status, StatusMap0, StatusMap)
            ),
            !:SVarState = svar_state(StatusMap),
            ( if CurDepth = 0u then
                MaybeArgPos = arg_old(CurArgNum)
            else
                MaybeArgPos = non_arg
            ),
            one_or_more_map.add(StateVar, MaybeArgPos, !NewSVars)
        else if
            Functor = atom("!:"),
            SubTerms0 = [variable(StateVar, _)]
        then
            new_state_var_instance(StateVar, name_final, Var, !UrInfo),
            Term = variable(Var, Context),
            Status = status_unknown,

            !.SVarState = svar_state(StatusMap0),
            ( if map.search(StatusMap0, StateVar, OldStatus) then
                (
                    OldStatus = status_unknown,
                    % This is the second occurrence of !:StateVar.
                    % Since !.FinalMap will contain StateVar, we will generate
                    % the error message below.
                    StatusMap = StatusMap0
                ;
                    OldStatus = status_known(_),
                    % The !. part of this state var has already been processed.
                    % We have nothing more to do.
                    StatusMap = StatusMap0
                ;
                    OldStatus = status_unknown_updated(_),
                    unexpected($pred, "status_unknown_updated for !:")
                ;
                    OldStatus = status_known_updated(_, _),
                    unexpected($pred, "status_known_updated for !:")
                ;
                    OldStatus = status_known_ro(_, _, _),
                    % This can happen if the context outside a lambda
                    % expression has a state variable named StateVar,
                    % which make_svars_read_only has given this status,
                    % and the lambda expression itself also has !:StateVar.
                    map.det_update(StateVar, Status, StatusMap0, StatusMap)
                )
            else
                map.det_insert(StateVar, Status, StatusMap0, StatusMap)
            ),
            !:SVarState = svar_state(StatusMap),
            map.search_insert(StateVar, Var, MaybeOldVar, !FinalMap),
            (
                MaybeOldVar = yes(_),
                report_repeated_head_state_var(Context, StateVar, !UrInfo)
            ;
                MaybeOldVar = no
            ),
            ( if CurDepth = 0u then
                MaybeArgPos = arg_new(CurArgNum)
            else
                MaybeArgPos = non_arg
            ),
            one_or_more_map.add(StateVar, MaybeArgPos, !NewSVars)
        else
            svar_prepare_head_terms(CurDepth + 1u, 1u, SubTerms0, SubTerms,
                !FinalMap, !SVarState, !NewSVars, !UrInfo),
            Term = functor(Functor, SubTerms, Context)
        )
    ).

%---------------------------------------------------------------------------%
%
% Handle the start of processing a lambda expression.
%

svar_prepare_for_lambda_head(Context, Args0, Args, FinalMap,
        NewSVars, OutsideState, InsideState, !UrInfo) :-
    % Make all currently visible state vars readonly, since they cannot
    % be updated inside the lambda expression.
    %
    % Note that some of these state vars may already be readonly, since
    % we may already be inside e.g. a lambda expression. We must make sure
    % that readonly references work even from code that is inside two or more
    % lambda expressions.
    OutsideState = svar_state(OutsideStatusMap),
    map.to_sorted_assoc_list(OutsideStatusMap, OutsideStatusList),
    make_svars_read_only(roc_lambda, Context,
        OutsideStatusList, InsideStatusList),
    map.from_sorted_assoc_list(InsideStatusList, InsideStatusMap),
    InsideState0 = svar_state(InsideStatusMap),

    % Handle the arguments of the lambda expression as if they were the head
    % of a clause.
    svar_prepare_head_terms(0u, 1u, Args0, Args, map.init, FinalMap,
        InsideState0, InsideState, init_new_statevar_map, NewSVars, !UrInfo).

:- pred make_svars_read_only(readonly_context_kind::in, prog_context::in,
    assoc_list(svar, svar_status)::in, assoc_list(svar, svar_status)::out)
    is det.

make_svars_read_only(_ROC, _Context, [], []).
make_svars_read_only(ROC, Context, [SVar - CurStatus | CurTail], LambdaList) :-
    make_svars_read_only(ROC, Context, CurTail, LambdaTail),
    (
        ( CurStatus = status_unknown
        ; CurStatus = status_unknown_updated(_)
        ),
        LambdaList = LambdaTail
    ;
        CurStatus = status_known_ro(_, _, _),
        LambdaList = [SVar - CurStatus | LambdaTail]
    ;
        ( CurStatus = status_known(Var)
        ; CurStatus = status_known_updated(Var, _)
        ),
        LambdaStatus = status_known_ro(Var, ROC, Context),
        LambdaList = [SVar - LambdaStatus | LambdaTail]
    ).

%---------------------------------------------------------------------------%
%
% Handle the end of processing a clause or lambda expression.
%

svar_finish_clause_body(Context, NewSVars, FinalMap,
        InitialSVarState, FinalSVarState,
        HeadUnificationsGoal, BodyGoal0, Goal,
        StateVarSpecs, UnusedSVarDescs, !UrInfo) :-
    svar_finish_body(Context, FinalMap, [HeadUnificationsGoal, BodyGoal0],
        Goal1, InitialSVarState, FinalSVarState, !UrInfo),
    SVarStore1 = !.UrInfo ^ ui_state_var_store,
    SVarStore1 = svar_store(_, DelayedRenamings, LastIdMap, StateVarSpecs),
    ( if
        map.is_empty(FinalMap),
        map.is_empty(DelayedRenamings)
    then
        Goal = Goal1
    else
        trace [compiletime(flag("state-var-lambda")), io(!IO)] (
            ModuleInfo = !.UrInfo ^ ui_module_info,
            module_info_get_globals(ModuleInfo, Globals),
            module_info_get_name(ModuleInfo, ModuleName),
            get_debug_output_stream(Globals, ModuleName, DebugStream, !IO),
            map.to_assoc_list(FinalMap, FinalList),
            map.to_assoc_list(DelayedRenamings, DelayedList),
            InitialSVarState = svar_state(InitialSVarStateMap),
            map.to_assoc_list(InitialSVarStateMap, InitialSVarStateList),
            FinalSVarState = svar_state(FinalSVarStateMap),
            map.to_assoc_list(FinalSVarStateMap, FinalSVarStateList),

            io.write_string(DebugStream, "\nFINISH CLAUSE BODY in ", !IO),
            io.write_line(DebugStream, Context, !IO),
            io.write_string(DebugStream, "applying subn\n", !IO),
            io.write_line(DebugStream, FinalList, !IO),
            io.write_string(DebugStream, "with incremental subn\n", !IO),
            io.write_line(DebugStream, DelayedList, !IO),
            io.write_string(DebugStream, "with initial svar states\n", !IO),
            io.write_line(DebugStream, InitialSVarStateList, !IO),
            io.write_string(DebugStream, "with final svar states\n", !IO),
            io.write_line(DebugStream, FinalSVarStateList, !IO),
            io.nl(DebugStream, !IO)
        ),
        incremental_rename_vars_in_goal(map.init, DelayedRenamings,
            Goal1, Goal2),
        delete_unneeded_copy_goals_in_clause(HeadUnificationsGoal,
            Goal2, Goal)
    ),
    VarSet = !.UrInfo ^ ui_varset,
    find_unused_statevar_args(VarSet, NewSVars, LastIdMap, UnusedSVarDescs).

svar_finish_lambda_body(Context, Modes, NewSVars, FinalMap, ParseTreeGoal,
        Goals0, Goal, InitialSVarState, FinalSVarState, !UrInfo) :-
    svar_finish_body(Context, FinalMap, Goals0, Goal,
        InitialSVarState, FinalSVarState, !UrInfo),
    VarSet = !.UrInfo ^ ui_varset,
    LastIdMap = !.UrInfo ^ ui_state_var_store ^ store_last_id_map,
    find_unused_statevar_args(VarSet, NewSVars, LastIdMap, UnusedSVarDescs),
    report_any_unneeded_svars_in_lambda(Context, Modes, ParseTreeGoal, Goal,
        UnusedSVarDescs, !UrInfo).

:- pred svar_finish_body(prog_context::in, map(svar, prog_var)::in,
    list(hlds_goal)::in, hlds_goal::out, svar_state::in, svar_state::in,
    unravel_info::in, unravel_info::out) is det.

svar_finish_body(Context, FinalMap, Goals0, Goal,
        InitialSVarState, FinalSVarState, !UrInfo) :-
    map.to_assoc_list(FinalMap, FinalAssocList),
    InitialSVarState = svar_state(InitialSVarStatusMap),
    FinalSVarState = svar_state(FinalSVarStatusMap),
    svar_find_final_renames_and_copy_goals(FinalAssocList,
        InitialSVarStatusMap, FinalSVarStatusMap,
        [], FinalSVarSubn, [], CopyGoals),
    (
        CopyGoals = [],
        Goals1 = Goals0
    ;
        CopyGoals = [_ | _],
        Goals1 = Goals0 ++ CopyGoals
    ),
    svar_flatten_conj(Context, Goals1, Goal1, !UrInfo),

    Goal1 = hlds_goal(GoalExpr1, GoalInfo1),
    GoalId1 = goal_info_get_goal_id(GoalInfo1),
    SVarStore1 = !.UrInfo ^ ui_state_var_store,
    SVarStore1 = svar_store(NextGoalId1, DelayedRenamingMap1,
        LastIdMap1, StateVarSpecs1),
    ( if map.search(DelayedRenamingMap1, GoalId1, DelayedRenaming0) then
        trace [compiletime(flag("state-var-lambda")), io(!IO)] (
            ModuleInfo = !.UrInfo ^ ui_module_info,
            module_info_get_globals(ModuleInfo, Globals),
            module_info_get_name(ModuleInfo, ModuleName),
            get_debug_output_stream(Globals, ModuleName, DebugStream, !IO),
            io.write_string(DebugStream, "\nfinishing body, ", !IO),
            io.write_string(DebugStream,
                "attaching subn to existing goal_id ", !IO),
            io.write_line(DebugStream, GoalId1, !IO),
            io.write_string(DebugStream, "subn is ", !IO),
            io.write_line(DebugStream, FinalSVarSubn, !IO)
        ),

        map.det_update(GoalId1, DelayedRenaming0 ++ FinalSVarSubn,
        DelayedRenamingMap1, DelayedRenamingMap),
        NextGoalId = NextGoalId1,
        Goal = Goal1
    else
        (
            FinalSVarSubn = [],
            NextGoalId = NextGoalId1,
            DelayedRenamingMap = DelayedRenamingMap1,
            Goal = Goal1
        ;
            FinalSVarSubn = [_ | _],
            counter.uallocate(GoalIdNum, NextGoalId1, NextGoalId),
            GoalId = goal_id(GoalIdNum),

            trace [compiletime(flag("state-var-lambda")), io(!IO)] (
                ModuleInfo = !.UrInfo ^ ui_module_info,
                module_info_get_globals(ModuleInfo, Globals),
                module_info_get_name(ModuleInfo, ModuleName),
                get_debug_output_stream(Globals, ModuleName, DebugStream, !IO),
                io.write_string(DebugStream, "\nfinishing body, ", !IO),
                io.write_string(DebugStream,
                    "attaching subn to new goal_id ", !IO),
                io.write_line(DebugStream, GoalId, !IO),
                io.write_string(DebugStream, "subn is ", !IO),
                io.write_line(DebugStream, FinalSVarSubn, !IO)
            ),

            map.det_insert(GoalId, FinalSVarSubn,
                DelayedRenamingMap1, DelayedRenamingMap),
            goal_info_set_goal_id(GoalId, GoalInfo1, GoalInfo),
            Goal = hlds_goal(GoalExpr1, GoalInfo)
        )
    ),
    SVarStore = svar_store(NextGoalId, DelayedRenamingMap,
        LastIdMap1, StateVarSpecs1),
    !UrInfo ^ ui_state_var_store := SVarStore.

:- pred svar_find_final_renames_and_copy_goals(assoc_list(svar, prog_var)::in,
    map(svar, svar_status)::in, map(svar, svar_status)::in,
    assoc_list(prog_var, prog_var)::in, assoc_list(prog_var, prog_var)::out,
    list(hlds_goal)::in, list(hlds_goal)::out) is det.

svar_find_final_renames_and_copy_goals([], _, _, !FinalSVarSubn, !CopyGoals).
svar_find_final_renames_and_copy_goals([Head | Tail],
        InitialStatusMap, FinalStatusMap, !FinalSVarSubn, !CopyGoals) :-
    Head = SVar - FinalHeadVar,
    map.lookup(InitialStatusMap, SVar, InitialStatus),
    map.lookup(FinalStatusMap, SVar, FinalStatus),
    (
        FinalStatus = status_known(LastVar),
        ( if FinalStatus = InitialStatus then
            % The state variable was not updated by the body.
            % Leaving the unification between two headvars representing the
            % initial and final states to the done at the start of the clause
            % causes problems at the moment for the mode checker in the
            % presence of unique modes.
            make_copy_goal(LastVar, FinalHeadVar, CopyGoal),
            !:CopyGoals = [CopyGoal | !.CopyGoals]
        else
            !:FinalSVarSubn = [LastVar - FinalHeadVar | !.FinalSVarSubn]
        )
    ;
        FinalStatus = status_unknown
        % The state variable was never defined.
        % The clause head already refers to the final version.
    ;
        FinalStatus = status_known_ro(_, _, _),
        unexpected($pred, "readonly status")
    ;
        ( FinalStatus = status_known_updated(_, _)
        ; FinalStatus = status_unknown_updated(_)
        ),
        unexpected($pred, "updated status")
    ),
    svar_find_final_renames_and_copy_goals(Tail,
        InitialStatusMap, FinalStatusMap, !FinalSVarSubn, !CopyGoals).

%---------------------------------------------------------------------------%
%
% Handle the completion of an atomic goal. Any variable that was updated in the
% goal gets the updated value as its new current value. The Loc argument is
% needed because sometimes what looks like an atomic goal (such as the
% condition of an if-then-else) is inside another atomic goal (such as an
% if-then-else expression). In such cases, the end of the inside atomic goal
% does NOT mean that we finished the containing atomic goal.
%

svar_finish_atomic_goal(Loc, !SVarState) :-
    (
        Loc = loc_whole_goal,
        !.SVarState = svar_state(StatusMap0),
        map.map_values_only(reset_updated_status, StatusMap0, StatusMap),
        !:SVarState = svar_state(StatusMap)
    ;
        Loc = loc_inside_atomic_goal
    ).

:- pred reset_updated_status(svar_status::in, svar_status::out) is det.

reset_updated_status(!Status) :-
    (
        ( !.Status = status_unknown
        ; !.Status = status_known_ro(_, _, _)
        ; !.Status = status_known(_)
        )
    ;
        !.Status = status_unknown_updated(NewProgVar),
        !:Status = status_known(NewProgVar)
    ;
        !.Status = status_known_updated(_OldProgVar, NewProgVar),
        !:Status = status_known(NewProgVar)
    ).

%---------------------------------------------------------------------------%
%
% Handle scopes that introduce state variables.
%

svar_prepare_for_local_state_vars(Context, StateVars,
        OutsideState, InsideState, !UrInfo) :-
    OutsideState = svar_state(StatusMapOutside),
    prepare_svars_for_scope(Context, StateVars,
        StatusMapOutside, StatusMapInside, !UrInfo),
    InsideState = svar_state(StatusMapInside).

:- pred prepare_svars_for_scope(prog_context::in, list(svar)::in,
    map(svar, svar_status)::in, map(svar, svar_status)::out,
    unravel_info::in, unravel_info::out) is det.

prepare_svars_for_scope(_Context, [], !StatusMap, !UrInfo).
prepare_svars_for_scope(Context, [SVar | SVars],
        !StatusMap, !UrInfo) :-
    ( if map.search(!.StatusMap, SVar, _OldStatus) then
        report_state_var_shadow(Context, SVar, !UrInfo),
        map.det_update(SVar, status_unknown, !StatusMap)
    else
        map.det_insert(SVar, status_unknown, !StatusMap)
    ),
    prepare_svars_for_scope(Context, SVars, !StatusMap, !UrInfo).

svar_finish_local_state_vars(UrInfo, StateVars,
        StateBeforeOutside, StateAfterInside, StateAfterOutside) :-
    StateBeforeOutside = svar_state(StatusMapBeforeOutside),
    StateAfterInside = svar_state(StatusMapAfterInside),
    trace [compiletime(flag("state-var-scope")), io(!IO)] (
        ModuleInfo = UrInfo ^ ui_module_info,
        module_info_get_globals(ModuleInfo, Globals),
        module_info_get_name(ModuleInfo, ModuleName),
        get_debug_output_stream(Globals, ModuleName, DebugStream, !IO),
        map.to_assoc_list(StatusMapBeforeOutside, BeforeOutsideStatuses),
        map.to_assoc_list(StatusMapAfterInside, AfterInsideStatuses),
        io.write_string(DebugStream, "Finish of scope\n", !IO),
        io.write_string(DebugStream, "quantified state vars\n", !IO),
        io.write_line(DebugStream, StateVars, !IO),
        io.write_string(DebugStream, "status before outside\n", !IO),
        list.foldl(io.write_line(DebugStream), BeforeOutsideStatuses, !IO),
        io.write_string(DebugStream, "status after inside\n", !IO),
        list.foldl(io.write_line(DebugStream), AfterInsideStatuses, !IO)
    ),
    % Remove access to the state vars introduced in the scope.
    % Leave the status of all other state vars unaffected.
    StatusMapAfterOutside0 = StatusMapAfterInside,
    finish_svars_for_scope(StateVars, StatusMapBeforeOutside,
        StatusMapAfterOutside0, StatusMapAfterOutside),
    StateAfterOutside = svar_state(StatusMapAfterOutside).

:- pred finish_svars_for_scope(list(svar)::in, map(svar, svar_status)::in,
    map(svar, svar_status)::in, map(svar, svar_status)::out) is det.

finish_svars_for_scope([], _, !StatusMapAfterOutside).
finish_svars_for_scope([SVar | SVars], StatusMapBeforeOutside,
        !StatusMapAfterOutside) :-
    ( if map.search(StatusMapBeforeOutside, SVar, BeforeOutsideStatus) then
        % The state var was visible before the scope. The outside state var
        % was shadowed by a state var in the scope. Now that we are leaving
        % the scope, restore access to the outside state var. Due to the
        % shadowing, its status couldn't have changed inside the scope.
        map.det_update(SVar, BeforeOutsideStatus, !StatusMapAfterOutside)
    else
        % The state var introduced in the scope wasn't visible before it.
        map.det_remove(SVar, _, !StatusMapAfterOutside)
    ),
    finish_svars_for_scope(SVars, StatusMapBeforeOutside,
        !StatusMapAfterOutside).

%---------------------------------------------------------------------------%
%
% Handle disjunctions. The algorithm we use has two passes over the disjuncts.
%
% - Pass 1 finds out, for each state variable known at the start of the
%   disjunction, whether it was updated by any arms, and if yes, it picks
%   the final prog_var from one of the updated arms to represent the state var
%   after the disjunction.
%
% - Pass 2 processes the arms to ensure that the picked prog_var represents
%   the final value of the state variable in all the arms. In arms that do not
%   update the state variable, it introduces unifications to copy the initial
%   value of the state var to be the final value. In arms that do update the
%   state var, it schedules the prog_var representing the final value in
%   that arm to be renamed to the picked prog_var.

svar_finish_disjunction(DisjStates, Disjs, StateBefore, StateAfter, !UrInfo) :-
    StateBefore = svar_state(StatusMapBefore),
    ( if map.is_empty(StatusMapBefore) then
        % Optimize the common case.
        get_disjuncts_with_empty_states(DisjStates, [], RevDisjs),
        list.reverse(RevDisjs, Disjs),
        StateAfter = StateBefore
    else
        map.to_sorted_assoc_list(StatusMapBefore, StatusListBefore),
        compute_status_after_arms(StatusListBefore, DisjStates,
            map.init, ChangedStatusMapAfter, StatusMapBefore, StatusMapAfter),
        map.to_sorted_assoc_list(ChangedStatusMapAfter,
            ChangedStatusListAfter),
        StateAfter = svar_state(StatusMapAfter),

        VarSet0 = !.UrInfo ^ ui_varset,
        SVarStore0 = !.UrInfo ^ ui_state_var_store,
        SVarStore0 = svar_store(NextGoalId0, DelayedRenamings0,
            LastIdMap0, SVarSpecs0),
        merge_changes_made_by_arms(VarSet0, DisjStates, StatusMapBefore,
            ChangedStatusListAfter, [], RevDisjs,
            NextGoalId0, NextGoalId, DelayedRenamings0, DelayedRenamings,
            SVarSpecs0, SVarSpecs),
        list.reverse(RevDisjs, Disjs),
        SVarStore = svar_store(NextGoalId, DelayedRenamings,
            LastIdMap0, SVarSpecs),
        !UrInfo ^ ui_state_var_store := SVarStore
    ).

:- pred get_disjuncts_with_empty_states(list(hlds_goal_svar_state)::in,
    list(hlds_goal)::in, list(hlds_goal)::out) is det.

get_disjuncts_with_empty_states([], !RevDisjuncts).
get_disjuncts_with_empty_states([GoalState | GoalStates], !RevDisjuncts) :-
    GoalState = hlds_goal_svar_state(Goal, State),
    StatusMapAfterGoal = State ^ state_status_map,
    expect(map.is_empty(StatusMapAfterGoal), $pred,
        "map after goal not empty"),
    !:RevDisjuncts = [Goal | !.RevDisjuncts],
    get_disjuncts_with_empty_states(GoalStates, !RevDisjuncts).

    % Pass 1. Compute the changes in the status map.
    %
:- pred compute_status_after_arms(assoc_list(svar, svar_status)::in,
    list(hlds_goal_svar_state)::in,
    map(svar, svar_status)::in, map(svar, svar_status)::out,
    map(svar, svar_status)::in, map(svar, svar_status)::out) is det.

compute_status_after_arms(_StatusListBefore, [],
        !ChangedStatusMapAfter, !StatusMapAfter).
compute_status_after_arms(StatusListBefore, [ArmState | ArmStates],
        !ChangedStatusMapAfter, !StatusMapAfter) :-
    ArmState = hlds_goal_svar_state(_Armunct, StateAfterArm),
    StatusMapAfterArm = StateAfterArm ^ state_status_map,
    find_changes_in_arm_and_update_changed_status_map(StatusListBefore,
        StatusMapAfterArm, !ChangedStatusMapAfter, !StatusMapAfter),
    compute_status_after_arms(StatusListBefore, ArmStates,
        !ChangedStatusMapAfter, !StatusMapAfter).

:- pred find_changes_in_arm_and_update_changed_status_map(
    assoc_list(svar, svar_status)::in, map(svar, svar_status)::in,
    map(svar, svar_status)::in, map(svar, svar_status)::out,
    map(svar, svar_status)::in, map(svar, svar_status)::out) is det.

find_changes_in_arm_and_update_changed_status_map([], _,
        !ChangedStatusMapAfter, !StatusMapAfter).
find_changes_in_arm_and_update_changed_status_map([Before | Befores],
        StatusMapAfterArm, !ChangedStatusMapAfter, !StatusMapAfter) :-
    Before = SVar - StatusBefore,
    map.lookup(StatusMapAfterArm, SVar, StatusAfter),
    ( if StatusBefore = StatusAfter then
        true
    else
        ( if map.search(!.ChangedStatusMapAfter, SVar, _AlreadyUpdated) then
            true
        else
            map.det_insert(SVar, StatusAfter, !ChangedStatusMapAfter),
            map.det_update(SVar, StatusAfter, !StatusMapAfter)
        )
    ),
    find_changes_in_arm_and_update_changed_status_map(Befores,
        StatusMapAfterArm, !ChangedStatusMapAfter, !StatusMapAfter).

    % Pass 2. Effect the computed changes in the status map.
    %
:- pred merge_changes_made_by_arms(prog_varset::in,
    list(hlds_goal_svar_state)::in,
    map(svar, svar_status)::in, assoc_list(svar, svar_status)::in,
    list(hlds_goal)::in, list(hlds_goal)::out,
    ucounter::in, ucounter::out,
    incremental_rename_map::in, incremental_rename_map::out,
    list(error_spec)::in, list(error_spec)::out) is det.

merge_changes_made_by_arms(_, [], _, _,
        !RevArms, !NextGoalId, !DelayedRenamings, !Specs).
merge_changes_made_by_arms(VarSet, [ArmState | ArmStates],
        StatusMapBefore, ChangedStatusListAfter,
        !RevArms, !NextGoalId, !DelayedRenamings, !Specs) :-
    ArmState = hlds_goal_svar_state(Arm0, StateAfterArm),
    StatusMapAfterArm = StateAfterArm ^ state_status_map,
    counter.uallocate(ArmIdNum, !NextGoalId),
    ArmId = goal_id(ArmIdNum),
    handle_arm_updated_state_vars(VarSet,
        ChangedStatusListAfter, StatusMapBefore, StatusMapAfterArm,
        UninitVarNames, CopyGoals, ArmRenames),
    map.det_insert(ArmId, ArmRenames, !DelayedRenamings),
    Arm0 = hlds_goal(ArmExpr0, ArmInfo0),
    (
        CopyGoals = [],
        ArmExpr = ArmExpr0
    ;
        CopyGoals = [_ | _],
        svar_goal_to_conj_list_internal(Arm0, ArmGoals0,
            !NextGoalId, !DelayedRenamings),
        ArmExpr = conj(plain_conj, ArmGoals0 ++ CopyGoals)
    ),
    (
        UninitVarNames = []
    ;
        UninitVarNames = [_ | _],
        % It is ok for an arm that cannot succeed not to initialize
        % a variable, but we record an informational message anyway,
        % to be printed in case the procedure has a mode error.
        ArmContext = goal_info_get_context(ArmInfo0),
        report_missing_inits_in_disjunct(ArmContext, UninitVarNames, !Specs)
    ),
    goal_info_set_goal_id(ArmId, ArmInfo0, ArmInfo),
    Arm = hlds_goal(ArmExpr, ArmInfo),
    !:RevArms = [Arm | !.RevArms],
    merge_changes_made_by_arms(VarSet, ArmStates,
        StatusMapBefore, ChangedStatusListAfter,
        !RevArms, !NextGoalId, !DelayedRenamings, !Specs).

:- pred handle_arm_updated_state_vars(prog_varset::in,
    assoc_list(svar, svar_status)::in,
    map(svar, svar_status)::in, map(svar, svar_status)::in, list(string)::out,
    list(hlds_goal)::out, assoc_list(prog_var, prog_var)::out) is det.

handle_arm_updated_state_vars(_, [], _, _, [], [], []).
handle_arm_updated_state_vars(VarSet, [Change | Changes], StatusMapBefore,
        StatusMapAfterArm, UninitVarNames, CopyGoals, Renames) :-
    handle_arm_updated_state_vars(VarSet, Changes,
        StatusMapBefore, StatusMapAfterArm,
        UninitVarNamesTail, CopyGoalsTail, RenamesTail),
    Change = StateVar - AfterAllArmsStatus,
    map.lookup(StatusMapBefore, StateVar, BeforeStatus),
    map.lookup(StatusMapAfterArm, StateVar, AfterArmStatus),
    ( if AfterArmStatus = BeforeStatus then
        expect_not(unify(AfterArmStatus, AfterAllArmsStatus),
            $pred, "AfterArmStatus = AfterAllArmsStatus"),
        (
            % If the state var is readonly in this context, then it shouldn't
            % have been updated by any arms. However, if it was, then we have
            % (a) already generated an error message for it, and (b) changed
            % its status to writeable to suppress duplicate error messages.
            % This is why this code treats known_ro the same as known.
            ( BeforeStatus = status_known(BeforeVar)
            ; BeforeStatus = status_known_ro(BeforeVar, _, _)
            ),
            (
                AfterAllArmsStatus = status_known(AfterAllVar),
                make_copy_goal(BeforeVar, AfterAllVar, CopyGoal),
                CopyGoals = [CopyGoal | CopyGoalsTail],
                UninitVarNames = UninitVarNamesTail,
                Renames = RenamesTail
            ;
                ( AfterAllArmsStatus = status_known_ro(_, _, _)
                ; AfterAllArmsStatus = status_known_updated(_, _)
                ; AfterAllArmsStatus = status_unknown
                ; AfterAllArmsStatus = status_unknown_updated(_)
                ),
                unexpected($pred,
                    "AfterAllArmsStatus != status_known (Before == After)")
            )
        ;
            BeforeStatus = status_unknown,
            varset.lookup_name(VarSet, StateVar, Name),
            UninitVarName = "!:" ++ Name,
            CopyGoals = CopyGoalsTail,
            UninitVarNames = [UninitVarName | UninitVarNamesTail],
            Renames = RenamesTail
        ;
            ( BeforeStatus = status_known_updated(_, _)
            ; BeforeStatus = status_unknown_updated(_)
            ),
            % If the state var was updated before this disjunction,
            % then any reference to !:StateVar should refer to the already
            % known updated prog_var, and thus AfterAllArmsStatus should be
            % the same as StatusBefore, which means we shouldn't get here.
            unexpected($pred, "BeforeStatus is updated")
        )
    else
        (
            AfterArmStatus = status_known(AfterArmVar),
            (
                AfterAllArmsStatus = status_known(AfterAllVar),
                CopyGoals = CopyGoalsTail,
                UninitVarNames = UninitVarNamesTail,
                ( if AfterArmVar = AfterAllVar then
                    Renames = RenamesTail
                else
                    Renames = [AfterArmVar - AfterAllVar | RenamesTail]
                )
            ;
                ( AfterAllArmsStatus = status_known_ro(_, _, _)
                ; AfterAllArmsStatus = status_known_updated(_, _)
                ; AfterAllArmsStatus = status_unknown
                ; AfterAllArmsStatus = status_unknown_updated(_)
                ),
                unexpected($pred,
                    "AfterAllArmsStatus != status_known (Before != After)")
            )
        ;
            AfterArmStatus = status_known_ro(_, _, _),
            unexpected($pred, "AfterArmStatus = status_known_ro")
        ;
            AfterArmStatus = status_known_updated(_, _),
            unexpected($pred, "AfterArmStatus = status_known_updated")
        ;
            AfterArmStatus = status_unknown,
            unexpected($pred, "AfterArmStatus = status_unknown")
        ;
            AfterArmStatus = status_unknown_updated(_),
            unexpected($pred, "AfterArmStatus = status_unknown")
        )
    ).

:- pred make_copy_goal(prog_var::in, prog_var::in, hlds_goal::out) is det.

make_copy_goal(FromVar, ToVar, CopyGoal) :-
    % We can do the copying in one of two ways. Using unifications
    % can cause problems because the (plain, non-unique) mode analysis pass
    % feels free to schedule them in places where the unique mode analysis pass
    % does not like them; specifically, it can cause a di reference to a
    % variable to appear before a ui reference.
    %
    % The alternative is to add a builtin predicate to the standard library
    % that just does copying, and to make make_copy_goal construct a call to
    % that predicate. That predicate would need to be able to be called in
    % three modes: di/uo, mdi/muo and in/out. However, it needs to have inst
    % parameters so that whatever shape information we have about the source
    % (subtype info, higher order mode info), we copy to the target.
    %
    % We generate a unification, and try to ensure that we don't generate
    % di references to state variables before possible ui references. See the
    % comment in svar_find_final_renames_and_copy_goals before the call to
    % make_copy_goal.

    create_pure_atomic_complicated_unification(ToVar, rhs_var(FromVar),
        dummy_context, umc_implicit("state variable"), [], CopyGoal0),
    goal_add_features([feature_do_not_warn_singleton, feature_state_var_copy],
        CopyGoal0, CopyGoal).

%---------------------------------------------------------------------------%
%
% Handle if-then-else goals. The basic idea is the same as for disjunctions,
% but we also have to handle three complications.
%
% First, the first disjunct consists of two parts: the condition and the then
% part, with data flowing between them.
%
% Second, variables can be quantified over the condition and the then part.
%
% Third, the if-then-else need not be a goal; it can also be an expression.
% This means that it is ok for variables to have status known_updated or
% unknown_updated in any of the status maps we handle.
%

svar_finish_if_then_else(LocKind, Context, QuantStateVars,
        ThenGoal0, ThenGoal, ElseGoal0, ElseGoal,
        StateBefore, StateAfterCond, StateAfterThen, StateAfterElse,
        StateAfterITE, !UrInfo) :-
    StateBefore = svar_state(StatusMapBefore),
    StatusMapAfterCond = StateAfterCond ^ state_status_map,
    StatusMapAfterThen = StateAfterThen ^ state_status_map,
    StatusMapAfterElse = StateAfterElse ^ state_status_map,
    map.keys(StatusMapBefore, SVarsBefore),
    map.keys(StatusMapAfterCond, SVarsAfterCond),
    map.keys(StatusMapAfterThen, SVarsAfterThen),
    map.keys(StatusMapAfterElse, SVarsAfterElse),
    expect(list.sublist(SVarsBefore, SVarsAfterCond), $pred,
        "vars Before not sublist of Cond"),
    expect(unify(SVarsBefore, SVarsAfterThen), $pred,
        "vars Before != AfterThen"),
    expect(unify(SVarsBefore, SVarsAfterElse), $pred,
        "vars Before != AfterElse"),

    handle_state_vars_in_ite(LocKind, QuantStateVars,
        SVarsBefore, StatusMapBefore, StatusMapAfterCond,
        StatusMapAfterThen, StatusMapAfterElse,
        map.init, StatusMapAfterITE,
        [], NeckCopyGoals, [], ThenEndCopyGoals, [], ElseEndCopyGoals,
        [], ThenRenames, [], ElseRenames,
        [], ThenMissingInits, [], ElseMissingInits, !UrInfo),
    StateAfterITE = svar_state(StatusMapAfterITE),

    % It is ok for an arm that cannot succeed not to initialize a variable,
    % but we record warnings for them anyway, to be printed in case the
    % procedure has a mode error.
    (
        ThenMissingInits = []
    ;
        ThenMissingInits = [_ | _],
        report_missing_inits_in_ite(Context, ThenMissingInits,
            "succeeds", "fails", !UrInfo)
    ),
    (
        ElseMissingInits = []
    ;
        ElseMissingInits = [_ | _],
        report_missing_inits_in_ite(Context, ElseMissingInits,
            "fails", "succeeds", !UrInfo)
    ),

    svar_goal_to_conj_list(ThenGoal0, ThenGoals0, !UrInfo),
    svar_goal_to_conj_list(ElseGoal0, ElseGoals0, !UrInfo),
    ThenGoals = NeckCopyGoals ++ ThenGoals0 ++ ThenEndCopyGoals,
    ElseGoals = ElseGoals0 ++ ElseEndCopyGoals,
    ThenGoal0 = hlds_goal(_ThenExpr0, ThenInfo0),
    ElseGoal0 = hlds_goal(_ElseExpr0, ElseInfo0),
    conj_list_to_goal(ThenGoals, ThenInfo0, ThenGoal1),
    conj_list_to_goal(ElseGoals, ElseInfo0, ElseGoal1),

    SVarStore0 = !.UrInfo ^ ui_state_var_store,
    SVarStore0 = svar_store(NextGoalId0, DelayedRenamings0,
        LastIdMap0, SVarSpecs0),
    counter.uallocate(ThenGoalIdNum, NextGoalId0, NextGoalId1),
    counter.uallocate(ElseGoalIdNum, NextGoalId1, NextGoalId),
    ThenGoalId = goal_id(ThenGoalIdNum),
    ElseGoalId = goal_id(ElseGoalIdNum),
    goal_set_goal_id(ThenGoalId, ThenGoal1, ThenGoal),
    goal_set_goal_id(ElseGoalId, ElseGoal1, ElseGoal),
    map.det_insert(ThenGoalId, ThenRenames,
        DelayedRenamings0, DelayedRenamings1),
    map.det_insert(ElseGoalId, ElseRenames,
        DelayedRenamings1, DelayedRenamings),
    SVarStore = svar_store(NextGoalId, DelayedRenamings,
        LastIdMap0, SVarSpecs0),
    !UrInfo ^ ui_state_var_store := SVarStore.

:- pred handle_state_vars_in_ite(loc_kind::in, list(svar)::in, list(svar)::in,
    map(svar, svar_status)::in, map(svar, svar_status)::in,
    map(svar, svar_status)::in, map(svar, svar_status)::in,
    map(svar, svar_status)::in, map(svar, svar_status)::out,
    list(hlds_goal)::in, list(hlds_goal)::out,
    list(hlds_goal)::in, list(hlds_goal)::out,
    list(hlds_goal)::in, list(hlds_goal)::out,
    assoc_list(prog_var, prog_var)::in, assoc_list(prog_var, prog_var)::out,
    assoc_list(prog_var, prog_var)::in, assoc_list(prog_var, prog_var)::out,
    list(string)::in, list(string)::out, list(string)::in, list(string)::out,
    unravel_info::in, unravel_info::out) is det.

handle_state_vars_in_ite(_, _, [], _, _, _, _, !StatusMapAfterITE,
        !NeckCopyGoals, !ThenEndCopyGoals, !ElseEndCopyGoals,
        !ThenRenames, !ElseRenames, !ThenMissingInits, !ElseMissingInits,
        !UrInfo).
handle_state_vars_in_ite(LocKind, QuantStateVars, [SVar | SVars],
        StatusMapBefore, StatusMapAfterCond,
        StatusMapAfterThen, StatusMapAfterElse, !StatusMapAfterITE,
        !NeckCopyGoals, !ThenEndCopyGoals, !ElseEndCopyGoals,
        !ThenRenames, !ElseRenames, !ThenMissingInits, !ElseMissingInits,
        !UrInfo) :-
    map.lookup(StatusMapBefore, SVar, StatusBefore),
    map.lookup(StatusMapAfterCond, SVar, StatusAfterCond),
    map.lookup(StatusMapAfterThen, SVar, StatusAfterThen),
    map.lookup(StatusMapAfterElse, SVar, StatusAfterElse),

    ( if list.member(SVar, QuantStateVars) then
        expect(unify(StatusBefore, StatusAfterThen), $pred,
            "state var shadowed in if-then-else is nevertheless updated"),
        % SVar is quantified in the if-then-else. That means that Cond and Then
        % may update a state variable with the same name as SVar, but this
        % won't be SVar itself. The status of SVar itself after Cond and after
        % Then will thus be unchanged. This is why we pass StatusBefore
        % not just for itself, but in place of StatusAfterCond and
        % StatusAfterThen as well.
        handle_state_var_in_ite(LocKind, SVar,
            StatusBefore, StatusBefore, StatusBefore,
            StatusAfterElse, StatusAfterITE,
            !NeckCopyGoals, !ThenEndCopyGoals, !ElseEndCopyGoals,
            !ElseRenames, !ThenMissingInits, !ElseMissingInits, !UrInfo)
    else
        % If StatusBefore = status_known_ro(_, _, _), then we would expect
        % StatusBefore = StatusAfterCond
        % StatusBefore = StatusAfterThen
        % StatusBefore = StatusAfterElse
        % However, if the user program actually updates a state variable
        % that should be readonly in this scope, then our recovery from that
        % error would invalidate these expectations.

        handle_state_var_in_ite(LocKind, SVar,
            StatusBefore, StatusAfterCond, StatusAfterThen,
            StatusAfterElse, StatusAfterITE,
            !NeckCopyGoals, !ThenEndCopyGoals, !ElseEndCopyGoals,
            !ElseRenames, !ThenMissingInits, !ElseMissingInits, !UrInfo)
    ),
    map.det_insert(SVar, StatusAfterITE, !StatusMapAfterITE),
    handle_state_vars_in_ite(LocKind, QuantStateVars, SVars,
        StatusMapBefore, StatusMapAfterCond,
        StatusMapAfterThen, StatusMapAfterElse, !StatusMapAfterITE,
        !NeckCopyGoals, !ThenEndCopyGoals, !ElseEndCopyGoals,
        !ThenRenames, !ElseRenames, !ThenMissingInits, !ElseMissingInits,
        !UrInfo).

:- pred handle_state_var_in_ite(loc_kind::in, svar::in,
    svar_status::in, svar_status::in, svar_status::in, svar_status::in,
    svar_status::out,
    list(hlds_goal)::in, list(hlds_goal)::out,
    list(hlds_goal)::in, list(hlds_goal)::out,
    list(hlds_goal)::in, list(hlds_goal)::out,
    assoc_list(prog_var, prog_var)::in, assoc_list(prog_var, prog_var)::out,
    list(string)::in, list(string)::out, list(string)::in, list(string)::out,
    unravel_info::in, unravel_info::out) is det.

handle_state_var_in_ite(LocKind, SVar, StatusBefore,
        StatusAfterCond, StatusAfterThen, StatusAfterElse, StatusAfterITE,
        !NeckCopyGoals, !ThenEndCopyGoals, !ElseEndCopyGoals,
        !ElseRenames, !ThenMissingInits, !ElseMissingInits, !UrInfo) :-
    trace [compiletime(flag("state-var-ite")), io(!IO)] (
        ModuleInfo = !.UrInfo ^ ui_module_info,
        module_info_get_globals(ModuleInfo, Globals),
        module_info_get_name(ModuleInfo, ModuleName),
        get_debug_output_stream(Globals, ModuleName, DebugStream, !IO),
        io.write_string(DebugStream, "state variable ", !IO),
        io.write_line(DebugStream, SVar, !IO),
        io.write_string(DebugStream, "status before: ", !IO),
        io.write_line(DebugStream, StatusBefore, !IO),
        io.write_string(DebugStream, "status after cond: ", !IO),
        io.write_line(DebugStream, StatusAfterCond, !IO),
        io.write_string(DebugStream, "status after then: ", !IO),
        io.write_line(DebugStream, StatusAfterThen, !IO),
        io.write_string(DebugStream, "status after else: ", !IO),
        io.write_line(DebugStream, StatusAfterElse, !IO)
    ),

    % There are eight cases depending on which of Then, Else and Cond
    % update the state variable:
    %
    % #  Then Else  Cond Action
    % A1 no   no    no   do nothing
    % A2 no   no    yes  copy from cond at start of then, copy at end of else
    % B1 no   yes   no   copy at end of then
    % B2 no   yes   yes  copy from cond at start of then
    % C1 yes  no    no   copy at end of else
    % C2 yes  no    yes  copy at end of else
    % D1 yes  yes   no   rename else to match then
    % D2 yes  yes   yes  rename else to match then
    %
    % The nesting structure is:
    %
    % - Did Then update SVar? (Is StatusAfterThen = StatusAfterCond?)
    % - Did Else update SVar? (Is StatusAfterElse = StatusBefore?)
    % - did Cond update SVar? (Is StatusAfterCond = StatusBefore?)
    %
    % Note that if Then updates SVar, as it does in these cases, then
    % it does not matter whether Cond also updates SVar, since its final state
    % will not depend on that intermediate state. This is why we do not
    % differentiate between C1 and C2, and between D1 and D2. And this
    % in turn is the reason for why we test for updates by Then (and Else)
    % before we test for updates by Cond.

    ( if StatusAfterThen = StatusAfterCond then
        % Cases A-B.
        ( if StatusAfterElse = StatusBefore then
            % Case A.
            ( if StatusAfterCond = StatusBefore then
                % Case A1.
                StatusAfterITE = StatusBefore
            else
                % Case A2.
                handle_state_var_in_ite_cond(LocKind, SVar,
                    StatusBefore, StatusAfterCond, StatusAfterITE,
                    !NeckCopyGoals, !ElseEndCopyGoals, !ElseMissingInits,
                    !UrInfo)
            )
        else
            % Case B.
            ( if StatusAfterCond = StatusBefore then
                % Case B1.
                handle_state_var_in_ite_else(!.UrInfo, LocKind, SVar,
                    StatusBefore, StatusAfterElse, StatusAfterITE,
                    !ThenEndCopyGoals, !ThenMissingInits)
            else
                % Case B2.
                handle_state_var_in_ite_cond_else(LocKind,
                    StatusAfterCond, StatusAfterElse, StatusAfterITE,
                    !NeckCopyGoals)
            )
        )
    else
        % Cases C-D.
        ( if StatusAfterElse = StatusBefore then
            % Case C. We handle C1 and C2 the same way.
            handle_state_var_in_ite_maybe_cond_then(!.UrInfo, LocKind,
                SVar, StatusBefore, StatusAfterThen, StatusAfterITE,
                !ElseEndCopyGoals, !ElseMissingInits)
        else
            % Case D. We handle D1 and D2 the same way.
            handle_state_var_in_ite_maybe_cond_then_else(LocKind,
                StatusAfterThen, StatusAfterElse, StatusAfterITE, !ElseRenames)
        )
    ).

:- pred handle_state_var_in_ite_cond(loc_kind::in, svar::in,
    svar_status::in, svar_status::in, svar_status::out,
    list(hlds_goal)::in, list(hlds_goal)::out,
    list(hlds_goal)::in, list(hlds_goal)::out,
    list(string)::in, list(string)::out,
    unravel_info::in, unravel_info::out) is det.
:- pragma inline(pred(handle_state_var_in_ite_cond/13)).

handle_state_var_in_ite_cond(LocKind, SVar,
        StatusBefore, StatusAfterCond, StatusAfterITE,
        !NeckCopyGoals, !ElseEndCopyGoals, !ElseMissingInits, !UrInfo) :-
    (
        StatusBefore = status_known(VarBefore),
        new_state_var_instance(SVar, name_middle, FinalVar, !UrInfo),
        VarAfterCond = svar_get_current_progvar(LocKind, StatusAfterCond),
        make_copy_goal(VarAfterCond, FinalVar, NeckCopyGoal),
        !:NeckCopyGoals = [NeckCopyGoal | !.NeckCopyGoals],
        make_copy_goal(VarBefore, FinalVar, ElseCopyGoal),
        !:ElseEndCopyGoals = [ElseCopyGoal | !.ElseEndCopyGoals],
        StatusAfterITE = status_known(FinalVar)
    ;
        StatusBefore = status_unknown,
        VarSet = !.UrInfo ^ ui_varset,
        varset.lookup_name(VarSet, SVar, SVarName),
        !:ElseMissingInits = ["!:" ++ SVarName | !.ElseMissingInits],
        % We pretend the else part defines StateVar, since this is
        % the right thing to do when the else part cannot succeed.
        % If it can, we will generate an error message during
        % mode analysis.
        new_state_var_instance(SVar, name_middle, FinalVar, !UrInfo),
        VarAfterCond = svar_get_current_progvar(LocKind, StatusAfterCond),
        make_copy_goal(VarAfterCond, FinalVar, NeckCopyGoal),
        !:NeckCopyGoals = [NeckCopyGoal | !.NeckCopyGoals],
        StatusAfterITE = status_known(FinalVar)
    ;
        StatusBefore = status_known_ro(_, _, _),
        % The update of !SVar in the condition was an error, for which
        % we have already generated an error message. Because of that,
        % this dummy value won't be used.
        % XXX Returning StatusAfterCond would cause fewer cascading
        % error messages, but are those messages useful?
        StatusAfterITE = StatusBefore
    ;
        ( StatusBefore = status_known_updated(_, _)
        ; StatusBefore = status_unknown_updated(_)
        ),
        % This can happen if LocKind = loc_inside_atomic_goal, but
        % any reference to !:SVar in the condition should have
        % just returned the new progvar for SVar.
        unexpected($pred, "updated before (case 5)")
    ).

:- pred handle_state_var_in_ite_else(unravel_info::in, loc_kind::in, svar::in,
    svar_status::in, svar_status::in, svar_status::out,
    list(hlds_goal)::in, list(hlds_goal)::out,
    list(string)::in, list(string)::out) is det.
:- pragma inline(pred(handle_state_var_in_ite_else/10)).

handle_state_var_in_ite_else(UrInfo, LocKind, SVar, StatusBefore,
        StatusAfterElse, StatusAfterITE,
        !ThenEndCopyGoals, !ThenMissingInits) :-
    (
        StatusBefore = status_known(VarBefore),
        VarAfterElse = svar_get_current_progvar(LocKind, StatusAfterElse),
        make_copy_goal(VarBefore, VarAfterElse, CopyGoal),
        !:ThenEndCopyGoals = [CopyGoal | !.ThenEndCopyGoals],
        StatusAfterITE = StatusAfterElse
    ;
        StatusBefore = status_unknown,
        VarSet = UrInfo ^ ui_varset,
        varset.lookup_name(VarSet, SVar, SVarName),
        !:ThenMissingInits = ["!:" ++ SVarName | !.ThenMissingInits],
        % We pretend the then part defines StateVar, since this is the right
        % thing to do when the then part cannot succeed. If it can, we will
        % generate an error message during mode analysis.
        StatusAfterITE = StatusAfterElse
    ;
        StatusBefore = status_known_ro(_, _, _),
        % The update of !SVar in the else case was an error, for which
        % we have already generated an error message. Because of that,
        % this dummy value won't be used.
        % XXX Returning StatusAfterElse would cause fewer cascading
        % error messages, but are those messages useful?
        StatusAfterITE = StatusBefore
    ;
        ( StatusBefore = status_known_updated(_, _)
        ; StatusBefore = status_unknown_updated(_)
        ),
        % This can happen if LocKind = loc_inside_atomic_goal, but
        % any reference to !:SVar in the else case should have just returned
        % the new progvar for SVar.
        unexpected($pred, "updated before (case 2)")
    ).

:- pred handle_state_var_in_ite_cond_else(loc_kind::in,
    svar_status::in, svar_status::in, svar_status::out,
    list(hlds_goal)::in, list(hlds_goal)::out) is det.
:- pragma inline(pred(handle_state_var_in_ite_cond_else/6)).

handle_state_var_in_ite_cond_else(LocKind,
        StatusAfterCond, StatusAfterElse, StatusAfterITE, !NeckCopyGoals) :-
    VarAfterCond = svar_get_current_progvar(LocKind, StatusAfterCond),
    VarAfterElse = svar_get_current_progvar(LocKind, StatusAfterElse),
    make_copy_goal(VarAfterCond, VarAfterElse, CopyGoal),
    !:NeckCopyGoals = [CopyGoal | !.NeckCopyGoals],
    StatusAfterITE = StatusAfterElse.

:- pred handle_state_var_in_ite_maybe_cond_then(unravel_info::in,
    loc_kind::in, svar::in,
    svar_status::in, svar_status::in, svar_status::out,
    list(hlds_goal)::in, list(hlds_goal)::out,
    list(string)::in, list(string)::out) is det.
:- pragma inline(pred(handle_state_var_in_ite_maybe_cond_then/10)).

handle_state_var_in_ite_maybe_cond_then(UrInfo, LocKind, SVar,
        StatusBefore, StatusAfterThen, StatusAfterITE,
        !ElseEndCopyGoals, !ElseMissingInits) :-
    (
        StatusBefore = status_known(VarBefore),
        VarAfterThen = svar_get_current_progvar(LocKind, StatusAfterThen),
        make_copy_goal(VarBefore, VarAfterThen, CopyGoal),
        !:ElseEndCopyGoals = [CopyGoal | !.ElseEndCopyGoals],
        StatusAfterITE = StatusAfterThen
    ;
        StatusBefore = status_unknown,
        VarSet = UrInfo ^ ui_varset,
        varset.lookup_name(VarSet, SVar, SVarName),
        !:ElseMissingInits = ["!:" ++ SVarName | !.ElseMissingInits],
        % We pretend the else part defines StateVar, since this is the
        % right thing to do when the else part cannot succeed. If it can,
        % we will generate an error message during mode analysis.
        StatusAfterITE = StatusAfterThen
    ;
        StatusBefore = status_known_ro(_, _, _),
        % The updates of !SVar in the condition and then cases were errors,
        % for which we already generated messages. Because of that,
        % this dummy value won't be used.
        % XXX Returning StatusAfterThen would cause fewer cascading
        % error messages, but are those messages useful?
        StatusAfterITE = StatusBefore
    ;
        ( StatusBefore = status_known_updated(_, _)
        ; StatusBefore = status_unknown_updated(_)
        ),
        % This can happen if LocKind = loc_inside_atomic_goal, but
        % any reference to !:SVar in the condition and then case
        % should have just returned the new progvar for SVar.
        unexpected($pred, "updated before (case 7)")
    ).

:- pred handle_state_var_in_ite_maybe_cond_then_else(loc_kind::in,
    svar_status::in, svar_status::in, svar_status::out,
    assoc_list(prog_var, prog_var)::in, assoc_list(prog_var, prog_var)::out)
    is det.
:- pragma inline(pred(handle_state_var_in_ite_maybe_cond_then_else/6)).

handle_state_var_in_ite_maybe_cond_then_else(LocKind,
        StatusAfterThen, StatusAfterElse, StatusAfterITE, !ElseRenames) :-
    VarAfterThen = svar_get_current_progvar(LocKind, StatusAfterThen),
    VarAfterElse = svar_get_current_progvar(LocKind, StatusAfterElse),
    !:ElseRenames = [VarAfterElse - VarAfterThen | !.ElseRenames],
    StatusAfterITE = StatusAfterThen.

%---------------------------------------------------------------------------%
%
% Handle atomic goals. Atomic goals are basically a disjunction between
% the main goal and the orelse goals.
%

:- type svar_outer_atomic_scope_info
    --->    svar_outer_atomic_scope_info(
                soasi_state_var         :: svar,
                soasi_before_status     :: svar_status,
                soasi_after_status      :: svar_status
            )
    ;       no_svar_outer_atomic_scope_info.

svar_start_outer_atomic_scope(Context, OuterStateVar, OuterDIVar, OuterUOVar,
        OuterScopeInfo, !SVarState, !UrInfo) :-
    StatusMap0 = !.SVarState ^ state_status_map,
    ( if map.remove(OuterStateVar, BeforeStatus, StatusMap0, StatusMap) then
        !SVarState ^ state_status_map := StatusMap,
        (
            BeforeStatus = status_unknown,
            report_uninitialized_state_var(warn_dodgy_simple_code, Context,
                OuterStateVar, !UrInfo),
            new_state_var_instance(OuterStateVar, name_middle, OuterDIVar,
                !UrInfo),
            new_state_var_instance(OuterStateVar, name_middle, OuterUOVar,
                !UrInfo),
            OuterScopeInfo = svar_outer_atomic_scope_info(OuterStateVar,
                BeforeStatus, BeforeStatus)
        ;
            BeforeStatus = status_known_ro(OuterDIVar, RO_Construct,
                RO_Context),
            report_illegal_state_var_update(Context,
                ro_construct_name(RO_Construct), RO_Context, OuterStateVar,
                !UrInfo),
            new_state_var_instance(OuterStateVar, name_middle, OuterUOVar,
                !UrInfo),
            OuterScopeInfo = svar_outer_atomic_scope_info(OuterStateVar,
                BeforeStatus, BeforeStatus)
        ;
            BeforeStatus = status_known(OuterDIVar),
            new_state_var_instance(OuterStateVar, name_middle, OuterUOVar,
                !UrInfo),
            AfterStatus = status_known(OuterUOVar),
            OuterScopeInfo = svar_outer_atomic_scope_info(OuterStateVar,
                BeforeStatus, AfterStatus)
        ;
            ( BeforeStatus = status_known_updated(_, _)
            ; BeforeStatus = status_unknown_updated(_)
            ),
            % This status should exist in a status map only when we are in the
            % middle of processing an atomic goal.
            unexpected($pred, "status updated")
        )
    else
        report_non_visible_state_var("", Context, OuterStateVar, !UrInfo),
        new_state_var_instance(OuterStateVar, name_middle, OuterDIVar,
            !UrInfo),
        new_state_var_instance(OuterStateVar, name_middle, OuterUOVar,
            !UrInfo),
        OuterScopeInfo = no_svar_outer_atomic_scope_info
    ).

svar_finish_outer_atomic_scope(OuterScopeInfo, !SVarState) :-
    (
        OuterScopeInfo = svar_outer_atomic_scope_info(OuterStateVar,
            _BeforeStatus, AfterStatus),
        StatusMap0 = !.SVarState ^ state_status_map,
        map.det_insert(OuterStateVar, AfterStatus, StatusMap0, StatusMap),
        !SVarState ^ state_status_map := StatusMap
    ;
        OuterScopeInfo = no_svar_outer_atomic_scope_info
    ).

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

:- type svar_inner_atomic_scope_info
    --->    svar_inner_atomic_scope_info(
                siasi_state_var             :: svar,
                siasi_di_var                :: prog_var,
                siasi_state_before          :: svar_state
            ).

svar_start_inner_atomic_scope(_Context, InnerStateVar, InnerScopeInfo,
        !SVarState, !UrInfo) :-
    SVarStateBefore = !.SVarState,
    new_state_var_instance(InnerStateVar, name_initial, InnerDIVar, !UrInfo),
    StatusMap0 = !.SVarState ^ state_status_map,
    map.set(InnerStateVar, status_known(InnerDIVar), StatusMap0, StatusMap),
    !SVarState ^ state_status_map := StatusMap,
    InnerScopeInfo = svar_inner_atomic_scope_info(InnerStateVar, InnerDIVar,
        SVarStateBefore).

svar_finish_inner_atomic_scope(_Context, InnerScopeInfo,
        InnerDIVar, InnerUOVar, !SVarState) :-
    InnerScopeInfo = svar_inner_atomic_scope_info(InnerStateVar, InnerDIVar,
        SVarStateBefore),
    StatusMap0 = !.SVarState ^ state_status_map,
    map.lookup(StatusMap0, InnerStateVar, Status),
    (
        Status = status_known(InnerUOVar)
    ;
        ( Status = status_unknown
        ; Status = status_unknown_updated(_)
        ; Status = status_known_ro(_, _, _)
        ; Status = status_known_updated(_, _)
        ),
        unexpected($pred, "status != known")
    ),
    !:SVarState = SVarStateBefore.

%---------------------------------------------------------------------------%
%
% Look up prog_vars for a state_var.
%

replace_any_dot_colon_state_var_in_terms([], [], !SVarState, !UrInfo).
replace_any_dot_colon_state_var_in_terms([Arg0 | Args0], [Arg | Args],
        !SVarState, !UrInfo) :-
    replace_any_dot_colon_state_var_in_term(Arg0, Arg, !SVarState, !UrInfo),
    replace_any_dot_colon_state_var_in_terms(Args0, Args, !SVarState, !UrInfo).

replace_any_dot_colon_state_var_in_term(Arg0, Arg, !SVarState, !UrInfo) :-
    ( if Arg0 = functor(atom("!."), [variable(StateVar, _)], Context) then
        lookup_dot_state_var(Context, StateVar, Var, !SVarState, !UrInfo),
        Arg = variable(Var, Context)
    else if Arg0 = functor(atom("!:"), [variable(StateVar, _)], Context) then
        lookup_colon_state_var(Context, StateVar, Var, !SVarState, !UrInfo),
        Arg = variable(Var, Context)
    else
        Arg = Arg0
    ).

lookup_dot_state_var(Context, StateVar, Var, !SVarState, !UrInfo) :-
    StatusMap0 = !.SVarState ^ state_status_map,
    ( if map.search(StatusMap0, StateVar, Status) then
        (
            Status = status_unknown,
            report_uninitialized_state_var(warn_dodgy_simple_code, Context,
                StateVar, !UrInfo),
            % We make StateVar known to avoid duplicate reports.
            new_state_var_instance(StateVar, name_middle, Var, !UrInfo),
            map.det_update(StateVar, status_known(Var),
                StatusMap0, StatusMap),
            !SVarState ^ state_status_map := StatusMap
        ;
            Status = status_unknown_updated(NewVar),
            report_uninitialized_state_var(warn_dodgy_simple_code, Context,
                StateVar, !UrInfo),
            % We make StateVar known to avoid duplicate reports.
            new_state_var_instance(StateVar, name_middle, Var, !UrInfo),
            map.det_update(StateVar, status_known_updated(Var, NewVar),
                StatusMap0, StatusMap),
            !SVarState ^ state_status_map := StatusMap
        ;
            ( Status = status_known(Var)
            ; Status = status_known_ro(Var, _, _)
            ; Status = status_known_updated(Var, _)
            )
        )
    else
        report_non_visible_state_var(".", Context, StateVar, !UrInfo),
        Var = StateVar
    ).

lookup_colon_state_var(Context, StateVar, Var, !SVarState, !UrInfo) :-
    StatusMap0 = !.SVarState ^ state_status_map,
    ( if map.search(StatusMap0, StateVar, Status) then
        (
            Status = status_unknown,
            new_state_var_instance(StateVar, name_middle, Var, !UrInfo),
            map.det_update(StateVar, status_unknown_updated(Var),
                StatusMap0, StatusMap),
            !SVarState ^ state_status_map := StatusMap
        ;
            Status = status_known(OldVar),
            new_state_var_instance(StateVar, name_middle, Var, !UrInfo),
            map.det_update(StateVar, status_known_updated(OldVar, Var),
                StatusMap0, StatusMap),
            !SVarState ^ state_status_map := StatusMap
        ;
            Status = status_known_ro(OldVar, RO_Construct, RO_Context),
            (
                RO_Construct = roc_lambda,
                RO_ConstructName = "lambda expression"
            ),
            report_illegal_state_var_update(Context, RO_ConstructName,
                RO_Context, StateVar, !UrInfo),
            % We remove the readonly notation to avoid duplicate reports.
            new_state_var_instance(StateVar, name_middle, Var, !UrInfo),
            map.det_update(StateVar, status_known_updated(OldVar, Var),
                StatusMap0, StatusMap),
            !SVarState ^ state_status_map := StatusMap
        ;
            Status = status_known_updated(_OldVar, Var)
        ;
            Status = status_unknown_updated(Var)
        )
    else
        report_non_visible_state_var(":", Context, StateVar, !UrInfo),
        % We could make StateVar known to avoid duplicate reports.
        % new_state_var_instance(StateVar, name_initial, Var, !UrInfo),
        % map.det_insert(StateVar, status_known_updated(Var, Var),
        %     StatusMap0, StatusMap),
        % !SVarState ^ state_status_map := StatusMap
        Var = StateVar
    ).

    % Look up the prog_var representing the current state of the state_var
    % whose status is given as the second argument.
    %
:- func svar_get_current_progvar(loc_kind, svar_status) = prog_var.

svar_get_current_progvar(LocKind, Status) = ProgVar :-
    (
        LocKind = loc_whole_goal,
        (
            Status = status_known(ProgVar)
        ;
            ( Status = status_known_ro(_, _, _)
            ; Status = status_known_updated(_, _)
            ; Status = status_unknown
            ; Status = status_unknown_updated(_)
            ),
            unexpected($pred, "Status not known")
        )
    ;
        LocKind = loc_inside_atomic_goal,
        (
            Status = status_known(ProgVar)
        ;
            Status = status_known_updated(_, ProgVar)
        ;
            Status = status_unknown_updated(ProgVar)
        ;
            ( Status = status_known_ro(_, _, _)
            ; Status = status_unknown
            ),
            unexpected($pred, "Status not known or updated")
        )
    ).

%---------------------------------------------------------------------------%
%
% Code to handle the flattening of conjunctions. We need to be careful when we
% do so, since the goal we flatten could have a goal id, which would mean that
% the svar_store could have a delayed remapping for that goal_id. Just
% flattening the goal would remove the goal_info containing the goal_id from
% the HLDS, and the delayed renaming would not get done.
%
% Therefore when we flatten such a goal, we ensure that its subgoals
% all have goal_ids (creating new ones if needed), and that the delayed
% renaming that now won't get done on the conjunction as a whole
% *will* get done on each conjunct.
%

svar_flatten_conj(Context, Goals, Goal, !UrInfo) :-
    list.map_foldl(svar_goal_to_conj_list, Goals, GoalConjuncts, !UrInfo),
    list.condense(GoalConjuncts, Conjuncts),
    GoalExpr = conj(plain_conj, Conjuncts),
    goal_info_init(Context, GoalInfo),
    Goal = hlds_goal(GoalExpr, GoalInfo).

svar_goal_to_conj_list(Goal, Conjuncts, !UrInfo) :-
    % The code here is the same as in svar_goal_to_conj_list_internal,
    % modulo the differences in the argument list.
    Goal = hlds_goal(GoalExpr, GoalInfo),
    ( if GoalExpr = conj(plain_conj, Conjuncts0) then
        SVarStore0 = !.UrInfo ^ ui_state_var_store,
        SVarStore0 = svar_store(NextGoalId0, DelayedRenamingMap0,
            LastIdMap0, SVarSpecs0),
        GoalId = goal_info_get_goal_id(GoalInfo),
        ( if map.search(DelayedRenamingMap0, GoalId, GoalDelayedRenaming) then
            list.map_foldl2(
                add_conjunct_delayed_renames(GoalDelayedRenaming),
                    Conjuncts0, Conjuncts, NextGoalId0, NextGoalId,
                    DelayedRenamingMap0, DelayedRenamingMap),
            SVarStore = svar_store(NextGoalId, DelayedRenamingMap,
                LastIdMap0, SVarSpecs0),
            !UrInfo ^ ui_state_var_store := SVarStore
        else
            Conjuncts = Conjuncts0
        )
    else
        Conjuncts = [Goal]
    ).

:- pred svar_goal_to_conj_list_internal(hlds_goal::in, list(hlds_goal)::out,
    ucounter::in, ucounter::out,
    incremental_rename_map::in, incremental_rename_map::out) is det.

svar_goal_to_conj_list_internal(Goal, Conjuncts,
        !NextGoalId, !DelayedRenamingMap) :-
    % The code here is the same as in svar_goal_to_conj_list,
    % modulo the differences in the argument list.
    Goal = hlds_goal(GoalExpr, GoalInfo),
    ( if GoalExpr = conj(plain_conj, Conjuncts0) then
        GoalId = goal_info_get_goal_id(GoalInfo),
        ( if map.search(!.DelayedRenamingMap, GoalId, GoalDelayedRenaming) then
            list.map_foldl2(
                add_conjunct_delayed_renames(GoalDelayedRenaming),
                Conjuncts0, Conjuncts, !NextGoalId, !DelayedRenamingMap)
        else
            Conjuncts = Conjuncts0
        )
    else
        Conjuncts = [Goal]
    ).

:- pred add_conjunct_delayed_renames(assoc_list(prog_var, prog_var)::in,
    hlds_goal::in, hlds_goal::out, ucounter::in, ucounter::out,
    incremental_rename_map::in, incremental_rename_map::out) is det.

add_conjunct_delayed_renames(DelayedRenamingToAdd, Goal0, Goal,
        !NextGoalId, !DelayedRenamingMap) :-
    Goal0 = hlds_goal(GoalExpr, GoalInfo0),
    GoalId0 = goal_info_get_goal_id(GoalInfo0),
    ( if map.search(!.DelayedRenamingMap, GoalId0, DelayedRenaming0) then
        % The goal id must be valid.
        DelayedRenaming = DelayedRenamingToAdd ++ DelayedRenaming0,
        map.det_update(GoalId0, DelayedRenaming, !DelayedRenamingMap),
        Goal = Goal0
    else
        % The goal id must be invalid, since the only thing that attaches goal
        % ids to goals at this stage of the compilation process is this module,
        % and it attaches goal_ids to goals only if it also puts them the
        % delayed renaming map.
        counter.uallocate(GoalIdNum, !NextGoalId),
        GoalId = goal_id(GoalIdNum),
        goal_info_set_goal_id(GoalId, GoalInfo0, GoalInfo),
        map.det_insert(GoalId, DelayedRenamingToAdd, !DelayedRenamingMap),
        Goal = hlds_goal(GoalExpr, GoalInfo)
    ).

%---------------------------------------------------------------------------%
%
% A post-pass to delete unneeded copy unifications. Such unifications
% can hide singleton variable problems.
%

    % Suppose we have a goal such as this:
    %
    %   ( if p(..., !.S, !:S) then
    %       <then_part>
    %   else
    %       <else_part>
    %   )
    %
    % and that !:S is not referred to anywhere in the clause
    % (not in then_part, not in else_part, not in the following code).
    %
    % In this form, warn_singletons can generate a warning about !:S
    % being a singleton. However, the state variable transformation
    % transforms it to code like this:
    %
    %   ( if p(..., STATE_VARIABLE_S_5, STATE_VARIABLE_S_6) then
    %       <then_part>, STATE_VARIABLE_S_7 = STATE_VARIABLE_S_6
    %   else
    %       <else_part>, STATE_VARIABLE_S_7 = STATE_VARIABLE_S_5
    %   )
    %
    % and marks both assignments to STATE_VARIABLE_S_7 as not being subject
    % to singleton warnings.
    %
    % We don't actually *want* to generate warnings about the assignments
    % to STATE_VARIABLE_S_7, because the problem is not in those assignments
    % or in the if-then-else arms that contain them. Instead, it is
    % in the condition. However, with this version of the code, the occurrence
    % of STATE_VARIABLE_S_6 in the condition is *not* a singleton.
    %
    % To allow us to generate a warning about !:S in the condition,
    % we delete all copy unifications inserted by the state variable
    % transformation that assign to a variable that is not referred to
    % either in the code that follows the assignment, or in the head.
    % (The head contains the output arguments, which will live
    % beyond the lifetime of anything in the clause body.)
    %
    % To find which variables occur after a copy goal, we have
    % delete_unneeded_copy_goals do a backwards traversal of the clause body,
    % keeping track of all the variables it has seen.
    %
    % To find which variables occur in the head, we use the call to goal_vars
    % below. The variables in HeadUnificationsGoal contain not just the
    % head_vars of the clause, but also the state variable instances any
    % of them are unified with. (This includes the input arguments as well as
    % the output arguments, but the clause body won't contain any assignments
    % to either the input arguments or the state variable instances
    % they are unified with, so including them in SeenLater0 is harmless.
    % As it happens, we have to include them because we don't know
    % which arguments are input and which are output, a distinction
    % that in any case may be mode-dependent.)
    %
    % We cannot count on the definitions of those state var instances being
    % before any of the occurrences of the head vars they are unified with.
    % For example, if a fact contains an !S argument pair, the call to
    % svar_finish_body in our caller will put the unification of the state
    % var instances representing !.S and !:S *after* HeadUnificationsGoal.
    % If we initialized SeenLater0 to just the head_vars of the clause,
    % this unification would assign to a variable that is *not* in SeenLater0,
    % and would thus be eliminated, which would be a bug.
    %
:- pred delete_unneeded_copy_goals_in_clause(hlds_goal::in,
    hlds_goal::in, hlds_goal::out) is det.

delete_unneeded_copy_goals_in_clause(HeadUnificationsGoal, Goal0, Goal) :-
    vars_in_goal(HeadUnificationsGoal, HeadUnificationsGoalVars),
    SeenLater0 = HeadUnificationsGoalVars,
    delete_unneeded_copy_goals(Goal0, Goal, SeenLater0, _SeenLater).

:- pred delete_unneeded_copy_goals(hlds_goal::in, hlds_goal::out,
    set_of_progvar::in, set_of_progvar::out) is det.

delete_unneeded_copy_goals(Goal0, Goal, SeenAfter, SeenBefore) :-
    Goal0 = hlds_goal(GoalExpr0, GoalInfo),
    (
        GoalExpr0 = unify(LHSVar, _, _, _, _),
        vars_in_goal(Goal0, GoalVars0),
        ( if
            goal_info_has_feature(GoalInfo, feature_state_var_copy),
            not set_of_var.member(SeenAfter, LHSVar)
        then
            Goal = hlds_goal(true_goal_expr, GoalInfo),
            SeenBefore = SeenAfter
        else
            set_of_var.union(GoalVars0, SeenAfter, SeenBefore),
            Goal = Goal0
        )
    ;
        ( GoalExpr0 = plain_call(_, _, _, _, _, _)
        ; GoalExpr0 = generic_call(_, _, _, _, _)
        ; GoalExpr0 = call_foreign_proc(_, _, _, _, _, _, _)
        ),
        vars_in_goal(Goal0, GoalVars0),
        set_of_var.union(GoalVars0, SeenAfter, SeenBefore),
        Goal = Goal0
    ;
        GoalExpr0 = conj(ConjKind, Conjuncts0),
        % Processing Conjuncts0 without reversing it would lead to recursion
        % as deep as Conjuncts0 is long. Since Conjuncts0 can be very long,
        % we prefer to pay the price of reversing and unreversing the list
        % to achieve tail recursion.
        list.reverse(Conjuncts0, RevConjuncts0),
        delete_unneeded_copy_goals_rev_conj(RevConjuncts0, RevConjuncts,
            SeenAfter, SeenBefore),
        list.reverse(RevConjuncts, Conjuncts),
        GoalExpr = conj(ConjKind, Conjuncts),
        Goal = hlds_goal(GoalExpr, GoalInfo)
    ;
        GoalExpr0 = disj(Disjuncts0),
        delete_unneeded_copy_goals_disj(Disjuncts0, Disjuncts,
            SeenAfter, SeenBefores),
        GoalExpr = disj(Disjuncts),
        set_of_var.union_list(SeenBefores, SeenBefore),
        Goal = hlds_goal(GoalExpr, GoalInfo)
    ;
        GoalExpr0 = switch(SwitchVar, CanFail, Cases0),
        % Switches should not exist at this point in the compilation process,
        % but it is simple enough to prepare here for the eventuality that
        % this may change in the future.
        delete_unneeded_copy_goals_switch(Cases0, Cases,
            SeenAfter, SeenBefores),
        GoalExpr = switch(SwitchVar, CanFail, Cases),
        set_of_var.union_list(SeenBefores, SeenBefore0),
        set_of_var.insert(SwitchVar, SeenBefore0, SeenBefore),
        Goal = hlds_goal(GoalExpr, GoalInfo)
    ;
        GoalExpr0 = if_then_else(ITEVars, Cond0, Then0, Else0),
        delete_unneeded_copy_goals(Else0, Else, SeenAfter, SeenBeforeElse),
        delete_unneeded_copy_goals(Then0, Then, SeenAfter, SeenAfterThen),
        delete_unneeded_copy_goals(Cond0, Cond, SeenAfterThen, SeenBeforeCond),
        GoalExpr = if_then_else(ITEVars, Cond, Then, Else),
        set_of_var.union(SeenBeforeCond, SeenBeforeElse, SeenBefore0),
        set_of_var.insert_list(ITEVars, SeenBefore0, SeenBefore),
        Goal = hlds_goal(GoalExpr, GoalInfo)
    ;
        GoalExpr0 = negation(SubGoal0),
        delete_unneeded_copy_goals(SubGoal0, SubGoal, SeenAfter, SeenBefore),
        GoalExpr = negation(SubGoal),
        Goal = hlds_goal(GoalExpr, GoalInfo)
    ;
        GoalExpr0 = scope(Reason, SubGoal0),
        (
            Reason = from_ground_term(TermVar, _Kind),
            % There won't be any feature_state_var_copy goals inside SubGoal0.
            SubGoal = SubGoal0,
            % None of the variables in SubGoal can occur in the rest of the
            % procedure body, with the exception of TermVar.
            set_of_var.insert(TermVar, SeenAfter, SeenBefore)
        ;
            ( Reason = require_complete_switch(ScopeVar)
            ; Reason = require_switch_arms_detism(ScopeVar, _Detism)
            ),
            delete_unneeded_copy_goals(SubGoal0, SubGoal,
                SeenAfter, SeenBefore0),
            set_of_var.insert(ScopeVar, SeenBefore0, SeenBefore)
        ;
            Reason = loop_control(LCVar, LCSVar, _UseParentStack),
            delete_unneeded_copy_goals(SubGoal0, SubGoal,
                SeenAfter, SeenBefore0),
            set_of_var.insert_list([LCVar, LCSVar], SeenBefore0, SeenBefore)
        ;
            ( Reason = exist_quant(ScopeVars, _)
            ; Reason = promise_solutions(ScopeVars, _PromiseKind)
            ; Reason = trace_goal(_Comp, _Run, _MaybeIO, _Mutables, ScopeVars)
            ),
            delete_unneeded_copy_goals(SubGoal0, SubGoal,
                SeenAfter, SeenBefore0),
            set_of_var.insert_list(ScopeVars, SeenBefore0, SeenBefore)
        ;
            ( Reason = disable_warnings(_, _)
            ; Reason = promise_purity(_)
            ; Reason = require_detism(_)
            ; Reason = commit(_)
            ; Reason = barrier(_)
            ),
            delete_unneeded_copy_goals(SubGoal0, SubGoal,
                SeenAfter, SeenBefore)
        ),
        GoalExpr = scope(Reason, SubGoal),
        Goal = hlds_goal(GoalExpr, GoalInfo)
    ;
        GoalExpr0 = shorthand(ShortHand0),
        (
            ShortHand0 = atomic_goal(AtomicType,
                atomic_interface_vars(OuterInitVar, OuterFinalVar),
                atomic_interface_vars(InnerInitVar, InnerFinalVar),
                MaybeOutputVars, MainGoal0, OrElseGoals0, OrElseInners),
            expect(unify(OrElseInners, []), $pred, "OrElseInners != []"),
            Disjuncts0 = [MainGoal0 | OrElseGoals0],
            delete_unneeded_copy_goals_disj(Disjuncts0, Disjuncts,
                SeenAfter, SeenBefores),
            (
                Disjuncts = [],
                unexpected($pred, "Disjuncts = []")
            ;
                Disjuncts = [MainGoal | OrElseGoals]
            ),
            ShortHand = atomic_goal(AtomicType,
                atomic_interface_vars(OuterInitVar, OuterFinalVar),
                atomic_interface_vars(InnerInitVar, InnerFinalVar),
                MaybeOutputVars, MainGoal, OrElseGoals, OrElseInners),
            set_of_var.union_list(SeenBefores, SeenBefore0),
            set_of_var.insert_list([OuterInitVar, OuterFinalVar,
                InnerInitVar, InnerFinalVar], SeenBefore0, SeenBefore1),
            (
                MaybeOutputVars = no,
                SeenBefore = SeenBefore1
            ;
                MaybeOutputVars = yes(OutputVars),
                set_of_var.insert_list(OutputVars, SeenBefore1, SeenBefore)
            )
        ;
            ShortHand0 = try_goal(MaybeIOStateVars, ResultVar, SubGoal0),
            delete_unneeded_copy_goals(SubGoal0, SubGoal,
                SeenAfter, SeenBefore0),
            set_of_var.insert(ResultVar, SeenBefore0, SeenBefore1),
            (
                MaybeIOStateVars = no,
                SeenBefore = SeenBefore1
            ;
                MaybeIOStateVars = yes(try_io_state_vars(InitVar, FinalVar)),
                set_of_var.insert(InitVar, SeenBefore1, SeenBefore2),
                set_of_var.insert(FinalVar, SeenBefore2, SeenBefore)
            ),
            ShortHand = try_goal(MaybeIOStateVars, ResultVar, SubGoal)
        ;
            ShortHand0 = bi_implication(LeftGoal0, RightGoal0),
            delete_unneeded_copy_goals(LeftGoal0, LeftGoal,
                SeenAfter, SeenBeforeLeft),
            delete_unneeded_copy_goals(RightGoal0, RightGoal,
                SeenAfter, SeenBeforeRight),
            set_of_var.union(SeenBeforeLeft, SeenBeforeRight, SeenBefore),
            ShortHand = bi_implication(LeftGoal, RightGoal)
        ),
        GoalExpr = shorthand(ShortHand),
        Goal = hlds_goal(GoalExpr, GoalInfo)
    ).

:- pred delete_unneeded_copy_goals_rev_conj(
    list(hlds_goal)::in, list(hlds_goal)::out,
    set_of_progvar::in, set_of_progvar::out) is det.

delete_unneeded_copy_goals_rev_conj([], [], SeenAfter, SeenBefore) :-
    SeenBefore = SeenAfter.
delete_unneeded_copy_goals_rev_conj(
        [RevConjunct0 | RevConjuncts0], [RevConjunct | RevConjuncts],
        SeenAfter, SeenBefore) :-
    delete_unneeded_copy_goals(RevConjunct0, RevConjunct,
        SeenAfter, SeenBetween),
    delete_unneeded_copy_goals_rev_conj(RevConjuncts0, RevConjuncts,
        SeenBetween, SeenBefore).

:- pred delete_unneeded_copy_goals_disj(
    list(hlds_goal)::in, list(hlds_goal)::out,
    set_of_progvar::in, list(set_of_progvar)::out) is det.

delete_unneeded_copy_goals_disj([], [], _, []).
delete_unneeded_copy_goals_disj(
        [Disjunct0 | Disjuncts0], [Disjunct | Disjuncts],
        SeenAfter, [SeenBefore | SeenBefores]) :-
    delete_unneeded_copy_goals(Disjunct0, Disjunct, SeenAfter, SeenBefore),
    delete_unneeded_copy_goals_disj(Disjuncts0, Disjuncts,
        SeenAfter, SeenBefores).

:- pred delete_unneeded_copy_goals_switch(list(case)::in, list(case)::out,
    set_of_progvar::in, list(set_of_progvar)::out) is det.

delete_unneeded_copy_goals_switch([], [], _, []).
delete_unneeded_copy_goals_switch([Case0 | Cases0], [Case | Cases],
        SeenAfter, [SeenBefore | SeenBefores]) :-
    Case0 = case(MainConsId, OtherConsIds, Goal0),
    delete_unneeded_copy_goals(Goal0, Goal, SeenAfter, SeenBefore),
    Case = case(MainConsId, OtherConsIds, Goal),
    delete_unneeded_copy_goals_switch(Cases0, Cases, SeenAfter, SeenBefores).

%---------------------------------------------------------------------------%
%
% Test for various kinds of errors.
%

illegal_state_var_func_result(pf_function, ArgTerms, StateVar, Context) :-
    list.last(ArgTerms, LastArgTerm),
    is_term_a_bang_state_pair(LastArgTerm, StateVar, Context).

is_term_a_bang_state_pair(ArgTerm, StateVar, Context) :-
    ArgTerm = functor(atom("!"), [variable(StateVar, Context)], _).

%---------------------------------------------------------------------------%
%
% Report various kinds of errors.
%

report_illegal_state_var_update(Context, RO_Construct, RO_Context,
        StateVar, !UrInfo) :-
    VarSet = !.UrInfo ^ ui_varset,
    Name = varset.lookup_name(VarSet, StateVar),
    Pieces1 = [words("Error: you cannot use")] ++
        color_as_incorrect([quote("!:" ++ Name)]) ++
        [words("here due to the surrounding"), words(RO_Construct),
            suffix(";"),
        words("you may only refer to")] ++
        color_as_correct([quote("!." ++ Name), suffix(".")]) ++ [nl],
    Msg1 = msg(Context, Pieces1),
    Pieces2 = [words("Here is the surrounding context that makes"),
        words("state variable"), quote(Name), words("readonly."), nl],
    Msg2 = msg(RO_Context, Pieces2),
    Spec = error_spec($pred, severity_error, phase_pt2h, [Msg1, Msg2]),
    add_unravel_spec(Spec, !UrInfo).

:- func ro_construct_name(readonly_context_kind) = string.

ro_construct_name(roc_lambda) = "lambda expression".

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

report_illegal_func_svar_result(Context, StateVar, !UrInfo) :-
    VarSet = !.UrInfo ^ ui_varset,
    Spec = report_illegal_func_svar_result_raw(Context, VarSet, StateVar),
    add_unravel_spec(Spec, !UrInfo).

report_illegal_func_svar_result_raw(Context, VarSet, StateVar) = Spec :-
    Name = varset.lookup_name(VarSet, StateVar),
    % While having !.Var appear as a function argument is quite ordinary,
    % having it appear as a function *result* is not. We therefore do not
    % suggest it as a likely correction.
    Pieces = [words("Error: since it represents two arguments, not one,")] ++
        color_as_incorrect([quote("!" ++ Name)]) ++
        [words("cannot be a function result. You probably meant")] ++
        color_as_correct([fixed("!:" ++ Name), suffix(".")]) ++ [nl],
    Spec = spec($pred, severity_error, phase_pt2h, Context, Pieces).

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

report_illegal_bang_svar_lambda_arg(Context, StateVar, !UrInfo) :-
    VarSet = !.UrInfo ^ ui_varset,
    Spec = report_illegal_bang_svar_lambda_arg_raw(Context, VarSet, StateVar),
    add_unravel_spec(Spec, !UrInfo).

report_illegal_bang_svar_lambda_arg_raw(Context, VarSet, StateVar) = Spec :-
    Name = varset.lookup_name(VarSet, StateVar),
    Pieces = [words("Error:")] ++
        color_as_incorrect([quote("!" ++ Name)]) ++
        [words("cannot be a lambda argument."), nl,
        words("Perhaps you meant")] ++
        color_as_correct([quote("!." ++ Name)]) ++
        [words("or")] ++
        color_as_correct([quote("!:" ++ Name), suffix(".")]) ++ [nl],
    Spec = spec($pred, severity_error, phase_pt2h, Context, Pieces).

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

:- pred report_non_visible_state_var(string::in, prog_context::in, svar::in,
    unravel_info::in, unravel_info::out) is det.

report_non_visible_state_var(DorC, Context, StateVar, !UrInfo) :-
    VarSet = !.UrInfo ^ ui_varset,
    Name = varset.lookup_name(VarSet, StateVar),
    Pieces = [words("Error: state variable")] ++
        color_as_incorrect([quote("!" ++ DorC ++ Name)]) ++
        [words("is not visible in this context."), nl],
    Spec = spec($pred, severity_error, phase_pt2h, Context, Pieces),
    add_unravel_spec(Spec, !UrInfo).

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

:- pred report_uninitialized_state_var(option::in, prog_context::in, svar::in,
    unravel_info::in, unravel_info::out) is det.

report_uninitialized_state_var(WarnOption, Context, StateVar, !UrInfo) :-
    VarSet = !.UrInfo ^ ui_varset,
    Name = varset.lookup_name(VarSet, StateVar),
    Pieces = [words("Warning: you cannot refer to")] ++
        color_as_subject([quote("!." ++ Name)]) ++
        [words("here, because that state variable has")] ++
        color_as_incorrect([words("not been initialized")]) ++
        [words("yet."), nl],
    Spec = spec($pred, severity_warning(WarnOption), phase_pt2h,
        Context, Pieces),
    add_unravel_spec(Spec, !UrInfo).

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

:- pred report_repeated_head_state_var(prog_context::in, svar::in,
    unravel_info::in, unravel_info::out) is det.

report_repeated_head_state_var(Context, StateVar, !UrInfo) :-
    VarSet = !.UrInfo ^ ui_varset,
    Name = varset.lookup_name(VarSet, StateVar),
    Pieces = [words("Warning: clause head introduces")] ++
        color_as_incorrect([words("state variable"), quote(Name)]) ++
        [words("more than once."), nl],
    Spec = spec($pred, severity_error, phase_pt2h, Context, Pieces),
    add_unravel_spec(Spec, !UrInfo).

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

:- pred report_state_var_shadow(prog_context::in, svar::in,
    unravel_info::in, unravel_info::out) is det.

report_state_var_shadow(Context, StateVar, !UrInfo) :-
    VarSet = !.UrInfo ^ ui_varset,
    Name = varset.lookup_name(VarSet, StateVar),
    Pieces = [words("Warning: new state variable")] ++
        color_as_subject([quote(Name)]) ++
        color_as_incorrect([words("shadows old one.")]) ++ [nl],
    Spec = spec($pred, severity_warning(warn_state_var_shadowing), phase_pt2h,
        Context, Pieces),
    add_unravel_spec(Spec, !UrInfo).

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

:- pred report_missing_inits_in_ite(prog_context::in, list(string)::in,
    string::in, string::in, unravel_info::in, unravel_info::out) is det.

report_missing_inits_in_ite(Context, NextStateVars,
        WhenMissing, WhenNotMissing, !UrInfo) :-
    NextStateVarsPieces = quote_list_to_color_pieces(color_subject, "and",
        [suffix(",")], NextStateVars),
    Pieces = [words("When the condition"), words(WhenNotMissing), suffix(","),
        words("the if-then-else")] ++
        color_as_inconsistent([words("defines")]) ++
        NextStateVarsPieces ++
        [words("but when the condition"), words(WhenMissing), suffix(",")] ++
        color_as_inconsistent([words("it does not.")]) ++ [nl],
    Spec = spec($pred, severity_warning(warn_missing_state_var_init),
        phase_pt2h, Context, Pieces),
    Specs0 = !.UrInfo ^ ui_state_var_store ^ store_missing_init_specs,
    Specs = [Spec | Specs0],
    !UrInfo ^ ui_state_var_store ^ store_missing_init_specs := Specs.

:- pred report_missing_inits_in_disjunct(prog_context::in, list(string)::in,
    list(error_spec)::in, list(error_spec)::out) is det.

report_missing_inits_in_disjunct(Context, NextStateVars, !Specs) :-
    Pieces = [words("Other disjuncts define")] ++
        quote_list_to_color_pieces(color_subject, "and", [suffix(",")],
            NextStateVars) ++
        color_as_incorrect([words("but not this one.")]) ++ [nl],
    Spec = spec($pred, severity_warning(warn_missing_state_var_init),
        phase_pt2h, Context, Pieces),
    % The intention is that our caller got !.Specs from the state var store's
    % store_missing_init_specs field, and will put the updated list back there.
    !:Specs = [Spec | !.Specs].

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

report_svar_unify_error(Context, StateVar, !SVarState, !UrInfo) :-
    VarSet = !.UrInfo ^ ui_varset,
    Name = varset.lookup_name(VarSet, StateVar),
    Pieces = [words("Error:")] ++
        color_as_incorrect([fixed("!" ++ Name)]) ++
        [words("cannot appear as a unification argument."), nl,
        words("You probably meant")] ++
        color_as_correct([fixed("!." ++ Name)]) ++ [words("or")] ++
        color_as_correct([fixed("!:" ++ Name), suffix(".")]) ++ [nl],
    Spec = spec($pred, severity_error, phase_pt2h, Context, Pieces),
    add_unravel_spec(Spec, !UrInfo),
    !.SVarState = svar_state(StatusMap0),
    % If StateVar was not known before, then this is the first occurrence
    % of this state variable, and the user almost certainly intended it
    % to define its initial value. Any messages from later goals complaining
    % about the variable not being defined there would only be a distraction.
    %
    % Adding this dummy entry to the state, means we cannot generate valid
    % HLDS goals, but the error reported just above ensures that we will
    % throw away the HLDS goals we generate, so this is ok.
    ( if
        map.search(StatusMap0, StateVar, OldStatus),
        OldStatus \= status_unknown
    then
        % The state variable is already known.
        true
    else
        new_state_var_instance(StateVar, name_initial, Var, !UrInfo),
        Status = status_known(Var),
        map.set(StateVar, Status, StatusMap0, StatusMap),
        !:SVarState = svar_state(StatusMap)
    ).

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

:- pred find_unused_statevar_args(prog_varset::in, new_statevar_map::in,
    last_id_map::in, unused_statevar_arg_map::out) is det.

find_unused_statevar_args(VarSet, NewSVars, LastIdMap, UnusedSVarArgMap) :-
    map.foldl(record_statevar_if_unused(VarSet, LastIdMap), NewSVars,
        map.init, UnusedSVarArgMap).

:- pred record_statevar_if_unused(prog_varset::in, last_id_map::in,
    prog_var::in, one_or_more(maybe_statevar_arg_pos)::in,
    unused_statevar_arg_map::in, unused_statevar_arg_map::out) is det.

record_statevar_if_unused(VarSet, LastIdMap, SVar, OoMArgPos,
        !UnusedSVarArgMap) :-
    list.sort(one_or_more_to_list(OoMArgPos), SortedArgPoss),
    ( if
        not map.search(LastIdMap, SVar, _),
        (
            SortedArgPoss = [arg_old(InitPos), arg_new(FinalPos)],
            ArgPos = InitPos,
            ArgKind = init_and_final_arg(FinalPos)
        ;
            SortedArgPoss = [arg_old(InitPos)],
            ArgPos = InitPos,
            ArgKind = init_arg_only
        ;
            SortedArgPoss = [arg_new(FinalPos)],
            ArgPos = FinalPos,
            ArgKind = final_arg_only
        )
    then
        varset.lookup_name(VarSet, SVar, SVarName),
        SVarArgDesc = statevar_arg_desc(ArgKind, SVarName),
        map.det_insert(ArgPos, SVarArgDesc, !UnusedSVarArgMap)
    else
        true
    ).

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

:- pred report_any_unneeded_svars_in_lambda(prog_context::in,
    list(mer_mode)::in, goal::in, hlds_goal::in, unused_statevar_arg_map::in,
    unravel_info::in, unravel_info::out) is det.

report_any_unneeded_svars_in_lambda(Context, Modes, ParseTreeGoal, Goal,
        UnusedSVarArgMap, !UrInfo) :-
    ( if map.is_empty(UnusedSVarArgMap) then
        true
    else
        VarSet = !.UrInfo ^ ui_varset,
        non_svar_copy_vars_in_goal(Goal, GoalVarsSet),
        set_of_var.to_sorted_list(GoalVarsSet, GoalVars),
        list.filter_map(is_prog_var_for_some_state_var(VarSet),
            GoalVars, GoalVarSVarNames),
        map.foldl(
            report_unneeded_svar_in_lambda(Context, Modes,
                ParseTreeGoal, GoalVarSVarNames),
            UnusedSVarArgMap, !UrInfo)
    ).

:- pred report_unneeded_svar_in_lambda(prog_context::in, list(mer_mode)::in,
    goal::in, list(string)::in, uint::in, statevar_arg_desc::in,
    unravel_info::in, unravel_info::out) is det.

report_unneeded_svar_in_lambda(Context, Modes, ParseTreeGoal, GoalVarSVarNames,
        ArgNum, SVarArgDesc, !UrInfo) :-
    SVarArgDesc = statevar_arg_desc(InitOrFinal, SVarName),
    % Please keep the wording of the three warnings generated here
    % in sync with the code of the following predicates in pre_typecheck.m:
    % - warn_about_any_unneeded_initial_statevars
    % - warn_about_unneeded_final_statevar
    % - warn_about_unneeded_initial_final_statevar.
    (
        ( InitOrFinal = init_arg_only,  Prefix = "!."
        ; InitOrFinal = final_arg_only, Prefix = "!:"
        ),
        Pieces = [words("Warning: the state variable")] ++
            color_as_subject([quote(Prefix ++ SVarName)]) ++ [words("is")] ++
            color_as_incorrect([words("never updated")]) ++
            [words("in this lambda expressions, so it should be"),
            words("replaced with an ordinary variable."), nl],
        Severity = severity_warning(warn_unneeded_initial_statevars_lambda),
        Spec = spec($pred, Severity, phase_pt2h, Context, Pieces),
        add_unravel_spec(Spec, !UrInfo)
    ;
        InitOrFinal = init_and_final_arg(_),
        % Please keep this wording in sync with the code of the
        % warn_about_unneeded_final_statevar predicate in pre_typecheck.m.
        InitOrFinal = init_and_final_arg(FinalArgNum),
        ( if list.member(SVarName, GoalVarSVarNames) then
            % The initial version of SVarName is used by user-written code
            % in the lambda goal, so only the final version of SVarName
            % is unneeded.
            ModuleInfo = !.UrInfo ^ ui_module_info,
            FinalArgNumI = uint.cast_to_int(FinalArgNum),
            InitArgNumI = uint.cast_to_int(ArgNum),
            list.det_index1(Modes, InitArgNumI, InitArgMode),
            list.det_index1(Modes, FinalArgNumI, FinalArgMode),
            ( if
                % See the comments in warn_about_any_unneeded_statevars
                % for the reasoning behind this test.
                %
                % Note that we cannot test the HLDS goal from which our caller
                % derived GoalVarSVarNames, because that contains the
                % unifications implicitly added by the state variable
                % transformation itself. We need the goal from *before*
                % that transformation.
                not ( ParseTreeGoal = true_expr(_) ),
                % See the comments in maybe_warn_about_unneeded_final_statevar
                % for the reasoning behind this test.
                mode_is_free_of_uniqueness(ModuleInfo, InitArgMode),
                mode_is_free_of_uniqueness(ModuleInfo, FinalArgMode)
            then
                Pieces = [words("Warning: the argument")] ++
                    color_as_subject([quote("!:" ++ SVarName)]) ++
                    [words("in this lambda expression")] ++
                    color_as_incorrect([words("could be deleted,")]) ++
                    [words("because its value"),
                    words("is always the same as its initial value."), nl],
                Severity =
                    severity_warning(warn_unneeded_final_statevars_lambda),
                Spec = spec($pred, Severity, phase_pt2h, Context, Pieces),
                add_unravel_spec(Spec, !UrInfo)
            else
                true
            )
        else
            % The initial version of SVarName is NOT used by user-written code
            % in the lambda goal, so both the initial and final versions
            % of SVarName are unneeded.
            Pieces = [words("Warning: the arguments")] ++
                color_as_subject([quote("!." ++ SVarName)]) ++
                [words("and")] ++
                color_as_subject([quote("!:" ++ SVarName)]) ++
                [words("in this lambda expression")] ++
                color_as_incorrect([words("could be deleted,")]) ++
                [words("because they are not used in the lambda goal,"),
                words("and because the final value"),
                words("is always the same as the initial value."), nl],
            Severity = severity_warning(warn_unneeded_final_statevars_lambda),
            Spec = spec($pred, Severity, phase_pt2h, Context, Pieces),
            add_unravel_spec(Spec, !UrInfo)
        )
    ).

%---------------------------------------------------------------------------%
:- end_module hlds.make_hlds.state_var.
%---------------------------------------------------------------------------%
