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       * Let symbols.
75       */
76      private LexicalScope lexicalVariables;
77  
78      /**
79       * Creates a new scope with a list of parameters.
80       * @param scope the parent scope if any
81       * @param parameters the list of parameters
82       */
83      public Scope(final Scope scope, final String... parameters) {
84          if (parameters != null) {
85              parms = parameters.length;
86              namedVariables = new LinkedHashMap<>();
87              for (int p = 0; p < parms; ++p) {
88                  namedVariables.put(parameters[p], p);
89              }
90          } else {
91              parms = 0;
92          }
93          vars = 0;
94          parent = scope;
95      }
96  
97      /**
98       * Marks a symbol as a lexical, declared through let or const.
99       * @param s the symbol
100      * @return true if the symbol was not already present in the lexical set
101      */
102     public boolean addLexical(final int s) {
103         if (lexicalVariables == null) {
104             lexicalVariables = new LexicalScope();
105         }
106         return lexicalVariables.addSymbol(s);
107     }
108 
109     /**
110      * Creates a frame by copying values up to the number of parameters.
111      * <p>This captures the captured variables values.</p>
112      * @param frame the caller frame
113      * @param args the arguments
114      * @return the arguments array
115      */
116     public Frame createFrame(final boolean ref, final Frame frame, final Object...args) {
117         if (namedVariables == null) {
118             return null;
119         }
120         final Object[] arguments = new Object[namedVariables.size()];
121         Arrays.fill(arguments, UNDECLARED);
122         final Frame newFrame;
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.capture(source);
128                 arguments[target] = arg;
129             }
130             newFrame = frame.newFrame(this, arguments, 0);
131         } else {
132             newFrame = ref
133             ? new ReferenceFrame(this, arguments, 0)
134             : new Frame(this, arguments, 0);
135         }
136         return newFrame.assign(args);
137     }
138 
139     /**
140      * Declares a parameter.
141      * <p>
142      * This method creates an new entry in the symbol map.
143      * </p>
144      * @param param the parameter name
145      * @return the register index storing this variable
146      */
147     public int declareParameter(final String param) {
148         if (namedVariables == null) {
149             namedVariables = new LinkedHashMap<>();
150         } else if (vars > 0) {
151             throw new IllegalStateException("cant declare parameters after variables");
152         }
153         return namedVariables.computeIfAbsent(param, name -> {
154             final int register = namedVariables.size();
155             parms += 1;
156             return register;
157         });
158     }
159 
160     /**
161      * Declares a local variable.
162      * <p>
163      * This method creates an new entry in the symbol map.
164      * </p>
165      * @param varName the variable name
166      * @return the register index storing this variable
167      */
168     public int declareVariable(final String varName) {
169         if (namedVariables == null) {
170             namedVariables = new LinkedHashMap<>();
171         }
172         return namedVariables.computeIfAbsent(varName, name -> {
173            final int register = namedVariables.size();
174             vars += 1;
175             // check if local is redefining captured
176             if (parent != null) {
177                 final Integer pr = parent.getSymbol(name, true);
178                 if (pr != null) {
179                     if (capturedVariables == null) {
180                         capturedVariables = new LinkedHashMap<>();
181                     }
182                     capturedVariables.put(register, pr);
183                 }
184             }
185             return register;
186         });
187     }
188 
189     /**
190      * Gets the (maximum) number of arguments this script expects.
191      * @return the number of parameters
192      */
193     public int getArgCount() {
194         return parms;
195     }
196 
197     /**
198      * Gets the captured index of a given symbol, ie the target index of a symbol in a child scope.
199      * @param symbol the symbol index
200      * @return the target symbol index or null if the symbol is not captured
201      */
202     public Integer getCaptured(final int symbol) {
203         if (capturedVariables != null) {
204             for (final Map.Entry<Integer, Integer> capture : capturedVariables.entrySet()) {
205                 final Integer source = capture.getValue();
206                 if (source == symbol) {
207                     return capture.getKey();
208                 }
209             }
210         }
211         return null;
212     }
213 
214     /**
215      * Gets the index of a captured symbol, ie the source index of a symbol in a parent scope.
216      * @param symbol the symbol index
217      * @return the source symbol index or -1 if the symbol is not captured
218      */
219     public int getCaptureDeclaration(final int symbol) {
220         final Integer declared = capturedVariables != null ? capturedVariables.get(symbol)  : null;
221         return declared != null ? declared : -1;
222     }
223 
224     /**
225      * Gets this script captured symbols names, i.e. local variables defined in outer scopes and used
226      * by this scope.
227      * @return the captured names
228      */
229     public String[] getCapturedVariables() {
230         if (capturedVariables != null) {
231             final List<String> captured = new ArrayList<>(vars);
232             for (final Map.Entry<String, Integer> entry : namedVariables.entrySet()) {
233                 final int symnum = entry.getValue();
234                 if (symnum >= parms && capturedVariables.containsKey(symnum)) {
235                     captured.add(entry.getKey());
236                 }
237             }
238             if (!captured.isEmpty()) {
239                 return captured.toArray(new String[0]);
240             }
241         }
242         return EMPTY_STRS;
243     }
244 
245     /**
246      * Gets this script local variable, i.e. symbols assigned to local variables excluding captured variables.
247      * @return the local variable names
248      */
249     public String[] getLocalVariables() {
250         if (namedVariables == null || vars <= 0) {
251             return EMPTY_STRS;
252         }
253         final List<String> locals = new ArrayList<>(vars);
254         for (final Map.Entry<String, Integer> entry : namedVariables.entrySet()) {
255             final int symnum = entry.getValue();
256             if (symnum >= parms && (capturedVariables == null || !capturedVariables.containsKey(symnum))) {
257                 locals.add(entry.getKey());
258             }
259         }
260         return locals.toArray(new String[0]);
261     }
262 
263     /**
264      * Gets this script parameters, i.e. symbols assigned before creating local variables.
265      * @return the parameter names
266      */
267     public String[] getParameters() {
268         return getParameters(0);
269     }
270 
271     /**
272      * Gets this script parameters.
273      * @param bound number of known bound parameters (curry)
274      * @return the parameter names
275      */
276      String[] getParameters(final int bound) {
277         final int unbound = parms - bound;
278         if (namedVariables == null || unbound <= 0) {
279             return EMPTY_STRS;
280         }
281         final String[] pa = new String[unbound];
282         int p = 0;
283         for (final Map.Entry<String, Integer> entry : namedVariables.entrySet()) {
284             final int argn = entry.getValue();
285             if (argn >= bound && argn < parms) {
286                 pa[p++] = entry.getKey();
287             }
288         }
289         return pa;
290     }
291 
292     Scope getParent() {
293         return parent;
294     }
295 
296     /**
297      * Checks whether an identifier is a local variable or argument, ie a symbol.
298      * If this fails, look in parents for symbol that can be captured.
299      * @param name the symbol name
300      * @return the symbol index
301      */
302     public Integer getSymbol(final String name) {
303         return getSymbol(name, true);
304     }
305 
306     /**
307      * Checks whether an identifier is a local variable or argument, ie a symbol.
308      * @param name the symbol name
309      * @param capture whether solving by capturing a parent symbol is allowed
310      * @return the symbol index
311      */
312     private Integer getSymbol(final String name, final boolean capture) {
313         Integer register = namedVariables != null ? namedVariables.get(name) : null;
314         if (register == null && capture && parent != null) {
315             final Integer pr = parent.getSymbol(name, true);
316             if (pr != null) {
317                 if (capturedVariables == null) {
318                     capturedVariables = new LinkedHashMap<>();
319                 }
320                 if (namedVariables == null) {
321                     namedVariables = new LinkedHashMap<>();
322                 }
323                 register = namedVariables.size();
324                 namedVariables.put(name, register);
325                 capturedVariables.put(register, pr);
326             }
327         }
328         return register;
329     }
330 
331     /**
332      * Gets this script symbols names, i.e. parameters and local variables.
333      * @return the symbol names
334      */
335     public String[] getSymbols() {
336         return namedVariables != null ? namedVariables.keySet().toArray(new String[0]) : EMPTY_STRS;
337     }
338 
339     /**
340      * Checks whether a given symbol is captured.
341      * @param symbol the symbol number
342      * @return true if captured, false otherwise
343      */
344     public boolean isCapturedSymbol(final int symbol) {
345         return capturedVariables != null && capturedVariables.containsKey(symbol);
346     }
347 
348     /**
349      * Checks whether a symbol is declared through a let or const.
350      * @param s the symbol
351      * @return true if symbol was declared through let or const
352      */
353     public boolean isLexical(final int s) {
354         return lexicalVariables != null && s >= 0 && lexicalVariables.hasSymbol(s);
355     }
356 }