OLD | NEW |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library tree_ir.optimization.logical_rewriter; | 5 library tree_ir.optimization.logical_rewriter; |
6 | 6 |
7 import '../tree_ir_nodes.dart'; | 7 import '../tree_ir_nodes.dart'; |
8 import 'optimization.dart' show Pass; | 8 import 'optimization.dart' show Pass; |
9 import '../../constants/values.dart' as values; | 9 import '../../constants/values.dart' as values; |
10 | 10 |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
45 /// Conditionals are tricky to rewrite when they occur out of boolean context. | 45 /// Conditionals are tricky to rewrite when they occur out of boolean context. |
46 /// Here we must apply more conservative rules, such as: | 46 /// Here we must apply more conservative rules, such as: |
47 /// | 47 /// |
48 /// x ? true : false ==> !!x | 48 /// x ? true : false ==> !!x |
49 /// | 49 /// |
50 /// If the possible falsy values of the condition are known, we can sometimes | 50 /// If the possible falsy values of the condition are known, we can sometimes |
51 /// introduce a logical operator: | 51 /// introduce a logical operator: |
52 /// | 52 /// |
53 /// !x ? y : false ==> !x && y | 53 /// !x ? y : false ==> !x && y |
54 /// | 54 /// |
55 class LogicalRewriter extends RecursiveTransformer | 55 class LogicalRewriter extends RecursiveTransformer implements Pass { |
56 implements Pass { | |
57 String get passName => 'Logical rewriter'; | 56 String get passName => 'Logical rewriter'; |
58 | 57 |
59 @override | 58 @override |
60 void rewrite(FunctionDefinition node) { | 59 void rewrite(FunctionDefinition node) { |
61 node.body = visitStatement(node.body); | 60 node.body = visitStatement(node.body); |
62 } | 61 } |
63 | 62 |
64 final FallthroughStack fallthrough = new FallthroughStack(); | 63 final FallthroughStack fallthrough = new FallthroughStack(); |
65 | 64 |
66 /// True if the given statement is equivalent to its fallthrough semantics. | 65 /// True if the given statement is equivalent to its fallthrough semantics. |
67 /// | 66 /// |
68 /// This means it will ultimately translate to an empty statement. | 67 /// This means it will ultimately translate to an empty statement. |
69 bool isFallthrough(Statement node) { | 68 bool isFallthrough(Statement node) { |
70 return node is Break && isFallthroughBreak(node) || | 69 return node is Break && isFallthroughBreak(node) || |
71 node is Continue && isFallthroughContinue(node) || | 70 node is Continue && isFallthroughContinue(node) || |
72 node is Return && isFallthroughReturn(node); | 71 node is Return && isFallthroughReturn(node); |
73 } | 72 } |
74 | 73 |
75 bool isFallthroughBreak(Break node) { | 74 bool isFallthroughBreak(Break node) { |
76 Statement target = fallthrough.target; | 75 Statement target = fallthrough.target; |
77 return node.target.binding.next == target || | 76 return node.target.binding.next == target || |
78 target is Break && target.target == node.target; | 77 target is Break && target.target == node.target; |
79 } | 78 } |
80 | 79 |
81 bool isFallthroughContinue(Continue node) { | 80 bool isFallthroughContinue(Continue node) { |
82 Statement target = fallthrough.target; | 81 Statement target = fallthrough.target; |
83 return node.target.binding == target || | 82 return node.target.binding == target || |
84 target is Continue && target.target == node.target; | 83 target is Continue && target.target == node.target; |
85 } | 84 } |
86 | 85 |
87 bool isFallthroughReturn(Return node) { | 86 bool isFallthroughReturn(Return node) { |
88 return isNull(node.value) && fallthrough.target == null; | 87 return isNull(node.value) && fallthrough.target == null; |
89 } | 88 } |
90 | 89 |
91 bool isTerminator(Statement node) { | 90 bool isTerminator(Statement node) { |
92 return (node is Jump || node is Return) && !isFallthrough(node) || | 91 return (node is Jump || node is Return) && !isFallthrough(node) || |
93 (node is ExpressionStatement && node.next is Unreachable) || | 92 (node is ExpressionStatement && node.next is Unreachable) || |
94 node is Throw; | 93 node is Throw; |
95 } | 94 } |
96 | 95 |
97 Statement visitIf(If node) { | 96 Statement visitIf(If node) { |
98 // If one of the branches is empty (i.e. just a fallthrough), then that | 97 // If one of the branches is empty (i.e. just a fallthrough), then that |
99 // branch should preferably be the 'else' so we won't have to print it. | 98 // branch should preferably be the 'else' so we won't have to print it. |
100 // In other words, we wish to perform this rewrite: | 99 // In other words, we wish to perform this rewrite: |
101 // if (E) {} else {S} | 100 // if (E) {} else {S} |
102 // ==> | 101 // ==> |
103 // if (!E) {S} | 102 // if (!E) {S} |
104 // In the tree language, empty statements do not exist yet, so we must check | 103 // In the tree language, empty statements do not exist yet, so we must check |
(...skipping 11 matching lines...) Expand all Loading... |
116 const int THEN = 1; | 115 const int THEN = 1; |
117 const int NEITHER = 0; | 116 const int NEITHER = 0; |
118 const int ELSE = -1; | 117 const int ELSE = -1; |
119 int bestThenBranch = NEITHER; | 118 int bestThenBranch = NEITHER; |
120 if (isFallthrough(node.thenStatement) && | 119 if (isFallthrough(node.thenStatement) && |
121 !isFallthrough(node.elseStatement)) { | 120 !isFallthrough(node.elseStatement)) { |
122 // Put the empty statement in the 'else' branch. | 121 // Put the empty statement in the 'else' branch. |
123 // if (E) {} else {S} ==> if (!E) {S} | 122 // if (E) {} else {S} ==> if (!E) {S} |
124 bestThenBranch = ELSE; | 123 bestThenBranch = ELSE; |
125 } else if (isFallthrough(node.elseStatement) && | 124 } else if (isFallthrough(node.elseStatement) && |
126 !isFallthrough(node.thenStatement)) { | 125 !isFallthrough(node.thenStatement)) { |
127 // Keep the empty statement in the 'else' branch. | 126 // Keep the empty statement in the 'else' branch. |
128 // if (E) {S} else {} | 127 // if (E) {S} else {} |
129 bestThenBranch = THEN; | 128 bestThenBranch = THEN; |
130 } else if (thenHasFallthrough && !elseHasFallthrough) { | 129 } else if (thenHasFallthrough && !elseHasFallthrough) { |
131 // Put abrupt termination in the 'then' branch to omit 'else'. | 130 // Put abrupt termination in the 'then' branch to omit 'else'. |
132 // if (E) {S1} else {S2; return v} ==> if (!E) {S2; return v}; S1 | 131 // if (E) {S1} else {S2; return v} ==> if (!E) {S2; return v}; S1 |
133 bestThenBranch = ELSE; | 132 bestThenBranch = ELSE; |
134 } else if (!thenHasFallthrough && elseHasFallthrough) { | 133 } else if (!thenHasFallthrough && elseHasFallthrough) { |
135 // Keep abrupt termination in the 'then' branch to omit 'else'. | 134 // Keep abrupt termination in the 'then' branch to omit 'else'. |
136 // if (E) {S1; return v}; S2 | 135 // if (E) {S1; return v}; S2 |
137 bestThenBranch = THEN; | 136 bestThenBranch = THEN; |
138 } else if (isTerminator(node.elseStatement) && | 137 } else if (isTerminator(node.elseStatement) && |
139 !isTerminator(node.thenStatement)) { | 138 !isTerminator(node.thenStatement)) { |
140 // Put early termination in the 'then' branch to reduce nesting depth. | 139 // Put early termination in the 'then' branch to reduce nesting depth. |
141 // if (E) {S}; return v ==> if (!E) return v; S | 140 // if (E) {S}; return v ==> if (!E) return v; S |
142 bestThenBranch = ELSE; | 141 bestThenBranch = ELSE; |
143 } else if (isTerminator(node.thenStatement) && | 142 } else if (isTerminator(node.thenStatement) && |
144 !isTerminator(node.elseStatement)) { | 143 !isTerminator(node.elseStatement)) { |
145 // Keep early termination in the 'then' branch to reduce nesting depth. | 144 // Keep early termination in the 'then' branch to reduce nesting depth. |
146 // if (E) {return v;} S | 145 // if (E) {return v;} S |
147 bestThenBranch = THEN; | 146 bestThenBranch = THEN; |
148 } | 147 } |
149 | 148 |
150 // Swap branches if 'else' is better as 'then' | 149 // Swap branches if 'else' is better as 'then' |
151 if (bestThenBranch == ELSE) { | 150 if (bestThenBranch == ELSE) { |
152 node.condition = new Not(node.condition); | 151 node.condition = new Not(node.condition); |
153 Statement tmp = node.thenStatement; | 152 Statement tmp = node.thenStatement; |
154 node.thenStatement = node.elseStatement; | 153 node.thenStatement = node.elseStatement; |
155 node.elseStatement = tmp; | 154 node.elseStatement = tmp; |
156 } | 155 } |
157 | 156 |
158 // If neither branch is better, eliminate a negation in the condition | 157 // If neither branch is better, eliminate a negation in the condition |
159 // if (!E) S1 else S2 | 158 // if (!E) S1 else S2 |
160 // ==> | 159 // ==> |
161 // if (E) S2 else S1 | 160 // if (E) S2 else S1 |
162 node.condition = makeCondition(node.condition, true, | 161 node.condition = makeCondition(node.condition, true, |
163 liftNots: bestThenBranch == NEITHER); | 162 liftNots: bestThenBranch == NEITHER); |
164 if (bestThenBranch == NEITHER && node.condition is Not) { | 163 if (bestThenBranch == NEITHER && node.condition is Not) { |
165 node.condition = (node.condition as Not).operand; | 164 node.condition = (node.condition as Not).operand; |
166 Statement tmp = node.thenStatement; | 165 Statement tmp = node.thenStatement; |
167 node.thenStatement = node.elseStatement; | 166 node.thenStatement = node.elseStatement; |
168 node.elseStatement = tmp; | 167 node.elseStatement = tmp; |
169 } | 168 } |
170 | 169 |
171 return node; | 170 return node; |
172 } | 171 } |
173 | 172 |
(...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
262 // x ? false : true --> !x | 261 // x ? false : true --> !x |
263 if (isFalse(node.thenExpression) && isTrue(node.elseExpression)) { | 262 if (isFalse(node.thenExpression) && isTrue(node.elseExpression)) { |
264 return toBoolean(makeCondition(node.condition, false, liftNots: false)); | 263 return toBoolean(makeCondition(node.condition, false, liftNots: false)); |
265 } | 264 } |
266 | 265 |
267 // x ? y : false ==> x && y (if x is truthy or false) | 266 // x ? y : false ==> x && y (if x is truthy or false) |
268 // x ? y : null ==> x && y (if x is truthy or null) | 267 // x ? y : null ==> x && y (if x is truthy or null) |
269 // x ? y : 0 ==> x && y (if x is truthy or zero) (and so on...) | 268 // x ? y : 0 ==> x && y (if x is truthy or zero) (and so on...) |
270 if (matchesFalsyValue(node.condition, getConstant(node.elseExpression))) { | 269 if (matchesFalsyValue(node.condition, getConstant(node.elseExpression))) { |
271 return new LogicalOperator.and( | 270 return new LogicalOperator.and( |
272 visitExpression(node.condition), | 271 visitExpression(node.condition), node.thenExpression); |
273 node.thenExpression); | |
274 } | 272 } |
275 // x ? true : y ==> x || y (if x is falsy or true) | 273 // x ? true : y ==> x || y (if x is falsy or true) |
276 // x ? 1 : y ==> x || y (if x is falsy or one) (and so on...) | 274 // x ? 1 : y ==> x || y (if x is falsy or one) (and so on...) |
277 if (matchesTruthyValue(node.condition, getConstant(node.thenExpression))) { | 275 if (matchesTruthyValue(node.condition, getConstant(node.thenExpression))) { |
278 return new LogicalOperator.or( | 276 return new LogicalOperator.or( |
279 visitExpression(node.condition), | 277 visitExpression(node.condition), node.elseExpression); |
280 node.elseExpression); | |
281 } | 278 } |
282 // x ? y : true ==> !x || y | 279 // x ? y : true ==> !x || y |
283 if (isTrue(node.elseExpression)) { | 280 if (isTrue(node.elseExpression)) { |
284 return new LogicalOperator.or( | 281 return new LogicalOperator.or( |
285 toBoolean(makeCondition(node.condition, false, liftNots: false)), | 282 toBoolean(makeCondition(node.condition, false, liftNots: false)), |
286 node.thenExpression); | 283 node.thenExpression); |
287 } | 284 } |
288 // x ? false : y ==> !x && y | 285 // x ? false : y ==> !x && y |
289 if (isFalse(node.thenExpression)) { | 286 if (isFalse(node.thenExpression)) { |
290 return new LogicalOperator.and( | 287 return new LogicalOperator.and( |
(...skipping 30 matching lines...) Expand all Loading... |
321 node.right = visitExpression(node.right); | 318 node.right = visitExpression(node.right); |
322 return node; | 319 return node; |
323 } | 320 } |
324 | 321 |
325 /// True if the given expression is known to evaluate to a boolean. | 322 /// True if the given expression is known to evaluate to a boolean. |
326 /// This will not recursively traverse [Conditional] expressions, but if | 323 /// This will not recursively traverse [Conditional] expressions, but if |
327 /// applied to the result of [visitExpression] conditionals will have been | 324 /// applied to the result of [visitExpression] conditionals will have been |
328 /// rewritten anyway. | 325 /// rewritten anyway. |
329 bool isBooleanValued(Expression e) { | 326 bool isBooleanValued(Expression e) { |
330 return isTrue(e) || | 327 return isTrue(e) || |
331 isFalse(e) || | 328 isFalse(e) || |
332 e is Not || | 329 e is Not || |
333 e is LogicalOperator && isBooleanValuedLogicalOperator(e) || | 330 e is LogicalOperator && isBooleanValuedLogicalOperator(e) || |
334 e is ApplyBuiltinOperator && operatorReturnsBool(e.operator) || | 331 e is ApplyBuiltinOperator && operatorReturnsBool(e.operator) || |
335 e is TypeOperator && isBooleanValuedTypeOperator(e); | 332 e is TypeOperator && isBooleanValuedTypeOperator(e); |
336 } | 333 } |
337 | 334 |
338 bool isBooleanValuedLogicalOperator(LogicalOperator e) { | 335 bool isBooleanValuedLogicalOperator(LogicalOperator e) { |
339 return isBooleanValued(e.left) && isBooleanValued(e.right); | 336 return isBooleanValued(e.left) && isBooleanValued(e.right); |
340 } | 337 } |
341 | 338 |
342 /// True if the given operator always returns `true` or `false`. | 339 /// True if the given operator always returns `true` or `false`. |
343 bool operatorReturnsBool(BuiltinOperator operator) { | 340 bool operatorReturnsBool(BuiltinOperator operator) { |
344 switch (operator) { | 341 switch (operator) { |
345 case BuiltinOperator.StrictEq: | 342 case BuiltinOperator.StrictEq: |
(...skipping 15 matching lines...) Expand all Loading... |
361 return false; | 358 return false; |
362 } | 359 } |
363 } | 360 } |
364 | 361 |
365 bool isBooleanValuedTypeOperator(TypeOperator e) { | 362 bool isBooleanValuedTypeOperator(TypeOperator e) { |
366 return e.isTypeTest; | 363 return e.isTypeTest; |
367 } | 364 } |
368 | 365 |
369 BuiltinOperator negateBuiltin(BuiltinOperator operator) { | 366 BuiltinOperator negateBuiltin(BuiltinOperator operator) { |
370 switch (operator) { | 367 switch (operator) { |
371 case BuiltinOperator.StrictEq: return BuiltinOperator.StrictNeq; | 368 case BuiltinOperator.StrictEq: |
372 case BuiltinOperator.StrictNeq: return BuiltinOperator.StrictEq; | 369 return BuiltinOperator.StrictNeq; |
373 case BuiltinOperator.LooseEq: return BuiltinOperator.LooseNeq; | 370 case BuiltinOperator.StrictNeq: |
374 case BuiltinOperator.LooseNeq: return BuiltinOperator.LooseEq; | 371 return BuiltinOperator.StrictEq; |
375 case BuiltinOperator.IsNumber: return BuiltinOperator.IsNotNumber; | 372 case BuiltinOperator.LooseEq: |
376 case BuiltinOperator.IsNotNumber: return BuiltinOperator.IsNumber; | 373 return BuiltinOperator.LooseNeq; |
377 case BuiltinOperator.IsInteger: return BuiltinOperator.IsNotInteger; | 374 case BuiltinOperator.LooseNeq: |
378 case BuiltinOperator.IsNotInteger: return BuiltinOperator.IsInteger; | 375 return BuiltinOperator.LooseEq; |
| 376 case BuiltinOperator.IsNumber: |
| 377 return BuiltinOperator.IsNotNumber; |
| 378 case BuiltinOperator.IsNotNumber: |
| 379 return BuiltinOperator.IsNumber; |
| 380 case BuiltinOperator.IsInteger: |
| 381 return BuiltinOperator.IsNotInteger; |
| 382 case BuiltinOperator.IsNotInteger: |
| 383 return BuiltinOperator.IsInteger; |
379 case BuiltinOperator.IsUnsigned32BitInteger: | 384 case BuiltinOperator.IsUnsigned32BitInteger: |
380 return BuiltinOperator.IsNotUnsigned32BitInteger; | 385 return BuiltinOperator.IsNotUnsigned32BitInteger; |
381 case BuiltinOperator.IsNotUnsigned32BitInteger: | 386 case BuiltinOperator.IsNotUnsigned32BitInteger: |
382 return BuiltinOperator.IsUnsigned32BitInteger; | 387 return BuiltinOperator.IsUnsigned32BitInteger; |
383 | 388 |
384 // Because of NaN, these do not have a negated form. | 389 // Because of NaN, these do not have a negated form. |
385 case BuiltinOperator.NumLt: | 390 case BuiltinOperator.NumLt: |
386 case BuiltinOperator.NumLe: | 391 case BuiltinOperator.NumLe: |
387 case BuiltinOperator.NumGt: | 392 case BuiltinOperator.NumGt: |
388 case BuiltinOperator.NumGe: | 393 case BuiltinOperator.NumGe: |
(...skipping 10 matching lines...) Expand all Loading... |
399 return e; | 404 return e; |
400 else | 405 else |
401 return new Not(new Not(e)); | 406 return new Not(new Not(e)); |
402 } | 407 } |
403 | 408 |
404 /// Creates an equivalent boolean expression. The expression must occur in a | 409 /// Creates an equivalent boolean expression. The expression must occur in a |
405 /// context where its result is immediately subject to boolean conversion. | 410 /// context where its result is immediately subject to boolean conversion. |
406 /// If [polarity] if false, the negated condition will be created instead. | 411 /// If [polarity] if false, the negated condition will be created instead. |
407 /// If [liftNots] is true (default) then Not expressions will be lifted toward | 412 /// If [liftNots] is true (default) then Not expressions will be lifted toward |
408 /// the root of the condition so they can be eliminated by the caller. | 413 /// the root of the condition so they can be eliminated by the caller. |
409 Expression makeCondition(Expression e, bool polarity, {bool liftNots:true}) { | 414 Expression makeCondition(Expression e, bool polarity, {bool liftNots: true}) { |
410 if (e is Not) { | 415 if (e is Not) { |
411 // !!E ==> E | 416 // !!E ==> E |
412 return makeCondition(e.operand, !polarity, liftNots: liftNots); | 417 return makeCondition(e.operand, !polarity, liftNots: liftNots); |
413 } | 418 } |
414 if (e is LogicalOperator) { | 419 if (e is LogicalOperator) { |
415 // If polarity=false, then apply the rewrite !(x && y) ==> !x || !y | 420 // If polarity=false, then apply the rewrite !(x && y) ==> !x || !y |
416 e.left = makeCondition(e.left, polarity); | 421 e.left = makeCondition(e.left, polarity); |
417 e.right = makeCondition(e.right, polarity); | 422 e.right = makeCondition(e.right, polarity); |
418 if (!polarity) { | 423 if (!polarity) { |
419 e.isAnd = !e.isAnd; | 424 e.isAnd = !e.isAnd; |
(...skipping 26 matching lines...) Expand all Loading... |
446 // x ? true : false ==> x | 451 // x ? true : false ==> x |
447 if (isTrue(e.thenExpression) && isFalse(e.elseExpression)) { | 452 if (isTrue(e.thenExpression) && isFalse(e.elseExpression)) { |
448 return makeCondition(e.condition, true, liftNots: liftNots); | 453 return makeCondition(e.condition, true, liftNots: liftNots); |
449 } | 454 } |
450 // x ? false : true ==> !x | 455 // x ? false : true ==> !x |
451 if (isFalse(e.thenExpression) && isTrue(e.elseExpression)) { | 456 if (isFalse(e.thenExpression) && isTrue(e.elseExpression)) { |
452 return makeCondition(e.condition, false, liftNots: liftNots); | 457 return makeCondition(e.condition, false, liftNots: liftNots); |
453 } | 458 } |
454 // x ? true : y ==> x || y | 459 // x ? true : y ==> x || y |
455 if (isTrue(e.thenExpression)) { | 460 if (isTrue(e.thenExpression)) { |
456 return makeOr(makeCondition(e.condition, true), | 461 return makeOr(makeCondition(e.condition, true), e.elseExpression, |
457 e.elseExpression, | 462 liftNots: liftNots); |
458 liftNots: liftNots); | |
459 } | 463 } |
460 // x ? false : y ==> !x && y | 464 // x ? false : y ==> !x && y |
461 if (isFalse(e.thenExpression)) { | 465 if (isFalse(e.thenExpression)) { |
462 return makeAnd(makeCondition(e.condition, false), | 466 return makeAnd(makeCondition(e.condition, false), e.elseExpression, |
463 e.elseExpression, | 467 liftNots: liftNots); |
464 liftNots: liftNots); | |
465 } | 468 } |
466 // x ? y : true ==> !x || y | 469 // x ? y : true ==> !x || y |
467 if (isTrue(e.elseExpression)) { | 470 if (isTrue(e.elseExpression)) { |
468 return makeOr(makeCondition(e.condition, false), | 471 return makeOr(makeCondition(e.condition, false), e.thenExpression, |
469 e.thenExpression, | 472 liftNots: liftNots); |
470 liftNots: liftNots); | |
471 } | 473 } |
472 // x ? y : false ==> x && y | 474 // x ? y : false ==> x && y |
473 if (isFalse(e.elseExpression)) { | 475 if (isFalse(e.elseExpression)) { |
474 return makeAnd(makeCondition(e.condition, true), | 476 return makeAnd(makeCondition(e.condition, true), e.thenExpression, |
475 e.thenExpression, | 477 liftNots: liftNots); |
476 liftNots: liftNots); | |
477 } | 478 } |
478 | 479 |
479 e.condition = makeCondition(e.condition, true); | 480 e.condition = makeCondition(e.condition, true); |
480 | 481 |
481 // !x ? y : z ==> x ? z : y | 482 // !x ? y : z ==> x ? z : y |
482 if (e.condition is Not) { | 483 if (e.condition is Not) { |
483 e.condition = (e.condition as Not).operand; | 484 e.condition = (e.condition as Not).operand; |
484 Expression tmp = e.thenExpression; | 485 Expression tmp = e.thenExpression; |
485 e.thenExpression = e.elseExpression; | 486 e.thenExpression = e.elseExpression; |
486 e.elseExpression = tmp; | 487 e.elseExpression = tmp; |
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
556 } else if (e1 is Assign) { | 557 } else if (e1 is Assign) { |
557 return e2 is VariableUse && e1.variable == e2.variable; | 558 return e2 is VariableUse && e1.variable == e2.variable; |
558 } | 559 } |
559 return false; | 560 return false; |
560 } | 561 } |
561 | 562 |
562 void destroyVariableUse(VariableUse node) { | 563 void destroyVariableUse(VariableUse node) { |
563 --node.variable.readCount; | 564 --node.variable.readCount; |
564 } | 565 } |
565 } | 566 } |
OLD | NEW |