| 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 // Do not generate no such method handlers if there is no class. | |
| 23 if (compiler.codegenWorld.directlyInstantiatedClasses.isEmpty) return; | |
| 24 | |
| 25 String noSuchMethodName = namer.publicInstanceMethodNameByArity( | |
| 26 Compiler.NO_SUCH_METHOD, Compiler.NO_SUCH_METHOD_ARG_COUNT); | |
| 27 | |
| 28 // Keep track of the JavaScript names we've already added so we | |
| 29 // do not introduce duplicates (bad for code size). | |
| 30 Map<String, Selector> addedJsNames = new Map<String, Selector>(); | |
| 31 | |
| 32 void addNoSuchMethodHandlers(String ignore, Set<Selector> selectors) { | |
| 33 // Cache the object class and type. | |
| 34 ClassElement objectClass = compiler.objectClass; | |
| 35 DartType objectType = objectClass.rawType; | |
| 36 | |
| 37 for (Selector selector in selectors) { | |
| 38 TypeMask mask = selector.mask; | |
| 39 if (mask == null) { | |
| 40 mask = new TypeMask.subclass(compiler.objectClass, compiler.world); | |
| 41 } | |
| 42 | |
| 43 if (!mask.needsNoSuchMethodHandling(selector, compiler.world)) continue; | |
| 44 String jsName = namer.invocationMirrorInternalName(selector); | |
| 45 addedJsNames[jsName] = selector; | |
| 46 String reflectionName = emitter.getReflectionName(selector, jsName); | |
| 47 if (reflectionName != null) { | |
| 48 emitter.mangledFieldNames[jsName] = reflectionName; | |
| 49 } | |
| 50 } | |
| 51 } | |
| 52 | |
| 53 compiler.codegenWorld.invokedNames.forEach(addNoSuchMethodHandlers); | |
| 54 compiler.codegenWorld.invokedGetters.forEach(addNoSuchMethodHandlers); | |
| 55 compiler.codegenWorld.invokedSetters.forEach(addNoSuchMethodHandlers); | |
| 56 | |
| 57 // Set flag used by generateMethod helper below. If we have very few | |
| 58 // handlers we use addProperty for them all, rather than try to generate | |
| 59 // them at runtime. | |
| 60 bool haveVeryFewNoSuchMemberHandlers = | |
| 61 (addedJsNames.length < VERY_FEW_NO_SUCH_METHOD_HANDLERS); | |
| 62 | |
| 63 jsAst.Expression generateMethod(String jsName, Selector selector) { | |
| 64 // Values match JSInvocationMirror in js-helper library. | |
| 65 int type = selector.invocationMirrorKind; | |
| 66 List<String> parameterNames = | |
| 67 new List.generate(selector.argumentCount, (i) => '\$$i'); | |
| 68 | |
| 69 List<jsAst.Expression> argNames = | |
| 70 selector.getOrderedNamedArguments().map((String name) => | |
| 71 js.string(name)).toList(); | |
| 72 | |
| 73 String methodName = selector.invocationMirrorMemberName; | |
| 74 String internalName = namer.invocationMirrorInternalName(selector); | |
| 75 String reflectionName = emitter.getReflectionName(selector, internalName); | |
| 76 if (!haveVeryFewNoSuchMemberHandlers && | |
| 77 isTrivialNsmHandler(type, argNames, selector, internalName) && | |
| 78 reflectionName == null) { | |
| 79 trivialNsmHandlers.add(selector); | |
| 80 return null; | |
| 81 } | |
| 82 | |
| 83 assert(backend.isInterceptedName(Compiler.NO_SUCH_METHOD)); | |
| 84 jsAst.Expression expression = js('this.#(this, #(#, #, #, #, #))', [ | |
| 85 noSuchMethodName, | |
| 86 namer.elementAccess(backend.getCreateInvocationMirror()), | |
| 87 js.string(compiler.enableMinification ? | |
| 88 internalName : methodName), | |
| 89 js.string(internalName), | |
| 90 js.number(type), | |
| 91 new jsAst.ArrayInitializer.from(parameterNames.map(js)), | |
| 92 new jsAst.ArrayInitializer.from(argNames)]); | |
| 93 | |
| 94 if (backend.isInterceptedName(selector.name)) { | |
| 95 return js(r'function($receiver, #) { return # }', | |
| 96 [parameterNames, expression]); | |
| 97 } else { | |
| 98 return js(r'function(#) { return # }', [parameterNames, expression]); | |
| 99 } | |
| 100 } | |
| 101 | |
| 102 for (String jsName in addedJsNames.keys.toList()..sort()) { | |
| 103 Selector selector = addedJsNames[jsName]; | |
| 104 jsAst.Expression method = generateMethod(jsName, selector); | |
| 105 if (method != null) { | |
| 106 addProperty(jsName, method); | |
| 107 String reflectionName = emitter.getReflectionName(selector, jsName); | |
| 108 if (reflectionName != null) { | |
| 109 bool accessible = compiler.world.allFunctions.filter(selector).any( | |
| 110 (Element e) => backend.isAccessibleByReflection(e)); | |
| 111 addProperty('+$reflectionName', js(accessible ? '2' : '0')); | |
| 112 } | |
| 113 } | |
| 114 } | |
| 115 } | |
| 116 | |
| 117 // Identify the noSuchMethod handlers that are so simple that we can | |
| 118 // generate them programatically. | |
| 119 bool isTrivialNsmHandler( | |
| 120 int type, List argNames, Selector selector, String internalName) { | |
| 121 if (!generateTrivialNsmHandlers) return false; | |
| 122 // Check for interceptor calling convention. | |
| 123 if (backend.isInterceptedName(selector.name)) { | |
| 124 // We can handle the calling convention used by intercepted names in the | |
| 125 // diff encoding, but we don't use that for non-minified code. | |
| 126 if (!compiler.enableMinification) return false; | |
| 127 String shortName = namer.invocationMirrorInternalName(selector); | |
| 128 if (shortName.length > MAX_MINIFIED_LENGTH_FOR_DIFF_ENCODING) { | |
| 129 return false; | |
| 130 } | |
| 131 } | |
| 132 // Check for named arguments. | |
| 133 if (argNames.length != 0) return false; | |
| 134 // Check for unexpected name (this doesn't really happen). | |
| 135 if (internalName.startsWith(namer.getterPrefix[0])) return type == 1; | |
| 136 if (internalName.startsWith(namer.setterPrefix[0])) return type == 2; | |
| 137 return type == 0; | |
| 138 } | |
| 139 | |
| 140 /** | |
| 141 * Adds (at runtime) the handlers to the Object class which catch calls to | |
| 142 * methods that the object does not have. The handlers create an invocation | |
| 143 * mirror object. | |
| 144 * | |
| 145 * The current version only gives you the minified name when minifying (when | |
| 146 * not minifying this method is not called). | |
| 147 * | |
| 148 * In order to generate the noSuchMethod handlers we only need the minified | |
| 149 * name of the method. We test the first character of the minified name to | |
| 150 * determine if it is a getter or a setter, and we use the arguments array at | |
| 151 * runtime to get the number of arguments and their values. If the method | |
| 152 * involves named arguments etc. then we don't handle it here, but emit the | |
| 153 * handler method directly on the Object class. | |
| 154 * | |
| 155 * The minified names are mostly 1-4 character names, which we emit in sorted | |
| 156 * order (primary key is length, secondary ordering is lexicographic). This | |
| 157 * gives an order like ... dD dI dX da ... | |
| 158 * | |
| 159 * Gzip is good with repeated text, but it can't diff-encode, so we do that | |
| 160 * for it. We encode the minified names in a comma-separated string, but all | |
| 161 * the 1-4 character names are encoded before the first comma as a series of | |
| 162 * base 26 numbers. The last digit of each number is lower case, the others | |
| 163 * are upper case, so 1 is "b" and 26 is "Ba". | |
| 164 * | |
| 165 * We think of the minified names as base 88 numbers using the ASCII | |
| 166 * characters from # to z. The base 26 numbers each encode the delta from | |
| 167 * the previous minified name to the next. So if there is a minified name | |
| 168 * called Df and the next is Dh, then they are 2971 and 2973 when thought of | |
| 169 * as base 88 numbers. The difference is 2, which is "c" in lower-case- | |
| 170 * terminated base 26. | |
| 171 * | |
| 172 * The reason we don't encode long minified names with this method is that | |
| 173 * decoding the base 88 numbers would overflow JavaScript's puny integers. | |
| 174 * | |
| 175 * There are some selectors that have a special calling convention (because | |
| 176 * they are called with the receiver as the first argument). They need a | |
| 177 * slightly different noSuchMethod handler, so we handle these first. | |
| 178 */ | |
| 179 List<jsAst.Statement> buildTrivialNsmHandlers() { | |
| 180 List<jsAst.Statement> statements = <jsAst.Statement>[]; | |
| 181 if (trivialNsmHandlers.length == 0) return statements; | |
| 182 // Sort by calling convention, JS name length and by JS name. | |
| 183 trivialNsmHandlers.sort((a, b) { | |
| 184 bool aIsIntercepted = backend.isInterceptedName(a.name); | |
| 185 bool bIsIntercepted = backend.isInterceptedName(b.name); | |
| 186 if (aIsIntercepted != bIsIntercepted) return aIsIntercepted ? -1 : 1; | |
| 187 String aName = namer.invocationMirrorInternalName(a); | |
| 188 String bName = namer.invocationMirrorInternalName(b); | |
| 189 if (aName.length != bName.length) return aName.length - bName.length; | |
| 190 return aName.compareTo(bName); | |
| 191 }); | |
| 192 | |
| 193 // Find out how many selectors there are with the special calling | |
| 194 // convention. | |
| 195 int firstNormalSelector = trivialNsmHandlers.length; | |
| 196 for (int i = 0; i < trivialNsmHandlers.length; i++) { | |
| 197 if (!backend.isInterceptedName(trivialNsmHandlers[i].name)) { | |
| 198 firstNormalSelector = i; | |
| 199 break; | |
| 200 } | |
| 201 } | |
| 202 | |
| 203 // Get the short names (JS names, perhaps minified). | |
| 204 Iterable<String> shorts = trivialNsmHandlers.map((selector) => | |
| 205 namer.invocationMirrorInternalName(selector)); | |
| 206 final diffShorts = <String>[]; | |
| 207 var diffEncoding = new StringBuffer(); | |
| 208 | |
| 209 // Treat string as a number in base 88 with digits in ASCII order from # to | |
| 210 // z. The short name sorting is based on length, and uses ASCII order for | |
| 211 // equal length strings so this means that names are ascending. The hash | |
| 212 // character, #, is never given as input, but we need it because it's the | |
| 213 // implicit leading zero (otherwise we could not code names with leading | |
| 214 // dollar signs). | |
| 215 int fromBase88(String x) { | |
| 216 int answer = 0; | |
| 217 for (int i = 0; i < x.length; i++) { | |
| 218 int c = x.codeUnitAt(i); | |
| 219 // No support for Unicode minified identifiers in JS. | |
| 220 assert(c >= $$ && c <= $z); | |
| 221 answer *= 88; | |
| 222 answer += c - $HASH; | |
| 223 } | |
| 224 return answer; | |
| 225 } | |
| 226 | |
| 227 // Big endian encoding, A = 0, B = 1... | |
| 228 // A lower case letter terminates the number. | |
| 229 String toBase26(int x) { | |
| 230 int c = x; | |
| 231 var encodingChars = <int>[]; | |
| 232 encodingChars.add($a + (c % 26)); | |
| 233 while (true) { | |
| 234 c ~/= 26; | |
| 235 if (c == 0) break; | |
| 236 encodingChars.add($A + (c % 26)); | |
| 237 } | |
| 238 return new String.fromCharCodes(encodingChars.reversed.toList()); | |
| 239 } | |
| 240 | |
| 241 bool minify = compiler.enableMinification; | |
| 242 bool useDiffEncoding = minify && shorts.length > 30; | |
| 243 | |
| 244 int previous = 0; | |
| 245 int nameCounter = 0; | |
| 246 for (String short in shorts) { | |
| 247 // Emit period that resets the diff base to zero when we switch to normal | |
| 248 // calling convention (this avoids the need to code negative diffs). | |
| 249 if (useDiffEncoding && nameCounter == firstNormalSelector) { | |
| 250 diffEncoding.write("."); | |
| 251 previous = 0; | |
| 252 } | |
| 253 if (short.length <= MAX_MINIFIED_LENGTH_FOR_DIFF_ENCODING && | |
| 254 useDiffEncoding) { | |
| 255 int base63 = fromBase88(short); | |
| 256 int diff = base63 - previous; | |
| 257 previous = base63; | |
| 258 String base26Diff = toBase26(diff); | |
| 259 diffEncoding.write(base26Diff); | |
| 260 } else { | |
| 261 if (useDiffEncoding || diffEncoding.length != 0) { | |
| 262 diffEncoding.write(","); | |
| 263 } | |
| 264 diffEncoding.write(short); | |
| 265 } | |
| 266 nameCounter++; | |
| 267 } | |
| 268 | |
| 269 // Startup code that loops over the method names and puts handlers on the | |
| 270 // Object class to catch noSuchMethod invocations. | |
| 271 ClassElement objectClass = compiler.objectClass; | |
| 272 jsAst.Expression createInvocationMirror = namer.elementAccess( | |
| 273 backend.getCreateInvocationMirror()); | |
| 274 String noSuchMethodName = namer.publicInstanceMethodNameByArity( | |
| 275 Compiler.NO_SUCH_METHOD, Compiler.NO_SUCH_METHOD_ARG_COUNT); | |
| 276 var type = 0; | |
| 277 if (useDiffEncoding) { | |
| 278 statements.add(js.statement('''{ | |
| 279 var objectClassObject = | |
| 280 collectedClasses[#], // # is name of class Object. | |
| 281 shortNames = #.split(","), // # is diffEncoding. | |
| 282 nameNumber = 0, | |
| 283 diffEncodedString = shortNames[0], | |
| 284 calculatedShortNames = [0, 1]; // 0, 1 are args for splice. | |
| 285 // If we are loading a deferred library the object class will not be i
n | |
| 286 // the collectedClasses so objectClassObject is undefined, and we skip | |
| 287 // setting up the names. | |
| 288 | |
| 289 if (objectClassObject) { | |
| 290 if (objectClassObject instanceof Array) | |
| 291 objectClassObject = objectClassObject[1]; | |
| 292 for (var i = 0; i < diffEncodedString.length; i++) { | |
| 293 var codes = [], | |
| 294 diff = 0, | |
| 295 digit = diffEncodedString.charCodeAt(i); | |
| 296 if (digit == ${$PERIOD}) { | |
| 297 nameNumber = 0; | |
| 298 digit = diffEncodedString.charCodeAt(++i); | |
| 299 } | |
| 300 for (; digit <= ${$Z};) { | |
| 301 diff *= 26; | |
| 302 diff += (digit - ${$A}); | |
| 303 digit = diffEncodedString.charCodeAt(++i); | |
| 304 } | |
| 305 diff *= 26; | |
| 306 diff += (digit - ${$a}); | |
| 307 nameNumber += diff; | |
| 308 for (var remaining = nameNumber; | |
| 309 remaining > 0; | |
| 310 remaining = (remaining / 88) | 0) { | |
| 311 codes.unshift(${$HASH} + remaining % 88); | |
| 312 } | |
| 313 calculatedShortNames.push( | |
| 314 String.fromCharCode.apply(String, codes)); | |
| 315 } | |
| 316 shortNames.splice.apply(shortNames, calculatedShortNames); | |
| 317 } | |
| 318 }''', [ | |
| 319 js.string(namer.getNameOfClass(objectClass)), | |
| 320 js.string('$diffEncoding')])); | |
| 321 } else { | |
| 322 // No useDiffEncoding version. | |
| 323 Iterable<String> longs = trivialNsmHandlers.map((selector) => | |
| 324 selector.invocationMirrorMemberName); | |
| 325 statements.add(js.statement( | |
| 326 'var objectClassObject = collectedClasses[#],' | |
| 327 ' shortNames = #.split(",")', [ | |
| 328 js.string(namer.getNameOfClass(objectClass)), | |
| 329 js.string('$diffEncoding')])); | |
| 330 if (!minify) { | |
| 331 statements.add(js.statement('var longNames = #.split(",")', | |
| 332 js.string(longs.join(',')))); | |
| 333 } | |
| 334 statements.add(js.statement( | |
| 335 'if (objectClassObject instanceof Array)' | |
| 336 ' objectClassObject = objectClassObject[1];')); | |
| 337 } | |
| 338 | |
| 339 // TODO(9631): This is no longer valid for native methods. | |
| 340 String whatToPatch = emitter.nativeEmitter.handleNoSuchMethod ? | |
| 341 "Object.prototype" : | |
| 342 "objectClassObject"; | |
| 343 | |
| 344 List<jsAst.Expression> sliceOffsetArguments = | |
| 345 firstNormalSelector == 0 | |
| 346 ? [] | |
| 347 : (firstNormalSelector == shorts.length | |
| 348 ? [js.number(1)] | |
| 349 : [js('(j < #) ? 1 : 0', js.number(firstNormalSelector))]); | |
| 350 | |
| 351 var sliceOffsetParams = sliceOffsetArguments.isEmpty ? [] : ['sliceOffset']; | |
| 352 | |
| 353 statements.add(js.statement(''' | |
| 354 // If we are loading a deferred library the object class will not be in | |
| 355 // the collectedClasses so objectClassObject is undefined, and we skip | |
| 356 // setting up the names. | |
| 357 if (objectClassObject) { | |
| 358 for (var j = 0; j < shortNames.length; j++) { | |
| 359 var type = 0; | |
| 360 var short = shortNames[j]; | |
| 361 if (short[0] == "${namer.getterPrefix[0]}") type = 1; | |
| 362 if (short[0] == "${namer.setterPrefix[0]}") type = 2; | |
| 363 // Generate call to: | |
| 364 // | |
| 365 // createInvocationMirror(String name, internalName, type, | |
| 366 // arguments, argumentNames) | |
| 367 // | |
| 368 $whatToPatch[short] = (function(name, short, type, #) { | |
| 369 return function() { | |
| 370 return this.#(this, | |
| 371 #(name, short, type, | |
| 372 Array.prototype.slice.call(arguments, #), | |
| 373 [])); | |
| 374 } | |
| 375 })(#[j], short, type, #); | |
| 376 } | |
| 377 }''', [ | |
| 378 sliceOffsetParams, // parameter | |
| 379 noSuchMethodName, | |
| 380 createInvocationMirror, | |
| 381 sliceOffsetParams, // argument to slice | |
| 382 minify ? 'shortNames' : 'longNames', | |
| 383 sliceOffsetArguments | |
| 384 ])); | |
| 385 | |
| 386 return statements; | |
| 387 } | |
| 388 } | |
| OLD | NEW |