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 |