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.AstPackage.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 protected 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 assert !hasOwnName(ident);
098 return doCreateName(ident);
099 }
100
101 private String getNextTempName() {
102 // introduced by the compiler
103 return "tmp$" + (scopeId != null ? scopeId + "$" : "") + tempIndex++;
104 }
105
106 /**
107 * Creates a temporary variable with an unique name in this scope.
108 * The generated temporary is guaranteed to have an identifier (but not short
109 * name) that does not clash with any existing variables in the scope.
110 * Future declarations of variables might however clash with the temporary.
111 */
112 @NotNull
113 public JsName declareTemporary() {
114 return declareFreshName(getNextTempName());
115 }
116
117 /**
118 * Attempts to find the name object for the specified ident, searching in this
119 * scope, and if not found, in the parent scopes.
120 *
121 * @return <code>null</code> if the identifier has no associated name
122 */
123 @Nullable
124 public final JsName findName(String ident) {
125 JsName name = findOwnName(ident);
126 if (name == null && parent != null) {
127 return parent.findName(ident);
128 }
129 return name;
130 }
131
132 public boolean hasOwnName(@NotNull String name) {
133 return names.containsKey(name);
134 }
135
136 /**
137 * Returns the parent scope of this scope, or <code>null</code> if this is the
138 * root scope.
139 */
140 public final JsScope getParent() {
141 return parent;
142 }
143
144 public JsProgram getProgram() {
145 assert (parent != null) : "Subclasses must override getProgram() if they do not set a parent";
146 return parent.getProgram();
147 }
148
149 @Override
150 public final String toString() {
151 if (parent != null) {
152 return description + "->" + parent;
153 }
154 else {
155 return description;
156 }
157 }
158
159 public void copyOwnNames(JsScope other) {
160 names = new HashMap<String, JsName>(names);
161 names.putAll(other.names);
162 }
163
164 @NotNull
165 public String getDescription() {
166 return description;
167 }
168
169 @NotNull
170 protected JsName doCreateName(@NotNull String ident) {
171 JsName name = new JsName(this, ident);
172 names = Maps.put(names, ident, name);
173 return name;
174 }
175
176 /**
177 * Attempts to find the name object for the specified ident, searching in this
178 * scope only.
179 *
180 * @return <code>null</code> if the identifier has no associated name
181 */
182 protected JsName findOwnName(@NotNull String ident) {
183 return names.get(ident);
184 }
185
186 /**
187 * During inlining names can be refreshed multiple times,
188 * so "a" becomes "a_0", then becomes "a_0_0"
189 * in case a_0 has been declared in calling scope.
190 *
191 * That's ugly. To resolve it, we rename
192 * clashing names with "[_$]\\d+" suffix,
193 * incrementing last number.
194 *
195 * Fresh name for "a0" should still be "a0_0".
196 */
197 @NotNull
198 protected String getFreshIdent(@NotNull String suggestedIdent) {
199 char sep = '_';
200 String baseName = suggestedIdent;
201 int counter = 0;
202
203 Matcher matcher = FRESH_NAME_SUFFIX.matcher(suggestedIdent);
204 if (matcher.find()) {
205 String group = matcher.group();
206 baseName = matcher.replaceAll("");
207 sep = group.charAt(0);
208 counter = Integer.valueOf(group.substring(1));
209 }
210
211 String freshName = suggestedIdent;
212 while (hasOwnName(freshName)) {
213 freshName = baseName + sep + counter++;
214 }
215
216 return freshName;
217 }
218 }