%-----------------------------------------------------------------------------e
% vim: ft=mercury ts=4 sw=4 et
%-----------------------------------------------------------------------------e
% Copyright (C) 2014, 2016, 2019, 2022-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.
%---------------------------------------------------------------------------%

:- module parse_tree.parse_error.
:- interface.

:- import_module parse_tree.error_spec.

:- import_module io.
:- import_module list.
:- import_module one_or_more.
:- import_module set.

    % 1. We obviously cannot process the module if we could not read it.
    %
    % 2. If the module contains some bad ":- module" declarations, then
    %    the identity of the module that contains any items following
    %    such declarations will be in doubt. The compiler can guess the
    %    identity of the module intended by the programmer only by chance,
    %    and any mistake will typically lead to an avalanche of misleading
    %    errors.
    %
    % 3. If the module contains some bad ":- end_module" declarations,
    %    then the same is true for any items following those declarations.
    %
    % Items follow most ":- module" declarations, but not most ":- end_module"
    % declarations, and if no items follow a bad ":- end_module" declaration,
    % then the badness of the declaration has no further consequences.
    % It would therefore be worthwhile making a distinction between bad
    % ":- end_module" declarations that have items following them, and those
    % that don't. Unfortunately, since each item is processed independently,
    % that is not trivial to do.
    %
:- type fatal_read_module_error
    --->    frme_could_not_find_file
            % We could not find the specified file.

    ;       frme_could_not_open_file
            % We could not open the specified file.

    ;       frme_could_not_read_file
            % We could open the specified file, but could not read its
            % contents.

    ;       frme_bad_submodule_start
            % We encountered a declaration for the start of a submodule,
            % but the name of the new submodule is not one that
            % can be immediately nested inside the current module.
            % NOTE This error can happen only for submodules of the main
            % module in the file. For the main module itself, we would generate
            % the rme_unexpected_module_name error.

    ;       frme_bad_module_end.
            % We encountered a declaration for the end of a module,
            % but the name of the ended module is not the name of the
            % until-then-current module.
            % NOTE This error can happen both for the main module of the
            % file and for its submodules.

    % This type represents the kinds of nonfatal errors that can happen
    % when we read in a Mercury module (which will be either a source file,
    % or an interface file).
    %
:- type nonfatal_read_module_error
    --->    rme_unexpected_module_name
            % The file starts with a module declaration for a module
            % other than the one we expected.

    ;       rme_no_module_decl_at_start
            % The file does not start with a module declaration at all.

    ;       rme_no_section_decl_at_start
            % The module does not start with either an interface or an
            % implementation section marker.

    ;       rme_end_module_not_at_end_of_src
            % The source code of a module has at least one term
            % after the end_module marker for the main module.

    ;       rme_unexpected_term_in_int_or_opt
            % The interface or optimization file of a module has at least one
            % term that is not expected in such a file.

    ;       rme_could_not_read_term
    ;       rme_could_not_parse_item
            % When we attempted to read an item from the file, we got a failure
            % either in the first stage of parsing (reading in a term, i.e.
            % converting a subsequence of the character in the file to a term),
            % or the second stage of parsing (converting that term to an item).
            % Since the rest of the compiler should not care whether parsing
            % is done in one or two stages, it should treat both these errors
            % the same. We distinguish them only for completeness.

    ;       rme_cannot_find_modify_time
            % We cannot find out the modification time of he file.
            % This error is not fatal, because its only effect is
            % to disable smart recompilation, whose implementation
            % is not completed yet.

    ;       rme_nec.
            % A read module error that is Not Elsewhere Classified, i.e.
            % is not one of the error kinds listed above.
            %
            % Before the change away from read_module_errors being just a set
            % of read_module_error values, we used to not include these errors
            % in the set at all, which could lead to an empty set of
            % read_module_error values being paired with a nonempty list
            % of severity_error error_specs.

    % This type represents the set of errors that were encountered
    % during an attempt to read in a Mercury module's source file,
    % interface file, or optimization file.
    %
    % There are two kinds of tests that code will typically perform
    % on values of this type.
    %
    % 1. Are there any errors?
    % 2. Are there any FATAL errors?
    %
    % You can use there_are_no_errors and there_are_some_errors for
    % the first kind of test, and a direct invocation of an emptiness test
    % on the rm_fatal_errors field for the second kind.
    %
:- type read_module_errors
    --->    read_module_errors(
                % The fatal errors we have encountered, and their messages.
                % All these error_specs should have severity_error.
                rm_fatal_errors         :: set(fatal_read_module_error),
                rm_fatal_error_specs    :: list(error_spec),

                % The nonfatal errors we have encountered, and their messages.
                % All these error_specs should have severity_error.
                rm_nonfatal_errors      :: set(nonfatal_read_module_error),
                rm_nonfatal_error_specs :: list(error_spec),

                % The warnings we have encountered. All these should have
                % severity levels *below* severity_error.
                rm_warning_specs        :: list(error_spec)
            ).

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

    % Return a structure that records no errors (so far).
    %
:- func init_read_module_errors = read_module_errors.

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

    % Add a fatal error, and its message.
    %
:- pred add_fatal_error(fatal_read_module_error::in, list(error_spec)::in,
    read_module_errors::in, read_module_errors::out) is det.

    % Add nonfatal errors, and their messages.
    %
:- pred add_nonfatal_error(nonfatal_read_module_error::in,
    error_spec::in,
    read_module_errors::in, read_module_errors::out) is det.
:- pred add_nonfatal_errors(nonfatal_read_module_error::in,
    one_or_more(error_spec)::in,
    read_module_errors::in, read_module_errors::out) is det.

    % If there are any error_specs in the input list,
    % record them as representing a not-elsewhere-classified nonfatal error.
    %
:- pred add_any_nec_errors(list(error_spec)::in,
    read_module_errors::in, read_module_errors::out) is det.

    % Add some warning messages.
    %
:- pred add_warning(list(error_spec)::in,
    read_module_errors::in, read_module_errors::out) is det.

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

    % Succeed if the read_module_errors structure records zero errors.
    % (Though it may contain warnings.)
    %
:- pred there_are_no_errors(read_module_errors::in) is semidet.

    % Succeed if the read_module_errors structure records at least one error.
    %
:- pred there_are_some_errors(read_module_errors::in) is semidet.

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

    % Return all the error_specs in the argument, regardless of severity.
    %
:- func get_read_module_specs(read_module_errors) = list(error_spec).

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

:- pred io_error_to_error_spec(spec_phase::in, string::in, error_spec::out,
    io::di, io::uo) is det.

:- pred io_error_to_read_module_errors(fatal_read_module_error::in,
    spec_phase::in, string::in, read_module_errors::out,
    io::di, io::uo) is det.

%---------------------------------------------------------------------------%
:- implementation.
%---------------------------------------------------------------------------%

init_read_module_errors = read_module_errors(set.init, [], set.init, [], []).

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

add_fatal_error(Error, Specs, Errors0, Errors) :-
    Errors0 = read_module_errors(FatalErrors0, FatalSpecs0,
        NonFatalErrors, NonFatalSpecs, WarningSpecs),
    set.insert(Error, FatalErrors0, FatalErrors),
    FatalSpecs = Specs ++  FatalSpecs0,
    Errors = read_module_errors(FatalErrors, FatalSpecs,
        NonFatalErrors, NonFatalSpecs, WarningSpecs).

add_nonfatal_error(Error, Spec, Errors0, Errors) :-
    add_nonfatal_errors(Error, one_or_more(Spec, []), Errors0, Errors).

add_nonfatal_errors(Error, OoMSpecs, Errors0, Errors) :-
    Errors0 = read_module_errors(FatalErrors, FatalSpecs,
        NonFatalErrors0, NonFatalSpecs0, WarningSpecs),
    set.insert(Error, NonFatalErrors0, NonFatalErrors),
    NonFatalSpecs = one_or_more_to_list(OoMSpecs) ++ NonFatalSpecs0,
    Errors = read_module_errors(FatalErrors, FatalSpecs,
        NonFatalErrors, NonFatalSpecs, WarningSpecs).

add_any_nec_errors(Specs, !Errors) :-
    (
        Specs = []
    ;
        Specs = [HeadSpec | TailSpecs],
        add_nonfatal_errors(rme_nec, one_or_more(HeadSpec, TailSpecs), !Errors)
    ).

add_warning(Specs, Errors0, Errors) :-
    Errors0 = read_module_errors(FatalErrors, FatalSpecs,
        NonFatalErrors, NonFatalSpecs, WarningSpecs0),
    WarningSpecs = Specs ++ WarningSpecs0,
    Errors = read_module_errors(FatalErrors, FatalSpecs,
        NonFatalErrors, NonFatalSpecs, WarningSpecs).

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

there_are_no_errors(Errors) :-
    Errors = read_module_errors(FatalErrors, _, NonFatalErrors, _, _),
    set.is_empty(FatalErrors),
    set.is_empty(NonFatalErrors).

there_are_some_errors(Errors) :-
    Errors = read_module_errors(FatalErrors, _, NonFatalErrors, _, _),
    ( set.is_non_empty(FatalErrors)
    ; set.is_non_empty(NonFatalErrors)
    ).

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

get_read_module_specs(Errors) = Specs :-
    Errors = read_module_errors(_FatalErrors, FatalSpecs,
        _NonFatalErrors, NonFatalSpecs, WarningSpecs),
    Specs = FatalSpecs ++ NonFatalSpecs ++ WarningSpecs.

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

io_error_to_error_spec(Phase, ErrorMsg, Spec, !IO) :-
    io.progname_base("mercury_compile", ProgName, !IO),
    Pieces = [fixed(ProgName), suffix(":"), words(ErrorMsg), nl],
    Spec = no_ctxt_spec($pred, severity_error, Phase, Pieces).

io_error_to_read_module_errors(FatalError, Phase, ErrorMsg, Errors, !IO) :-
    io_error_to_error_spec(Phase, ErrorMsg, Spec, !IO),
    Errors0 = init_read_module_errors,
    add_fatal_error(FatalError, [Spec], Errors0, Errors).

%---------------------------------------------------------------------------%
:- end_module parse_tree.parse_error.
%---------------------------------------------------------------------------%
