blob: 882182ef52fd307bc7679d276f8b2ad664c8e19e [file] [log] [blame]
// Copyright (c) 2022, 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.
import 'package:dart2wasm/code_generator.dart';
import 'package:dart2wasm/translator.dart';
import 'package:kernel/ast.dart';
import 'package:wasm_builder/wasm_builder.dart' as w;
/// A local function or function expression.
class Lambda {
final FunctionNode functionNode;
final w.DefinedFunction function;
Lambda(this.functionNode, this.function);
}
/// The context for one or more closures, containing their captured variables.
///
/// Contexts can be nested, corresponding to the scopes covered by the contexts.
/// Each local function, function expression or loop (`while`, `do`/`while` or
/// `for`) gives rise to its own context nested inside the context of its
/// surrounding scope. At runtime, each context has a reference to its parent
/// context.
///
/// Closures corresponding to local functions or function expressions in the
/// same scope share the same context. Thus, a closure can potentially keep more
/// values alive than the ones captured by the closure itself.
///
/// A context may be empty (containing no captured variables), in which case it
/// is skipped in the context parent chain and never allocated. A context can
/// also be skipped if it only contains variables that are not in scope for the
/// child context (and its descendants).
class Context {
/// The node containing the scope covered by the context. This is either a
/// [FunctionNode] (for members, local functions and function expressions),
/// a [ForStatement], a [DoStatement] or a [WhileStatement].
final TreeNode owner;
/// The parent of this context, corresponding to the lexically enclosing
/// owner. This is null if the context is a member context, or if all contexts
/// in the parent chain are skipped.
final Context? parent;
/// The variables captured by this context.
final List<VariableDeclaration> variables = [];
/// The type parameters captured by this context.
final List<TypeParameter> typeParameters = [];
/// Whether this context contains a captured `this`. Only member contexts can.
bool containsThis = false;
/// The Wasm struct representing this context at runtime.
late final w.StructType struct;
/// The local variable currently pointing to this context. Used during code
/// generation.
late w.Local currentLocal;
bool get isEmpty =>
variables.isEmpty && typeParameters.isEmpty && !containsThis;
int get parentFieldIndex {
assert(parent != null);
return 0;
}
int get thisFieldIndex {
assert(containsThis);
return 0;
}
Context(this.owner, this.parent);
}
/// A captured variable.
class Capture {
final TreeNode variable;
late final Context context;
late final int fieldIndex;
bool written = false;
Capture(this.variable);
w.ValueType get type => context.struct.fields[fieldIndex].type.unpacked;
}
/// Compiler passes to find all captured variables and construct the context
/// tree for a member.
class Closures {
final CodeGenerator codeGen;
final Map<TreeNode, Capture> captures = {};
bool isThisCaptured = false;
final Map<FunctionNode, Lambda> lambdas = {};
final Map<TreeNode, Context> contexts = {};
final Set<FunctionDeclaration> closurizedFunctions = {};
Closures(this.codeGen);
Translator get translator => codeGen.translator;
w.Module get m => translator.m;
late final w.ValueType typeType =
translator.classInfo[translator.typeClass]!.nullableType;
void findCaptures(Member member) {
var find = CaptureFinder(this, member);
if (member is Constructor) {
Class cls = member.enclosingClass;
for (Field field in cls.fields) {
if (field.isInstanceMember && field.initializer != null) {
field.initializer!.accept(find);
}
}
}
member.accept(find);
}
void collectContexts(TreeNode node, {TreeNode? container}) {
if (captures.isNotEmpty || isThisCaptured) {
node.accept(ContextCollector(this, container));
}
}
void buildContexts() {
// Make struct definitions
for (Context context in contexts.values) {
if (!context.isEmpty) {
context.struct = m.addStructType("<context>");
}
}
// Build object layouts
for (Context context in contexts.values) {
if (!context.isEmpty) {
w.StructType struct = context.struct;
if (context.parent != null) {
assert(!context.containsThis);
struct.fields.add(w.FieldType(
w.RefType.def(context.parent!.struct, nullable: true)));
}
if (context.containsThis) {
struct.fields.add(w.FieldType(
codeGen.preciseThisLocal!.type.withNullability(true)));
}
for (VariableDeclaration variable in context.variables) {
int index = struct.fields.length;
struct.fields.add(w.FieldType(
translator.translateType(variable.type).withNullability(true)));
captures[variable]!.fieldIndex = index;
}
for (TypeParameter parameter in context.typeParameters) {
int index = struct.fields.length;
struct.fields.add(w.FieldType(typeType));
captures[parameter]!.fieldIndex = index;
}
}
}
}
}
class CaptureFinder extends RecursiveVisitor {
final Closures closures;
final Member member;
final Map<TreeNode, int> variableDepth = {};
int depth = 0;
CaptureFinder(this.closures, this.member);
Translator get translator => closures.translator;
w.Module get m => translator.m;
@override
void visitAssertStatement(AssertStatement node) {}
@override
void visitVariableDeclaration(VariableDeclaration node) {
if (depth > 0) {
variableDepth[node] = depth;
}
super.visitVariableDeclaration(node);
}
@override
void visitTypeParameter(TypeParameter node) {
if (node.parent is FunctionNode) {
if (depth > 0) {
variableDepth[node] = depth;
}
}
super.visitTypeParameter(node);
}
void _visitVariableUse(TreeNode variable) {
int declDepth = variableDepth[variable] ?? 0;
assert(declDepth <= depth);
if (declDepth < depth) {
closures.captures[variable] = Capture(variable);
} else if (variable is VariableDeclaration &&
variable.parent is FunctionDeclaration) {
closures.closurizedFunctions.add(variable.parent as FunctionDeclaration);
}
}
@override
void visitVariableGet(VariableGet node) {
_visitVariableUse(node.variable);
super.visitVariableGet(node);
}
@override
void visitVariableSet(VariableSet node) {
_visitVariableUse(node.variable);
super.visitVariableSet(node);
}
void _visitThis() {
if (depth > 0) {
closures.isThisCaptured = true;
}
}
@override
void visitThisExpression(ThisExpression node) {
_visitThis();
}
@override
void visitSuperMethodInvocation(SuperMethodInvocation node) {
_visitThis();
super.visitSuperMethodInvocation(node);
}
@override
void visitSuperPropertyGet(SuperPropertyGet node) {
_visitThis();
super.visitSuperPropertyGet(node);
}
@override
void visitSuperPropertySet(SuperPropertySet node) {
_visitThis();
super.visitSuperPropertySet(node);
}
@override
void visitTypeParameterType(TypeParameterType node) {
if (node.parameter.parent != null &&
node.parameter.parent == member.enclosingClass) {
_visitThis();
} else if (node.parameter.parent is FunctionNode) {
_visitVariableUse(node.parameter);
}
super.visitTypeParameterType(node);
}
void _visitLambda(FunctionNode node) {
if (node.positionalParameters.length != node.requiredParameterCount ||
node.namedParameters.isNotEmpty) {
throw "Not supported: Optional parameters for "
"function expression or local function at ${node.location}";
}
if (node.typeParameters.isNotEmpty) {
throw "Not supported: Type parameters for "
"function expression or local function at ${node.location}";
}
int parameterCount = node.requiredParameterCount;
w.FunctionType type = translator.closureFunctionType(parameterCount);
w.DefinedFunction function = m.addFunction(type, "$member (closure)");
closures.lambdas[node] = Lambda(node, function);
depth++;
node.visitChildren(this);
depth--;
}
@override
void visitFunctionExpression(FunctionExpression node) {
_visitLambda(node.function);
}
@override
void visitFunctionDeclaration(FunctionDeclaration node) {
// Variable is in outer scope
node.variable.accept(this);
_visitLambda(node.function);
}
}
class ContextCollector extends RecursiveVisitor {
final Closures closures;
Context? currentContext;
ContextCollector(this.closures, TreeNode? container) {
if (container != null) {
currentContext = closures.contexts[container]!;
}
}
@override
void visitAssertStatement(AssertStatement node) {}
void _newContext(TreeNode node) {
bool outerMost = currentContext == null;
Context? oldContext = currentContext;
Context? parent = currentContext;
while (parent != null && parent.isEmpty) {
parent = parent.parent;
}
currentContext = Context(node, parent);
if (closures.isThisCaptured && outerMost) {
currentContext!.containsThis = true;
}
closures.contexts[node] = currentContext!;
node.visitChildren(this);
currentContext = oldContext;
}
@override
void visitConstructor(Constructor node) {
node.function.accept(this);
currentContext = closures.contexts[node.function]!;
visitList(node.initializers, this);
}
@override
void visitFunctionNode(FunctionNode node) {
_newContext(node);
}
@override
void visitWhileStatement(WhileStatement node) {
_newContext(node);
}
@override
void visitDoStatement(DoStatement node) {
_newContext(node);
}
@override
void visitForStatement(ForStatement node) {
_newContext(node);
}
@override
void visitVariableDeclaration(VariableDeclaration node) {
Capture? capture = closures.captures[node];
if (capture != null) {
currentContext!.variables.add(node);
capture.context = currentContext!;
}
super.visitVariableDeclaration(node);
}
@override
void visitTypeParameter(TypeParameter node) {
Capture? capture = closures.captures[node];
if (capture != null) {
currentContext!.typeParameters.add(node);
capture.context = currentContext!;
}
super.visitTypeParameter(node);
}
@override
void visitVariableSet(VariableSet node) {
closures.captures[node.variable]?.written = true;
super.visitVariableSet(node);
}
}