%---------------------------------------------------------------------------%
% vim: ts=4 sw=4 et ft=mercury
%---------------------------------------------------------------------------%
% Copyright (C) 2005-2008, 2010-2012 The University of Melbourne.
% Copyright (C) 2014-2015, 2017-2018, 2021-2025 The Mercury team.
% This file is distributed under the terms specified in COPYING.LIB.
%---------------------------------------------------------------------------%
%
% File: trace_counts.m.
% Main author: wangp.
% Modifications by zs and maclarty.
%
% This module defines operations on execution trace summaries
% generated by programs compiled using the compiler's tracing options.
%
%---------------------------------------------------------------------------%

:- module mdbcomp.trace_counts.
:- interface.

:- import_module mdbcomp.goal_path.
:- import_module mdbcomp.prim_data.
:- import_module mdbcomp.sym_name.

:- import_module list.
:- import_module map.

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

:- type all_or_nonzero
    --->    user_all
            % The file contains counts for all labels from user-defined
            % procedures.

    ;       user_nonzero.
            % The file contains counts for all labels from user-defined
            % procedures, provided the count is nonzero.

:- type base_count_file_type
    --->    base_count_file_type(all_or_nonzero, string).
            % The first argument says whether we have all the counts;
            % the second gives the name of the program.

:- type trace_count_file_type
    --->    single_file(base_count_file_type)
            % The file contains counts from a single execution.

    ;       union_file(int, list(trace_count_file_type))
            % The file is a union of some other trace count files.
            % The number of test cases in the union is recorded, and
            % so is the set of kinds of trace count files they came from.
            % (We represent the set as a sorted list, because we write out
            % values of trace_count_file_type to files, and we don't want to
            % expose the implementation of sets.)

    ;       diff_file(trace_count_file_type, trace_count_file_type).
            % The file is a difference between two other trace count files.

:- inst union_file for trace_count_file_type/0
    --->    union_file(ground, ground).

:- func sum_trace_count_file_type(trace_count_file_type, trace_count_file_type)
    = trace_count_file_type.
:- mode sum_trace_count_file_type(in(union_file), in) = out(union_file) is det.
:- mode sum_trace_count_file_type(in, in) = out is det.

:- type trace_counts == map(proc_label_in_context, proc_trace_counts).

:- type proc_label_in_context
    --->    proc_label_in_context(
                context_module_symname  :: sym_name,
                context_filename        :: string,
                proc_label              :: proc_label
            ).

:- type proc_trace_counts == map(path_port, line_no_and_count).

:- type path_port
    --->    port_only(trace_port)
    ;       path_only(reverse_goal_path)
    ;       port_and_path(trace_port, reverse_goal_path).

:- type line_no_and_count
    --->    line_no_and_count(
                line_number             :: int,
                exec_count              :: int,
                num_tests               :: int
            ).

:- func make_path_port(reverse_goal_path, trace_port) = path_port.

:- pred summarize_trace_counts_list(list(trace_counts)::in, trace_counts::out)
    is det.

:- pred sum_trace_counts(trace_counts::in, trace_counts::in, trace_counts::out)
    is det.

:- pred diff_trace_counts(trace_counts::in, trace_counts::in,
    trace_counts::out) is det.


:- pred restrict_trace_counts_to_module(module_name::in, trace_counts::in,
    trace_counts::out) is det.

    % Return the number of tests cases used to generate the trace counts with
    % the given list of file types.
    %
:- func calc_num_tests(list(trace_count_file_type)) = int.

    % Return the number of tests used to create a trace counts file of the
    % given type.
    %
:- func num_tests_for_file_type(trace_count_file_type) = int.

:- func trace_count_file_id = string.

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

:- implementation.

:- import_module int.
:- import_module set.

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

sum_trace_count_file_type(Type1, Type2) = UnionType :-
    (
        Type1 = single_file(_),
        Type2 = single_file(_),
        UnionType = union_file(2, sort_and_remove_dups([Type1, Type2]))
    ;
        Type1 = single_file(_),
        Type2 = union_file(N, IncludedTypes2),
        UnionType = union_file(N + 1,
            insert_into_list_as_set(IncludedTypes2, Type1))
    ;
        Type1 = single_file(_),
        Type2 = diff_file(_, _),
        UnionType = union_file(2, sort_and_remove_dups([Type1, Type2]))
    ;
        Type1 = union_file(N, IncludedTypes1),
        Type2 = single_file(_),
        UnionType = union_file(N + 1,
            insert_into_list_as_set(IncludedTypes1, Type2))
    ;
        Type1 = union_file(N1, IncludedTypes1),
        Type2 = union_file(N2, IncludedTypes2),
        UnionType = union_file(N1 + N2,
            sort_and_remove_dups(IncludedTypes1 ++ IncludedTypes2))
    ;
        Type1 = union_file(N, IncludedTypes1),
        Type2 = diff_file(_, _),
        UnionType = union_file(N + 1,
            insert_into_list_as_set(IncludedTypes1, Type2))
    ;
        Type1 = diff_file(_, _),
        Type2 = single_file(_),
        UnionType = union_file(2, sort_and_remove_dups([Type1, Type2]))
    ;
        Type1 = diff_file(_, _),
        Type2 = union_file(N, IncludedTypes2),
        UnionType = union_file(N + 1,
            insert_into_list_as_set(IncludedTypes2, Type1))
    ;
        Type1 = diff_file(_, _),
        Type2 = diff_file(_, _),
        UnionType = union_file(2, sort_and_remove_dups([Type1, Type2]))
    ).

:- func insert_into_list_as_set(list(T), T) = list(T).

insert_into_list_as_set(List0, Item) = List :-
    set.list_to_set(List0, Set0),
    set.insert(Item, Set0, Set),
    set.to_sorted_list(Set, List).

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

    % This function should be kept in sync with the MR_named_count_port array
    % in runtime/mercury_trace_base.c.
    %
make_path_port(_GoalPath, port_call) = port_only(port_call).
make_path_port(_GoalPath, port_exit) = port_only(port_exit).
make_path_port(_GoalPath, port_redo) = port_only(port_redo).
make_path_port(_GoalPath, port_fail) = port_only(port_fail).
make_path_port(GoalPath, port_tailrec_call) = path_only(GoalPath).
make_path_port(_GoalPath, port_exception) = port_only(port_exception).
make_path_port(GoalPath, port_ite_cond) = path_only(GoalPath).
make_path_port(GoalPath, port_ite_then) = path_only(GoalPath).
make_path_port(GoalPath, port_ite_else) = path_only(GoalPath).
make_path_port(GoalPath, port_neg_enter) =
    port_and_path(port_neg_enter, GoalPath).
make_path_port(GoalPath, port_neg_success) =
    port_and_path(port_neg_success, GoalPath).
make_path_port(GoalPath, port_neg_failure) =
    port_and_path(port_neg_failure, GoalPath).
make_path_port(GoalPath, port_disj_first) = path_only(GoalPath).
make_path_port(GoalPath, port_disj_later) = path_only(GoalPath).
make_path_port(GoalPath, port_switch) = path_only(GoalPath).
make_path_port(_GoalPath, port_user) = port_only(port_user).

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

summarize_trace_counts_list(TraceCountsList, TraceCounts) :-
    ( if TraceCountsList = [TraceCountsPrime] then
        % optimize the common case
        TraceCounts = TraceCountsPrime
    else
        list.foldl(sum_trace_counts, TraceCountsList, map.init, TraceCounts)
    ).

sum_trace_counts(TraceCountsA, TraceCountsB, TraceCounts) :-
    map.union(sum_proc_trace_counts, TraceCountsA, TraceCountsB, TraceCounts).

:- pred sum_proc_trace_counts(proc_trace_counts::in, proc_trace_counts::in,
    proc_trace_counts::out) is det.

sum_proc_trace_counts(ProcTraceCountsA, ProcTraceCountsB, ProcTraceCounts) :-
    ProcTraceCounts = map.union(sum_counts_on_line,
        ProcTraceCountsA, ProcTraceCountsB).

:- func sum_counts_on_line(line_no_and_count, line_no_and_count)
    = line_no_and_count.

sum_counts_on_line(LC1, LC2) = LC :-
    % We don't check that LineNumber1 = LineNumber2 since that does not
    % necessarily represent an error. (Consider the case when the two trace
    % files are derived from sources that are identical except for the addition
    % of a comment.)

    LC1 = line_no_and_count(LineNumber1, Count1, NumTests1),
    LC2 = line_no_and_count(_LineNumber, Count2, NumTests2),
    LC = line_no_and_count(LineNumber1, Count1 + Count2,
        NumTests1 + NumTests2).

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

diff_trace_counts(TraceCountsA, TraceCountsB, TraceCounts) :-
    map.foldl(diff_trace_counts_acc(TraceCountsB), TraceCountsA,
        map.init, TraceCounts).

:- pred diff_trace_counts_acc(trace_counts::in,
    proc_label_in_context::in, proc_trace_counts::in,
    trace_counts::in, trace_counts::out) is det.

diff_trace_counts_acc(TraceCountsB, ProcLabelInContextA, ProcTraceCountsA,
        !TraceCounts) :-
    ( if map.search(TraceCountsB, ProcLabelInContextA, ProcTraceCountsB) then
        ProcTraceCounts = diff_proc_counts(ProcTraceCountsA, ProcTraceCountsB),
        map.det_insert(ProcLabelInContextA, ProcTraceCounts, !TraceCounts)
    else
        map.det_insert(ProcLabelInContextA, ProcTraceCountsA, !TraceCounts)
    ).

:- func diff_proc_counts(proc_trace_counts, proc_trace_counts)
    = proc_trace_counts.

diff_proc_counts(ProcTraceCountsA, ProcTraceCountsB) = ProcTraceCounts :-
    map.foldl(diff_proc_counts_acc(ProcTraceCountsB), ProcTraceCountsA,
        map.init, ProcTraceCounts).

:- pred diff_proc_counts_acc(proc_trace_counts::in,
    path_port::in, line_no_and_count::in,
    proc_trace_counts::in, proc_trace_counts::out) is det.

diff_proc_counts_acc(ProcTraceCountsB, PathPortA, LineNoCountA,
        !ProcTraceCounts) :-
    ( if map.search(ProcTraceCountsB, PathPortA, LineNoCountB) then
        LineNoCount = diff_counts_on_line(LineNoCountA, LineNoCountB),
        map.det_insert(PathPortA, LineNoCount, !ProcTraceCounts)
    else
        map.det_insert(PathPortA, LineNoCountA, !ProcTraceCounts)
    ).

:- func diff_counts_on_line(line_no_and_count, line_no_and_count)
    = line_no_and_count.

diff_counts_on_line(LC1, LC2) = LC :-
    % We don't check that LineNumber1 = LineNumber2 since that does not
    % necessarily represent an error. (Consider the case when the two trace
    % files are derived from sources that are identical except for the addition
    % of a comment.)

    % The number of tests field doesn't make sense in the result of a diff
    % operation. We signal this fact by using a plainly dummy value.
    % XXX It would be better to use a different return type that
    % lacks this meaningless field. Or, sort-of equivalently, we could make
    % line_no_and_count take a type argument, which would specify the type
    % of the third field. This would be set to "int" when the third field
    % is meaningful, and to "unit" when it is not.

    LC1 = line_no_and_count(LineNumber1, Count1, _NumTests1),
    LC2 = line_no_and_count(_LineNumber, Count2, _NumTests2),
    LC = line_no_and_count(LineNumber1, Count1 - Count2, -1).

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

restrict_trace_counts_to_module(ModuleName, TraceCounts0, TraceCounts) :-
    map.foldl(restrict_trace_counts_2(ModuleName), TraceCounts0,
        map.init, TraceCounts).

:- pred restrict_trace_counts_2(module_name::in, proc_label_in_context::in,
    proc_trace_counts::in, trace_counts::in, trace_counts::out) is det.

restrict_trace_counts_2(ModuleName, ProcLabelInContext, ProcCounts,
        !TraceCounts) :-
    ProcLabel = ProcLabelInContext ^ proc_label,
    ( if ProcLabel = ordinary_proc_label(ModuleName, _, _, _, _, _) then
        map.det_insert(ProcLabelInContext, ProcCounts, !TraceCounts)
    else
        true
    ).

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

calc_num_tests([]) = 0.
calc_num_tests([FileType | Rest]) =
    num_tests_for_file_type(FileType) + calc_num_tests(Rest).

num_tests_for_file_type(union_file(N, _)) = N.
num_tests_for_file_type(single_file(_)) = 1.
num_tests_for_file_type(diff_file(_, _)) = -1.

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

trace_count_file_id = "Mercury trace counts file".

%---------------------------------------------------------------------------%
:- end_module mdbcomp.trace_counts.
%---------------------------------------------------------------------------%
