blob: 42ae6cda760e3632c55cde3e93e220b1dac4f276 [file] [log] [blame]
// Copyright (c) 2014, 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 dart2js.cps_ir.shrinking_reductions;
import 'cps_ir_nodes.dart';
import 'optimizers.dart';
/**
* [ShrinkingReducer] applies shrinking reductions to CPS terms as described
* in 'Compiling with Continuations, Continued' by Andrew Kennedy.
*/
class ShrinkingReducer extends Pass {
String get passName => 'Shrinking reductions';
final List<_ReductionTask> _worklist = new List<_ReductionTask>();
/// Applies shrinking reductions to root, mutating root in the process.
@override
void rewrite(FunctionDefinition root) {
_RedexVisitor redexVisitor = new _RedexVisitor(_worklist);
// Sweep over the term, collecting redexes into the worklist.
redexVisitor.visit(root);
_iterateWorklist();
}
void _iterateWorklist() {
while (_worklist.isNotEmpty) {
_ReductionTask task = _worklist.removeLast();
_processTask(task);
}
}
/// Call instead of [_iterateWorklist] to check at every step that no
/// redex was missed.
void _debugWorklist(FunctionDefinition root) {
while (_worklist.isNotEmpty) {
_ReductionTask task = _worklist.removeLast();
String irBefore = root.debugString({
task.node: '${task.kind} applied here'
});
_processTask(task);
Set seenRedexes = _worklist.where(isValidTask).toSet();
Set actualRedexes = (new _RedexVisitor([])..visit(root)).worklist.toSet();
if (!seenRedexes.containsAll(actualRedexes)) {
_ReductionTask missedTask =
actualRedexes.firstWhere((x) => !seenRedexes.contains(x));
print('\nBEFORE $task:\n');
print(irBefore);
print('\nAFTER $task:\n');
root.debugPrint({
missedTask.node: 'MISSED ${missedTask.kind}'
});
throw 'Missed $missedTask after processing $task';
}
}
}
bool isValidTask(_ReductionTask task) {
switch (task.kind) {
case _ReductionKind.DEAD_VAL:
return _isDeadVal(task.node);
case _ReductionKind.DEAD_CONT:
return _isDeadCont(task.node);
case _ReductionKind.BETA_CONT_LIN:
return _isBetaContLin(task.node);
case _ReductionKind.ETA_CONT:
return _isEtaCont(task.node);
case _ReductionKind.DEAD_PARAMETER:
return _isDeadParameter(task.node);
case _ReductionKind.BRANCH:
return _isBranchRedex(task.node);
}
}
/// Removes the given node from the CPS graph, replacing it with its body
/// and marking it as deleted. The node's parent must be a [[InteriorNode]].
void _removeNode(InteriorNode node) {
Node body = node.body;
InteriorNode parent = node.parent;
assert(parent.body == node);
body.parent = parent;
parent.body = body;
node.parent = null;
// The removed node could be the last node between a continuation and
// an InvokeContinuation in the body.
if (parent is Continuation) {
_checkEtaCont(parent);
_checkUselessBranchTarget(parent);
}
}
/// Remove a given continuation from the CPS graph. The LetCont itself is
/// removed if the given continuation is the only binding.
void _removeContinuation(Continuation cont) {
LetCont parent = cont.parent;
if (parent.continuations.length == 1) {
_removeNode(parent);
} else {
parent.continuations.remove(cont);
}
cont.parent = null;
}
void _processTask(_ReductionTask task) {
// Skip tasks for deleted nodes.
if (task.node.parent == null) {
return;
}
switch (task.kind) {
case _ReductionKind.DEAD_VAL:
_reduceDeadVal(task);
break;
case _ReductionKind.DEAD_CONT:
_reduceDeadCont(task);
break;
case _ReductionKind.BETA_CONT_LIN:
_reduceBetaContLin(task);
break;
case _ReductionKind.ETA_CONT:
_reduceEtaCont(task);
break;
case _ReductionKind.DEAD_PARAMETER:
_reduceDeadParameter(task);
break;
case _ReductionKind.BRANCH:
_reduceBranch(task);
break;
default:
assert(false);
}
}
/// Applies the dead-val reduction:
/// letprim x = V in E -> E (x not free in E).
void _reduceDeadVal(_ReductionTask task) {
if (_isRemoved(task.node)) return;
assert(_isDeadVal(task.node));
LetPrim deadLet = task.node;
Primitive deadPrim = deadLet.primitive;
assert(deadPrim.hasNoRefinedUses);
// The node has no effective uses but can have refinement uses, which
// themselves can have more refinements uses (but only refinement uses).
// We must remove the entire refinement tree while looking for redexes
// whenever we remove one.
List<Primitive> deadlist = <Primitive>[deadPrim];
while (deadlist.isNotEmpty) {
Primitive node = deadlist.removeLast();
while (node.firstRef != null) {
Reference ref = node.firstRef;
Refinement use = ref.parent;
deadlist.add(use);
ref.unlink();
}
LetPrim binding = node.parent;
_removeNode(binding); // Remove the binding and check for eta redexes.
}
// Perform bookkeeping on removed body and scan for new redexes.
new _RemovalVisitor(_worklist).visit(deadPrim);
}
/// Applies the dead-cont reduction:
/// letcont k x = E0 in E1 -> E1 (k not free in E1).
void _reduceDeadCont(_ReductionTask task) {
assert(_isDeadCont(task.node));
// Remove dead continuation.
Continuation cont = task.node;
_removeContinuation(cont);
// Perform bookkeeping on removed body and scan for new redexes.
new _RemovalVisitor(_worklist).visit(cont);
}
/// Applies the beta-cont-lin reduction:
/// letcont k x = E0 in E1[k y] -> E1[E0[y/x]] (k not free in E1).
void _reduceBetaContLin(_ReductionTask task) {
// Might have been mutated, recheck if reduction is still valid.
// In the following example, the beta-cont-lin reduction of k0 could have
// been invalidated by removal of the dead continuation k1:
//
// letcont k0 x0 = E0 in
// letcont k1 x1 = k0 x1 in
// return x2
if (!_isBetaContLin(task.node)) {
return;
}
Continuation cont = task.node;
InvokeContinuation invoke = cont.firstRef.parent;
InteriorNode invokeParent = invoke.parent;
Expression body = cont.body;
// Replace the invocation with the continuation body.
invokeParent.body = body;
body.parent = invokeParent;
cont.body = null;
// Substitute the invocation argument for the continuation parameter.
for (int i = 0; i < invoke.arguments.length; i++) {
Parameter param = cont.parameters[i];
Primitive argument = invoke.arguments[i].definition;
param.replaceUsesWith(argument);
argument.useElementAsHint(param.hint);
_checkConstantBranchCondition(argument);
}
// Remove the continuation after inlining it so we can check for eta redexes
// which may arise after removing the LetCont.
_removeContinuation(cont);
// Perform bookkeeping on substituted body and scan for new redexes.
new _RemovalVisitor(_worklist).visit(invoke);
if (invokeParent is Continuation) {
_checkEtaCont(invokeParent);
_checkUselessBranchTarget(invokeParent);
}
}
/// Applies the eta-cont reduction:
/// letcont k x = j x in E -> E[j/k].
/// If k is unused, degenerates to dead-cont.
void _reduceEtaCont(_ReductionTask task) {
// Might have been mutated, recheck if reduction is still valid.
// In the following example, the eta-cont reduction of k1 could have been
// invalidated by an earlier beta-cont-lin reduction of k0.
//
// letcont k0 x0 = E0 in
// letcont k1 x1 = k0 x1 in E1
if (!_isEtaCont(task.node)) {
return;
}
// Remove the continuation.
Continuation cont = task.node;
_removeContinuation(cont);
InvokeContinuation invoke = cont.body;
Continuation wrappedCont = invoke.continuation.definition;
for (int i = 0; i < cont.parameters.length; ++i) {
wrappedCont.parameters[i].useElementAsHint(cont.parameters[i].hint);
}
// If the invocation of wrappedCont is escaping, then all invocations of
// cont will be as well, after the reduction.
if (invoke.isEscapingTry) {
Reference current = cont.firstRef;
while (current != null) {
InvokeContinuation owner = current.parent;
owner.isEscapingTry = true;
current = current.next;
}
}
// Replace all occurrences with the wrapped continuation and find redexes.
while (cont.firstRef != null) {
Reference ref = cont.firstRef;
ref.changeTo(wrappedCont);
Node use = ref.parent;
if (use is InvokeContinuation && use.parent is Continuation) {
_checkUselessBranchTarget(use.parent);
}
}
// Perform bookkeeping on removed body and scan for new redexes.
new _RemovalVisitor(_worklist).visit(cont);
}
void _reduceBranch(_ReductionTask task) {
Branch branch = task.node;
// Replace Branch with InvokeContinuation of one of the targets. When the
// branch is deleted the other target becomes unreferenced and the chosen
// target becomes available for eta-cont and further reductions.
Continuation target;
Primitive condition = branch.condition.definition;
if (condition is Constant) {
target = isTruthyConstant(condition.value, strict: branch.isStrictCheck)
? branch.trueContinuation.definition
: branch.falseContinuation.definition;
} else if (_isBranchTargetOfUselessIf(branch.trueContinuation.definition)) {
target = branch.trueContinuation.definition;
} else {
return;
}
InvokeContinuation invoke = new InvokeContinuation(
target, <Primitive>[]
// TODO(sra): Add sourceInformation.
/*, sourceInformation: branch.sourceInformation*/);
branch.parent.body = invoke;
invoke.parent = branch.parent;
branch.parent = null;
new _RemovalVisitor(_worklist).visit(branch);
}
void _reduceDeadParameter(_ReductionTask task) {
// Continuation eta-reduction can destroy a dead parameter redex. For
// example, in the term:
//
// let cont k0(v0) = /* v0 is not used */ in
// let cont k1(v1) = k0(v1) in
// call foo () k1
//
// Continuation eta-reduction of k1 gives:
//
// let cont k0(v0) = /* v0 is not used */ in
// call foo () k0
//
// Where the dead parameter reduction is no longer valid because we do not
// allow removing the paramter of call continuations. We disallow such eta
// reductions in [_isEtaCont].
Parameter parameter = task.node;
if (_isParameterRemoved(parameter)) return;
assert(_isDeadParameter(parameter));
Continuation continuation = parameter.parent;
int index = continuation.parameters.indexOf(parameter);
assert(index != -1);
continuation.parameters.removeAt(index);
parameter.parent = null; // Mark as removed.
// Remove the index'th argument from each invocation.
for (Reference ref = continuation.firstRef; ref != null; ref = ref.next) {
InvokeContinuation invoke = ref.parent;
Reference<Primitive> argument = invoke.arguments[index];
argument.unlink();
invoke.arguments.removeAt(index);
// Removing an argument can create a dead primitive or an eta-redex
// in case the parent is a continuation that now has matching parameters.
_checkDeadPrimitive(argument.definition);
if (invoke.parent is Continuation) {
_checkEtaCont(invoke.parent);
_checkUselessBranchTarget(invoke.parent);
}
}
// Removing an unused parameter can create an eta-redex, in case the
// body is an InvokeContinuation that now has matching arguments.
_checkEtaCont(continuation);
}
void _checkEtaCont(Continuation continuation) {
if (_isEtaCont(continuation)) {
_worklist.add(new _ReductionTask(_ReductionKind.ETA_CONT, continuation));
}
}
void _checkUselessBranchTarget(Continuation continuation) {
if (_isBranchTargetOfUselessIf(continuation)) {
_worklist.add(new _ReductionTask(_ReductionKind.BRANCH,
continuation.firstRef.parent));
}
}
void _checkConstantBranchCondition(Primitive primitive) {
if (primitive is! Constant) return;
for (Reference ref = primitive.firstRef; ref != null; ref = ref.next) {
Node use = ref.parent;
if (use is Branch) {
_worklist.add(new _ReductionTask(_ReductionKind.BRANCH, use));
}
}
}
void _checkDeadPrimitive(Primitive primitive) {
primitive = primitive.unrefined;
if (primitive is Parameter) {
if (_isDeadParameter(primitive)) {
_worklist.add(new _ReductionTask(_ReductionKind.DEAD_PARAMETER,
primitive));
}
} else if (primitive.parent is LetPrim) {
LetPrim letPrim = primitive.parent;
if (_isDeadVal(letPrim)) {
_worklist.add(new _ReductionTask(_ReductionKind.DEAD_VAL, letPrim));
}
}
}
}
bool _isRemoved(InteriorNode node) {
return node.parent == null;
}
bool _isParameterRemoved(Parameter parameter) {
// A parameter can be removed directly or because its continuation is removed.
return parameter.parent == null || _isRemoved(parameter.parent);
}
/// Returns true iff the bound primitive is unused, and has no effects
/// preventing it from being eliminated.
bool _isDeadVal(LetPrim node) {
return !_isRemoved(node) &&
node.primitive.hasNoRefinedUses &&
node.primitive.isSafeForElimination;
}
/// Returns true iff the continuation is unused.
bool _isDeadCont(Continuation cont) {
return !_isRemoved(cont) &&
!cont.isReturnContinuation &&
!cont.hasAtLeastOneUse;
}
/// Returns true iff the continuation has a body (i.e., it is not the return
/// continuation), it is used exactly once, and that use is as the continuation
/// of a continuation invocation.
bool _isBetaContLin(Continuation cont) {
if (_isRemoved(cont)) return false;
// There is a restriction on continuation eta-redexes that the body is not an
// invocation of the return continuation, because that leads to worse code
// when translating back to direct style (it duplicates returns). There is no
// such restriction here because continuation beta-reduction is only performed
// for singly referenced continuations. Thus, there is no possibility of code
// duplication.
if (cont.isReturnContinuation || !cont.hasExactlyOneUse) {
return false;
}
if (cont.firstRef.parent is! InvokeContinuation) return false;
InvokeContinuation invoke = cont.firstRef.parent;
// Beta-reduction will move the continuation's body to its unique invocation
// site. This is not safe if the body is moved into an exception handler
// binding.
if (invoke.isEscapingTry) return false;
return true;
}
/// Returns true iff the continuation consists of a continuation
/// invocation, passing on all parameters. Special cases exist (see below).
bool _isEtaCont(Continuation cont) {
if (_isRemoved(cont)) return false;
if (!cont.isJoinContinuation || cont.body is! InvokeContinuation) {
return false;
}
InvokeContinuation invoke = cont.body;
Continuation invokedCont = invoke.continuation.definition;
// Do not eta-reduce return join-points since the direct-style code is worse
// in the common case (i.e. returns are moved inside `if` branches).
if (invokedCont.isReturnContinuation) {
return false;
}
// Translation to direct style generates different statements for recursive
// and non-recursive invokes. It should still be possible to apply eta-cont if
// this is not a self-invocation.
//
// TODO(kmillikin): Remove this restriction if it makes sense to do so.
if (invoke.isRecursive) {
return false;
}
// If cont has more parameters than the invocation has arguments, the extra
// parameters will be dead and dead-parameter will eventually create the
// eta-redex if possible.
//
// If the invocation's arguments are simply a permutation of cont's
// parameters, then there is likewise a possible reduction that involves
// rewriting the invocations of cont. We are missing that reduction here.
//
// If cont has fewer parameters than the invocation has arguments then a
// reduction would still possible, since the extra invocation arguments must
// be in scope at all the invocations of cont. For example:
//
// let cont k1(x1) = k0(x0, x1) in E -eta-> E'
// where E' has k0(x0, v) substituted for each k1(v).
//
// HOWEVER, adding continuation parameters is unlikely to be an optimization
// since it duplicates assignments used in direct-style to implement parameter
// passing.
//
// TODO(kmillikin): find real occurrences of these patterns, and see if they
// can be optimized.
if (cont.parameters.length != invoke.arguments.length) {
return false;
}
// TODO(jgruber): Linear in the parameter count. Can be improved to near
// constant time by using union-find data structure.
for (int i = 0; i < cont.parameters.length; i++) {
if (invoke.arguments[i].definition != cont.parameters[i]) {
return false;
}
}
return true;
}
Expression _unfoldDeadRefinements(Expression node) {
while (node is LetPrim) {
LetPrim let = node;
Primitive prim = let.primitive;
if (prim.hasAtLeastOneUse || prim is! Refinement) return node;
node = node.next;
}
return node;
}
bool _isBranchRedex(Branch branch) {
return _isUselessIf(branch) || branch.condition.definition is Constant;
}
bool _isBranchTargetOfUselessIf(Continuation cont) {
// A useless-if has an empty then and else branch, e.g. `if (cond);`.
//
// Detect T or F in
//
// let cont Join() = ...
// in let cont T() = Join()
// F() = Join()
// in branch condition T F
//
if (!cont.hasExactlyOneUse) return false;
Node use = cont.firstRef.parent;
if (use is! Branch) return false;
return _isUselessIf(use);
}
bool _isUselessIf(Branch branch) {
Continuation trueCont = branch.trueContinuation.definition;
Expression trueBody = _unfoldDeadRefinements(trueCont.body);
if (trueBody is! InvokeContinuation) return false;
Continuation falseCont = branch.falseContinuation.definition;
Expression falseBody = _unfoldDeadRefinements(falseCont.body);
if (falseBody is! InvokeContinuation) return false;
InvokeContinuation trueInvoke = trueBody;
InvokeContinuation falseInvoke = falseBody;
if (trueInvoke.continuation.definition !=
falseInvoke.continuation.definition) {
return false;
}
assert(trueInvoke.arguments.length == falseInvoke.arguments.length);
// Matching zero arguments should be adequate, since isomorphic true and false
// invocations should result in redundant phis which are removed elsewhere.
if (trueInvoke.arguments.isNotEmpty) return false;
return true;
}
bool _isDeadParameter(Parameter parameter) {
if (_isParameterRemoved(parameter)) return false;
// We cannot remove function parameters as an intraprocedural optimization.
if (parameter.parent is! Continuation || parameter.hasAtLeastOneUse) {
return false;
}
// We cannot remove the parameter to a call continuation, because the
// resulting expression will not be well-formed (call continuations have
// exactly one argument). The return continuation is a call continuation, so
// we cannot remove its dummy parameter.
Continuation continuation = parameter.parent;
if (!continuation.isJoinContinuation) return false;
return true;
}
/// Traverses a term and adds any found redexes to the worklist.
class _RedexVisitor extends TrampolineRecursiveVisitor {
final List<_ReductionTask> worklist;
_RedexVisitor(this.worklist);
void processLetPrim(LetPrim node) {
if (_isDeadVal(node)) {
worklist.add(new _ReductionTask(_ReductionKind.DEAD_VAL, node));
}
}
void processBranch(Branch node) {
if (_isBranchRedex(node)) {
worklist.add(new _ReductionTask(_ReductionKind.BRANCH, node));
}
}
void processContinuation(Continuation node) {
// While it would be nice to remove exception handlers that are provably
// unnecessary (e.g., the body cannot throw), that takes more sophisticated
// analysis than we do in this pass.
if (node.parent is LetHandler) return;
// Continuation beta- and eta-redexes can overlap, namely when an eta-redex
// is invoked exactly once. We prioritize continuation beta-redexes over
// eta-redexes because some reductions (e.g., dead parameter elimination)
// can destroy a continuation eta-redex. If we prioritized eta- over
// beta-redexes, this would implicitly "create" the corresponding beta-redex
// (in the sense that it would still apply) and the algorithm would not
// detect it.
if (_isDeadCont(node)) {
worklist.add(new _ReductionTask(_ReductionKind.DEAD_CONT, node));
} else if (_isBetaContLin(node)){
worklist.add(new _ReductionTask(_ReductionKind.BETA_CONT_LIN, node));
} else if (_isEtaCont(node)) {
worklist.add(new _ReductionTask(_ReductionKind.ETA_CONT, node));
}
}
void processParameter(Parameter node) {
if (_isDeadParameter(node)) {
worklist.add(new _ReductionTask(_ReductionKind.DEAD_PARAMETER, node));
}
}
}
/// Traverses a deleted CPS term, marking nodes that might participate in a
/// redex as deleted and adding newly created redexes to the worklist.
///
/// Deleted nodes that might participate in a reduction task are marked so that
/// any corresponding tasks can be skipped. Nodes are marked so by setting
/// their parent to the deleted sentinel.
class _RemovalVisitor extends TrampolineRecursiveVisitor {
final List<_ReductionTask> worklist;
_RemovalVisitor(this.worklist);
void processLetPrim(LetPrim node) {
node.parent = null;
}
void processContinuation(Continuation node) {
node.parent = null;
}
void processReference(Reference reference) {
reference.unlink();
if (reference.definition is Primitive) {
Primitive primitive = reference.definition.unrefined;
Node parent = primitive.parent;
// The parent might be the deleted sentinel, or it might be a
// Continuation or FunctionDefinition if the primitive is an argument.
if (parent is LetPrim && _isDeadVal(parent)) {
worklist.add(new _ReductionTask(_ReductionKind.DEAD_VAL, parent));
} else if (primitive is Parameter && _isDeadParameter(primitive)) {
worklist.add(new _ReductionTask(_ReductionKind.DEAD_PARAMETER,
primitive));
}
} else if (reference.definition is Continuation) {
Continuation cont = reference.definition;
Node parent = cont.parent;
// The parent might be the deleted sentinel, or it might be a
// Body if the continuation is the return continuation.
if (parent is LetCont) {
if (cont.isRecursive && cont.hasAtMostOneUse) {
// Convert recursive to nonrecursive continuations. If the
// continuation is still in use, it is either dead and will be
// removed, or it is called nonrecursively outside its body.
cont.isRecursive = false;
}
if (_isDeadCont(cont)) {
worklist.add(new _ReductionTask(_ReductionKind.DEAD_CONT, cont));
} else if (_isBetaContLin(cont)) {
worklist.add(new _ReductionTask(_ReductionKind.BETA_CONT_LIN, cont));
} else if (_isBranchTargetOfUselessIf(cont)) {
worklist.add(
new _ReductionTask(_ReductionKind.BRANCH, cont.firstRef.parent));
}
}
}
}
}
enum _ReductionKind {
DEAD_VAL,
DEAD_CONT,
BETA_CONT_LIN,
ETA_CONT,
DEAD_PARAMETER,
BRANCH
}
/// Represents a reduction task on the worklist. Implements both hashCode and
/// operator== since instantiations are used as Set elements.
class _ReductionTask {
final _ReductionKind kind;
final Node node;
int get hashCode {
return (node.hashCode << 3) | kind.index;
}
_ReductionTask(this.kind, this.node) {
assert(node is Continuation || node is LetPrim || node is Parameter ||
node is Branch);
}
bool operator==(_ReductionTask that) {
return (that.kind == this.kind && that.node == this.node);
}
String toString() => "$kind: $node";
}