| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 part of dart2js.js_emitter; | |
| 6 | |
| 7 class NsmEmitter extends CodeEmitterHelper { | |
| 8 final List<Selector> trivialNsmHandlers = <Selector>[]; | |
| 9 | |
| 10 /// If this is true then we can generate the noSuchMethod handlers at startup | |
| 11 /// time, instead of them being emitted as part of the Object class. | |
| 12 bool get generateTrivialNsmHandlers => true; | |
| 13 | |
| 14 // If we need fewer than this many noSuchMethod handlers we can save space by | |
| 15 // just emitting them in JS, rather than emitting the JS needed to generate | |
| 16 // them at run time. | |
| 17 static const VERY_FEW_NO_SUCH_METHOD_HANDLERS = 10; | |
| 18 | |
| 19 static const MAX_MINIFIED_LENGTH_FOR_DIFF_ENCODING = 4; | |
| 20 | |
| 21 void emitNoSuchMethodHandlers(AddPropertyFunction addProperty) { | |
| 22 | |
| 23 ClassStubGenerator generator = | |
| 24 new ClassStubGenerator(compiler, namer, backend); | |
| 25 | |
| 26 // Keep track of the JavaScript names we've already added so we | |
| 27 // do not introduce duplicates (bad for code size). | |
| 28 Map<jsAst.Name, Selector> addedJsNames | |
| 29 = generator.computeSelectorsForNsmHandlers(); | |
| 30 | |
| 31 // Set flag used by generateMethod helper below. If we have very few | |
| 32 // handlers we use addProperty for them all, rather than try to generate | |
| 33 // them at runtime. | |
| 34 bool haveVeryFewNoSuchMemberHandlers = | |
| 35 (addedJsNames.length < VERY_FEW_NO_SUCH_METHOD_HANDLERS); | |
| 36 List<jsAst.Name> names = addedJsNames.keys.toList() | |
| 37 ..sort(); | |
| 38 for (jsAst.Name jsName in names) { | |
| 39 Selector selector = addedJsNames[jsName]; | |
| 40 String reflectionName = emitter.getReflectionName(selector, jsName); | |
| 41 | |
| 42 if (reflectionName != null) { | |
| 43 emitter.mangledFieldNames[jsName] = reflectionName; | |
| 44 } | |
| 45 | |
| 46 List<jsAst.Expression> argNames = | |
| 47 selector.callStructure.getOrderedNamedArguments().map((String name) => | |
| 48 js.string(name)).toList(); | |
| 49 int type = selector.invocationMirrorKind; | |
| 50 if (!haveVeryFewNoSuchMemberHandlers && | |
| 51 isTrivialNsmHandler(type, argNames, selector, jsName) && | |
| 52 reflectionName == null) { | |
| 53 trivialNsmHandlers.add(selector); | |
| 54 } else { | |
| 55 StubMethod method = | |
| 56 generator.generateStubForNoSuchMethod(jsName, selector); | |
| 57 addProperty(method.name, method.code); | |
| 58 if (reflectionName != null) { | |
| 59 bool accessible = | |
| 60 compiler.world.allFunctions.filter(selector, null).any( | |
| 61 (Element e) => backend.isAccessibleByReflection(e)); | |
| 62 addProperty(namer.asName('+$reflectionName'), | |
| 63 js(accessible ? '2' : '0')); | |
| 64 } | |
| 65 } | |
| 66 } | |
| 67 } | |
| 68 | |
| 69 // Identify the noSuchMethod handlers that are so simple that we can | |
| 70 // generate them programatically. | |
| 71 bool isTrivialNsmHandler( | |
| 72 int type, List argNames, Selector selector, jsAst.Name internalName) { | |
| 73 if (!generateTrivialNsmHandlers) return false; | |
| 74 // Check for named arguments. | |
| 75 if (argNames.length != 0) return false; | |
| 76 // Check for unexpected name (this doesn't really happen). | |
| 77 if (internalName is GetterName) return type == 1; | |
| 78 if (internalName is SetterName) return type == 2; | |
| 79 return type == 0; | |
| 80 } | |
| 81 | |
| 82 /** | |
| 83 * Adds (at runtime) the handlers to the Object class which catch calls to | |
| 84 * methods that the object does not have. The handlers create an invocation | |
| 85 * mirror object. | |
| 86 * | |
| 87 * The current version only gives you the minified name when minifying (when | |
| 88 * not minifying this method is not called). | |
| 89 * | |
| 90 * In order to generate the noSuchMethod handlers we only need the minified | |
| 91 * name of the method. We test the first character of the minified name to | |
| 92 * determine if it is a getter or a setter, and we use the arguments array at | |
| 93 * runtime to get the number of arguments and their values. If the method | |
| 94 * involves named arguments etc. then we don't handle it here, but emit the | |
| 95 * handler method directly on the Object class. | |
| 96 * | |
| 97 * The minified names are mostly 1-4 character names, which we emit in sorted | |
| 98 * order (primary key is length, secondary ordering is lexicographic). This | |
| 99 * gives an order like ... dD dI dX da ... | |
| 100 * | |
| 101 * Gzip is good with repeated text, but it can't diff-encode, so we do that | |
| 102 * for it. We encode the minified names in a comma-separated string, but all | |
| 103 * the 1-4 character names are encoded before the first comma as a series of | |
| 104 * base 26 numbers. The last digit of each number is lower case, the others | |
| 105 * are upper case, so 1 is "b" and 26 is "Ba". | |
| 106 * | |
| 107 * We think of the minified names as base 88 numbers using the ASCII | |
| 108 * characters from # to z. The base 26 numbers each encode the delta from | |
| 109 * the previous minified name to the next. So if there is a minified name | |
| 110 * called Df and the next is Dh, then they are 2971 and 2973 when thought of | |
| 111 * as base 88 numbers. The difference is 2, which is "c" in lower-case- | |
| 112 * terminated base 26. | |
| 113 * | |
| 114 * The reason we don't encode long minified names with this method is that | |
| 115 * decoding the base 88 numbers would overflow JavaScript's puny integers. | |
| 116 * | |
| 117 * There are some selectors that have a special calling convention (because | |
| 118 * they are called with the receiver as the first argument). They need a | |
| 119 * slightly different noSuchMethod handler, so we handle these first. | |
| 120 */ | |
| 121 List<jsAst.Statement> buildTrivialNsmHandlers() { | |
| 122 List<jsAst.Statement> statements = <jsAst.Statement>[]; | |
| 123 if (trivialNsmHandlers.length == 0) return statements; | |
| 124 | |
| 125 bool minify = compiler.enableMinification; | |
| 126 bool useDiffEncoding = minify && trivialNsmHandlers.length > 30; | |
| 127 | |
| 128 // Find out how many selectors there are with the special calling | |
| 129 // convention. | |
| 130 Iterable<Selector> interceptedSelectors = trivialNsmHandlers.where( | |
| 131 (Selector s) => backend.isInterceptedName(s.name)); | |
| 132 Iterable<Selector> ordinarySelectors = trivialNsmHandlers.where( | |
| 133 (Selector s) => !backend.isInterceptedName(s.name)); | |
| 134 | |
| 135 // Get the short names (JS names, perhaps minified). | |
| 136 Iterable<jsAst.Name> interceptedShorts = | |
| 137 interceptedSelectors.map(namer.invocationMirrorInternalName); | |
| 138 Iterable<jsAst.Name> ordinaryShorts = | |
| 139 ordinarySelectors.map(namer.invocationMirrorInternalName); | |
| 140 | |
| 141 jsAst.Expression sortedShorts; | |
| 142 Iterable<String> sortedLongs; | |
| 143 if (useDiffEncoding) { | |
| 144 assert(minify); | |
| 145 sortedShorts = new _DiffEncodedListOfNames( | |
| 146 [interceptedShorts, ordinaryShorts]); | |
| 147 } else { | |
| 148 Iterable<Selector> sorted = | |
| 149 [interceptedSelectors, ordinarySelectors].expand((e) => (e)); | |
| 150 sortedShorts = js.concatenateStrings( | |
| 151 js.joinLiterals( | |
| 152 sorted.map(namer.invocationMirrorInternalName), | |
| 153 js.stringPart(",")), | |
| 154 addQuotes: true); | |
| 155 | |
| 156 if (!minify) { | |
| 157 sortedLongs = sorted.map((selector) => | |
| 158 selector.invocationMirrorMemberName); | |
| 159 } | |
| 160 } | |
| 161 // Startup code that loops over the method names and puts handlers on the | |
| 162 // Object class to catch noSuchMethod invocations. | |
| 163 ClassElement objectClass = compiler.objectClass; | |
| 164 jsAst.Expression createInvocationMirror = backend.emitter | |
| 165 .staticFunctionAccess(backend.getCreateInvocationMirror()); | |
| 166 if (useDiffEncoding) { | |
| 167 statements.add(js.statement('''{ | |
| 168 var objectClassObject = processedClasses.collected[#objectClass], | |
| 169 nameSequences = #diffEncoding.split("."), | |
| 170 shortNames = []; | |
| 171 if (objectClassObject instanceof Array) | |
| 172 objectClassObject = objectClassObject[1]; | |
| 173 for (var j = 0; j < nameSequences.length; ++j) { | |
| 174 var sequence = nameSequences[j].split(","), | |
| 175 nameNumber = 0; | |
| 176 // If we are loading a deferred library the object class will not be | |
| 177 // in the collectedClasses so objectClassObject is undefined, and we | |
| 178 // skip setting up the names. | |
| 179 if (!objectClassObject) break; | |
| 180 // Likewise, if the current sequence is empty, we don't process it. | |
| 181 if (sequence.length == 0) continue; | |
| 182 var diffEncodedString = sequence[0]; | |
| 183 for (var i = 0; i < diffEncodedString.length; i++) { | |
| 184 var codes = [], | |
| 185 diff = 0, | |
| 186 digit = diffEncodedString.charCodeAt(i); | |
| 187 for (; digit <= ${$Z};) { | |
| 188 diff *= 26; | |
| 189 diff += (digit - ${$A}); | |
| 190 digit = diffEncodedString.charCodeAt(++i); | |
| 191 } | |
| 192 diff *= 26; | |
| 193 diff += (digit - ${$a}); | |
| 194 nameNumber += diff; | |
| 195 for (var remaining = nameNumber; | |
| 196 remaining > 0; | |
| 197 remaining = (remaining / 88) | 0) { | |
| 198 codes.unshift(${$HASH} + remaining % 88); | |
| 199 } | |
| 200 shortNames.push( | |
| 201 String.fromCharCode.apply(String, codes)); | |
| 202 } | |
| 203 if (sequence.length > 1) { | |
| 204 Array.prototype.push.apply(shortNames, sequence.shift()); | |
| 205 } | |
| 206 } | |
| 207 }''', {'objectClass': js.quoteName(namer.className(objectClass)), | |
| 208 'diffEncoding': sortedShorts})); | |
| 209 } else { | |
| 210 // No useDiffEncoding version. | |
| 211 statements.add(js.statement( | |
| 212 'var objectClassObject = processedClasses.collected[#objectClass],' | |
| 213 ' shortNames = #diffEncoding.split(",")', | |
| 214 {'objectClass': js.quoteName(namer.className(objectClass)), | |
| 215 'diffEncoding': sortedShorts})); | |
| 216 if (!minify) { | |
| 217 statements.add(js.statement('var longNames = #longs.split(",")', | |
| 218 {'longs': js.string(sortedLongs.join(','))})); | |
| 219 } | |
| 220 statements.add(js.statement( | |
| 221 'if (objectClassObject instanceof Array)' | |
| 222 ' objectClassObject = objectClassObject[1];')); | |
| 223 } | |
| 224 | |
| 225 dynamic isIntercepted = // jsAst.Expression or bool. | |
| 226 interceptedSelectors.isEmpty | |
| 227 ? false | |
| 228 : ordinarySelectors.isEmpty | |
| 229 ? true | |
| 230 : js('j < #', js.number(interceptedSelectors.length)); | |
| 231 | |
| 232 statements.add(js.statement(''' | |
| 233 // If we are loading a deferred library the object class will not be in | |
| 234 // the collectedClasses so objectClassObject is undefined, and we skip | |
| 235 // setting up the names. | |
| 236 if (objectClassObject) { | |
| 237 for (var j = 0; j < shortNames.length; j++) { | |
| 238 var type = 0; | |
| 239 var shortName = shortNames[j]; | |
| 240 if (shortName[0] == "${namer.getterPrefix[0]}") type = 1; | |
| 241 if (shortName[0] == "${namer.setterPrefix[0]}") type = 2; | |
| 242 // Generate call to: | |
| 243 // | |
| 244 // createInvocationMirror(String name, internalName, type, | |
| 245 // arguments, argumentNames) | |
| 246 // | |
| 247 | |
| 248 // This 'if' is either a static choice or dynamic choice depending on | |
| 249 // [isIntercepted]. | |
| 250 if (#isIntercepted) { | |
| 251 objectClassObject[shortName] = | |
| 252 (function(name, shortName, type) { | |
| 253 return function(receiver) { | |
| 254 return this.#noSuchMethodName( | |
| 255 receiver, | |
| 256 #createInvocationMirror(name, shortName, type, | |
| 257 // Create proper Array with all arguments except first | |
| 258 // (receiver). | |
| 259 Array.prototype.slice.call(arguments, 1), | |
| 260 [])); | |
| 261 } | |
| 262 })(#names[j], shortName, type); | |
| 263 } else { | |
| 264 objectClassObject[shortName] = | |
| 265 (function(name, shortName, type) { | |
| 266 return function() { | |
| 267 return this.#noSuchMethodName( | |
| 268 // Object.noSuchMethodName ignores the explicit receiver | |
| 269 // argument. We could pass anything in place of [this]. | |
| 270 this, | |
| 271 #createInvocationMirror(name, shortName, type, | |
| 272 // Create proper Array with all arguments. | |
| 273 Array.prototype.slice.call(arguments, 0), | |
| 274 [])); | |
| 275 } | |
| 276 })(#names[j], shortName, type); | |
| 277 } | |
| 278 } | |
| 279 }''', { | |
| 280 'noSuchMethodName': namer.noSuchMethodName, | |
| 281 'createInvocationMirror': createInvocationMirror, | |
| 282 'names': minify ? 'shortNames' : 'longNames', | |
| 283 'isIntercepted': isIntercepted})); | |
| 284 | |
| 285 return statements; | |
| 286 } | |
| 287 } | |
| 288 | |
| 289 /// When pretty printed, this node computes a diff-encoded string for the list | |
| 290 /// of given names. | |
| 291 /// | |
| 292 /// See [buildTrivialNsmHandlers]. | |
| 293 class _DiffEncodedListOfNames extends jsAst.DeferredString | |
| 294 implements jsAst.AstContainer { | |
| 295 String _cachedValue; | |
| 296 List<jsAst.ArrayInitializer> ast; | |
| 297 | |
| 298 Iterable<jsAst.Node> get containedNodes => ast; | |
| 299 | |
| 300 _DiffEncodedListOfNames(Iterable<Iterable<jsAst.Name>> names) { | |
| 301 // Store the names in ArrayInitializer nodes to make them discoverable | |
| 302 // by traversals of the ast. | |
| 303 ast = names.map((Iterable i) => new jsAst.ArrayInitializer(i.toList())) | |
| 304 .toList(); | |
| 305 } | |
| 306 | |
| 307 void _computeDiffEncodingForList(Iterable<jsAst.Name> names, | |
| 308 StringBuffer diffEncoding) { | |
| 309 // Treat string as a number in base 88 with digits in ASCII order from # to | |
| 310 // z. The short name sorting is based on length, and uses ASCII order for | |
| 311 // equal length strings so this means that names are ascending. The hash | |
| 312 // character, #, is never given as input, but we need it because it's the | |
| 313 // implicit leading zero (otherwise we could not code names with leading | |
| 314 // dollar signs). | |
| 315 int fromBase88(String x) { | |
| 316 int answer = 0; | |
| 317 for (int i = 0; i < x.length; i++) { | |
| 318 int c = x.codeUnitAt(i); | |
| 319 // No support for Unicode minified identifiers in JS. | |
| 320 assert(c >= $$ && c <= $z); | |
| 321 answer *= 88; | |
| 322 answer += c - $HASH; | |
| 323 } | |
| 324 return answer; | |
| 325 } | |
| 326 | |
| 327 // Big endian encoding, A = 0, B = 1... | |
| 328 // A lower case letter terminates the number. | |
| 329 String toBase26(int x) { | |
| 330 int c = x; | |
| 331 var encodingChars = <int>[]; | |
| 332 encodingChars.add($a + (c % 26)); | |
| 333 while (true) { | |
| 334 c ~/= 26; | |
| 335 if (c == 0) break; | |
| 336 encodingChars.add($A + (c % 26)); | |
| 337 } | |
| 338 return new String.fromCharCodes(encodingChars.reversed.toList()); | |
| 339 } | |
| 340 | |
| 341 // Sort by length, then lexicographic. | |
| 342 int compare(String a, String b) { | |
| 343 if (a.length != b.length) return a.length - b.length; | |
| 344 return a.compareTo(b); | |
| 345 } | |
| 346 | |
| 347 List<String> shorts = | |
| 348 names.map((jsAst.Name name) => name.name) | |
| 349 .toList() | |
| 350 ..sort(compare); | |
| 351 | |
| 352 int previous = 0; | |
| 353 for (String short in shorts) { | |
| 354 if (short.length <= NsmEmitter.MAX_MINIFIED_LENGTH_FOR_DIFF_ENCODING) { | |
| 355 int base63 = fromBase88(short); | |
| 356 int diff = base63 - previous; | |
| 357 previous = base63; | |
| 358 String base26Diff = toBase26(diff); | |
| 359 diffEncoding.write(base26Diff); | |
| 360 } else { | |
| 361 if (diffEncoding.length != 0) { | |
| 362 diffEncoding.write(','); | |
| 363 } | |
| 364 diffEncoding.write(short); | |
| 365 } | |
| 366 } | |
| 367 } | |
| 368 | |
| 369 String _computeDiffEncoding() { | |
| 370 StringBuffer buffer = new StringBuffer(); | |
| 371 for (jsAst.ArrayInitializer list in ast) { | |
| 372 if (buffer.isNotEmpty) { | |
| 373 // Emit period that resets the diff base to zero when we switch to | |
| 374 // normal calling convention (this avoids the need to code negative | |
| 375 // diffs). | |
| 376 buffer.write("."); | |
| 377 } | |
| 378 List<jsAst.Name> names = list.elements; | |
| 379 _computeDiffEncodingForList(names, buffer); | |
| 380 } | |
| 381 return '"${buffer.toString()}"'; | |
| 382 } | |
| 383 | |
| 384 String get value { | |
| 385 if (_cachedValue == null) { | |
| 386 _cachedValue = _computeDiffEncoding(); | |
| 387 } | |
| 388 | |
| 389 return _cachedValue; | |
| 390 } | |
| 391 } | |
| OLD | NEW |