OLD | NEW |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, 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 part of resolution; | 5 part of resolution; |
6 | 6 |
7 abstract class TreeElements { | 7 abstract class TreeElements { |
8 Element get currentElement; | 8 Element get currentElement; |
9 Set<Node> get superUses; | 9 Set<Node> get superUses; |
10 | 10 |
(...skipping 21 matching lines...) Expand all Loading... |
32 * Returns [:true:] if [node] is a type literal. | 32 * Returns [:true:] if [node] is a type literal. |
33 * | 33 * |
34 * Resolution marks this by setting the type on the node to be the | 34 * Resolution marks this by setting the type on the node to be the |
35 * [:Type:] type. | 35 * [:Type:] type. |
36 */ | 36 */ |
37 bool isTypeLiteral(Send node); | 37 bool isTypeLiteral(Send node); |
38 | 38 |
39 /// Register additional dependencies required by [currentElement]. | 39 /// Register additional dependencies required by [currentElement]. |
40 /// For example, elements that are used by a backend. | 40 /// For example, elements that are used by a backend. |
41 void registerDependency(Element element); | 41 void registerDependency(Element element); |
| 42 |
| 43 /// Returns [:true:] if [element] is potentially mutated anywhere in its |
| 44 /// scope. |
| 45 bool isPotentiallyMutated(VariableElement element); |
| 46 |
| 47 /// Returns [:true:] if [element] is potentially mutated in [node]. |
| 48 bool isPotentiallyMutatedIn(Node node, VariableElement element); |
| 49 |
| 50 /// Returns [:true:] if [element] is potentially mutated in a closure. |
| 51 bool isPotentiallyMutatedInClosure(VariableElement element); |
| 52 |
| 53 /// Returns [:true:] if [element] is accessed by a closure in [node]. |
| 54 bool isAccessedByClosureIn(Node node, VariableElement element); |
42 } | 55 } |
43 | 56 |
44 class TreeElementMapping implements TreeElements { | 57 class TreeElementMapping implements TreeElements { |
45 final Element currentElement; | 58 final Element currentElement; |
46 final Map<Spannable, Selector> selectors = new Map<Spannable, Selector>(); | 59 final Map<Spannable, Selector> selectors = new Map<Spannable, Selector>(); |
47 final Map<Node, DartType> types = new Map<Node, DartType>(); | 60 final Map<Node, DartType> types = new Map<Node, DartType>(); |
48 final Set<Node> superUses = new Set<Node>(); | 61 final Set<Node> superUses = new Set<Node>(); |
49 final Set<Element> otherDependencies = new Set<Element>(); | 62 final Set<Element> otherDependencies = new Set<Element>(); |
50 final Map<Node, Constant> constants = new Map<Node, Constant>(); | 63 final Map<Node, Constant> constants = new Map<Node, Constant>(); |
| 64 final Set<VariableElement> potentiallyMutated = new Set<VariableElement>(); |
| 65 final Map<Node, Set<VariableElement>> potentiallyMutatedIn = |
| 66 new Map<Node, Set<VariableElement>>(); |
| 67 final Set<VariableElement> potentiallyMutatedInClosure = |
| 68 new Set<VariableElement>(); |
| 69 final Map<Node, Set<VariableElement>> accessedByClosureIn = |
| 70 new Map<Node, Set<VariableElement>>(); |
51 final int hashCode = ++hashCodeCounter; | 71 final int hashCode = ++hashCodeCounter; |
52 static int hashCodeCounter = 0; | 72 static int hashCodeCounter = 0; |
53 | 73 |
54 TreeElementMapping(this.currentElement); | 74 TreeElementMapping(this.currentElement); |
55 | 75 |
56 operator []=(Node node, Element element) { | 76 operator []=(Node node, Element element) { |
57 assert(invariant(node, () { | 77 assert(invariant(node, () { |
58 FunctionExpression functionExpression = node.asFunctionExpression(); | 78 FunctionExpression functionExpression = node.asFunctionExpression(); |
59 if (functionExpression != null) { | 79 if (functionExpression != null) { |
60 return !functionExpression.modifiers.isExternal(); | 80 return !functionExpression.modifiers.isExternal(); |
(...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
151 } | 171 } |
152 | 172 |
153 bool isTypeLiteral(Send node) { | 173 bool isTypeLiteral(Send node) { |
154 return getType(node) != null; | 174 return getType(node) != null; |
155 } | 175 } |
156 | 176 |
157 void registerDependency(Element element) { | 177 void registerDependency(Element element) { |
158 otherDependencies.add(element.implementation); | 178 otherDependencies.add(element.implementation); |
159 } | 179 } |
160 | 180 |
| 181 bool isPotentiallyMutated(VariableElement element) { |
| 182 return potentiallyMutated.contains(element); |
| 183 } |
| 184 |
| 185 void setPotentiallyMutated(VariableElement element) { |
| 186 potentiallyMutated.add(element); |
| 187 } |
| 188 |
| 189 bool isPotentiallyMutatedIn(Node node, VariableElement element) { |
| 190 Set<Element> mutatedIn = potentiallyMutatedIn[node]; |
| 191 return mutatedIn != null && mutatedIn.contains(element); |
| 192 } |
| 193 |
| 194 void setPotentiallyMutatedIn(Node node, VariableElement element) { |
| 195 potentiallyMutatedIn.putIfAbsent( |
| 196 node, () => new Set<VariableElement>()).add(element); |
| 197 } |
| 198 |
| 199 bool isPotentiallyMutatedInClosure(VariableElement element) { |
| 200 return potentiallyMutatedInClosure.contains(element); |
| 201 } |
| 202 |
| 203 void setPotentiallyMutatedInClosure(VariableElement element) { |
| 204 potentiallyMutatedInClosure.add(element); |
| 205 } |
| 206 |
| 207 bool isAccessedByClosureIn(Node node, VariableElement element) { |
| 208 Set<Element> accessedIn = accessedByClosureIn[node]; |
| 209 return accessedIn != null && accessedIn.contains(element); |
| 210 } |
| 211 |
| 212 void setAccessedByClosureIn(Node node, VariableElement element) { |
| 213 accessedByClosureIn.putIfAbsent( |
| 214 node, () => new Set<VariableElement>()).add(element); |
| 215 } |
| 216 |
161 String toString() => 'TreeElementMapping($currentElement)'; | 217 String toString() => 'TreeElementMapping($currentElement)'; |
162 } | 218 } |
163 | 219 |
164 class ResolverTask extends CompilerTask { | 220 class ResolverTask extends CompilerTask { |
165 ResolverTask(Compiler compiler) : super(compiler); | 221 ResolverTask(Compiler compiler) : super(compiler); |
166 | 222 |
167 String get name => 'Resolver'; | 223 String get name => 'Resolver'; |
168 | 224 |
169 TreeElements resolve(Element element) { | 225 TreeElements resolve(Element element) { |
170 return measure(() { | 226 return measure(() { |
(...skipping 1667 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1838 bool sendIsMemberAccess = false; | 1894 bool sendIsMemberAccess = false; |
1839 StatementScope statementScope; | 1895 StatementScope statementScope; |
1840 int allowedCategory = ElementCategory.VARIABLE | ElementCategory.FUNCTION | 1896 int allowedCategory = ElementCategory.VARIABLE | ElementCategory.FUNCTION |
1841 | ElementCategory.IMPLIES_TYPE; | 1897 | ElementCategory.IMPLIES_TYPE; |
1842 | 1898 |
1843 /// When visiting the type declaration of the variable in a [ForIn] loop, | 1899 /// When visiting the type declaration of the variable in a [ForIn] loop, |
1844 /// the initializer of the variable is implicit and we should not emit an | 1900 /// the initializer of the variable is implicit and we should not emit an |
1845 /// error when verifying that all final variables are initialized. | 1901 /// error when verifying that all final variables are initialized. |
1846 bool allowFinalWithoutInitializer = false; | 1902 bool allowFinalWithoutInitializer = false; |
1847 | 1903 |
| 1904 /// The nodes for which variable access and mutation must be registered in |
| 1905 /// order to determine when the static type of variables types is promoted. |
| 1906 Link<Node> promotionScope = const Link<Node>(); |
| 1907 |
| 1908 bool isPotentiallyMutableTarget(Element target) { |
| 1909 if (target == null) return false; |
| 1910 return (target.isVariable() || target.isParameter()) && |
| 1911 !(target.modifiers.isFinal() || target.modifiers.isConst()); |
| 1912 } |
| 1913 |
1848 // TODO(ahe): Find a way to share this with runtime implementation. | 1914 // TODO(ahe): Find a way to share this with runtime implementation. |
1849 static final RegExp symbolValidationPattern = | 1915 static final RegExp symbolValidationPattern = |
1850 new RegExp(r'^(?:[a-zA-Z$][a-zA-Z$0-9_]*\.)*(?:[a-zA-Z$][a-zA-Z$0-9_]*=?|' | 1916 new RegExp(r'^(?:[a-zA-Z$][a-zA-Z$0-9_]*\.)*(?:[a-zA-Z$][a-zA-Z$0-9_]*=?|' |
1851 r'-|' | 1917 r'-|' |
1852 r'unary-|' | 1918 r'unary-|' |
1853 r'\[\]=|' | 1919 r'\[\]=|' |
1854 r'~|' | 1920 r'~|' |
1855 r'==|' | 1921 r'==|' |
1856 r'\[\]|' | 1922 r'\[\]|' |
1857 r'\*|' | 1923 r'\*|' |
(...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1936 } | 2002 } |
1937 | 2003 |
1938 inStaticContext(action()) { | 2004 inStaticContext(action()) { |
1939 bool wasInstanceContext = inInstanceContext; | 2005 bool wasInstanceContext = inInstanceContext; |
1940 inInstanceContext = false; | 2006 inInstanceContext = false; |
1941 var result = action(); | 2007 var result = action(); |
1942 inInstanceContext = wasInstanceContext; | 2008 inInstanceContext = wasInstanceContext; |
1943 return result; | 2009 return result; |
1944 } | 2010 } |
1945 | 2011 |
| 2012 doInPromotionScope(Node node, action()) { |
| 2013 promotionScope = promotionScope.prepend(node); |
| 2014 var result = action(); |
| 2015 promotionScope = promotionScope.tail; |
| 2016 return result; |
| 2017 } |
| 2018 |
1946 visitInStaticContext(Node node) { | 2019 visitInStaticContext(Node node) { |
1947 inStaticContext(() => visit(node)); | 2020 inStaticContext(() => visit(node)); |
1948 } | 2021 } |
1949 | 2022 |
1950 ErroneousElement warnAndCreateErroneousElement(Node node, | 2023 ErroneousElement warnAndCreateErroneousElement(Node node, |
1951 SourceString name, | 2024 SourceString name, |
1952 DualKind kind, | 2025 DualKind kind, |
1953 [Map arguments = const {}]) { | 2026 [Map arguments = const {}]) { |
1954 ResolutionWarning warning = new ResolutionWarning( | 2027 ResolutionWarning warning = new ResolutionWarning( |
1955 kind.warning, arguments, compiler.terseDiagnostics); | 2028 kind.warning, arguments, compiler.terseDiagnostics); |
(...skipping 222 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2178 statementScope = oldStatementScope; | 2251 statementScope = oldStatementScope; |
2179 | 2252 |
2180 scope = oldScope; | 2253 scope = oldScope; |
2181 enclosingElement = previousEnclosingElement; | 2254 enclosingElement = previousEnclosingElement; |
2182 | 2255 |
2183 world.registerClosure(function, mapping); | 2256 world.registerClosure(function, mapping); |
2184 world.registerInstantiatedClass(compiler.functionClass, mapping); | 2257 world.registerInstantiatedClass(compiler.functionClass, mapping); |
2185 } | 2258 } |
2186 | 2259 |
2187 visitIf(If node) { | 2260 visitIf(If node) { |
2188 visit(node.condition); | 2261 doInPromotionScope(node.condition.expression, () => visit(node.condition)); |
2189 visitIn(node.thenPart, new BlockScope(scope)); | 2262 doInPromotionScope(node.thenPart, |
| 2263 () => visitIn(node.thenPart, new BlockScope(scope))); |
2190 visitIn(node.elsePart, new BlockScope(scope)); | 2264 visitIn(node.elsePart, new BlockScope(scope)); |
2191 } | 2265 } |
2192 | 2266 |
2193 static bool isLogicalOperator(Identifier op) { | 2267 static bool isLogicalOperator(Identifier op) { |
2194 String str = op.source.stringValue; | 2268 String str = op.source.stringValue; |
2195 return (identical(str, '&&') || str == '||' || str == '!'); | 2269 return (identical(str, '&&') || str == '||' || str == '!'); |
2196 } | 2270 } |
2197 | 2271 |
2198 Element resolveSend(Send node) { | 2272 Element resolveSend(Send node) { |
2199 Selector selector = resolveSelector(node, null); | 2273 Selector selector = resolveSelector(node, null); |
(...skipping 205 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2405 } else if (!seenNamedArguments.isEmpty) { | 2479 } else if (!seenNamedArguments.isEmpty) { |
2406 error(argument, MessageKind.INVALID_ARGUMENT_AFTER_NAMED); | 2480 error(argument, MessageKind.INVALID_ARGUMENT_AFTER_NAMED); |
2407 } | 2481 } |
2408 } | 2482 } |
2409 sendIsMemberAccess = oldSendIsMemberAccess; | 2483 sendIsMemberAccess = oldSendIsMemberAccess; |
2410 } | 2484 } |
2411 | 2485 |
2412 visitSend(Send node) { | 2486 visitSend(Send node) { |
2413 bool oldSendIsMemberAccess = sendIsMemberAccess; | 2487 bool oldSendIsMemberAccess = sendIsMemberAccess; |
2414 sendIsMemberAccess = node.isPropertyAccess || node.isCall; | 2488 sendIsMemberAccess = node.isPropertyAccess || node.isCall; |
2415 Element target = resolveSend(node); | 2489 Element target; |
| 2490 if (node.isLogicalAnd) { |
| 2491 target = doInPromotionScope(node.receiver, () => resolveSend(node)); |
| 2492 } else { |
| 2493 target = resolveSend(node); |
| 2494 } |
2416 sendIsMemberAccess = oldSendIsMemberAccess; | 2495 sendIsMemberAccess = oldSendIsMemberAccess; |
2417 | 2496 |
2418 if (target != null | 2497 if (target != null |
2419 && target == compiler.mirrorSystemGetNameFunction | 2498 && target == compiler.mirrorSystemGetNameFunction |
2420 && !compiler.mirrorUsageAnalyzerTask.hasMirrorUsage(enclosingElement)) { | 2499 && !compiler.mirrorUsageAnalyzerTask.hasMirrorUsage(enclosingElement)) { |
2421 compiler.reportHint( | 2500 compiler.reportHint( |
2422 node.selector, MessageKind.STATIC_FUNCTION_BLOAT, | 2501 node.selector, MessageKind.STATIC_FUNCTION_BLOAT, |
2423 {'class': compiler.mirrorSystemClass.name, | 2502 {'class': compiler.mirrorSystemClass.name, |
2424 'name': compiler.mirrorSystemGetNameFunction.name}); | 2503 'name': compiler.mirrorSystemGetNameFunction.name}); |
2425 } | 2504 } |
(...skipping 19 matching lines...) Expand all Loading... |
2445 world.registerTypeLiteral(target, mapping); | 2524 world.registerTypeLiteral(target, mapping); |
2446 } else if (target.impliesType() && !sendIsMemberAccess) { | 2525 } else if (target.impliesType() && !sendIsMemberAccess) { |
2447 // Set the type of the node to [Type] to mark this send as a | 2526 // Set the type of the node to [Type] to mark this send as a |
2448 // type literal. | 2527 // type literal. |
2449 mapping.setType(node, compiler.typeClass.computeType(compiler)); | 2528 mapping.setType(node, compiler.typeClass.computeType(compiler)); |
2450 world.registerTypeLiteral(target, mapping); | 2529 world.registerTypeLiteral(target, mapping); |
2451 | 2530 |
2452 // Don't try to make constants of calls to type literals. | 2531 // Don't try to make constants of calls to type literals. |
2453 analyzeConstant(node, isConst: !node.isCall); | 2532 analyzeConstant(node, isConst: !node.isCall); |
2454 } | 2533 } |
| 2534 if (isPotentiallyMutableTarget(target)) { |
| 2535 if (enclosingElement != target.enclosingElement) { |
| 2536 for (Node scope in promotionScope) { |
| 2537 mapping.setAccessedByClosureIn(scope, target); |
| 2538 } |
| 2539 } |
| 2540 } |
2455 } | 2541 } |
2456 | 2542 |
2457 bool resolvedArguments = false; | 2543 bool resolvedArguments = false; |
2458 if (node.isOperator) { | 2544 if (node.isOperator) { |
2459 String operatorString = node.selector.asOperator().source.stringValue; | 2545 String operatorString = node.selector.asOperator().source.stringValue; |
2460 if (identical(operatorString, 'is')) { | 2546 if (identical(operatorString, 'is')) { |
| 2547 // TODO(johnniwinther): Use seen type tests to avoid registration of |
| 2548 // mutation/access to unpromoted variables. |
2461 DartType type = | 2549 DartType type = |
2462 resolveTypeExpression(node.typeAnnotationFromIsCheckOrCast); | 2550 resolveTypeExpression(node.typeAnnotationFromIsCheckOrCast); |
2463 if (type != null) { | 2551 if (type != null) { |
2464 compiler.enqueuer.resolution.registerIsCheck(type, mapping); | 2552 compiler.enqueuer.resolution.registerIsCheck(type, mapping); |
2465 } | 2553 } |
2466 resolvedArguments = true; | 2554 resolvedArguments = true; |
2467 } else if (identical(operatorString, 'as')) { | 2555 } else if (identical(operatorString, 'as')) { |
2468 DartType type = resolveTypeExpression(node.arguments.head); | 2556 DartType type = resolveTypeExpression(node.arguments.head); |
2469 if (type != null) { | 2557 if (type != null) { |
2470 compiler.enqueuer.resolution.registerAsCheck(type, mapping); | 2558 compiler.enqueuer.resolution.registerAsCheck(type, mapping); |
2471 } | 2559 } |
2472 resolvedArguments = true; | 2560 resolvedArguments = true; |
| 2561 } else if (identical(operatorString, '&&')) { |
| 2562 doInPromotionScope(node.arguments.head, |
| 2563 () => resolveArguments(node.argumentsNode)); |
| 2564 resolvedArguments = true; |
2473 } | 2565 } |
2474 } | 2566 } |
2475 | 2567 |
2476 if (!resolvedArguments) { | 2568 if (!resolvedArguments) { |
2477 resolveArguments(node.argumentsNode); | 2569 resolveArguments(node.argumentsNode); |
2478 } | 2570 } |
2479 | 2571 |
2480 // If the selector is null, it means that we will not be generating | 2572 // If the selector is null, it means that we will not be generating |
2481 // code for this as a send. | 2573 // code for this as a send. |
2482 Selector selector = mapping.getSelector(node); | 2574 Selector selector = mapping.getSelector(node); |
(...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2565 compiler.backend.registerThrowNoSuchMethod(mapping); | 2657 compiler.backend.registerThrowNoSuchMethod(mapping); |
2566 } else if (target.modifiers.isFinal() || | 2658 } else if (target.modifiers.isFinal() || |
2567 target.modifiers.isConst() || | 2659 target.modifiers.isConst() || |
2568 (target.isFunction() && | 2660 (target.isFunction() && |
2569 Elements.isStaticOrTopLevelFunction(target) && | 2661 Elements.isStaticOrTopLevelFunction(target) && |
2570 !target.isSetter())) { | 2662 !target.isSetter())) { |
2571 setter = warnAndCreateErroneousElement( | 2663 setter = warnAndCreateErroneousElement( |
2572 node.selector, target.name, MessageKind.CANNOT_RESOLVE_SETTER); | 2664 node.selector, target.name, MessageKind.CANNOT_RESOLVE_SETTER); |
2573 compiler.backend.registerThrowNoSuchMethod(mapping); | 2665 compiler.backend.registerThrowNoSuchMethod(mapping); |
2574 } | 2666 } |
| 2667 if (isPotentiallyMutableTarget(target)) { |
| 2668 mapping.setPotentiallyMutated(target); |
| 2669 if (enclosingElement != target.enclosingElement) { |
| 2670 mapping.setPotentiallyMutatedInClosure(target); |
| 2671 } |
| 2672 for (Node scope in promotionScope) { |
| 2673 mapping.setPotentiallyMutatedIn(scope, target); |
| 2674 } |
| 2675 } |
2575 } | 2676 } |
2576 | 2677 |
2577 visit(node.argumentsNode); | 2678 visit(node.argumentsNode); |
2578 | 2679 |
2579 // TODO(ngeoffray): Check if the target can be assigned. | 2680 // TODO(ngeoffray): Check if the target can be assigned. |
2580 // TODO(ngeoffray): Warn if target is null and the send is | 2681 // TODO(ngeoffray): Warn if target is null and the send is |
2581 // unqualified. | 2682 // unqualified. |
2582 | 2683 |
2583 Selector selector = mapping.getSelector(node); | 2684 Selector selector = mapping.getSelector(node); |
2584 if (isComplex) { | 2685 if (isComplex) { |
(...skipping 410 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2995 mapping.setType(node, listType); | 3096 mapping.setType(node, listType); |
2996 world.registerInstantiatedType(listType, mapping); | 3097 world.registerInstantiatedType(listType, mapping); |
2997 compiler.backend.registerRequiredType(listType, enclosingElement); | 3098 compiler.backend.registerRequiredType(listType, enclosingElement); |
2998 visit(node.elements); | 3099 visit(node.elements); |
2999 if (node.isConst()) { | 3100 if (node.isConst()) { |
3000 analyzeConstant(node); | 3101 analyzeConstant(node); |
3001 } | 3102 } |
3002 } | 3103 } |
3003 | 3104 |
3004 visitConditional(Conditional node) { | 3105 visitConditional(Conditional node) { |
3005 node.visitChildren(this); | 3106 doInPromotionScope(node.condition, () => visit(node.condition)); |
| 3107 doInPromotionScope(node.thenExpression, () => visit(node.thenExpression)); |
| 3108 visit(node.elseExpression); |
3006 } | 3109 } |
3007 | 3110 |
3008 visitStringInterpolation(StringInterpolation node) { | 3111 visitStringInterpolation(StringInterpolation node) { |
3009 world.registerInstantiatedClass(compiler.stringClass, mapping); | 3112 world.registerInstantiatedClass(compiler.stringClass, mapping); |
3010 compiler.backend.registerStringInterpolation(mapping); | 3113 compiler.backend.registerStringInterpolation(mapping); |
3011 node.visitChildren(this); | 3114 node.visitChildren(this); |
3012 } | 3115 } |
3013 | 3116 |
3014 visitStringInterpolationPart(StringInterpolationPart node) { | 3117 visitStringInterpolationPart(StringInterpolationPart node) { |
3015 registerImplicitInvocation(const SourceString('toString'), 0); | 3118 registerImplicitInvocation(const SourceString('toString'), 0); |
(...skipping 1522 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
4538 return finishConstructorReference(visit(expression), | 4641 return finishConstructorReference(visit(expression), |
4539 expression, expression); | 4642 expression, expression); |
4540 } | 4643 } |
4541 } | 4644 } |
4542 | 4645 |
4543 /// Looks up [name] in [scope] and unwraps the result. | 4646 /// Looks up [name] in [scope] and unwraps the result. |
4544 Element lookupInScope(Compiler compiler, Node node, | 4647 Element lookupInScope(Compiler compiler, Node node, |
4545 Scope scope, SourceString name) { | 4648 Scope scope, SourceString name) { |
4546 return Elements.unwrap(scope.lookup(name), compiler, node); | 4649 return Elements.unwrap(scope.lookup(name), compiler, node); |
4547 } | 4650 } |
OLD | NEW |