[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