blob: 06d5941e9bb4eed25c067cf3528e30a49d0cece7 [file] [log] [blame]
// Copyright (c) 2015, 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.
// @dart = 2.9
// TODO(jmesserly): import from its own package
import '../js_ast/js_ast.dart';
import 'js_names.dart' show TemporaryId;
import 'shared_compiler.dart' show YieldFinder;
/// A synthetic `let*` node, similar to that found in Scheme.
///
/// For example, postfix increment can be desugared as:
///
/// // psuedocode mix of Scheme and JS:
/// (let* (x1=expr1, x2=expr2, t=x1[x2]) { x1[x2] = t + 1; t })
///
/// [MetaLet] will simplify itself automatically when [toExpression],
/// [toStatement], [toReturn], or [toYieldStatement] is called.
///
/// * variables used once will be inlined.
/// * if used in a statement context they can emit as blocks.
/// * if return value is not used it can be eliminated, see [statelessResult].
/// * if there are no variables, the codegen will be simplified.
///
/// Because this deals with JS AST nodes, it is not aware of any Dart semantics
/// around statelessness (such as `final` variables). [variables] should not
/// be created for these Dart expressions.
///
class MetaLet extends Expression {
/// Creates a temporary to contain the value of [expr]. The temporary can be
/// used multiple times in the resulting expression. For example:
/// `expr ** 2` could be compiled as `expr * expr`. The temporary scope will
/// ensure `expr` is only evaluated once: `(x => x * x)(expr)`.
///
/// If the expression does not end up using `x` more than once, or if those
/// expressions can be treated as [stateless] (e.g. they are non-mutated
/// variables), then the resulting code will be simplified automatically.
final Map<MetaLetVariable, Expression> variables;
/// A list of expressions in the body.
/// The last value should represent the returned value.
final List<Expression> body;
/// True if the final expression in [body] can be skipped in [toStatement].
final bool statelessResult;
/// We run [toExpression] implicitly when the JS AST is visited, to get the
/// transformation to happen before the tree is printed.
/// This happens multiple times, so ensure the expression form is cached.
Expression _expression;
MetaLet(this.variables, this.body, {this.statelessResult = false});
/// Returns an expression that ignores the result. This is a cross between
/// [toExpression] and [toStatement]. Used for C-style for-loop updaters,
/// which is an expression syntactically, but functions more like a statement.
@override
Expression toVoidExpression() {
var block = toStatement();
var s = block.statements;
if (s.length == 1 && s.first is ExpressionStatement) {
return (s.first as ExpressionStatement).expression;
}
return _toInvokedFunction(block);
}
@override
Expression toAssignExpression(Expression left, [String op]) {
if (left is Identifier) {
return _simplifyAssignment(left, op: op) ?? _toAssign(left, op);
} else if (left is PropertyAccess &&
left.receiver is This &&
(left.selector is Identifier || left.selector is LiteralString)) {
return _toAssign(left, op);
}
return super.toAssignExpression(left, op);
}
Expression _toAssign(Expression left, [String op]) {
var exprs = body.toList();
exprs.add(exprs.removeLast().toAssignExpression(left, op));
return MetaLet(variables, exprs);
}
@override
Statement toVariableDeclaration(VariableBinding name) {
if (name is Identifier) {
var simple = _simplifyAssignment(name, isDeclaration: true);
if (simple != null) return simple.toStatement();
}
// We can still optimize something like:
//
// let x = ((l) => l == null ? null : l.xyz)(some.expr);
//
// can be transformed to:
//
// let l = some.expr;
// let x = l == null ? null : l.xyz;
//
// Because `x` is a declaration, we know it is safe to move.
// (see also _toAssign)
var statements = body
.map((e) =>
e == body.last ? e.toVariableDeclaration(name) : e.toStatement())
.toList();
return _finishStatement(statements);
}
Expression toExpression() {
if (_expression != null) return _expression;
var block = toReturn();
var s = block.statements;
if (s.length == 1 && s.first is Return) {
return _expression = (s.first as Return).value;
}
// Wrap it in an immediately called function to get in expression context.
return _expression = _toInvokedFunction(block);
}
@override
Block toStatement() {
// Skip return value if not used.
var statements = body.map((e) => e.toStatement()).toList();
if (statelessResult) statements.removeLast();
return _finishStatement(statements);
}
@override
Block toReturn() {
var statements = body
.map((e) => e == body.last ? e.toReturn() : e.toStatement())
.toList();
return _finishStatement(statements);
}
@override
Block toYieldStatement({bool star = false}) {
var statements = body
.map((e) =>
e == body.last ? e.toYieldStatement(star: star) : e.toStatement())
.toList();
return _finishStatement(statements);
}
@override
T accept<T>(NodeVisitor<T> visitor) {
// TODO(jmesserly): we special case vistors from js_ast.Template, because it
// doesn't know about MetaLet. Should we integrate directly?
NodeVisitor v = visitor;
if (v is InstantiatorGeneratorVisitor) {
return _templateVisitMetaLet(v) as T;
} else if (v is InterpolatedNodeAnalysis) {
return v.visitNode(this) as T;
} else {
return toExpression().accept(visitor);
}
}
@override
void visitChildren(NodeVisitor visitor) {
// TODO(jmesserly): we special case vistors from js_ast.Template, because it
// doesn't know about MetaLet. Should we integrate directly?
if (visitor is InterpolatedNodeAnalysis ||
visitor is InstantiatorGeneratorVisitor) {
variables.values.forEach((v) => v.accept(visitor));
body.forEach((v) => v.accept(visitor));
} else {
toExpression().visitChildren(visitor);
}
}
/// This generates as either a comma expression or a call.
@override
int get precedenceLevel => toExpression().precedenceLevel;
/// Patch to pretend [Template] supports visitMetaLet.
Instantiator _templateVisitMetaLet(InstantiatorGeneratorVisitor visitor) {
var valueInstantiators = variables.values.map(visitor.visit);
var bodyInstantiators = body.map(visitor.visit);
return (args) => MetaLet(
Map.fromIterables(variables.keys,
valueInstantiators.map((i) => i(args) as Expression)),
bodyInstantiators.map((i) => i(args) as Expression).toList(),
statelessResult: statelessResult);
}
Expression _toInvokedFunction(Block block) {
var finder = YieldFinder();
block.accept(finder);
if (!finder.hasYield) {
return Call(ArrowFun([], block), []);
}
// If we have a yield, it's more tricky. We'll create a `function*`, which
// we `yield*` to immediately invoke. We also may need to bind this:
Expression fn = Fun([], block, isGenerator: true);
if (finder.hasThis) fn = js.call('#.bind(this)', fn);
return Yield(Call(fn, []), star: true);
}
Block _finishStatement(List<Statement> statements) {
// Visit the tree and count how many times each temp was used.
var counter = _VariableUseCounter();
var node = Block(statements);
node.accept(counter);
// Also count the init expressions.
for (var init in variables.values) {
init.accept(counter);
}
var initializers = <VariableInitialization>[];
var substitutions = <MetaLetVariable, Expression>{};
variables.forEach((variable, init) {
// Since this is let*, subsequent variables can refer to previous ones,
// so we need to substitute here.
init = _substitute(init, substitutions);
var n = counter.counts[variable];
if (n == 1) {
// Replace interpolated exprs with their value, if it only occurs once.
substitutions[variable] = init;
} else {
// Otherwise replace it with a temp, which will be assigned once.
var temp = TemporaryId(variable.displayName);
substitutions[variable] = temp;
initializers.add(VariableInitialization(temp, init));
}
});
// Interpolate the body.
node = _substitute(node, substitutions);
if (initializers.isNotEmpty) {
var first = initializers[0];
node = Block([
initializers.length == 1
? first.value.toVariableDeclaration(first.declaration)
: VariableDeclarationList('let', initializers).toStatement(),
node
]);
}
return node;
}
/// If we finish with an assignment to an identifier, try to simplify the
/// block. For example:
///
/// result = ((_) => _.add(1), _.add(2), _)([])
///
/// Can be transformed to:
///
/// (result = [], result.add(1), result.add(2), result)
///
/// However we should not simplify in this case because `result` is read:
///
/// result = ((_) => _.addAll(result), _.add(2), _)([])
///
MetaLet _simplifyAssignment(Identifier left,
{String op, bool isDeclaration = false}) {
// See if the result value is a let* temporary variable.
var result = body.last;
if (result is MetaLetVariable && variables.containsKey(result)) {
// For assignments, make sure the identifier isn't used in body, as that
// would change the assignment order and be an invalid optimization.
if (!isDeclaration && _IdentFinder.foundIn(left.name, body)) return null;
var vars = Map<MetaLetVariable, Expression>.from(variables);
var value = vars.remove(result);
Expression assign;
if (isDeclaration) {
// Technically, putting one of these in a comma expression is not
// legal. However when isDeclaration is true, toStatement will be
// called immediately on the MetaLet, which results in legal JS.
assign = VariableDeclarationList(
'let', [VariableInitialization(left, value)]);
} else {
assign = value.toAssignExpression(left, op);
}
assert(body.isNotEmpty);
var newBody = Expression.binary([assign, ...body], ',') as Binary;
newBody = _substitute(newBody, {result: left});
return MetaLet(vars, newBody.commaToExpressionList(),
statelessResult: statelessResult);
}
return null;
}
}
/// Similar to [Template.instantiate] but works with free variables.
T _substitute<T extends Node>(
T tree, Map<MetaLetVariable, Expression> substitutions) {
var generator = InstantiatorGeneratorVisitor(/*forceCopy:*/ false);
var instantiator = generator.compile(tree);
var nodes = List<MetaLetVariable>.from(
generator.analysis.containsInterpolatedNode.whereType<MetaLetVariable>());
if (nodes.isEmpty) return tree;
return instantiator(
{for (var v in nodes) v.nameOrPosition: substitutions[v] ?? v}) as T;
}
/// A temporary variable used in a [MetaLet].
///
/// Each instance of this class represents a fresh variable. The same object
/// should be used everywhere to refer to the same variable. Different variables
/// with the same name are different, and will be renamed later on, if needed.
///
/// These variables will be replaced when the `let*` is complete, depending on
/// how often they occur and whether they can be optimized away. See [MetaLet]
/// for more information.
///
/// This class should never reach our final JS code.
class MetaLetVariable extends InterpolatedExpression {
/// The suggested display name of this variable.
///
/// This name should not be used
final String displayName;
/// Compute fresh IDs to avoid
static int _uniqueId = 0;
MetaLetVariable(this.displayName) : super(displayName + '@${++_uniqueId}');
}
class _VariableUseCounter extends BaseVisitor<void> {
final counts = <MetaLetVariable, int>{};
@override
void visitInterpolatedExpression(InterpolatedExpression node) {
if (node is MetaLetVariable) {
var n = counts[node];
counts[node] = n == null ? 1 : n + 1;
}
}
}
class _IdentFinder extends BaseVisitor<void> {
final String name;
bool found = false;
_IdentFinder(this.name);
static bool foundIn(String name, List<Node> body) {
var finder = _IdentFinder(name);
for (var expr in body) {
expr.accept(finder);
if (finder.found) return true;
}
return false;
}
@override
void visitIdentifier(Identifier node) {
if (node.name == name) found = true;
}
@override
void visitNode(Node node) {
if (!found) super.visitNode(node);
}
}