env) {
};
+ /**
+ * Checks if this variable is one of the reserved keywords.
+ *
+ * @return true if this is a keyword (TRUE, FALSE, NIL, LAMBDA, END, IF, ELSE, etc.)
+ */
+ public boolean isKeyword() {
+ return this == TRUE || this == FALSE || this == NIL || this == LAMBDA || this == END
+ || this == IF || this == ELSE || this == FOR || this == IN || this == RETURN
+ || this == BREAK || this == CONTINUE || this == LET || this == WHILE || this == FN
+ || this == ELSIF || this == TRY || this == CATCH || this == FINALLY || this == THROW
+ || this == NEW || this == USE;
+ }
+
+ /**
+ * Checks if this variable is a literal keyword (TRUE, FALSE, NIL).
+ *
+ * @return true if this is a literal keyword
+ */
+ public boolean isLiteralKeyword() {
+ return this == TRUE || this == FALSE || this == NIL;
+ }
+
@Override
public com.googlecode.aviator.lexer.token.Token.TokenType getType() {
return TokenType.Variable;
diff --git a/src/main/java/com/googlecode/aviator/parser/ExpressionParser.java b/src/main/java/com/googlecode/aviator/parser/ExpressionParser.java
index d4207bc1..6ffc1e5a 100644
--- a/src/main/java/com/googlecode/aviator/parser/ExpressionParser.java
+++ b/src/main/java/com/googlecode/aviator/parser/ExpressionParser.java
@@ -45,10 +45,29 @@
/**
- * Syntex parser for expression
+ * Recursive descent parser for AviatorScript expressions.
*
- * @author dennis
+ *
+ * Operator Precedence (lowest to highest):
+ *
+ *
+ * 1. parseTernary ?:
+ * 2. parseLogicalOr || (logical or)
+ * 3. parseLogicalAnd && (logical and)
+ * 4. parseBitOr | (bitwise or)
+ * 5. parseBitXor ^ (bitwise xor)
+ * 6. parseBitAnd & (bitwise and)
+ * 7. parseEquality == != =~ = (comparison and assignment)
+ * 8. parseRelational < <= > >= (relational)
+ * 9. parseShift << >> >>> (bit shift)
+ * 10. parseAdditive + - (additive)
+ * 11. parseMultiplicative * / % (multiplicative)
+ * 12. parseUnary ! - ~ (unary operators)
+ * 13. parseExponent ** (power)
+ * 14. parseFactor literals, variables, function calls, parentheses
+ *
*
+ * @author dennis
*/
public class ExpressionParser implements Parser {
private final ExpressionLexer lexer;
@@ -60,6 +79,9 @@ public class ExpressionParser implements Parser {
private final ArrayDeque> prevTokens = new ArrayDeque<>();
+ /** Maximum number of previous tokens to keep for lookback operations */
+ private static final int MAX_PREV_TOKENS = 256;
+
private CodeGenerator codeGenerator;
private ScopeInfo scope;
@@ -186,13 +208,13 @@ public void returnStatement() {
} else {
if (this.scope.newLexicalScope) {
cg.onMethodName(Constants.ReducerReturnFn);
- if (!ternary()) {
+ if (!parseTernary()) {
reportSyntaxError("invalid value for return, missing ';'?");
}
cg.onMethodParameter(this.lookahead);
cg.onMethodInvoke(this.lookahead);
} else {
- if (!ternary()) {
+ if (!parseTernary()) {
reportSyntaxError("invalid value for return, missing ';'?");
}
}
@@ -204,7 +226,7 @@ public void returnStatement() {
move(true);
}
- public boolean ternary() {
+ public boolean parseTernary() {
int gcTimes = this.getCGTimes;
if (this.lookahead == Variable.NEW) {
@@ -212,7 +234,7 @@ public boolean ternary() {
return true;
}
- join();
+ parseLogicalOr();
if (this.lookahead == null || expectChar(':') || expectChar(',')) {
return gcTimes < this.getCGTimes;
}
@@ -221,13 +243,13 @@ public boolean ternary() {
move(true);
CodeGenerator cg = getCodeGeneratorWithTimes();
cg.onTernaryBoolean(opToken);
- if (!ternary()) {
+ if (!parseTernary()) {
reportSyntaxError("invalid token for ternary operator");
}
if (expectChar(':')) {
move(true);
cg.onTernaryLeft(this.lookahead);
- if (!ternary()) {
+ if (!parseTernary()) {
reportSyntaxError("invalid token for ternary operator");
}
cg.onTernaryRight(this.lookahead);
@@ -239,8 +261,8 @@ public boolean ternary() {
}
- public void join() {
- and();
+ public void parseLogicalOr() {
+ parseLogicalAnd();
while (true) {
Token> opToken = this.lookahead;
if (expectChar('|')) {
@@ -248,7 +270,7 @@ public void join() {
move(true);
if (expectChar('|')) {
move(true);
- and();
+ parseLogicalAnd();
getCodeGeneratorWithTimes().onJoinRight(opToken);
} else {
reportSyntaxError("expect '|'");
@@ -262,7 +284,7 @@ public void join() {
CodeGenerator cg = getCodeGeneratorWithTimes();
cg.onJoinLeft(opToken);
move(true);
- and();
+ parseLogicalAnd();
cg.onJoinRight(opToken);
continue;
}
@@ -283,8 +305,8 @@ private boolean expectChar(final char ch) {
}
- public void bitOr() {
- xor();
+ public void parseBitOr() {
+ parseBitXor();
while (true) {
Token> opToken = this.lookahead;
if (expectChar('|')) {
@@ -293,7 +315,7 @@ public void bitOr() {
back();
break;
}
- xor();
+ parseBitXor();
getCodeGeneratorWithTimes().onBitOr(opToken);
} else {
break;
@@ -302,13 +324,13 @@ public void bitOr() {
}
- public void xor() {
- bitAnd();
+ public void parseBitXor() {
+ parseBitAnd();
while (true) {
Token> opToken = this.lookahead;
if (expectChar('^')) {
move(true);
- bitAnd();
+ parseBitAnd();
getCodeGeneratorWithTimes().onBitXor(opToken);
} else {
break;
@@ -317,8 +339,8 @@ public void xor() {
}
- public void bitAnd() {
- equality();
+ public void parseBitAnd() {
+ parseEquality();
while (true) {
Token> opToken = this.lookahead;
if (expectChar('&')) {
@@ -327,7 +349,7 @@ public void bitAnd() {
back();
break;
}
- equality();
+ parseEquality();
getCodeGeneratorWithTimes().onBitAnd(opToken);
} else {
break;
@@ -336,8 +358,8 @@ public void bitAnd() {
}
- public void and() {
- bitOr();
+ public void parseLogicalAnd() {
+ parseBitOr();
while (true) {
Token> opToken = this.lookahead;
@@ -347,7 +369,7 @@ public void and() {
move(true);
if (expectChar('&')) {
move(true);
- bitOr();
+ parseBitOr();
cg.onAndRight(opToken);
} else {
reportSyntaxError("expect '&'");
@@ -361,7 +383,7 @@ public void and() {
CodeGenerator cg = getCodeGeneratorWithTimes();
cg.onAndLeft(opToken);
move(true);
- bitOr();
+ parseBitOr();
cg.onAndRight(opToken);
continue;
}
@@ -376,8 +398,8 @@ public void and() {
}
- public void equality() {
- rel();
+ public void parseEquality() {
+ parseRelational();
while (true) {
Token> opToken = this.lookahead;
Token> prevToken = getPrevToken();
@@ -385,12 +407,12 @@ public void equality() {
move(true);
if (expectChar('=')) {
move(true);
- rel();
+ parseRelational();
getCodeGeneratorWithTimes().onEq(opToken);
} else if (expectChar('~')) {
// It is a regular expression
move(true);
- rel();
+ parseRelational();
getCodeGeneratorWithTimes().onMatch(opToken);
} else {
// this.back();
@@ -451,7 +473,7 @@ public void equality() {
move(true);
if (expectChar('=')) {
move(true);
- rel();
+ parseRelational();
getCodeGeneratorWithTimes().onNeq(opToken);
} else {
reportSyntaxError("expect '='");
@@ -480,28 +502,28 @@ private void checkVarIsInit(final Token> prevToken, StatementType stmtType) {
}
- public void rel() {
- shift();
+ public void parseRelational() {
+ parseShift();
while (true) {
Token> opToken = this.lookahead;
if (expectChar('<')) {
move(true);
if (expectChar('=')) {
move(true);
- expr();
+ parseAdditive();
getCodeGeneratorWithTimes().onLe(opToken);
} else {
- expr();
+ parseAdditive();
getCodeGeneratorWithTimes().onLt(opToken);
}
} else if (expectChar('>')) {
move(true);
if (expectChar('=')) {
move(true);
- expr();
+ parseAdditive();
getCodeGeneratorWithTimes().onGe(opToken);
} else {
- expr();
+ parseAdditive();
getCodeGeneratorWithTimes().onGt(opToken);
}
} else {
@@ -511,15 +533,15 @@ public void rel() {
}
- public void shift() {
- expr();
+ public void parseShift() {
+ parseAdditive();
while (true) {
Token> opToken = this.lookahead;
if (expectChar('<')) {
move(true);
if (expectChar('<')) {
move(true);
- expr();
+ parseAdditive();
getCodeGeneratorWithTimes().onShiftLeft(opToken);
} else {
back();
@@ -531,10 +553,10 @@ public void shift() {
move(true);
if (expectChar('>')) {
move(true);
- expr();
+ parseAdditive();
getCodeGeneratorWithTimes().onUnsignedShiftRight(opToken);
} else {
- expr();
+ parseAdditive();
getCodeGeneratorWithTimes().onShiftRight(opToken);
}
@@ -549,17 +571,17 @@ public void shift() {
}
- public void expr() {
- term();
+ public void parseAdditive() {
+ parseMultiplicative();
while (true) {
Token> opToken = this.lookahead;
if (expectChar('+')) {
move(true);
- term();
+ parseMultiplicative();
getCodeGeneratorWithTimes().onAdd(opToken);
} else if (expectChar('-')) {
move(true);
- term();
+ parseMultiplicative();
getCodeGeneratorWithTimes().onSub(opToken);
} else {
break;
@@ -567,15 +589,15 @@ public void expr() {
}
}
- public void exponent() {
- factor();
+ public void parseExponent() {
+ parseFactor();
while (true) {
Token> opToken = this.lookahead;
if (expectChar('*')) {
move(true);
if (expectChar('*')) {
move(true);
- unary();
+ parseUnary();
getCodeGeneratorWithTimes().onExponent(opToken);
} else {
back();
@@ -588,21 +610,21 @@ public void exponent() {
}
- public void term() {
- unary();
+ public void parseMultiplicative() {
+ parseUnary();
while (true) {
Token> opToken = this.lookahead;
if (expectChar('*')) {
move(true);
- unary();
+ parseUnary();
getCodeGeneratorWithTimes().onMult(opToken);
} else if (expectChar('/')) {
move(true);
- unary();
+ parseUnary();
getCodeGeneratorWithTimes().onDiv(opToken);
} else if (expectChar('%')) {
move(true);
- unary();
+ parseUnary();
getCodeGeneratorWithTimes().onMod(opToken);
} else {
break;
@@ -611,16 +633,16 @@ public void term() {
}
- public void unary() {
+ public void parseUnary() {
Token> opToken = this.lookahead;
if (expectChar('!')) {
move(true);
// check if it is a seq function call,"!" as variable
if (expectChar(',') || expectChar(')')) {
back();
- exponent();
+ parseExponent();
} else {
- unary();
+ parseUnary();
getCodeGeneratorWithTimes().onNot(opToken);
}
} else if (expectChar('-')) {
@@ -628,9 +650,9 @@ public void unary() {
// check if it is a seq function call,"!" as variable
if (expectChar(',') || expectChar(')')) {
back();
- exponent();
+ parseExponent();
} else {
- unary();
+ parseUnary();
getCodeGeneratorWithTimes().onNeg(opToken);
}
} else if (expectChar('~')) {
@@ -638,13 +660,13 @@ public void unary() {
// check if it is a seq function call,"~" as variable
if (expectChar(',') || expectChar(')')) {
back();
- exponent();
+ parseExponent();
} else {
- unary();
+ parseUnary();
getCodeGeneratorWithTimes().onBitNot(opToken);
}
} else {
- exponent();
+ parseExponent();
}
}
@@ -696,15 +718,15 @@ public boolean isOPVariable(final Token> token) {
}
}
- public void factor() {
- if (factor0()) {
+ public void parseFactor() {
+ if (parseFactor0()) {
methodInvokeOrArrayAccess();
}
}
- private boolean factor0() {
+ private boolean parseFactor0() {
if (this.lookahead == null) {
reportSyntaxError("illegal token");
}
@@ -714,7 +736,7 @@ private boolean factor0() {
if (expectChar('(')) {
move(true);
this.scope.enterParen();
- ternary();
+ parseTernary();
if (expectChar(')')) {
move(true);
this.scope.leaveParen();
@@ -891,11 +913,11 @@ private boolean arrayAccess() {
private void array() {
this.scope.enterBracket();
- if (getPrevToken() == Variable.TRUE || getPrevToken() == Variable.FALSE
- || getPrevToken() == Variable.NIL) {
- reportSyntaxError(getPrevToken().getLexeme() + " could not use [] operator");
+ Token> prev = getPrevToken();
+ if (prev instanceof Variable && ((Variable) prev).isLiteralKeyword()) {
+ reportSyntaxError(prev.getLexeme() + " could not use [] operator");
}
- if (!ternary()) {
+ if (!parseTernary()) {
reportSyntaxError("missing index for array access");
}
if (expectChar(']')) {
@@ -912,13 +934,32 @@ private void checkVariableName(final Token> token) {
if (!((Variable) token).isQuote()) {
String[] names = token.getLexeme().split("\\.");
for (String name : names) {
- if (!isJavaIdentifier(name)) {
+ if (!isValidPropertySegment(name)) {
reportSyntaxError("illegal identifier: " + name);
}
}
}
}
+ private boolean isValidPropertySegment(final String segment) {
+ if (segment == null || segment.isEmpty()) {
+ return true; // For formats like "a.[0].b"
+ }
+
+ int bracketIdx = segment.indexOf('[');
+ if (bracketIdx < 0) {
+ return isJavaIdentifier(segment);
+ }
+
+ if (bracketIdx == 0) {
+ return segment.endsWith("]"); // "[0]" format
+ }
+
+ // "bars[0]" format
+ String baseName = segment.substring(0, bracketIdx);
+ return isJavaIdentifier(baseName) && segment.endsWith("]");
+ }
+
private void methodInvokeOrArrayAccess() {
while (expectChar('[') || expectChar('(')) {
if (isConstant(getPrevToken(), this.instance)) {
@@ -961,7 +1002,7 @@ private void method(final Token> methodName) {
}
}
- ternary();
+ parseTernary();
if (isPackArgs) {
withMetaEnd(Constants.UNPACK_ARGS, true);
@@ -987,7 +1028,7 @@ private void method(final Token> methodName) {
}
}
- if (!ternary()) {
+ if (!parseTernary()) {
reportSyntaxError("invalid argument");
}
@@ -1109,6 +1150,10 @@ private boolean isValidLookahead() {
public void move(final boolean analyse) {
if (this.lookahead != null) {
this.prevTokens.push(this.lookahead);
+ // Limit memory usage by removing oldest tokens
+ if (this.prevTokens.size() > MAX_PREV_TOKENS) {
+ this.prevTokens.pollLast();
+ }
this.lookahead = this.lexer.scan(analyse);
if (this.lookahead != null) {
this.parsedTokens++;
@@ -1579,11 +1624,11 @@ private void newStatement() {
this.scope.enterParen();
move(true);
if (!expectChar(')')) {
- ternary();
+ parseTernary();
getCodeGeneratorWithTimes().onMethodParameter(this.lookahead);
while (expectChar(',')) {
move(true);
- if (!ternary()) {
+ if (!parseTernary()) {
reportSyntaxError("invalid argument");
}
getCodeGeneratorWithTimes().onMethodParameter(this.lookahead);
@@ -1706,7 +1751,7 @@ private StatementType statement() {
useStatement();
return StatementType.Other;
} else {
- if (ternary()) {
+ if (parseTernary()) {
return StatementType.Ternary;
} else {
return StatementType.Empty;
@@ -1795,7 +1840,7 @@ private void forStatement() {
{
getCodeGeneratorWithTimes().onMethodName(Constants.ReducerFn);
// The seq
- if (!ternary()) {
+ if (!parseTernary()) {
reportSyntaxError("missing collection");
}
getCodeGeneratorWithTimes().onMethodParameter(this.lookahead);
@@ -1943,7 +1988,7 @@ private boolean ifStatement(final boolean isWhile, final boolean isElsif) {
getCodeGeneratorWithTimes().onMethodName(Constants.IfReturnFn);
{
- if (!ternary()) {
+ if (!parseTernary()) {
reportSyntaxError("missing test statement for if");
}
@@ -2134,7 +2179,7 @@ public static boolean isLiteralToken(final Token> token,
final AviatorEvaluatorInstance instance) {
switch (token.getType()) {
case Variable:
- return token == Variable.TRUE || token == Variable.FALSE || token == Variable.NIL;
+ return ((Variable) token).isLiteralKeyword();
case Char:
case Number:
case Pattern:
diff --git a/src/test/java/com/googlecode/aviator/lexer/ExpressionLexerUnitTest.java b/src/test/java/com/googlecode/aviator/lexer/ExpressionLexerUnitTest.java
index a9977a05..242a1a64 100644
--- a/src/test/java/com/googlecode/aviator/lexer/ExpressionLexerUnitTest.java
+++ b/src/test/java/com/googlecode/aviator/lexer/ExpressionLexerUnitTest.java
@@ -417,6 +417,42 @@ public void testQuoteVar() {
}
+ @Test
+ public void testNormalVarWithArrayIndex() {
+ this.lexer = new ExpressionLexer(this.instance, "foo.bars[0].name");
+ Token> token = this.lexer.scan();
+
+ assertEquals(TokenType.Variable, token.getType());
+ assertEquals("foo.bars[0].name", token.getValue(null));
+ assertFalse(((Variable) token).isQuote());
+ assertNull(this.lexer.scan());
+ }
+
+
+ @Test
+ public void testNormalVarWithMultipleArrayIndices() {
+ this.lexer = new ExpressionLexer(this.instance, "a.b[0].c[1].d");
+ Token> token = this.lexer.scan();
+
+ assertEquals(TokenType.Variable, token.getType());
+ assertEquals("a.b[0].c[1].d", token.getValue(null));
+ assertFalse(((Variable) token).isQuote());
+ assertNull(this.lexer.scan());
+ }
+
+
+ @Test
+ public void testNormalVarWithDotOnly() {
+ this.lexer = new ExpressionLexer(this.instance, "obj.field.nested");
+ Token> token = this.lexer.scan();
+
+ assertEquals(TokenType.Variable, token.getType());
+ assertEquals("obj.field.nested", token.getValue(null));
+ assertFalse(((Variable) token).isQuote());
+ assertNull(this.lexer.scan());
+ }
+
+
@Test
public void testExpression_Logic_Join() {
this.lexer = new ExpressionLexer(this.instance, "a || c ");
diff --git a/src/test/java/com/googlecode/aviator/test/function/QuoteVarTest.java b/src/test/java/com/googlecode/aviator/test/function/QuoteVarTest.java
index da47fbf8..cec21a73 100644
--- a/src/test/java/com/googlecode/aviator/test/function/QuoteVarTest.java
+++ b/src/test/java/com/googlecode/aviator/test/function/QuoteVarTest.java
@@ -96,4 +96,50 @@ public void testQuoteVar() {
assertEquals("hello,bar", AviatorEvaluator.execute("'hello,' + #foo.bars[0].name", env));
assertEquals(3, AviatorEvaluator.execute("string.length(#foo.bars[0].name)", env));
}
+
+
+ @Test
+ public void testPropertyAccessWithoutQuote() {
+ Foo foo = new Foo(100, 3.14f, new Date());
+ Map env = new HashMap();
+ env.put("foo", foo);
+
+ // These should work WITHOUT # prefix
+ assertEquals("bar", AviatorEvaluator.execute("foo.bars[0].name", env));
+ assertEquals("hello,bar", AviatorEvaluator.execute("'hello,' + foo.bars[0].name", env));
+ assertEquals(3, AviatorEvaluator.execute("string.length(foo.bars[0].name)", env));
+ assertEquals(100, AviatorEvaluator.execute("foo.i", env));
+ assertEquals(3.14f, AviatorEvaluator.execute("foo.f", env));
+ }
+
+
+ @Test
+ public void testFullKeyPriority() {
+ Map env = new HashMap();
+ env.put("a.b.c", "full-key-value");
+
+ Map innerB = new HashMap();
+ innerB.put("c", "property-chain-value");
+ Map innerA = new HashMap();
+ innerA.put("b", innerB);
+ env.put("a", innerA);
+
+ // Full key should take priority
+ assertEquals("full-key-value", AviatorEvaluator.execute("a.b.c", env));
+ }
+
+
+ @Test
+ public void testPropertyChainWhenNoFullKey() {
+ Map env = new HashMap();
+
+ Map innerB = new HashMap();
+ innerB.put("c", "property-chain-value");
+ Map innerA = new HashMap();
+ innerA.put("b", innerB);
+ env.put("a", innerA);
+
+ // When "a.b.c" key doesn't exist, should traverse property chain
+ assertEquals("property-chain-value", AviatorEvaluator.execute("a.b.c", env));
+ }
}