blob: 6cb31335af7a5bd0a9cb1b3e41f223b46333d34d [file] [log] [blame]
// Copyright 2017 The Bazel 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
//
// 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;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.util.SpellChecker;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/** A syntax-tree-walking evaluator for StarlarkFunction bodies. */
final class Eval {
private Eval() {} // uninstantiable
// ---- entry point ----
// Called from StarlarkFunction.fastcall.
static Object execFunctionBody(StarlarkThread.Frame fr, List<Statement> statements)
throws EvalException, InterruptedException {
fr.thread.checkInterrupt();
execStatements(fr, statements, /*indented=*/ false);
return fr.result;
}
private static StarlarkFunction fn(StarlarkThread.Frame fr) {
return (StarlarkFunction) fr.fn;
}
private static TokenKind execStatements(
StarlarkThread.Frame fr, List<Statement> statements, boolean indented)
throws EvalException, InterruptedException {
boolean isToplevelFunction = fn(fr).isToplevel;
// Hot code path, good chance of short lists which don't justify the iterator overhead.
for (int i = 0; i < statements.size(); i++) {
Statement stmt = statements.get(i);
TokenKind flow = exec(fr, stmt);
if (flow != TokenKind.PASS) {
return flow;
}
// Hack for SkylarkImportLookupFunction's "export" semantics.
// We enable it only for statements outside any function (isToplevelFunction)
// and outside any if- or for- statements (!indented).
if (isToplevelFunction && !indented && fr.thread.postAssignHook != null) {
if (stmt instanceof AssignmentStatement) {
AssignmentStatement assign = (AssignmentStatement) stmt;
for (Identifier id : Identifier.boundIdentifiers(assign.getLHS())) {
String name = id.getName();
Object value = fn(fr).getModule().lookup(name);
fr.thread.postAssignHook.assign(name, value);
}
}
}
}
return TokenKind.PASS;
}
private static void execAssignment(StarlarkThread.Frame fr, AssignmentStatement node)
throws EvalException, InterruptedException {
if (node.isAugmented()) {
execAugmentedAssignment(fr, node);
} else {
Object rvalue = eval(fr, node.getRHS());
try {
assign(fr, node.getLHS(), rvalue);
} catch (EvalException ex) {
// TODO(adonovan): use location of = operator.
throw ex.ensureLocation(node.getStartLocation());
}
}
}
private static TokenKind execFor(StarlarkThread.Frame fr, ForStatement node)
throws EvalException, InterruptedException {
Object o = eval(fr, node.getCollection());
Iterable<?> seq = Starlark.toIterable(o);
EvalUtils.lock(o, node.getStartLocation());
try {
for (Object it : seq) {
assign(fr, node.getLHS(), it);
switch (execStatements(fr, node.getBlock(), /*indented=*/ true)) {
case PASS:
case CONTINUE:
// Stay in loop.
fr.thread.checkInterrupt();
continue;
case BREAK:
// Finish loop, execute next statement after loop.
return TokenKind.PASS;
case RETURN:
// Finish loop, return from function.
return TokenKind.RETURN;
default:
throw new IllegalStateException("unreachable");
}
}
} catch (EvalException ex) {
throw ex.ensureLocation(node.getLHS().getStartLocation());
} finally {
EvalUtils.unlock(o, node.getStartLocation());
}
return TokenKind.PASS;
}
private static void execDef(StarlarkThread.Frame fr, DefStatement node)
throws EvalException, InterruptedException {
FunctionSignature sig = node.getSignature();
// Evaluate default value expressions of optional parameters.
// They may be discontinuous:
// def f(a, b=1, *, c, d=2) has a defaults tuple of (1, 2).
// TODO(adonovan): record the gaps (e.g. c) with a sentinel
// to simplify Starlark.matchSignature.
Tuple<Object> defaults = Tuple.empty();
int ndefaults = node.getSignature().numOptionals();
if (ndefaults > 0) {
Object[] array = new Object[ndefaults];
for (int i = sig.numMandatoryPositionals(), j = 0; i < sig.numParameters(); i++) {
Expression expr = node.getParameters().get(i).getDefaultValue();
if (expr != null) {
array[j++] = eval(fr, expr);
}
}
defaults = Tuple.wrap(array);
}
assignIdentifier(
fr,
node.getIdentifier(),
new StarlarkFunction(
node.getIdentifier().getName(),
node.getIdentifier().getStartLocation(),
sig,
defaults,
node.getStatements(),
fn(fr).getModule()));
}
private static TokenKind execIf(StarlarkThread.Frame fr, IfStatement node)
throws EvalException, InterruptedException {
boolean cond = Starlark.truth(eval(fr, node.getCondition()));
if (cond) {
return execStatements(fr, node.getThenBlock(), /*indented=*/ true);
} else if (node.getElseBlock() != null) {
return execStatements(fr, node.getElseBlock(), /*indented=*/ true);
}
return TokenKind.PASS;
}
private static void execLoad(StarlarkThread.Frame fr, LoadStatement node) throws EvalException {
for (LoadStatement.Binding binding : node.getBindings()) {
Identifier orig = binding.getOriginalName();
// TODO(adonovan): make this a static check.
if (orig.isPrivate() && !node.mayLoadInternalSymbols()) {
throw new EvalException(
orig.getStartLocation(),
"symbol '" + orig.getName() + "' is private and cannot be imported.");
}
// Load module.
String moduleName = node.getImport().getValue();
StarlarkThread.Extension module = fr.thread.getExtension(moduleName);
if (module == null) {
throw new EvalException(
node.getImport().getStartLocation(),
String.format(
"file '%s' was not correctly loaded. "
+ "Make sure the 'load' statement appears in the global scope in your file",
moduleName));
}
// Extract symbol.
Object value = module.getBindings().get(orig.getName());
if (value == null) {
throw new EvalException(
orig.getStartLocation(),
String.format(
"file '%s' does not contain symbol '%s'%s",
moduleName,
orig.getName(),
SpellChecker.didYouMean(orig.getName(), module.getBindings().keySet())));
}
// Define module-local variable.
// TODO(adonovan): eventually the default behavior should be that
// loads bind file-locally. Either way, the resolver should designate
// the proper scope of binding.getLocalName() and this should become
// simply assign(binding.getLocalName(), value).
// Currently, we update the module but not module.exportedBindings;
// changing it to fr.locals.put breaks a test. TODO(adonovan): find out why.
try {
fn(fr).getModule().put(binding.getLocalName().getName(), value);
} catch (Mutability.MutabilityException ex) {
throw new AssertionError(ex);
}
}
}
private static TokenKind execReturn(StarlarkThread.Frame fr, ReturnStatement node)
throws EvalException, InterruptedException {
Expression ret = node.getReturnExpression();
if (ret != null) {
fr.result = eval(fr, ret);
}
return TokenKind.RETURN;
}
private static TokenKind exec(StarlarkThread.Frame fr, Statement st)
throws EvalException, InterruptedException {
if (fr.dbg != null) {
Location loc = st.getStartLocation();
fr.setLocation(loc);
fr.dbg.before(fr.thread, loc); // location is now redundant since it's in the thread
}
try {
return execDispatch(fr, st);
} catch (EvalException ex) {
throw maybeTransformException(st, ex);
}
}
private static TokenKind execDispatch(StarlarkThread.Frame fr, Statement st)
throws EvalException, InterruptedException {
switch (st.kind()) {
case ASSIGNMENT:
execAssignment(fr, (AssignmentStatement) st);
return TokenKind.PASS;
case EXPRESSION:
eval(fr, ((ExpressionStatement) st).getExpression());
return TokenKind.PASS;
case FLOW:
return ((FlowStatement) st).getKind();
case FOR:
return execFor(fr, (ForStatement) st);
case DEF:
execDef(fr, (DefStatement) st);
return TokenKind.PASS;
case IF:
return execIf(fr, (IfStatement) st);
case LOAD:
execLoad(fr, (LoadStatement) st);
return TokenKind.PASS;
case RETURN:
return execReturn(fr, (ReturnStatement) st);
}
throw new IllegalArgumentException("unexpected statement: " + st.kind());
}
/**
* Updates the environment bindings, and possibly mutates objects, so as to assign the given value
* to the given expression. May throw an EvalException without location.
*/
private static void assign(StarlarkThread.Frame fr, Expression lhs, Object value)
throws EvalException, InterruptedException {
if (lhs instanceof Identifier) {
// x = ...
assignIdentifier(fr, (Identifier) lhs, value);
} else if (lhs instanceof IndexExpression) {
// x[i] = ...
Object object = eval(fr, ((IndexExpression) lhs).getObject());
Object key = eval(fr, ((IndexExpression) lhs).getKey());
EvalUtils.setIndex(object, key, value);
} else if (lhs instanceof ListExpression) {
// a, b, c = ...
ListExpression list = (ListExpression) lhs;
assignList(fr, list.getElements(), value);
} else {
// Not possible for validated ASTs.
throw Starlark.errorf("cannot assign to '%s'", lhs);
}
}
private static void assignIdentifier(StarlarkThread.Frame fr, Identifier id, Object value)
throws EvalException {
ValidationEnvironment.Scope scope = id.getScope();
// Legacy hack for incomplete identifier resolution.
// In a <toplevel> function, assignments to unresolved identifiers
// update the module, except for load statements and comprehensions,
// which should both be file-local.
// Load statements don't yet use assignIdentifier,
// so we need consider only comprehensions.
// In effect, we do the missing resolution using fr.compcount.
if (scope == null) {
scope =
fn(fr).isToplevel && fr.compcount == 0
? ValidationEnvironment.Scope.Module //
: ValidationEnvironment.Scope.Local;
}
String name = id.getName();
switch (scope) {
case Local:
fr.locals.put(name, value);
break;
case Module:
// Updates a module binding and sets its 'exported' flag.
// (Only load bindings are not exported.
// But exportedBindings does at run time what should be done in the resolver.)
Module module = fn(fr).getModule();
try {
module.put(name, value);
module.exportedBindings.add(name);
} catch (Mutability.MutabilityException ex) {
throw new IllegalStateException(ex);
}
break;
default:
throw new IllegalStateException(scope.toString());
}
}
/**
* Recursively assigns an iterable value to a sequence of assignable expressions. May throw an
* EvalException without location.
*/
private static void assignList(StarlarkThread.Frame fr, List<Expression> lhs, Object x)
throws EvalException, InterruptedException {
// TODO(adonovan): lock/unlock rhs during iteration so that
// assignments fail when the left side aliases the right,
// which is a tricky case in Python assignment semantics.
int nrhs = Starlark.len(x);
if (nrhs < 0) {
throw Starlark.errorf("type '%s' is not iterable", Starlark.type(x));
}
Iterable<?> rhs = Starlark.toIterable(x); // fails if x is a string
int len = lhs.size();
if (len == 0) {
throw Starlark.errorf(
"lists or tuples on the left-hand side of assignments must have at least one item");
}
if (len != nrhs) {
throw Starlark.errorf(
"assignment length mismatch: left-hand side has length %d, but right-hand side evaluates"
+ " to value of length %d",
len, nrhs);
}
int i = 0;
for (Object item : rhs) {
assign(fr, lhs.get(i), item);
i++;
}
}
private static void execAugmentedAssignment(StarlarkThread.Frame fr, AssignmentStatement stmt)
throws EvalException, InterruptedException {
Expression lhs = stmt.getLHS();
TokenKind op = stmt.getOperator();
Expression rhs = stmt.getRHS();
if (lhs instanceof Identifier) {
Object x = eval(fr, lhs);
Object y = eval(fr, rhs);
Object z = inplaceBinaryOp(fr, op, x, y);
assignIdentifier(fr, (Identifier) lhs, z);
} else if (lhs instanceof IndexExpression) {
// object[index] op= y
// The object and key should be evaluated only once, so we don't use lhs.eval().
IndexExpression index = (IndexExpression) lhs;
Object object = eval(fr, index.getObject());
Object key = eval(fr, index.getKey());
Object x = EvalUtils.index(fr.thread.mutability(), fr.thread.getSemantics(), object, key);
// Evaluate rhs after lhs.
Object y = eval(fr, rhs);
Object z = inplaceBinaryOp(fr, op, x, y);
try {
EvalUtils.setIndex(object, key, z);
} catch (EvalException ex) {
Location loc = stmt.getStartLocation(); // TODO(adonovan): use operator location
throw ex.ensureLocation(loc);
}
} else if (lhs instanceof ListExpression) {
Location loc = stmt.getStartLocation(); // TODO(adonovan): use operator location
throw new EvalException(loc, "cannot perform augmented assignment on a list literal");
} else {
// Not possible for validated ASTs.
Location loc = stmt.getStartLocation(); // TODO(adonovan): use operator location
throw new EvalException(loc, "cannot perform augmented assignment on '" + lhs + "'");
}
}
private static Object inplaceBinaryOp(StarlarkThread.Frame fr, TokenKind op, Object x, Object y)
throws EvalException {
// list += iterable behaves like list.extend(iterable)
// TODO(b/141263526): following Python, allow list+=iterable (but not list+iterable).
if (op == TokenKind.PLUS && x instanceof StarlarkList && y instanceof StarlarkList) {
StarlarkList<?> list = (StarlarkList) x;
list.extend(y);
return list;
}
return EvalUtils.binaryOp(op, x, y, fr.thread.getSemantics(), fr.thread.mutability());
}
// ---- expressions ----
private static Object eval(StarlarkThread.Frame fr, Expression expr)
throws EvalException, InterruptedException {
try {
return doEval(fr, expr);
} catch (EvalException ex) {
throw maybeTransformException(expr, ex);
}
}
private static Object doEval(StarlarkThread.Frame fr, Expression expr)
throws EvalException, InterruptedException {
switch (expr.kind()) {
case BINARY_OPERATOR:
{
BinaryOperatorExpression binop = (BinaryOperatorExpression) expr;
Object x = eval(fr, binop.getX());
// AND and OR require short-circuit evaluation.
switch (binop.getOperator()) {
case AND:
return Starlark.truth(x) ? eval(fr, binop.getY()) : x;
case OR:
return Starlark.truth(x) ? x : eval(fr, binop.getY());
default:
Object y = eval(fr, binop.getY());
try {
return EvalUtils.binaryOp(
binop.getOperator(), x, y, fr.thread.getSemantics(), fr.thread.mutability());
} catch (EvalException ex) {
// TODO(adonovan): use operator location
ex.ensureLocation(binop.getStartLocation());
throw ex;
}
}
}
case COMPREHENSION:
return evalComprehension(fr, (Comprehension) expr);
case CONDITIONAL:
{
ConditionalExpression cond = (ConditionalExpression) expr;
Object v = eval(fr, cond.getCondition());
return eval(fr, Starlark.truth(v) ? cond.getThenCase() : cond.getElseCase());
}
case DICT_EXPR:
{
DictExpression dictexpr = (DictExpression) expr;
Dict<Object, Object> dict = Dict.of(fr.thread.mutability());
for (DictExpression.Entry entry : dictexpr.getEntries()) {
Object k = eval(fr, entry.getKey());
Object v = eval(fr, entry.getValue());
int before = dict.size();
try {
dict.put(k, v, (Location) null);
} catch (EvalException ex) {
// TODO(adonovan): use colon location
throw ex.ensureLocation(entry.getKey().getStartLocation());
}
if (dict.size() == before) {
// TODO(adonovan): use colon location
throw new EvalException(
entry.getKey().getStartLocation(),
"Duplicated key " + Starlark.repr(k) + " when creating dictionary");
}
}
return dict;
}
case DOT:
{
DotExpression dot = (DotExpression) expr;
Object object = eval(fr, dot.getObject());
String name = dot.getField().getName();
try {
Object result = EvalUtils.getAttr(fr.thread, object, name);
if (result == null) {
throw EvalUtils.getMissingAttrException(object, name, fr.thread.getSemantics());
}
return result;
} catch (EvalException ex) {
throw ex.ensureLocation(dot.getStartLocation()); // TODO(adonovan): use dot token
}
}
case CALL:
{
fr.thread.checkInterrupt();
CallExpression call = (CallExpression) expr;
Object fn = eval(fr, call.getFunction());
// StarStar and Star args are guaranteed to be last, if they occur.
ImmutableList<Argument> arguments = call.getArguments();
int n = arguments.size();
Argument.StarStar starstar = null;
if (n > 0 && arguments.get(n - 1) instanceof Argument.StarStar) {
starstar = (Argument.StarStar) arguments.get(n - 1);
n--;
}
Argument.Star star = null;
if (n > 0 && arguments.get(n - 1) instanceof Argument.Star) {
star = (Argument.Star) arguments.get(n - 1);
n--;
}
// Inv: n = |positional| + |named|
// Allocate assuming no *args/**kwargs.
int npos = call.getNumPositionalArguments();
int i;
// f(expr) -- positional args
Object[] positional = npos == 0 ? EMPTY : new Object[npos];
for (i = 0; i < npos; i++) {
Argument arg = arguments.get(i);
Object value = eval(fr, arg.getValue());
positional[i] = value;
}
// f(id=expr) -- named args
Object[] named = n == npos ? EMPTY : new Object[2 * (n - npos)];
for (int j = 0; i < n; i++) {
Argument.Keyword arg = (Argument.Keyword) arguments.get(i);
Object value = eval(fr, arg.getValue());
named[j++] = arg.getName();
named[j++] = value;
}
// f(*args) -- varargs
if (star != null) {
Object value = eval(fr, star.getValue());
if (!(value instanceof StarlarkIterable)) {
throw new EvalException(
star.getStartLocation(),
"argument after * must be an iterable, not " + Starlark.type(value));
}
// TODO(adonovan): opt: if value.size is known, preallocate (and skip if empty).
ArrayList<Object> list = new ArrayList<>();
Collections.addAll(list, positional);
Iterables.addAll(list, ((Iterable<?>) value));
positional = list.toArray();
}
// f(**kwargs)
if (starstar != null) {
Object value = eval(fr, starstar.getValue());
if (!(value instanceof Dict)) {
throw new EvalException(
starstar.getStartLocation(),
"argument after ** must be a dict, not " + Starlark.type(value));
}
Dict<?, ?> kwargs = (Dict<?, ?>) value;
int j = named.length;
named = Arrays.copyOf(named, j + 2 * kwargs.size());
for (Map.Entry<?, ?> e : kwargs.entrySet()) {
if (!(e.getKey() instanceof String)) {
throw new EvalException(
starstar.getStartLocation(),
"keywords must be strings, not " + Starlark.type(e.getKey()));
}
named[j++] = e.getKey();
named[j++] = e.getValue();
}
}
Location loc = call.getStartLocation(); // TODO(adonovan): use call lparen
fr.setLocation(loc);
try {
return Starlark.fastcall(fr.thread, fn, positional, named);
} catch (EvalException ex) {
throw ex.ensureLocation(loc);
}
}
case IDENTIFIER:
{
Identifier id = (Identifier) expr;
String name = id.getName();
if (id.getScope() == null) {
// Legacy behavior, to be removed.
Object result = fr.locals.get(name);
if (result != null) {
return result;
}
result = fn(fr).getModule().get(name);
if (result != null) {
return result;
}
String error =
ValidationEnvironment.createInvalidIdentifierException(
name, fr.thread.getVariableNames());
throw new EvalException(id.getStartLocation(), error);
}
Object result;
switch (id.getScope()) {
case Local:
result = fr.locals.get(name);
break;
case Module:
result = fn(fr).getModule().lookup(name);
break;
case Universe:
// TODO(laurentlb): look only at universe.
result = fn(fr).getModule().get(name);
break;
default:
throw new IllegalStateException(id.getScope().toString());
}
if (result == null) {
// Since Scope was set, we know that the variable is defined in the scope.
// However, the assignment was not yet executed.
String error = ValidationEnvironment.getErrorForObsoleteThreadLocalVars(id.getName());
if (error == null) {
error =
id.getScope().getQualifier()
+ " variable '"
+ name
+ "' is referenced before assignment.";
}
throw new EvalException(id.getStartLocation(), error);
}
return result;
}
case INDEX:
{
IndexExpression index = (IndexExpression) expr;
Object object = eval(fr, index.getObject());
Object key = eval(fr, index.getKey());
try {
return EvalUtils.index(fr.thread.mutability(), fr.thread.getSemantics(), object, key);
} catch (EvalException ex) {
// TODO(adonovan): use location of lbracket token
throw ex.ensureLocation(index.getStartLocation());
}
}
case INTEGER_LITERAL:
return ((IntegerLiteral) expr).getValue();
case LIST_EXPR:
{
ListExpression list = (ListExpression) expr;
int n = list.getElements().size();
Object[] array = new Object[n];
for (int i = 0; i < n; i++) {
array[i] = eval(fr, list.getElements().get(i));
}
return list.isTuple()
? Tuple.wrap(array)
: StarlarkList.wrap(fr.thread.mutability(), array);
}
case SLICE:
{
SliceExpression slice = (SliceExpression) expr;
Object x = eval(fr, slice.getObject());
Object start = slice.getStart() == null ? Starlark.NONE : eval(fr, slice.getStart());
Object stop = slice.getStop() == null ? Starlark.NONE : eval(fr, slice.getStop());
Object step = slice.getStep() == null ? Starlark.NONE : eval(fr, slice.getStep());
try {
return Starlark.slice(fr.thread.mutability(), x, start, stop, step);
} catch (EvalException ex) {
// TODO(adonovan): use lbracket location
throw ex.ensureLocation(slice.getStartLocation());
}
}
case STRING_LITERAL:
return ((StringLiteral) expr).getValue();
case UNARY_OPERATOR:
{
UnaryOperatorExpression unop = (UnaryOperatorExpression) expr;
Object x = eval(fr, unop.getX());
try {
return EvalUtils.unaryOp(unop.getOperator(), x);
} catch (EvalException ex) {
throw ex.ensureLocation(unop.getStartLocation());
}
}
}
throw new IllegalArgumentException("unexpected expression: " + expr.kind());
}
private static Object evalComprehension(StarlarkThread.Frame fr, Comprehension comp)
throws EvalException, InterruptedException {
final Dict<Object, Object> dict = comp.isDict() ? Dict.of(fr.thread.mutability()) : null;
final ArrayList<Object> list = comp.isDict() ? null : new ArrayList<>();
// Save previous value (if any) of local variables bound in a 'for' clause
// so we can restore them later.
// TODO(adonovan): throw all this away when we implement flat environments.
List<Object> saved = new ArrayList<>(); // alternating keys and values
for (Comprehension.Clause clause : comp.getClauses()) {
if (clause instanceof Comprehension.For) {
for (Identifier ident :
Identifier.boundIdentifiers(((Comprehension.For) clause).getVars())) {
String name = ident.getName();
Object value = fr.locals.get(ident.getName()); // may be null
saved.add(name);
saved.add(value);
}
}
}
fr.compcount++;
// The Lambda class serves as a recursive lambda closure.
class Lambda {
// execClauses(index) recursively executes the clauses starting at index,
// and finally evaluates the body and adds its value to the result.
void execClauses(int index) throws EvalException, InterruptedException {
fr.thread.checkInterrupt();
// recursive case: one or more clauses
if (index < comp.getClauses().size()) {
Comprehension.Clause clause = comp.getClauses().get(index);
if (clause instanceof Comprehension.For) {
Comprehension.For forClause = (Comprehension.For) clause;
Object iterable = eval(fr, forClause.getIterable());
Location loc = comp.getStartLocation(); // TODO(adonovan): use location of 'for' token
Iterable<?> listValue = Starlark.toIterable(iterable);
// TODO(adonovan): lock should not need loc.
EvalUtils.lock(iterable, loc);
try {
for (Object elem : listValue) {
assign(fr, forClause.getVars(), elem);
execClauses(index + 1);
}
} catch (EvalException ex) {
throw ex.ensureLocation(loc);
} finally {
EvalUtils.unlock(iterable, loc);
}
} else {
Comprehension.If ifClause = (Comprehension.If) clause;
if (Starlark.truth(eval(fr, ifClause.getCondition()))) {
execClauses(index + 1);
}
}
return;
}
// base case: evaluate body and add to result.
if (dict != null) {
DictExpression.Entry body = (DictExpression.Entry) comp.getBody();
Object k = eval(fr, body.getKey());
EvalUtils.checkHashable(k);
Object v = eval(fr, body.getValue());
try {
dict.put(k, v, (Location) null);
} catch (EvalException ex) {
// TODO(adonovan): use colon location
throw ex.ensureLocation(comp.getStartLocation());
}
} else {
list.add(eval(fr, ((Expression) comp.getBody())));
}
}
}
new Lambda().execClauses(0);
fr.compcount--;
// Restore outer scope variables.
// This loop implicitly undefines comprehension variables.
for (int i = 0; i != saved.size(); ) {
String name = (String) saved.get(i++);
Object value = saved.get(i++);
if (value != null) {
fr.locals.put(name, value);
} else {
fr.locals.remove(name);
}
}
return comp.isDict() ? dict : StarlarkList.copyOf(fr.thread.mutability(), list);
}
private static final Object[] EMPTY = {};
/** Returns an exception which should be thrown instead of the original one. */
private static EvalException maybeTransformException(Node node, EvalException original) {
// TODO(adonovan): the only place that should be doing this is Starlark.fastcall,
// and it should grab the entire callstack from the thread at that moment.
// If there is already a non-empty stack trace, we only add this node iff it describes a
// new scope (e.g. CallExpression).
if (original instanceof EvalExceptionWithStackTrace) {
EvalExceptionWithStackTrace real = (EvalExceptionWithStackTrace) original;
if (node instanceof CallExpression) {
real.registerNode(node);
}
return real;
}
if (original.canBeAddedToStackTrace()) {
return new EvalExceptionWithStackTrace(original, node);
} else {
return original;
}
}
}