UsedNameTracker.h (9840B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: set ts=8 sts=2 et sw=2 tw=80: 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #ifndef frontend_UsedNameTracker_h 8 #define frontend_UsedNameTracker_h 9 10 #include "mozilla/Assertions.h" 11 #include "mozilla/Maybe.h" 12 13 #include <stdint.h> 14 15 #include "frontend/ParserAtom.h" // TaggedParserAtomIndex 16 #include "frontend/TaggedParserAtomIndexHasher.h" // TaggedParserAtomIndexHasher 17 #include "frontend/Token.h" 18 #include "js/AllocPolicy.h" 19 #include "js/HashTable.h" 20 #include "js/Vector.h" 21 22 namespace js { 23 namespace frontend { 24 25 // A data structure for tracking used names per parsing session in order to 26 // compute which bindings are closed over. Scripts and scopes are numbered 27 // monotonically in textual order and unresolved uses of a name are tracked by 28 // lists of identifier uses, which are a pair of (ScriptId,ScopeId). 29 // 30 // For an identifier `i` with a use (ScriptId,ScopeId) in the Used list, 31 // ScriptId tracks the most nested script that has a use of u, and ScopeId 32 // tracks the most nested scope that is still being parsed (as the lists will be 33 // filtered as we finish processing a particular scope). 34 // 35 // ScriptId is used to answer the question "is `i` used by a nested function?" 36 // ScopeId is used to answer the question "is `i` used in any scopes currently 37 // being parsed?" 38 // 39 // The algorithm: 40 // 41 // Let Used be a map of names to lists. 42 // Let Declared(ScopeId) be a list of declarations for a scope numbered with 43 // ScopeId 44 // 45 // 1. Number all scopes in monotonic increasing order in textual order. 46 // 2. Number all scripts in monotonic increasing order in textual order. 47 // 3. When an identifier `i` is used in (ScriptId,ScopeId), append that use to 48 // the list Used[i] (creating the list and table entry if necessary). 49 // 4. When an identifier `i` is declared in a scope numbered ScopeId, append `i` 50 // to Declared(ScopeId). 51 // 5. When we finish parsing a scope numbered with ScopeId, in script numbered 52 // ScriptId, for each declared name d in Declared(ScopeId): 53 // a. If d is found in Used, mark d as closed over if there is a value 54 // (UsedScriptId, UsedScopeId) in Used[d] such that UsedScriptId > ScriptId 55 // and UsedScopeId > ScopeId. 56 // b. Remove all values uses in Used[d] where UsedScopeId > ScopeId. 57 // 58 // Steps 1 and 2 are implemented by UsedNameTracker::next{Script,Scope}Id. 59 // Step 3 is implemented by UsedNameTracker::noteUsedInScope. 60 // Step 4 is implemented by ParseContext::Scope::addDeclaredName. 61 // Step 5 is implemented by UsedNameTracker::noteBoundInScope and 62 // Parser::propagateFreeNamesAndMarkClosedOverBindings 63 // 64 // The following is a worked example to show how the algorithm works on a 65 // relatively simple piece of code. (clang-format is disabled due to the width 66 // of the example). 67 68 // clang-format off 69 // 70 // // Script 1, Scope 1 71 // var x = 1; // Declared(1) = [x]; 72 // function f() {// Script 2, Scope 2 73 // if (x > 10) { //Scope 3 // Used[x] = [(2,2)]; 74 // var x = 12; // Declared(3) = [x]; 75 // function g() // Script 3 76 // { // Scope 4 77 // return x; // Used[x] = [(2,2), (3,4)] 78 // } // Leaving Script 3, Scope 4: No declared variables. 79 // } // Leaving Script 2, Scope 3: Declared(3) = [x]; 80 // // - Used[x][1] = (2,2) is not > (2,3) 81 // // - Used[x][2] = (3,4) is > (2,3), so mark x as closed over. 82 // // - Update Used[x]: [] -- Makes sense, as at this point we have 83 // // bound all the unbound x to a particlar 84 // // declaration.. 85 // 86 // else { // Scope 5 87 // var x = 14; // Declared(5) = [x] 88 // function g() // Script 4 89 // { // Scope 6 90 // return y; // Used[y] = [(4,6)] 91 // } // Leaving Script 4, Scope 6: No declarations. 92 // } // Leaving Script 2, Scope 5: Declared(5) = [x] 93 // // - Used[x] = [], so don't mark x as closed over. 94 // var y = 12; // Declared(2) = [y] 95 // } // Leaving Script 2, Scope 2: Declared(2) = [y] 96 // // - Used[y][1] = (4,6) is > (2,2), so mark y as closed over. 97 // // - Update Used[y]: []. 98 99 // clang-format on 100 101 struct UnboundPrivateName { 102 TaggedParserAtomIndex atom; 103 TokenPos position; 104 105 UnboundPrivateName(TaggedParserAtomIndex atom, TokenPos position) 106 : atom(atom), position(position) {} 107 }; 108 109 class UsedNameTracker { 110 public: 111 struct Use { 112 uint32_t scriptId; 113 uint32_t scopeId; 114 }; 115 116 class UsedNameInfo { 117 friend class UsedNameTracker; 118 119 Vector<Use, 6> uses_; 120 121 void resetToScope(uint32_t scriptId, uint32_t scopeId); 122 123 NameVisibility visibility_ = NameVisibility::Public; 124 125 // The first place this name was used. This is important to track 126 // for private names, as we will use this location to issue 127 // diagnostics for using a name that's not defined lexically. 128 mozilla::Maybe<TokenPos> firstUsePos_; 129 130 public: 131 explicit UsedNameInfo(FrontendContext* fc, NameVisibility visibility, 132 mozilla::Maybe<TokenPos> position) 133 : uses_(fc), visibility_(visibility), firstUsePos_(position) {} 134 135 UsedNameInfo(UsedNameInfo&& other) = default; 136 137 bool noteUsedInScope(uint32_t scriptId, uint32_t scopeId) { 138 if (uses_.empty() || uses_.back().scopeId < scopeId) { 139 return uses_.append(Use{scriptId, scopeId}); 140 } 141 return true; 142 } 143 144 void noteBoundInScope(uint32_t scriptId, uint32_t scopeId, 145 bool* closedOver) { 146 *closedOver = false; 147 while (!uses_.empty()) { 148 Use& innermost = uses_.back(); 149 if (innermost.scopeId < scopeId) { 150 break; 151 } 152 if (innermost.scriptId > scriptId) { 153 *closedOver = true; 154 } 155 uses_.popBack(); 156 } 157 } 158 159 bool isUsedInScript(uint32_t scriptId) const { 160 return !uses_.empty() && uses_.back().scriptId >= scriptId; 161 } 162 163 bool isClosedOver(uint32_t scriptId) const { 164 return !uses_.empty() && uses_.back().scriptId > scriptId; 165 } 166 167 // To allow disambiguating public and private symbols 168 bool isPublic() { return visibility_ == NameVisibility::Public; } 169 170 bool empty() const { return uses_.empty(); } 171 172 mozilla::Maybe<TokenPos> pos() { return firstUsePos_; } 173 174 // When we leave a scope, and subsequently find a new private name 175 // reference, we don't want our error messages to be attributed to an old 176 // scope, so we update the position in that scenario. 177 void maybeUpdatePos(mozilla::Maybe<TokenPos> p) { 178 MOZ_ASSERT_IF(!isPublic(), p.isSome()); 179 180 if (empty() && !isPublic()) { 181 firstUsePos_ = p; 182 } 183 } 184 }; 185 186 using UsedNameMap = 187 HashMap<TaggedParserAtomIndex, UsedNameInfo, TaggedParserAtomIndexHasher>; 188 189 private: 190 // The map of names to chains of uses. 191 UsedNameMap map_; 192 193 // Monotonically increasing id for all nested scripts. 194 uint32_t scriptCounter_; 195 196 // Monotonically increasing id for all nested scopes. 197 uint32_t scopeCounter_; 198 199 // Set if a private name was encountered. 200 // Used to short circuit some private field early error checks 201 bool hasPrivateNames_; 202 203 public: 204 explicit UsedNameTracker(FrontendContext* fc) 205 : map_(fc), 206 scriptCounter_(0), 207 scopeCounter_(0), 208 hasPrivateNames_(false) {} 209 210 uint32_t nextScriptId() { 211 MOZ_ASSERT(scriptCounter_ != UINT32_MAX, 212 "ParseContext::Scope::init should have prevented wraparound"); 213 return scriptCounter_++; 214 } 215 216 uint32_t nextScopeId() { 217 MOZ_ASSERT(scopeCounter_ != UINT32_MAX); 218 return scopeCounter_++; 219 } 220 221 UsedNameMap::Ptr lookup(TaggedParserAtomIndex name) const { 222 return map_.lookup(name); 223 } 224 225 [[nodiscard]] bool noteUse( 226 FrontendContext* fc, TaggedParserAtomIndex name, 227 NameVisibility visibility, uint32_t scriptId, uint32_t scopeId, 228 mozilla::Maybe<TokenPos> tokenPosition = mozilla::Nothing()); 229 230 // Fill maybeUnboundName with the first (source order) unbound name, or 231 // Nothing() if there are no unbound names. 232 [[nodiscard]] bool hasUnboundPrivateNames( 233 FrontendContext* fc, 234 mozilla::Maybe<UnboundPrivateName>& maybeUnboundName); 235 236 // Return a list of unbound private names, sorted by increasing location in 237 // the source. 238 [[nodiscard]] bool getUnboundPrivateNames( 239 Vector<UnboundPrivateName, 8>& unboundPrivateNames); 240 241 struct RewindToken { 242 private: 243 friend class UsedNameTracker; 244 uint32_t scriptId; 245 uint32_t scopeId; 246 }; 247 248 RewindToken getRewindToken() const { 249 RewindToken token; 250 token.scriptId = scriptCounter_; 251 token.scopeId = scopeCounter_; 252 return token; 253 } 254 255 // Resets state so that scriptId and scopeId are the innermost script and 256 // scope, respectively. Used for rewinding state on syntax parse failure. 257 void rewind(RewindToken token); 258 259 const UsedNameMap& map() const { return map_; } 260 261 #if defined(DEBUG) || defined(JS_JITSPEW) 262 void dump(ParserAtomsTable& table); 263 #endif 264 }; 265 266 } // namespace frontend 267 } // namespace js 268 269 #endif