| // Copyright 2014 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.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.errorprone.annotations.FormatMethod; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import javax.annotation.Nullable; |
| import net.starlark.java.spelling.SpellChecker; |
| |
| /** |
| * The Resolver resolves each identifier in a syntax tree to its binding, and performs other |
| * validity checks. |
| * |
| * <p>When a variable is defined, it is visible in the entire block. For example, a global variable |
| * is visible in the entire file; a variable in a function is visible in the entire function block |
| * (even on the lines before its first assignment). |
| * |
| * <p>Resolution is a mutation of the syntax tree, as it attaches binding information to Identifier |
| * nodes. (In the future, it will attach additional information to functions to support lexical |
| * scope, and even compilation of the trees to bytecode.) Resolution errors are reported in the |
| * analogous manner to scan/parse errors: for a StarlarkFile, they are appended to {@code |
| * StarlarkFile.errors}; for an expression they are reported by an SyntaxError.Exception exception. |
| * It is legal to resolve a file that already contains scan/parse errors, though it may lead to |
| * secondary errors. |
| */ |
| public final class Resolver extends NodeVisitor { |
| |
| // TODO(adonovan): use "keyword" (not "named") and "required" (not "mandatory") terminology |
| // everywhere, including the spec. |
| |
| /** Scope discriminates the scope of a binding: global, local, etc. */ |
| public enum Scope { |
| // TODO(adonovan): Add UNIVERSAL, FREE, CELL. |
| // (PREDECLARED vs UNIVERSAL allows us to represent the app-dependent and fixed parts of the |
| // predeclared environment separately, reducing the amount of copying.) |
| |
| /** Binding is local to a function, comprehension, or file (e.g. load). */ |
| LOCAL, |
| /** Binding occurs outside any function or comprehension. */ |
| GLOBAL, |
| /** Binding is predeclared by the core or application. */ |
| PREDECLARED; |
| |
| @Override |
| public String toString() { |
| return super.toString().toLowerCase(); |
| } |
| } |
| |
| /** |
| * A Binding is a static abstraction of a variable. The Resolver maps each Identifier to a |
| * Binding. |
| */ |
| public static final class Binding { |
| |
| private final Scope scope; |
| @Nullable private final Identifier first; // first binding use, if syntactic |
| private final int index; // within its block (currently unused) |
| |
| private Binding(Scope scope, @Nullable Identifier first, int index) { |
| this.scope = scope; |
| this.first = first; |
| this.index = index; |
| } |
| |
| /** Returns the scope of the binding. */ |
| public Scope getScope() { |
| return scope; |
| } |
| |
| @Override |
| public String toString() { |
| return first == null |
| ? scope.toString() |
| : String.format( |
| "%s[%d] %s @ %s", scope, index, first.getName(), first.getStartLocation()); |
| } |
| } |
| |
| /** A Resolver.Function records information about a resolved function. */ |
| public static final class Function { |
| |
| private final String name; |
| private final Location location; |
| private final ImmutableList<Parameter> params; |
| private final ImmutableList<Statement> body; |
| private final boolean hasVarargs; |
| private final boolean hasKwargs; |
| private final int numKeywordOnlyParams; |
| private final ImmutableList<String> parameterNames; |
| private final boolean isToplevel; |
| |
| private Function( |
| String name, |
| Location loc, |
| ImmutableList<Parameter> params, |
| ImmutableList<Statement> body, |
| boolean hasVarargs, |
| boolean hasKwargs, |
| int numKeywordOnlyParams) { |
| this.name = name; |
| this.location = loc; |
| this.params = params; |
| this.body = body; |
| this.hasVarargs = hasVarargs; |
| this.hasKwargs = hasKwargs; |
| this.numKeywordOnlyParams = numKeywordOnlyParams; |
| |
| ImmutableList.Builder<String> names = ImmutableList.builderWithExpectedSize(params.size()); |
| for (Parameter p : params) { |
| names.add(p.getName()); |
| } |
| this.parameterNames = names.build(); |
| |
| this.isToplevel = name.equals("<toplevel>"); |
| } |
| |
| /** |
| * Returns the name of the function. It may be "<toplevel>" for the implicit function that holds |
| * the top-level statements of a file, or "<expr>" for the implicit function that evaluates a |
| * single expression. |
| */ |
| public String getName() { |
| return name; |
| } |
| |
| /** Returns the location of the function's identifier. */ |
| public Location getLocation() { |
| return location; |
| } |
| |
| /** |
| * Returns the function's parameters, in "run-time order": non-keyword-only parameters, |
| * keyword-only parameters, {@code *args}, and finally {@code **kwargs}. A bare {@code *} |
| * parameter is dropped. |
| */ |
| public ImmutableList<Parameter> getParameters() { |
| return params; |
| } |
| |
| /** |
| * Returns the effective statements of the function's body. (For the implicit function created |
| * to evaluate a single standalone expression, this may contain a synthesized Return statement.) |
| */ |
| // TODO(adonovan): eliminate when we switch to compiler. |
| public ImmutableList<Statement> getBody() { |
| return body; |
| } |
| |
| /** Reports whether the function has an {@code *args} parameter. */ |
| public boolean hasVarargs() { |
| return hasVarargs; |
| } |
| |
| /** Reports whether the function has a {@code **kwargs} parameter. */ |
| public boolean hasKwargs() { |
| return hasKwargs; |
| } |
| |
| /** |
| * Returns the number of the function's keyword-only parameters, such as {@code c} in {@code def |
| * f(a, *b, c, **d)} or {@code def f(a, *, c, **d)}. |
| */ |
| public int numKeywordOnlyParams() { |
| return numKeywordOnlyParams; |
| } |
| |
| /** Returns the names of the parameters. Order is as for {@link #getParameters}. */ |
| public ImmutableList<String> getParameterNames() { |
| return parameterNames; |
| } |
| |
| /** |
| * isToplevel indicates that this is the <toplevel> function containing top-level statements of |
| * a file. It causes assignments to unresolved identifiers to update the module, not the lexical |
| * frame. |
| */ |
| // TODO(adonovan): remove this hack when identifier resolution is accurate. |
| public boolean isToplevel() { |
| return isToplevel; |
| } |
| } |
| |
| /** |
| * Module is a static abstraction of a Starlark module. It describes the set of variable names for |
| * use during name resolution. |
| */ |
| public interface Module { |
| |
| // TODO(adonovan): opt: for efficiency, turn this into a predicate, not an enumerable set, |
| // and look up bindings as they are needed, not preemptively. |
| // Otherwise we must do work proportional to the number of bindings in the |
| // environment, not the number of free variables of the file/expression. |
| // |
| // A single method will then suffice: |
| // Scope resolve(String name) throws Undeclared |
| |
| /** Returns the set of names defined by this module. The caller must not modify the set. */ |
| Set<String> getNames(); |
| |
| /** |
| * Returns (optionally) a more specific error for an undeclared name than the generic message. |
| * This hook allows the module to implement flag-enabled names without any knowledge in this |
| * file. |
| */ |
| @Nullable |
| default String getUndeclaredNameError(String name) { |
| return null; |
| } |
| } |
| |
| private static class Block { |
| private final Map<String, Binding> bindings = new HashMap<>(); |
| private final Scope scope; |
| @Nullable private final Block parent; |
| |
| Block(Scope scope, @Nullable Block parent) { |
| this.scope = scope; |
| this.parent = parent; |
| } |
| } |
| |
| private final List<SyntaxError> errors; |
| private final FileOptions options; |
| private final Module module; |
| private Block block; |
| private int loopCount; |
| |
| // Shared binding for all predeclared names. |
| private static final Binding PREDECLARED = new Binding(Scope.PREDECLARED, null, 0); |
| |
| private Resolver(List<SyntaxError> errors, Module module, FileOptions options) { |
| this.errors = errors; |
| this.module = module; |
| this.options = options; |
| |
| this.block = new Block(Scope.PREDECLARED, null); |
| for (String name : module.getNames()) { |
| block.bindings.put(name, PREDECLARED); |
| } |
| } |
| |
| // Formats and reports an error at the start of the specified node. |
| @FormatMethod |
| private void errorf(Node node, String format, Object... args) { |
| errorf(node.getStartLocation(), format, args); |
| } |
| |
| // Formats and reports an error at the specified location. |
| @FormatMethod |
| private void errorf(Location loc, String format, Object... args) { |
| errors.add(new SyntaxError(loc, String.format(format, args))); |
| } |
| |
| /** |
| * First pass: add bindings for all variables to the current block. This is done because symbols |
| * are sometimes used before their definition point (e.g. functions are not necessarily declared |
| * in order). |
| */ |
| // TODO(adonovan): eliminate this first pass by using go.starlark.net one-pass approach. |
| private void createBindings(Iterable<Statement> stmts) { |
| for (Statement stmt : stmts) { |
| createBindings(stmt); |
| } |
| } |
| |
| private void createBindings(Statement stmt) { |
| switch (stmt.kind()) { |
| case ASSIGNMENT: |
| createBindings(((AssignmentStatement) stmt).getLHS()); |
| break; |
| case IF: |
| IfStatement ifStmt = (IfStatement) stmt; |
| createBindings(ifStmt.getThenBlock()); |
| if (ifStmt.getElseBlock() != null) { |
| createBindings(ifStmt.getElseBlock()); |
| } |
| break; |
| case FOR: |
| ForStatement forStmt = (ForStatement) stmt; |
| createBindings(forStmt.getVars()); |
| createBindings(forStmt.getBody()); |
| break; |
| case DEF: |
| DefStatement def = (DefStatement) stmt; |
| bind(def.getIdentifier()); |
| break; |
| case LOAD: |
| LoadStatement load = (LoadStatement) stmt; |
| Set<String> names = new HashSet<>(); |
| for (LoadStatement.Binding b : load.getBindings()) { |
| // Reject load('...', '_private'). |
| Identifier orig = b.getOriginalName(); |
| if (orig.isPrivate() && !options.allowLoadPrivateSymbols()) { |
| errorf(orig, "symbol '%s' is private and cannot be imported", orig.getName()); |
| } |
| |
| // The allowToplevelRebinding check is not applied to all files |
| // but we apply it to each load statement as a special case, |
| // and emit a better error message than the generic check. |
| if (!names.add(b.getLocalName().getName())) { |
| errorf( |
| b.getLocalName(), |
| "load statement defines '%s' more than once", |
| b.getLocalName().getName()); |
| } |
| } |
| |
| // TODO(adonovan): support options.loadBindsGlobally(). |
| // Requires that we open a LOCAL block for each file, |
| // as well as its Module block, and select which block |
| // to declare it in. See go.starlark.net implementation. |
| |
| for (LoadStatement.Binding b : load.getBindings()) { |
| bind(b.getLocalName()); |
| } |
| break; |
| case EXPRESSION: |
| case FLOW: |
| case RETURN: |
| // nothing to declare |
| } |
| } |
| |
| private void createBindings(Expression lhs) { |
| for (Identifier id : Identifier.boundIdentifiers(lhs)) { |
| bind(id); |
| } |
| } |
| |
| private void assign(Expression lhs) { |
| if (lhs instanceof Identifier) { |
| Identifier id = (Identifier) lhs; |
| // Bindings are created by the first pass (createBindings), |
| // so there's nothing to do here. |
| Preconditions.checkNotNull(block.bindings.get(id.getName())); |
| } else if (lhs instanceof IndexExpression) { |
| visit(lhs); |
| } else if (lhs instanceof ListExpression) { |
| for (Expression elem : ((ListExpression) lhs).getElements()) { |
| assign(elem); |
| } |
| } else { |
| // TODO(adonovan): support x.f = y. |
| errorf(lhs, "cannot assign to '%s'", lhs); |
| } |
| } |
| |
| @Override |
| public void visit(Identifier id) { |
| for (Block b = block; b != null; b = b.parent) { |
| Binding bind = b.bindings.get(id.getName()); |
| if (bind != null) { |
| if (options.recordScope()) { |
| id.setBinding(bind); |
| } |
| return; |
| } |
| } |
| |
| // The identifier might not exist because it was restricted (hidden) by flags. |
| // If this is the case, output a more helpful error message than 'not found'. |
| String error = module.getUndeclaredNameError(id.getName()); |
| if (error == null) { |
| // generic error |
| error = createInvalidIdentifierException(id.getName(), getAllSymbols()); |
| } |
| errorf(id, "%s", error); |
| } |
| |
| private static String createInvalidIdentifierException(String name, Set<String> candidates) { |
| if (!Identifier.isValid(name)) { |
| // Identifier was created by Parser.makeErrorExpression and contains misparsed text. |
| return "contains syntax errors"; |
| } |
| |
| String suggestion = SpellChecker.didYouMean(name, candidates); |
| return "name '" + name + "' is not defined" + suggestion; |
| } |
| |
| @Override |
| public void visit(ReturnStatement node) { |
| if (block.scope != Scope.LOCAL) { |
| errorf(node, "return statements must be inside a function"); |
| } |
| super.visit(node); |
| } |
| |
| @Override |
| public void visit(CallExpression node) { |
| // validate call arguments |
| boolean seenVarargs = false; |
| boolean seenKwargs = false; |
| Set<String> keywords = null; |
| for (Argument arg : node.getArguments()) { |
| if (arg instanceof Argument.Positional) { |
| if (seenVarargs) { |
| errorf(arg, "positional argument may not follow *args"); |
| } else if (seenKwargs) { |
| errorf(arg, "positional argument may not follow **kwargs"); |
| } else if (keywords != null) { |
| errorf(arg, "positional argument may not follow keyword argument"); |
| } |
| |
| } else if (arg instanceof Argument.Keyword) { |
| String keyword = ((Argument.Keyword) arg).getName(); |
| if (seenVarargs) { |
| errorf(arg, "keyword argument %s may not follow *args", keyword); |
| } else if (seenKwargs) { |
| errorf(arg, "keyword argument %s may not follow **kwargs", keyword); |
| } |
| if (keywords == null) { |
| keywords = new HashSet<>(); |
| } |
| if (!keywords.add(keyword)) { |
| errorf(arg, "duplicate keyword argument: %s", keyword); |
| } |
| |
| } else if (arg instanceof Argument.Star) { |
| if (seenKwargs) { |
| errorf(arg, "*args may not follow **kwargs"); |
| } else if (seenVarargs) { |
| errorf(arg, "multiple *args not allowed"); |
| } |
| seenVarargs = true; |
| |
| } else if (arg instanceof Argument.StarStar) { |
| if (seenKwargs) { |
| errorf(arg, "multiple **kwargs not allowed"); |
| } |
| seenKwargs = true; |
| } |
| } |
| |
| super.visit(node); |
| } |
| |
| @Override |
| public void visit(ForStatement node) { |
| if (block.scope != Scope.LOCAL) { |
| errorf( |
| node, |
| "for loops are not allowed at the top level. You may move it inside a function " |
| + "or use a comprehension, [f(x) for x in sequence]"); |
| } |
| loopCount++; |
| visit(node.getCollection()); |
| assign(node.getVars()); |
| visitBlock(node.getBody()); |
| Preconditions.checkState(loopCount > 0); |
| loopCount--; |
| } |
| |
| @Override |
| public void visit(LoadStatement node) { |
| if (block.scope == Scope.LOCAL) { |
| errorf(node, "load statement not at top level"); |
| } |
| // Skip super.visit: don't revisit local Identifier as a use. |
| } |
| |
| @Override |
| public void visit(FlowStatement node) { |
| if (node.getKind() != TokenKind.PASS && loopCount <= 0) { |
| errorf(node, "%s statement must be inside a for loop", node.getKind()); |
| } |
| super.visit(node); |
| } |
| |
| @Override |
| public void visit(DotExpression node) { |
| visit(node.getObject()); |
| // Do not visit the field. |
| } |
| |
| @Override |
| public void visit(Comprehension node) { |
| ImmutableList<Comprehension.Clause> clauses = node.getClauses(); |
| |
| // Following Python3, the first for clause is resolved |
| // outside the comprehension block. All the other loops |
| // are resolved in the scope of their own bindings, |
| // permitting forward references. |
| Comprehension.For for0 = (Comprehension.For) clauses.get(0); |
| visit(for0.getIterable()); |
| |
| openBlock(Scope.LOCAL); |
| for (Comprehension.Clause clause : clauses) { |
| if (clause instanceof Comprehension.For) { |
| Comprehension.For forClause = (Comprehension.For) clause; |
| createBindings(forClause.getVars()); |
| } |
| } |
| for (int i = 0; i < clauses.size(); i++) { |
| Comprehension.Clause clause = clauses.get(i); |
| if (clause instanceof Comprehension.For) { |
| Comprehension.For forClause = (Comprehension.For) clause; |
| if (i > 0) { |
| visit(forClause.getIterable()); |
| } |
| assign(forClause.getVars()); |
| } else { |
| Comprehension.If ifClause = (Comprehension.If) clause; |
| visit(ifClause.getCondition()); |
| } |
| } |
| visit(node.getBody()); |
| closeBlock(); |
| } |
| |
| @Override |
| public void visit(DefStatement node) { |
| if (block.scope == Scope.LOCAL) { |
| errorf(node, "nested functions are not allowed. Move the function to the top level."); |
| } |
| node.setResolvedFunction( |
| resolveFunction( |
| node.getIdentifier().getName(), |
| node.getIdentifier().getStartLocation(), |
| node.getParameters(), |
| node.getBody())); |
| } |
| |
| private Function resolveFunction( |
| String name, |
| Location loc, |
| ImmutableList<Parameter> parameters, |
| ImmutableList<Statement> body) { |
| |
| // Resolve defaults in enclosing environment. |
| for (Parameter param : parameters) { |
| if (param instanceof Parameter.Optional) { |
| visit(param.getDefaultValue()); |
| } |
| } |
| |
| // Enter function block. |
| openBlock(Scope.LOCAL); |
| |
| // Check parameter order and convert to run-time order: |
| // positionals, keyword-only, *args, **kwargs. |
| Parameter.Star star = null; |
| Parameter.StarStar starStar = null; |
| boolean seenOptional = false; |
| int numKeywordOnlyParams = 0; |
| // TODO(adonovan): opt: when all Identifiers are resolved to bindings accumulated |
| // in the function, params can be a prefix of the function's array of bindings. |
| ImmutableList.Builder<Parameter> params = |
| ImmutableList.builderWithExpectedSize(parameters.size()); |
| for (Parameter param : parameters) { |
| if (param instanceof Parameter.Mandatory) { |
| // e.g. id |
| if (starStar != null) { |
| errorf( |
| param, |
| "required parameter %s may not follow **%s", |
| param.getName(), |
| starStar.getName()); |
| } else if (star != null) { |
| numKeywordOnlyParams++; |
| } else if (seenOptional) { |
| errorf( |
| param, |
| "required positional parameter %s may not follow an optional parameter", |
| param.getName()); |
| } |
| bindParam(params, param); |
| |
| } else if (param instanceof Parameter.Optional) { |
| // e.g. id = default |
| seenOptional = true; |
| if (starStar != null) { |
| errorf(param, "optional parameter may not follow **%s", starStar.getName()); |
| } else if (star != null) { |
| numKeywordOnlyParams++; |
| } |
| bindParam(params, param); |
| |
| } else if (param instanceof Parameter.Star) { |
| // * or *args |
| if (starStar != null) { |
| errorf(param, "* parameter may not follow **%s", starStar.getName()); |
| } else if (star != null) { |
| errorf(param, "multiple * parameters not allowed"); |
| } else { |
| star = (Parameter.Star) param; |
| } |
| |
| } else { |
| // **kwargs |
| if (starStar != null) { |
| errorf(param, "multiple ** parameters not allowed"); |
| } |
| starStar = (Parameter.StarStar) param; |
| } |
| } |
| |
| // * or *args |
| if (star != null) { |
| if (star.getIdentifier() != null) { |
| bindParam(params, star); |
| } else if (numKeywordOnlyParams == 0) { |
| errorf(star, "bare * must be followed by keyword-only parameters"); |
| } |
| } |
| |
| // **kwargs |
| if (starStar != null) { |
| bindParam(params, starStar); |
| } |
| |
| createBindings(body); |
| visitAll(body); |
| closeBlock(); |
| |
| return new Function( |
| name, |
| loc, |
| params.build(), |
| body, |
| star != null && star.getIdentifier() != null, |
| starStar != null, |
| numKeywordOnlyParams); |
| } |
| |
| private void bindParam(ImmutableList.Builder<Parameter> params, Parameter param) { |
| if (bind(param.getIdentifier())) { |
| errorf(param, "duplicate parameter: %s", param.getName()); |
| } |
| params.add(param); |
| } |
| |
| @Override |
| public void visit(IfStatement node) { |
| if (block.scope != Scope.LOCAL) { |
| errorf( |
| node, |
| "if statements are not allowed at the top level. You may move it inside a function " |
| + "or use an if expression (x if condition else y)."); |
| } |
| super.visit(node); |
| } |
| |
| @Override |
| public void visit(AssignmentStatement node) { |
| visit(node.getRHS()); |
| |
| // Disallow: [e, ...] += rhs |
| // Other bad cases are handled in assign. |
| if (node.isAugmented() && node.getLHS() instanceof ListExpression) { |
| errorf( |
| node.getOperatorLocation(), |
| "cannot perform augmented assignment on a list or tuple expression"); |
| } |
| |
| assign(node.getLHS()); |
| } |
| |
| /** |
| * Process a binding use of a name by adding a binding to the current block if not already bound, |
| * and associate the identifier with it. Reports whether the name was already bound in this block. |
| */ |
| private boolean bind(Identifier id) { |
| Binding bind = block.bindings.get(id.getName()); |
| |
| // Already bound in this block? |
| if (bind != null) { |
| // Symbols defined in the module block cannot be reassigned. |
| if (block.scope == Scope.GLOBAL && !options.allowToplevelRebinding()) { |
| errorf( |
| id, |
| "cannot reassign global '%s' (read more at" |
| + " https://bazel.build/versions/master/docs/skylark/errors/read-only-variable.html)", |
| id.getName()); |
| if (bind.first != null) { |
| errorf(bind.first, "'%s' previously declared here", id.getName()); |
| } |
| } |
| |
| if (options.recordScope()) { |
| id.setBinding(bind); |
| } |
| return true; |
| } |
| |
| // new binding |
| // TODO(adonovan): accumulate locals in the enclosing function/file block. |
| bind = new Binding(block.scope, id, block.bindings.size()); |
| block.bindings.put(id.getName(), bind); |
| if (options.recordScope()) { |
| id.setBinding(bind); |
| } |
| return false; |
| } |
| |
| /** Returns the set of all accessible symbols (both local and global) */ |
| private Set<String> getAllSymbols() { |
| Set<String> all = new HashSet<>(); |
| for (Block b = block; b != null; b = b.parent) { |
| all.addAll(b.bindings.keySet()); |
| } |
| return all; |
| } |
| |
| // Report an error if a load statement appears after another kind of statement. |
| private void checkLoadAfterStatement(List<Statement> statements) { |
| Statement firstStatement = null; |
| |
| for (Statement statement : statements) { |
| // Ignore string literals (e.g. docstrings). |
| if (statement instanceof ExpressionStatement |
| && ((ExpressionStatement) statement).getExpression() instanceof StringLiteral) { |
| continue; |
| } |
| |
| if (statement instanceof LoadStatement) { |
| if (firstStatement == null) { |
| continue; |
| } |
| errorf(statement, "load statements must appear before any other statement"); |
| errorf(firstStatement, "\tfirst non-load statement appears here"); |
| } |
| |
| if (firstStatement == null) { |
| firstStatement = statement; |
| } |
| } |
| } |
| |
| private void resolveToplevelStatements(List<Statement> statements) { |
| // Check that load() statements are on top. |
| if (options.requireLoadStatementsFirst()) { |
| checkLoadAfterStatement(statements); |
| } |
| |
| openBlock(Scope.GLOBAL); |
| |
| // Add a binding for each variable defined by statements, not including definitions that appear |
| // in sub-scopes of the given statements (function bodies and comprehensions). |
| createBindings(statements); |
| |
| // Second pass: ensure that all symbols have been defined. |
| visitAll(statements); |
| closeBlock(); |
| } |
| |
| /** |
| * Performs static checks, including resolution of identifiers in {@code file} in the environment |
| * defined by {@code module}. The StarlarkFile is mutated. Errors are appended to {@link |
| * StarlarkFile#errors}. |
| */ |
| public static void resolveFile(StarlarkFile file, Module module) { |
| ImmutableList<Statement> stmts = file.getStatements(); |
| |
| Resolver r = new Resolver(file.errors, module, file.getOptions()); |
| r.resolveToplevelStatements(stmts); |
| // Check that no closeBlock was forgotten. |
| Preconditions.checkState(r.block.parent == null); |
| |
| // If the final statement is an expression, synthesize a return statement. |
| int n = stmts.size(); |
| if (n > 0 && stmts.get(n - 1) instanceof ExpressionStatement) { |
| Expression expr = ((ExpressionStatement) stmts.get(n - 1)).getExpression(); |
| stmts = |
| ImmutableList.<Statement>builder() |
| .addAll(stmts.subList(0, n - 1)) |
| .add(ReturnStatement.make(expr)) |
| .build(); |
| } |
| |
| // Annotate with resolved information about the toplevel function. |
| file.setResolvedFunction( |
| new Function( |
| "<toplevel>", |
| file.getStartLocation(), |
| /*params=*/ ImmutableList.of(), |
| /*body=*/ stmts, |
| /*hasVarargs=*/ false, |
| /*hasKwargs=*/ false, |
| /*numKeywordOnlyParams=*/ 0)); |
| } |
| |
| /** |
| * Performs static checks, including resolution of identifiers in {@code expr} in the environment |
| * defined by {@code module}. This operation mutates the Expression. Syntax must be resolved |
| * before it is evaluated. |
| */ |
| public static Function resolveExpr(Expression expr, Module module, FileOptions options) |
| throws SyntaxError.Exception { |
| List<SyntaxError> errors = new ArrayList<>(); |
| Resolver r = new Resolver(errors, module, options); |
| |
| r.visit(expr); |
| |
| if (!errors.isEmpty()) { |
| throw new SyntaxError.Exception(errors); |
| } |
| |
| // Return no-arg function that computes the expression. |
| return new Function( |
| "<expr>", |
| expr.getStartLocation(), |
| /*params=*/ ImmutableList.of(), |
| ImmutableList.of(ReturnStatement.make(expr)), |
| /*hasVarargs=*/ false, |
| /*hasKwargs=*/ false, |
| /*numKeywordOnlyParams=*/ 0); |
| } |
| |
| /** Open a new lexical block that will contain the future declarations. */ |
| private void openBlock(Scope scope) { |
| block = new Block(scope, block); |
| } |
| |
| /** Close a lexical block (and lose all declarations it contained). */ |
| private void closeBlock() { |
| block = Preconditions.checkNotNull(block.parent); |
| } |
| |
| } |