blob: 42def515583d11852bb3139399e9ea55655981c2 [file] [log] [blame]
// Copyright (c) 2016, 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.
library kernel.transformations.closure.converter;
import '../../ast.dart'
show
Arguments,
Block,
Catch,
Class,
ClosureCreation,
Constructor,
DartType,
EmptyStatement,
Expression,
ExpressionStatement,
Field,
ForInStatement,
ForStatement,
FunctionDeclaration,
FunctionExpression,
FunctionNode,
FunctionType,
Initializer,
InterfaceType,
InvalidExpression,
InvocationExpression,
Library,
LocalInitializer,
RedirectingInitializer,
Member,
MethodInvocation,
Name,
NamedExpression,
NamedType,
NullLiteral,
Procedure,
ProcedureKind,
PropertyGet,
ReturnStatement,
Statement,
StaticInvocation,
ThisExpression,
Transformer,
TreeNode,
TypeParameter,
TypeParameterType,
VariableDeclaration,
VariableGet,
VariableSet,
VectorCreation,
VectorType,
transformList;
import '../../frontend/accessors.dart' show VariableAccessor;
import '../../clone.dart' show CloneVisitor;
import '../../core_types.dart' show CoreTypes;
import '../../type_algebra.dart' show substitute;
import 'clone_without_body.dart' show CloneWithoutBody;
import 'context.dart' show Context, NoContext, LocalContext;
import 'info.dart' show ClosureInfo;
import 'rewriter.dart' show AstRewriter, BlockRewriter, InitializerListRewriter;
class ClosureConverter extends Transformer {
final CoreTypes coreTypes;
final Set<VariableDeclaration> capturedVariables;
// This map pairs variables that are captured with flags indicating whether
// they are used inside or outside an initializer. See
// [ClosureInfo.parameterUses].
final Map<VariableDeclaration, int> parameterUses;
final Map<FunctionNode, Set<TypeParameter>> capturedTypeVariables;
final Map<FunctionNode, VariableDeclaration> thisAccess;
final Map<FunctionNode, String> localNames;
/// Records place-holders for cloning contexts. See [visitForStatement].
final Set<InvalidExpression> contextClonePlaceHolders =
new Set<InvalidExpression>();
final CloneVisitor cloner = new CloneWithoutBody();
/// New members to add to [currentLibrary] after it has been
/// transformed. These members will not be transformed themselves.
final List<TreeNode> newLibraryMembers = <TreeNode>[];
/// New members to add to [currentClass] after it has been transformed. These
/// members will not be transformed themselves.
final List<Member> newClassMembers = <Member>[];
Library currentLibrary;
Class currentClass;
Member currentMember;
FunctionNode currentMemberFunction;
FunctionNode currentFunction;
Context context;
AstRewriter rewriter;
/// TODO(29181): update this comment when the type variables are restored.
/// Maps original type variable (aka type parameter) to a hoisted type
/// variable type.
///
/// For example, consider:
///
/// class C<T> {
/// f() => (x) => x is T;
/// }
///
/// This is currently converted to:
///
/// class C<T> {
/// f() => new Closure#0<T>();
/// }
/// class Closure#0<T_> implements Function {
/// call(x) => x is T_;
/// }
///
/// In this example, `typeSubstitution[T].parameter == T_` when transforming
/// the closure in `f`.
Map<TypeParameter, DartType> typeSubstitution =
const <TypeParameter, DartType>{};
ClosureConverter(this.coreTypes, ClosureInfo info)
: this.capturedVariables = info.variables,
this.parameterUses = info.parameterUses,
this.capturedTypeVariables = info.typeVariables,
this.thisAccess = info.thisAccess,
this.localNames = info.localNames;
bool get isOuterMostContext {
return currentFunction == null || currentMemberFunction == currentFunction;
}
String get currentFileUri {
if (currentMember is Constructor) return currentClass.fileUri;
if (currentMember is Field) return (currentMember as Field).fileUri;
if (currentMember is Procedure) return (currentMember as Procedure).fileUri;
throw "No file uri for ${currentMember.runtimeType}";
}
TreeNode saveContext(TreeNode f()) {
AstRewriter old = rewriter;
Context savedContext = context;
try {
return f();
} finally {
rewriter = old;
context = savedContext;
}
}
TreeNode visitLibrary(Library node) {
assert(newLibraryMembers.isEmpty);
currentLibrary = node;
node = super.visitLibrary(node);
for (TreeNode member in newLibraryMembers) {
if (member is Class) {
node.addClass(member);
} else {
node.addMember(member);
}
}
newLibraryMembers.clear();
currentLibrary = null;
return node;
}
TreeNode visitClass(Class node) {
assert(newClassMembers.isEmpty);
currentClass = node;
node = super.visitClass(node);
newClassMembers.forEach(node.addMember);
newClassMembers.clear();
currentClass = null;
return node;
}
extendContextConditionally({bool inInitializer}) {
return (VariableDeclaration parameter) {
if (!capturedVariables.contains(parameter)) return 0;
int flags = parameterUses[parameter];
if (flags == null) {
context.extend(parameter, new VariableGet(parameter));
return 0;
}
// When moving variables into the context while scanning initializers,
// we need to add the variable if it's captured in an initializer,
// whether or not it's used/captured in the body. However, in the body,
// we only need to add the variable into the context if it's *not*
// captured in an initializer.
if (inInitializer
? (flags & ClosureInfo.INSIDE_INITIALIZER) > 0
: flags == ClosureInfo.OUTSIDE_INITIALIZER) {
context.extend(parameter, new VariableGet(parameter));
}
return flags;
};
}
TreeNode visitConstructor(Constructor node) {
assert(isEmptyContext);
currentMember = node;
// If we created a context for the initializers, we need to re-use that
// context in the body of the function. Unfortunately, the context is
// declared in a local initializer and local initializers aren't visible
// in the body of the constructor. To work around this issue, we move the
// body into a new constructor and make this constructor redirect to that
// one, passing the context as an argument to the new constructor.
var movingCtor = false;
// Transform initializers.
if (node.initializers.length > 0) {
var initRewriter = new InitializerListRewriter(node);
rewriter = initRewriter;
context = new NoContext(this);
final int capturedBoth =
ClosureInfo.OUTSIDE_INITIALIZER | ClosureInfo.INSIDE_INITIALIZER;
// TODO(karlklose): add a fine-grained analysis of captured parameters.
handleParam(decl) {
if (extendContextConditionally(inInitializer: true)(decl) ==
capturedBoth) {
movingCtor = true;
}
}
node.function.positionalParameters.forEach(handleParam);
node.function.namedParameters.forEach(handleParam);
transformList(node.initializers, this, node);
node.initializers.insertAll(0, initRewriter.prefix);
rewriter = null;
}
// Transform constructor body.
FunctionNode function = node.function;
if (function.body != null && function.body is! EmptyStatement) {
setupRewriterForFunctionBody(function);
if (!movingCtor) context = new NoContext(this);
VariableDeclaration self = thisAccess[currentMemberFunction];
if (self != null) {
context.extend(self, new ThisExpression());
}
node.function.accept(this);
if (movingCtor) {
var contextDecl = (context as LocalContext).self;
var newCtorName = new Name("${node.name.name}#redir");
var newCtor = new Constructor(node.function, name: newCtorName);
newClassMembers.add(newCtor);
LocalInitializer contextDeclInit = null;
for (var init in node.initializers) {
if (init is LocalInitializer && init.variable == contextDecl) {
contextDeclInit = init;
} else {
newCtor.initializers.add(init);
}
}
node.initializers = <Initializer>[contextDeclInit];
var cv = new CloneVisitor();
var oldCtorParams = function.positionalParameters
.map(cv.visitVariableDeclaration)
.toList();
var oldCtorNamedParams =
function.namedParameters.map(cv.visitVariableDeclaration).toList();
function.positionalParameters.addAll(function.namedParameters);
function.namedParameters = [];
var args = <Expression>[];
args.addAll(oldCtorParams.map((decl) => new VariableGet(decl)));
args.addAll(oldCtorNamedParams.map((decl) => new VariableGet(decl)));
node.function = new FunctionNode(new EmptyStatement(),
typeParameters: [],
positionalParameters: oldCtorParams,
namedParameters: oldCtorNamedParams,
requiredParameterCount: function.requiredParameterCount,
returnType: function.returnType,
asyncMarker: function.asyncMarker,
dartAsyncMarker: function.dartAsyncMarker);
node.function.parent = node;
var oldCtorDecl = cv.visitVariableDeclaration(contextDecl);
contextDecl.initializer = null;
function.positionalParameters.add(contextDecl);
function.requiredParameterCount++;
contextDeclInit.variable = oldCtorDecl;
oldCtorDecl.parent = contextDeclInit;
args.add(new VariableGet(oldCtorDecl));
var redirInit =
new RedirectingInitializer(newCtor, new Arguments(args));
node.initializers.add(redirInit);
}
}
resetContext();
return node;
}
AstRewriter makeRewriterForBody(FunctionNode function) {
Statement body = function.body;
if (body is! Block) {
body = new Block(<Statement>[body]);
function.body = function.body.parent = body;
}
return new BlockRewriter(body);
}
bool isObject(DartType type) {
return type is InterfaceType && type.classNode.supertype == null;
}
TreeNode visitField(Field node) {
currentMember = node;
context = new NoContext(this);
node = super.visitField(node);
context = null;
currentMember = null;
return node;
}
Expression handleLocalFunction(FunctionNode function) {
FunctionNode enclosingFunction = currentFunction;
Map<TypeParameter, DartType> enclosingTypeSubstitution = typeSubstitution;
currentFunction = function;
Statement body = function.body;
assert(body != null);
rewriter = makeRewriterForBody(function);
VariableDeclaration contextVariable =
new VariableDeclaration("#contextParameter", type: const VectorType());
Context parent = context;
context = context.toNestedContext(
new VariableAccessor(contextVariable, null, TreeNode.noOffset));
Set<TypeParameter> captured =
capturedTypeVariables[currentFunction] ?? new Set<TypeParameter>();
typeSubstitution = copyTypeVariables(captured);
function.transformChildren(this);
Expression result = addClosure(function, contextVariable, parent.expression,
typeSubstitution, enclosingTypeSubstitution);
currentFunction = enclosingFunction;
typeSubstitution = enclosingTypeSubstitution;
return result;
}
TreeNode visitFunctionDeclaration(FunctionDeclaration node) {
/// Is this closure itself captured by a closure?
bool isCaptured = capturedVariables.contains(node.variable);
if (isCaptured) {
context.extend(node.variable, new InvalidExpression());
}
Context parent = context;
return saveContext(() {
Expression expression = handleLocalFunction(node.function);
if (isCaptured) {
parent.update(node.variable, expression);
return null;
} else {
node.variable.initializer = expression;
expression.parent = node.variable;
return node.variable;
}
});
}
TreeNode visitFunctionExpression(FunctionExpression node) {
return saveContext(() => handleLocalFunction(node.function));
}
/// Add a new procedure to the current library that looks like this:
///
/// static method closure#0(Vector #c, /* Parameters of [function]. */)
/// → dynamic {
///
/// /* Context is represented by #c. */
///
/// /* Body of [function]. */
///
/// }
///
/// Returns an invocation of the closure creation primitive that binds the
/// above top-level function to a context represented as Vector.
Expression addClosure(
FunctionNode function,
VariableDeclaration contextVariable,
Expression accessContext,
Map<TypeParameter, DartType> substitution,
Map<TypeParameter, DartType> enclosingTypeSubstitution) {
var fnTypeParams = <TypeParameter>[];
var fnTypeArgs = <TypeParameterType>[];
for (TypeParameter t in substitution.keys) {
var fnTypeParam = (substitution[t] as TypeParameterType).parameter;
fnTypeParams.add(fnTypeParam);
fnTypeArgs
.add(substitute(new TypeParameterType(t), enclosingTypeSubstitution));
}
function.typeParameters.insertAll(0, fnTypeParams);
function.positionalParameters.insert(0, contextVariable);
++function.requiredParameterCount;
Procedure closedTopLevelFunction = new Procedure(
new Name(createNameForClosedTopLevelFunction(function)),
ProcedureKind.Method,
function,
isStatic: true,
fileUri: currentFileUri);
newLibraryMembers.add(closedTopLevelFunction);
// We need to again make new type parameters for the function's function
// type, and substitute them into the function type's arguments' types.
var closureTypeParams = <TypeParameter>[];
var closureTypeSubstitutionMap = copyTypeVariables(function.typeParameters);
for (DartType d in closureTypeSubstitutionMap.values)
closureTypeParams.add((d as TypeParameterType).parameter);
FunctionType closureType = new FunctionType(
function.positionalParameters
.skip(1)
.map((VariableDeclaration decl) =>
substitute(decl.type, closureTypeSubstitutionMap))
.toList(),
substitute(function.returnType, closureTypeSubstitutionMap),
namedParameters: function.namedParameters
.map((VariableDeclaration decl) => new NamedType(
decl.name, substitute(decl.type, closureTypeSubstitutionMap)))
.toList(),
typeParameters: closureTypeParams,
requiredParameterCount: function.requiredParameterCount - 1);
// If we capture type parameters but not regular variables, we still need to
// make a context.
if (capturedTypeVariables[function] != null &&
accessContext is NullLiteral) {
accessContext = new VectorCreation(1);
}
return new ClosureCreation(
closedTopLevelFunction, accessContext, closureType, fnTypeArgs);
}
TreeNode visitProcedure(Procedure node) {
assert(isEmptyContext);
currentMember = node;
FunctionNode function = node.function;
if (function.body != null) {
bool hadSingleStatementBody = function.body is! Block;
setupRewriterForFunctionBody(function);
// Start with no context. This happens after setting up _currentBlock
// so statements can be emitted into _currentBlock if necessary.
context = new NoContext(this);
VariableDeclaration self = thisAccess[currentMemberFunction];
if (self != null) {
context.extend(self, new ThisExpression());
}
node.transformChildren(this);
resetContext();
// Here a special case is handled: the body of the procedure was a single
// statement and after the transformation it is a block with a single
// statement inside. In this case we make this statement the body of the
// procedure again. It is required to follow the conventions imposed by
// [addClass] in [DillLibraryBuilder].
// See [dill_library_builder.dart]
// (../../../../front_end/lib/src/fasta/dill/dill_library_builder.dart)
// for details.
if (hadSingleStatementBody && function.body is Block) {
Block body = function.body;
if (body.statements.length == 1) {
function.body = body.statements[0];
function.body.parent = function;
}
}
}
return node;
}
void setupRewriterForFunctionBody(FunctionNode function) {
Statement body = function.body;
assert(body != null);
currentMemberFunction = function;
// Ensure that the body is a block which becomes the current block.
rewriter = makeRewriterForBody(function);
}
void resetContext() {
rewriter = null;
context = null;
currentMemberFunction = null;
currentMember = null;
}
bool get isEmptyContext {
return rewriter == null && context == null;
}
TreeNode visitLocalInitializer(LocalInitializer node) {
assert(!capturedVariables.contains(node.variable));
node.transformChildren(this);
return node;
}
TreeNode visitFunctionNode(FunctionNode node) {
transformList(node.typeParameters, this, node);
// Initializers for optional parameters must be compile-time constants,
// which excludes closures. Therefore, we can avoid looking for closures in
// initializers of the parameters.
node.positionalParameters
.forEach(extendContextConditionally(inInitializer: false));
node.namedParameters
.forEach(extendContextConditionally(inInitializer: false));
assert(node.body != null);
node.body = node.body.accept(this);
node.body.parent = node;
return node;
}
TreeNode visitBlock(Block node) {
return saveContext(() {
BlockRewriter blockRewriter = rewriter = rewriter.forNestedBlock(node);
blockRewriter.transformStatements(this);
return node;
});
}
TreeNode visitVariableDeclaration(VariableDeclaration node) {
node.transformChildren(this);
if (!capturedVariables.contains(node)) return node;
if (node.initializer == null && node.parent is FunctionNode) {
// If the variable is a function parameter and doesn't have an
// initializer, just use this variable name to put it into the context.
context.extend(node, new VariableGet(node));
} else {
context.extend(node, node.initializer ?? new NullLiteral());
}
if (node.parent == currentFunction) {
return node;
} else {
assert(node.parent is Block);
// When returning null, the parent block will remove
// this node from its list of statements.
return null;
}
}
TreeNode visitVariableGet(VariableGet node) {
return capturedVariables.contains(node.variable)
? context.lookup(node.variable)
: node;
}
TreeNode visitVariableSet(VariableSet node) {
node.transformChildren(this);
return capturedVariables.contains(node.variable)
? context.assign(node.variable, node.value,
voidContext: isInVoidContext(node))
: node;
}
bool isInVoidContext(Expression node) {
TreeNode parent = node.parent;
return parent is ExpressionStatement ||
parent is ForStatement && parent.condition != node;
}
DartType visitDartType(DartType node) {
return substitute(node, typeSubstitution);
}
VariableDeclaration getReplacementLoopVariable(VariableDeclaration variable) {
VariableDeclaration newVariable = new VariableDeclaration(variable.name,
initializer: variable.initializer, type: variable.type)
..flags = variable.flags;
variable.initializer = new VariableGet(newVariable);
variable.initializer.parent = variable;
return newVariable;
}
Expression cloneContext() {
InvalidExpression placeHolder = new InvalidExpression();
contextClonePlaceHolders.add(placeHolder);
return placeHolder;
}
TreeNode visitInvalidExpression(InvalidExpression node) {
return contextClonePlaceHolders.remove(node) ? context.clone() : node;
}
TreeNode visitForStatement(ForStatement node) {
if (node.variables.any(capturedVariables.contains)) {
// In Dart, loop variables are new variables on each iteration of the
// loop. This is only observable when a loop variable is captured by a
// closure, which is the situation we're in here. So we transform the
// loop.
//
// Consider the following example, where `x` is `node.variables.first`,
// and `#t1` is a temporary variable:
//
// for (var x = 0; x < 10; x++) body;
//
// This is transformed to:
//
// {
// var x = 0;
// for (; x < 10; clone-context, x++) body;
// }
//
// `clone-context` is a place-holder that will later be replaced by an
// expression that clones the current closure context (see
// [visitInvalidExpression]).
return saveContext(() {
context = context.toNestedContext();
List<Statement> statements = <Statement>[];
statements.addAll(node.variables);
statements.add(node);
node.variables.clear();
node.updates.insert(0, cloneContext());
Block block = new Block(statements);
rewriter = new BlockRewriter(block);
return block.accept(this);
});
}
return super.visitForStatement(node);
}
TreeNode visitForInStatement(ForInStatement node) {
if (capturedVariables.contains(node.variable)) {
// In Dart, loop variables are new variables on each iteration of the
// loop. This is only observable when the loop variable is captured by a
// closure, so we need to transform the for-in loop when `node.variable`
// is captured.
//
// Consider the following example, where `x` is `node.variable`, and
// `#t1` is a temporary variable:
//
// for (var x in expr) body;
//
// Notice that we can assume that `x` doesn't have an initializer based
// on invariants in the Kernel AST. This is transformed to:
//
// for (var #t1 in expr) { var x = #t1; body; }
//
// After this, we call super to apply the normal closure conversion to
// the transformed for-in loop.
VariableDeclaration variable = node.variable;
VariableDeclaration newVariable = getReplacementLoopVariable(variable);
node.variable = newVariable;
newVariable.parent = node;
node.body = new Block(<Statement>[variable, node.body]);
node.body.parent = node;
}
return super.visitForInStatement(node);
}
TreeNode visitThisExpression(ThisExpression node) {
return isOuterMostContext
? node
: context.lookup(thisAccess[currentMemberFunction]);
}
TreeNode visitCatch(Catch node) {
VariableDeclaration exception = node.exception;
VariableDeclaration stackTrace = node.stackTrace;
if (stackTrace != null && capturedVariables.contains(stackTrace)) {
Block block = node.body = ensureBlock(node.body);
block.parent = node;
node.stackTrace = new VariableDeclaration(null);
node.stackTrace.parent = node;
stackTrace.initializer = new VariableGet(node.stackTrace);
block.statements.insert(0, stackTrace);
stackTrace.parent = block;
}
if (exception != null && capturedVariables.contains(exception)) {
Block block = node.body = ensureBlock(node.body);
block.parent = node;
node.exception = new VariableDeclaration(null);
node.exception.parent = node;
exception.initializer = new VariableGet(node.exception);
block.statements.insert(0, exception);
exception.parent = block;
}
return super.visitCatch(node);
}
Block ensureBlock(Statement statement) {
return statement is Block ? statement : new Block(<Statement>[statement]);
}
/// Creates a function that has the same signature as `procedure.function`
/// and which forwards all arguments to `procedure`.
FunctionNode forwardFunction(
Procedure procedure,
VariableDeclaration receiver,
VariableDeclaration contextVariable,
Map<TypeParameter, DartType> substitution) {
CloneVisitor cloner = substitution.isEmpty
? this.cloner
: new CloneWithoutBody(typeSubstitution: substitution);
FunctionNode function = procedure.function;
List<TypeParameter> typeParameters =
function.typeParameters.map(cloner.clone).toList();
List<VariableDeclaration> positionalParameters =
function.positionalParameters.map(cloner.clone).toList();
if (contextVariable != null) {
positionalParameters.insert(0, contextVariable);
}
List<VariableDeclaration> namedParameters =
function.namedParameters.map(cloner.clone).toList();
List<DartType> types = typeParameters
.map((TypeParameter parameter) => new TypeParameterType(parameter))
.toList();
List<Expression> positional = positionalParameters
.map((VariableDeclaration parameter) => new VariableGet(parameter))
.toList();
if (contextVariable != null) {
positional.removeAt(0);
}
List<NamedExpression> named =
namedParameters.map((VariableDeclaration parameter) {
return new NamedExpression(parameter.name, new VariableGet(parameter));
}).toList();
Arguments arguments = new Arguments(positional, types: types, named: named);
InvocationExpression invocation = procedure.isInstanceMember
? new MethodInvocation(
context.lookup(receiver), procedure.name, arguments, procedure)
: new StaticInvocation(procedure, arguments);
int requiredParameterCount = function.requiredParameterCount;
if (contextVariable != null) {
++requiredParameterCount;
}
return new FunctionNode(new ReturnStatement(invocation),
typeParameters: typeParameters,
positionalParameters: positionalParameters,
namedParameters: namedParameters,
requiredParameterCount: requiredParameterCount,
returnType: substitute(function.returnType, cloner.typeSubstitution));
}
/// Creates copies of the type variables in [original] and returns a
/// substitution that can be passed to [substitute] to substitute all uses of
/// [original] with their copies.
///
Map<TypeParameter, DartType> copyTypeVariables(
Iterable<TypeParameter> original) {
if (original.isEmpty) return const <TypeParameter, DartType>{};
Map<TypeParameter, DartType> substitution = <TypeParameter, DartType>{};
for (TypeParameter t in original) {
substitution[t] = new TypeParameterType(new TypeParameter(t.name));
}
substitution.forEach((TypeParameter t, DartType copy) {
if (copy is TypeParameterType) {
copy.parameter.bound = substitute(t.bound, substitution);
}
});
return substitution;
}
String createNameForClosedTopLevelFunction(FunctionNode function) {
return 'closure#${localNames[function]}';
}
Statement forwardToThisProperty(Member node) {
assert(node is Field || (node is Procedure && node.isGetter));
return new ReturnStatement(
new PropertyGet(new ThisExpression(), node.name, node));
}
void addFieldForwarder(Name name, Field field) {
newClassMembers.add(new Procedure(name, ProcedureKind.Getter,
new FunctionNode(forwardToThisProperty(field)),
fileUri: currentFileUri));
}
Procedure copyWithBody(Procedure procedure, Statement body) {
Procedure copy = cloner.clone(procedure);
copy.function.body = body;
copy.function.body.parent = copy.function;
return copy;
}
void addGetterForwarder(Name name, Procedure getter) {
assert(getter.isGetter);
newClassMembers
.add(copyWithBody(getter, forwardToThisProperty(getter))..name = name);
}
}