m4sugar: clean up iteration impls and test them more thoroughly
All of the m4sugar macros that iterate over a macro argument list
have two implementations. One is in m4sugar.m4 and operates by
recursion, processing some of the leading elements of $@ and then
re-invoking itself on the remainder, using `m4_shift`. This is the
most straightforward way to iterate over an argument list in M4, but
it is quadratically expensive in GNU M4 1.4.x.
Thus, we have a second implementation of each such macro in
m4sugar/foreach.m4. Those work by generating a temporary helper macro
that references each argument by number, no matter how many there are,
so that $@ only needs to be expanded once. This is linear in the size
of the argument list, but less portable (as it uses $10 and up) and
much harder to understand. According to the comments in foreach.m4,
GNU M4 1.6.x is going to be cleverer about recursive macro expansion,
and the straightforward implementations will be *faster* than the
non-recursive ones. So we’ve kept the straightforward implementations
around, and m4_init replaces them with the foreach.m4 implementations
if it detects M4 1.4.x is in use.
I say “according to” and “is going to be” because those comments were
written in 2008 and, as of 2024, the latest released version of GNU M4
is still from the 1.4.x series. Development branches for versions
1.6.x and 2.x exist (see <https://git.savannah.gnu.org/cgit/m4.git>)
but have not seen any activity in the past fourteen and eight years,
respectively.
This is a *problem* for us because:
- If you forget to call m4_init, you get the recursive implementations,
which might mean whatever weird one-off test you’re doing hangs.
- Some of the recursive implementations have fallen out of sync with
the foreach implementations, as seen in the previous two patches.
(I noticed this because I forgot to call m4_init in some weird
one-off tests.)
This patch therefore makes the following changes:
- All the recursive implementations are moved from m4sugar.m4 to a
new file m4sugar/recursive.m4.
- m4sugar.m4 loads m4sugar/foreach.m4 at freeze time, so if you
forget to call m4_init, you get the foreach implementations.
This should also make Autoconf an eensy bit faster in the normal
case. m4_init loads m4sugar/recursive.m4 if it detects M4 1.6.x or later.
- Since we now require M4 1.4.8 or later, some compatibility glue for
M4 1.4.[67] is removed from m4_init.
- AT_CHECK_M4SUGAR and AT_CHECK_M4SUGAR_TEXT now, by default, run
their test twice, once with the recursive implementations forced and
once with the foreach implementations forced, and fails the overall
test if the results are not identical. I was expecting this to
expose more mismatches between the two implementations but it did not.
Currently I don’t think it’s necessary to do this for any of the
other tests, but I could have missed something.
Only two subtests need to alter the new default: both are specifically
testing for linear performance with large argument lists. Each of
these is split from its parent test and divided into two new tests,
one which compares the foreach implementation to the *default*
implementation and runs unconditionally, and a second which tests
only the recursive implementation and is skipped with M4 1.4.x.
Both pairs of tests have constant expected output, so we still get
a cross-check between recursive and foreach when M4 1.6.x is used.
- To make it easy for AT_CHECK_M4SUGAR to force one implementation or
the other, there is now an internal override macro that can be
defined before invoking m4_init. It only affects the choice of
recursive.m4 versus foreach.m4, not any of the other things m4_init
does if it detects M4 1.6.x.
- autom4te encapsulates use of the override macro using --language:
--language m4sugar-recursive-iteration will always use the recursive
implementations and --language m4sugar-foreach-iteration will always
use the foreach implementations. (There are no equivalents of these
for any of the other languages built on top of m4sugar.)
- To make *that* possible, autom4te grows support for -D and -U on
its own command line, which are directly mapped to -D and -U on
m4’s command line. This should be the only user visible change in
the patch.
I did not test this patch with M4 1.6.x or 2.x, but I *did* locally
comment out the AT_SKIP_IFs on the tests that are expected to be slow
with M4 1.4.x. They pass (providing more evidence that both
implementations are now in sync) but they take several minutes each to
run on my 2018-era workstation (verifying that foreach.m4 is still
necessary).
* lib/m4sugar/m4sugar.m4 (m4_bmatch, _m4_bpatsubsts, m4_case, _m4_cond)
(m4_do, m4_dquote_elt, _m4_foreach, m4_join, _m4_list_cmp)
(_m4_list_cmp_1, _m4_list_cmp_2, _m4_list_cmp_raw, m4_map_args_pair)
(_m4_minmax, m4_reverse, _m4_set_add_all_check, _m4_set_add_all_clean)
(_m4_shiftn): Move definition to lib/m4sugar/recursive.m4.
(m4_cr_all): Use _m4_for instead of m4_for in the definition.
(m4_min): Definition is textually identical to m4_max, so m4_copy it
instead of repeating it.
(m4_init): Instead of including m4sugar/foreach.m4 if __m4_version__
is not defined, include m4sugar/recursive.m4 if that macro *is* defined.
However, if __m4sugar_use_iteration is defined to either ‘recursive’
or ‘foreach’, then use that implementation regardless of __m4_version__.
Remove backward compatibility code for M4 <1.4.8.
(top level): Include m4sugar/foreach.m4 at freeze time.
(throughout): Update comments related to above changes.
* lib/m4sugar/recursive.m4: New file containing the recursive
versions of each m4sugar macro that has a non-recursive version
in foreach.m4. Each macro defined in this file should also be
defined in foreach.m4 and vice versa, except for subroutines used
only by one or the other.
* lib/m4sugar/foreach.m4: Update comments throughout. No code changes.
* lib/local.mk: Install lib/m4sugar/recursive.m4.
* lib/autom4te.in: Add new languages 'm4sugar-recursive-iteration'
and 'm4sugar-foreach-iteration', marked as intended for testing only.
These are the same as the plain 'm4sugar' language except that
__m4sugar_use_iteration is predefined to 'recursive' or 'foreach'
respectively.
* bin/autom4te.in: Recognize command line options -D/--define and
-U/--undefine and pass them along to M4, preserving their order
relative to each other and relative to .m4(f) files.
* doc/autoconf.texi: Document new autom4te options.
* tests/local.at (AT_PREPARE): Set shell variable at_mfour_slow_recursion
to ':' if M4 doesn't define __m4_version__, or to 'false' if it does.
Log the choice.
(_AT_CHECK_M4SUGAR, __AT_CHECK_M4SUGAR): New helper macros.
(AT_CHECK_M4SUGAR): New fifth argument, VARIANTS, expected to be a
space-separated list of keywords 'default', 'recursive', and/or
'foreach'; defaults to 'recursive foreach'. Run the entire test
repeatedly, once for each specified variant, with identical
expectations for each. In addition, expect the generated script
to be identical for each variant tested.
(AT_CHECK_M4SUGAR_TEXT): Move here from m4sugar.at; reimplement
without using '-o -'; add same fifth argument as was added to
AT_CHECK_M4SUGAR.
* tests/m4sugar.at (AT_CHECK_M4SUGAR_TEXT): Moved to local.at.
(m4st_long_iteration_script, m4st_long_iteration_output)
(m4st_set_stress_script, m4st_set_stress_output): New helper macros.
(recursion test): Split into two tests, now called
“iteration macros, long lists (foreach/default)” and
“iteration macros, long lists (recursive)”.
Test the recursive implementation only if M4 >=1.6 is detected.
(m4_set test): Similarly for the final subtest.
(throughout): Do not use '-o -' in AT_CHECK_M4SUGAR tests.
Redo subtests using AT_CHECK_M4SUGAR_TEXT when possible.
11 files changed