%---------------------------------------------------------------------------%
% vim: ft=mercury ts=4 sw=4 et
%---------------------------------------------------------------------------%
% Copyright (C) 2015-2016, 2019, 2021-2024  The Mercury team.
% This file may only be copied under the terms of the GNU General
% Public License - see the file COPYING in the Mercury distribution.
%---------------------------------------------------------------------------%
%
% This module defines the data structures we use to record
% what entities are available from which modules and with what permissions.
%

:- module parse_tree.module_qual.id_set.
:- interface.

:- import_module mdbcomp.
:- import_module mdbcomp.sym_name.
:- import_module parse_tree.error_spec.
:- import_module parse_tree.module_qual.mq_info.
:- import_module parse_tree.module_qual.qual_errors.
:- import_module parse_tree.prog_data.

:- import_module list.

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

    % We keep track of these kinds of entities.
:- type qual_id_kind
    --->    qual_id_type
    ;       qual_id_inst
    ;       qual_id_mode
    ;       qual_id_class.

    % This identifies an entity among other entities of the same kind.
:- type mq_id
    --->    mq_id(sym_name, int).

%---------------------------------------------------------------------------%
%
% We record two kinds of permissions for each entity:
%
% - whether it may be used in the interface, and
% - whether it may be used without full module qualification.
%
% An entity may be used in the interface if it is defined in the current
% module or one of its ancestors, or if it is defined in a module that is
% imported (or used) in the interface.
%
% An entity may be used without full module qualification if it is defined
% in the current module, in one of its ancestors, or in a module that is
% the subject of an explicit `import_module' declaration in the current module.
% It may not be used without full qualification if it is made available by
% a `use_module' declaration, or if it is defined in a `.opt' and `.trans_opt'
% file.
%
% The two are not independent: an entity may be usable in the interface
% only if fully qualified (if it is defined in a module that has a
% `use_module' declaration for it in the interface), while it may be
% usable in the implementation even if not fully qualified (if that defining
% module has an `import_module' declaration in the implementation.)
%

:- type module_permissions
    --->    module_permissions(
                mp_in_int               :: perm_in_int,
                mp_in_imp               :: perm_in_imp
            ).

:- type perm_in_int
    --->    may_not_use_in_int
    ;       may_use_in_int(need_qualifier).

:- type perm_in_imp
    --->    may_use_in_imp(need_qualifier).

    % When we process types, typeclasses, insts or modes, we need to know
    % whether they occur in the interface of the current module. This is
    % so that if we see e.g. m1.t1 in the interface, we can mark module m1
    % as being used in the interface, so we can avoid generating a warning
    % about m1 being unused in the interface.
    %
:- type mq_in_interface
    --->    mq_not_used_in_interface
    ;       mq_used_in_interface.

%---------------------------------------------------------------------------%
%
% An id_set represents the set of entities of a particular kind
% whose definitions we have seen so far, and which are therefore available
% to resolve any ambiguities in unqualified references.
%
% Modules don't have an arity, but for simplicity we use the same
% data structure for modules as for types etc, assigning arity zero
% to all module names.
%

:- type id_set.

:- type type_id_set == id_set.
:- type inst_id_set == id_set.
:- type mode_id_set == id_set.
:- type class_id_set == id_set.
:- type module_id_set == id_set.

%---------------------------------------------------------------------------%
%
% The operations on id_sets.
%

:- pred id_set_init(id_set::out) is det.

    % Insert an mq_id into an id_set, aborting with an error if the
    % mq_id is not module qualified.
    %
:- pred id_set_insert(module_permissions::in, mq_id::in,
    id_set::in, id_set::out) is det.

:- pred get_names_in_id_set(id_set::in, list(string)::out) is det.

    % Find the unique match in the current name space for a given mq_id
    % from a list of ids. If none exists, either because no match was found
    % or multiple matches were found, report an error.
    %
    % This predicate assumes that type_ids, inst_ids, mode_ids and
    % class_ids have the same representation.
    %
:- pred find_unique_match(mq_in_interface::in, mq_error_context::in,
    id_set::in, qual_id_kind::in, mq_id::in, sym_name::out,
    mq_info::in, mq_info::out,
    list(error_spec)::in, list(error_spec)::out) is det.

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

    % Check whether the parent module was imported, given the name of a
    % child (or grandchild, etc.) module occurring in that parent module.
    %
:- pred parent_module_is_imported(mq_in_interface::in, module_id_set::in,
    module_name::in, module_name::in) is semidet.

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

:- implementation.

:- import_module recompilation.
:- import_module recompilation.item_types.
:- import_module recompilation.record_uses.

:- import_module assoc_list.
:- import_module bool.
:- import_module map.
:- import_module maybe.
:- import_module pair.
:- import_module require.
:- import_module set.
:- import_module set_tree234.

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

% We want efficient retrieval of all the modules which define an id
% with a certain name and arity, and we want support for the generation
% of useful diagnostics for slighly-wrong references. We therefore implement
% each id_set as a three stage map from
%
% - first the base name of an entity,
% - and then its arity,
% - and then its module name,
%
% to the permissions for the sym_name_arity we can construct for these.
%
% Having these three stages is good for performance. The first stage
% does simple, fast comparisons but nevertheless drastically reduces
% the size of the remaining search space. The second and third stages are
% usually done in very small trees, which means that having to do comparisons
% on the relatively complex structure of module names (when compared to
% strings and ints) does not slow us down.
%
% The three stages also help with the construction of the useful diagnostics.
%
% Going through just the first two stages allows us to see which modules
% define an entity with the given base name and arity. This allows us
% diagnostics to point out e.g. matches for a name/arity pair that this
% module knows about (usually due to reading them in from .int2 files)
% that are not visible to the module being compiled due to missing imports.
%
% Going through just the first stage allows us to see which arities
% have definitions for the given base name. This allows diagnostics to
% point out e.g. how many arguments too few or too many a call has.
%
% Going through none of the stages allow us to see which names exist
% for this kind of entity. This allows us to print "did you mean" messages
% that list similar names that the programmer may have meant to use.

:- type id_set == map(string, map(arity, permissions_map)).
:- type permissions_map == map(module_name, module_permissions).

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

id_set_init(IdSet) :-
    map.init(IdSet).

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

id_set_insert(Permissions, MQId, !IdSet) :-
    MQId = mq_id(SymName, Arity),
    (
        SymName = unqualified(_),
        unexpected($pred, "unqualified id")
    ;
        SymName = qualified(ModuleName, BaseName),
        % Most of the time, BaseName does not occur in !.IdSet.
        % We therefore try the insertion first, and only if it fails
        % do we update the existing entry that caused that failure.
        FreshPermissionsMap = map.singleton(ModuleName, Permissions),
        FreshSubMap = map.singleton(Arity, FreshPermissionsMap),
        ( if map.insert(BaseName, FreshSubMap, !IdSet) then
            true
        else
            map.lookup(!.IdSet, BaseName, SubMap0),
            ( if map.search(SubMap0, Arity, PermissionsMap0) then
                insert_into_permissions_map(Permissions, ModuleName,
                    PermissionsMap0, PermissionsMap),
                map.det_update(Arity, PermissionsMap, SubMap0, SubMap),
                map.det_update(BaseName, SubMap, !IdSet)
            else
                map.det_insert(Arity, FreshPermissionsMap, SubMap0, SubMap),
                map.det_update(BaseName, SubMap, !IdSet)
            )
        )
    ).

:- pred insert_into_permissions_map(module_permissions::in, module_name::in,
    permissions_map::in, permissions_map::out) is det.

insert_into_permissions_map(NewPermissions, ModuleName, !PermissionsMap) :-
    ( if map.search(!.PermissionsMap, ModuleName, OldPermissions) then
        % Grant the permissions granted by either OldPermissions or
        % NewPermissions.
        OldPermissions = module_permissions(OldPermInt, OldPermImp),
        NewPermissions = module_permissions(NewPermInt, NewPermImp),
        (
            OldPermInt = may_not_use_in_int,
            PermInt = NewPermInt
        ;
            OldPermInt = may_use_in_int(OldIntNeedQual),
            (
                NewPermInt = may_not_use_in_int,
                PermInt = OldPermInt
            ;
                NewPermInt = may_use_in_int(NewIntNeedQual),
                need_qual_only_if_both(OldIntNeedQual, NewIntNeedQual,
                    IntNeedQual),
                PermInt = may_use_in_int(IntNeedQual)
            )
        ),
        OldPermImp = may_use_in_imp(OldImpNeedQual),
        NewPermImp = may_use_in_imp(NewImpNeedQual),
        need_qual_only_if_both(OldImpNeedQual, NewImpNeedQual, ImpNeedQual),
        PermImp = may_use_in_imp(ImpNeedQual),

        % Update the entry only if it changed.
        ( if
            PermInt = OldPermInt,
            PermImp = OldPermImp
        then
            true
        else
            Permissions = module_permissions(PermInt, PermImp),
            map.det_update(ModuleName, Permissions, !PermissionsMap)
        )
    else
        map.det_insert(ModuleName, NewPermissions, !PermissionsMap)
    ).

:- pred need_qual_only_if_both(need_qualifier::in, need_qualifier::in,
    need_qualifier::out) is det.

need_qual_only_if_both(NeedQualA, NeedQualB, NeedQual) :-
    ( if
        NeedQualA = must_be_qualified,
        NeedQualB = must_be_qualified
    then
        NeedQual = must_be_qualified
    else
        NeedQual = may_be_unqualified
    ).

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

get_names_in_id_set(IdSet, Names) :-
    map.sorted_keys(IdSet, Names).

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

find_unique_match(InInt, ErrorContext, IdSet, IdType, Id0, SymName,
        !Info, !Specs) :-
    % Find all IDs which match the current id.
    Id0 = mq_id(SymName0, Arity),
    BaseName = unqualify_name(SymName0),
    id_set_search_sym_arity(InInt, IdSet, SymName0, BaseName, Arity,
        Matches, IntMismatches, QualMismatches),
    (
        Matches = [],
        % No matches for this id.
        MaybeUniqModuleName = no,
        mq_info_get_should_report_errors(!.Info, ReportErrors),
        (
            ReportErrors = should_report_errors,
            mq_info_record_undef_mq_id(IdType, Id0, !Info),

            mq_info_get_this_module(!.Info, ThisModuleName),
            id_set_search_sym(IdSet, SymName0, PossibleArities),
            report_undefined_mq_id(!.Info, ErrorContext, Id0, IdType,
                ThisModuleName, IntMismatches, QualMismatches,
                PossibleArities, !Specs),

            % If a module defines an entity that this module refers to,
            % even without the required module qualification, then reporting
            % that module as being unused would be wrong.
            %
            % This is so even if the correct definition of Id0 could have
            % come from any one of several modules.
            list.foldl(mq_info_set_module_used(InInt), QualMismatches, !Info)
        ;
            ReportErrors = should_not_report_errors
        )
    ;
        Matches = [ModuleName],
        % A unique match for this ID.
        MaybeUniqModuleName = yes(ModuleName)
    ;
        Matches = [_, _ | _],
        MaybeUniqModuleName = no,
        mq_info_get_should_report_errors(!.Info, ReportErrors),
        (
            ReportErrors = should_report_errors,
            mq_info_record_undef_mq_id(IdType, Id0, !Info),
            NonUsableModuleNames = IntMismatches ++ QualMismatches,
            report_ambiguous_match(ErrorContext, Id0, IdType,
                Matches, NonUsableModuleNames, !Specs)
        ;
            ReportErrors = should_not_report_errors
        )
    ),
    (
        MaybeUniqModuleName = no,
        % Returning any SymName is fine, since it won't be used.
        Id0 = mq_id(SymName, _)
    ;
        MaybeUniqModuleName = yes(UniqModuleName),
        SymName = qualified(UniqModuleName, BaseName),
        mq_info_set_module_used(InInt, UniqModuleName, !Info),
        UsedItemType = convert_used_item_type(IdType),
        ItemName0 = recomp_item_name(SymName0, Arity),
        ItemName = recomp_item_name(SymName, Arity),
        update_recompilation_info(
            record_used_item(UsedItemType, ItemName0, ItemName), !Info)
    ).

:- pred mq_info_record_undef_mq_id(qual_id_kind::in, mq_id::in,
    mq_info::in, mq_info::out) is det.

mq_info_record_undef_mq_id(IdType, Id, !Info) :-
    mq_info_get_suppress_found_undef(!.Info, SuppressFoundUndef),
    (
        SuppressFoundUndef = suppress_found_undef
    ;
        SuppressFoundUndef = do_not_suppress_found_undef,
        Id = mq_id(SymName, Arity),
        (
            IdType = qual_id_type,
            TypeCtor = type_ctor(SymName, Arity),
            mq_info_get_undef_types(!.Info, UndefTypes0),
            set_tree234.insert(TypeCtor, UndefTypes0, UndefTypes),
            mq_info_set_undef_types(UndefTypes, !Info)
        ;
            IdType = qual_id_inst,
            InstCtor = inst_ctor(SymName, Arity),
            mq_info_get_undef_insts(!.Info, UndefInsts0),
            set_tree234.insert(InstCtor, UndefInsts0, UndefInsts),
            mq_info_set_undef_insts(UndefInsts, !Info)
        ;
            IdType = qual_id_mode,
            ModeCtor = mode_ctor(SymName, Arity),
            mq_info_get_undef_modes(!.Info, UndefModes0),
            set_tree234.insert(ModeCtor, UndefModes0, UndefModes),
            mq_info_set_undef_modes(UndefModes, !Info)
        ;
            IdType = qual_id_class,
            SNA = sym_name_arity(SymName, Arity),
            mq_info_get_undef_typeclasses(!.Info, UndefTypeclasses0),
            set_tree234.insert(SNA, UndefTypeclasses0, UndefTypeclasses),
            mq_info_set_undef_typeclasses(UndefTypeclasses, !Info)
        )
    ).

:- func convert_used_item_type(qual_id_kind) = used_item_type.

convert_used_item_type(qual_id_type) = used_type_name.
convert_used_item_type(qual_id_inst) = used_inst.
convert_used_item_type(qual_id_mode) = used_mode.
convert_used_item_type(qual_id_class) = used_typeclass.

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

:- pred id_set_search_sym_arity(mq_in_interface::in, id_set::in,
    sym_name::in, string::in, int::in, list(module_name)::out,
    list(module_name)::out, list(module_name)::out) is det.

id_set_search_sym_arity(InInt, IdSet, SymName, UnqualName, Arity,
        Matches, IntMismatches, QualMismatches) :-
    ( if
        map.search(IdSet, UnqualName, SubMap),
        map.search(SubMap, Arity, PermissionsMap)
    then
        find_matches_in_permissions_map(InInt, SymName, PermissionsMap,
            Matches, IntMismatches, QualMismatches)
    else
        Matches = [],
        IntMismatches = [],
        QualMismatches = []
    ).

:- pred find_matches_in_permissions_map(mq_in_interface::in, sym_name::in,
    permissions_map::in, list(module_name)::out,
    list(module_name)::out, list(module_name)::out) is det.

find_matches_in_permissions_map(InInt, SymName, PermissionsMap,
        Matches, IntMismatches, QualMismatches) :-
    map.foldr3(add_matching_and_nearmiss_modules(InInt, SymName),
        PermissionsMap, [], Matches, [], IntMismatches, [], QualMismatches).

:- pred add_matching_and_nearmiss_modules(mq_in_interface::in, sym_name::in,
    module_name::in, module_permissions::in,
    list(module_name)::in, list(module_name)::out,
    list(module_name)::in, list(module_name)::out,
    list(module_name)::in, list(module_name)::out) is det.

add_matching_and_nearmiss_modules(InInt, SymName, ModuleName, Permissions,
        !Matches, !IntMismatches, !QualMismatches) :-
    (
        SymName = unqualified(_),
        FullyModuleQualified = no,
        add_matching_and_nearmiss_modules_int(InInt, FullyModuleQualified,
            ModuleName, Permissions,
            !Matches, !IntMismatches, !QualMismatches)
    ;
        SymName = qualified(QualModuleName, _),
        ( if
            partial_sym_name_matches_full(QualModuleName, ModuleName)
        then
            ( if QualModuleName = ModuleName then
                FullyModuleQualified = yes
            else
                FullyModuleQualified = no
            ),
            add_matching_and_nearmiss_modules_int(InInt, FullyModuleQualified,
                ModuleName, Permissions,
                !Matches, !IntMismatches, !QualMismatches)
        else if
            ModuleNameComponents = sym_name_to_list(ModuleName),
            QualModuleNameComponents = sym_name_to_list(QualModuleName),
            list.sublist(QualModuleNameComponents, ModuleNameComponents)
        then
            % The missing module name components are not all at the start.
            % XXX *Should* this be a problem?
            !:QualMismatches = [ModuleName | !.QualMismatches]
        else
            true
        )
    ).

:- pred add_matching_and_nearmiss_modules_int(mq_in_interface::in, bool::in,
    module_name::in, module_permissions::in,
    list(module_name)::in, list(module_name)::out,
    list(module_name)::in, list(module_name)::out,
    list(module_name)::in, list(module_name)::out) is det.

add_matching_and_nearmiss_modules_int(InInt, FullyModuleQualified,
        ModuleName, Permissions, !Matches, !IntMismatches, !QualMismatches) :-
    Permissions = module_permissions(PermInInt, PermInImp),
    (
        InInt = mq_used_in_interface,
        (
            PermInInt = may_not_use_in_int,
            !:IntMismatches = [ModuleName | !.IntMismatches]
        ;
            PermInInt = may_use_in_int(NeedQual),
            add_matching_and_nearmiss_modules_qual(FullyModuleQualified,
                NeedQual, ModuleName, !Matches, !QualMismatches)
        )
    ;
        InInt = mq_not_used_in_interface,
        PermInImp = may_use_in_imp(NeedQual),
        add_matching_and_nearmiss_modules_qual(FullyModuleQualified,
            NeedQual, ModuleName, !Matches, !QualMismatches)
    ).

:- pred add_matching_and_nearmiss_modules_qual(bool::in, need_qualifier::in,
    module_name::in,
    list(module_name)::in, list(module_name)::out,
    list(module_name)::in, list(module_name)::out) is det.

add_matching_and_nearmiss_modules_qual(FullyModuleQualified, NeedQual,
        ModuleName, !Matches, !QualMismatches) :-
    ( if
        ( FullyModuleQualified = yes
        ; NeedQual = may_be_unqualified
        )
    then
        !:Matches = [ModuleName | !.Matches]
    else
        !:QualMismatches = [ModuleName | !.QualMismatches]
    ).

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

:- pred id_set_search_sym(id_set::in, sym_name::in, set(int)::out) is det.

id_set_search_sym(IdSet, SymName, PossibleArities) :-
    UnqualName = unqualify_name(SymName),
    ( if
        map.search(IdSet, UnqualName, SubMap)
    then
        map.to_assoc_list(SubMap, SubMapPairs),
        find_matching_arities(SymName, SubMapPairs, set.init, PossibleArities)
    else
        set.init(PossibleArities)
    ).

:- pred find_matching_arities(sym_name::in,
    assoc_list(int, permissions_map)::in, set(int)::in, set(int)::out) is det.

find_matching_arities(_SymName, [], !PossibleArities).
find_matching_arities(SymName, [Pair | Pairs], !PossibleArities) :-
    Pair = Arity - PermissionsMap,
    find_matches_in_permissions_map(mq_not_used_in_interface, SymName,
        PermissionsMap, Matches, IntMismatches, QualMismatches),
    ( if
        ( Matches = [_ | _]
        ; IntMismatches = [_ | _]
        ; QualMismatches = [_ | _]
        )
    then
        set.insert(Arity, !PossibleArities)
    else
        true
    ),
    find_matching_arities(SymName, Pairs, !PossibleArities).

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

parent_module_is_imported(InInt, ModuleIdSet, ParentModule, ChildModule) :-
    % Find the module name at the start of the ChildModule;
    % this submodule will be a direct sub-module of ParentModule.
    DirectSubModuleName = get_first_module_name(ChildModule),

    % Check that the ParentModule was imported.
    % We do this by looking up the definitions for the direct sub-module
    % and checking that the one in ParentModule came from an
    % imported module.
    Arity = 0,
    map.search(ModuleIdSet, DirectSubModuleName, SubMap),
    map.search(SubMap, Arity, PermissionsMap),
    map.search(PermissionsMap, ParentModule, ParentModulePermissions),
    ParentModulePermissions = module_permissions(PermInInt, PermInImp),
    (
        InInt = mq_used_in_interface,
        PermInInt = may_use_in_int(may_be_unqualified)
    ;
        InInt = mq_not_used_in_interface,
        PermInImp = may_use_in_imp(may_be_unqualified)
    ).

    % Given a module name, possibly module-qualified, return the name
    % of the first module in the qualifier list. For example, given
    % `foo.bar.baz', this returns `foo', and given just `baz',
    % it returns `baz'.
    %
:- func get_first_module_name(module_name) = string.

get_first_module_name(unqualified(ModuleName)) = ModuleName.
get_first_module_name(qualified(Parent, _)) = get_first_module_name(Parent).

%---------------------------------------------------------------------------%
:- end_module parse_tree.module_qual.id_set.
%---------------------------------------------------------------------------%
