diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ConditionalExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/ConditionalExpression.java new file mode 100644 index 00000000000000..46471b363f3a28 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/syntax/ConditionalExpression.java @@ -0,0 +1,70 @@ +// Copyright 2014 Google Inc. 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 +// +// http://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 com.google.devtools.build.lib.syntax; + +/** + * Syntax node for an if/else expression. + */ +public final class ConditionalExpression extends Expression { + + // Python conditional expressions: $thenCase if $condition else $elseCase + // https://docs.python.org/3.5/reference/expressions.html#conditional-expressions + private final Expression thenCase; + private final Expression condition; + private final Expression elseCase; + + public Expression getThenCase() { return thenCase; } + public Expression getCondition() { return condition; } + public Expression getElseCase() { return elseCase; } + + /** + * Constructor for a conditional expression + */ + public ConditionalExpression( + Expression thenCase, Expression condition, Expression elseCase) { + this.thenCase = thenCase; + this.condition = condition; + this.elseCase = elseCase; + } + + /** + * Constructs a string representation of the if expression + */ + @Override + public String toString() { + return thenCase + " if " + condition + " else " + elseCase; + } + + @Override + Object eval(Environment env) throws EvalException, InterruptedException { + if (EvalUtils.toBoolean(condition.eval(env))) { + return thenCase.eval(env); + } else { + return elseCase.eval(env); + } + } + + @Override + public void accept(SyntaxTreeVisitor visitor) { + visitor.visit(this); + } + + @Override + SkylarkType validate(ValidationEnvironment env) throws EvalException { + condition.validate(env); + return thenCase.validate(env) + .infer(elseCase.validate(env), "else case", thenCase.getLocation(), elseCase.getLocation()); + } +} diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Parser.java b/src/main/java/com/google/devtools/build/lib/syntax/Parser.java index 97a4bbd015ba19..90f79ec92c7fac 100644 --- a/src/main/java/com/google/devtools/build/lib/syntax/Parser.java +++ b/src/main/java/com/google/devtools/build/lib/syntax/Parser.java @@ -341,6 +341,8 @@ private NODE setLocation(NODE node, int startOffset, int // Convenience method that uses end offset from the last node. private NODE setLocation(NODE node, int startOffset, ASTNode lastNode) { + Preconditions.checkNotNull(lastNode, "can't extract end offset from a null node"); + Preconditions.checkNotNull(lastNode.getLocation(), "lastNode doesn't have a location"); return setLocation(node, startOffset, lastNode.getLocation().getEndOffset()); } @@ -908,7 +910,23 @@ private Expression optimizeBinOpExpression( } private Expression parseExpression() { - return parseExpression(0); + int start = token.left; + Expression expr = parseExpression(0); + if (token.kind == TokenKind.IF) { + nextToken(); + Expression condition = parseExpression(0); + if (token.kind == TokenKind.ELSE) { + nextToken(); + Expression elseClause = parseExpression(); + return setLocation(new ConditionalExpression(expr, condition, elseClause), + start, elseClause); + } else { + reportError(lexer.createLocation(start, token.left), + "missing else clause in conditional expression or semicolon before if"); + return expr; // Try to recover from error: drop the if and the expression after it. Ouch. + } + } + return expr; } private Expression parseExpression(int prec) { diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SyntaxTreeVisitor.java b/src/main/java/com/google/devtools/build/lib/syntax/SyntaxTreeVisitor.java index e53a7aa9ed6cb1..2463d2560076fc 100644 --- a/src/main/java/com/google/devtools/build/lib/syntax/SyntaxTreeVisitor.java +++ b/src/main/java/com/google/devtools/build/lib/syntax/SyntaxTreeVisitor.java @@ -140,4 +140,11 @@ public void visit(NotExpression node) { public void visit(Comment node) { } + public void visit(ConditionalExpression node) { + visit(node.getThenCase()); + visit(node.getCondition()); + if (node.getElseCase() != null) { + visit(node.getElseCase()); + } + } } diff --git a/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java index 342f34e7558f97..3fd5508922a2e6 100644 --- a/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java +++ b/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java @@ -199,6 +199,19 @@ public void testGreaterThan() throws Exception { assertEquals(true, eval("'c' > 'a'")); } + @Test + public void testConditionalExpressions() throws Exception { + assertEquals(1, eval("1 if True else 2")); + assertEquals(2, eval("1 if False else 2")); + assertEquals(3, eval("1 + 2 if 3 + 4 else 5 + 6")); + + syntaxEvents.setFailFast(false); + parseExpr("1 if 2"); + syntaxEvents.assertContainsEvent( + "missing else clause in conditional expression or semicolon before if"); + syntaxEvents.collector().clear(); + } + @Test public void testCompareStringInt() throws Exception { checkEvalError("'a' >= 1", "Cannot compare string with int"); diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java index 3e0b0f328556b2..b0735ae75445f5 100644 --- a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java +++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java @@ -877,4 +877,28 @@ public void testListConcatenation() throws Exception {} @Override @Test public void testKeywordArgs() {} + + @Test + public void testConditionalExpressionAtToplevel() throws Exception { + exec(parseFileForSkylark("x = 1 if 2 else 3"), env); + assertEquals(1, env.lookup("x")); + } + + @Test + public void testConditionalExpressionInFunction() throws Exception { + exec(parseFileForSkylark( + "def foo(a, b, c):\n" + + " return a+b if c else a-b\n" + + "x = foo(23, 5, 0)"), env); + assertEquals(18, env.lookup("x")); + } + + @Test + public void testBadConditionalExpressionInFunction() throws Exception { + syntaxEvents.setFailFast(false); + parseFileForSkylark("def foo(a): return [] if a else 0\n"); + syntaxEvents.assertContainsEvent( + "bad else case: int is incompatible with list at /some/file.txt:1:33"); + syntaxEvents.collector().clear(); + } }