@@ -1328,6 +1328,12 @@ export class Compiler {
13281328 const patternsByRule = new Map < string , Map < string , Expr [ ] > > ( ) ;
13291329 const refCounts = new Map ( ) ;
13301330
1331+ // Track how many unique specializations we've created per base rule.
1332+ // If this grows beyond a reasonable limit, the rule's parameters are
1333+ // expanding without bound (e.g. `grow<e> = e | grow<(e | "x")>`).
1334+ const MAX_SPECIALIZATIONS_PER_RULE = 32 ;
1335+ const specializationCounts = new Map < string , number > ( ) ;
1336+
13311337 const specialize = ( exp : Expr ) : Expr =>
13321338 ir . rewrite ( exp , {
13331339 Apply : app => {
@@ -1339,6 +1345,17 @@ export class Compiler {
13391345 // If not yet seen, recursively visit the body of the specialized
13401346 // rule. Note that this also applies to non-parameterized rules!
13411347 if ( ! newRules . has ( specializedName ) ) {
1348+ if ( children . length > 0 ) {
1349+ const count = ( specializationCounts . get ( ruleName ) || 0 ) + 1 ;
1350+ specializationCounts . set ( ruleName , count ) ;
1351+ if ( count > MAX_SPECIALIZATIONS_PER_RULE ) {
1352+ throw new Error (
1353+ `Too many specializations of rule '${ ruleName } ' (>${ MAX_SPECIALIZATIONS_PER_RULE } ). ` +
1354+ 'This usually means its parameters grow on each recursive call, ' +
1355+ 'producing an infinite number of specialized rules.'
1356+ ) ;
1357+ }
1358+ }
13421359 newRules . set ( specializedName , { } as RuleInfo ) ; // Prevent infinite recursion.
13431360
13441361 // Visit the body with the parameter substituted, to ensure we
0 commit comments