Cap recursion depth when expanding macros recursively#22730
Conversation
The "Expand macro recursively" walk in expand_macro re-expands every nested macro-call descendant with no depth guard, so a deep enough expansion overflows the native stack and aborts the process. Thread a depth counter through expand/expand_macro_recur and stop at 128 (the default recursion_limit), returning a truncated expansion with a note instead of recursing without bound. Fixes the crash half of rust-lang#21824 (the "Expand macro recursively" abort on the higher-kinded-types ForLt! call site). The separate name-resolution hang in the same report is not addressed here. Investigation and drafting were assisted by AI tooling; I reviewed, understand, and own the change.
|
| // truncates with a note instead of overflowing the stack. | ||
| #[test] | ||
| fn macro_expand_recursion_limit_is_bounded() { | ||
| let depth = (super::MACRO_EXPANSION_RECURSION_LIMIT + 12) as usize; |
There was a problem hiding this comment.
You don't need more than a macro that calls itself with no base condition.
|
I'm interested to hear details about the hang issue. If it is indeed inside name resolution, I find it unlikely that Salsa is involved here, since def map is one query. |
|
I didn't attach a debugger to the hang, so the |
Fixes #21824 (the crash half).
Opening a file that invokes
higher-kinded-types'hkt::ForLt!macro with an elided lifetime, then running "Expand macro recursively" on that call site, can abort rust-analyzer with a native stack overflow (thread 'WorkerN' has overflowed its stack). Reproduced locally against the exact repro in the issue.Root cause:
expand/expand_macro_recurincrates/ide/src/expand_macro.rs(the code behind "Expand macro recursively") re-expand every nested macro-call descendant of the already-expanded AST with no recursion-depth guard at all. It is a plausible design for ordinary nestedmacro_rules!expansions, which bottom out in a few levels, but a tt-muncher macro likeForLt!can expand into a chain deep enough to overflow the native call stack before the AST stops containing macro calls.The fix threads a
depth: u32counter through theexpand/expand_macro_recurmutual recursion and stops at 128, which is rustc's own defaultrecursion_limit. That means a macro the compiler itself accepts is never truncated, only expansions the compiler would already reject on recursion grounds get a truncated result with a note instead of a crash. Added a deterministic regression test (macro_expand_recursion_limit_is_bounded) using a localmacro_rules!fixture nested well past the limit, so the guard is covered without depending on the external crate or on Salsa scheduling. Fullcargo test -p ide --lib expand_macrosuite passes (27/27), including all pre-existing nesting tests unmodified.One thing worth flagging while I have your attention: digging into this issue, I found it is actually two distinct bugs sharing the same repro, not one. There is this crash (unbounded recursion in the display walk above), and separately a name-resolution/Salsa deadlock where
collect_defs's prelude seeding calls back into the same tracked query it is currently running inside of, which parks a worker thread forever instead of returning. That hang is a different subsystem (hir-def's collector, notide::expand_macro) and needs someone who knows the Salsa scheduler better than a first pass gives; this PR only fixes the crash. Happy to comment on the issue with the hang details separately if useful, or take a swing at it in a follow-up.Also open to a small refinement if you would prefer it: 128 matches the default
recursion_limit, but a crate that raises its ownrecursion_limitcould still hit the truncation on an expansion it legitimately allows. I can switch the guard to read the crate's actualrecursion_limit(viaDefMap) instead of the hardcoded constant, if you would rather have that than a fixed default. Left it as a constant for this first pass since it is simpler and covers the reported case exactly.Small disclosure per the AI policy: I used AI tooling to help investigate the crash and draft the patch and this description. I reviewed the change, understand why it works, and I am the one submitting and standing behind it.