001    // Copyright (c) 2011, the Dart project authors.  Please see the AUTHORS file
002    // for details. All rights reserved. Use of this source code is governed by a
003    // BSD-style license that can be found in the LICENSE file.
004    
005    package org.jetbrains.kotlin.js.backend.ast;
006    
007    import org.jetbrains.kotlin.js.util.Maps;
008    import org.jetbrains.annotations.NotNull;
009    import org.jetbrains.annotations.Nullable;
010    
011    import java.util.*;
012    import java.util.regex.Matcher;
013    import java.util.regex.Pattern;
014    
015    
016    /**
017     * A scope is a factory for creating and allocating
018     * {@link JsName}s. A JavaScript AST is
019     * built in terms of abstract name objects without worrying about obfuscation,
020     * keyword/identifier blacklisting, and so on.
021     * <p/>
022     * <p/>
023     * <p/>
024     * Scopes are associated with
025     * {@link JsFunction}s, but the two are
026     * not equivalent. Functions <i>have</i> scopes, but a scope does not
027     * necessarily have an associated Function. Examples of this include the
028     * {@link JsRootScope} and synthetic
029     * scopes that might be created by a client.
030     * <p/>
031     * <p/>
032     * <p/>
033     * Scopes can have parents to provide constraints when allocating actual
034     * identifiers for names. Specifically, names in child scopes are chosen such
035     * that they do not conflict with names in their parent scopes. The ultimate
036     * parent is usually the global scope (see
037     * {@link JsProgram#getRootScope()}),
038     * but parentless scopes are useful for managing names that are always accessed
039     * with a qualifier and could therefore never be confused with the global scope
040     * hierarchy.
041     */
042    public abstract class JsScope {
043        @NotNull
044        private final String description;
045        private Map<String, JsName> names = Collections.emptyMap();
046        private Map<JsName, Object> temporaryNames;
047        private Set<JsName> readonlyTemporaryNames = null;
048        private final JsScope parent;
049    
050        private static final Pattern FRESH_NAME_SUFFIX = Pattern.compile("[\\$_]\\d+$");
051    
052        public JsScope(JsScope parent, @NotNull String description) {
053            this.description = description;
054            this.parent = parent;
055        }
056    
057        protected JsScope(@NotNull String description) {
058            this.description = description;
059            parent = null;
060        }
061    
062        public Set<JsName> getTemporaryNames() {
063            if (temporaryNames == null) {
064                return Collections.emptySet();
065            }
066            if (readonlyTemporaryNames == null) {
067                readonlyTemporaryNames = Collections.unmodifiableSet(temporaryNames.keySet());
068            }
069            return readonlyTemporaryNames;
070        }
071    
072        @NotNull
073        public JsScope innerObjectScope(@NotNull String scopeName) {
074            return new JsObjectScope(this, scopeName);
075        }
076    
077        /**
078         * Gets a name object associated with the specified identifier in this scope,
079         * creating it if necessary.<br/>
080         * If the JsName does not exist yet, a new JsName is created. The identifier,
081         * short name, and original name of the newly created JsName are equal to
082         * the given identifier.
083         *
084         * @param identifier An identifier that is unique within this scope.
085         */
086        @NotNull
087        public JsName declareName(@NotNull String identifier) {
088            JsName name = findOwnName(identifier);
089            return name != null ? name : doCreateName(identifier);
090        }
091    
092        /**
093         * Creates a new variable with an unique ident in this scope.
094         * The generated JsName is guaranteed to have an identifier that does not clash with any existing variables in the scope.
095         * Future declarations of variables might however clash with the temporary
096         * (unless they use this function).
097         */
098        @NotNull
099        public JsName declareFreshName(@NotNull String suggestedName) {
100            assert !suggestedName.isEmpty();
101            String ident = getFreshIdent(suggestedName);
102            return doCreateName(ident);
103        }
104    
105        @NotNull
106        public JsName declareTemporaryName(@NotNull String suggestedName) {
107            assert !suggestedName.isEmpty();
108            JsName name = new JsName(this, suggestedName, true);
109            if (temporaryNames == null) {
110                temporaryNames = new WeakHashMap<JsName, Object>();
111            }
112            temporaryNames.put(name, this);
113            return name;
114        }
115    
116        /**
117         * Creates a temporary variable with an unique name in this scope.
118         * The generated temporary is guaranteed to have an identifier (but not short
119         * name) that does not clash with any existing variables in the scope.
120         * Future declarations of variables might however clash with the temporary.
121         */
122        @NotNull
123        public JsName declareTemporary() {
124            return declareTemporaryName("tmp$");
125        }
126    
127        /**
128         * Attempts to find the name object for the specified ident, searching in this
129         * scope, and if not found, in the parent scopes.
130         *
131         * @return <code>null</code> if the identifier has no associated name
132         */
133        @Nullable
134        public final JsName findName(@NotNull String ident) {
135            JsName name = findOwnName(ident);
136            if (name == null && parent != null) {
137                return parent.findName(ident);
138            }
139            return name;
140        }
141    
142        public boolean hasOwnName(@NotNull String name) {
143            return names.containsKey(name);
144        }
145    
146        private boolean hasName(@NotNull String name) {
147            return hasOwnName(name) || (parent != null && parent.hasName(name));
148        }
149    
150        /**
151         * Returns the parent scope of this scope, or <code>null</code> if this is the
152         * root scope.
153         */
154        public final JsScope getParent() {
155            return parent;
156        }
157    
158        public JsProgram getProgram() {
159            assert (parent != null) : "Subclasses must override getProgram() if they do not set a parent";
160            return parent.getProgram();
161        }
162    
163        @Override
164        public final String toString() {
165            if (parent != null) {
166                return description + "->" + parent;
167            }
168            else {
169                return description;
170            }
171        }
172    
173        public void copyOwnNames(JsScope other) {
174            names = new HashMap<String, JsName>(names);
175            names.putAll(other.names);
176        }
177    
178        @NotNull
179        public String getDescription() {
180            return description;
181        }
182    
183        @NotNull
184        protected JsName doCreateName(@NotNull String ident) {
185            JsName name = new JsName(this, ident, false);
186            names = Maps.put(names, ident, name);
187            return name;
188        }
189    
190        /**
191         * Attempts to find the name object for the specified ident, searching in this
192         * scope only.
193         *
194         * @return <code>null</code> if the identifier has no associated name
195         */
196        protected JsName findOwnName(@NotNull String ident) {
197            return names.get(ident);
198        }
199    
200        /**
201         * During inlining names can be refreshed multiple times,
202         * so "a" becomes "a_0", then becomes "a_0_0"
203         * in case a_0 has been declared in calling scope.
204         *
205         * That's ugly. To resolve it, we rename
206         * clashing names with "[_$]\\d+" suffix,
207         * incrementing last number.
208         *
209         * Fresh name for "a0" should still be "a0_0".
210         */
211        @NotNull
212        protected String getFreshIdent(@NotNull String suggestedIdent) {
213            char sep = '_';
214            String baseName = suggestedIdent;
215            int counter = 0;
216    
217            Matcher matcher = FRESH_NAME_SUFFIX.matcher(suggestedIdent);
218            if (matcher.find()) {
219                String group = matcher.group();
220                baseName = matcher.replaceAll("");
221                sep = group.charAt(0);
222                counter = Integer.valueOf(group.substring(1));
223            }
224    
225            String freshName = suggestedIdent;
226            while (hasName(freshName)) {
227                freshName = baseName + sep + counter++;
228            }
229    
230            return freshName;
231        }
232    }