Re: [RFC PATCH] pre-process: add __VA_OPT__ support

Al Viro <[email protected]> Mon, 16 Mar 2026 06:56:22 +0000
Newsgroups org.kernel.vger.linux-sparse
Message-ID <20260316065622.GA607739@ZenIV>
On Thu, Feb 26, 2026 at 07:29:45AM +0000, Al Viro wrote:
> On Wed, Feb 25, 2026 at 10:18:51PM +0000, Al Viro wrote:
> 
> > NOTE: substitute() is the second hottest loop in the entire thing; only
> > tokenizer is hotter.  And gcc is too enthusiastic about the inlining
> > around that function, ending up with bad register spills, along with
> > a bunch of stalls.  Worse, decisions are sensitive to minor changes in
> > places textually far away, making it a real bitch to deal with.
> > Makes for fun reordering the commits in local queue... ;-/
> 
> FWIW, looking at that thing again, I wonder if we would be better off
> with doing argument expansion on demand rather than doing it in
> expand_arguments().  Should be doable with a bit of care - we'd need
> to mark the TOKEN_..._ARG with several bits to decide whether we
> want to duplicate or not, etc., but that's worth doing anyway -
> better than playing with the counters.
> 
> Note, BTW, that collapsing TOKEN_..._ARG together, with "kind of argument"
> moved into bits stolen from ->argnum improves code generation - that
> switch by token type is _hot_ and it reducing the number of cases
> gives a measurable speedup.  Sure, we don't want heavy work at #define
> time - most of the macros are never expanded at all, but AFAICS this
> kind of processing can be dealt with while parsing the body, with no
> extra passes needed, etc.
> 
> I'm going down right now, will look into that tomorrow morning...

That turned out to be trickier than I hoped, but I've got something that
works.

See git://git.kernel.org/pub/scm/linux/kernel/git/viro/sparse.git #va_opt
(or individual patches in followups)

__VA_OPT__ supported, AFAICS behaviour matches C23.
	* expansion and stringifying of arguments is full-lazy now -
done on demand and at most once.
	* va-opt-replacement parsed at #define time, handled correctly
by dump_macro() (i.e. -dM), comparisons when redefining and at expansion
time.
	* arglist mangling is gone, so's the argcount kludge.
	* it's no slower than it used to be prior to that series.

I have local followups (tentative fixes for whitespace handling in preprocessor
and optimizations in tokenizer), but let's deal with that one first.

Shortlog:
Al Viro (21):
      split copy() into "need to copy" and "can move in place" cases
      expand and simplify the call of dup_token() in copy()
      more dup_token() optimizations
      parsing #define: saner handling of argument count, part 1
      simplify collect_arguments() and fix error handling there
      try_arg(): don't use arglist for argument name lookups
      make expand_has_...() responsible for expanding its argument
      preparing to change argument number encoding for TOKEN_..._ARGUMENT
      steal 2 bits from argnum for argument kind
      on-demand argument expansion
      kill create_arglist()
      stop mangling arglist, get rid of TOKEN_ARG_COUNT
      deal with ## on arguments separately
      preparations for __VA_OPT__ support: reshuffle argument slot assignments
      pre-process.c: split try_arg()
      __VA_OPT__: parsing
      expansion-time va_opt handling
      merge(): saner handling of ->noexpand
      simplify the calling conventions of collect_arguments()
      make expand_one_symbol() inline
      substitute(): convert switch() into cascade of ifs

Diffstat:
 ident-list.h                                |   1 +
 pre-process.c                               | 929 +++++++++++++++++-----------
 symbol.h                                    |   1 +
 token.h                                     |  32 +-
 tokenize.c                                  |   4 -
 validation/preprocessor/bad-args.c          |  18 +
 validation/preprocessor/dump-macro.c        |  13 +
 validation/preprocessor/has-attribute.c     |   3 +
 validation/preprocessor/has-builtin.c       |   3 +
 validation/preprocessor/va_opt.c            |  54 ++
 validation/preprocessor/va_opt2.c           |  34 +
 validation/preprocessor/va_opt_compare.c    |  28 +
 validation/preprocessor/va_opt_parse.c      |  37 ++
 validation/preprocessor/va_opt_whitespace.c |  14 +
 14 files changed, 797 insertions(+), 374 deletions(-)
 create mode 100644 validation/preprocessor/bad-args.c
 create mode 100644 validation/preprocessor/dump-macro.c
 create mode 100644 validation/preprocessor/va_opt.c
 create mode 100644 validation/preprocessor/va_opt2.c
 create mode 100644 validation/preprocessor/va_opt_compare.c
 create mode 100644 validation/preprocessor/va_opt_parse.c
 create mode 100644 validation/preprocessor/va_opt_whitespace.c


PS: as for the interesting uses of __VA_OPT__, consider this:
; cat >test.c <<'EOF'
// based on a fun trick from David Mazières
// see https://www.scs.stanford.edu/~dm/blog/va-opt.html for the entire story
// No, it's not unbounded recursion - up to 256 (4^4) elements in __VA_ARGS__;
// more with trivial modifications, just add more levels to EXPAND...
#define PARENS ()
#define EXPAND(...) EXPAND4(EXPAND4(EXPAND4(EXPAND4(__VA_ARGS__))))
#define EXPAND4(...) EXPAND3(EXPAND3(EXPAND3(EXPAND3(__VA_ARGS__))))
#define EXPAND3(...) EXPAND2(EXPAND2(EXPAND2(EXPAND2(__VA_ARGS__))))
#define EXPAND2(...) EXPAND1(EXPAND1(EXPAND1(EXPAND1(__VA_ARGS__))))
#define EXPAND1(...) __VA_ARGS__
#define FOR_EACH_PAIR(macro, ...)				\
  __VA_OPT__(EXPAND(FOR_EACH_PAIR_HELPER(macro, __VA_ARGS__)))
#define FOR_EACH_PAIR_HELPER(macro, a1, a2, ...)		\
  macro(a1, a2)							\
  __VA_OPT__(FOR_EACH_PAIR_AGAIN PARENS (macro, __VA_ARGS__))
#define FOR_EACH_PAIR_AGAIN() FOR_EACH_PAIR_HELPER

FOR_EACH_PAIR(F, t1, id1, t2, id2, t3, id3, t4, id4, t5, id5, t6, id6)
EOF
; cpp -E test.c
# 0 "test.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "test.c"
# 18 "test.c"
F(t1, id1) F(t2, id2) F(t3, id3) F(t4, id4) F(t5, id5) F(t6, id6)
;

and the same output from sparse, modulo the # ... lines - sparse -E doesn't
produce those.  Our (fairly brittle) analogue is __MAP in linux/syscalls.h
and if nothing else, unlike __MAP() this thing does not need the number
of pairs passed as explicit argument.  Would be interesting to try unifying
SYSCALL0..SYSCALL6 into a single macro that would bloody well _count_ the
arguments...