Skip to content

Fix #742 (Add a macro expander that uses hide sets) - #743

Draft
danmar wants to merge 28 commits into
masterfrom
fix-742
Draft

danmar wants to merge 28 commits into
masterfrom
fix-742

Conversation

@danmar

@danmar danmar commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #742 (draft, work in progress).

Adds a macro expander that uses Prosser's hide-set algorithm, next to the old Macro::expand(). It is off by default and selected with DUI::hideSetExpansion (simplecpp option -hideset-expansion). testrunner runs every test with both expansions; HIDESET_EXPANSION_TODO() skips a test that the new expansion can't handle yet.

How well the new macro expansion works

clang and gcc preprocessor testsuites (run-tests.py; a test passes when the output is the same as clang's or gcc's):

Testsuite Tests Old expansion New expansion
clang 136 124 126
gcc 19 16 16
total 155 140 142
  • No test that passes with the old expansion fails with the new one.
  • macro_paste_hard.c and macro_rescan_varargs.c fail with the old expansion (todo list of run-tests.py) and pass with the new one.
  • The 13 tests that fail with the new expansion also fail with the old one.

testrunner: 323 tests, all run with both expansions. With the new expansion 6 are skipped:

Not handled yet Tests
the old Token::macro and location rules 4
the comma before an empty __VA_ARGS__ is removed 1
an unterminated macro call 1

Real code: for 1,687 files (/usr/include, cppcheck lib and test, simplecpp testsuite) the output is the same with both expansions, except for 14 files of the clang/gcc testsuites. In 11 of them the new expansion gives clang's or gcc's result and the old one doesn't; in the other 3 both are wrong in the same way.

Correct with the new expansion, wrong with the old one (the results are the same as gcc's; the tests use TODO_OLD_EXPANSION_ASSERT_EQUALS()):

Input gcc / new old
#define f(x) x / f(f)(1) f(1) 1
#define R() / R at the end of the file R error: wrong number of parameters
ELLIP_FUNC(F LPAREN 'a', 'b' RPAREN) (clang macro_rescan_varargs.c) 'a' + 'b' F ( 'a' , 'b' )
#define r(x,y) x ## y / { r(,) } (C11 6.10.3.5 EXAMPLE 3 and 5) { } an empty token between the braces
#define A x######x / A (gcc pr58844-1.c) xx x## x
#define F(...) <__VA_OPT__(x)> / F(E), E expands to nothing <> <x>
#define F(a,...) #__VA_OPT__(a b) / F(X,2) with #define X 1 "1 b" "X" b
#define F(a,...) a ## __VA_OPT__(b c) ## d / F(x) xd x# "d"
#define HASH # / HASH define foo bar # define foo bar on line 2 # on line 1 (the location of the #define)

simplecpp issues fixed by the new macro expansion

The examples in these issues give the same result as gcc with the new expansion; they are added as tests (TODO_OLD_EXPANSION_ASSERT_EQUALS() where the old expansion is wrong).

Issue Example Old New / gcc
#226, #713 EVAL ( HELLO EMPTY() () ) HELLO ( ) "Hello World"
#713 EVAL(REPEAT(4, M, ~)) (cloak.h idioms) REPEAT ( 3 , M , ~ ) 3 0 1 2 3
#656 PAR(PREFIX_ ## kind, (__VA_ARGS__)) PREFIX_SCALAR ( int , x ) int x
#394 MACRO_COMPOSE(FOO, BAR(1)) with A ## B FOOBAR ( 1 ) 1
#259 PREFIX##NAME##EXTENSION with empty arguments error: invalid ## int foo ;
#378 a ## ## c (consecutive ##) x# "y" xy
#458 # __VA_OPT__(# x), # __VA_OPT__(x ## __VA_ARGS__) "x" "Hello" world "\"x\"" "Helloworld"
#491 #define HASH # / HASH define foo bar # on line 1 # define foo bar on line 2

For the #713 iteration idioms, a hide set only applies while an expansion is rescanned: a macro name that is found in the hide set of its own macro is never replaced ("painted", C11 6.10.3.4p2), but other tokens can be expanded again when they are rescanned later, for example as part of an argument.

Not fixed by this PR: defined(X ## Y) with parentheses in #if (#653, #218, #75) is not handled by either expansion; it can be fixed in findDefinedOperand(), which both use. An unterminated macro call (#414) is still not an error.

Commits

Preparation, can be split out into separate PRs:

  • Regression tests: C11 6.10.3.5 examples, C23 __VA_OPT__ examples, invalid ##, and Token::macro, expanded token locations and MacroUsage (used by cppcheck). Known bugs use the new TODO_ASSERT_EQUALS().
  • Functions broken out of the old expansion and used by the new one: the ## checks, stringify(), the argument count check in getMacroParameters(), expandBuiltin(), findDefinedOperand()/definedMacroName(), copyMacroCall(), setLocation(), and combineWithNext() in combineOperators().
  • Fixes: ## of a name or number with a string literal is an error (cat(a,"s")); # keeps the space after an operator of two characters (str(a == 0)); a # from a macro at the start of an output line gets a space before it, as gcc and clang do.

Hide sets and the new expansion:

  • Token::mExpandedFrom becomes the hide set mHideSet, with macro names instead of Macro pointers.
  • HideSetExpander, added step by step: object-like and function-like macros, __VA_ARGS__ and the built-in macros; then #, ## (with placemarkers, as in C11 6.10.3.3), defined in a replacement list, and __VA_OPT__ (C23 6.10.5.1). A hide set only applies while the expansion is rescanned.

All commits except the fixes above have no behaviour change with the default (old) expansion.

Still to do

  • the comma before an empty __VA_ARGS__ (MSVC-like, done by the old expansion)
  • an unterminated macro call
  • decide the Token::macro and location rules and follow them (cppcheck uses them)
  • test in cppcheck with hideSetExpansion
  • compare the speed

🤖 Generated with Claude Code

@danmar

danmar commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator Author

@chrchr-github @pfultz2 @web-flow @glankk @ludviggunne @wjakobsson I do not plan to merge this PR "as is" . This is a proof-of-concept PR written by AI. But can you please give me feedback about the "philosophy".

I want to restructure macro expansion in simplecpp to follow the standard better and use best-practices for C preprocessors. When I wrote simplecpp I did not have a complete understanding of macro expansion. Issue #742 has a section "Problems with the current macro expansion" that explains the problems.

If we agree about the goal and philosophy then I will try to start splitting out some refactorings from this PR that are small and safe. I will thereby reduce the size of this PR. And then when it's smaller we can make up a plan about how we convert macro expansion in simplecpp as safely as possible.
Testing the changes on a large number of Debian packages before we convert would seem logical to me.
We could also test some certain repos that we know are difficult.. I know we have problems with zephyr for instance.

dmcppcheck and others added 28 commits November 8, 2026 16:26
C11 6.10.3.5 examples 3, 4, 5 and 7, the C23 __VA_OPT__ examples and invalid
## pastes. Known bugs are tested with the new TODO_ASSERT_EQUALS.

Tests for Token::macro, the locations of expanded tokens and the macro usage,
which are used by cppcheck. A new helper expandedTokens() writes each output
token with its macro name and location; the tokenMacro tests use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
checkHashHashOperands(), checkPunctuatorPaste() and
checkUniversalCharacterPaste() only depend on the tokens, not on the
expansion state, so a new expander can use them too. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… error

Only an encoding prefix such as L, u8 or R gives a valid token, as in
L ## "s". Before, cat(a,"s") gave the invalid token a"s".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stringify() only depends on the tokens, so a new expander can use it too.
No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ors()

No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
combineOperators() did not copy whitespaceahead from the second character,
so str(a == 0) gave "a ==0" (C11 6.10.3.5 EXAMPLE 4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It now returns the argument delimiters of a valid call, or throws
wrongNumberOfParameters, so a new expander can use it. Tests are added for
the argument count. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e an error

gcc outputs the name, simplecpp reports wrong number of parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No behaviour change. A test is added for defined at the end of the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The whitespace is copied in Macro::newMacroToken() instead. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setExpandedFrom(), markExpandedFrom() and isExpandedFrom() are renamed to
setHideSet(), addToHideSet() and isInHideSet(). No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The names are what Prosser's algorithm uses, and they can not dangle
after #undef. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DUI::hideSetExpansion (simplecpp option -hideset-expansion) selects it. It
implements Prosser's algorithm for object-like and function-like macros:
arguments are expanded once, and the result of an expansion is pushed back
onto the input, so a call whose arguments follow in the source is found by
normal rescanning.

testrunner runs all tests with both macro expansions; HIDESET_EXPANSION_TODO()
skips a test that the new one can not handle yet. The macro usage in the tests
is sorted, its order depends on the standard library.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# needs the arguments that are not expanded. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# is an operator in a function-like macro when it is followed by a parameter;
the argument as written in the call is turned into a string with stringify().
The whitespace after a parameter comes after its argument, and the expansion
is followed by the whitespace after the macro call.

Output: a # from a macro expansion at the start of a line gets a space before
it, so it is not read as a directive, as gcc and clang do (clang tests
hash_space.c and macro_not_define.c).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s replaced by

## needs the operands before they are joined. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As in C11 6.10.3.3: an argument that is an operand of ## is not macro
expanded, an empty argument is a placemarker, and the pasted token is
rescanned. The ## checks of the old expansion are used. Like the old
expansion, ", ## __VA_ARGS__" removes the comma when there are no variable
arguments, and a string literal followed by a name is not pasted.

With the hide-set expansion, 141 of the 155 clang/gcc testsuite tests pass
(140 with the old expansion). The new test macro TODO_OLD_EXPANSION_ASSERT_EQUALS()
is for known bugs of the old expansion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MacroCall has what the replacement of a macro call depends on: the hide set,
the arguments and the whitespace after the call. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Token::macro is set in one place. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…roName()

They do not depend on how the old expansion stores the arguments, so the
hide-set expansion can use them. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
findDefinedOperand() and definedMacroName() are used, as in the old
expansion. 5 more tests pass with the hide-set expansion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… list

__VA_OPT__ will use it for its content. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As in C23 6.10.5.1: __VA_OPT__(content) is replaced by nothing when the
variable arguments are macro expanded to nothing, otherwise by its content,
which is substituted like the replacement list. #__VA_OPT__(...) gives a
string literal, and __VA_OPT__ can be an operand of ##.

These give the same result as gcc with the hide-set expansion, but not with
the old one: F(E) where E expands to nothing, #__VA_OPT__(a b), and
a ## __VA_OPT__(b c) ## d with no variable arguments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getOperand() was getting long. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants