Add limits to parser recursion (#1918)

This commit is contained in:
Islon Scherer authored and GitHub committed 2026-10-27 16:57:36 +01:00
1 parent 3e703995ca
commit 434f699887
12 files changed
+435 -99

No files matched your search

@@ -299,6 +299,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
private final ExternalMemberRegistry externalMemberRegistry;
private final SymbolTable symbolTable;
private final boolean isMethodReturnTypeChecked;
private @Nullable Node lastVisited;
public AstBuilder(
Source source, VmLanguage language, ModuleInfo moduleInfo, ModuleResolver moduleResolver) {
@@ -2327,13 +2328,20 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
}
public UnresolvedTypeNode visitType(Type type) {
lastVisited = type;
return (UnresolvedTypeNode) type.accept(this);
}
public ExpressionNode visitExpr(Expr expr) {
lastVisited = expr;
return (ExpressionNode) expr.accept(this);
}
/** Returns the source section of the last type or expression that was entered. */
public @Nullable SourceSection getLastVisitedSection() {
return lastVisited == null ? null : createSourceSection(lastVisited);
}
public TypeParameter[] visitTypeParameterList(
@Nullable TypeParameterList ctx, FrameSlotVariable @Nullable [] typeBindings) {
if (ctx == null) return new TypeParameter[0];
@@ -23,6 +23,7 @@ import com.oracle.truffle.api.instrumentation.ProvidedTags;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.Source;
import org.jspecify.annotations.Nullable;
import org.pkl.core.ast.PklRootNode;
import org.pkl.core.ast.builder.AstBuilder;
import org.pkl.core.module.ModuleKey;
import org.pkl.core.module.ResolvedModuleKey;
@@ -107,10 +108,21 @@ public final class VmLanguage extends TruffleLanguage<VmContext> {
throw VmUtils.toVmException(e, source, moduleName);
}
PklRootNode moduleNode;
var builder =
AstBuilder.create(
source, this, moduleContext, moduleKey, resolvedModuleKey, moduleResolver);
var moduleNode = builder.visitModule(moduleContext);
try {
moduleNode = builder.visitModule(moduleContext);
} catch (StackOverflowError e) {
// the syntax tree is too deep for the AST builder
var section = builder.getLastVisitedSection();
throw new VmExceptionBuilder()
.evalError("moduleTooDeeplyNested")
.withSourceSection(section != null ? section : source.createSection(1))
.withMemberName(IoUtils.inferModuleName(moduleKey))
.build();
}
moduleNode.getCallTarget().call(emptyModule, emptyModule);
MinPklVersionChecker.check(emptyModule, importNode);
}
@@ -846,6 +846,9 @@ Microbenchmark is a constant expression.
stackOverflow=\
A stack overflow occurred.
moduleTooDeeplyNested=\
Code is nested too deeply, or contains too many chained operations.
cannotHaveCircularProjectDependenciesSingle=\
Local project dependencies cannot be circular.\n\
\n\
@@ -0,0 +1,79 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.parser
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.DynamicTest.dynamicTest
import org.junit.jupiter.api.TestFactory
import org.pkl.core.Evaluator
import org.pkl.core.ModuleSource
import org.pkl.core.PklException
/** Maliciously deep modules must fail with a [PklException] rather than a [StackOverflowError]. */
class EvaluatorDepthTest {
companion object {
private const val STACK_SIZE = 512L * 1024
private const val HUGE = 100_000
private val nesting: Map<String, (Int) -> String> =
mapOf(
"parens" to { n -> "x = " + "(".repeat(n) + "1" + ")".repeat(n) },
"if" to { n -> "x = " + "if (true) 1 else ".repeat(n) + "2" },
"objects" to { n -> "x " + "{ y ".repeat(n) + "{}" + " }".repeat(n) },
)
private val chains: Map<String, (Int) -> String> =
mapOf(
"binary operator" to { n -> "x = " + "1 + ".repeat(n) + "1" },
"qualified access" to { n -> "x = new Listing {}" + ".toList()".repeat(n) },
"subscript" to { n -> "x = a" + "[1]".repeat(n) },
"non-null" to { n -> "x = a" + "!!".repeat(n) },
"constrained type" to { n -> "x: Int" + "(isPositive)".repeat(n) + " = 1" },
)
private fun runOnSmallStack(action: () -> Unit): Throwable? {
var thrown: Throwable? = null
val thread =
Thread(null, { thrown = runCatching(action).exceptionOrNull() }, "test", STACK_SIZE)
thread.start()
thread.join()
return thrown
}
private fun evaluate(source: String) {
Evaluator.preconfigured().use { it.evaluate(ModuleSource.text(source)) }
}
}
@TestFactory
fun `deep nesting fails with a PklException`(): List<DynamicTest> = nesting.map { (shape, gen) ->
dynamicTest(shape) {
val thrown = runOnSmallStack { evaluate(gen(HUGE)) }
assertThat(thrown).isInstanceOf(PklException::class.java)
assertThat(thrown).hasMessageContaining("nested too deeply")
}
}
@TestFactory
fun `long chains do not overflow the stack`(): List<DynamicTest> = chains.map { (shape, gen) ->
dynamicTest(shape) {
val thrown = runOnSmallStack { evaluate(gen(HUGE)) }
assertThat(thrown).isInstanceOf(PklException::class.java)
assertThat(thrown).hasMessageContaining("nested too deeply")
}
}
}
@@ -21,7 +21,9 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.pkl.parser.GenericParser;
import org.pkl.parser.GenericParserError;
import org.pkl.parser.syntax.generic.Node;
import org.pkl.parser.util.ErrorMessages;
/**
* A formatter for Pkl files that applies canonical formatting rules.
@@ -144,8 +146,15 @@ public class Formatter {
}
private void format(String input, Appendable output) {
var ast = new GenericParser().parseModule(input);
generate(new Builder(input, grammarVersion).format(ast), output);
format(input, new GenericParser().parseModule(input), output);
}
void format(String input, Node ast, Appendable output) {
try {
generate(new Builder(input, grammarVersion).format(ast), output);
} catch (StackOverflowError e) {
throw new GenericParserError(ErrorMessages.create("tooDeeplyNested"), ast.span);
}
}
private static void generate(FormatNode formatAst, Appendable output) {
@@ -0,0 +1,75 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.formatter
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.DynamicTest.dynamicTest
import org.junit.jupiter.api.TestFactory
import org.pkl.parser.GenericParser
import org.pkl.parser.GenericParserError
/** Maliciously deep input must not make the formatter throw a [StackOverflowError]. */
class FormatterDepthTest {
companion object {
private const val STACK_SIZE = 512L * 1024
private const val LARGE_STACK_SIZE = 512L * 1024 * 1024
private const val NESTING_DEPTH = 10_000
private const val CHAIN_LENGTH = 100_000
private val nesting: Map<String, (Int) -> String> =
mapOf(
"parens" to { n -> "x = " + "(".repeat(n) + "1" + ")".repeat(n) },
"types" to { n -> "x: " + "List<".repeat(n) + "Int" + ">".repeat(n) },
"objects" to { n -> "x " + "{ y ".repeat(n) + "{}" + " }".repeat(n) },
)
private val chains: Map<String, (Int) -> String> =
mapOf(
"binary operator" to { n -> "x = " + "1 + ".repeat(n) + "1" },
"qualified access" to { n -> "x = a" + ".b".repeat(n) },
"subscript" to { n -> "x = a" + "[1]".repeat(n) },
"constrained type" to { n -> "x: Int" + "(isPositive)".repeat(n) },
)
private fun <T> runOn(stackSize: Long, action: () -> T): Result<T> {
var result: Result<T>? = null
val thread = Thread(null, { result = runCatching(action) }, "test", stackSize)
thread.start()
thread.join()
return result!!
}
}
@TestFactory
fun `deep nesting fails with a formatter error`(): List<DynamicTest> =
nesting.map { (shape, gen) ->
dynamicTest(shape) { assertFormatFails(gen(NESTING_DEPTH)) }
}
@TestFactory
fun `long chains fail with a formatter error`(): List<DynamicTest> = chains.map { (shape, gen) ->
dynamicTest(shape) { assertFormatFails(gen(CHAIN_LENGTH)) }
}
private fun assertFormatFails(source: String) {
val ast = runOn(LARGE_STACK_SIZE) { GenericParser().parseModule(source) }.getOrThrow()
val thrown =
runOn(STACK_SIZE) { Formatter().format(source, ast, StringBuilder()) }.exceptionOrNull()
assertThat(thrown).isInstanceOf(GenericParserError::class.java)
assertThat(thrown).hasMessageContaining("nested too deeply")
}
}
@@ -19,7 +19,12 @@ import org.pkl.parser.syntax.generic.Node;
public final class GenericParser {
public Node parseModule(String source) {
return new GenericParserImpl(source).parseModule();
var parser = new GenericParserImpl(source);
try {
return parser.parseModule();
} catch (StackOverflowError e) {
throw parser.tooDeeplyNested();
}
}
public Node parseExpressionInput(String source) {
@@ -944,7 +944,7 @@ class GenericParserImpl {
@SuppressWarnings("DuplicatedCode")
private Node parseExprRest(Node expr) {
// amends
if (lookahead() == Token.LBRACE) {
while (lookahead() == Token.LBRACE) {
var children = new ArrayList<Node>();
children.add(expr);
ff(children);
@@ -952,7 +952,8 @@ class GenericParserImpl {
|| expr.type == NodeType.AMENDS_EXPR
|| expr.type == NodeType.NEW_EXPR) {
children.add(parseObjectBody());
return parseExprRest(new Node(NodeType.AMENDS_EXPR, children));
expr = new Node(NodeType.AMENDS_EXPR, children);
continue;
}
throw parserError("unexpectedCurlyProbablyAmendsExpression", expr.text(lexer.getSource()));
}
@@ -1251,31 +1252,33 @@ class GenericParserImpl {
}
private Node parseTypeEnd(Node type) {
var children = new ArrayList<Node>();
children.add(type);
// nullable types
if (lookahead() == Token.QUESTION) {
ff(children);
children.add(makeTerminal(next()));
var res = new Node(NodeType.NULLABLE_TYPE, children);
return parseTypeEnd(res);
while (true) {
var children = new ArrayList<Node>();
children.add(type);
// nullable types
if (lookahead() == Token.QUESTION) {
ff(children);
children.add(makeTerminal(next()));
type = new Node(NodeType.NULLABLE_TYPE, children);
continue;
}
// constrained types: have to start in the same line as the type
var fla = fullLookahead();
if (fla.tk.token == Token.LPAREN && noSemicolonInbetween() && fla.tk.newLinesBetween == 0) {
ff(children);
var constraint = new ArrayList<Node>();
constraint.add(makeTerminal(next()));
var elements = new ArrayList<Node>();
ff(elements);
parseListOf(Token.RPAREN, elements, () -> parseExpr(")"));
constraint.add(new Node(NodeType.CONSTRAINED_TYPE_ELEMENTS, elements));
expect(Token.RPAREN, constraint, "unexpectedToken2", ",", ")");
children.add(new Node(NodeType.CONSTRAINED_TYPE_CONSTRAINT, constraint));
type = new Node(NodeType.CONSTRAINED_TYPE, children);
continue;
}
return type;
}
// constrained types: have to start in the same line as the type
var fla = fullLookahead();
if (fla.tk.token == Token.LPAREN && noSemicolonInbetween() && fla.tk.newLinesBetween == 0) {
ff(children);
var constraint = new ArrayList<Node>();
constraint.add(makeTerminal(next()));
var elements = new ArrayList<Node>();
ff(elements);
parseListOf(Token.RPAREN, elements, () -> parseExpr(")"));
constraint.add(new Node(NodeType.CONSTRAINED_TYPE_ELEMENTS, elements));
expect(Token.RPAREN, constraint, "unexpectedToken2", ",", ")");
children.add(new Node(NodeType.CONSTRAINED_TYPE_CONSTRAINT, constraint));
var res = new Node(NodeType.CONSTRAINED_TYPE, children);
return parseTypeEnd(res);
}
return type;
}
private Node parseAnnotation() {
@@ -1470,6 +1473,10 @@ class GenericParserImpl {
}
}
GenericParserError tooDeeplyNested() {
return parserError("tooDeeplyNested");
}
private GenericParserError parserError(String messageKey, Object... args) {
return new GenericParserError(ErrorMessages.create(messageKey, args), spanLookahead);
}
@@ -23,14 +23,29 @@ public final class Parser {
public Parser() {}
public Module parseModule(String source) {
return new ParserImpl(source).parseModule();
var parser = new ParserImpl(source);
try {
return parser.parseModule();
} catch (StackOverflowError e) {
throw parser.tooDeeplyNested();
}
}
public Expr parseExpressionInput(String source) {
return new ParserImpl(source).parseExpressionInput();
var parser = new ParserImpl(source);
try {
return parser.parseExpressionInput();
} catch (StackOverflowError e) {
throw parser.tooDeeplyNested();
}
}
public ReplInput parseReplInput(String source) {
return new ParserImpl(source).parseReplInput();
var parser = new ParserImpl(source);
try {
return parser.parseReplInput();
} catch (StackOverflowError e) {
throw parser.tooDeeplyNested();
}
}
}
@@ -1089,58 +1089,62 @@ final class ParserImpl {
@SuppressWarnings("DuplicatedCode")
private Expr parseExprRest(Expr expr) {
// non-null
if (lookahead == Token.NON_NULL) {
var end = next().span;
var res = new NonNullExpr(expr, expr.span().endWith(end));
return parseExprRest(res);
}
// amends
if (lookahead == Token.LBRACE) {
if (expr instanceof ParenthesizedExpr
|| expr instanceof AmendsExpr
|| expr instanceof NewExpr) {
var body = parseObjectBody();
return parseExprRest(new AmendsExpr(expr, body, expr.span().endWith(body.span())));
while (true) {
// non-null
if (lookahead == Token.NON_NULL) {
var end = next().span;
expr = new NonNullExpr(expr, expr.span().endWith(end));
}
throw parserError("unexpectedCurlyProbablyAmendsExpression", expr.text(lexer.getSource()));
}
// qualified access
if (lookahead == Token.DOT || lookahead == Token.QDOT) {
var isNullable = next().token == Token.QDOT;
var identifier = parseIdentifier();
TypeArgumentList typeArgumentList = null;
if (lookahead == Token.DCOLON && !precededBySemicolon && _lookahead.newLinesBetween == 0) {
// amends
else if (lookahead == Token.LBRACE) {
if (expr instanceof ParenthesizedExpr
|| expr instanceof AmendsExpr
|| expr instanceof NewExpr) {
var body = parseObjectBody();
expr = new AmendsExpr(expr, body, expr.span().endWith(body.span()));
} else {
throw parserError(
"unexpectedCurlyProbablyAmendsExpression", expr.text(lexer.getSource()));
}
}
// qualified access
else if (lookahead == Token.DOT || lookahead == Token.QDOT) {
var isNullable = next().token == Token.QDOT;
var identifier = parseIdentifier();
TypeArgumentList typeArgumentList = null;
if (lookahead == Token.DCOLON && !precededBySemicolon && _lookahead.newLinesBetween == 0) {
next();
typeArgumentList = parseTypeArgumentList();
}
ArgumentList argumentList = null;
if (lookahead == Token.LPAREN && !precededBySemicolon && _lookahead.newLinesBetween == 0) {
argumentList = parseArgumentList();
} else if (typeArgumentList != null) {
// type args but no args
throw new ParserError(ErrorMessages.create("unexpectedToken", "("), spanLookahead);
}
var lastSpan = argumentList != null ? argumentList.span() : identifier.span();
expr =
new QualifiedAccessExpr(
expr,
identifier,
isNullable,
typeArgumentList,
argumentList,
expr.span().endWith(lastSpan));
}
// subscript (needs to be in the same line as the expression)
else if (lookahead == Token.LBRACK
&& !precededBySemicolon
&& _lookahead.newLinesBetween == 0) {
next();
typeArgumentList = parseTypeArgumentList();
var exp = parseExpr("]");
var end = expect(Token.RBRACK, "unexpectedToken", "]").span;
expr = new SubscriptExpr(expr, exp, expr.span().endWith(end));
} else {
return expr;
}
ArgumentList argumentList = null;
if (lookahead == Token.LPAREN && !precededBySemicolon && _lookahead.newLinesBetween == 0) {
argumentList = parseArgumentList();
} else if (typeArgumentList != null) {
// type args but no args
throw new ParserError(ErrorMessages.create("unexpectedToken", "("), spanLookahead);
}
var lastSpan = argumentList != null ? argumentList.span() : identifier.span();
var res =
new QualifiedAccessExpr(
expr,
identifier,
isNullable,
typeArgumentList,
argumentList,
expr.span().endWith(lastSpan));
return parseExprRest(res);
}
// subscript (needs to be in the same line as the expression)
if (lookahead == Token.LBRACK && !precededBySemicolon && _lookahead.newLinesBetween == 0) {
next();
var exp = parseExpr("]");
var end = expect(Token.RBRACK, "unexpectedToken", "]").span;
var res = new SubscriptExpr(expr, exp, expr.span().endWith(end));
return parseExprRest(res);
}
return expr;
}
private Expr parseSingleLineStringLiteralExpr() {
@@ -1493,25 +1497,28 @@ final class ParserImpl {
}
private Type parseTypeEnd(Type type) {
// nullable types
if (lookahead == Token.QUESTION) {
var end = spanLookahead;
next();
var res = new Type.NullableType(type, type.span().endWith(end));
return parseTypeEnd(res);
while (true) {
// nullable types
if (lookahead == Token.QUESTION) {
var end = spanLookahead;
next();
type = new Type.NullableType(type, type.span().endWith(end));
}
// constrained types: have to start in the same line as the type
else if (lookahead == Token.LPAREN
&& !precededBySemicolon
&& _lookahead.newLinesBetween == 0) {
next();
var constraints = parseListOf(Token.COMMA, Token.RPAREN, () -> parseExpr(")"));
var end = expect(Token.RPAREN, "unexpectedToken2", ",", ")").span;
var children = new ArrayList<Node>(constraints.size() + 1);
children.add(type);
children.addAll(constraints);
type = new Type.ConstrainedType(children, type.span().endWith(end));
} else {
return type;
}
}
// constrained types: have to start in the same line as the type
if (lookahead == Token.LPAREN && !precededBySemicolon && _lookahead.newLinesBetween == 0) {
next();
var constraints = parseListOf(Token.COMMA, Token.RPAREN, () -> parseExpr(")"));
var end = expect(Token.RPAREN, "unexpectedToken2", ",", ")").span;
var children = new ArrayList<Node>(constraints.size() + 1);
children.add(type);
children.addAll(constraints);
var res = new Type.ConstrainedType(children, type.span().endWith(end));
return parseTypeEnd(res);
}
return type;
}
private Annotation parseAnnotation() {
@@ -1829,6 +1836,10 @@ final class ParserImpl {
return res;
}
ParserError tooDeeplyNested() {
return parserError("tooDeeplyNested");
}
private ParserError parserError(String messageKey, Object... args) {
return new ParserError(ErrorMessages.create(messageKey, args), spanLookahead);
}
@@ -19,6 +19,9 @@ Unexpected token `{0}`. Expected a type.
unexpectedTokenForType2=\
Unexpected token `{0}`. Expected a type or `{1}`.
tooDeeplyNested=\
Code is nested too deeply, or contains too many chained operations.
unexpectedCurlyProbablyAmendsExpression=\
Unexpected token: `'{'`.\n\
\n\
@@ -0,0 +1,109 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.parser
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.DynamicTest.dynamicTest
import org.junit.jupiter.api.TestFactory
/**
* Malicious input must produce a regular parse error instead of a [StackOverflowError], even on a
* small thread stack.
*/
class ParserDepthTest {
companion object {
private const val STACK_SIZE = 512L * 1024
private const val HUGE = 100_000
private val nesting: Map<String, (Int) -> String> =
mapOf(
"parens" to { n -> "x = " + "(".repeat(n) + "1" + ")".repeat(n) },
"unary minus" to { n -> "x = " + "-".repeat(n) + "1" },
"types" to { n -> "x: " + "List<".repeat(n) + "Int" + ">".repeat(n) },
"parenthesized types" to { n -> "x: " + "(".repeat(n) + "Int" + ")".repeat(n) },
"objects" to { n -> "x " + "{ y ".repeat(n) + "{}" + " }".repeat(n) },
"string interpolation" to { n -> "x = " + "\"\\(".repeat(n) + "1" + ")\"".repeat(n) },
"if" to { n -> "x = " + "if (true) 1 else ".repeat(n) + "2" },
"let" to { n -> "x = " + "let (a = 1) ".repeat(n) + "2" },
"function literal" to { n -> "x = " + "(a) -> ".repeat(n) + "2" },
"right associative operator" to { n -> "x = " + "a ?? ".repeat(n) + "b" },
"pow" to { n -> "x = " + "a ** ".repeat(n) + "b" },
)
// shapes that build deep, left-nested trees without recursing in the parser
private val chains: Map<String, (Int) -> String> =
mapOf(
"binary operator" to { n -> "x = " + "1 + ".repeat(n) + "1" },
"qualified access" to { n -> "x = a" + ".b".repeat(n) },
"subscript" to { n -> "x = a" + "[1]".repeat(n) },
"non-null" to { n -> "x = a" + "!!".repeat(n) },
"constrained type" to { n -> "x: Int" + "(isPositive)".repeat(n) },
"amends" to { n -> "x = (y) " + "{} ".repeat(n) },
)
private fun parsers(): Map<String, (String) -> Unit> =
mapOf(
"Parser" to { src -> Parser().parseModule(src) },
"GenericParser" to { src -> GenericParser().parseModule(src) },
)
/** Runs [action] on a thread with a small stack, and returns what it threw (if anything). */
private fun runOnSmallStack(action: () -> Unit): Throwable? {
var thrown: Throwable? = null
val thread =
Thread(null, { thrown = runCatching(action).exceptionOrNull() }, "test", STACK_SIZE)
thread.start()
thread.join()
return thrown
}
}
@TestFactory
fun `deep nesting fails with a parse error`(): List<DynamicTest> =
nesting.flatMap { (shape, gen) ->
parsers().map { (parser, parse) ->
dynamicTest("$parser: $shape x $HUGE") {
val thrown = runOnSmallStack { parse(gen(HUGE)) }
assertThat(thrown).isNotNull
assertThat(thrown).isNotInstanceOf(StackOverflowError::class.java)
assertThat(thrown).hasMessageContaining("nested too deeply")
}
}
}
@TestFactory
fun `long chains are parsed iteratively`(): List<DynamicTest> = chains.flatMap { (shape, gen) ->
parsers().map { (parser, parse) ->
dynamicTest("$parser: $shape x $HUGE") {
assertThat(runOnSmallStack { parse(gen(HUGE)) }).isNull()
}
}
}
@TestFactory
fun `flat repetition is unaffected`(): List<DynamicTest> =
mapOf(
"union type" to "x: " + "Int|".repeat(HUGE) + "Int",
"listing elements" to "x = new Listing { " + "1\n".repeat(HUGE) + "}",
"properties" to (1..10_000).joinToString("\n") { "p$it = 1" },
)
.flatMap { (shape, src) ->
parsers().map { (parser, parse) ->
dynamicTest("$parser: $shape") { assertThat(runOnSmallStack { parse(src) }).isNull() }
}
}
}