Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(155)

Side by Side Diff: pkg/compiler/lib/src/cps_ir/bounds_checker.dart

Issue 1353443002: dart2js cps: Add a pass for eliminating bounds checks. (Closed) Base URL: git@github.com:dart-lang/sdk.git@master
Patch Set: Put "pending statics" error in status file Created 5 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | pkg/compiler/lib/src/cps_ir/cps_fragment.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 dart2js.cps_ir.bounds_checker;
6
7 import 'cps_ir_nodes.dart';
8 import 'optimizers.dart' show Pass;
9 import 'octagon.dart';
10 import '../constants/values.dart';
11 import 'cps_fragment.dart';
12 import 'type_mask_system.dart';
13 import '../world.dart';
14 import '../elements/elements.dart';
15
16 /// Eliminates bounds checks when they can be proven safe.
17 ///
18 /// In general, this pass will try to eliminate any branch with arithmetic
19 /// in the condition, i.e. `x < y`, `x <= y`, `x == y` etc.
20 ///
21 /// The analysis uses an [Octagon] abstract domain. Unlike traditional octagon
22 /// analyzers, we do not use a closed matrix representation, but just maintain
23 /// a bucket of constraints. Constraints can therefore be added and removed
24 /// on-the-fly without significant overhead.
25 ///
26 /// We never copy the constraint system. While traversing the IR, the
27 /// constraint system is mutated to take into account the knowledge that is
28 /// valid for the current location. Constraints are added when entering a
29 /// branch, for instance, and removed again after the branch has been processed.
30 ///
31 /// Loops are analyzed in two passes. The first pass establishes monotonicity
32 /// of loop variables, which the second pass uses to compute upper/lower bounds.
33 /// The first pass also records whether any side effects occurred in the loop.
34 ///
35 /// The two-pass scheme is suboptimal compared to a least fixed-point
36 /// computation, but does not require repeated iteration. Repeated iteration
37 /// would be expensive, since we cannot perform a sparse analysis with our
38 /// mutable octagon representation.
39 class BoundsChecker extends TrampolineRecursiveVisitor implements Pass {
40 String get passName => 'Bounds checker';
41
42 static const int MAX_UINT32 = (1 << 32) - 1;
43
44 /// All integers of this magnitude or less are representable as JS numbers.
45 static const int MAX_SAFE_INT = (1 << 53) - 1;
46
47 /// Marker to indicate that a continuation should get a unique effect number.
48 static const int NEW_EFFECT = -1;
49
50 final TypeMaskSystem types;
51 final World world;
52
53 /// Fields for the constraint system and its variables.
54 final Octagon octagon = new Octagon();
55 final Map<Primitive, SignedVariable> valueOf = {};
56 final Map<Primitive, Map<int, SignedVariable>> lengthOf = {};
57
58 /// Fields for the two-pass handling of loops.
59 final Set<Continuation> loopsWithSideEffects = new Set<Continuation>();
60 final Map<Parameter, Monotonicity> monotonicity = <Parameter, Monotonicity>{};
61 bool isStrongLoopPass;
62 bool foundLoop = false;
63
64 /// Fields for tracking side effects.
65 ///
66 /// The IR is divided into regions wherein the lengths of indexable objects
67 /// are known not to change. Regions are identified by their "effect number".
68 final Map<Continuation, int> effectNumberAt = <Continuation, int>{};
69 int currentEffectNumber = 0;
70 int effectNumberCounter = 0;
71
72 BoundsChecker(this.types, this.world);
73
74 void rewrite(FunctionDefinition node) {
75 isStrongLoopPass = false;
76 visit(node);
77 if (foundLoop) {
78 isStrongLoopPass = true;
79 effectNumberAt.clear();
80 visit(node);
81 }
82 }
83
84 // ------------- VARIABLES -----------------
85
86 int makeNewEffect() => ++effectNumberCounter;
87
88 bool isInt(Primitive prim) {
89 return types.isDefinitelyInt(prim.type);
90 }
91
92 bool isUInt32(Primitive prim) {
93 return types.isDefinitelyUint32(prim.type);
94 }
95
96 bool isNonNegativeInt(Primitive prim) {
97 return types.isDefinitelyNonNegativeInt(prim.type);
98 }
99
100 /// Get a constraint variable representing the numeric value of [number].
101 SignedVariable getValue(Primitive number) {
102 number = number.effectiveDefinition;
103 int min, max;
104 if (isUInt32(number)) {
105 min = 0;
106 max = MAX_UINT32;
107 } else if (isNonNegativeInt(number)) {
108 min = 0;
109 }
110 return valueOf.putIfAbsent(number, () => octagon.makeVariable(min, max));
111 }
112
113 /// Get a constraint variable representing the length of [indexableObject] at
114 /// program locations with the given [effectCounter].
115 SignedVariable getLength(Primitive indexableObject, int effectCounter) {
116 indexableObject = indexableObject.effectiveDefinition;
117 if (indexableObject.type != null &&
118 types.isDefinitelyFixedLengthIndexable(indexableObject.type)) {
119 // Always use the same effect counter if the length is immutable.
120 effectCounter = 0;
121 }
122 return lengthOf
123 .putIfAbsent(indexableObject, () => <int, SignedVariable>{})
124 .putIfAbsent(effectCounter, () => octagon.makeVariable(0, MAX_UINT32));
125 }
126
127 // ------------- CONSTRAINT HELPERS -----------------
128
129 /// Puts the given constraint "in scope" by adding it to the octagon, and
130 /// pushing a stack action that will remove it again.
131 void applyConstraint(SignedVariable v1, SignedVariable v2, int k) {
132 Constraint constraint = new Constraint(v1, v2, k);
133 octagon.pushConstraint(constraint);
134 pushAction(() => octagon.popConstraint(constraint));
135 }
136
137 /// Return true if we can prove that `v1 + v2 <= k`.
138 bool testConstraint(SignedVariable v1, SignedVariable v2, int k) {
139 // Add the negated constraint and check for solvability.
140 // !(v1 + v2 <= k) <==> -v1 - v2 <= -k-1
141 Constraint constraint = new Constraint(v1.negated, v2.negated, -k - 1);
142 octagon.pushConstraint(constraint);
143 bool answer = octagon.isUnsolvable;
144 octagon.popConstraint(constraint);
145 return answer;
146 }
147
148 void makeLessThanOrEqual(SignedVariable v1, SignedVariable v2) {
149 // v1 <= v2 <==> v1 - v2 <= 0
150 applyConstraint(v1, v2.negated, 0);
151 }
152
153 void makeLessThan(SignedVariable v1, SignedVariable v2) {
154 // v1 < v2 <==> v1 - v2 <= -1
155 applyConstraint(v1, v2.negated, -1);
156 }
157
158 void makeGreaterThanOrEqual(SignedVariable v1, SignedVariable v2) {
159 // v1 >= v2 <==> v2 - v1 <= 0
160 applyConstraint(v2, v1.negated, 0);
161 }
162
163 void makeGreaterThan(SignedVariable v1, SignedVariable v2) {
164 // v1 > v2 <==> v2 - v1 <= -1
165 applyConstraint(v2, v1.negated, -1);
166 }
167
168 void makeConstant(SignedVariable v1, int k) {
169 // We model this using the constraints:
170 // v1 + v1 <= 2k
171 // -v1 - v1 <= -2k
172 applyConstraint(v1, v1, 2 * k);
173 applyConstraint(v1.negated, v1.negated, -2 * k);
174 }
175
176 /// Make `v1 = v2 + k`.
177 void makeExactSum(SignedVariable v1, SignedVariable v2, int k) {
178 applyConstraint(v1, v2.negated, k);
179 applyConstraint(v1.negated, v2, -k);
180 }
181
182 /// Make `v1 = v2 [+] k` where [+] represents floating-point addition.
183 void makeFloatingPointSum(SignedVariable v1, SignedVariable v2, int k) {
184 if (isDefinitelyLessThanOrEqualToConstant(v2, MAX_SAFE_INT - k) &&
185 isDefinitelyGreaterThanOrEqualToConstant(v2, -MAX_SAFE_INT + k)) {
186 // The result is known to be in the 53-bit range, so no rounding occurs.
187 makeExactSum(v1, v2, k);
188 } else {
189 // A rounding error may occur, so the result may not be exactly v2 + k.
190 // We can still add monotonicity constraints:
191 // adding a positive number cannot return a lesser number
192 // adding a negative number cannot return a greater number
193 if (k >= 0) {
194 // v1 >= v2 <==> v2 - v1 <= 0 <==> -v1 + v2 <= 0
195 applyConstraint(v1.negated, v2, 0);
196 } else {
197 // v1 <= v2 <==> v1 - v2 <= 0
198 applyConstraint(v1, v2.negated, 0);
199 }
200 }
201 }
202
203 void makeEqual(SignedVariable v1, SignedVariable v2) {
204 // We model this using the constraints:
205 // v1 <= v2 <==> v1 - v2 <= 0
206 // v1 >= v2 <==> v2 - v1 <= 0
207 applyConstraint(v1, v2.negated, 0);
208 applyConstraint(v2, v1.negated, 0);
209 }
210
211 void makeNotEqual(SignedVariable v1, SignedVariable v2) {
212 // The octagon cannot represent non-equality, but we can sharpen a weak
213 // inequality to a sharp one. If v1 and v2 are already known to be equal,
214 // this will create a contradiction and eliminate a dead branch.
215 // This is necessary for eliminating concurrent modification checks.
216 if (isDefinitelyLessThanOrEqualTo(v1, v2)) {
217 makeLessThan(v1, v2);
218 } else if (isDefinitelyGreaterThanOrEqualTo(v1, v2)) {
219 makeGreaterThan(v1, v2);
220 }
221 }
222
223 /// Return true if we can prove that `v1 <= v2`.
224 bool isDefinitelyLessThanOrEqualTo(SignedVariable v1, SignedVariable v2) {
225 return testConstraint(v1, v2.negated, 0);
226 }
227
228 /// Return true if we can prove that `v1 >= v2`.
229 bool isDefinitelyGreaterThanOrEqualTo(SignedVariable v1, SignedVariable v2) {
230 return testConstraint(v2, v1.negated, 0);
231 }
232
233 bool isDefinitelyLessThanOrEqualToConstant(SignedVariable v1, int value) {
234 // v1 <= value <==> v1 + v1 <= 2 * value
235 return testConstraint(v1, v1, 2 * value);
236 }
237
238 bool isDefinitelyGreaterThanOrEqualToConstant(SignedVariable v1, int value) {
239 // v1 >= value <==> -v1 - v1 <= -2 * value
240 return testConstraint(v1.negated, v1.negated, -2 * value);
241 }
242
243 // ------------- TAIL EXPRESSIONS -----------------
244
245 @override
246 void visitBranch(Branch node) {
247 Primitive condition = node.condition.definition;
248 Continuation trueCont = node.trueContinuation.definition;
249 Continuation falseCont = node.falseContinuation.definition;
250 effectNumberAt[trueCont] = currentEffectNumber;
251 effectNumberAt[falseCont] = currentEffectNumber;
252 pushAction(() {
253 // If the branching condition is known statically, either or both of the
254 // branch continuations will be replaced by Unreachable. Clean up the
255 // branch afterwards.
256 if (trueCont.body is Unreachable && falseCont.body is Unreachable) {
257 destroyAndReplace(node, new Unreachable());
258 } else if (trueCont.body is Unreachable) {
259 destroyAndReplace(
260 node, new InvokeContinuation(falseCont, <Parameter>[]));
261 } else if (falseCont.body is Unreachable) {
262 destroyAndReplace(
263 node, new InvokeContinuation(trueCont, <Parameter>[]));
264 }
265 });
266 void pushTrue(makeConstraint()) {
267 pushAction(() {
268 makeConstraint();
269 push(trueCont);
270 });
271 }
272 void pushFalse(makeConstraint()) {
273 pushAction(() {
274 makeConstraint();
275 push(falseCont);
276 });
277 }
278 if (condition is ApplyBuiltinOperator &&
279 condition.arguments.length == 2 &&
280 isInt(condition.arguments[0].definition) &&
281 isInt(condition.arguments[1].definition)) {
282 SignedVariable v1 = getValue(condition.arguments[0].definition);
283 SignedVariable v2 = getValue(condition.arguments[1].definition);
284 switch (condition.operator) {
285 case BuiltinOperator.NumLe:
286 pushTrue(() => makeLessThanOrEqual(v1, v2));
287 pushFalse(() => makeGreaterThan(v1, v2));
288 return;
289 case BuiltinOperator.NumLt:
290 pushTrue(() => makeLessThan(v1, v2));
291 pushFalse(() => makeGreaterThanOrEqual(v1, v2));
292 return;
293 case BuiltinOperator.NumGe:
294 pushTrue(() => makeGreaterThanOrEqual(v1, v2));
295 pushFalse(() => makeLessThan(v1, v2));
296 return;
297 case BuiltinOperator.NumGt:
298 pushTrue(() => makeGreaterThan(v1, v2));
299 pushFalse(() => makeLessThanOrEqual(v1, v2));
300 return;
301 case BuiltinOperator.StrictEq:
302 pushTrue(() => makeEqual(v1, v2));
303 pushFalse(() => makeNotEqual(v1, v2));
304 return;
305 case BuiltinOperator.StrictNeq:
306 pushTrue(() => makeNotEqual(v1, v2));
307 pushFalse(() => makeEqual(v1, v2));
308 return;
309 default:
310 }
311 }
312
313 push(trueCont);
314 push(falseCont);
315 }
316
317 @override
318 void visitConstant(Constant node) {
319 // TODO(asgerf): It might be faster to inline the constant in the
320 // constraints that reference it.
321 if (node.value.isInt) {
322 IntConstantValue constant = node.value;
323 makeConstant(getValue(node), constant.primitiveValue);
324 }
325 }
326
327 @override
328 void visitApplyBuiltinOperator(ApplyBuiltinOperator node) {
329 if (node.operator != BuiltinOperator.NumAdd &&
330 node.operator != BuiltinOperator.NumSubtract) {
331 return;
332 }
333 if (!isInt(node.arguments[0].definition) ||
334 !isInt(node.arguments[1].definition)) {
335 return;
336 }
337 if (!isInt(node)) {
338 // TODO(asgerf): The result of this operation should always be an integer,
339 // but currently type propagation does not always prove this.
340 return;
341 }
342 // We have `v1 = v2 +/- v3`, but the octagon cannot represent constraints
343 // involving more than two variables. Check if one operand is a constant.
344 int getConstantArgument(int n) {
345 Primitive prim = node.arguments[n].definition;
346 if (prim is Constant && prim.value.isInt) {
347 IntConstantValue constant = prim.value;
348 return constant.primitiveValue;
349 }
350 return null;
351 }
352 int constant = getConstantArgument(0);
353 int operandIndex = 1;
354 if (constant == null) {
355 constant = getConstantArgument(1);
356 operandIndex = 0;
357 }
358 if (constant == null) {
359 // Neither argument was a constant.
360 // Classical octagon-based analyzers would compute upper and lower bounds
361 // for the two operands and add constraints for the result based on
362 // those. For performance reasons we omit that.
363 // TODO(asgerf): It seems expensive, but we should evaluate it.
364 return;
365 }
366 SignedVariable v1 = getValue(node);
367 SignedVariable v2 = getValue(node.arguments[operandIndex].definition);
368
369 if (node.operator == BuiltinOperator.NumAdd) {
370 // v1 = v2 + const
371 makeFloatingPointSum(v1, v2, constant);
372 } else if (operandIndex == 0) {
373 // v1 = v2 - const
374 makeFloatingPointSum(v1, v2, -constant);
375 } else {
376 // v1 = const - v2 <==> v1 = (-v2) + const
377 makeFloatingPointSum(v1, v2.negated, constant);
378 }
379 }
380
381 @override
382 void visitGetLength(GetLength node) {
383 valueOf[node] = getLength(node.object.definition, currentEffectNumber);
384 }
385
386 void analyzeLoopEntry(InvokeContinuation node) {
387 foundLoop = true;
388 Continuation cont = node.continuation.definition;
389 if (isStrongLoopPass) {
390 for (int i = 0; i < node.arguments.length; ++i) {
391 Parameter param = cont.parameters[i];
392 if (!isInt(param)) continue;
393 Primitive initialValue = node.arguments[i].definition;
394 SignedVariable initialVariable = getValue(initialValue);
395 Monotonicity mono = monotonicity[param];
396 if (mono == null) {
397 // Value never changes. This is extremely uncommon.
398 initialValue.substituteFor(param);
399 } else if (mono == Monotonicity.Increasing) {
400 makeGreaterThanOrEqual(getValue(param), initialVariable);
401 } else if (mono == Monotonicity.Decreasing) {
402 makeLessThanOrEqual(getValue(param), initialVariable);
403 }
404 }
405 if (loopsWithSideEffects.contains(cont)) {
406 currentEffectNumber = makeNewEffect();
407 }
408 } else {
409 // During the weak pass, conservatively make a new effect number in the
410 // loop body. This may be strengthened during the strong pass.
411 currentEffectNumber = effectNumberAt[cont] = makeNewEffect();
412 }
413 push(cont);
414 }
415
416 void analyzeLoopContinue(InvokeContinuation node) {
417 Continuation cont = node.continuation.definition;
418
419 // During the strong loop phase, there is no need to compute monotonicity,
420 // and we already put bounds on the loop variables when we went into the
421 // loop.
422 if (isStrongLoopPass) return;
423
424 // For each loop parameter, try to prove that the new value is definitely
425 // less/greater than its old value. When we fail to prove this, update the
426 // monotonicity flag accordingly.
427 for (int i = 0; i < node.arguments.length; ++i) {
428 Parameter param = cont.parameters[i];
429 if (!isInt(param)) continue;
430 SignedVariable arg = getValue(node.arguments[i].definition);
431 SignedVariable paramVar = getValue(param);
432 if (!isDefinitelyLessThanOrEqualTo(arg, paramVar)) {
433 // We couldn't prove that the value does not increase, so assume
434 // henceforth that it might be increasing.
435 markMonotonicity(cont.parameters[i], Monotonicity.Increasing);
436 }
437 if (!isDefinitelyGreaterThanOrEqualTo(arg, paramVar)) {
438 // We couldn't prove that the value does not decrease, so assume
439 // henceforth that it might be decreasing.
440 markMonotonicity(cont.parameters[i], Monotonicity.Decreasing);
441 }
442 }
443
444 // If a side effect has occurred between the entry and continue, mark
445 // the loop as having side effects.
446 if (currentEffectNumber != effectNumberAt[cont]) {
447 loopsWithSideEffects.add(cont);
448 }
449 }
450
451 void markMonotonicity(Parameter param, Monotonicity mono) {
452 Monotonicity current = monotonicity[param];
453 if (current == null) {
454 monotonicity[param] = mono;
455 } else if (current != mono) {
456 monotonicity[param] = Monotonicity.NotMonotone;
457 }
458 }
459
460 @override
461 void visitInvokeContinuation(InvokeContinuation node) {
462 Continuation cont = node.continuation.definition;
463 if (node.isRecursive) {
464 analyzeLoopContinue(node);
465 } else if (cont.isRecursive) {
466 analyzeLoopEntry(node);
467 } else {
468 int effect = effectNumberAt[cont];
469 if (effect == null) {
470 effectNumberAt[cont] = currentEffectNumber;
471 } else if (effect != currentEffectNumber && effect != NEW_EFFECT) {
472 effectNumberAt[cont] = NEW_EFFECT;
473 }
474 // TODO(asgerf): Compute join for parameters to increase precision?
475 }
476 }
477
478 // ---------------- CALL EXPRESSIONS --------------------
479
480 @override
481 void visitInvokeMethod(InvokeMethod node) {
482 // TODO(asgerf): What we really need is a "changes length" side effect flag.
483 if (world
484 .getSideEffectsOfSelector(node.selector, node.mask)
485 .changesIndex()) {
486 currentEffectNumber = makeNewEffect();
487 }
488 push(node.continuation.definition);
489 }
490
491 @override
492 void visitInvokeStatic(InvokeStatic node) {
493 if (world.getSideEffectsOfElement(node.target).changesIndex()) {
494 currentEffectNumber = makeNewEffect();
495 }
496 push(node.continuation.definition);
497 }
498
499 @override
500 void visitInvokeMethodDirectly(InvokeMethodDirectly node) {
501 FunctionElement target = node.target;
502 if (target is ConstructorBodyElement) {
503 ConstructorBodyElement body = target;
504 target = body.constructor;
505 }
506 if (world.getSideEffectsOfElement(target).changesIndex()) {
507 currentEffectNumber = makeNewEffect();
508 }
509 push(node.continuation.definition);
510 }
511
512 @override
513 void visitInvokeConstructor(InvokeConstructor node) {
514 if (world.getSideEffectsOfElement(node.target).changesIndex()) {
515 currentEffectNumber = makeNewEffect();
516 }
517 push(node.continuation.definition);
518 }
519
520 @override
521 void visitTypeCast(TypeCast node) {
522 push(node.continuation.definition);
523 }
524
525 @override
526 void visitGetLazyStatic(GetLazyStatic node) {
527 // TODO(asgerf): How do we get the side effects of a lazy field initializer?
528 currentEffectNumber = makeNewEffect();
529 push(node.continuation.definition);
530 }
531
532 @override
533 void visitForeignCode(ForeignCode node) {
534 if (node.nativeBehavior.sideEffects.changesIndex()) {
535 currentEffectNumber = makeNewEffect();
536 }
537 push(node.continuation.definition);
538 }
539
540 @override
541 void visitAwait(Await node) {
542 currentEffectNumber = makeNewEffect();
543 push(node.continuation.definition);
544 }
545
546 @override
547 void visitYield(Yield node) {
548 currentEffectNumber = makeNewEffect();
549 push(node.continuation.definition);
550 }
551
552 // ---------------- PRIMITIVES --------------------
553
554 @override
555 void visitApplyBuiltinMethod(ApplyBuiltinMethod node) {
556 Primitive receiver = node.receiver.definition;
557 int effectBefore = currentEffectNumber;
558 currentEffectNumber = makeNewEffect();
559 int effectAfter = currentEffectNumber;
560 SignedVariable lengthBefore = getLength(receiver, effectBefore);
561 SignedVariable lengthAfter = getLength(receiver, effectAfter);
562 switch (node.method) {
563 case BuiltinMethod.Push:
564 // after = before + count
565 int count = node.arguments.length;
566 makeExactSum(lengthAfter, lengthBefore, count);
567 break;
568
569 case BuiltinMethod.Pop:
570 // after = before - 1
571 makeExactSum(lengthAfter, lengthBefore, -1);
572 break;
573 }
574 }
575
576 @override
577 void visitLiteralList(LiteralList node) {
578 makeConstant(getLength(node, currentEffectNumber), node.values.length);
579 }
580
581 // ---------------- INTERIOR EXPRESSIONS --------------------
582
583 @override
584 Expression traverseContinuation(Continuation cont) {
585 if (octagon.isUnsolvable) {
586 destroyAndReplace(cont.body, new Unreachable());
587 } else {
588 int effect = effectNumberAt[cont];
589 if (effect != null) {
590 currentEffectNumber = effect == NEW_EFFECT ? makeNewEffect() : effect;
591 }
592 }
593 return cont.body;
594 }
595
596 @override
597 Expression traverseLetCont(LetCont node) {
598 // Join continuations should be pushed at declaration-site, so all their
599 // call sites are seen before they are analyzed.
600 // Other continuations are pushed at the use site.
601 for (Continuation cont in node.continuations) {
602 if (cont.hasAtLeastOneUse &&
603 !cont.isRecursive &&
604 cont.firstRef.parent is InvokeContinuation) {
605 push(cont);
606 }
607 }
608 return node.body;
609 }
610 }
611
612 /// Lattice representing the known (weak) monotonicity of a loop variable.
613 ///
614 /// The lattice bottom is represented by `null` and represents the case where
615 /// the loop variable never changes value during the loop.
616 enum Monotonicity { NotMonotone, Increasing, Decreasing, }
OLDNEW
« no previous file with comments | « no previous file | pkg/compiler/lib/src/cps_ir/cps_fragment.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698