diff --git a/run-tests.py b/run-tests.py index e3b0eeae..54cf2b4f 100644 --- a/run-tests.py +++ b/run-tests.py @@ -77,7 +77,6 @@ def cleanup(out: str) -> str: 'macro_fn_disable_expand.c', 'macro_paste_commaext.c', 'macro_paste_hard.c', - 'macro_rescan_varargs.c', # todo, high priority 'expr_usual_conversions.c', # condition is true: 4U - 30 >= 0 diff --git a/simplecpp.cpp b/simplecpp.cpp index bdec34a6..1e4f2d95 100644 --- a/simplecpp.cpp +++ b/simplecpp.cpp @@ -1731,7 +1731,7 @@ namespace simplecpp { } else if (rawtok->op == '(') { macro2tok = output2.back(); } - if (!macro2tok || !macro2tok->name) + if (!macro2tok || !macro2tok->name || macro2tok->skippedByRescan) break; if (output2.cfront() != output2.cback() && macro2tok->str() == this->name()) break; @@ -1949,6 +1949,10 @@ namespace simplecpp { return ~0U; } + bool isParameter(const Token *tok) const { + return tok->name && getArgNum(tok->str()) < args.size(); + } + std::vector getMacroParameters(const Token *nameTokInst, bool calledInDefine) const { if (!nameTokInst->next || nameTokInst->next->op != '(' || !functionLike()) return {}; @@ -2193,6 +2197,10 @@ namespace simplecpp { const Token * const callEnd = functionLike() ? parametertokens2.back() : nameTokInst; + // the call expanded to nothing: the rescan has passed the token before it + if (output.back() == output_end_1) + skipTrailingName(output); + // Whitespace at the end of the replacement list does not count. // The expansion is followed by the whitespace after the macro call. if (output.back() && output.back() != output_end_1) @@ -2201,50 +2209,185 @@ namespace simplecpp { return callEnd->next; } - /** Returns the macro to expand when the last token of @p temp is the name of a - * function-like macro and the tokens after @p tok supply its arguments; nullptr otherwise */ - static const Macro *rescanMacro(const TokenList &temp, const Token *tok, const MacroMap ¯os, const std::set &expandedmacros) { - if (!temp.cback() || !temp.cback()->name || !sameline(tok, tok->next) || tok->next->op != '(') + /** Returns the macro when @p tok is the name of a function-like macro + * that the rescan can still expand; nullptr otherwise */ + static const Macro *callableMacro(const Token *tok, const MacroMap ¯os, const std::set &expandedmacros) { + if (!tok || !tok->name || tok->skippedByRescan) return nullptr; - const MacroMap::const_iterator it = macros.find(temp.cback()->str()); - if (it == macros.end() || expandedmacros.find(temp.cback()->str()) != expandedmacros.end()) + const MacroMap::const_iterator it = macros.find(tok->str()); + if (it == macros.end() || expandedmacros.find(tok->str()) != expandedmacros.end()) return nullptr; - if (!it->second.functionLike() || temp.cback()->isExpandedFrom(&it->second)) + if (!it->second.functionLike() || tok->isExpandedFrom(&it->second)) return nullptr; return &it->second; } + /** Mark @p tok when it is the name of a macro that is being expanded, so + * that it is never expanded later */ + static void markExpanding(Token *tok, const MacroMap ¯os, const std::set &expandedmacros) { + if (!tok->name || expandedmacros.find(tok->str()) == expandedmacros.end()) + return; + const MacroMap::const_iterator it = macros.find(tok->str()); + if (it != macros.end()) + tok->markExpandedFrom(&it->second); + } + + /** A macro call that expanded to nothing came after the last token of + * @p output: the rescan has passed that token, so when it is the name of + * a function-like macro, a '(' that comes later does not start a call. */ + static void skipTrailingName(TokenList &output) { + if (output.back() && output.back()->name) + output.back()->skippedByRescan = true; + } + + /** Copy the call of the function-like macro named by @p nameTok whose + * "( ... )" follows @p tok into @p call, stopping before @p end. Returns + * the matching ')', or nullptr when there is no complete call. */ + static const Token *gatherCall(TokenList &call, const Token *nameTok, const Token *tok, const Token *end = nullptr) { + if (!sameline(tok, tok->next) || tok->next->op != '(') + return nullptr; + call.push_back(new Token(nameTok->str(), tok->next->location)); + unsigned int par = 0; + for (const Token *tok2 = tok->next; tok2 && tok2 != end; tok2 = tok2->next) { + call.push_back(new Token(*tok2)); + if (tok2->op == '(') { + ++par; + } else if (tok2->op == ')') { + if (--par == 0U) + return tok2; + } + } + return nullptr; + } + + /** Gather the call of the function-like macro named by @p nameTok whose + * arguments follow the replacement-list token @p tok into @p call. + * Parameters are substituted before the rescan, so the parentheses are + * counted in the substituted tokens: parameters that expand to nothing are + * skipped, and the '(' and the ')' of the call can each come from the + * replacement list or from a parameter. Returns the replacement-list token + * that supplied the ')', or nullptr when there is no complete call. When + * that token is a parameter, its expansion is copied whole since it may + * hold more than the end of the call. */ + const Token *gatherCallAfter(TokenList &call, const Token *nameTok, const Location &loc, const Token *tok, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens, OutputList *outputList) const { + TokenList callArgs(files); + unsigned int par = 0; + for (const Token *after = tok->next; sameline(tok, after);) { + if (par > 0U && after->op == '#' && sameline(after, after->next) && (after->next->op != '#' || sameline(after, after->next->next))) { + // # and ## keep the parentheses balanced + if (after->next->op == '#') + after = expandHashHash(callArgs, loc, after, macros, expandedmacros, parametertokens, outputList, false); + else + after = expandHash(callArgs, loc, after, expandedmacros, parametertokens, outputList); + continue; + } + const Token * const end = callArgs.cback(); + if (!expandArg(callArgs, after, loc, macros, expandedmacros, parametertokens, outputList)) { + // only parameters, which can expand to nothing, can come between + // the macro name and its '(' + if (par == 0U && after->op != '(') + return nullptr; + callArgs.push_back(new Token(*after)); + if (after->macro.empty() && par > 0U) + callArgs.back()->macro = name(); + } + for (const Token *tok2 = end ? end->next : callArgs.cfront(); tok2; tok2 = tok2->next) { + if (tok2->op == '(') { + ++par; + } else if (par == 0U) { + // the parameter does not start with '(' + return nullptr; + } else if (tok2->op == ')' && --par == 0U) { + // the tokens of the call must be on the same line + for (Token *tok3 = callArgs.front(); tok3 != tok2->next; tok3 = tok3->next) + tok3->location = callArgs.cfront()->location; + call.push_back(new Token(nameTok->str(), callArgs.cfront()->location)); + call.takeTokens(callArgs); + return after; + } + } + after = after->next; + } + return nullptr; + } + /** 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. The @p gatherCall - * callback copies the "( ... )" tokens that follow @p lpar into its output list - * and returns the matching ')', or nullptr when no complete call is available. - * Returns the first token that was not consumed (tok->next when no call was - * expanded). */ + * callback copies the macro name and its call into its list and returns the + * last token of the call, or nullptr when no complete call is available (see + * gatherCall and gatherCallAfter). Returns the first token that was not + * consumed (tok->next when no call was expanded). */ template const Token *expandTrailingCalls(TokenList &output, TokenList &temp, const Location &loc, const Token *tok, const MacroMap ¯os, const std::set &expandedmacros, OutputList *outputList, GatherCall gatherCall) const { - while (const Macro * const calledMacro = rescanMacro(temp, tok, macros, expandedmacros)) { - TokenList temp2(files); - temp2.push_back(new Token(temp.cback()->str(), tok->next->location)); - - const Token * const closingPar = gatherCall(temp2, tok->next); - if (!closingPar) + while (const Macro * const calledMacro = callableMacro(temp.cback(), macros, expandedmacros)) { + TokenList call(files); + const Token * const last = gatherCall(call, temp.cback(), tok); + if (!last) break; output.takeTokens(temp); output.deleteToken(output.back()); - calledMacro->expand(temp, loc, temp2.cfront(), macros, expandedmacros, outputList); - tok = closingPar; + if (last->name) { + // the call ended in a parameter that may hold more than the call + temp.takeTokens(call); + rescanArg(temp, loc, macros, expandedmacros, outputList); + } else { + calledMacro->expand(temp, loc, call.cfront(), macros, expandedmacros, outputList); + } + if (temp.empty()) + skipTrailingName(output); + tok = last; } output.takeTokens(temp); return tok->next; } const Token *recursiveExpandToken(TokenList &output, TokenList &temp, const Location &loc, const Token *tok, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens, OutputList *outputList) const { - return expandTrailingCalls(output, temp, loc, tok, macros, expandedmacros, outputList, [&](TokenList &temp2, const Token *lpar) { - return appendTokens(temp2, loc, lpar, macros, expandedmacros, parametertokens, outputList); + // the arguments of a trailing macro name can only follow as '(' or as + // parameters, which are substituted before the rescan + if (!sameline(tok, tok->next) || (tok->next->op != '(' && !isParameter(tok->next))) { + output.takeTokens(temp); + return tok->next; + } + return expandTrailingCalls(output, temp, loc, tok, macros, expandedmacros, outputList, [&](TokenList &call, const Token *nameTok, const Token *tok2) { + return gatherCallAfter(call, nameTok, loc, tok2, macros, expandedmacros, parametertokens, outputList); }); } + /** Rescan the macro argument in @p tokens after it has been substituted into + * the replacement list, expanding each call of a function-like macro that is + * complete within it. The argument pre-expansion leaves such a call behind + * when the macro name and its '(' were separated by tokens that expanded to + * nothing, as in "f EMPTY() (1)". The result of each call is rescanned + * against the tokens that follow it. Names of macros that are being expanded + * are marked so that they are never expanded later. The marks that the + * pre-expansion left on the tokens do not apply to this rescan. */ + void rescanArg(TokenList &tokens, const Location &loc, const MacroMap ¯os, const std::set &expandedmacros, OutputList *outputList) const { + // Usually the pre-expansion has left no call behind. Then the tokens are + // only changed in place, which avoids copying them at each nesting level. + bool hasCall = false; + for (Token *tok = tokens.front(); tok; tok = tok->next) { + tok->skippedByRescan = false; + if (hasCall) + continue; + hasCall = sameline(tok, tok->next) && tok->next->op == '(' && callableMacro(tok, macros, expandedmacros) != nullptr; + if (!hasCall) + markExpanding(tok, macros, expandedmacros); + } + if (!hasCall) + return; + TokenList input(files); + input.takeTokens(tokens); + for (const Token *tok = input.cfront(); tok;) { + TokenList temp(files); + temp.push_back(new Token(*tok)); + markExpanding(temp.back(), macros, expandedmacros); + tok = expandTrailingCalls(tokens, temp, loc, tok, macros, expandedmacros, outputList, [](TokenList &call, const Token *nameTok, const Token *tok2) { + return gatherCall(call, nameTok, tok2); + }); + } + } + const Token *expandToken(TokenList &output, const Location &loc, const Token *tok, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens, OutputList *outputList) const { // Not name.. if (!tok->name) { @@ -2255,10 +2398,18 @@ namespace simplecpp { // Macro parameter.. { TokenList temp(files); - if (expandArg(temp, tok, loc, macros, expandedmacros, parametertokens, outputList)) { + bool hasMacroName = false; + if (expandArg(temp, tok, loc, macros, expandedmacros, parametertokens, outputList, &hasMacroName)) { if (tok->str() == "__VA_ARGS__" && temp.empty() && output.cback() && output.cback()->str() == "," && tok->nextSkipComments() && tok->nextSkipComments()->str() == ")") output.deleteToken(output.back()); + if (hasMacroName && !temp.empty()) { + rescanArg(temp, loc, macros, expandedmacros, outputList); + // the argument held only macro calls that expanded to nothing: + // the rescan has passed the token before it + if (temp.empty()) + skipTrailingName(output); + } return recursiveExpandToken(output, temp, loc, tok, macros, expandedmacros, parametertokens, outputList); } } @@ -2273,30 +2424,14 @@ namespace simplecpp { if (!calledMacro.functionLike()) { TokenList temp(files); calledMacro.expand(temp, loc, tok, macros, expandedmacros, outputList); + if (temp.empty()) + skipTrailingName(output); return recursiveExpandToken(output, temp, loc, tok, macros, expandedmacros2, parametertokens, outputList); } - if (!sameline(tok, tok->next)) { - output.push_back(newMacroToken(tok->str(), loc, true, tok)); - return tok->next; - } - TokenList tokens(files); - tokens.push_back(new Token(*tok)); - const Token * tok2 = nullptr; - if (tok->next->op == '(') { - tok2 = appendTokens(tokens, loc, tok->next, macros, expandedmacros, parametertokens, outputList); - } - else if (expandArg(tokens, tok->next, loc, macros, expandedmacros, parametertokens, outputList)) { - tokens.front()->location = loc; - if (tokens.cfront()->next && tokens.cfront()->next->op == '(') - tok2 = tok->next; - } - if (!tok2) { - output.push_back(newMacroToken(tok->str(), loc, true, tok)); - return tok->next; - } + // the call's arguments follow in the replacement list TokenList temp(files); - calledMacro.expand(temp, loc, tokens.cfront(), macros, expandedmacros, outputList); - return recursiveExpandToken(output, temp, loc, tok2, macros, expandedmacros, parametertokens, outputList); + temp.push_back(newMacroToken(tok->str(), loc, true, tok)); + return recursiveExpandToken(output, temp, loc, tok, macros, expandedmacros, parametertokens, outputList); } if (tok->str() == DEFINED) { @@ -2353,17 +2488,29 @@ namespace simplecpp { return true; } - bool expandArg(TokenList &output, const Token *tok, const Location &loc, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens, OutputList *outputList) const { + /** Append the fully macro-expanded argument for parameter @p tok to @p output. + * @p hasMacroName, when given, is set when the argument held a macro name + * (otherwise the output cannot hold one); the caller then passes the output + * to rescanArg, which clears the marks that the pre-expansion left on it. + * Returns false when @p tok is not a parameter. */ + bool expandArg(TokenList &output, const Token *tok, const Location &loc, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens, OutputList *outputList, bool *hasMacroName = nullptr) const { if (!tok->name) return false; const unsigned int argnr = getArgNum(tok->str()); if (argnr >= args.size()) return false; + if (hasMacroName) + *hasMacroName = false; if (variadic && argnr + 1U >= parametertokens.size()) // empty variadic parameter return true; const Token * const argEnd = parametertokens[argnr + 1U]; + // NOLINTNEXTLINE(misc-const-correctness) - technically correct but used to access non-const data + Token * const outputEnd = output.back(); + bool macroName = false; for (const Token *partok = parametertokens[argnr]->next; partok != argEnd;) { const MacroMap::const_iterator it = macros.find(partok->str()); + if (it != macros.end()) + macroName = true; if (it != macros.end() && !partok->isExpandedFrom(&it->second) && (partok->str() == name() || expandedmacros.find(partok->str()) == expandedmacros.end())) { std::set expandedmacros2(expandedmacros); // temporary amnesia to allow reexpansion of currently expanding macros during argument evaluation expandedmacros2.erase(name()); @@ -2374,18 +2521,8 @@ namespace simplecpp { // macro whose arguments are supplied by the remaining argument tokens std::set expandedmacros3(expandedmacros); expandedmacros3.erase(name()); - partok = expandTrailingCalls(output, temp, loc, partok->previous, macros, expandedmacros3, outputList, [&](TokenList &temp2, const Token *lpar) -> const Token * { - unsigned int par = 0; - for (const Token *tok2 = lpar; tok2 != argEnd; tok2 = tok2->next) { - temp2.push_back(new Token(*tok2)); - if (tok2->op == '(') { - ++par; - } else if (tok2->op == ')') { - if (--par == 0U) - return tok2; - } - } - return nullptr; + partok = expandTrailingCalls(output, temp, loc, partok->previous, macros, expandedmacros3, outputList, [&](TokenList &call, const Token *nameTok, const Token *tok2) { + return gatherCall(call, nameTok, tok2, argEnd); }); } else { output.takeTokens(temp); @@ -2398,6 +2535,16 @@ namespace simplecpp { } if (tok->whitespaceahead && output.back()) output.back()->whitespaceahead = true; + if (macroName) { + if (hasMacroName) { + *hasMacroName = true; + } else { + // the substituted tokens are rescanned again, so the marks that the + // expansions above left on them do not apply + for (Token *tok2 = outputEnd ? outputEnd->next : output.front(); tok2; tok2 = tok2->next) + tok2->skippedByRescan = false; + } + } return true; } diff --git a/simplecpp.h b/simplecpp.h index 86573fa6..88bf48fa 100644 --- a/simplecpp.h +++ b/simplecpp.h @@ -165,7 +165,7 @@ namespace simplecpp { } Token(const Token &tok) : - macro(tok.macro), op(tok.op), comment(tok.comment), name(tok.name), number(tok.number), whitespaceahead(tok.whitespaceahead), location(tok.location), string(tok.string), mExpandedFrom(tok.mExpandedFrom) {} + macro(tok.macro), op(tok.op), comment(tok.comment), name(tok.name), number(tok.number), whitespaceahead(tok.whitespaceahead), skippedByRescan(tok.skippedByRescan), location(tok.location), string(tok.string), mExpandedFrom(tok.mExpandedFrom) {} Token &operator=(const Token &tok) = delete; @@ -191,6 +191,11 @@ namespace simplecpp { bool name; bool number; bool whitespaceahead; + /** The macro rescan has examined this token and passed it: a function-like + * macro name that was not followed by '(' at that point. A '(' that comes + * later does not make it a macro call, until the token is substituted into + * a replacement list and rescanned again. */ + bool skippedByRescan{}; Location location; Token *previous{}; Token *next{}; diff --git a/test.cpp b/test.cpp index 2561e3bf..2f26d77c 100644 --- a/test.cpp +++ b/test.cpp @@ -1041,6 +1041,146 @@ static void define27() ASSERT_EQUALS("", preprocess(code4)); } +static void define28() +{ + // a function-like macro name and its '(' are separated by a call that + // expands to nothing: the call is found when the substituted argument is + // rescanned, but not when the tokens are rescanned only once + const char code1[] = "#define EMPTY()\n" + "#define EVAL(...) __VA_ARGS__\n" + "#define HELLO() \"Hello World\"\n" + "EVAL ( HELLO EMPTY() () )\n" + "HELLO EMPTY() ()\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "\"Hello World\"\n" + "HELLO ( )", preprocess(code1)); + + // the macro that is being expanded is not expanded by that rescan, and the + // name stays unexpandable afterwards + const char code2[] = "#define EMPTY()\n" + "#define EVAL(...) __VA_ARGS__\n" + "#define ID(x) x\n" + "EVAL(EVAL EMPTY() (1)) ID(EVAL(EVAL EMPTY() (2)))\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "EVAL ( 1 ) EVAL ( 2 )", preprocess(code2)); + + // calls found by the rescan are nested and their results are rescanned + // against the tokens that follow, which can be a parameter + const char code3[] = "#define EMPTY()\n" + "#define EVAL(...) __VA_ARGS__\n" + "#define ID(x) x\n" + "#define HELLO() \"Hello World\"\n" + "#define G1() G2\n" + "#define G2() \"g2\"\n" + "#define F(x) G1 () x\n" + "EVAL(ID EMPTY() (HELLO EMPTY() ())) EVAL(G1 EMPTY() () ()) F(())\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\"Hello World\" \"g2\" \"g2\"", preprocess(code3)); + + // parameters are substituted before the rescan: an empty parameter does + // not separate a macro name from its '(', and a parameter may hold more + // than the arguments of the call + const char code4[] = "#define EMPTY()\n" + "#define HELLO() \"Hello World\"\n" + "#define F(x) HELLO x ()\n" + "#define G(x, y) x y\n" + "#define H(x) EMPTY x\n" + "F() F(EMPTY()) F(1) G(HELLO, ()) H(() 1)\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "\n" + "\n" + "\"Hello World\" \"Hello World\" HELLO 1 ( ) \"Hello World\" 1", preprocess(code4)); + + // a macro name that the rescan has passed is not a call: DEFER and OBSTRUCT + // delay the expansion by one and two rescans + const char code5[] = "#define EMPTY()\n" + "#define DEFER(id) id EMPTY()\n" + "#define OBSTRUCT(...) __VA_ARGS__ DEFER(EMPTY)()\n" + "#define EXPAND(...) __VA_ARGS__\n" + "#define A() 123\n" + "DEFER(A)() EXPAND(DEFER(A)())\n" + "OBSTRUCT(A)() EXPAND(OBSTRUCT(A)()) EXPAND(EXPAND(OBSTRUCT(A)()))\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "\n" + "\n" + "A ( ) 123\n" + "A EMPTY ( ) ( ) A ( ) 123", preprocess(code5)); + + // recursion through deferred expansion + const char code6[] = "#define EMPTY()\n" + "#define DEFER(id) id EMPTY()\n" + "#define OBSTRUCT(...) __VA_ARGS__ DEFER(EMPTY)()\n" + "#define EXPAND(...) __VA_ARGS__\n" + "#define EVAL(...) EVAL1(EVAL1(EVAL1(__VA_ARGS__)))\n" + "#define EVAL1(...) EVAL2(EVAL2(EVAL2(__VA_ARGS__)))\n" + "#define EVAL2(...) EVAL3(EVAL3(EVAL3(__VA_ARGS__)))\n" + "#define EVAL3(...) __VA_ARGS__\n" + "#define PRIMITIVE_CAT(a, ...) a ## __VA_ARGS__\n" + "#define DEC(x) PRIMITIVE_CAT(DEC_, x)\n" + "#define DEC_0 0\n" + "#define DEC_1 0\n" + "#define DEC_2 1\n" + "#define DEC_3 2\n" + "#define CHECK_N(x, n, ...) n\n" + "#define CHECK(...) CHECK_N(__VA_ARGS__, 0,)\n" + "#define PROBE(x) x, 1,\n" + "#define NOT(x) CHECK(PRIMITIVE_CAT(NOT_, x))\n" + "#define NOT_0 PROBE(~)\n" + "#define COMPL(b) PRIMITIVE_CAT(COMPL_, b)\n" + "#define COMPL_0 1\n" + "#define COMPL_1 0\n" + "#define BOOL(x) COMPL(NOT(x))\n" + "#define IIF(c) PRIMITIVE_CAT(IIF_, c)\n" + "#define IIF_0(t, ...) __VA_ARGS__\n" + "#define IIF_1(t, ...) t\n" + "#define IF(c) IIF(BOOL(c))\n" + "#define EAT(...)\n" + "#define WHEN(c) IF(c)(EXPAND, EAT)\n" + "#define REPEAT(count, macro, ...) WHEN(count)(OBSTRUCT(REPEAT_INDIRECT)()(DEC(count), macro, __VA_ARGS__) OBSTRUCT(macro)(DEC(count), __VA_ARGS__))\n" + "#define REPEAT_INDIRECT() REPEAT\n" + "#define M(i, _) i\n" + "REPEAT(2, M, ~)\n" + "EVAL(REPEAT(3, M, ~))\n"; + ASSERT_EQUALS(std::string(32, '\n') + + "REPEAT_INDIRECT ( ) ( 1 , M , ~ ) M ( 1 , ~ )\n" + "0 1 2", preprocess(code6)); + + // the parentheses of a call are counted after the parameters are + // substituted, so its '(' and its ')' can each come from a parameter. The + // tokens that a parameter holds after the ')' are rescanned with the + // tokens that follow. + const char code7[] = "#define LP (\n" + "#define RP )\n" + "#define F(x) [x]\n" + "#define G(x) \n" + "#define A(f, x) f x 1)\n" + "#define B(x) F(1 x 2)\n" + "#define C(x) F(1 x (2))\n" + "A(F, (LP)) B(RP) B(RP LP) C(RP G)\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "[ ( ) 1 ] [ 1 ] 2 ) [ 1 ] ( 2 ) [ 1 ] < 2 > )", preprocess(code7)); +} + static void define_invalid_1() { @@ -4840,6 +4980,7 @@ static void runTests(int argc, char **argv, Input input) TEST_CASE(define25); TEST_CASE(define26); TEST_CASE(define27); + TEST_CASE(define28); TEST_CASE(define_invalid_1); TEST_CASE(define_invalid_2); TEST_CASE(define_invalid_3);