%---------------------------------------------------------------------------%
% vim: ft=mercury ts=4 sw=4 et
%---------------------------------------------------------------------------%
% Copyright (C) 2017, 2019, 2021-2023 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: mode_comparison.m.
%
% This file contains code compare different modes of a predicate.
%
%---------------------------------------------------------------------------%

:- module check_hlds.mode_comparison.
:- interface.

:- import_module check_hlds.mode_info.
:- import_module hlds.
:- import_module hlds.hlds_module.
:- import_module hlds.hlds_pred.
:- import_module parse_tree.
:- import_module parse_tree.prog_data.

:- import_module list.

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

    % Given two modes of a predicate, figure out whether they are
    % indistinguishable; that is, whether any valid call to one mode
    % would also be a valid call to the other. (If so, it is a mode error.)
    % Note that mode declarations which only have different final insts
    % do not count as distinguishable.
    %
:- pred modes_are_indistinguishable(module_info::in, pred_info::in,
    proc_id::in, proc_id::in) is semidet.

    % Given two modes of a predicate, figure out whether they are identical,
    % except that one is cc_nondet/cc_multi and the other is nondet/multi.
    % This is used by determinism analysis to substitute a multi mode
    % for a cc_multi one if the call occurs in a non-cc context.
    %
:- pred modes_are_identical_bar_cc(module_info::in, pred_info::in,
    proc_id::in, proc_id::in) is semidet.

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

:- type proc_mode
    --->    proc_mode(proc_id, inst_var_sub, list(mer_mode)).

:- type match
    --->    better
    ;       worse
    ;       same
    ;       incomparable.

    % The algorithm for choose_best_match is supposed to be equivalent
    % to the following specification:
    %
    %   1.  Remove any modes that are strictly less instantiated or
    %       less informative on input than other valid modes. For example,
    %
    %       - we prefer an (in, in, out) mode over an (out, in, out) mode,
    %         but not necessarily over an (out, out, in) mode;
    %       - we prefer a (ground -> ...) mode over a (any -> ...) mode;
    %       - we prefer a (bound(f) -> ...) mode over a (ground -> ...) mode;
    %       - we prefer a (... -> dead) mode over a (... -> not dead) mode.
    %
    %       Also, we prefer a (any -> ...) mode over a (free -> ...) mode,
    %       unless the actual argument is free, in which case we prefer
    %       the (free -> ...) mode.
    %
    %   2.  If neither is prefered over the other by step 1, then prioritize
    %       them by determinism, according to the standard partial order
    %       (best first):
    %
    %                           erroneous
    %                          /       \
    %                       det         failure
    %                     /    \       /
    %                 multi     semidet
    %                    \      /
    %                     nondet
    %
    %   3.  If there are still multiple possibilities, take them in
    %       declaration order.
    %
:- pred choose_best_match(mode_info::in, list(proc_mode)::in, pred_id::in,
    proc_table::in, list(prog_var)::in, proc_id::out, inst_var_sub::out,
    list(mer_mode)::out) is det.

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

:- implementation.

:- import_module check_hlds.inst_match.
:- import_module check_hlds.mode_util.
:- import_module check_hlds.modecheck_util.
:- import_module hlds.hlds_proc_util.
:- import_module parse_tree.prog_detism.
:- import_module parse_tree.var_table.

:- import_module bool.
:- import_module map.
:- import_module maybe.
:- import_module require.
:- import_module term.

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

modes_are_indistinguishable(ModuleInfo, PredInfo, ProcId, OtherProcId) :-
    % The code of this predicate is similar to the code for
    % modes_are_identical/4 and compare_proc/5 below.
    pred_info_get_proc_table(PredInfo, Procs),
    map.lookup(Procs, ProcId, ProcInfo),
    map.lookup(Procs, OtherProcId, OtherProcInfo),

    % Compare the initial insts of the arguments.
    proc_info_get_argmodes(ProcInfo, ProcArgModes),
    proc_info_get_argmodes(OtherProcInfo, OtherProcArgModes),
    mode_list_get_initial_insts(ModuleInfo, ProcArgModes, InitialInsts),
    mode_list_get_initial_insts(ModuleInfo, OtherProcArgModes,
        OtherInitialInsts),
    pred_info_get_arg_types(PredInfo, ArgTypes),
    compare_inst_list(ModuleInfo, InitialInsts, OtherInitialInsts, no,
        ArgTypes, CompareInsts),
    CompareInsts = same,

    % Compare the expected livenesses of the arguments.
    get_arg_lives(ModuleInfo, ProcArgModes, ProcArgLives),
    get_arg_lives(ModuleInfo, OtherProcArgModes, OtherProcArgLives),
    compare_liveness_list(ProcArgLives, OtherProcArgLives, CompareLives),
    CompareLives = same,

    % Compare the determinisms; if both are cc_, or if both are not cc_,
    % then they are indistinguishable.
    proc_info_interface_determinism(ProcInfo, Detism),
    proc_info_interface_determinism(OtherProcInfo, OtherDetism),
    determinism_components(Detism, _CanFail, Solns),
    determinism_components(OtherDetism, _OtherCanFail, OtherSolns),
    ( Solns = at_most_many_cc, OtherSolns = at_most_many_cc
    ; Solns \= at_most_many_cc, OtherSolns \= at_most_many_cc
    ).

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

modes_are_identical_bar_cc(ModuleInfo, PredInfo, ProcId, OtherProcId) :-
    % The code of this predicate is similar to the code for
    % compare_proc/5 below and modes_are_indistinguishable/4 above.

    pred_info_get_proc_table(PredInfo, Procs),
    map.lookup(Procs, ProcId, ProcInfo),
    map.lookup(Procs, OtherProcId, OtherProcInfo),

    % Compare the initial insts of the arguments
    proc_info_get_argmodes(ProcInfo, ProcArgModes),
    proc_info_get_argmodes(OtherProcInfo, OtherProcArgModes),
    mode_list_get_initial_insts(ModuleInfo, ProcArgModes, InitialInsts),
    mode_list_get_initial_insts(ModuleInfo, OtherProcArgModes,
        OtherInitialInsts),
    pred_info_get_arg_types(PredInfo, ArgTypes),
    compare_inst_list(ModuleInfo, InitialInsts, OtherInitialInsts, no,
        ArgTypes, CompareInitialInsts),
    CompareInitialInsts = same,

    % Compare the final insts of the arguments
    mode_list_get_final_insts(ModuleInfo, ProcArgModes, FinalInsts),
    mode_list_get_final_insts(ModuleInfo, OtherProcArgModes,
        OtherFinalInsts),
    compare_inst_list(ModuleInfo, FinalInsts, OtherFinalInsts, no,
        ArgTypes, CompareFinalInsts),
    CompareFinalInsts = same,

    % Compare the expected livenesses of the arguments
    get_arg_lives(ModuleInfo, ProcArgModes, ProcArgLives),
    get_arg_lives(ModuleInfo, OtherProcArgModes, OtherProcArgLives),
    compare_liveness_list(ProcArgLives, OtherProcArgLives, CompareLives),
    CompareLives = same,

    % Compare the determinisms, ignoring the cc part.
    proc_info_interface_determinism(ProcInfo, Detism),
    proc_info_interface_determinism(OtherProcInfo, OtherDetism),
    determinism_components(Detism, CanFail, Solns),
    determinism_components(OtherDetism, OtherCanFail, OtherSolns),
    CanFail = OtherCanFail,
    ( Solns = OtherSolns
    ; Solns = at_most_many_cc, OtherSolns = at_most_many
    ; Solns = at_most_many, OtherSolns = at_most_many_cc
    ).

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

choose_best_match(_, [], _, _, _, _, _, _) :-
    unexpected($pred, "no best match").
choose_best_match(ModeInfo, [ProcMode | ProcModes], PredId,
        Procs, ArgVars, TheProcId, TheInstVarSub, TheArgModes) :-
    ProcMode = proc_mode(ProcId, InstVarSub, ArgModes),
    % ProcMode is best iff there is no other proc_mode which is better.
    ( if
        some [OtherProcId] (
            list.member(proc_mode(OtherProcId, _, _), ProcModes),
            compare_proc(ModeInfo, OtherProcId, ProcId, ArgVars, Procs, better)
        )
    then
        choose_best_match(ModeInfo, ProcModes, PredId, Procs, ArgVars,
            TheProcId, TheInstVarSub, TheArgModes)
    else
        TheProcId = ProcId,
        TheInstVarSub = InstVarSub,
        TheArgModes = ArgModes
    ).

    % Given two modes of a predicate, figure out whether one of them is a
    % better match than the other, for calls which could match either mode.
    %
:- pred compare_proc(mode_info::in, proc_id::in, proc_id::in,
    list(prog_var)::in, proc_table::in, match::out) is det.

compare_proc(ModeInfo, ProcId, OtherProcId, ArgVars, Procs, Compare) :-
    % The code of this predicate is similar to the code for
    % modes_are_indistinguishable/4 and modes_are_identical_bar_cc/4 above.

    map.lookup(Procs, ProcId, ProcInfo),
    map.lookup(Procs, OtherProcId, OtherProcInfo),

    % Compare the initial insts of the arguments.
    proc_info_get_argmodes(ProcInfo, ProcArgModes),
    proc_info_get_argmodes(OtherProcInfo, OtherProcArgModes),
    mode_info_get_module_info(ModeInfo, ModuleInfo),
    mode_info_get_var_table(ModeInfo, VarTable),
    lookup_var_types(VarTable, ArgVars, ArgTypes),
    mode_list_get_initial_insts(ModuleInfo, ProcArgModes, InitialInsts),
    mode_list_get_initial_insts(ModuleInfo, OtherProcArgModes,
        OtherInitialInsts),
    get_var_insts(ModeInfo, ArgVars, ArgInitialInsts),
    compare_inst_list(ModuleInfo, InitialInsts, OtherInitialInsts,
        yes(ArgInitialInsts), ArgTypes, CompareInsts),

    % Compare the expected livenesses of the arguments.
    get_arg_lives(ModuleInfo, ProcArgModes, ProcArgLives),
    get_arg_lives(ModuleInfo, OtherProcArgModes, OtherProcArgLives),
    compare_liveness_list(ProcArgLives, OtherProcArgLives, CompareLives),

    % Compare the determinisms.
    proc_info_interface_determinism(ProcInfo, Detism),
    proc_info_interface_determinism(OtherProcInfo, OtherDetism),
    determinism_components(Detism, CanFail, SolnCount),
    determinism_components(OtherDetism, OtherCanFail, OtherSolnCount),
    compare_solncounts(SolnCount, OtherSolnCount, CompareSolnCounts),
    (
        CompareSolnCounts = first_tighter_than,
        CompareDet = better
    ;
        CompareSolnCounts = first_same_as,
        compare_canfails(CanFail, OtherCanFail, CompareCanFails),
        (
            CompareCanFails = first_tighter_than,
            CompareDet = better
        ;
            CompareCanFails = first_same_as,
            CompareDet = same
        ;
            CompareCanFails = first_looser_than,
            CompareDet = worse
        )
    ;
        CompareSolnCounts = first_looser_than,
        CompareDet = worse
    ),

    % Combine the results, with the insts & lives comparisons
    % taking priority over the determinism comparison.
    combine_results(CompareInsts, CompareLives, Compare0),
    prioritized_combine_results(Compare0, CompareDet, Compare).

:- pred get_var_insts(mode_info::in, list(prog_var)::in,
    list(mer_inst)::out) is det.

get_var_insts(_, [], []).
get_var_insts(ModeInfo, [Var | Vars], [Inst | Insts]) :-
    get_var_inst(ModeInfo, Var, Inst),
    get_var_insts(ModeInfo, Vars, Insts).

:- pred compare_inst_list(module_info::in,
    list(mer_inst)::in, list(mer_inst)::in,
    maybe(list(mer_inst))::in, list(mer_type)::in, match::out) is det.

compare_inst_list(ModuleInfo, InstsA, InstsB, ArgInsts, Types, Result) :-
    ( if
        compare_inst_list_2(ModuleInfo, InstsA, InstsB, ArgInsts,
            Types, Result0)
    then
        Result = Result0
    else
        unexpected($pred, "length mismatch")
    ).

:- pred compare_inst_list_2(module_info::in,
    list(mer_inst)::in, list(mer_inst)::in,
    maybe(list(mer_inst))::in, list(mer_type)::in, match::out) is semidet.

compare_inst_list_2(_, [], [], _, [], same).
compare_inst_list_2(ModuleInfo, [InstA | InstsA], [InstB | InstsB],
        no, [Type | Types], Result) :-
    compare_inst(ModuleInfo, InstA, InstB, no, Type, Result0),
    compare_inst_list_2(ModuleInfo, InstsA, InstsB, no, Types, Result1),
    combine_results(Result0, Result1, Result).
compare_inst_list_2(ModuleInfo, [InstA | InstsA], [InstB | InstsB],
        yes([ArgInst | ArgInsts]), [Type | Types], Result) :-
    compare_inst(ModuleInfo, InstA, InstB, yes(ArgInst), Type, Result0),
    compare_inst_list_2(ModuleInfo, InstsA, InstsB, yes(ArgInsts), Types,
        Result1),
    combine_results(Result0, Result1, Result).

:- pred compare_liveness_list(list(is_live)::in, list(is_live)::in, match::out)
    is det.

compare_liveness_list([], [], same).
compare_liveness_list([_ | _], [], _) :-
    unexpected($pred, "length mismatch").
compare_liveness_list([], [_ | _], _) :-
    unexpected($pred, "length mismatch").
compare_liveness_list([LiveA | LiveAs], [LiveB | LiveBs], Result) :-
    compare_liveness(LiveA, LiveB, Result0),
    compare_liveness_list(LiveAs, LiveBs, Result1),
    combine_results(Result0, Result1, Result).

    % Compare_liveness -- prefer dead to live. If either is a valid match,
    % then the actual argument must be dead, so prefer the mode which can take
    % advantage of that).
    %
:- pred compare_liveness(is_live::in, is_live::in, match::out) is det.

compare_liveness(is_dead, is_dead, same).
compare_liveness(is_dead, is_live, better).
compare_liveness(is_live, is_dead, worse).
compare_liveness(is_live, is_live, same).

    % Combine two results, giving priority to the first one.
    %
:- pred prioritized_combine_results(match::in, match::in, match::out) is det.

prioritized_combine_results(better, _, better).
prioritized_combine_results(worse, _, worse).
prioritized_combine_results(same, Result, Result).
prioritized_combine_results(incomparable, _, incomparable).

    % Combine two results, giving them equal priority.
    %
:- pred combine_results(match::in, match::in, match::out) is det.

combine_results(better, better, better).
combine_results(better, same, better).
combine_results(better, worse, incomparable).
combine_results(better, incomparable, incomparable).
combine_results(worse, worse, worse).
combine_results(worse, same, worse).
combine_results(worse, better, incomparable).
combine_results(worse, incomparable, incomparable).
combine_results(same, Result, Result).
combine_results(incomparable, _, incomparable).

    % Compare two initial insts, to figure out which would be a better match.
    %
    % More information is better:
    %   prefer bound(f) to ground
    %   prefer unique to mostly_unique or ground, and
    %   prefer mostly_unique to ground
    %       (unique > mostly_unique > shared > mostly_dead > dead)
    % More bound is better:
    %       (if both can match, the one which is more bound
    %       is better, because it may be an exact match, whereas
    %       the other one would be an implied mode)
    %   prefer ground to free   (i.e. prefer in to out)
    %   prefer ground to any    (e.g. prefer in to in(any))
    %   prefer any to free  (e.g. prefer any->ground to out)
    %
:- pred compare_inst(module_info::in, mer_inst::in, mer_inst::in,
    maybe(mer_inst)::in, mer_type::in, match::out) is det.

compare_inst(ModuleInfo, InstA, InstB, MaybeArgInst, Type, Result) :-
    % inst_matches_initial(A,B) succeeds iff
    %   A specifies at least as much information
    %   and at least as much binding as B --
    %   with the exception that `any' matches_initial `free'
    %   and perhaps vice versa.
    ( if inst_matches_initial(ModuleInfo, Type, InstA, InstB) then
        A_mi_B = yes
    else
        A_mi_B = no
    ),
    ( if inst_matches_initial(ModuleInfo, Type, InstB, InstA) then
        B_mi_A = yes
    else
        B_mi_A = no
    ),
    ( A_mi_B = yes, B_mi_A = no,  Result = better
    ; A_mi_B = no,  B_mi_A = yes, Result = worse
    ; A_mi_B = no,  B_mi_A = no,  Result = incomparable
    ; A_mi_B = yes, B_mi_A = yes,
        % We need to further disambiguate the cases involving `any' and `free',
        % since `any' matches_initial `free' and vice versa.
        % For these cases, we want to take the actual inst of the argument
        % into account: if the argument is `free', we should prefer `free',
        % but otherwise, we should prefer `any'.
        %
        (
            MaybeArgInst = no,
            Result0 = same
        ;
            MaybeArgInst = yes(ArgInst),
            ( if
                inst_matches_initial_no_implied_modes(ModuleInfo, Type,
                    ArgInst, InstA)
            then
                Arg_mf_A = yes
            else
                Arg_mf_A = no
            ),
            ( if
                inst_matches_initial_no_implied_modes(ModuleInfo, Type,
                    ArgInst, InstB)
            then
                Arg_mf_B = yes
            else
                Arg_mf_B = no
            ),
            ( Arg_mf_A = yes, Arg_mf_B = no,  Result0 = better
            ; Arg_mf_A = no,  Arg_mf_B = yes, Result0 = worse
            ; Arg_mf_A = yes, Arg_mf_B = yes, Result0 = same
            ; Arg_mf_A = no,  Arg_mf_B = no,  Result0 = same
            )
        ),
        (
            Result0 = same,
            % If the actual arg inst is not available, or comparing with
            % the arg inst doesn't help, then compare the two proc insts.
            ( if
                inst_matches_initial_no_implied_modes(ModuleInfo, Type,
                    InstA, InstB)
            then
                A_mf_B = yes
            else
                A_mf_B = no
            ),
            ( if
                inst_matches_initial_no_implied_modes(ModuleInfo, Type,
                    InstB, InstA)
            then
                B_mf_A = yes
            else
                B_mf_A = no
            ),
            ( A_mf_B = yes, B_mf_A = no,  Result = better
            ; A_mf_B = no,  B_mf_A = yes, Result = worse
            ; A_mf_B = no,  B_mf_A = no,  Result = incomparable
            ; A_mf_B = yes, B_mf_A = yes, Result = same
            )
        ;
            ( Result0 = better
            ; Result0 = worse
            ),
            Result = Result0
        )
    ).

%---------------------------------------------------------------------------%
:- end_module check_hlds.mode_comparison.
%---------------------------------------------------------------------------%
