%---------------------------------------------------------------------------%
% vim: ts=4 sw=4 et ft=mercury
%---------------------------------------------------------------------------%
% Copyright (C) 1994-2012 The University of Melbourne.
% Copyright (C) 2017-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: mercury_compile_args.m.
% Main authors: fjh, zs.
%
% This module handles argument processing for the Mercury compiler.
%
%---------------------------------------------------------------------------%

:- module top_level.mercury_compile_args.
:- interface.

:- import_module libs.
:- import_module libs.globals.
:- import_module libs.options.
:- import_module make.
:- import_module make.options_file.
:- import_module parse_tree.
:- import_module parse_tree.error_spec.

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

:- type arg_processing_result
    --->    apr_failure(
                option_table,
                one_or_more(diag_spec)
            )
    ;       apr_success(
                aprs_globals                :: globals,
                aprs_arg_pack               :: compiler_arg_pack
            ).

:- pred setup_all_args(io.text_output_stream::in, list(string)::in,
    arg_processing_result::out, io::di, io::uo) is det.

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

:- pred get_args_representing_env_vars(list(string)::out,
    io::di, io::uo) is det.

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

:- implementation.

:- import_module libs.file_util.
:- import_module libs.handle_options.
:- import_module libs.optimization_options.
:- import_module parse_tree.maybe_error.
:- import_module parse_tree.write_error_spec.

:- import_module bool.
:- import_module cord.
:- import_module dir.
:- import_module getopt.
:- import_module io.environment.
:- import_module map.
:- import_module maybe.
:- import_module require.
:- import_module string.

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

setup_all_args(ProgressStream, CmdLineArgs, ArgResult, !IO) :-
    trace [
        compile_time(flag("cmd_line_args")),
        run_time(env("MMC_CMD_LINE_ARGS")),
        io(!TIO)]
    (
        dump_args(ProgressStream, "REAL_MAIN", CmdLineArgs, !TIO)
    ),

    % Replace all @file arguments with the contents of the file.
    expand_at_file_arguments_loop(CmdLineArgs,
        cord.init, ExpandedCmdLineArgsCord, [], ExpandSpecs0, !IO),
    ExpandSpecs = coerce(ExpandSpecs0),
    % NOTE: We need the DefaultOptionTable both
    % - on all the success paths, to help build the Globals structure, and
    % - on all the failure paths, to print the err_specs.
    get_option_default_table(DefaultOptionTable),
    (
        ExpandSpecs = [],
        ExpandedCmdLineArgs = cord.list(ExpandedCmdLineArgsCord),
        ( if ExpandedCmdLineArgs = ["--arg-file", ArgFile | ExtraArgs] then
            % Diagnose bad invocations, such as shell redirection operators
            % treated as command line arguments.
            (
                ExtraArgs = []
            ;
                ExtraArgs = [_ | _],
                unexpected($pred,
                    "extra arguments with --arg-file: " ++ string(ExtraArgs))
            ),
            process_options_arg_file(ProgressStream, DefaultOptionTable,
                ArgFile, OptResult, !IO)
        else
            process_options_std(ProgressStream, DefaultOptionTable,
                ExpandedCmdLineArgs, OptResult, !IO)
        ),
        (
            OptResult = opr_success(EnvOptFileVariables, MCFlags,
                OptionArgs, NonOptionArgs, OptionSpecs),

            get_args_representing_env_vars(EnvVarArgs, !IO),
            % NOTE: The order of the flags here must be:
            %
            %   (1) flags from Mercury.config and any Mercury.options files
            %   (2) flags from environment variables
            %   (3) flags from any command line options
            %
            % The order is important, because flags given later in this list
            % can override those given earlier.
            AllFlags = MCFlags ++ EnvVarArgs ++ OptionArgs,
            trace [compile_time(flag("cmd_line_args")),
                run_time(env("MMC_CMD_LINE_ARGS")),
                io(!TIO)]
            (
                dump_args(ProgressStream, "AllFlags", AllFlags, !TIO)
            ),
            MaybeStdLibGrades = stdlib_grades_unknown,
            lookup_mercury_stdlib_dir(EnvOptFileVariables,
                MaybeEnvOptFileStdLibDirs0),
            maybe1_to_maybe1el(MaybeEnvOptFileStdLibDirs0,
                MaybeEnvOptFileStdLibDirs),
            handle_given_options(ProgressStream, DefaultOptionTable,
                MaybeStdLibGrades, MaybeEnvOptFileStdLibDirs, AllFlags, _, _,
                Specs, Globals, !IO),

            % Now that we have constructed a globals, print out any errors
            % and/or warnings generated by the predicates in options_file.m.
            write_diag_specs(ProgressStream, Globals, OptionSpecs, !IO),

            % When computing the option arguments to pass to `--make',
            % only include the command-line arguments, not the contents of
            % DEFAULT_MCFLAGS.
            (
                Specs = [HeadSpec | TailSpecs],
                OoMSpecs = one_or_more(HeadSpec, TailSpecs),
                globals.get_options(Globals, OptionTable),
                ArgResult = apr_failure(OptionTable, OoMSpecs)
            ;
                Specs = [],
                ArgPack = compiler_arg_pack(EnvOptFileVariables, EnvVarArgs,
                    OptionArgs, NonOptionArgs),
                ArgResult = apr_success(Globals, ArgPack)
            )
        ;
            OptResult = opr_failure(OoMOptionErrSpecs, OptionWarnSpecs),
            OoMOptionSpecs = oom_err_to_oom_diag(OoMOptionErrSpecs),
            append_one_or_more_list(OoMOptionSpecs, OptionWarnSpecs,
                OptionSpecs),
            % Usually, these OptionSpecs were generated in options_file.m.
            ArgResult = apr_failure(DefaultOptionTable, OptionSpecs)
        )
    ;
        ExpandSpecs = [HeadExpandSpec | TailExpandSpecs],
        OoMExpandSpecs = one_or_more(HeadExpandSpec, TailExpandSpecs),
        ArgResult = apr_failure(DefaultOptionTable, OoMExpandSpecs)
    ).

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

    % Expand @File arguments.
    % Each argument in the above form is replaced with a list of arguments
    % where each arg is each line in the file File which is not just
    % whitespace.
    %
:- pred expand_at_file_arguments_loop(list(string)::in,
    cord(string)::in, cord(string)::out,
    list(err_spec)::in, list(err_spec)::out, io::di, io::uo) is det.

expand_at_file_arguments_loop([], !ExpandedArgs, !Specs, !IO).
expand_at_file_arguments_loop([Arg | Args], !ExpandedArgs, !Specs, !IO) :-
    ( if string.remove_prefix("@", Arg, FileName) then
        io.read_named_file_as_lines_wf(FileName, FileLinesResult, !IO),
        (
            FileLinesResult = ok(FileLines0),
            list.negated_filter(unify(""), FileLines0, FileLines),
            cord.snoc_list(FileLines, !ExpandedArgs)
        ;
            FileLinesResult = error(IOError),
            % NOTE read_named_file_as_lines_wf does not just open the file;
            % it also reads its contents. Failures can occur in either part.
            Spec = construct_spec_for_cannot_open_file_for_input(FileName,
                no, IOError),
            !:Specs = [Spec | !.Specs]
        )
    else
        cord.snoc(Arg, !ExpandedArgs)
    ),
    expand_at_file_arguments_loop(Args, !ExpandedArgs, !Specs, !IO).

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

:- type option_processing_result
    --->    opr_failure(
                oprf_fatal_error_specs      :: one_or_more(err_spec),
                oprf_warn_specs             :: list(diag_spec)
            )
    ;       opr_success(
                oprs_env_optfile_variables  :: env_optfile_variables,
                oprs_mcflags                :: list(string),
                oprs_option_args            :: list(string),
                oprs_nonoption_args         :: list(string),
                oprs_nonfatal_errors        :: list(diag_spec)
            ).

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

:- pred process_options_arg_file(io.text_output_stream::in, option_table::in,
    string::in, option_processing_result::out, io::di, io::uo) is det.

process_options_arg_file(ProgressStream, DefaultOptionTable, ArgFile,
        Result, !IO) :-
    io.environment.get_environment_var_map(EnvVarMap, !IO),
    % All the configuration and options file options are passed in the
    % given file, which is created by the parent `mmc --make' process.
    % (make.module_target does this to overcome limits on the lengths
    % of command lines on Windows.) The environment is ignored, unlike
    % with @file syntax.
    read_args_file(ProgressStream, ArgFile, MaybeArgs1,
        ArgsNonUndefErrSpecs, ArgsUndefWarnSpecs, !IO),
    % Since the args file is supposed to be generated by the parent
    % `mmc --make' process, there shouldn't be any references to
    % undefined make variables in it. If there are, that process has
    % screwed up. We can't fix that bug unless the bug is reported,
    % which requires printing the error messages that it yields.
    Specs = coerce(ArgsNonUndefErrSpecs) ++ coerce(ArgsUndefWarnSpecs),
    EnvOptFileVariables = env_optfile_variables_init(EnvVarMap),
    (
        MaybeArgs1 = yes(Args1),
        get_short_option(ShortOption),
        get_long_option(LongOption),
        % Separate the option args from the non-option args.
        getopt.recognize_all_options(ShortOption, LongOption,
            DefaultOptionTable, Args1, Errors, _OptionValues,
            OptionArgs, NonOptionArgs),
        (
            Errors = [_ | _],
            OptionSpecs = list.map(option_error_to_err_spec, Errors),
            det_list_to_one_or_more(OptionSpecs, OoMOptionSpecs),
            Result = opr_failure(OoMOptionSpecs, [])
        ;
            Errors = [],
            Result = opr_success(EnvOptFileVariables, [],
                OptionArgs, NonOptionArgs, Specs)
        )
    ;
        MaybeArgs1 = no,
        OptionArgs = [],
        NonOptionArgs = [],
        Result = opr_success(EnvOptFileVariables, [],
            OptionArgs, NonOptionArgs, Specs)
    ).

:- func option_error_to_err_spec(option_error(option)) = err_spec.

option_error_to_err_spec(OptionError) = Spec :-
    OptionErrorStr = option_error_to_string(OptionError),
    Pieces = [words(OptionErrorStr), suffix("."), nl],
    Spec = no_ctxt_spec($pred, severity_error, phase_options, Pieces).

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

:- pred process_options_std(io.text_output_stream::in,
    option_table::in, list(string)::in, option_processing_result::out,
    io::di, io::uo) is det.

process_options_std(ProgressStream, DefaultOptionTable, CmdLineArgs,
        Result, !IO) :-
    % Find out which options files to read.
    % Don't report errors yet, as the errors may no longer exist
    % after we have read in options files.
    get_short_option(ShortOption),
    get_long_option(LongOption),
    OptionOps = option_ops_userdata(ShortOption, LongOption, special_handler),
    getopt.process_options_userdata_io(OptionOps, CmdLineArgs,
        OptionArgs, NonOptionArgs, MaybeError, _OptionsSet,
        DefaultOptionTable, ArgsOptionTable, cord.init, _UserData, !IO),
    (
        MaybeError = yes(OptionError),
        OoMErrSpecs = report_option_error(OptionError),
        Result = opr_failure(OoMErrSpecs, [])
    ;
        MaybeError = no,
        read_options_files_named_in_options_file_option(ProgressStream,
            ArgsOptionTable, EnvOptFileVariables0,
            OptFileErrSpecs, OptFileUndefWarnSpecs0, !IO),
        getopt.lookup_bool_option(ArgsOptionTable,
            warn_undefined_options_variables, WarnUndef),
        (
            WarnUndef = no,
            OptFileUndefWarnSpecs = []
        ;
            WarnUndef = yes,
            OptFileUndefWarnSpecs = OptFileUndefWarnSpecs0
        ),
        io.environment.get_environment_var_map(EnvVarMap, !IO),
        % XXX DIAG_SPEC we used to include OptFileUndefWarnSpecs in this
        (
            OptFileErrSpecs = [HeadSpec | TailSpecs],
            OoMOptFileErrSpecs = one_or_more(HeadSpec, TailSpecs),
            Result = opr_failure(OoMOptFileErrSpecs,
                coerce(OptFileUndefWarnSpecs))
        ;
            OptFileErrSpecs = [],
            maybe_dump_options_file(ProgressStream, ArgsOptionTable,
                EnvOptFileVariables0, !IO),
            lookup_mmc_options(EnvOptFileVariables0, MaybeMCFlags0),
            (
                MaybeMCFlags0 = error1(OoMErrSpecs),
                Result = opr_failure(OoMErrSpecs, [])
            ;
                MaybeMCFlags0 = ok1(MCFlags0),
                trace [
                    compile_time(flag("cmd_line_args")),
                    run_time(env("MMC_CMD_LINE_ARGS")),
                    io(!TIO)]
                (
                    dump_args(ProgressStream, "MCFlags0", MCFlags0, !TIO),
                    dump_args(ProgressStream, "CmdLineArgs", CmdLineArgs, !TIO)
                ),
                % Process the options again to find out which configuration
                % file to read.
                %
                % Note that MaybeErrorMC can be yes(_) even though MaybeError
                % was no, due to the addition of MCFlags0 to the argument list.
                getopt.process_options_userdata_io(OptionOps,
                    MCFlags0 ++ CmdLineArgs, _OptionArgsMC, _NonOptionArgsMC,
                    MaybeErrorMC, _OptionsSetMC,
                    DefaultOptionTable, FlagsArgsOptionTable,
                    cord.init, _UserDataMC, !IO),
                (
                    MaybeErrorMC = yes(OptionError),
                    OoMErrSpecs = report_option_error(OptionError),
                    Result = opr_failure(OoMErrSpecs, [])
                ;
                    MaybeErrorMC = no,
                    process_options_std_config_file(ProgressStream,
                        FlagsArgsOptionTable, EnvVarMap, WarnUndef,
                        EnvOptFileVariables0, EnvOptFileVariables,
                        MaybeMCFlags, OtherConfigSpecs, !IO),
                    Specs = OtherConfigSpecs ++ coerce(OptFileUndefWarnSpecs),
                    (
                        MaybeMCFlags = error1(OoMFlagSpecs),
                        Result = opr_failure(OoMFlagSpecs, Specs)
                    ;
                        MaybeMCFlags = ok1(MCFlags),
                        Result = opr_success(EnvOptFileVariables, MCFlags,
                            OptionArgs, NonOptionArgs, Specs)
                    )
                )
            )
        )
    ).

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

:- pred process_options_std_config_file(io.text_output_stream::in,
    option_table::in, environment_var_map::in, bool::in,
    env_optfile_variables::in, env_optfile_variables::out,
    maybe1(list(string))::out, list(diag_spec)::out, io::di, io::uo) is det.

process_options_std_config_file(ProgressStream, FlagsArgsOptionTable,
        EnvVarMap, WarnUndef, EnvOptFileVariables0, EnvOptFileVariables,
        MaybeMCFlags, OtherSpecs, !IO) :-
    getopt.lookup_maybe_string_option(FlagsArgsOptionTable, config_file,
        MaybeConfigFile0),
    % The meanings of the possible values of MaybeConfigFile0 are as follows.
    %
    % - A value of yes("") means either that the command line had no
    %   --config-file option (since yes("") is the default for this option),
    %   or that the command line contained --config-file ''.
    %   The latter is *extremely* unlikely.
    %
    % - A value of yes(ConfigFile0) where ConfigFile0 != "" means that
    %   the command line had a meaningful --config-file option.
    %
    % - A value of no means that the command line contained --no-config-file.
    %
    % The next if-then-else replaces yes("") with one or the other
    % of the other two options.
    ( if MaybeConfigFile0 = yes("") then
        getopt.lookup_maybe_string_option(FlagsArgsOptionTable,
            mercury_configuration_directory, MaybeConfDir),
        (
            MaybeConfDir = yes(ConfDir),
            MaybeConfigFile = yes(ConfDir/"conf"/"Mercury.config")
        ;
            MaybeConfDir = no,
            MaybeConfigFile = no
        )
    else
        MaybeConfigFile = MaybeConfigFile0
    ),
    (
        MaybeConfigFile = yes(ConfigFile),
        read_named_options_file(ProgressStream, ConfigFile,
            EnvOptFileVariables0, EnvOptFileVariables,
            ConfigNonUndefSpecs, ConfigUndefSpecs0, !IO),
        % All entries in ConfigNonUndefSpecs are unconditionally errors.
        % All entries in ConfigUndefSpecs are unconditionally warnings.
        (
            WarnUndef = no,
            ConfigUndefSpecs = []
        ;
            WarnUndef = yes,
            ConfigUndefSpecs = coerce(ConfigUndefSpecs0)
        ),
        (
            ConfigNonUndefSpecs = [],
            lookup_mmc_options(EnvOptFileVariables, MaybeMCFlags),
            % maybe_libgrade_opts_for_detected_stdlib_grades does this lookup,
            % but only if --mercury-stdlib-dir is NOT specified. Because of
            % that, it is simpler to repeat the call here than to try to
            % optimize it away.
            % XXX I (zs) think that checking whether this call returns any
            % errors would be a good idea even if --mercury-stdlib-dir IS
            % specified, because it would force any reported problems to be
            % found and fixed up front.
            lookup_mercury_stdlib_dir(EnvOptFileVariables,
                MaybeConfigMerStdLibDir),
            OtherSpecs = ConfigUndefSpecs ++
                coerce(get_any_errors1(MaybeConfigMerStdLibDir))
        ;
            ConfigNonUndefSpecs = [HeadSpec | TailSpecs],
            MaybeMCFlags = error1(one_or_more(HeadSpec, TailSpecs)),
            OtherSpecs = ConfigUndefSpecs
        )
    ;
        MaybeConfigFile = no,
        EnvOptFileVariables = env_optfile_variables_init(EnvVarMap),
        lookup_mmc_options(EnvOptFileVariables, MaybeMCFlags),
        OtherSpecs = []
    ).

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

:- func report_option_error(option_error(option)) = one_or_more(err_spec).

report_option_error(OptionError) = Specs :-
    OptionErrorStr = option_error_to_string(OptionError),
    MainMsg = no_ctxt_msg([words("Error:"), words(OptionErrorStr),
        suffix("."), nl]),
    ( if
        OptionError = unrecognized_option(OptionStr),
        ( if string.remove_prefix("--no-", OptionStr, BaseOptionStr0) then
            IsNegatedOption = yes,
            BaseOptionStr = BaseOptionStr0
        else if string.remove_prefix("--", OptionStr, BaseOptionStr0) then
            IsNegatedOption = no,
            BaseOptionStr = BaseOptionStr0
        else
            % The option was a short option and we cannot meaningfully
            % find good "Did you mean ..." suggestions for those.
            fail
        ),
        BaseOptionStr \= ""
    then
        (
            IsNegatedOption = yes,
            Prefix = "--no-",
            all_negatable_long_option_strings(OptionStrs)
        ;
            IsNegatedOption = no,
            Prefix = "--",
            all_long_option_strings(OptionStrs)
        ),
        maybe_construct_prefixed_did_you_mean_pieces(Prefix, BaseOptionStr,
            OptionStrs, DidYouMeanPieces),
        DidYouMeanMsg = gen_msg(no, always_treat_as_first, 0u,
            [always(DidYouMeanPieces)]),
        Msgs = [MainMsg, DidYouMeanMsg]
    else
        Msgs = [MainMsg]
    ),
    Spec = gen_spec($pred, severity_error, phase_options, Msgs),
    Specs = one_or_more(Spec, []).

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

:- pred maybe_dump_options_file(io.text_output_stream::in, option_table::in,
    env_optfile_variables::in, io::di, io::uo) is det.

maybe_dump_options_file(OutStream, ArgsOptionTable, EnvOptFileVariables,
        !IO) :-
    lookup_string_option(ArgsOptionTable, dump_options_file, DumpOptionsFile),
    ( if DumpOptionsFile = "" then
        true
    else
        dump_options_file(OutStream, DumpOptionsFile, EnvOptFileVariables, !IO)
    ).

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

:- pred dump_args(io.text_output_stream::in, string::in, list(string)::in,
    io::di, io::uo) is det.

dump_args(OutStream, Marker, Args, !IO) :-
    io.format(OutStream, "%s START\n", [s(Marker)], !IO),
    list.foldl(dump_arg(OutStream), Args, !IO),
    io.format(OutStream, "%s END\n", [s(Marker)], !IO).

:- pred dump_arg(io.text_output_stream::in, string::in, io::di, io::uo) is det.

dump_arg(OutStream, Arg, !IO) :-
    io.format(OutStream, "<%s>\n", [s(Arg)], !IO).

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

get_args_representing_env_vars(EnvVarArgs, !IO) :-
    io.environment.get_environment_var("MERCURY_COLOR_SCHEME",
        MaybeColorScheme, !IO),
    ( if
        MaybeColorScheme = yes(ColorScheme),
        ColorScheme \= ""
    then
        EnvVarColorSchemeArgs = ["--color-scheme-envvar", ColorScheme]
    else
        EnvVarColorSchemeArgs = []
    ),
    io.environment.get_environment_var("MERCURY_ENABLE_COLOR",
        MaybeEnableColor, !IO),
    ( if
        MaybeEnableColor = yes(EnableColor),
        % We ignore the value of EnableColor if it has a value *other than*
        % the ones recognized in this switch.
        (
            ( EnableColor = "never"
            ; EnableColor = "0"
            ),
            EnableArg = "--no-color-diagnostics"
        ;
            ( EnableColor = "always"
            ; EnableColor = "1"
            ),
            EnableArg = "--color-diagnostics"
        )
    then
        EnvVarEnableColorArgs = [EnableArg]
    else
        io.environment.get_environment_var("NO_COLOR", MaybeNoColor, !IO),
        ( if
            MaybeNoColor = yes(NoColorValue),
            NoColorValue \= ""
        then
            % The environment variable NO_COLOR is present and nonempty.
            EnvVarEnableColorArgs = ["--no-color-diagnostics"]
        else
            EnvVarEnableColorArgs = []
        )
    ),
    EnvVarArgs = EnvVarColorSchemeArgs ++ EnvVarEnableColorArgs.

%---------------------------------------------------------------------------%
:- end_module top_level.mercury_compile_args.
%---------------------------------------------------------------------------%
