Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 library dev_compiler.src.codegen.js_metalet; | |
| 6 | |
| 7 // TODO(jmesserly): import from its own package | |
| 8 import 'package:dev_compiler/src/js/js_ast.dart'; | |
| 9 import 'package:dev_compiler/src/js/precedence.dart'; | |
| 10 | |
| 11 import 'js_names.dart' show JSTemporary; | |
| 12 | |
| 13 /// A synthetic `let*` node, similar to that found in Scheme. | |
| 14 /// | |
| 15 /// For example, postfix increment can be desugared as: | |
| 16 /// | |
| 17 /// // psuedocode mix of Scheme and JS: | |
| 18 /// (let* (x1=expr1, x2=expr2, t=expr1[expr2]) { x1[x2] = t + 1; t }) | |
|
Leaf
2015/04/08 04:08:34
t=x1[x2] ?
Jennifer Messerly
2015/04/08 16:35:46
Good catch! Yes :)
| |
| 19 /// | |
| 20 /// [JSMetaLet] will simplify itself automatically when [toExpression], | |
| 21 /// [toStatement], or [toReturn] is called. | |
| 22 /// | |
| 23 /// * variables used once will be inlined. | |
| 24 /// * if used in a statement context they can emit as blocks. | |
| 25 /// * if return value is not used it can be eliminated, see [statelessResult]. | |
| 26 /// * if there are no variables, the codegen will be simplified. | |
| 27 /// | |
| 28 /// Because this deals with JS AST nodes, it is not aware of any Dart semantics | |
| 29 /// around statelessness (such as `final` variables). [variables] should not | |
| 30 /// be created for these Dart expressions. | |
| 31 /// | |
| 32 class JSMetaLet extends Expression { | |
| 33 /// Creates a temporary to contain the value of [expr]. The temporary can be | |
| 34 /// used multiple times in the resulting expression. For example: | |
| 35 /// `expr ** 2` could be compiled as `expr * expr`. The temporary scope will | |
| 36 /// ensure `expr` is only evaluated once: `(x => x * x)(expr)`. | |
| 37 /// | |
| 38 /// If the expression does not end up using `x` more than once, or if those | |
| 39 /// expressions can be treated as [stateless] (e.g. they are non-mutated | |
| 40 /// variables), then the resulting code will be simplified automatically. | |
| 41 final Map<String, Expression> variables; | |
| 42 | |
| 43 /// A list of expressions in the body. | |
| 44 /// Conceptually this is like a comma expression: the last value is returned. | |
| 45 final List<Expression> body; | |
| 46 | |
| 47 /// True if the final expression in [body] can be skipped in [toStatement]. | |
| 48 final bool statelessResult; | |
| 49 | |
| 50 Expression _expression; | |
| 51 Statement _statement; | |
| 52 Statement _return; | |
| 53 | |
| 54 JSMetaLet(this.variables, this.body, {this.statelessResult: false}); | |
| 55 | |
| 56 /// Returns an expression that ignores the result. This is similar to | |
| 57 /// [toStatement] but returns an expression. | |
| 58 Expression toVoidExpression() { | |
| 59 if (!statelessResult) return this; | |
| 60 // Return a new MetaLet. | |
| 61 // This allows [toStatement] and [toReturn] to compose nicely. | |
| 62 return new JSMetaLet(variables, body.toList()..removeLast()); | |
| 63 } | |
| 64 | |
| 65 Expression toAssignExpression(Expression left) { | |
| 66 if (left is Identifier) { | |
| 67 var simple = _simplifyAssignment(left); | |
| 68 if (simple != null) return simple; | |
| 69 | |
| 70 var exprs = body.toList(); | |
| 71 exprs.add(exprs.removeLast().toAssignExpression(left)); | |
| 72 return new JSMetaLet(variables, exprs); | |
| 73 } | |
| 74 return super.toAssignExpression(left); | |
| 75 } | |
| 76 | |
| 77 Statement toVariableDeclaration(Identifier name) { | |
| 78 var simple = _simplifyAssignment(name, isDeclaration: true); | |
| 79 if (simple != null) return simple.toStatement(); | |
| 80 return super.toVariableDeclaration(name); | |
| 81 } | |
| 82 | |
| 83 Expression toExpression() { | |
| 84 if (_expression != null) return _expression; | |
| 85 | |
| 86 var params = []; | |
| 87 var values = []; | |
| 88 var letBody = _build(params, values, new Expression.binary(body, ',')); | |
| 89 | |
| 90 // Note that [precedenceLevel] must be aware of this possibility, because | |
| 91 // comma expression has different precedence from call. | |
| 92 if (params.isEmpty) return _expression = letBody; | |
| 93 | |
| 94 // Assign parameters inside the body, to get sequential let* behavior | |
|
Jennifer Messerly
2015/04/07 22:09:52
for a single variable we could emit:
(x => x
Leaf
2015/04/08 04:08:34
I like the former, but I suspect it's because I'm
| |
| 95 // (subsequent bindings can refer to previous ones), and also to be a bit | |
| 96 // more readable (lexical order matches source order). | |
| 97 var vars = []; | |
| 98 for (int i = 0; i < params.length; i++) { | |
| 99 vars.add(new Assignment(params[i], values[i])); | |
| 100 } | |
| 101 letBody = new Binary(',', new Expression.binary(vars, ','), letBody); | |
| 102 | |
| 103 // Convert the comma back to a block, for readability. | |
| 104 if (vars.length + body.length >= 4) letBody = letBody.toReturn(); | |
| 105 | |
| 106 return new Call(new ArrowFun(params, letBody), []); | |
| 107 } | |
| 108 | |
| 109 Statement toStatement() { | |
| 110 if (_statement != null) return _statement; | |
|
Leaf
2015/04/08 04:08:34
I didn't follow the need for caching. Do these re
Jennifer Messerly
2015/04/08 16:35:46
That's fair. It's only really needed for toExpress
| |
| 111 | |
| 112 // Skip return value if not used. | |
| 113 var statements = body.map((e) => e.toStatement()).toList(); | |
| 114 if (statelessResult) statements.removeLast(); | |
| 115 return _statement = _finishStatement(statements); | |
| 116 } | |
| 117 | |
| 118 Statement toReturn() { | |
| 119 if (_return != null) return _return; | |
| 120 var statements = body | |
| 121 .map((e) => e == body.last ? e.toReturn() : e.toStatement()) | |
| 122 .toList(); | |
| 123 return _return = _finishStatement(statements); | |
| 124 } | |
| 125 | |
| 126 accept(NodeVisitor visitor) => toExpression().accept(visitor); | |
| 127 | |
| 128 void visitChildren(NodeVisitor visitor) { | |
| 129 toExpression().visitChildren(visitor); | |
| 130 } | |
| 131 | |
| 132 /// This generates as either a comma expression or a call. | |
| 133 int get precedenceLevel => variables.isEmpty ? EXPRESSION : CALL; | |
| 134 | |
| 135 Statement _finishStatement(List<Statement> statements) { | |
| 136 var params = []; | |
| 137 var values = []; | |
| 138 var block = _build(params, values, new Block(statements)); | |
| 139 if (params.isEmpty) return _return = block; | |
| 140 | |
| 141 var vars = []; | |
| 142 for (int i = 0; i < params.length; i++) { | |
| 143 vars.add(new VariableInitialization(params[i], values[i])); | |
| 144 } | |
| 145 | |
| 146 return new Block( | |
| 147 [new VariableDeclarationList('let', vars).toStatement(), block]); | |
| 148 } | |
| 149 | |
| 150 Node _build(List<JSTemporary> params, List<Expression> values, Node node) { | |
| 151 // Visit the tree and count how many times each temp was used. | |
| 152 var counter = new _VariableUseCounter(); | |
| 153 node.accept(counter); | |
| 154 // Also count the init expressions. | |
| 155 for (var init in variables.values) init.accept(counter); | |
| 156 | |
| 157 var substitutions = {}; | |
| 158 _substitute(node) => new Template(null, node).safeCreate(substitutions); | |
| 159 | |
| 160 variables.forEach((name, init) { | |
| 161 // Since this is let*, subsequent variables can refer to previous ones, | |
| 162 // so we need to substitute here. | |
| 163 init = _substitute(init); | |
| 164 int n = counter.counts[name]; | |
| 165 if (n == null || n < 2) { | |
| 166 substitutions[name] = _substitute(init); | |
| 167 } else { | |
| 168 params.add(substitutions[name] = new JSTemporary(name)); | |
| 169 values.add(init); | |
| 170 } | |
| 171 }); | |
| 172 | |
| 173 // Interpolate the body: | |
| 174 // Replace interpolated exprs with their value, if it only occurs once. | |
| 175 // Otherwise replace it with a temp, which will be assigned once. | |
| 176 return _substitute(node); | |
| 177 } | |
| 178 | |
| 179 /// If we finish with an assignment to an identifier, try to simplify the | |
| 180 /// block. For example: | |
| 181 /// | |
| 182 /// ((_) => _.add(1), _.add(2), result = _)([]) | |
| 183 /// | |
| 184 /// Can be transformed to: | |
| 185 /// | |
| 186 /// (result = [], result.add(1), result.add(2), result) | |
| 187 /// | |
| 188 /// However we should not simplify in this case because `result` is read: | |
| 189 /// | |
| 190 /// ((_) => _.addAll(result), _.add(2), result = _)([]) | |
| 191 /// | |
| 192 JSMetaLet _simplifyAssignment(Identifier left, {bool isDeclaration: false}) { | |
| 193 // See if the result value is a let* temporary variable. | |
| 194 if (body.last is! InterpolatedExpression) return null; | |
| 195 | |
| 196 InterpolatedExpression last = body.last; | |
| 197 String name = last.nameOrPosition; | |
| 198 if (!variables.containsKey(name)) return null; | |
| 199 | |
| 200 // Variables declared can't be used inside their initializer. | |
| 201 if (!isDeclaration) { | |
| 202 var finder = new _IdentFinder(left.name); | |
| 203 for (var expr in body) { | |
| 204 if (finder.found) break; | |
| 205 expr.accept(finder); | |
| 206 } | |
| 207 // If the identifier was used elsewhere, bail, because we're going to chan ge | |
|
Jennifer Messerly
2015/04/07 22:00:28
oops, long line.
| |
| 208 // the order of when the assignment happens. | |
| 209 if (finder.found) return null; | |
| 210 } | |
| 211 | |
| 212 var vars = new Map<String, Expression>.from(variables); | |
| 213 var value = vars.remove(name); | |
| 214 Expression assign; | |
| 215 if (isDeclaration) { | |
| 216 // Technically, putting one of these in a comma expression is not | |
| 217 // legal. However when isDeclaration is true, toStatement will be | |
| 218 // called immediately on the JSMetaLet, which results in legal JS. | |
| 219 assign = new VariableDeclarationList( | |
| 220 'let', [new VariableInitialization(left, value)]); | |
| 221 } else { | |
| 222 assign = value.toAssignExpression(left); | |
| 223 } | |
| 224 | |
| 225 var newBody = new Expression.binary([assign]..addAll(body), ','); | |
| 226 Binary comma = new Template(null, newBody).safeCreate({name: left}); | |
| 227 return new JSMetaLet(vars, comma.commaToExpressionList(), | |
| 228 statelessResult: statelessResult); | |
| 229 } | |
| 230 } | |
| 231 | |
| 232 class _VariableUseCounter extends BaseVisitor { | |
| 233 final counts = <String, int>{}; | |
| 234 visitInterpolatedExpression(InterpolatedExpression node) { | |
| 235 int n = counts[node.nameOrPosition]; | |
| 236 counts[node.nameOrPosition] = n == null ? 1 : n + 1; | |
| 237 } | |
| 238 } | |
| 239 | |
| 240 class _IdentFinder extends BaseVisitor { | |
| 241 final String name; | |
| 242 bool found = false; | |
| 243 _IdentFinder(this.name); | |
| 244 | |
| 245 visitIdentifier(Identifier node) { | |
| 246 if (node.name == name) found = true; | |
| 247 } | |
| 248 visitNode(Node node) { | |
| 249 if (!found) super.visitNode(node); | |
| 250 } | |
| 251 } | |
| OLD | NEW |