fluffos/docs/lpc/preprocessor/define.md
Yucong Sun f3e5bfa799 compiler: eliminate C-stack recursion from macro expansion and lexer; raise nesting cap to 65535
The macro-expansion rescan path recursed one yylex() frame per nesting
level, the textual argument pre-expander (lpc_lex_expand_string) recursed
per level with per-level guard-vector copies, the #if/#elif evaluator
recursed per unary/paren/ternary token, and the lexer's no-token recovery
paths (malformed heredocs, over-long $N, template-interpolation close)
retried via recursive yylex() -- error REPORTING stops after 5 parse
errors but scanning does not, so runs of malformed constructs nested a
frame each (a 600KB file of '@' lines segfaulted the driver).

All of these are now iterative:
- lpc_lex_resolve_identifier returns LPC_TOKEN_RESCAN (no token); the
  identifier rule falls through and the SAME yylex() frame keeps
  scanning the pushed expansion buffer -- one Flex buffer per level,
  zero C-stack growth.
- lpc_lex_expand_string is an explicit work-stack machine (one shared
  guard stack + O(1) name-count lookups instead of per-level copies).
- the #if evaluator is an explicit-stack machine (ifexpr_eval), depth
  bounded by token count on the heap; no cap needed. Keeps the audit's
  int64_t retyping (LLP64 correctness).
- heredoc recovery / @@ splice, over-long $N, and template '}' close
  fall through instead of recursing (pinned by lexer_retry_chains.lpc,
  which segfaults the previous binary).

This supersedes the 2026-07-20 audit's stack-overflow mitigations for
the preprocessor (kMaxIfExprDepth, MAX_EXPANSION_NESTING lowered to 32
under the sanitizer build's measured crash boundary): the recursion
itself is gone, so MAX_EXPANSION_NESTING becomes kLpcMaxExpansionNesting
= 65535, shared by both expansion engines as a runaway/memory bound and
counted in LIVE frames only via a live-index stack (dead same-line
provenance frames no longer trip it, so 128+ sequential uses of a macro
on one line compile again). deep_nesting_caps.lpc's deep cases now
compile -- under ASan included -- instead of being rejected; its
comments and AGENTS.md's cap-sizing guidance are updated to the new
design. The self-reference guard is an O(1) hash lookup.
innermost_real_buffer_index -- behind every current_line read, per
matched token -- is O(1) via a maintained include-buffer index stack
instead of walking the whole buffer stack (the walk made deep-chain
compiles quadratic).

Diagnostics keep 16 innermost + 16 outermost expansion notes with an
elision marker instead of one note per level.

Tests: deep_macro_nesting.lpc (60000-deep chains through both engines,
same-line frame accounting, beyond-cap clean error, 262144-token #if
shapes), lexer_retry_chains.lpc, deep_ternary_nesting.lpc (parser-stack
bound pins). Validated post-rebase on Debug and clang ASan+UBSan full
suites (621/621 files each) plus RelWithDebInfo pre-rebase; gtests
320/320.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 01:39:28 -07:00

3 KiB

title
preprocessor / define

#define and #undef

#define creates a macro: a name that is replaced by a body of text wherever it appears later in the file.

Object-like macros

#define MAX_HP 100
#define GREETING "Welcome to " MUD_NAME

Every later occurrence of the name (as a whole identifier — not inside strings, comments, or longer identifiers) is replaced by the body.

Function-like macros

A parameter list attached directly to the name (no space before () makes the macro function-like:

#define SQUARE(x) ((x) * (x))
#define MSG(who, text) tell_object(who, text "\n")

Arguments are collected with full nesting awareness — commas inside parentheses, strings, or character literals do not split arguments. Parenthesize parameters in the body (as above) to avoid precedence surprises at the use site.

Inside a macro body, #param produces the argument's text as a string literal, and a ## b pastes two tokens together. ## may not appear at the very start or end of a body.

Expansion and rescan

A macro's body is rescanned after substitution, so macros may reference other macros freely. A macro that (directly or indirectly) references itself stops expanding at the self-reference instead of recursing forever, as in C. Expansion nesting (a chain of distinct macros each referencing the next) is iterative inside the compiler and bounded at 65535 levels; exceeding that is a compile error ("Macro expansion nested too deep").

Because #if expressions are evaluated over the same token stream, arithmetic keeps C precedence across macro boundaries:

#define X 1+1
#if X*2 == 3   // 1+1*2 -- true

Multi-line macros

End a line with \ to continue the definition:

#define LONG_MACRO(x) do { \
    write(x);              \
} while (0)

A block comment opened on a directive line may also span lines — it reads as whitespace and does not end the directive, so both of these define WARNING_LEVEL as 1:

#define WARNING_LEVEL 1 /* change to a higher value to
                           show more warnings */

#define WARNING_LEVEL 1 // single-line form

Text after the comment's close on its final line still belongs to the directive, matching C.

Comments on a directive line are whitespace, never part of the macro: a trailing // or /* */ after the body (or after the name on #undef/#ifdef) is stripped before the directive is parsed, and a block comment inside the body separates tokens like a space would:

#define CREDITS "credits" // key into the economy mapping

Redefinition

Redefining a macro with a different body is allowed: the compiler emits a warning (with a note pointing at the previous definition) and the new definition takes effect. Redefining with an identical body is silent. Redefining or #undef-ing a predefined macro (__FILE__, FLUFFOS, ...) is a compile error.

#undef

#undef MAX_HP

Removes a user macro definition; the name is ordinary text again. #undef of an unknown name is harmless.