aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
blob: 6afb96a0fb276ef54d031c16f0858b2739f18310 (plain)
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
// 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;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.collect.CollectionUtils;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.syntax.SkylarkType.SkylarkFunctionType;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

/**
 * An Environment for the semantic checking of Skylark files.
 *
 * @see Statement#validate
 * @see Expression#validate
 */
public class ValidationEnvironment {

  private final ValidationEnvironment parent;

  private Map<SkylarkType, Map<String, SkylarkType>> variableTypes = new HashMap<>();

  private Map<String, Location> variableLocations = new HashMap<>();

  private Set<String> readOnlyVariables = new HashSet<>();

  // A stack of variable-sets which are read only but can be assigned in different
  // branches of if-else statements.
  private Stack<Set<String>> futureReadOnlyVariables = new Stack<>();

  // The function we are currently validating.
  private SkylarkFunctionType currentFunction;

  // Whether this validation environment is not modified therefore clonable or not.
  private boolean clonable;

  public ValidationEnvironment(
      ImmutableMap<SkylarkType, ImmutableMap<String, SkylarkType>> builtinVariableTypes) {
    parent = null;
    variableTypes = CollectionUtils.copyOf(builtinVariableTypes);
    readOnlyVariables.addAll(builtinVariableTypes.get(SkylarkType.GLOBAL).keySet());
    clonable = true;
  }

  private ValidationEnvironment(Map<SkylarkType, Map<String, SkylarkType>> builtinVariableTypes,
      Set<String> readOnlyVariables) {
    parent = null;
    this.variableTypes = CollectionUtils.copyOf(builtinVariableTypes);
    this.readOnlyVariables = new HashSet<>(readOnlyVariables);
    clonable = false;
  }

  @Override
  public ValidationEnvironment clone() {
    Preconditions.checkState(clonable);
    return new ValidationEnvironment(variableTypes, readOnlyVariables);
  }

  /**
   * Creates a local ValidationEnvironment to validate user defined function bodies.
   */
  public ValidationEnvironment(ValidationEnvironment parent, SkylarkFunctionType currentFunction) {
    this.parent = parent;
    this.variableTypes.put(SkylarkType.GLOBAL, new HashMap<String, SkylarkType>());
    this.currentFunction = currentFunction;
    for (String var : parent.readOnlyVariables) {
      if (!parent.variableLocations.containsKey(var)) {
        // Mark built in global vars readonly. Variables defined in Skylark may be shadowed locally.
        readOnlyVariables.add(var);
      }
    }
    this.clonable = false;
  }

  /**
   * Returns true if this ValidationEnvironment is top level i.e. has no parent.
   */
  public boolean isTopLevel() {
    return parent == null;
  }

  /**
   * Updates the variable type if the new type is "stronger" then the old one.
   * The old and the new vartype has to be compatible, otherwise an EvalException is thrown.
   * The new type is stronger if the old one doesn't exist or unknown.
   */
  public void update(String varname, SkylarkType newVartype, Location location)
      throws EvalException {
    checkReadonly(varname, location);
    if (parent == null) {  // top-level values are immutable
      readOnlyVariables.add(varname);
      if (!futureReadOnlyVariables.isEmpty()) {
        // Currently validating an if-else statement
        futureReadOnlyVariables.peek().add(varname);
      }
    }
    SkylarkType oldVartype = variableTypes.get(SkylarkType.GLOBAL).get(varname);
    if (oldVartype != null) {
      newVartype = oldVartype.infer(newVartype, "variable '" + varname + "'",
          location, variableLocations.get(varname));
    }
    variableTypes.get(SkylarkType.GLOBAL).put(varname, newVartype);
    variableLocations.put(varname, location);
    clonable = false;
  }

  private void checkReadonly(String varname, Location location) throws EvalException {
    if (readOnlyVariables.contains(varname)) {
      throw new EvalException(location, String.format("Variable %s is read only", varname));
    }
  }

  public void checkIterable(SkylarkType type, Location loc) throws EvalException {
    if (type == SkylarkType.UNKNOWN) {
      // Until all the language is properly typed, we ignore Object types.
      return;
    }
    if (!Iterable.class.isAssignableFrom(type.getType())
        && !Map.class.isAssignableFrom(type.getType())
        && !String.class.equals(type.getType())) {
      throw new EvalException(loc,
          "type '" + EvalUtils.getDataTypeNameFromClass(type.getType()) + "' is not iterable");
    }
  }

  /**
   * Returns true if the symbol exists in the validation environment.
   */
  public boolean hasSymbolInEnvironment(String varname) {
    return variableTypes.get(SkylarkType.GLOBAL).containsKey(varname)
        || topLevel().variableTypes.get(SkylarkType.GLOBAL).containsKey(varname);
  }

  /**
   * Returns the type of the existing variable.
   */
  public SkylarkType getVartype(String varname) {
    SkylarkType type = variableTypes.get(SkylarkType.GLOBAL).get(varname);
    if (type == null && parent != null) {
      type = parent.getVartype(varname);
    }
    return Preconditions.checkNotNull(type,
        String.format("Variable %s is not found in the validation environment", varname));
  }

  public SkylarkFunctionType getCurrentFunction() {
    return currentFunction;
  }

  /**
   * Returns the return type of the function.
   */
  public SkylarkType getReturnType(String funcName, Location loc) throws EvalException {
    return getReturnType(SkylarkType.GLOBAL, funcName, loc);
  }

  /**
   * Returns the return type of the object function.
   */
  public SkylarkType getReturnType(SkylarkType objectType, String funcName, Location loc)
      throws EvalException {
    // All functions are registered in the top level ValidationEnvironment.
    Map<String, SkylarkType> functions = topLevel().variableTypes.get(objectType);
    // TODO(bazel-team): eventually not finding the return type should be a validation error,
    // because it means the function doesn't exist. First we have to make sure that we register
    // every possible function before.
    if (functions != null) {
      SkylarkType functionType = functions.get(funcName);
      if (functionType != null && functionType != SkylarkType.UNKNOWN) {
        if (!(functionType instanceof SkylarkFunctionType)) {
          throw new EvalException(loc, (objectType == SkylarkType.GLOBAL ? "" : objectType + ".")
              + funcName + " is not a function");
        }
        return ((SkylarkFunctionType) functionType).getReturnType();
      }
    }
    return SkylarkType.UNKNOWN;
  }

  private ValidationEnvironment topLevel() {
    return Preconditions.checkNotNull(parent == null ? this : parent);
  }

  /**
   * Adds a user defined function to the validation environment is not exists.
   */
  public void updateFunction(String name, SkylarkFunctionType type, Location loc)
      throws EvalException {
    checkReadonly(name, loc);
    if (variableTypes.get(SkylarkType.GLOBAL).containsKey(name)) {
      throw new EvalException(loc, "function " + name + " already exists");
    }
    variableTypes.get(SkylarkType.GLOBAL).put(name, type);
    clonable = false;
  }

  /**
   * Starts a session with temporarily disabled readonly checking for variables between branches.
   * This is useful to validate control flows like if-else when we know that certain parts of the
   * code cannot both be executed. 
   */
  public void startTemporarilyDisableReadonlyCheckSession() {
    futureReadOnlyVariables.add(new HashSet<String>());
    clonable = false;
  }

  /**
   * Finishes the session with temporarily disabled readonly checking.
   */
  public void finishTemporarilyDisableReadonlyCheckSession() {
    Set<String> variables = futureReadOnlyVariables.pop();
    readOnlyVariables.addAll(variables);
    if (!futureReadOnlyVariables.isEmpty()) {
      futureReadOnlyVariables.peek().addAll(variables);
    }
    clonable = false;
  }

  /**
   * Finishes a branch of temporarily disabled readonly checking.
   */
  public void finishTemporarilyDisableReadonlyCheckBranch() {
    readOnlyVariables.removeAll(futureReadOnlyVariables.peek());
    clonable = false;
  }
}