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    *      https://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.Collections;
22  import java.util.LinkedHashMap;
23  import java.util.List;
24  import java.util.Map;
25  
26  /**
27   * A script scope, stores the declaration of parameters and local variables as symbols.
28   * <p>This also acts as the functional scope and variable definition store.</p>
29   *
30   * @since 3.0
31   */
32  public final class Scope {
33  
34      /**
35       * The value of an as-yet undeclared but variable, for instance: x; before var x;.
36       */
37      static final Object UNDECLARED = new Object() {
38          @Override public String toString() {
39              return "??";
40          }
41      };
42  
43      /**
44       * The value of a declared but undefined variable, for instance: var x;.
45       */
46      static final Object UNDEFINED = new Object() {
47          @Override public String toString() {
48              return "?";
49          }
50      };
51  
52      /**
53       * The empty string array.
54       */
55      private static final String[] EMPTY_STRS = {};
56  
57      /**
58       * The parent scope.
59       */
60      private final Scope parent;
61  
62      /**
63       * The number of parameters.
64       */
65      private int parms;
66  
67      /**
68       * The number of local variables.
69       */
70      private int vars;
71  
72      /**
73       * The map of named variables aka script parameters and local variables.
74       * Each parameter is associated with a symbol and is materialized as an offset in the stacked array used
75       * during evaluation.
76       */
77      private Map<String, Integer> namedVariables;
78  
79      /**
80       * The map of local captured variables to parent scope variables, i.e., closure.
81       */
82      private Map<Integer, Integer> capturedVariables;
83  
84      /**
85       * Let symbols.
86       */
87      private LexicalScope lexicalVariables;
88  
89      /**
90       * Creates a new scope with a list of parameters.
91       *
92       * @param scope the parent scope if any
93       * @param parameters the list of parameters
94       */
95      public Scope(final Scope scope, final String... parameters) {
96          if (parameters != null) {
97              parms = parameters.length;
98              namedVariables = new LinkedHashMap<>();
99              for (int p = 0; p < parms; ++p) {
100                 namedVariables.put(parameters[p], p);
101             }
102         } else {
103             parms = 0;
104         }
105         vars = 0;
106         parent = scope;
107     }
108 
109     /**
110      * Gets an unmodifiable view of a scope&quote;s symbols map.
111      *
112      * @param scope the scope
113      * @return the symbols map
114      */
115      static Map<String, Integer> getSymbolsMap(final Scope scope) {
116         if (scope != null && scope.namedVariables != null) {
117             return Collections.unmodifiableMap(scope.namedVariables);
118         }
119         return Collections.emptyMap();
120     }
121 
122     /**
123      * Marks a symbol as lexical, declared through let or const.
124      *
125      * @param s the symbol
126      * @return true if the symbol was not already present in the lexical set
127      */
128     public boolean addLexical(final int s) {
129         if (lexicalVariables == null) {
130             lexicalVariables = new LexicalScope();
131         }
132         return lexicalVariables.addSymbol(s);
133     }
134 
135     /**
136      * Creates a frame by copying values up to the number of parameters.
137      * <p>This captures the captured variables values.</p>
138      *
139      * @param frame the caller frame
140      * @param args the arguments
141      * @return the arguments array
142      */
143     public Frame createFrame(final boolean ref, final Frame frame, final Object...args) {
144         if (namedVariables == null) {
145             return null;
146         }
147         final Object[] arguments = new Object[namedVariables.size()];
148         Arrays.fill(arguments, UNDECLARED);
149         final Frame newFrame;
150         if (frame != null && capturedVariables != null && parent != null) {
151             for (final Map.Entry<Integer, Integer> capture : capturedVariables.entrySet()) {
152                 final Integer target = capture.getKey();
153                 final Integer source = capture.getValue();
154                 final boolean lexical = lexicalVariables != null && lexicalVariables.hasSymbol(target);
155                 final Object arg = frame.capture(source, lexical);
156                 arguments[target] = arg;
157             }
158             newFrame = frame.newFrame(this, arguments, 0);
159         } else {
160             newFrame = ref
161             ? new ReferenceFrame(this, arguments, 0)
162             : new Frame(this, arguments, 0);
163         }
164         return newFrame.assign(args);
165     }
166 
167     /**
168      * Declares a parameter.
169      * <p>
170      * This method creates a new entry in the symbol map.
171      * </p>
172      *
173      * @param param the parameter name
174      * @return the register index storing this variable
175      */
176     public int declareParameter(final String param) {
177         if (namedVariables == null) {
178             namedVariables = new LinkedHashMap<>();
179         } else if (vars > 0) {
180             throw new IllegalStateException("cant declare parameters after variables");
181         }
182         return namedVariables.computeIfAbsent(param, name -> {
183             final int register = namedVariables.size();
184             parms += 1;
185             return register;
186         });
187     }
188 
189     /**
190      * Declares a local variable.
191      * <p>
192      * This method creates a new entry in the symbol map.
193      * </p>
194      *
195      * @param varName the variable name
196      * @return the register index storing this variable
197      */
198     public int declareVariable(final String varName) {
199         if (namedVariables == null) {
200             namedVariables = new LinkedHashMap<>();
201         }
202         return namedVariables.computeIfAbsent(varName, name -> {
203            final int register = namedVariables.size();
204             vars += 1;
205             // check if local is redefining captured
206             if (parent != null) {
207                 final Integer pr = parent.getSymbol(name, true);
208                 if (pr != null) {
209                     if (capturedVariables == null) {
210                         capturedVariables = new LinkedHashMap<>();
211                     }
212                     capturedVariables.put(register, pr);
213                 }
214             }
215             return register;
216         });
217     }
218 
219     /**
220      * Gets the (maximum) number of arguments this script expects.
221      *
222      * @return the number of parameters
223      */
224     public int getArgCount() {
225         return parms;
226     }
227 
228     /**
229      * Gets the captured index of a given symbol, i.e., the target index of a symbol in a child scope.
230      *
231      * @param symbol the symbol index
232      * @return the target symbol index or null if the symbol is not captured
233      */
234     public Integer getCaptured(final int symbol) {
235         if (capturedVariables != null) {
236             for (final Map.Entry<Integer, Integer> capture : capturedVariables.entrySet()) {
237                 final Integer source = capture.getValue();
238                 if (source == symbol) {
239                     return capture.getKey();
240                 }
241             }
242         }
243         return null;
244     }
245 
246     /**
247      * Gets the index of a captured symbol, i.e., the source index of a symbol in a parent scope.
248      *
249      * @param symbol the symbol index
250      * @return the source symbol index or -1 if the symbol is not captured
251      */
252     public int getCaptureDeclaration(final int symbol) {
253         final Integer declared = capturedVariables != null ? capturedVariables.get(symbol)  : null;
254         return declared != null ? declared : -1;
255     }
256 
257     /**
258      * Gets this script captured symbols names, i.e., local variables defined in outer scopes and used
259      * by this scope.
260      *
261      * @return the captured names
262      */
263     public String[] getCapturedVariables() {
264         if (capturedVariables != null) {
265             final List<String> captured = new ArrayList<>(vars);
266             for (final Map.Entry<String, Integer> entry : namedVariables.entrySet()) {
267                 final int symnum = entry.getValue();
268                 if (symnum >= parms && capturedVariables.containsKey(symnum)) {
269                     captured.add(entry.getKey());
270                 }
271             }
272             if (!captured.isEmpty()) {
273                 return captured.toArray(new String[0]);
274             }
275         }
276         return EMPTY_STRS;
277     }
278 
279     /**
280      * Gets this script local variable, i.e. symbols assigned to local variables excluding captured variables.
281      *
282      * @return the local variable names
283      */
284     public String[] getLocalVariables() {
285         if (namedVariables == null || vars <= 0) {
286             return EMPTY_STRS;
287         }
288         final List<String> locals = new ArrayList<>(vars);
289         for (final Map.Entry<String, Integer> entry : namedVariables.entrySet()) {
290             final int symnum = entry.getValue();
291             if (symnum >= parms && (capturedVariables == null || !capturedVariables.containsKey(symnum))) {
292                 locals.add(entry.getKey());
293             }
294         }
295         return locals.toArray(new String[0]);
296     }
297 
298     /**
299      * Gets this script parameters, i.e., symbols assigned before creating local variables.
300      *
301      * @return the parameter names
302      */
303     public String[] getParameters() {
304         return getParameters(0);
305     }
306 
307     /**
308      * Gets this script parameters.
309      *
310      * @param bound number of known bound parameters (curry)
311      * @return the parameter names
312      */
313      String[] getParameters(final int bound) {
314         final int unbound = parms - bound;
315         if (namedVariables == null || unbound <= 0) {
316             return EMPTY_STRS;
317         }
318         final String[] pa = new String[unbound];
319         int p = 0;
320         for (final Map.Entry<String, Integer> entry : namedVariables.entrySet()) {
321             final int argn = entry.getValue();
322             if (argn >= bound && argn < parms) {
323                 pa[p++] = entry.getKey();
324             }
325         }
326         return pa;
327     }
328 
329     Scope getParent() {
330         return parent;
331     }
332 
333     /**
334      * Checks whether an identifier is a local variable or argument, i.e., a symbol.
335      * If this fails, look within parents for a symbol that can be captured.
336      *
337      * @param name the symbol name
338      * @return the symbol index
339      */
340     public Integer getSymbol(final String name) {
341         return getSymbol(name, true);
342     }
343 
344     /**
345      * Checks whether an identifier is a local variable or argument, i.e., a symbol.
346      *
347      * @param name the symbol name
348      * @param capture whether solving by capturing a parent symbol is allowed
349      * @return the symbol index
350      */
351     private Integer getSymbol(final String name, final boolean capture) {
352         Integer register = namedVariables != null ? namedVariables.get(name) : null;
353         if (register == null && capture && parent != null) {
354             final Integer pr = parent.getSymbol(name, true);
355             if (pr != null) {
356                 if (capturedVariables == null) {
357                     capturedVariables = new LinkedHashMap<>();
358                 }
359                 if (namedVariables == null) {
360                     namedVariables = new LinkedHashMap<>();
361                 }
362                 register = namedVariables.size();
363                 namedVariables.put(name, register);
364                 capturedVariables.put(register, pr);
365             }
366         }
367         return register;
368     }
369 
370     /**
371      * Gets this script symbols names, i.e., parameters and local variables.
372      *
373      * @return the symbol names
374      */
375     public String[] getSymbols() {
376         return namedVariables != null ? namedVariables.keySet().toArray(new String[0]) : EMPTY_STRS;
377     }
378 
379     /**
380      * Checks whether a given symbol is captured.
381      *
382      * @param symbol the symbol number
383      * @return true if captured, false otherwise
384      */
385     public boolean isCapturedSymbol(final int symbol) {
386         return capturedVariables != null && capturedVariables.containsKey(symbol);
387     }
388 
389     /**
390      * Checks whether a symbol is declared through a let or const.
391      *
392      * @param s the symbol
393      * @return true if symbol was declared through let or const
394      */
395     public boolean isLexical(final int s) {
396         return lexicalVariables != null && s >= 0 && lexicalVariables.hasSymbol(s);
397     }
398 }