blob: 25a4fd3c8442962c5661756e68a08dce148cc33a [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;
/// This class should morph into something that makes it easy to build
/// JavaScript representations of libraries, class-sides, and instance-sides.
/// Initially, it is just a placeholder for code that is moved from
/// [CodeEmitterTask].
class ContainerBuilder extends CodeEmitterHelper {
final Map<Element, Element> staticGetters = new Map<Element, Element>();
/// A cache of synthesized closures for top-level, static or
/// instance methods.
final Map<String, Element> methodClosures = <String, Element>{};
/**
* Generate stubs to handle invocation of methods with optional
* arguments.
*
* A method like [: foo([x]) :] may be invoked by the following
* calls: [: foo(), foo(1), foo(x: 1) :]. See the sources of this
* function for detailed examples.
*/
void addParameterStub(FunctionElement member,
Selector selector,
DefineStubFunction defineStub,
Set<String> alreadyGenerated) {
FunctionSignature parameters = member.computeSignature(compiler);
int positionalArgumentCount = selector.positionalArgumentCount;
if (positionalArgumentCount == parameters.parameterCount) {
assert(selector.namedArgumentCount == 0);
return;
}
if (parameters.optionalParametersAreNamed
&& selector.namedArgumentCount == parameters.optionalParameterCount) {
// If the selector has the same number of named arguments as the element,
// we don't need to add a stub. The call site will hit the method
// directly.
return;
}
ConstantHandler handler = compiler.constantHandler;
List<SourceString> names = selector.getOrderedNamedArguments();
String invocationName = namer.invocationName(selector);
if (alreadyGenerated.contains(invocationName)) return;
alreadyGenerated.add(invocationName);
bool isInterceptedMethod = backend.isInterceptedMethod(member);
// If the method is intercepted, we need to also pass the actual receiver.
int extraArgumentCount = isInterceptedMethod ? 1 : 0;
// Use '$receiver' to avoid clashes with other parameter names. Using
// '$receiver' works because [:namer.safeName:] used for getting parameter
// names never returns a name beginning with a single '$'.
String receiverArgumentName = r'$receiver';
// The parameters that this stub takes.
List<jsAst.Parameter> parametersBuffer =
new List<jsAst.Parameter>(selector.argumentCount + extraArgumentCount);
// The arguments that will be passed to the real method.
List<jsAst.Expression> argumentsBuffer =
new List<jsAst.Expression>(
parameters.parameterCount + extraArgumentCount);
int count = 0;
if (isInterceptedMethod) {
count++;
parametersBuffer[0] = new jsAst.Parameter(receiverArgumentName);
argumentsBuffer[0] = js(receiverArgumentName);
task.interceptorInvocationNames.add(invocationName);
}
int optionalParameterStart = positionalArgumentCount + extraArgumentCount;
// Includes extra receiver argument when using interceptor convention
int indexOfLastOptionalArgumentInParameters = optionalParameterStart - 1;
TreeElements elements =
compiler.enqueuer.resolution.getCachedElements(member);
int parameterIndex = 0;
parameters.orderedForEachParameter((Element element) {
// Use generic names for closures to facilitate code sharing.
String jsName = member is ClosureInvocationElement
? 'p${parameterIndex++}'
: backend.namer.safeName(element.name.slowToString());
assert(jsName != receiverArgumentName);
if (count < optionalParameterStart) {
parametersBuffer[count] = new jsAst.Parameter(jsName);
argumentsBuffer[count] = js(jsName);
} else {
int index = names.indexOf(element.name);
if (index != -1) {
indexOfLastOptionalArgumentInParameters = count;
// The order of the named arguments is not the same as the
// one in the real method (which is in Dart source order).
argumentsBuffer[count] = js(jsName);
parametersBuffer[optionalParameterStart + index] =
new jsAst.Parameter(jsName);
} else {
Constant value = handler.initialVariableValues[element];
if (value == null) {
argumentsBuffer[count] = task.constantReference(new NullConstant());
} else {
if (!value.isNull()) {
// If the value is the null constant, we should not pass it
// down to the native method.
indexOfLastOptionalArgumentInParameters = count;
}
argumentsBuffer[count] = task.constantReference(value);
}
}
}
count++;
});
List body;
if (member.hasFixedBackendName()) {
body = task.nativeEmitter.generateParameterStubStatements(
member, isInterceptedMethod, invocationName,
parametersBuffer, argumentsBuffer,
indexOfLastOptionalArgumentInParameters);
} else {
body = [js.return_(
js('this')[namer.getNameOfInstanceMember(member)](argumentsBuffer))];
}
jsAst.Fun function = js.fun(parametersBuffer, body);
defineStub(invocationName, function);
String reflectionName = task.getReflectionName(selector, invocationName);
if (reflectionName != null) {
var reflectable =
js(backend.isAccessibleByReflection(member) ? '1' : '0');
defineStub('+$reflectionName', reflectable);
}
}
void addParameterStubs(FunctionElement member,
DefineStubFunction defineStub) {
// We fill the lists depending on the selector. For example,
// take method foo:
// foo(a, b, {c, d});
//
// We may have multiple ways of calling foo:
// (1) foo(1, 2);
// (2) foo(1, 2, c: 3);
// (3) foo(1, 2, d: 4);
// (4) foo(1, 2, c: 3, d: 4);
// (5) foo(1, 2, d: 4, c: 3);
//
// What we generate at the call sites are:
// (1) foo$2(1, 2);
// (2) foo$3$c(1, 2, 3);
// (3) foo$3$d(1, 2, 4);
// (4) foo$4$c$d(1, 2, 3, 4);
// (5) foo$4$c$d(1, 2, 3, 4);
//
// The stubs we generate are (expressed in Dart):
// (1) foo$2(a, b) => foo$4$c$d(a, b, null, null)
// (2) foo$3$c(a, b, c) => foo$4$c$d(a, b, c, null);
// (3) foo$3$d(a, b, d) => foo$4$c$d(a, b, null, d);
// (4) No stub generated, call is direct.
// (5) No stub generated, call is direct.
// Keep a cache of which stubs have already been generated, to
// avoid duplicates. Note that even if selectors are
// canonicalized, we would still need this cache: a typed selector
// on A and a typed selector on B could yield the same stub.
Set<String> generatedStubNames = new Set<String>();
bool isClosureInvocation =
member.name == namer.closureInvocationSelectorName;
if (backend.isNeededForReflection(member) ||
(compiler.enabledFunctionApply && isClosureInvocation)) {
// If [Function.apply] is called, we pessimistically compile all
// possible stubs for this closure.
FunctionSignature signature = member.computeSignature(compiler);
Set<Selector> selectors = signature.optionalParametersAreNamed
? computeSeenNamedSelectors(member)
: computeOptionalSelectors(signature, member);
for (Selector selector in selectors) {
addParameterStub(member, selector, defineStub, generatedStubNames);
}
if (signature.optionalParametersAreNamed && isClosureInvocation) {
addCatchAllParameterStub(member, signature, defineStub);
}
} else {
Set<Selector> selectors = compiler.codegenWorld.invokedNames[member.name];
if (selectors == null) return;
for (Selector selector in selectors) {
if (!selector.applies(member, compiler)) continue;
addParameterStub(member, selector, defineStub, generatedStubNames);
}
}
}
Set<Selector> computeSeenNamedSelectors(FunctionElement element) {
Set<Selector> selectors = compiler.codegenWorld.invokedNames[element.name];
Set<Selector> result = new Set<Selector>();
if (selectors == null) return result;
for (Selector selector in selectors) {
if (!selector.applies(element, compiler)) continue;
result.add(selector);
}
return result;
}
void addCatchAllParameterStub(FunctionElement member,
FunctionSignature signature,
DefineStubFunction defineStub) {
// See Primities.applyFunction in js_helper.dart for details.
List<jsAst.Property> properties = <jsAst.Property>[];
for (Element element in signature.orderedOptionalParameters) {
String jsName = backend.namer.safeName(element.name.slowToString());
Constant value = compiler.constantHandler.initialVariableValues[element];
jsAst.Expression reference = null;
if (value == null) {
reference = new jsAst.LiteralNull();
} else {
reference = task.constantReference(value);
}
properties.add(new jsAst.Property(js.string(jsName), reference));
}
defineStub(
backend.namer.callCatchAllName,
js.fun([], js.return_(new jsAst.ObjectInitializer(properties))));
}
/**
* Compute the set of possible selectors in the presence of optional
* non-named parameters.
*/
Set<Selector> computeOptionalSelectors(FunctionSignature signature,
FunctionElement element) {
Set<Selector> selectors = new Set<Selector>();
// Add the selector that does not have any optional argument.
selectors.add(new Selector(SelectorKind.CALL,
element.name,
element.getLibrary(),
signature.requiredParameterCount,
<SourceString>[]));
// For each optional parameter, we increment the number of passed
// argument.
for (int i = 1; i <= signature.optionalParameterCount; i++) {
selectors.add(new Selector(SelectorKind.CALL,
element.name,
element.getLibrary(),
signature.requiredParameterCount + i,
<SourceString>[]));
}
return selectors;
}
void emitStaticFunctionGetters(CodeBuffer eagerBuffer) {
task.addComment('Static function getters', task.mainBuffer);
for (FunctionElement element in
Elements.sortedByPosition(staticGetters.keys)) {
Element closure = staticGetters[element];
CodeBuffer buffer =
task.isDeferred(element) ? task.deferredConstants : eagerBuffer;
String closureClass = namer.isolateAccess(closure);
String name = namer.getStaticClosureName(element);
String closureName = namer.getStaticClosureName(element);
jsAst.Node assignment = js(
'init.globalFunctions["$closureName"] ='
' ${namer.globalObjectFor(element)}.$name ='
' new $closureClass(#, "$closureName")',
namer.elementAccess(element));
buffer.write(jsAst.prettyPrint(assignment, compiler));
buffer.write('$N');
}
}
void emitStaticFunctionClosures() {
Set<FunctionElement> functionsNeedingGetter =
compiler.codegenWorld.staticFunctionsNeedingGetter;
for (FunctionElement element in
Elements.sortedByPosition(functionsNeedingGetter)) {
String superName = namer.getNameOfClass(compiler.closureClass);
int parameterCount = element.functionSignature.parameterCount;
String name = 'Closure\$$parameterCount';
assert(task.instantiatedClasses.contains(compiler.closureClass));
ClassElement closureClassElement = new ClosureClassElement(
null, new SourceString(name), compiler, element,
element.getCompilationUnit());
// Now add the methods on the closure class. The instance method does not
// have the correct name. Since [addParameterStubs] use the name to create
// its stubs we simply create a fake element with the correct name.
// Note: the callElement will not have any enclosingElement.
FunctionElement callElement =
new ClosureInvocationElement(namer.closureInvocationSelectorName,
element);
String invocationName = namer.instanceMethodName(callElement);
String mangledName = namer.getNameOfClass(closureClassElement);
// Define the constructor with a name so that Object.toString can
// find the class name of the closure class.
ClassBuilder closureBuilder = new ClassBuilder();
// If a static function is used as a closure we need to add its name
// in case it is used in spawnFunction.
String methodName = namer.STATIC_CLOSURE_NAME_NAME;
List<String> fieldNames = <String>[invocationName, methodName];
closureBuilder.addProperty('',
js.string("$superName;${fieldNames.join(',')}"));
addParameterStubs(callElement, closureBuilder.addProperty);
void emitFunctionTypeSignature(Element method, FunctionType methodType) {
RuntimeTypes rti = backend.rti;
// [:() => null:] is dummy encoding of [this] which is never needed for
// the encoding of the type of the static [method].
jsAst.Expression encoding =
rti.getSignatureEncoding(methodType, js('null'));
String operatorSignature = namer.operatorSignature();
// TODO(johnniwinther): Make MiniJsParser support function expressions.
closureBuilder.addProperty(operatorSignature, encoding);
}
void emitIsFunctionTypeTest(FunctionType functionType) {
String operator = namer.operatorIsType(functionType);
closureBuilder.addProperty(operator, js('true'));
}
FunctionType methodType = element.computeType(compiler);
Map<FunctionType, bool> functionTypeChecks =
task.typeTestEmitter.getFunctionTypeChecksOn(methodType);
task.typeTestEmitter.generateFunctionTypeTests(
element, methodType, functionTypeChecks,
emitFunctionTypeSignature, emitIsFunctionTypeTest);
closureClassElement =
addClosureIfNew(closureBuilder, closureClassElement, fieldNames);
staticGetters[element] = closureClassElement;
}
}
ClassElement addClosureIfNew(ClassBuilder builder,
ClassElement closure,
List<String> fieldNames) {
String key =
jsAst.prettyPrint(builder.toObjectInitializer(), compiler).getText();
return methodClosures.putIfAbsent(key, () {
String mangledName = namer.getNameOfClass(closure);
emitClosureInPrecompiledFunction(mangledName, fieldNames);
return closure;
});
}
void emitClosureInPrecompiledFunction(String mangledName,
List<String> fieldNames) {
List<String> fields = fieldNames;
String constructorName = mangledName;
task.precompiledFunction.add(new jsAst.FunctionDeclaration(
new jsAst.VariableDeclaration(constructorName),
js.fun(fields, fields.map(
(name) => js('this.$name = $name')).toList())));
task.precompiledFunction.addAll([
js('$constructorName.builtin\$cls = "$constructorName"'),
js('\$desc=\$collectedClasses.$constructorName'),
js.if_('\$desc instanceof Array', js('\$desc = \$desc[1]')),
js('$constructorName.prototype = \$desc'),
]);
task.precompiledConstructorNames.add(js(constructorName));
}
/**
* Documentation wanted -- johnniwinther
*
* Invariant: [member] must be a declaration element.
*/
void emitDynamicFunctionGetter(FunctionElement member,
DefineStubFunction defineStub) {
assert(invariant(member, member.isDeclaration));
assert(task.instantiatedClasses.contains(compiler.boundClosureClass));
// For every method that has the same name as a property-get we create a
// getter that returns a bound closure. Say we have a class 'A' with method
// 'foo' and somewhere in the code there is a dynamic property get of
// 'foo'. Then we generate the following code (in pseudo Dart/JavaScript):
//
// class A {
// foo(x, y, z) { ... } // Original function.
// get foo { return new BoundClosure499(this, "foo"); }
// }
// class BoundClosure499 extends BoundClosure {
// BoundClosure499(this.self, this.name);
// $call3(x, y, z) { return self[name](x, y, z); }
// }
bool hasOptionalParameters = member.optionalParameterCount(compiler) != 0;
int parameterCount = member.parameterCount(compiler);
// Intercepted methods take an extra parameter, which is the
// receiver of the call.
bool inInterceptor = backend.isInterceptedMethod(member);
List<String> fieldNames = <String>[];
compiler.boundClosureClass.forEachInstanceField((_, Element field) {
fieldNames.add(namer.getNameOfInstanceMember(field));
});
ClassElement classElement = member.getEnclosingClass();
String name = inInterceptor
? 'BoundClosure\$i${parameterCount}'
: 'BoundClosure\$${parameterCount}';
ClassElement closureClassElement = new ClosureClassElement(
null, new SourceString(name), compiler, member,
member.getCompilationUnit());
String superName = namer.getNameOfClass(closureClassElement.superclass);
// Define the constructor with a name so that Object.toString can
// find the class name of the closure class.
ClassBuilder boundClosureBuilder = new ClassBuilder();
boundClosureBuilder.addProperty('',
js.string("$superName;${fieldNames.join(',')}"));
// Now add the methods on the closure class. The instance method does not
// have the correct name. Since [addParameterStubs] use the name to create
// its stubs we simply create a fake element with the correct name.
// Note: the callElement will not have any enclosingElement.
FunctionElement callElement = new ClosureInvocationElement(
namer.closureInvocationSelectorName, member);
String invocationName = namer.instanceMethodName(callElement);
List<String> parameters = <String>[];
List<jsAst.Expression> arguments =
<jsAst.Expression>[js('this')[fieldNames[0]]];
if (inInterceptor) {
arguments.add(js('this')[fieldNames[2]]);
}
for (int i = 0; i < parameterCount; i++) {
String name = 'p$i';
parameters.add(name);
arguments.add(js(name));
}
jsAst.Expression fun = js.fun(
parameters,
js.return_(
js('this')[fieldNames[1]]['call'](arguments)));
boundClosureBuilder.addProperty(invocationName, fun);
addParameterStubs(callElement, boundClosureBuilder.addProperty);
void emitFunctionTypeSignature(Element method, FunctionType methodType) {
jsAst.Expression encoding = backend.rti.getSignatureEncoding(
methodType, js('this')[fieldNames[0]]);
String operatorSignature = namer.operatorSignature();
boundClosureBuilder.addProperty(operatorSignature, encoding);
}
void emitIsFunctionTypeTest(FunctionType functionType) {
String operator = namer.operatorIsType(functionType);
boundClosureBuilder.addProperty(operator,
new jsAst.LiteralBool(true));
}
DartType memberType = member.computeType(compiler);
Map<FunctionType, bool> functionTypeChecks =
task.typeTestEmitter.getFunctionTypeChecksOn(memberType);
task.typeTestEmitter.generateFunctionTypeTests(
member, memberType, functionTypeChecks,
emitFunctionTypeSignature, emitIsFunctionTypeTest);
closureClassElement =
addClosureIfNew(boundClosureBuilder, closureClassElement, fieldNames);
String closureClass = namer.isolateAccess(closureClassElement);
// And finally the getter.
String getterName = namer.getterName(member);
String targetName = namer.instanceMethodName(member);
parameters = <String>[];
jsAst.PropertyAccess method =
backend.namer.elementAccess(classElement)['prototype'][targetName];
arguments = <jsAst.Expression>[js('this'), method];
if (inInterceptor) {
String receiverArg = fieldNames[2];
parameters.add(receiverArg);
arguments.add(js(receiverArg));
} else {
// Put null in the intercepted receiver field.
arguments.add(new jsAst.LiteralNull());
}
arguments.add(js.string(targetName));
jsAst.Expression getterFunction = js.fun(
parameters, js.return_(js(closureClass).newWith(arguments)));
defineStub(getterName, getterFunction);
}
/**
* Documentation wanted -- johnniwinther
*
* Invariant: [member] must be a declaration element.
*/
void emitCallStubForGetter(Element member,
Set<Selector> selectors,
DefineStubFunction defineStub) {
assert(invariant(member, member.isDeclaration));
LibraryElement memberLibrary = member.getLibrary();
// If the method is intercepted, the stub gets the
// receiver explicitely and we need to pass it to the getter call.
bool isInterceptedMethod = backend.isInterceptedMethod(member);
const String receiverArgumentName = r'$receiver';
jsAst.Expression buildGetter() {
if (member.isGetter()) {
String getterName = namer.getterName(member);
return js('this')[getterName](
isInterceptedMethod
? <jsAst.Expression>[js(receiverArgumentName)]
: <jsAst.Expression>[]);
} else {
String fieldName = member.hasFixedBackendName()
? member.fixedBackendName()
: namer.instanceFieldName(member);
return js('this')[fieldName];
}
}
// Two selectors may match but differ only in type. To avoid generating
// identical stubs for each we track untyped selectors which already have
// stubs.
Set<Selector> generatedSelectors = new Set<Selector>();
for (Selector selector in selectors) {
if (selector.applies(member, compiler)) {
selector = selector.asUntyped;
if (generatedSelectors.contains(selector)) continue;
generatedSelectors.add(selector);
String invocationName = namer.invocationName(selector);
Selector callSelector = new Selector.callClosureFrom(selector);
String closureCallName = namer.invocationName(callSelector);
List<jsAst.Parameter> parameters = <jsAst.Parameter>[];
List<jsAst.Expression> arguments = <jsAst.Expression>[];
if (isInterceptedMethod) {
parameters.add(new jsAst.Parameter(receiverArgumentName));
}
for (int i = 0; i < selector.argumentCount; i++) {
String name = 'arg$i';
parameters.add(new jsAst.Parameter(name));
arguments.add(js(name));
}
jsAst.Fun function = js.fun(
parameters,
js.return_(buildGetter()[closureCallName](arguments)));
defineStub(invocationName, function);
}
}
}
/**
* Documentation wanted -- johnniwinther
*
* Invariant: [member] must be a declaration element.
*/
void emitExtraAccessors(Element member, ClassBuilder builder) {
assert(invariant(member, member.isDeclaration));
if (member.isGetter() || member.isField()) {
Set<Selector> selectors = compiler.codegenWorld.invokedNames[member.name];
if (selectors != null && !selectors.isEmpty) {
emitCallStubForGetter(member, selectors, builder.addProperty);
}
} else if (member.isFunction()) {
if (compiler.codegenWorld.hasInvokedGetter(member, compiler)) {
emitDynamicFunctionGetter(member, builder.addProperty);
}
}
}
void addMember(Element member, ClassBuilder builder) {
assert(invariant(member, member.isDeclaration));
if (member.isField()) {
addMemberField(member, builder);
} else if (member.isFunction() ||
member.isGenerativeConstructorBody() ||
member.isGenerativeConstructor() ||
member.isAccessor()) {
addMemberMethod(member, builder);
} else {
compiler.internalErrorOnElement(
member, 'unexpected kind: "${member.kind}"');
}
if (member.isInstanceMember()) emitExtraAccessors(member, builder);
}
void addMemberMethod(FunctionElement member, ClassBuilder builder) {
if (member.isAbstract(compiler)) return;
jsAst.Expression code = backend.generatedCode[member];
if (code == null) return;
String name = namer.getNameOfMember(member);
if (backend.isInterceptedMethod(member)) {
task.interceptorInvocationNames.add(name);
}
code = extendWithMetadata(member, code);
builder.addProperty(name, code);
String reflectionName = task.getReflectionName(member, name);
if (reflectionName != null) {
var reflectable =
js(backend.isAccessibleByReflection(member) ? '1' : '0');
builder.addProperty('+$reflectionName', reflectable);
jsAst.Node defaultValues = task.reifyDefaultArguments(member);
if (defaultValues != null) {
String unmangledName = member.name.slowToString();
builder.addProperty('*$unmangledName', defaultValues);
}
}
code = backend.generatedBailoutCode[member];
if (code != null) {
builder.addProperty(namer.getBailoutName(member), code);
}
if (member.isInstanceMember()) {
// TODO(ahe): Where is this done for static/top-level methods?
FunctionSignature parameters = member.computeSignature(compiler);
if (!parameters.optionalParameters.isEmpty) {
addParameterStubs(member, builder.addProperty);
}
}
}
void addMemberField(VariableElement member, ClassBuilder builder) {
// For now, do nothing.
}
jsAst.Fun extendWithMetadata(FunctionElement element, jsAst.Fun code) {
if (!backend.retainMetadataOf(element)) return code;
return compiler.withCurrentElement(element, () {
List<int> metadata = <int>[];
FunctionSignature signature = element.functionSignature;
if (element.isConstructor()) {
metadata.add(task.reifyType(element.getEnclosingClass().thisType));
} else {
metadata.add(task.reifyType(signature.returnType));
}
signature.forEachParameter((Element parameter) {
metadata
..add(task.reifyName(parameter.name))
..add(task.reifyType(parameter.computeType(compiler)));
});
Link link = element.metadata;
// TODO(ahe): Why is metadata sometimes null?
if (link != null) {
for (; !link.isEmpty; link = link.tail) {
metadata.add(task.reifyMetadata(link.head));
}
}
code.body.statements.add(js.string(metadata.join(',')).toStatement());
return code;
});
}
}