Skip to content

Commit f539f08

Browse files
committed
compiler: detect excessively-deep specialization
1 parent 84e0a7d commit f539f08

2 files changed

Lines changed: 36 additions & 0 deletions

File tree

packages/compiler/src/Compiler.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

packages/compiler/test/test-wasm.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1939,3 +1939,22 @@ test('chunkedBindings: false', async t => {
19391939
wasmGrammar.match('hello;').use(r => t.true(r.succeeded()));
19401940
}
19411941
});
1942+
1943+
// When parameters grow at each recursive step — e.g., grow<(e | "x")> where
1944+
// e keeps expanding — each specialization produces a new unique name, so the
1945+
// placeholder cycle detection never fires. The specializer should detect this
1946+
// and throw a clear error rather than blowing the stack / running out of memory.
1947+
test('parameterized rules: growing parameters should not blow the stack', t => {
1948+
t.throws(
1949+
() => {
1950+
const compiler = new Compiler(ohm.grammar(`
1951+
G {
1952+
start = grow<"a">
1953+
grow<e> = e | grow<(e | "x")>
1954+
}
1955+
`));
1956+
compiler.compile();
1957+
},
1958+
{message: /Too many specializations/}
1959+
);
1960+
});

0 commit comments

Comments
 (0)