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