Skip to content

Commit 4177b05

Browse files
authored
compiler: detect excessively-deep specialization (#599)
1 parent 84e0a7d commit 4177b05

2 files changed

Lines changed: 42 additions & 0 deletions

File tree

packages/compiler/src/Compiler.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,13 @@ export class Compiler {
13281328
const patternsByRule = new Map<string, Map<string, Expr[]>>();
13291329
const refCounts = new Map();
13301330

1331+
// Track the recursive specialization depth per base rule name.
1332+
// If specializing a rule leads to specializing the *same* rule again
1333+
// beyond this depth, the parameters are expanding without bound
1334+
// (e.g. `grow<e> = e | grow<(e | "x")>`).
1335+
const MAX_SPECIALIZATION_DEPTH = 32;
1336+
const specializationDepth = new Map<string, number>();
1337+
13311338
const specialize = (exp: Expr): Expr =>
13321339
ir.rewrite(exp, {
13331340
Apply: app => {
@@ -1339,6 +1346,17 @@ export class Compiler {
13391346
// If not yet seen, recursively visit the body of the specialized
13401347
// rule. Note that this also applies to non-parameterized rules!
13411348
if (!newRules.has(specializedName)) {
1349+
const prevDepth = specializationDepth.get(ruleName) ?? 0;
1350+
if (children.length > 0) {
1351+
if (prevDepth >= MAX_SPECIALIZATION_DEPTH) {
1352+
throw new Error(
1353+
`Excessively deep specialization of rule '${ruleName}' (>${MAX_SPECIALIZATION_DEPTH} levels). ` +
1354+
'This usually means its parameters grow on each recursive call, ' +
1355+
'producing an infinite number of specialized rules.'
1356+
);
1357+
}
1358+
specializationDepth.set(ruleName, prevDepth + 1);
1359+
}
13421360
newRules.set(specializedName, {} as RuleInfo); // Prevent infinite recursion.
13431361

13441362
// Visit the body with the parameter substituted, to ensure we
@@ -1347,6 +1365,9 @@ export class Compiler {
13471365
ir.substituteParams(ruleInfo.body, children as Exclude<Expr, ir.Param>[])
13481366
);
13491367

1368+
// Restore the depth after the recursive visit.
1369+
specializationDepth.set(ruleName, prevDepth);
1370+
13501371
// If there are any args, replace the body with an application of
13511372
// the generalized rule.
13521373
if (children.length > 0) {

packages/compiler/test/test-wasm.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1939,3 +1939,24 @@ 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(
1951+
ohm.grammar(`
1952+
G {
1953+
start = grow<"a">
1954+
grow<e> = e | grow<(e | "x")>
1955+
}
1956+
`)
1957+
);
1958+
compiler.compile();
1959+
},
1960+
{message: /Excessively deep specialization/}
1961+
);
1962+
});

0 commit comments

Comments
 (0)