View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jexl3.internal;
18  
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.LinkedHashMap;
22  import java.util.List;
23  import java.util.Map;
24  
25  /**
26   * A script scope, stores the declaration of parameters and local variables as symbols.
27   * <p>This also acts as the functional scope and variable definition store.</p>
28   * @since 3.0
29   */
30  public final class Scope {
31      /**
32       * The value of an as-yet  undeclared but variable, for instance: x; before var x;.
33       */
34      static final Object UNDECLARED = new Object() {
35          @Override public String toString() {
36              return "??";
37          }
38      };
39      /**
40       * The value of a declared but undefined variable, for instance: var x;.
41       */
42      static final Object UNDEFINED = new Object() {
43          @Override public String toString() {
44              return "?";
45          }
46      };
47      /**
48       * The empty string array.
49       */
50      private static final String[] EMPTY_STRS = {};
51      /**
52       * The parent scope.
53       */
54      private final Scope parent;
55      /**
56       * The number of parameters.
57       */
58      private int parms;
59      /**
60       * The number of local variables.
61       */
62      private int vars;
63      /**
64       * The map of named variables aka script parameters and local variables.
65       * Each parameter is associated to a symbol and is materialized as an offset in the stacked array used
66       * during evaluation.
67       */
68      private Map<String, Integer> namedVariables;
69      /**
70       * The map of local captured variables to parent scope variables, ie closure.
71       */
72      private Map<Integer, Integer> capturedVariables;
73  
74      /**
75       * Let symbols.
76       */
77      private LexicalScope lexicalVariables;
78  
79      /**
80       * Creates a new scope with a list of parameters.
81       * @param scope the parent scope if any
82       * @param parameters the list of parameters
83       */
84      public Scope(final Scope scope, final String... parameters) {
85          if (parameters != null) {
86              parms = parameters.length;
87              namedVariables = new LinkedHashMap<>();
88              for (int p = 0; p < parms; ++p) {
89                  namedVariables.put(parameters[p], p);
90              }
91          } else {
92              parms = 0;
93          }
94          vars = 0;
95          parent = scope;
96      }
97  
98      /**
99       * Marks a symbol as a lexical, declared through let or const.
100      * @param s the symbol
101      * @return true if the symbol was not already present in the lexical set
102      */
103     public boolean addLexical(final int s) {
104         if (lexicalVariables == null) {
105             lexicalVariables = new LexicalScope();
106         }
107         return lexicalVariables.addSymbol(s);
108     }
109 
110     /**
111      * Creates a frame by copying values up to the number of parameters.
112      * <p>This captures the captured variables values.</p>
113      * @param frame the caller frame
114      * @param args the arguments
115      * @return the arguments array
116      */
117     public Frame createFrame(final Frame frame, final Object...args) {
118         if (namedVariables == null) {
119             return null;
120         }
121         final Object[] arguments = new Object[namedVariables.size()];
122         Arrays.fill(arguments, UNDECLARED);
123         if (frame != null && capturedVariables != null && parent != null) {
124             for (final Map.Entry<Integer, Integer> capture : capturedVariables.entrySet()) {
125                 final Integer target = capture.getKey();
126                 final Integer source = capture.getValue();
127                 final Object arg = frame.get(source);
128                 arguments[target] = arg;
129             }
130         }
131         return new Frame(this, arguments, 0).assign(args);
132     }
133 
134     /**
135      * Declares a parameter.
136      * <p>
137      * This method creates an new entry in the symbol map.
138      * </p>
139      * @param name the parameter name
140      * @return the register index storing this variable
141      */
142     public int declareParameter(final String name) {
143         if (namedVariables == null) {
144             namedVariables = new LinkedHashMap<>();
145         } else if (vars > 0) {
146             throw new IllegalStateException("cant declare parameters after variables");
147         }
148         Integer register = namedVariables.get(name);
149         if (register == null) {
150             register = namedVariables.size();
151             namedVariables.put(name, register);
152             parms += 1;
153         }
154         return register;
155     }
156 
157     /**
158      * Declares a local variable.
159      * <p>
160      * This method creates an new entry in the symbol map.
161      * </p>
162      * @param name the variable name
163      * @return the register index storing this variable
164      */
165     public int declareVariable(final String name) {
166         if (namedVariables == null) {
167             namedVariables = new LinkedHashMap<>();
168         }
169         Integer register = namedVariables.get(name);
170         if (register == null) {
171             register = namedVariables.size();
172             namedVariables.put(name, register);
173             vars += 1;
174             // check if local is redefining captured
175             if (parent != null) {
176                 final Integer pr = parent.getSymbol(name, true);
177                 if (pr != null) {
178                     if (capturedVariables == null) {
179                         capturedVariables = new LinkedHashMap<>();
180                     }
181                     capturedVariables.put(register, pr);
182                 }
183             }
184         }
185         return register;
186     }
187 
188     /**
189      * Gets the (maximum) number of arguments this script expects.
190      * @return the number of parameters
191      */
192     public int getArgCount() {
193         return parms;
194     }
195 
196     /**
197      * Gets the captured index of a given symbol, ie the target index of a symbol in a child scope.
198      * @param symbol the symbol index
199      * @return the target symbol index or null if the symbol is not captured
200      */
201     public Integer getCaptured(final int symbol) {
202         if (capturedVariables != null) {
203             for (final Map.Entry<Integer, Integer> capture : capturedVariables.entrySet()) {
204                 final Integer source = capture.getValue();
205                 if (source == symbol) {
206                     return capture.getKey();
207                 }
208             }
209         }
210         return null;
211     }
212 
213     /**
214      * Gets the index of a captured symbol, ie the source index of a symbol in a parent scope.
215      * @param symbol the symbol index
216      * @return the source symbol index or -1 if the symbol is not captured
217      */
218     public int getCaptureDeclaration(final int symbol) {
219         final Integer declared = capturedVariables != null ? capturedVariables.get(symbol)  : null;
220         return declared != null ? declared.intValue() : -1;
221     }
222 
223     /**
224      * Gets this script captured symbols names, i.e. local variables defined in outer scopes and used
225      * by this scope.
226      * @return the captured names
227      */
228     public String[] getCapturedVariables() {
229         if (capturedVariables != null) {
230             final List<String> captured = new ArrayList<>(vars);
231             for (final Map.Entry<String, Integer> entry : namedVariables.entrySet()) {
232                 final int symnum = entry.getValue();
233                 if (symnum >= parms && capturedVariables.containsKey(symnum)) {
234                     captured.add(entry.getKey());
235                 }
236             }
237             if (!captured.isEmpty()) {
238                 return captured.toArray(new String[0]);
239             }
240         }
241         return EMPTY_STRS;
242     }
243 
244     /**
245      * Gets this script local variable, i.e. symbols assigned to local variables excluding captured variables.
246      * @return the local variable names
247      */
248     public String[] getLocalVariables() {
249         if (namedVariables == null || vars <= 0) {
250             return EMPTY_STRS;
251         }
252         final List<String> locals = new ArrayList<>(vars);
253         for (final Map.Entry<String, Integer> entry : namedVariables.entrySet()) {
254             final int symnum = entry.getValue();
255             if (symnum >= parms && (capturedVariables == null || !capturedVariables.containsKey(symnum))) {
256                 locals.add(entry.getKey());
257             }
258         }
259         return locals.toArray(new String[0]);
260     }
261 
262     /**
263      * Gets this script parameters, i.e. symbols assigned before creating local variables.
264      * @return the parameter names
265      */
266     public String[] getParameters() {
267         return getParameters(0);
268     }
269 
270     /**
271      * Gets this script parameters.
272      * @param bound number of known bound parameters (curry)
273      * @return the parameter names
274      */
275      String[] getParameters(final int bound) {
276         final int unbound = parms - bound;
277         if (namedVariables == null || unbound <= 0) {
278             return EMPTY_STRS;
279         }
280         final String[] pa = new String[unbound];
281         int p = 0;
282         for (final Map.Entry<String, Integer> entry : namedVariables.entrySet()) {
283             final int argn = entry.getValue();
284             if (argn >= bound && argn < parms) {
285                 pa[p++] = entry.getKey();
286             }
287         }
288         return pa;
289     }
290 
291     Scope getParent() {
292         return parent;
293     }
294 
295     /**
296      * Checks whether an identifier is a local variable or argument, ie a symbol.
297      * If this fails, look in parents for symbol that can be captured.
298      * @param name the symbol name
299      * @return the symbol index
300      */
301     public Integer getSymbol(final String name) {
302         return getSymbol(name, true);
303     }
304 
305     /**
306      * Checks whether an identifier is a local variable or argument, ie a symbol.
307      * @param name the symbol name
308      * @param capture whether solving by capturing a parent symbol is allowed
309      * @return the symbol index
310      */
311     private Integer getSymbol(final String name, final boolean capture) {
312         Integer register = namedVariables != null ? namedVariables.get(name) : null;
313         if (register == null && capture && parent != null) {
314             final Integer pr = parent.getSymbol(name, true);
315             if (pr != null) {
316                 if (capturedVariables == null) {
317                     capturedVariables = new LinkedHashMap<>();
318                 }
319                 if (namedVariables == null) {
320                     namedVariables = new LinkedHashMap<>();
321                 }
322                 register = namedVariables.size();
323                 namedVariables.put(name, register);
324                 capturedVariables.put(register, pr);
325             }
326         }
327         return register;
328     }
329 
330     /**
331      * Gets this script symbols names, i.e. parameters and local variables.
332      * @return the symbol names
333      */
334     public String[] getSymbols() {
335         return namedVariables != null ? namedVariables.keySet().toArray(new String[0]) : EMPTY_STRS;
336     }
337 
338     /**
339      * Checks whether a given symbol is captured.
340      * @param symbol the symbol number
341      * @return true if captured, false otherwise
342      */
343     public boolean isCapturedSymbol(final int symbol) {
344         return capturedVariables != null && capturedVariables.containsKey(symbol);
345     }
346 
347     /**
348      * Checks whether a symbol is declared through a let or const.
349      * @param s the symbol
350      * @return true if symbol was declared through let or const
351      */
352     public boolean isLexical(final int s) {
353         return lexicalVariables != null && s >= 0 && lexicalVariables.hasSymbol(s);
354     }
355 }