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 }