[m-rev.] for review: pragma input_spec

Julien Fischer jfischer at opturion.com
Fri Aug 14 17:56:27 AEST 2026


Hi Zoltan,

Here are some initial review comments on this.

On Thu, 13 Aug 2026 at 22:03, Zoltan Somogyi <zoltan.somogyi at runbox.com> wrote:

> Add a new pragma, ":- pragma input_spec(...)".

I would prefer the name "input_mode_spec". Despite being slightly
redundant (an input is a mode), having mode in the pragma name better
ties it to the mode declaration.

> This new pragma automates the task of creating specialized modes
> of all the predicates in a module that take inputs that select
> one of several ways to deal with an issue. We have (hand-written)
> examples of this in some modules of the compiler, including
> ml_elim_nested.m and quantification.m. This predicate from the former
> shows what it looks like:
>
>     :- pred ml_maybe_add_args(action, mlds_stmt, prog_context,
>         list(mlds_argument), elim_info, elim_info).
>     :- mode ml_maybe_add_args(in(hoist), in, in, in, in, out) is det.
>     :- mode ml_maybe_add_args(in(chain), in, in, in, in, out) is det.
>
> The point of the specialized modes is that
>
> - it allows the parts of the code that do the same thing regardless
>   of the action to be written just once (they do not have to repeated);
>
> - in the parts of the code that want to do different things for
>   hoist vs chain, it allows the decision to be made at compile time,
>   not at runtime,
>
> - and the compile-time execution of those decisions makes the action
>   parameter itself effectively unused at runtime, allowing to be
>   optimized away.

... allowing *it* to be optimized away.

> The new pragma automates the creation of the specialized modes.
> Given the existing type and insts,
>
>     :- type action
>         --->    hoist_nested_funcs
>         ;       chain_gc_stack_frames.
>
>     :- inst hoist for action/0
>         --->    hoist_nested_funcs.
>     :- inst chain for action/0
>         --->    chain_gc_stack_frames.
>
> the pragma
>
>     :- pragma input_spec(action, replace_in_mode, [hoist, chain]).
>
> would generate the above pair of mode declarations from just
>
>     :- mode ml_maybe_add_args(in, in, in, in, in, out) is det.
>
> or even from its predmode equivalent. This pragma is therefore
> not so much a *program* optimization, as a *programmer time*
> optimization.

Is this sufficiently common that it justifies the addition of this pragma?

> (If the second argument of the pragma were "add_to_in_mode",
> the compiler would just add the new modes in front of the existing one,
> instead of entirely replacing the existing mode.)

The diff does not allow replace_in_mode for exported predicates, which makes
sense since there would be a inconsistency with the interface files.
It does seemingly allow add_to_mode for exported predicates. If that
is the case, some comment on its interaction wiht the interface files is
needed.

> compiler/prog_item_pragma.m:
>     Define this new pragma as a decl pragma. This should allow us
>     in the future to include them in .opt files. (The current diff
>     does not do that.)
>
> compiler/parse_pragma.m:
>     Add code to parse the new pragma.
>
> compiler/add_pragma_decl.m:
>     Add the new pragmas to the HLDS.
>
> compiler/hlds_module.m:
>     Add a slot for the new pragmas to the HLDS.
>
> compiler/input_specialization.m:
>     A new module that implements the new pragma.
>
> compiler/mercury_compile_front_end.m:
>     Invoke input_specialization.m just before mode analysis.
>
> compiler/hlds.m:
> compiler/notes/compiler_design.html:
>     Include and document the new module.
>
> compiler/options.m:
>     Provide a way to detect whether an installed compiler
>     supports the new pragma.
>
> compiler/hlds_markers.m:
> compiler/style_checks.m:
>     Input specialization typically replaces a single mode declaration
>     with two or more (usually modified) copies of that mode declaration.
>     This creates sequences of mode declarations with identical item
>     sequence numbers. Do not warn about such sequences *if* the predicate
>     was subject to input specialization.
>
> compiler/check_import_accessibility.m:
> compiler/convert_parse_tree.m:
> compiler/equiv_type_parse_tree.m:
> compiler/error_spec.m:
> compiler/error_util.m:
> compiler/get_dependencies.m:
> compiler/grab_modules.m:
> compiler/intermod.m:
> compiler/item_util.m:
> compiler/make_hlds_passes.m:
> compiler/make_hlds_separate_items.m:
> compiler/module_qual.qualify_items.m:
> compiler/parse_tree_out.m:
> compiler/parse_tree_out_pragma.m:
> compiler/parse_type_name.m:
> compiler/prog_item_stats.m:
> compiler/prog_parse_tree.m:
> compiler/recompilation.version.m:
> compiler/table_gen.m:
>     Conform to the changes above.
>
> tests/hard_coded/test_input_spec.{m,exp}:
> tests/invalid/bad_input_spec.{m,err_exp}:
>     Test cases for correct and incorrect uses of the new pragma.
>
> tests/hard_coded/Mmakefile:
> tests/invalid/Mmakefile:
>     Enable the new test cases.
>
> tests/warnings/help_text.err_exp:
>     Expect the change to compiler/options.m.

> diff --git a/compiler/add_pragma_decl.m b/compiler/add_pragma_decl.m
> index 5ad7de9b5..061a971ce 100644
> --- a/compiler/add_pragma_decl.m
> +++ b/compiler/add_pragma_decl.m

...

> @@ -366,6 +385,270 @@ mark_pred_as_format_call(FormatCallInfo, PragmaStatus, !ModuleInfo,
>
>  %---------------------%
>
> +:- pred add_pragma_input_spec(item_mercury_status::in,
> +    decl_pragma_input_spec_info::in, module_info::in, module_info::out,
> +    list(err_spec)::in, list(err_spec)::out) is det.
> +
> +add_pragma_input_spec(ItemMercuryStatus, InputSpec, !ModuleInfo, !ErrSpecs) :-
> +    % XXX If we ever want to get smart recompilation working, we may
> +    % have to do something with _RecompIds. What we do for type_spec pragmas
> +    % may, or may not, be appropriate for input spec pragmas as well.
> +    InputSpec = decl_pragma_input_spec_info(ContainingModuleName, Type,
> +        ReplaceOrAdd, OoMInstCtors, OoMInsts, _RecompIds,
> +        TVarSet, Context, _),
> +    some [!InputSpecs] (
> +        !:InputSpecs = [],
> +        (
> +            ItemMercuryStatus = item_defined_in_other_module(_)
> +        ;
> +            ItemMercuryStatus = item_defined_in_this_module(ItemExport),
> +            (
> +                ItemExport = item_export_anywhere,
> +                (
> +                    ReplaceOrAdd = replace_in_mode,
> +                    StatusPieces = [words("Error: a"),
> +                        pragma_decl("input_spec"), words("declaration"),
> +                        words("that occurs in the interface of its module"),
> +                        words("is not allowed to specify")] ++
> +                        color_as_incorrect([words("replace_in_mode,")]) ++
> +                        [words("as this would contradict"),
> +                        words("the mode declarations of the"),
> +                        words("predicates and/or functions"),
> +                        words("in its public interface."), nl],
> +                    StatusSpec = spec($pred, severity_error, phase_pt2h,
> +                        Context, StatusPieces),
> +                    !:InputSpecs = [StatusSpec | !.InputSpecs]
> +                ;
> +                    ReplaceOrAdd = add_to_in_mode
> +                )
> +            ;
> +                ( ItemExport = item_export_nowhere
> +                ; ItemExport = item_export_only_submodules
> +                )
> +            )
> +        ),
> +        module_info_get_type_table(!.ModuleInfo, TypeTable),
> +        check_input_spec_type(TypeTable, Type,
> +            [], UnknownTypeCtors, [], NonDuTypeCtors, bag.init, TVars),
> +        (
> +            UnknownTypeCtors = []
> +        ;
> +            UnknownTypeCtors = [_ | _],
> +            UCtors = choose_number(UnknownTypeCtors,
> +                "constructor", "constructors"),
> +            Have = choose_number(UnknownTypeCtors, "has", "have"),
> +            UDefns = choose_number(UnknownTypeCtors,
> +                "definition", "definitions"),
> +            UnknownPieces = [words("Error: the type"), words(UCtors)] ++
> +                piece_list_to_color_pieces(color_subject, "and", [],
> +                    UnknownTypeCtors) ++
> +                [words(Have)] ++
> +                color_as_incorrect([words("no visible"), words(UDefns),
> +                    suffix(".")]) ++
> +            [nl],
> +            UnknownSpec = spec($pred, severity_error, phase_pt2h,
> +                Context, UnknownPieces),
> +            !:InputSpecs = [UnknownSpec | !.InputSpecs]
> +        ),
> +        (
> +            NonDuTypeCtors = []
> +        ;
> +            NonDuTypeCtors = [_ | _],
> +            NDefns = choose_number(NonDuTypeCtors,
> +                "definition", "definitions"),
> +            NCtors = choose_number(NonDuTypeCtors,
> +                "constructor", "constructors"),
> +            Are = choose_number(NonDuTypeCtors, "is a", "are"),
> +            Types = choose_number(NonDuTypeCtors, "type", "type"),

Both of the alternatives there are "type".

> +            NonDuPieces = [words("Error: the"), words(NDefns),
> +                words("of the type"), words(NCtors)] ++
> +                piece_list_to_color_pieces(color_subject, "and", [],
> +                    NonDuTypeCtors) ++
> +                [words(Are)] ++
> +                color_as_incorrect([words("not discriminated union"),
> +                    words(Types), suffix(".")]) ++
> +                [nl],


In the case where NonDuTypeCtors has one member, the last part of
that error message will say "is a not discriminated union type".

...

> diff --git a/compiler/input_specialization.m b/compiler/input_specialization.m
> new file mode 100644
> index 000000000..768d4601e
> --- /dev/null
> +++ b/compiler/input_specialization.m
> @@ -0,0 +1,248 @@
> +%---------------------------------------------------------------------------%
> +% vim: ft=mercury ts=4 sw=4 et
> +%---------------------------------------------------------------------------%
> +% Copyright (C) 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: input_specialization.m.
> +% Main author: zs.
> +%
> +% This file contains code for improving the names of head variables,
> +% replacing HeadVar__n with user-given names whereever the clauses
> +% agree on the names.

This is copy-and-pasted from compiler/headvar_names.m.

> +
> +:- module hlds.input_specialization.
> +
> +:- interface.
> +
> +:- import_module hlds.hlds_module.
> +
> +    % If all clauses give a given head variables the same name, use this name
> +    % instead of the introduced `HeadVar__n' names for the head variables
> +    % in the pred_info. This gives better error messages, more meaningful
> +    % variable names in the debugger and slightly faster compilation.


Ditto here.

> +    %
> +:- pred input_specialize_in_module(module_info::in, module_info::out) is det.
> +
> +%---------------------------------------------------------------------------%
> +
> +:- implementation.
> +
> +:- import_module hlds.hlds_markers.
> +:- import_module hlds.hlds_pred.
> +:- import_module hlds.mode_util.
> +:- import_module libs.
> +:- import_module libs.maybe_util.
> +:- import_module mdbcomp.
> +:- import_module mdbcomp.builtin_modules.
> +:- import_module mdbcomp.sym_name.
> +:- import_module parse_tree.
> +:- import_module parse_tree.prog_data.
> +:- import_module parse_tree.prog_item_pragma.
> +
> +:- import_module int.
> +:- import_module list.
> +:- import_module map.
> +:- import_module one_or_more.
> +:- import_module require.
> +
> +%---------------------------------------------------------------------------%
> +
> +input_specialize_in_module(!ModuleInfo) :-
> +    module_info_get_input_spec_table(!.ModuleInfo, InputSpecTable),
> +    module_info_get_valid_pred_ids(!.ModuleInfo, PredIds),

Won't this apply this transformation to uci preds and other compiler-generated
predicates?  Is that intentional?

> +    list.foldl(maybe_input_specialize_in_pred(InputSpecTable), PredIds,
> +        !ModuleInfo).
> +
> +:- pred maybe_input_specialize_in_pred(input_spec_table::in, pred_id::in,
> +    module_info::in, module_info::out) is det.
> +
> +maybe_input_specialize_in_pred(InputSpecTable, PredId, !ModuleInfo) :-
> +    module_info_pred_info(!.ModuleInfo, PredId, PredInfo0),
> +    pred_info_get_module_name(PredInfo0, ModuleName),
> +    ( if map.search(InputSpecTable, ModuleName, InModuleMap) then
> +        input_specialize_in_pred_if_possible(!.ModuleInfo, InModuleMap,
> +            PredInfo0, PredInfo),
> +        module_info_set_pred_info(PredId, PredInfo, !ModuleInfo)
> +    else
> +        true
> +    ).
> +
> +:- pred input_specialize_in_pred_if_possible(module_info::in,
> +    input_spec_in_module_map::in, pred_info::in, pred_info::out) is det.
> +
> +input_specialize_in_pred_if_possible(ModuleInfo, InModuleMap, !PredInfo) :-
> +    pred_info_get_arg_types(!.PredInfo, ArgTypes),
> +    find_args_to_specialize(InModuleMap, 1, ArgTypes, ArgsToSpec),
> +    (
> +        ArgsToSpec = []
> +        % Pieces = [words("Error:")],
> +        % Spec = spec($pred, severity_error, phase_input_spec, Pieces),
> +        % !:Specs = [Spec | !.Specs]
> +    ;
> +        ArgsToSpec = [HeadArgToSpec | TailArgsToSpec],
> +        input_specialize_in_pred(ModuleInfo, HeadArgToSpec, TailArgsToSpec,
> +            !PredInfo)
> +    ).
> +
> +:- type arg_to_specialize
> +    --->    arg_to_specialize(int, input_spec_info).
> +            % The argument number, and how to specialize it.
> +
> +:- pred find_args_to_specialize(input_spec_in_module_map::in,
> +    int::in, list(mer_type)::in, list(arg_to_specialize)::out) is det.
> +
> +find_args_to_specialize(_, _, [], []).
> +find_args_to_specialize(InModuleMap, ArgNum, [ArgType | ArgTypes],
> +        ArgsToSpec) :-
> +    find_args_to_specialize(InModuleMap, ArgNum + 1, ArgTypes,
> +        ArgsToSpecTail),
> +    % This search works only if the types in input_spec pragmas are ground.
> +    % At the moment, add_pragma_decl.m does require them to be ground.
> +    ( if map.search(InModuleMap, ArgType, InputSpecInfo) then
> +        ArgToSpec = arg_to_specialize(ArgNum, InputSpecInfo),
> +        ArgsToSpec = [ArgToSpec | ArgsToSpecTail]
> +    else
> +        ArgsToSpec = ArgsToSpecTail
> +    ).
> +
> +%---------------------------------------------------------------------------%
> +
> +:- pred input_specialize_in_pred(module_info::in,
> +    arg_to_specialize::in, list(arg_to_specialize)::in,
> +    pred_info::in, pred_info::out) is det.
> +
> +input_specialize_in_pred(ModuleInfo, HeadArgToSpec, TailArgsToSpec,
> +        !PredInfo) :-
> +    HeadArgToSpec = arg_to_specialize(ArgNum, InputSpecInfo),
> +    pred_info_get_proc_table(!.PredInfo, ProcTable0),
> +    map.values(ProcTable0, ProcInfos0),
> +    input_specialize_proc_table_in_given_arg(ModuleInfo, ArgNum, InputSpecInfo,
> +        ProcInfos0, ProcInfos, _Changed),
> +    rebuild_proc_table_loop(0, ProcInfos, map.init, ProcTable),
> +    pred_info_set_proc_table(ProcTable, !PredInfo),
> +    pred_info_get_markers(!.PredInfo, Markers0),
> +    add_marker(marker_was_input_specialized, Markers0, Markers),
> +    pred_info_set_markers(Markers, !PredInfo),
> +    (
> +        TailArgsToSpec = []
> +    ;
> +        TailArgsToSpec = [HeadTailArgToSpec | TailTailArgsToSpec],
> +        input_specialize_in_pred(ModuleInfo,
> +            HeadTailArgToSpec, TailTailArgsToSpec, !PredInfo)
> +    ).
> +
> +%---------------------------------------------------------------------------%
> +
> +:- pred rebuild_proc_table_loop(int::in, list(proc_info)::in,
> +    proc_table::in, proc_table::out) is det.
> +
> +rebuild_proc_table_loop(_, [], !ProcTable).
> +rebuild_proc_table_loop(ProcNum, [ProcInfo | ProcInfos], !ProcTable) :-
> +    proc_id_to_int(ProcId, ProcNum),
> +    map.det_insert(ProcId, ProcInfo, !ProcTable),
> +    rebuild_proc_table_loop(ProcNum + 1, ProcInfos, !ProcTable).

This seems suspicious. The HLDS has a lot of references to proc_ids.
Why does this renumbering not invalidate the ones in, for example,
the list of pragma exported procs? At least, had a comment giving
a correctness argument.

> +%---------------------------------------------------------------------------%
> +
> +:- pred input_specialize_proc_table_in_given_arg(module_info::in,
> +    int::in, input_spec_info::in,
> +    list(proc_info)::in, list(proc_info)::out, maybe_changed::out) is det.
> +
> +input_specialize_proc_table_in_given_arg(_, _, _, [], [], unchanged).
> +input_specialize_proc_table_in_given_arg(ModuleInfo, ArgNum, InputSpecInfo,
> +        [HeadProcInfo0 | TailProcInfos0], ProcInfos, Changed) :-
> +    proc_info_get_argmodes(HeadProcInfo0, ArgModes),
> +    list.det_index1(ArgModes, ArgNum, SelectedArgMode),
> +    ( if
> +        mode_get_insts_semidet(ModuleInfo, SelectedArgMode,
> +            InitInst, FinalInst),
> +        InitInst = FinalInst,
> +        (
> +            InitInst = ground(shared, none_or_default_func)
> +        ;
> +            InitInst = defined_inst(user_inst(SymName, [])),
> +            SymName = qualified(mercury_public_builtin_module, Name),
> +            ( Name = "in" ; Name = "input" )


"in" and "input" are modes, not insts.

...

> +
> +%---------------------------------------------------------------------------%
> +
> +% XXX I (zs) think these should be in library/list.m.

No objection to adding them from me. list.m already has replace_nth and
det_replace_nth, although their arguments somehow survived the great state-var
reordering.

> +
> +:- pred det_replace_nth_element1(int::in, T::in, list(T)::in, list(T)::out)
> +    is det.
> +
> +det_replace_nth_element1(N, NewItem, Items0, Items) :-
> +    ( if replace_nth_element1(N, NewItem, Items0, ItemsPrime) then
> +        Items = ItemsPrime
> +    else
> +        unexpected($pred, "index out of range")
> +    ).
> +
> +:- pred replace_nth_element1(int::in, T::in, list(T)::in, list(T)::out)
> +    is semidet.
> +
> +replace_nth_element1(N, NewItem, Items0, Items) :-
> +    replace_nth_element0(N - 1, NewItem, Items0, Items).
> +
> +:- pred replace_nth_element0(int::in, T::in, list(T)::in, list(T)::out)
> +    is semidet.
> +
> +replace_nth_element0(N, NewItem, [Item0 | Items0], [Item | Items]) :-
> +    ( if N = 0 then
> +        Item = NewItem,
> +        Items = Items0
> +    else
> +        Item = Item0,
> +        replace_nth_element0(N - 1, NewItem, Items0, Items)
> +    ).

...

> diff --git a/compiler/intermod.m b/compiler/intermod.m
> index ed92cd45f..91cded7f2 100644
> --- a/compiler/intermod.m
> +++ b/compiler/intermod.m

The clauses in the .opt file are taken from the clauses_info, so should
never rely on an input_spec pragma being present.  Adding input_spec
pragmas to .opt files shoudl improve the ability of importing modules
to optimized, but their absence is safe.  Is that a correct reading
of this. (It's probably worth noting this in prog_parse_tree.m at the spot
where we define the contents of .opt files.
...

> diff --git a/compiler/prog_item_pragma.m b/compiler/prog_item_pragma.m
> index c20a9b365..6bfe4e948 100644
> --- a/compiler/prog_item_pragma.m
> +++ b/compiler/prog_item_pragma.m


...

>  %---------------------%
>
> +:- type decl_pragma_input_spec_info
> +    --->    decl_pragma_input_spec_info(
> +                % This pragma tells the compiler to replace code
> +                % that switches on values of a control type at runtime
> +                % with code that switches on those values at compile time.
> +                %
> +                % Given a type such as
> +                %
> +                %   :- type action
> +                %       --->    hoist_nested_funcs
> +                %       ;       chain_gc_stack_frames.
> +                %
> +                % and its insts
> +                %
> +                %   :- inst hoist for action/0
> +                %       --->    hoist_nested_funcs.
> +                %   :- inst chain for action/0
> +                %       --->    chain_gc_stack_frames.
> +                %
> +                % input_spec pragma for type action with insts
> +                % hoist and chain can replace a mode that contains
> +                % an "in" argument of the action type with two modes
> +                % that contain "in(hoist)" and "in(chain)" respectively.
> +                % This effectively allows switches on that argument
> +                % to be performed at compile time. (This is a real example
> +                % from ml_elim_nested.m.)
> +
> +                % The name of the module that this pragma occurs in.
> +                % The pragma applies to the predicates and functions
> +                % defined in this module, and *only* those defined
> +                % in this module.
> +                %
> +                % If and when we start --intermod-opt to include
> +                % input_spec pragmas in .opt files, we may also
> +                % need to record the section (interface vs implementation)
> +                % in which the pragma occurred.
> +                ispec_module_name       :: module_name,
> +
> +                % We input specialize arguments of this type.
> +                ispec_arg_type          :: mer_type,
> +
> +                % Do we replace the generic "in" mode with the set of
> +                % specialized "in(inst_n)" modes, or do keep the old "in"
> +                % mode as well? Only the latter preserves the ability to call
> +                % the transformed predicate or function without knowing
> +                % which of the specialized insts is applicable.
> +                ispec_replace_or_add    :: replace_or_add_in_mode,
> +
> +                % The insts we specialize arguments of the selected type for.
> +                % The pragma in the code contains each inst_ctor as simply
> +                % a name; the parser adds the arity, which will be zero.
> +                % (Input specialization is not applicable to any inst_ctor
> +                % that takes any arguments.)
> +                %
> +                % We keep each inst_ctor in two forms: an inst_ctor,
> +                % and an inst that applies that inst_ctor to the empty list
> +                % of arguments.
> +                %
> +                % Both forms start out as just containing the inst name
> +                % that the program contains, and then both get module
> +                % qualified along with the test of the compilation unit.

s/test/rest/

> +                % The difference between them is that the inst form
> +                % then also gets any inst equivalences in it expanded out.
> +                % It is the inst form that input_specialization.m uses
> +                % to actually implement the pragma, but for writing out
> +                % the pragma, we want the non-equivalence-expanded form
> +                % (since the expansion result can change if the set of
> +                % visible inst equivalences changes.)
> +                ispec_spec_inst_ctors   :: one_or_more(inst_ctor),
> +                ispec_spec_insts        :: one_or_more(mer_inst),
> +
> +                % The equivalence types and insts used.
> +                %
> +                % At the moment, we gather this info, but then ignore it.
> +                % For smart recompilation to work, we need to fix the latter.
> +                ispec_items             :: set(recomp_item_id),
> +
> +                ispec_tvarset           :: tvarset,
> +                % We do not need an inst_varset.
> +
> +                ispec_context           :: prog_context,
> +                ispec_seq_num           :: item_seq_num
> +            ).
> +
> +    % Do we want to *replace* the "in" mode with the specialized
> +    % "in(inst1)", "in(inst2)" modes, or do we want to *add* them?
> +    %
> +    % Note that add_to_in_mode is the only allowed value if the pragma
> +    % occurs in the interface (and therefore applies to predicates
> +    % in the interface).
> +:- type replace_or_add_in_mode
> +    --->    replace_in_mode
> +    ;       add_to_in_mode.
> +
> +%---------------------%
> +
>  :- type decl_pragma_oisu_info
>      --->    decl_pragma_oisu_info(
>                  oisu_type_ctor          :: type_ctor,

None of the additional tests cover the case where input_spec pragma occurs in
a module inteface with replace_in_mode.

Julien.


More information about the reviews mailing list