blob: 8b565f591bb8e7984c2c10dc69d93b18139183ed [file] [log] [blame]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of dart2js.js_emitter;
class NsmEmitter extends CodeEmitterHelper {
final List<Selector> trivialNsmHandlers = <Selector>[];
/// If this is true then we can generate the noSuchMethod handlers at startup
/// time, instead of them being emitted as part of the Object class.
bool get generateTrivialNsmHandlers => true;
// If we need fewer than this many noSuchMethod handlers we can save space by
// just emitting them in JS, rather than emitting the JS needed to generate
// them at run time.
static const VERY_FEW_NO_SUCH_METHOD_HANDLERS = 10;
static const MAX_MINIFIED_LENGTH_FOR_DIFF_ENCODING = 4;
void emitNoSuchMethodHandlers(DefineStubFunction defineStub) {
// Do not generate no such method handlers if there is no class.
if (compiler.codegenWorld.instantiatedClasses.isEmpty) return;
String noSuchMethodName = namer.publicInstanceMethodNameByArity(
Compiler.NO_SUCH_METHOD, Compiler.NO_SUCH_METHOD_ARG_COUNT);
// Keep track of the JavaScript names we've already added so we
// do not introduce duplicates (bad for code size).
Map<String, Selector> addedJsNames = new Map<String, Selector>();
void addNoSuchMethodHandlers(SourceString ignore, Set<Selector> selectors) {
// Cache the object class and type.
ClassElement objectClass = compiler.objectClass;
DartType objectType = objectClass.computeType(compiler);
for (Selector selector in selectors) {
TypeMask mask = selector.mask;
if (mask == null) {
mask = new TypeMask.subclass(compiler.objectClass);
}
if (!mask.needsNoSuchMethodHandling(selector, compiler)) continue;
String jsName = namer.invocationMirrorInternalName(selector);
addedJsNames[jsName] = selector;
String reflectionName = task.getReflectionName(selector, jsName);
if (reflectionName != null) {
task.mangledFieldNames[jsName] = reflectionName;
}
}
}
compiler.codegenWorld.invokedNames.forEach(addNoSuchMethodHandlers);
compiler.codegenWorld.invokedGetters.forEach(addNoSuchMethodHandlers);
compiler.codegenWorld.invokedSetters.forEach(addNoSuchMethodHandlers);
// Set flag used by generateMethod helper below. If we have very few
// handlers we use defineStub for them all, rather than try to generate them
// at runtime.
bool haveVeryFewNoSuchMemberHandlers =
(addedJsNames.length < VERY_FEW_NO_SUCH_METHOD_HANDLERS);
jsAst.Expression generateMethod(String jsName, Selector selector) {
// Values match JSInvocationMirror in js-helper library.
int type = selector.invocationMirrorKind;
List<jsAst.Parameter> parameters = <jsAst.Parameter>[];
CodeBuffer args = new CodeBuffer();
for (int i = 0; i < selector.argumentCount; i++) {
parameters.add(new jsAst.Parameter('\$$i'));
}
List<jsAst.Expression> argNames =
selector.getOrderedNamedArguments().map((SourceString name) =>
js.string(name.slowToString())).toList();
String methodName = selector.invocationMirrorMemberName;
String internalName = namer.invocationMirrorInternalName(selector);
String reflectionName = task.getReflectionName(selector, internalName);
if (!haveVeryFewNoSuchMemberHandlers &&
isTrivialNsmHandler(type, argNames, selector, internalName) &&
reflectionName == null) {
trivialNsmHandlers.add(selector);
return null;
}
assert(backend.isInterceptedName(Compiler.NO_SUCH_METHOD));
jsAst.Expression expression = js('this.$noSuchMethodName')(
[js('this'),
namer.elementAccess(backend.getCreateInvocationMirror())([
js.string(compiler.enableMinification ?
internalName : methodName),
js.string(internalName),
type,
new jsAst.ArrayInitializer.from(
parameters.map((param) => js(param.name)).toList()),
new jsAst.ArrayInitializer.from(argNames)])]);
parameters = backend.isInterceptedName(selector.name)
? ([new jsAst.Parameter('\$receiver')]..addAll(parameters))
: parameters;
return js.fun(parameters, js.return_(expression));
}
for (String jsName in addedJsNames.keys.toList()..sort()) {
Selector selector = addedJsNames[jsName];
jsAst.Expression method = generateMethod(jsName, selector);
if (method != null) {
defineStub(jsName, method);
String reflectionName = task.getReflectionName(selector, jsName);
if (reflectionName != null) {
bool accessible = compiler.world.allFunctions.filter(selector).any(
(Element e) => backend.isAccessibleByReflection(e));
defineStub('+$reflectionName', js(accessible ? '1' : '0'));
}
}
}
}
// Identify the noSuchMethod handlers that are so simple that we can
// generate them programatically.
bool isTrivialNsmHandler(
int type, List argNames, Selector selector, String internalName) {
if (!generateTrivialNsmHandlers) return false;
// Check for interceptor calling convention.
if (backend.isInterceptedName(selector.name)) {
// We can handle the calling convention used by intercepted names in the
// diff encoding, but we don't use that for non-minified code.
if (!compiler.enableMinification) return false;
String shortName = namer.invocationMirrorInternalName(selector);
if (shortName.length > MAX_MINIFIED_LENGTH_FOR_DIFF_ENCODING) {
return false;
}
}
// Check for named arguments.
if (argNames.length != 0) return false;
// Check for unexpected name (this doesn't really happen).
if (internalName.startsWith(namer.getterPrefix[0])) return type == 1;
if (internalName.startsWith(namer.setterPrefix[0])) return type == 2;
return type == 0;
}
/**
* Adds (at runtime) the handlers to the Object class which catch calls to
* methods that the object does not have. The handlers create an invocation
* mirror object.
*
* The current version only gives you the minified name when minifying (when
* not minifying this method is not called).
*
* In order to generate the noSuchMethod handlers we only need the minified
* name of the method. We test the first character of the minified name to
* determine if it is a getter or a setter, and we use the arguments array at
* runtime to get the number of arguments and their values. If the method
* involves named arguments etc. then we don't handle it here, but emit the
* handler method directly on the Object class.
*
* The minified names are mostly 1-4 character names, which we emit in sorted
* order (primary key is length, secondary ordering is lexicographic). This
* gives an order like ... dD dI dX da ...
*
* Gzip is good with repeated text, but it can't diff-encode, so we do that
* for it. We encode the minified names in a comma-separated string, but all
* the 1-4 character names are encoded before the first comma as a series of
* base 26 numbers. The last digit of each number is lower case, the others
* are upper case, so 1 is "b" and 26 is "Ba".
*
* We think of the minified names as base 88 numbers using the ASCII
* characters from # to z. The base 26 numbers each encode the delta from
* the previous minified name to the next. So if there is a minified name
* called Df and the next is Dh, then they are 2971 and 2973 when thought of
* as base 88 numbers. The difference is 2, which is "c" in lower-case-
* terminated base 26.
*
* The reason we don't encode long minified names with this method is that
* decoding the base 88 numbers would overflow JavaScript's puny integers.
*
* There are some selectors that have a special calling convention (because
* they are called with the receiver as the first argument). They need a
* slightly different noSuchMethod handler, so we handle these first.
*/
void addTrivialNsmHandlers(List<jsAst.Node> statements) {
if (trivialNsmHandlers.length == 0) return;
// Sort by calling convention, JS name length and by JS name.
trivialNsmHandlers.sort((a, b) {
bool aIsIntercepted = backend.isInterceptedName(a.name);
bool bIsIntercepted = backend.isInterceptedName(b.name);
if (aIsIntercepted != bIsIntercepted) return aIsIntercepted ? -1 : 1;
String aName = namer.invocationMirrorInternalName(a);
String bName = namer.invocationMirrorInternalName(b);
if (aName.length != bName.length) return aName.length - bName.length;
return aName.compareTo(bName);
});
// Find out how many selectors there are with the special calling
// convention.
int firstNormalSelector = trivialNsmHandlers.length;
for (int i = 0; i < trivialNsmHandlers.length; i++) {
if (!backend.isInterceptedName(trivialNsmHandlers[i].name)) {
firstNormalSelector = i;
break;
}
}
// Get the short names (JS names, perhaps minified).
Iterable<String> shorts = trivialNsmHandlers.map((selector) =>
namer.invocationMirrorInternalName(selector));
final diffShorts = <String>[];
var diffEncoding = new StringBuffer();
// Treat string as a number in base 88 with digits in ASCII order from # to
// z. The short name sorting is based on length, and uses ASCII order for
// equal length strings so this means that names are ascending. The hash
// character, #, is never given as input, but we need it because it's the
// implicit leading zero (otherwise we could not code names with leading
// dollar signs).
int fromBase88(String x) {
int answer = 0;
for (int i = 0; i < x.length; i++) {
int c = x.codeUnitAt(i);
// No support for Unicode minified identifiers in JS.
assert(c >= $$ && c <= $z);
answer *= 88;
answer += c - $HASH;
}
return answer;
}
// Big endian encoding, A = 0, B = 1...
// A lower case letter terminates the number.
String toBase26(int x) {
int c = x;
var encodingChars = <int>[];
encodingChars.add($a + (c % 26));
while (true) {
c ~/= 26;
if (c == 0) break;
encodingChars.add($A + (c % 26));
}
return new String.fromCharCodes(encodingChars.reversed.toList());
}
bool minify = compiler.enableMinification;
bool useDiffEncoding = minify && shorts.length > 30;
int previous = 0;
int nameCounter = 0;
for (String short in shorts) {
// Emit period that resets the diff base to zero when we switch to normal
// calling convention (this avoids the need to code negative diffs).
if (useDiffEncoding && nameCounter == firstNormalSelector) {
diffEncoding.write(".");
previous = 0;
}
if (short.length <= MAX_MINIFIED_LENGTH_FOR_DIFF_ENCODING &&
useDiffEncoding) {
int base63 = fromBase88(short);
int diff = base63 - previous;
previous = base63;
String base26Diff = toBase26(diff);
diffEncoding.write(base26Diff);
} else {
if (useDiffEncoding || diffEncoding.length != 0) {
diffEncoding.write(",");
}
diffEncoding.write(short);
}
nameCounter++;
}
// Startup code that loops over the method names and puts handlers on the
// Object class to catch noSuchMethod invocations.
ClassElement objectClass = compiler.objectClass;
String createInvocationMirror = namer.isolateAccess(
backend.getCreateInvocationMirror());
String noSuchMethodName = namer.publicInstanceMethodNameByArity(
Compiler.NO_SUCH_METHOD, Compiler.NO_SUCH_METHOD_ARG_COUNT);
var type = 0;
if (useDiffEncoding) {
statements.addAll([
js('var objectClassObject = '
' collectedClasses["${namer.getNameOfClass(objectClass)}"],'
' shortNames = "$diffEncoding".split(","),'
' nameNumber = 0,'
' diffEncodedString = shortNames[0],'
' calculatedShortNames = [0, 1]'), // 0, 1 are args for splice.
js.if_('objectClassObject instanceof Array',
js('objectClassObject = objectClassObject[1]')),
js.for_('var i = 0', 'i < diffEncodedString.length', 'i++', [
js('var codes = [],'
' diff = 0,'
' digit = diffEncodedString.charCodeAt(i)'),
js.if_('digit == ${$PERIOD}', [
js('nameNumber = 0'),
js('digit = diffEncodedString.charCodeAt(++i)')
]),
js.while_('digit <= ${$Z}', [
js('diff *= 26'),
js('diff += (digit - ${$A})'),
js('digit = diffEncodedString.charCodeAt(++i)')
]),
js('diff *= 26'),
js('diff += (digit - ${$a})'),
js('nameNumber += diff'),
js.for_('var remaining = nameNumber',
'remaining > 0',
'remaining = (remaining / 88) | 0', [
js('codes.unshift(${$HASH} + remaining % 88)')
]),
js('calculatedShortNames.push('
' String.fromCharCode.apply(String, codes))')
]),
js('shortNames.splice.apply(shortNames, calculatedShortNames)')
]);
} else {
// No useDiffEncoding version.
Iterable<String> longs = trivialNsmHandlers.map((selector) =>
selector.invocationMirrorMemberName);
String longNamesConstant = minify ? "" :
',longNames = "${longs.join(",")}".split(",")';
statements.add(
js('var objectClassObject = '
' collectedClasses["${namer.getNameOfClass(objectClass)}"],'
' shortNames = "$diffEncoding".split(",")'
' $longNamesConstant'));
statements.add(
js.if_('objectClassObject instanceof Array',
js('objectClassObject = objectClassObject[1]')));
}
String sliceOffset = ', (j < $firstNormalSelector) ? 1 : 0';
if (firstNormalSelector == 0) sliceOffset = '';
if (firstNormalSelector == shorts.length) sliceOffset = ', 1';
String whatToPatch = task.nativeEmitter.handleNoSuchMethod ?
"Object.prototype" :
"objectClassObject";
var params = ['name', 'short', 'type'];
var sliceOffsetParam = '';
var slice = 'Array.prototype.slice.call';
if (!sliceOffset.isEmpty) {
sliceOffsetParam = ', sliceOffset';
params.add('sliceOffset');
}
statements.addAll([
js.for_('var j = 0', 'j < shortNames.length', 'j++', [
js('var type = 0'),
js('var short = shortNames[j]'),
js.if_('short[0] == "${namer.getterPrefix[0]}"', js('type = 1')),
js.if_('short[0] == "${namer.setterPrefix[0]}"', js('type = 2')),
// Generate call to:
// createInvocationMirror(String name, internalName, type, arguments,
// argumentNames)
js('$whatToPatch[short] = #(${minify ? "shortNames" : "longNames"}[j], '
'short, type$sliceOffset)',
js.fun(params, [js.return_(js.fun([],
[js.return_(js(
'this.$noSuchMethodName('
'this, '
'$createInvocationMirror('
'name, short, type, '
'$slice(arguments$sliceOffsetParam), []))'))]))]))
])
]);
}
}