fluffos/testsuite/single/tests/compiler/preprocessor.lpc
Yucong Sun d38dc2c833
lexer: strip comments from directive payloads before parsing (#1240) (#1241)
A '//' comment after a #define body was captured INTO the stored macro
body. Expansion buffers carry no newline to end it, so when the macro
expanded inside a spliced line (a function-like macro's substituted
body, as in the report's MIN(credits, m[e][CREDITS])), the '//' ate the
rest of the splice and the parse failed with a baffling 'unexpected ;'
attributed to the outer macro. The same missed strip made
'#undef X // why' erase nothing and '#ifdef X // why' look up the
wrong name and silently take the false branch.

Comments are whitespace (C translation phase 3). dispatch_directive now
strips them from the payload before parsing for #define (name, params,
body -- body also right-trimmed so a stripped comment can't turn '1'
vs '1 ' into a spurious redefinition warning), #undef, #ifdef, #ifndef,
and #pragma (word-list payload); #if/#elif already stripped.
strip_directive_comments() now folds a block comment to ONE space
instead of nothing, so '1 -/*c*/-1' keeps its token boundaries instead
of pasting '--'. #error/#warn/#echo payloads stay raw.

Covered by five new Preprocessor unit tests (the report's repro shape,
paste-prevention via #if branch selection, param-list comment,
testsuite/single/tests/compiler/preprocessor.lpc; documented in
docs/lpc/preprocessor/define.md.

Fixes #1240


Claude-Session: https://claude.ai/code/session_016FMBJLkpkpVpZdz6PWzeWe

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-11 12:15:19 -04:00

143 lines
4 KiB
Text

// LPC-level pins for the preprocessor (the unit tests in
// src/tests/test_compiler.cc cover these paths in isolation; this file
// proves them end-to-end through a real object compile).
// Reference: docs/lpc/preprocessor/.
#define STR(x) #x
#define GLUE(a, b) a##b
#define TWICE(x) ((x) + (x))
#define PAIR(a, b) ({ a, b })
#define SELF SELF
#define ML(a) ((a) \
* 3)
// Redefining with a different body is a WARNING (non-fatal); the new
// definition takes effect.
#define REDEF 1
#define REDEF 2
#warn preprocessor.lpc exercising #warn -- expected, not a failure
#if defined(FLUFFOS) && defined(__VERSION__)
#define COND_OK 1
#else
#define COND_OK 0
#endif
#if !defined(SURELY_NOT_DEFINED_ANYWHERE)
#define NOTDEF_OK 1
#else
#define NOTDEF_OK 0
#endif
// Token-based #if arithmetic keeps C precedence across macro bodies.
#define ONE_PLUS_ONE 1+1
#if ONE_PLUS_ONE * 2 == 3
#define PRECEDENCE_OK 1
#else
#define PRECEDENCE_OK 0
#endif
// #1236: a block comment opened on a directive line may close on a
// LATER line -- the comment reads as whitespace, it neither ends the
// directive nor leaves its continuation lines to be compiled as code.
#define WARNING_LEVEL 1 /* Change this to higher values to
show more warnings. */
// Text after the close on the final line still belongs to the directive.
#define CMT_TAIL 10 /* spans
one line */ + 5
// On a live #if...
#if 1 /* comment on a live #if,
closed here */
#define CMT_IF_OK 1
#else
#define CMT_IF_OK 0
#endif
// ...and on a dead one: the branch below the comment is still skipped.
#if 0 /* opened on the #if line,
closed here */
this is not code and must never be compiled
#endif
// A string literal on the directive line must not open a comment.
#define CMT_STR "a/*b"
// #1240: comments are whitespace in a directive's payload -- a '//'
// tail must NOT be captured into the body (it would comment out the
// rest of whatever spliced line the macro expands into), and trailing
// comments on #ifdef/#undef must not corrupt the name.
#define CREDITS_KEY "credits" // Current number of credits
#define MIN2(x, y) ((x) < (y) ? (x) : (y))
// A body-internal block comment is ONE space: the two '-' below must
// not paste into '--'.
#define DOUBLE_NEG 1 -/*minus*/-1
#ifdef CREDITS_KEY // trailing comment here too
#define IFDEF_CMT_OK 1
#else
#define IFDEF_CMT_OK 0
#endif
#define UNDEF_ME 5
#undef UNDEF_ME // gone
void do_tests() {
// ## token paste builds an identifier.
int GLUE(fo, o) = 42;
ASSERT_EQ(42, foo);
// # stringizes the raw argument spelling.
ASSERT_EQ("hello", STR(hello));
ASSERT_EQ("1+2", STR(1+2));
// Function-like expansion; parenthesized body.
ASSERT_EQ(8, TWICE(4));
// Commas nested in parentheses do not split arguments.
ASSERT_EQ(({ ({ 1, 2 }), 3 }), PAIR(({ 1, 2 }), 3));
// Multi-line (backslash-continued) macro.
ASSERT_EQ(21, ML(7));
// Self-referencing macro stops expanding at the self-reference, so
// SELF is usable as an ordinary identifier.
int SELF = 7;
ASSERT_EQ(7, SELF);
// Redefinition took effect (and did not fail the compile).
ASSERT_EQ(2, REDEF);
// Conditionals evaluated as expected.
ASSERT_EQ(1, COND_OK);
ASSERT_EQ(1, NOTDEF_OK);
ASSERT_EQ(1, PRECEDENCE_OK);
// #1236 -- directive-line comments spanning physical lines.
ASSERT_EQ(1, WARNING_LEVEL);
ASSERT_EQ(15, CMT_TAIL);
ASSERT_EQ(1, CMT_IF_OK);
ASSERT_EQ("a/*b", CMT_STR);
// #1240 -- trailing comments on directives are whitespace.
mapping economies = ([ "e" : ([ "credits" : 100 ]) ]);
ASSERT_EQ(42, MIN2(42, economies["e"][CREDITS_KEY]));
ASSERT_EQ(2, DOUBLE_NEG);
ASSERT_EQ(1, IFDEF_CMT_OK);
int UNDEF_ME = 9;
ASSERT_EQ(9, UNDEF_ME);
// Builtin macros: __FILE__ / __DIR__ / __LINE__.
ASSERT_EQ("/single/tests/compiler/preprocessor.lpc", __FILE__);
ASSERT_EQ("/single/tests/compiler/", __DIR__);
ASSERT(__LINE__ > 60);
// #undef makes the name ordinary again.
#define GONE 5
#undef GONE
int GONE = 6;
ASSERT_EQ(6, GONE);
}