From 5d344bc4efd63433493c3c7eb28a048f72bb8ff9 Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Wed, 23 Sep 2026 13:54:50 -0700 Subject: [PATCH] Iteratively parse nested parentheses in PrattParser Replace the lookahead-based grouping parentheses counter (`countGroupingParentheses`) with a bottom-up iterative approach in `PrattParser.parsePrimary()` to eliminate quadratic $O(N^2)$ lookahead scans while keeping $O(1)$ call-stack frames on deeply nested parenthesized expressions. ### Benchmarks (`CelParserBenchmark`, `-c opt`, `parserType=PRATT`, `parseOnly`) | Benchmark Case | Before (Mean) | After (Mean) | Change / Speedup | | :--- | ---: | ---: | ---: | | `NESTED_PARENS` (N=200) | 8,528.69 ns | 5,119.12 ns | -40.0% (1.67x faster) | | `NESTED_LEFT_PARENS_CALC` (N=200) | 951,027.39 ns | 27,998.44 ns | -97.1% (34.0x faster) | | Benchmark Case (Allocations) | Before (Bytes / Objs) | After (Bytes / Objs) | Change | | :--- | ---: | ---: | ---: | | `NESTED_PARENS` (N=200) | 29,200 B / 855 objs | 16,336 B / 453 objs | -44.1% B / -47.0% objs | | `NESTED_LEFT_PARENS_CALC` (N=200) | ~2,690,000 B / ~84,500 objs | 127,016 B / 4,577 objs | -95.3% B / -94.6% objs | PiperOrigin-RevId: 986985005 --- .../main/java/dev/cel/parser/PrattParser.java | 182 +++++++++--------- .../dev/cel/parser/CelParserImplTest.java | 99 ++++++++-- 2 files changed, 169 insertions(+), 112 deletions(-) diff --git a/parser/src/main/java/dev/cel/parser/PrattParser.java b/parser/src/main/java/dev/cel/parser/PrattParser.java index 2f1e9ebeb..fe396c37e 100644 --- a/parser/src/main/java/dev/cel/parser/PrattParser.java +++ b/parser/src/main/java/dev/cel/parser/PrattParser.java @@ -142,7 +142,9 @@ private static final class UnaryOp { private Lexer.Token currentToken; private Lexer.Token peekToken; private int recursionDepth; - private int currentLhsDepth; + // Chain depth of the most recently parsed expression, returned as an out-parameter to avoid + // allocating a wrapper per node. Only valid immediately after the call that produced it. + private int lastParsedDepth; private long nextId; private boolean nodeLimitExceeded; private boolean recursionLimitExceeded; @@ -430,10 +432,14 @@ private CelExpr parseExpr() { return expr; } - @SuppressWarnings("EnumOrdinal") // Using ordinal for O(1) binary operator lookup table private CelExpr parseBinaryAndTernary(int minPrec) { CelExpr lhs = parseSelectorChain(); - int chainDepth = currentLhsDepth; + return parseBinaryAndTernaryFromLhs(lhs, minPrec, lastParsedDepth); + } + + @SuppressWarnings("EnumOrdinal") // Using ordinal for O(1) binary operator lookup table + private CelExpr parseBinaryAndTernaryFromLhs(CelExpr lhs, int minPrec, int initialChainDepth) { + int chainDepth = initialChainDepth; while (!recursionLimitExceeded && !isRecoveryLimitExceeded()) { Lexer.TokenType tok = peekToken.type; if (tok == Lexer.TokenType.QUESTION && minPrec <= 0) { @@ -460,7 +466,12 @@ private CelExpr parseBinaryAndTernary(int minPrec) { long opId = nextId(opTok); CelExpr rhs = parseBinaryAndTernary(opInfo.precedence + 1); lhs = buildBinaryCall(opId, opInfo.name, lhs, rhs); - currentLhsDepth = chainDepth; + // lastParsedDepth is the depth of the rhs just parsed. It hangs one level below this + // operator, while chainDepth already covers the lhs, so the operator node is as deep as + // whichever side is deeper: "x + a.b.c.d" reaches 4 through its rhs and "a.b.c.d + x" + // reaches 4 through its lhs. + chainDepth = Math.max(chainDepth, lastParsedDepth + 1); + lastParsedDepth = chainDepth; } return lhs; } @@ -482,6 +493,7 @@ private CelExpr parseBalancedLogicalChain(CelExpr lhs, BinaryOpInfo opInfo) { long opId = nextId(opTok.start); CelExpr rhs = parseBinaryAndTernary(opInfo.precedence + 1); if (peekToken.type != opInfo.type) { + lastParsedDepth = 0; return buildBinaryCall(opId, opInfo.name, lhs, rhs); } @@ -505,6 +517,7 @@ private CelExpr parseBalancedLogicalChain(CelExpr lhs, BinaryOpInfo opInfo) { ops[opsCount++] = opId; terms[termsCount++] = rhs; } + lastParsedDepth = 0; return balancedTree(opInfo.name, terms, ops, 0, opsCount - 1); } @@ -524,24 +537,26 @@ private static CelExpr buildUnaryCall(long id, String function, CelExpr operand) } private CelExpr parseSelectorChain() { + lastParsedDepth = 0; Lexer.TokenType tok = peekToken.type; CelExpr lhs = (tok == Lexer.TokenType.EXCLAMATION || tok == Lexer.TokenType.MINUS) ? parseUnaryOps() : parsePrimary(); - currentLhsDepth = 0; tok = peekToken.type; if (tok == Lexer.TokenType.DOT || tok == Lexer.TokenType.LEFT_BRACKET || tok == Lexer.TokenType.LEFT_BRACE) { - lhs = parseSelectorChainTail(lhs); + // A parenthesized primary such as "(a.b.c)" already contributes its own depth, which the + // selectors trailing the closing ')' continue to accumulate on top of. + lhs = parseSelectorChainTail(lhs, lastParsedDepth); } return lhs; } - private CelExpr parseSelectorChainTail(CelExpr initialLhs) { + private CelExpr parseSelectorChainTail(CelExpr initialLhs, int initialChainDepth) { CelExpr lhs = initialLhs; - int chainDepth = 0; + int chainDepth = initialChainDepth; while (true) { Lexer.TokenType tok = peekToken.type; if (tok == Lexer.TokenType.DOT) { @@ -566,7 +581,7 @@ private CelExpr parseSelectorChainTail(CelExpr initialLhs) { reportSyntaxError(idTok, "expected identifier after '.'"); } synchronizeOnDelimiter(); - currentLhsDepth = chainDepth; + lastParsedDepth = chainDepth; return lhs; } boolean isMemberCall = (peekToken.type == Lexer.TokenType.LEFT_PAREN); @@ -585,6 +600,10 @@ private CelExpr parseSelectorChainTail(CelExpr initialLhs) { expanded.isPresent() ? expanded.get() : CelExpr.ofCall(callId, Optional.of(lhs), idText, args); + // parseArguments leaves lastParsedDepth at the deepest argument. Arguments hang one + // level below the call node, so "a.f(b.c.d.e)" is 4 deep. The max preserves the + // selectors already walked when the arguments are shallower, as in "a.b.c.f(1)". + chainDepth = Math.max(chainDepth, lastParsedDepth + 1); } else { lhs = CelExpr.ofSelect(nextId(dotTok), lhs, idText, /* isTestOnly= */ false); } @@ -608,17 +627,26 @@ private CelExpr parseSelectorChainTail(CelExpr initialLhs) { String opName = optional ? Operator.OPTIONAL_INDEX.getFunction() : Operator.INDEX.getFunction(); lhs = buildBinaryCall(opId, opName, lhs, index); + // lastParsedDepth is the depth of the index expression just parsed. It hangs one level + // below the index node, so "a[b.c.d.e]" is 4 deep. The max preserves the selectors + // already walked when the index is shallower, as in "a.b.c[0]". + chainDepth = Math.max(chainDepth, lastParsedDepth + 1); } else if (tok == Lexer.TokenType.LEFT_BRACE) { String structName = extractStructName(lhs); if (structName == null) { break; } lhs = parseStruct(nextId(peekToken.start), structName); + // parseStruct leaves lastParsedDepth at the deepest field value, which carries through + // unchanged: "Msg{f: a.b.c.d}" is 3 deep. There is no +1 here because struct creation is + // a primary rather than a chain link, so it adds no level of its own. The max preserves + // the selectors already walked when the fields are shallower, as in "a.b.Msg{f: 1}". + chainDepth = Math.max(chainDepth, lastParsedDepth); } else { break; } } - currentLhsDepth = chainDepth; + lastParsedDepth = chainDepth; return lhs; } @@ -713,7 +741,7 @@ private CelExpr parseUnaryOpsChain(Lexer.Token firstOp) { (peekToken.type == Lexer.TokenType.INT) ? parseIntLiteral(negativeLiteralOpId, /* isNegative= */ true) : parseDoubleLiteral(negativeLiteralOpId, /* isNegative= */ true); - operand = parseSelectorChainTail(operand); + operand = parseSelectorChainTail(operand, /* initialChainDepth= */ 0); } else { operand = parseSelectorChain(); } @@ -772,17 +800,40 @@ private CelExpr parsePrimary() { switch (peekToken.type) { case LEFT_PAREN: { - int groupingParenCount = countGroupingParentheses(); - if (checkRecursion(groupingParenCount, peekToken)) { + if (recursionLimitExceeded || isRecoveryLimitExceeded()) { return ERROR; } - for (int i = 0; i < groupingParenCount; ++i) { + // To avoid deep call-stack recursion on heavily nested parentheses (e.g. "((((a))))" or + // "((((a + 1) + 1) + 1))"), consume all consecutive leading '(' tokens upfront, parse the + // innermost expression once, and then iteratively unwind each enclosing '(' from + // innermost to outermost. After consuming each matching ')', if more enclosing '(' remain + // open and the next token is not another ')', continue parsing any trailing selectors or + // binary/ternary operators belonging to that enclosing parenthesized level using the + // already-parsed inner expression as the LHS. + Lexer.Token firstParen = peekToken; + int openParens = 0; + while (peekToken.type == Lexer.TokenType.LEFT_PAREN) { + openParens++; nextToken(); } - CelExpr expr = parseExpr(); - for (int i = 0; i < groupingParenCount; ++i) { + // Every '(' is a nesting level and costs one unit of recursion budget, so charge all of + // them here. recursionDepth itself only advances by 1: the parens are unwound + // iteratively, so entering parseBinaryAndTernary(0) below adds just one stack frame. + if (checkRecursion(openParens, firstParen)) { + return ERROR; + } + recursionDepth++; + CelExpr expr = parseBinaryAndTernary(0); + int chainDepth = lastParsedDepth; + for (int i = 0; i < openParens; ++i) { expect(Lexer.TokenType.RIGHT_PAREN, ""); + if (i < openParens - 1 && peekToken.type != Lexer.TokenType.RIGHT_PAREN) { + expr = parseSelectorChainTail(expr, chainDepth); + expr = parseBinaryAndTernaryFromLhs(expr, 0, lastParsedDepth); + chainDepth = lastParsedDepth; + } } + recursionDepth--; return expr; } case NULL: @@ -827,12 +878,25 @@ private CelExpr parsePrimary() { } } + /** + * Parses one element of a delimited construct, accumulating {@code lastParsedDepth} to the + * deepest element seen so far. A construct is as deep as its deepest element, not its last one, + * so callers must reset {@code lastParsedDepth} to 0 before the first element. + */ + private CelExpr parseElementExpr() { + int maxDepth = lastParsedDepth; + CelExpr expr = parseExpr(); + lastParsedDepth = Math.max(maxDepth, lastParsedDepth); + return expr; + } + private CelExpr parseList() { Lexer.Token openTok = nextToken(); long listId = nextId(openTok); ImmutableList.Builder elements = ImmutableList.builder(); ImmutableList.Builder optionalIndices = ImmutableList.builder(); int elemIndex = 0; + lastParsedDepth = 0; while (peekToken.type != Lexer.TokenType.RIGHT_BRACKET && peekToken.type != Lexer.TokenType.END) { boolean optional = false; @@ -843,7 +907,7 @@ private CelExpr parseList() { reportError(q.start, "unsupported syntax '?'"); } } - elements.add(parseExpr()); + elements.add(parseElementExpr()); if (optional) { optionalIndices.add(elemIndex); } @@ -862,6 +926,7 @@ private CelExpr parseMap() { Lexer.Token openTok = nextToken(); long mapId = nextId(openTok); ImmutableList.Builder entries = ImmutableList.builder(); + lastParsedDepth = 0; while (peekToken.type != Lexer.TokenType.RIGHT_BRACE && peekToken.type != Lexer.TokenType.END) { boolean optional = false; Lexer.Token keyStart = peekToken; @@ -874,13 +939,13 @@ private CelExpr parseMap() { keyStart = peekToken; } long entryId = nextId(); - CelExpr key = parseExpr(); + CelExpr key = parseElementExpr(); Lexer.Token colon = peekToken; if (!expect(Lexer.TokenType.COLON, "expected ':' in map entry")) { break; } setPosition(entryId, colon); - CelExpr value = parseExpr(); + CelExpr value = parseElementExpr(); entries.add(CelExpr.ofMapEntry(entryId, key, value, optional)); if (peekToken.type == Lexer.TokenType.COMMA) { nextToken(); @@ -895,6 +960,7 @@ private CelExpr parseMap() { private CelExpr parseStruct(long objId, String structName) { nextToken(); ImmutableList.Builder entries = ImmutableList.builder(); + lastParsedDepth = 0; while (peekToken.type != Lexer.TokenType.RIGHT_BRACE && peekToken.type != Lexer.TokenType.END) { boolean optional = false; if (peekToken.type == Lexer.TokenType.QUESTION) { @@ -917,7 +983,7 @@ private CelExpr parseStruct(long objId, String structName) { break; } long fieldId = nextId(colon); - CelExpr value = parseExpr(); + CelExpr value = parseElementExpr(); entries.add(CelExpr.ofStructEntry(fieldId, fieldName, value, optional)); if (peekToken.type == Lexer.TokenType.COMMA) { nextToken(); @@ -931,9 +997,10 @@ private CelExpr parseStruct(long objId, String structName) { private ImmutableList parseArguments(Lexer.TokenType closeToken) { ImmutableList.Builder args = ImmutableList.builder(); + lastParsedDepth = 0; if (peekToken.type != closeToken && peekToken.type != Lexer.TokenType.END) { while (true) { - args.add(parseExpr()); + args.add(parseElementExpr()); if (peekToken.type == Lexer.TokenType.COMMA) { nextToken(); if (peekToken.type == closeToken) { @@ -1099,8 +1166,7 @@ private Optional tryExpandMacro( return Optional.empty(); } if (nodeLimitExceeded) { - reportError( - getPosition(exprId), "could not expand macro: expression node limit exceeded"); + reportError(getPosition(exprId), "could not expand macro: expression node limit exceeded"); return Optional.empty(); } @@ -1179,76 +1245,6 @@ private CelExpr buildMacroCallArgs(CelExpr expr) { return expr; } - private int countGroupingParentheses() { - if (peekToken.type != Lexer.TokenType.LEFT_PAREN) { - return 0; - } - - // Fast path: if the next non-whitespace character is not '(', leading open parens is 1. - int pos = peekToken.end; - int size = content.size(); - while (pos < size) { - int c = content.get(pos); - if (c != ' ' && c != '\t' && c != '\n' && c != '\r' && c != '\f' && c != 11) { - if (c == '/') { - // A comment might precede another '('. - break; - } - if (c == '(') { - break; - } - // Next significant token is definitely not '('. - return 1; - } - pos++; - } - - int savedPos = lexer.savePosition(); - try { - int leadingOpenParens = 1; - Lexer.Token tok = nextSignificantToken(/* reportError= */ false); - while (tok.type == Lexer.TokenType.LEFT_PAREN) { - leadingOpenParens++; - tok = nextSignificantToken(/* reportError= */ false); - } - if (leadingOpenParens == 1) { - return 1; - } - - int openParens = leadingOpenParens; - int consecutiveLeadingClosed = 0; - - while (openParens > 0) { - if (tok.type == Lexer.TokenType.END || tok.type == Lexer.TokenType.ERROR) { - return 1; - } - - if (tok.type == Lexer.TokenType.LEFT_PAREN) { - openParens++; - consecutiveLeadingClosed = 0; - } else if (tok.type == Lexer.TokenType.RIGHT_PAREN) { - if (leadingOpenParens == openParens) { - leadingOpenParens--; - consecutiveLeadingClosed++; - } else { - consecutiveLeadingClosed = 0; - } - openParens--; - } else { - consecutiveLeadingClosed = 0; - } - - if (openParens > 0) { - tok = nextSignificantToken(/* reportError= */ false); - } - } - - return Math.max(1, consecutiveLeadingClosed); - } finally { - lexer.restorePosition(savedPos); - } - } - private final class PrattMacroExprFactory extends CelMacroExprFactory { private final ArrayDeque macroPositions = new ArrayDeque<>(1); diff --git a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java index 6d70596d6..53417aa87 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java @@ -201,17 +201,21 @@ public void parse_throwsWhenExpressionSizeCodePointLimitExceeded() { private enum MaxParseRecursionDepthTestCase { LARGE_CALC( "1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 +" - + " 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30 + 31 + 32 + 33 + 34"), - NESTED_PARENS("((((((((((((((((((((((((((((((((7))))))))))))))))))))))))))))))))"), + + " 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30 + 31 + 32 + 33 + 34", + 32), + NESTED_PARENS("((((((((((((((((((((((((((((((((7))))))))))))))))))))))))))))))))", 32), NESTED_PARENS_WITH_CALC( "((((((((((((((((((((((((((((((((7)))))))))))))))))))))))))))))))) +" - + "(((((((((((((((((((((((((((((((7)))))))))))))))))))))))))))))))"), - FIELD_SELECTIONS("a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.A.B.C.D.E.F.G.H"), + + "(((((((((((((((((((((((((((((((7)))))))))))))))))))))))))))))))", + 32), + FIELD_SELECTIONS("a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.A.B.C.D.E.F.G.H", 32), INDEX_OPERATIONS( - "a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][22][23][24][25][26][27][28][29][30][31][32][33]"), + "a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][22][23][24][25][26][27][28][29][30][31][32][33]", + 32), RELATION_OPERATORS( "a < 1 < 2 < 3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < 11 < 12 < 13 < 14 < 15 < 16 < 17 < 18 < 19 <" - + " 20 < 21 < 22 < 23 < 24 < 25 < 26 < 27 < 28 < 29 < 30 < 31 < 32 < 33"), + + " 20 < 21 < 22 < 23 < 24 < 25 < 26 < 27 < 28 < 29 < 30 < 31 < 32 < 33", + 32), // More than 32 index / relation operators. Note, the recursion count is the // maximum recursion level on the left or right side index expression (20) plus // the number of relation operators (13) @@ -229,27 +233,88 @@ private enum MaxParseRecursionDepthTestCase { + " a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] !=" + " a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] !=" + " a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] !=" - + " a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]"), + + " a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]", + 32), TERNARY( "a ? b : a ? b : a ? b : a ? b : a ? b : a ? b : a ? b : a ? b :" + " a ? b : a ? b : a ? b : a ? b : a ? b : a ? b : a ? b : a ? b :" + " a ? b : a ? b : a ? b : a ? b : a ? b : a ? b : a ? b : a ? b :" - + " a ? b : a ? b : a ? b : a ? b : a ? b : a ? b : a ? b : a ? b : c"), + + " a ? b : a ? b : a ? b : a ? b : a ? b : a ? b : a ? b : a ? b : c", + 32), TERNARY_TRUE_BRANCH_PARENS( - "a ? ((((((((((((((((((((((((((((((((b)))))))))))))))))))))))))))))))) : c"); + "a ? ((((((((((((((((((((((((((((((((b)))))))))))))))))))))))))))))))) : c", 32), + NESTED_LEFT_PARENS_WITH_CALC( + "((((((((((((((((((((((((((((((((7) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1)" + + " + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) +" + + " 1) + 1) + 1) + 1)", + 32), + NESTED_RIGHT_PARENS_WITH_LOGICAL_OR( + "(true) || (true || (true || (true || (true || (true || (true || (true || (true || (true ||" + + " (true || (true || (true || (true || (true || (true || (true || (true || (true ||" + + " (true || (true || (true || (true || (true || (true || (true || (true || (true ||" + + " (true || (true || (true || (true || (true || false))))))))))))))))))))))))))))))))", + 32), + // Parens nested inside an operand are charged against the depth reached so far rather than + // accumulating on top of the enclosing parens, so the Pratt parser allows more depth here. + GROUPING_PARENS_AROUND_CALC( + "((1 + ((7))))", /* antlrMaxRecursionLimit= */ 5, /* prattMaxRecursionLimit= */ 3), + PARENTHESIZED_LHS_CALC("(1 + 1 + 1) + 1 + 1 + 1", 4), + NESTED_LEFT_PARENS_LHS_CALC("((1 + 1) + 1) + 1 + 1 + 1", 4), + GROUPING_PARENS_LHS_CALC("(((1 + 1 + 1))) + 1 + 1 + 1", 4), + PARENTHESIZED_RHS_CALC("1 + (1 + 1 + 1 + 1 + 1)", 4), + PARENTHESIZED_FIELD_SELECTIONS("(a.b.c).d.e.f", 4), + // Depth accumulated inside a call argument carries into the enclosing selector and operator + // chains that wrap the call. + CALL_ARGUMENT_FIELD_SELECTIONS("f(a.b.c.d).e", 3), + CALL_ARGUMENT_FIELD_SELECTIONS_WITH_CALC("x + f(a.b.c.d) + y", 4), + CALL_ARGUMENT_CALC_WITH_CALC("x + f(a + b + c + d) + y", 4), + INDEX_FIELD_SELECTIONS_WITH_CALC("x + a[b.c.d.e] + y", 5), + MEMBER_CALL_ARGUMENT_FIELD_SELECTIONS_WITH_CALC("x + a.f(b.c.d.e) + y", 5), + STRUCT_FIELD_SELECTIONS_WITH_CALC("x + Msg{f: a.b.c.d} + y", 4), + // A delimited construct contributes its deepest element, not its last one. + CALL_ARGUMENT_DEEPEST_NOT_LAST("x + f(a.b.c.d, 1) + y", 4), + MEMBER_CALL_ARGUMENT_DEEPEST_NOT_LAST("x + a.f(b.c.d.e, 1) + y", 5), + LIST_ELEMENT_DEEPEST_NOT_LAST("x + [a.b.c.d, 1][0] + y", 5), + MAP_VALUE_DEEPEST_NOT_LAST("x + {'k': a.b.c.d, 'j': 1}['k'] + y", 5), + MAP_KEY_DEEPEST_NOT_LAST("x + {a.b.c.d.e: 1, 'j': 2}['j'] + y", 6), + STRUCT_FIELD_DEEPEST_NOT_LAST("x + Msg{f: a.b.c.d, g: 1} + y", 4), + NESTED_LIST_ELEMENT_DEEPEST_NOT_LAST("x + [[a.b.c.d, 1], 1][0] + y", 5), + PARENTHESIZED_LOGICAL_AND_FIELD_SELECTION("((a && b.c.d).e)", 2), + PARENTHESIZED_LOGICAL_AND_CHAIN_FIELD_SELECTION("((a && b && c.d.e).f)", 2), + FIELD_SELECTIONS_WITH_CALC("a.b.c.d.e + f.g", 4); - static final int MAX_RECURSION_LIMIT = 32; final String source; + final int antlrMaxRecursionLimit; + final int prattMaxRecursionLimit; - MaxParseRecursionDepthTestCase(String source) { + MaxParseRecursionDepthTestCase(String source, int maxRecursionLimit) { + this(source, maxRecursionLimit, maxRecursionLimit); + } + + MaxParseRecursionDepthTestCase( + String source, int antlrMaxRecursionLimit, int prattMaxRecursionLimit) { this.source = source; + this.antlrMaxRecursionLimit = antlrMaxRecursionLimit; + this.prattMaxRecursionLimit = prattMaxRecursionLimit; } } + @Test + public void parse_nestedParenthesesWithTernaryAndSelectors_succeeds() throws Exception { + CelParser parser = newParserBuilder().build(); + + CelValidationResult parseResult = + parser.parse("((a ? b : c).d[0] ? (e ? f : g) : h) + ((x).y)"); + + assertThat(parseResult.hasError()).isFalse(); + assertThat(parseResult.getAst()).isNotNull(); + } + @Test public void parse_largeExprHitsMaxRecursionLimit_throws( @TestParameter MaxParseRecursionDepthTestCase testCase) { - int maxParseRecursionLimit = MaxParseRecursionDepthTestCase.MAX_RECURSION_LIMIT; + int maxParseRecursionLimit = + enablePrattParser ? testCase.prattMaxRecursionLimit : testCase.antlrMaxRecursionLimit; CelParser parser = newParserBuilder() .setOptions( @@ -277,7 +342,8 @@ public void parse_largeExprHitsMaxRecursionLimit_throws( @Test public void parse_exprUnderMaxRecursionLimit_doesNotThrow( @TestParameter MaxParseRecursionDepthTestCase testCase) throws CelValidationException { - int maxParseRecursionLimit = MaxParseRecursionDepthTestCase.MAX_RECURSION_LIMIT + 1; + int maxParseRecursionLimit = + (enablePrattParser ? testCase.prattMaxRecursionLimit : testCase.antlrMaxRecursionLimit) + 1; CelParser parser = newParserBuilder() .setOptions( @@ -478,10 +544,7 @@ public void parse_macroCopiesNodeWithoutPosition_noSourcePositionRecorded() thro 0, (exprFactory, target, args) -> { CelExpr nodeWithoutPosition = - CelExpr.newBuilder() - .setId(5L) - .setConstant(CelConstant.ofValue(10L)) - .build(); + CelExpr.newBuilder().setId(5L).setConstant(CelConstant.ofValue(10L)).build(); return Optional.of(exprFactory.copy(nodeWithoutPosition)); }); CelParser parser = newParserBuilder().addMacros(macro).build(); @@ -495,5 +558,3 @@ public void parse_macroCopiesNodeWithoutPosition_noSourcePositionRecorded() thro assertThat(result.getAst().getSource().getPositionsMap()).isEmpty(); } } - -