Visitar URL original
Expand function macros that have arguments at the end by pfultz2 · Pull Request #695 · cppcheck-opensource/simplecpp · GitHub
Skip to content

Expand function macros that have arguments at the end - #695

Open
pfultz2 wants to merge 5 commits into
cppcheck-opensource:masterfrom
pfultz2:recursive-expand-macro-function
Open

pfultz2 wants to merge 5 commits into
cppcheck-opensource:masterfrom
pfultz2:recursive-expand-macro-function

Conversation

@pfultz2

@pfultz2 pfultz2 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #549

Comment thread simplecpp.cpp Outdated
// consumes that macro call from the token stream, so tok always advances.
/** Expand while the expansion result in @p temp ends with the name of a
* function-like macro whose arguments are supplied by the tokens after @p tok.
* Each round consumes that macro call from the token stream, so tok always

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure that "so tok always advances" will be true now. If rescanMacro returns false directly should we return tok->next?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated it to return tok->next instead relying on the callers to call tok->next.

@pfultz2
pfultz2 requested a review from danmar October 27, 2026 19:35

@dmcppcheck dmcppcheck left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review

I found no new problems. When a macro argument expands to the name of a function-like macro and the call's ( ... ) comes later in the same argument, the PR now expands that call. I tested about 50 variations, and every one that changed now matches gcc and clang. Every remaining difference from them also exists on base. I couldn't find an input that this PR handles wrongly. The only test added (define27) is a fuzzer-style case whose expected output is empty. It doesn't show the basic behaviour, such as ID(G()(1)) → [1], so a few direct regression tests would help.

Testing

Build and test suites

  • make -j8 testrunner simplecpp CXXOPTS=-Werror in pr/ built with no warnings.
  • ./testrunner passed.
  • python3 run-tests.py: 0 failed, 16 todos. c99-6_10_3_4_p5.c now passes.
  • python3 -m pytest integration_test.py: 128 passed.
  • ./selfcheck.sh passed.
  • I also built base/ to compare results.

Comparison with gcc and clang
I wrote three files in scratch/ (t1.c, t2.c, t3.c) and compared pr/simplecpp, base/simplecpp, gcc -E -P and clang -E -P after removing spaces. They share these definitions:

#define f(x) <x>     /* [x] in t1.c and t2.c */
#define ID(x) x
#define G() f

The cases covered:

  • Calls inside the argument: ID(G()(1)), ID(G()(G()(2))), f(G()(f(3))), ID(G()((a,b))).
  • The macro name as the trailing token: #define GI() ID with ID(GI()(1)), and ID(ID(I)(0)(6)).
  • Calls that alternate or refer to themselves: #define A(x) B / #define B(x) A with ID(A(1)(2)(3)(4)), and #define R() R with ID(R()()()).
  • Nested and repeated arguments: two calls inside a two-parameter macro's arguments, the argument used twice (W(G()(4))), an object-like macro (ID(OBJ(5))), and GG()()(10).
  • Variadic macros, #x through XNUM, and ##.
  • A call that is completed only after the argument ends: ID(G())(1).
  • The ( on the next line.
  • An error case: ID(G()(1,2)) gives "Wrong number of parameters for macro 'f'". gcc and clang also report an error.

In all of these the PR output matches gcc and clang. The cases that base got wrong are now right, for example:

  • ID(A(1)(2)(3)(4)): base gave B(2)(3)(4), the PR gives A.
  • ID(G()(f)(f)(3)): base gave f(f)(f)(3), the PR gives [f](f)(3).

Two cases still differ from gcc, and base gives the same output for both, so the PR did not cause them:

  • ID(G() E (8)) with an empty E: simplecpp gives f(8), gcc gives <8>.
  • C2(G(), (3)) with #define C2(a,b) a b: simplecpp gives f(3), gcc gives [3].

Performance
A file with 60000 lines of nested B(B(n)) macros, each argument expanded more than once, took about 3.6 s with the PR and 3.7 s with base, so there is no slowdown.


Automated review of 148b1c2 by Claude. The pull request was built and tested locally.

Comment thread test.cpp Outdated
"#define k() l(j(, ), )\n"
"#define n(a) j(l(o, )(a), )\n"
"n(k)\n";
ASSERT_EQUALS("", preprocess(code));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review

The only new test is a large, fuzzer-style case whose expected output is "", so it doesn't show what behaviour it protects. Please add a few direct cases, for example with #define f(x) [x], #define ID(x) x and #define G() f:

  • ID(G()(1)) → [ 1 ]
  • ID(G()(G()(2))) → [ [ 2 ] ]
  • #define A(x) B / #define B(x) A with ID(A(1)(2)(3)(4)) → A (each round must use the right set of already-expanded macros)

I checked all three against gcc, and they pass with this PR and fail on base.

@dmcppcheck dmcppcheck left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review

I found no new problems. The code is the same as in 148b1c2, which the earlier review covered. The new commit 26ceaf9 adds the direct tests that review asked for: ID(G()(1)), ID(G()(G()(2))) and ID(A(1)(2)(3)(4)). I tried about 30 more inputs. In every one that this PR changes, the output now matches gcc and clang, and I couldn't find an input that it handles wrongly. Each difference from gcc and clang that remains also happens on base.

Testing

Build and test suites (in pr/)

  • make -j8 testrunner simplecpp CXXOPTS=-Werror built with no warnings.
  • ./testrunner passed (exit code 0), including define27.
  • python3 run-tests.py: 0 failed, 16 todos.
  • python3 -m pytest integration_test.py: 128 passed.
  • ./selfcheck.sh passed (exit code 0).
  • I also built base/ to compare results.

Comparison with gcc and clang
I wrote scratch/a.c, scratch/b.c and scratch/c.c, ran pr/simplecpp, base/simplecpp, gcc -E -P and clang -E -P on each, and compared the outputs. These are cases the earlier review did not cover:

  • Two calls after the name: ID(H()()(1)), where H() gives G.
  • A trailing call to a macro that takes two arguments: ID(G2()(1,2)).
  • A variadic V(G()(1), G()(2)).
  • Stringizing an argument after it is expanded: XS(G()(3)) gives "[3]".
  • An object-like macro that expands to a function-like name: #define O f with ID(O(1)). Base gave f(1); the PR gives [1].
  • The macro being expanded named again in its own argument: ID(GI()(2)) gives 2, and SELF(GS()(GS()(11))) gives < < <11> > >.
  • A result that ends with a macro that refers to itself: #define T(x) x T with ID(GT()(4)(5)) gives 4 T(5).
  • Painted tokens that must not expand again: #define P(x) [x] P with ID(P(1)(2)) gives [1] P(2), and ID(FF(1)(2)(3)) gives (1) FF(2)(3).
  • Argument tokens that come from a macro body: #define W() ID(G()(1)) with W() gives [1].
  • Calls nested inside the trailing call's arguments: ID(G()(ID(G()(9)))) and ID(G()((10))).
  • ID(ID(f)(5)) and ID(GID()(f)(11)).

All of these now match gcc and clang. Three inputs still differ from them, and base differs on them as well:

  • CAT(G()(4),) with #define CAT(a,b) a##b: simplecpp gives G ( ) ( 4 ), gcc gives [4].
  • ID(ID)(G()(10)): the PR gives [ 10 ] and base gives f ( 10 ), but gcc gives ID([10]). Both drop the ID, so the PR did not cause this.
  • TW(G(), (8)) with #define TW(x, y) x y: simplecpp gives f ( 8 ), gcc gives [8]. The earlier review already reported this one.

Automated review of 26ceaf9 by Claude. The pull request was built and tested locally.

@pfultz2

pfultz2 commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

@danmar Can you rereview?

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.

failure to preprocess zephyr (blinky example)

3 participants