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