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 }