OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2016, 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 kernel.transformations.reify.transformation.transformer; |
| 6 |
| 7 import '../analysis/program_analysis.dart'; |
| 8 import 'package:kernel/ast.dart'; |
| 9 import 'binding.dart' show RuntimeLibrary; |
| 10 import 'builder.dart' show RuntimeTypeSupportBuilder; |
| 11 import 'dart:collection' show LinkedHashMap; |
| 12 import '../asts.dart'; |
| 13 |
| 14 export 'binding.dart' show RuntimeLibrary; |
| 15 export 'builder.dart' show RuntimeTypeSupportBuilder; |
| 16 |
| 17 enum RuntimeTypeStorage { |
| 18 none, |
| 19 inheritedField, |
| 20 field, |
| 21 getter, |
| 22 } |
| 23 |
| 24 class TransformationContext { |
| 25 /// Describes how the runtime type is stored on the object. |
| 26 RuntimeTypeStorage runtimeTypeStorage; |
| 27 |
| 28 /// Field added to store the runtime type if [runtimeType] is |
| 29 /// [RuntimeTypeStorage.field]. |
| 30 Field runtimeTypeField; |
| 31 |
| 32 /// The parameter for the type information introduced to the constructor or |
| 33 /// to static initializers. |
| 34 VariableDeclaration parameter; |
| 35 |
| 36 /// A ordered collection of fields together with their initializers rewritten |
| 37 /// to static initializer functions that can be used in the constructor's |
| 38 /// initializer list. |
| 39 /// The order is important because of possible side-effects in the |
| 40 /// initializers. |
| 41 LinkedHashMap<Field, Procedure> initializers; |
| 42 |
| 43 // `true` if the visitor currently is in a field initializer, a initializer |
| 44 // list of a constructor, or the body of a factory method. In these cases, |
| 45 // type argument access is different than in an instance context, since `this` |
| 46 // is not available. |
| 47 bool inInitializer = false; |
| 48 |
| 49 String toString() => "s: ${runtimeTypeStorage} f: $runtimeTypeField," |
| 50 " p: $parameter, i: $inInitializer"; |
| 51 } |
| 52 |
| 53 abstract class DebugTrace { |
| 54 static const bool debugTrace = false; |
| 55 |
| 56 static const int lineLength = 80; |
| 57 |
| 58 TransformationContext get context; |
| 59 |
| 60 String getNodeLevel(TreeNode node) { |
| 61 String level = ""; |
| 62 while (node != null && node is! Library) { |
| 63 level = " $level"; |
| 64 node = node.parent; |
| 65 } |
| 66 return level; |
| 67 } |
| 68 |
| 69 String shorten(String s) { |
| 70 return s.length > lineLength ? s.substring(0, lineLength) : s; |
| 71 } |
| 72 |
| 73 void trace(TreeNode node) { |
| 74 if (debugTrace) { |
| 75 String nodeText = node.toString().replaceAll("\n", " "); |
| 76 print(shorten("trace:${getNodeLevel(node)}$context" |
| 77 " [${node.runtimeType}] $nodeText")); |
| 78 } |
| 79 } |
| 80 } |
| 81 |
| 82 /// Rewrites a tree to remove generic types and runtime type checks and replace |
| 83 /// them with Dart objects. |
| 84 /// |
| 85 /// Runtime types are stored in a field/getter called [runtimeTypeName] on the |
| 86 /// object, which for parameterized classes is initialized in the constructor. |
| 87 // TODO(karlklose): |
| 88 // - add a scoped namer |
| 89 // - rewrite types (supertypes, implemented types) |
| 90 // - rewrite as |
| 91 class ReifyVisitor extends Transformer with DebugTrace { |
| 92 final RuntimeLibrary rtiLibrary; |
| 93 final RuntimeTypeSupportBuilder builder; |
| 94 final ProgramKnowledge knowledge; |
| 95 |
| 96 ReifyVisitor(this.rtiLibrary, this.builder, this.knowledge, |
| 97 [this.libraryToTransform]); |
| 98 |
| 99 /// If not null, the transformation will only be applied to classes declared |
| 100 /// in this library. |
| 101 final Library libraryToTransform; |
| 102 |
| 103 // TODO(karlklose): find a way to get rid of this state in the visitor. |
| 104 TransformationContext context; |
| 105 |
| 106 bool libraryShouldBeTransformed(Library library) { |
| 107 return libraryToTransform == null || libraryToTransform == library; |
| 108 } |
| 109 |
| 110 bool needsTypeInformation(Class cls) { |
| 111 return !isObject(cls) && |
| 112 !rtiLibrary.contains(cls) && |
| 113 libraryShouldBeTransformed(cls.enclosingLibrary); |
| 114 } |
| 115 |
| 116 bool usesTypeGetter(Class cls) { |
| 117 return cls.typeParameters.isEmpty; |
| 118 } |
| 119 |
| 120 bool isObject(Class cls) { |
| 121 // TODO(karlklose): use [CoreTypes]. |
| 122 return "$cls" == 'dart.core::Object'; |
| 123 } |
| 124 |
| 125 Initializer addTypeAsArgument(initializer) { |
| 126 assert(initializer is SuperInitializer || |
| 127 initializer is RedirectingInitializer); |
| 128 Class cls = getEnclosingClass(initializer.target); |
| 129 if (needsTypeInformation(cls) && !usesTypeGetter(cls)) { |
| 130 // If the current class uses a getter for type information, we did not add |
| 131 // a parameter to the constructor, but we can pass `null` as the value to |
| 132 // initialize the type field, since it will be shadowed by the getter. |
| 133 Expression type = (context.parameter != null) |
| 134 ? new VariableGet(context.parameter) |
| 135 : new NullLiteral(); |
| 136 builder.insertAsFirstArgument(initializer.arguments, type); |
| 137 } |
| 138 return initializer; |
| 139 } |
| 140 |
| 141 Expression interceptInstantiation( |
| 142 InvocationExpression invocation, Member target) { |
| 143 Class targetClass = target.parent; |
| 144 Library targetLibrary = targetClass.parent; |
| 145 Library currentLibrary = getEnclosingLibrary(invocation); |
| 146 if (libraryShouldBeTransformed(currentLibrary) && |
| 147 !libraryShouldBeTransformed(targetLibrary) && |
| 148 !rtiLibrary.contains(target)) { |
| 149 return builder.attachTypeToConstructorInvocation(invocation, target); |
| 150 } |
| 151 return invocation; |
| 152 } |
| 153 |
| 154 Expression createRuntimeType(DartType type) { |
| 155 if (context?.inInitializer == true) { |
| 156 // In initializer context, the instance type is provided in |
| 157 // `context.parameter` as there is no `this`. |
| 158 return builder.createRuntimeType(type, typeContext: context.parameter); |
| 159 } else { |
| 160 return builder.createRuntimeType(type); |
| 161 } |
| 162 } |
| 163 |
| 164 TreeNode defaultTreeNode(TreeNode node) { |
| 165 trace(node); |
| 166 return super.defaultTreeNode(node); |
| 167 } |
| 168 |
| 169 Expression visitStaticInvocation(StaticInvocation invocation) { |
| 170 trace(invocation); |
| 171 |
| 172 invocation.transformChildren(this); |
| 173 |
| 174 Procedure target = invocation.target; |
| 175 if (target == rtiLibrary.reifyFunction) { |
| 176 /// Rewrite calls to reify(TypeLiteral) to a reified type. |
| 177 TypeLiteral literal = invocation.arguments.positional.single; |
| 178 return createRuntimeType(literal.type); |
| 179 } else if (target.kind == ProcedureKind.Factory) { |
| 180 // Intercept calls to factories of classes we do not transform |
| 181 return interceptInstantiation(invocation, target); |
| 182 } |
| 183 return invocation; |
| 184 } |
| 185 |
| 186 Library visitLibrary(Library library) { |
| 187 trace(library); |
| 188 |
| 189 if (libraryShouldBeTransformed(library)) { |
| 190 library.transformChildren(this); |
| 191 } |
| 192 return library; |
| 193 } |
| 194 |
| 195 Expression visitConstructorInvocation(ConstructorInvocation invocation) { |
| 196 invocation.transformChildren(this); |
| 197 return interceptInstantiation(invocation, invocation.target); |
| 198 } |
| 199 |
| 200 Member getStaticInvocationTarget(InvocationExpression invocation) { |
| 201 if (invocation is ConstructorInvocation) { |
| 202 return invocation.target; |
| 203 } else if (invocation is StaticInvocation) { |
| 204 return invocation.target; |
| 205 } else { |
| 206 throw "Unexpected InvocationExpression $invocation."; |
| 207 } |
| 208 } |
| 209 |
| 210 bool isInstantiation(TreeNode invocation) { |
| 211 return invocation is ConstructorInvocation || |
| 212 invocation is StaticInvocation && |
| 213 invocation.target.kind == ProcedureKind.Factory; |
| 214 } |
| 215 |
| 216 bool isTypeVariable(DartType type) => type is TypeParameterType; |
| 217 |
| 218 /// Add the runtime type as an extra argument to constructor invocations. |
| 219 Arguments visitArguments(Arguments arguments) { |
| 220 trace(arguments); |
| 221 |
| 222 arguments.transformChildren(this); |
| 223 TreeNode parent = arguments.parent; |
| 224 if (isInstantiation(parent)) { |
| 225 Class targetClass = getEnclosingClass(getStaticInvocationTarget(parent)); |
| 226 // Do not add the extra argument if the class does not need a type member |
| 227 // or if it can be implemented as a getter. |
| 228 if (!needsTypeInformation(targetClass) || usesTypeGetter(targetClass)) { |
| 229 return arguments; |
| 230 } |
| 231 |
| 232 List<DartType> typeArguments = arguments.types; |
| 233 |
| 234 Expression type = |
| 235 createRuntimeType(new InterfaceType(targetClass, typeArguments)); |
| 236 |
| 237 builder.insertAsFirstArgument(arguments, type); |
| 238 } |
| 239 return arguments; |
| 240 } |
| 241 |
| 242 Field visitField(Field field) { |
| 243 trace(field); |
| 244 |
| 245 visitDartType(field.type); |
| 246 for (Expression annotation in field.annotations) { |
| 247 annotation.accept(this); |
| 248 } |
| 249 // Do not visit initializers, they have already been transformed when the |
| 250 // class was handled. |
| 251 return field; |
| 252 } |
| 253 |
| 254 /// Go through all initializers of fields and record a static initializer |
| 255 /// function, if necessary. |
| 256 void rewriteFieldInitializers(Class cls) { |
| 257 assert(context != null); |
| 258 context.initializers = new LinkedHashMap<Field, Procedure>(); |
| 259 List<Field> fields = cls.fields; |
| 260 bool initializerRewritten = false; |
| 261 for (Field field in fields) { |
| 262 if (!initializerRewritten && knowledge.usedParameters(field).isEmpty) { |
| 263 // This field needs no static initializer. |
| 264 continue; |
| 265 } |
| 266 |
| 267 Expression initializer = field.initializer; |
| 268 if (initializer == null || field.isStatic) continue; |
| 269 // Declare a new variable that holds the type information and can be |
| 270 // used to access type variables in initializer context. |
| 271 // TODO(karlklose): some fields do not need the parameter. |
| 272 VariableDeclaration typeObject = new VariableDeclaration(r"$type"); |
| 273 context.parameter = typeObject; |
| 274 context.inInitializer = true; |
| 275 // Translate the initializer while keeping track of whether there was |
| 276 // already an initializers that required type information in |
| 277 // [typeVariableUsedInInitializer]. |
| 278 initializer = initializer.accept(this); |
| 279 context.parameter = null; |
| 280 context.inInitializer = false; |
| 281 // Create a static initializer function from the translated initializer |
| 282 // expression and record it. |
| 283 Name name = new Name("\$init\$${field.name.name}"); |
| 284 Statement body = new ReturnStatement(initializer); |
| 285 Procedure staticInitializer = new Procedure( |
| 286 name, |
| 287 ProcedureKind.Method, |
| 288 new FunctionNode(body, |
| 289 positionalParameters: <VariableDeclaration>[typeObject]), |
| 290 isStatic: true, |
| 291 fileUri: cls.fileUri); |
| 292 context.initializers[field] = staticInitializer; |
| 293 // Finally, remove the initializer from the field. |
| 294 field.initializer = null; |
| 295 } |
| 296 } |
| 297 |
| 298 bool inheritsTypeProperty(Class cls) { |
| 299 assert(needsTypeInformation(cls)); |
| 300 Class superclass = cls.superclass; |
| 301 return needsTypeInformation(superclass); |
| 302 } |
| 303 |
| 304 Class visitClass(Class cls) { |
| 305 trace(cls); |
| 306 |
| 307 if (needsTypeInformation(cls)) { |
| 308 context = new TransformationContext(); |
| 309 List<TypeParameter> typeParameters = cls.typeParameters; |
| 310 if (usesTypeGetter(cls)) { |
| 311 assert(typeParameters.isEmpty); |
| 312 context.runtimeTypeStorage = RuntimeTypeStorage.getter; |
| 313 Member getter = builder.createGetter(rtiLibrary.runtimeTypeName, |
| 314 createRuntimeType(cls.rawType), cls, rtiLibrary.typeType); |
| 315 cls.addMember(getter); |
| 316 } else if (!inheritsTypeProperty(cls)) { |
| 317 context.runtimeTypeStorage = RuntimeTypeStorage.field; |
| 318 // TODO(karlklose): should we add the field to [Object]? |
| 319 context.runtimeTypeField = new Field(rtiLibrary.runtimeTypeName, |
| 320 fileUri: cls.fileUri, isFinal: true, type: rtiLibrary.typeType); |
| 321 cls.addMember(context.runtimeTypeField); |
| 322 } else { |
| 323 context.runtimeTypeStorage = RuntimeTypeStorage.inheritedField; |
| 324 } |
| 325 |
| 326 for (int i = 0; i < typeParameters.length; ++i) { |
| 327 TypeParameter variable = typeParameters[i]; |
| 328 cls.addMember(builder.createTypeVariableGetter(cls, variable, i)); |
| 329 } |
| 330 |
| 331 // Tag the class as supporting the runtime type getter. |
| 332 InterfaceType interfaceTypeForSupertype = |
| 333 new InterfaceType(rtiLibrary.markerClass); |
| 334 cls.implementedTypes.add(new Supertype( |
| 335 interfaceTypeForSupertype.classNode, |
| 336 interfaceTypeForSupertype.typeArguments)); |
| 337 |
| 338 // Before transforming the parts of the class declaration, rewrite field |
| 339 // initializers that use type variables (or that would be called after one |
| 340 // that does) to static functions that can be used from constructors. |
| 341 rewriteFieldInitializers(cls); |
| 342 |
| 343 // Add properties for declaration tests. |
| 344 for (Class test in knowledge.classTests) { |
| 345 if (test == rtiLibrary.markerClass) continue; |
| 346 |
| 347 Procedure tag = builder.createGetter( |
| 348 builder.getTypeTestTagName(test), |
| 349 new BoolLiteral(isSuperClass(test, cls)), |
| 350 cls, |
| 351 builder.coreTypes.boolClass.rawType); |
| 352 cls.addMember(tag); |
| 353 } |
| 354 |
| 355 // Add a runtimeType getter. |
| 356 if (!usesTypeGetter(cls) && !inheritsTypeProperty(cls)) { |
| 357 cls.addMember(new Procedure( |
| 358 new Name("runtimeType"), |
| 359 ProcedureKind.Getter, |
| 360 new FunctionNode( |
| 361 new ReturnStatement(new DirectPropertyGet( |
| 362 new ThisExpression(), context.runtimeTypeField)), |
| 363 returnType: builder.coreTypes.typeClass.rawType), |
| 364 fileUri: cls.fileUri)); |
| 365 } |
| 366 } |
| 367 |
| 368 cls.transformChildren(this); |
| 369 |
| 370 // Add the static initializer functions. They have already been transformed. |
| 371 if (context?.initializers != null) { |
| 372 context.initializers.forEach((_, Procedure initializer) { |
| 373 cls.addMember(initializer); |
| 374 }); |
| 375 } |
| 376 |
| 377 // TODO(karlklose): clear type arguments later, the order of class |
| 378 // transformations otherwise influences the result. |
| 379 // cls.typeParameters.clear(); |
| 380 context = null; |
| 381 return cls; |
| 382 } |
| 383 |
| 384 // TODO(karlklose): replace with a structure that can answer also the question |
| 385 // which tags must be overriden due to different values. |
| 386 /// Returns `true` if [a] is a declaration used in a supertype of [b]. |
| 387 bool isSuperClass(Class a, Class b) { |
| 388 if (b == null) return false; |
| 389 if (a == b) return true; |
| 390 |
| 391 if (isSuperClass(a, b.superclass)) { |
| 392 return true; |
| 393 } |
| 394 |
| 395 Iterable<Class> interfaceClasses = b.implementedTypes |
| 396 .map((Supertype type) => type.classNode) |
| 397 .where((Class cls) => cls != rtiLibrary.markerClass); |
| 398 return interfaceClasses |
| 399 .any((Class declaration) => isSuperClass(a, declaration)); |
| 400 } |
| 401 |
| 402 bool isConstructorOrFactory(TreeNode node) { |
| 403 return isFactory(node) || node is Constructor; |
| 404 } |
| 405 |
| 406 bool isFactory(TreeNode node) { |
| 407 return node is Procedure && node.kind == ProcedureKind.Factory; |
| 408 } |
| 409 |
| 410 bool needsParameterForRuntimeType(TreeNode node) { |
| 411 if (!isConstructorOrFactory(node)) return false; |
| 412 |
| 413 RuntimeTypeStorage access = context.runtimeTypeStorage; |
| 414 assert(access != RuntimeTypeStorage.none); |
| 415 return access == RuntimeTypeStorage.field || |
| 416 access == RuntimeTypeStorage.inheritedField; |
| 417 } |
| 418 |
| 419 FunctionNode visitFunctionNode(FunctionNode node) { |
| 420 trace(node); |
| 421 |
| 422 // If we have a [TransformationContext] with a runtime type field and we |
| 423 // translate a constructor or factory, we need a parameter that the code of |
| 424 // initializers or the factory body can use to access type arguments. |
| 425 // The parameter field in the context will be reset in the visit-method of |
| 426 // the parent. |
| 427 if (context != null && needsParameterForRuntimeType(node.parent)) { |
| 428 assert(context.parameter == null); |
| 429 // Create the parameter and insert it as the function's first parameter. |
| 430 context.parameter = new VariableDeclaration( |
| 431 rtiLibrary.runtimeTypeName.name, |
| 432 type: rtiLibrary.typeType); |
| 433 context.parameter.parent = node; |
| 434 node.positionalParameters.insert(0, context.parameter); |
| 435 node.requiredParameterCount++; |
| 436 } |
| 437 node.transformChildren(this); |
| 438 return node; |
| 439 } |
| 440 |
| 441 SuperInitializer visitSuperInitializer(SuperInitializer initializer) { |
| 442 initializer.transformChildren(this); |
| 443 return addTypeAsArgument(initializer); |
| 444 } |
| 445 |
| 446 RedirectingInitializer visitRedirectingInitializer( |
| 447 RedirectingInitializer initializer) { |
| 448 initializer.transformChildren(this); |
| 449 return addTypeAsArgument(initializer); |
| 450 } |
| 451 |
| 452 Procedure visitProcedure(Procedure procedure) { |
| 453 trace(procedure); |
| 454 |
| 455 transformList(procedure.annotations, this, procedure.parent); |
| 456 // Visit the function body in a initializing context, if it is a factory. |
| 457 context?.inInitializer = isFactory(procedure); |
| 458 procedure.function?.accept(this); |
| 459 context?.inInitializer = false; |
| 460 |
| 461 context?.parameter = null; |
| 462 return procedure; |
| 463 } |
| 464 |
| 465 Constructor visitConstructor(Constructor constructor) { |
| 466 trace(constructor); |
| 467 |
| 468 transformList(constructor.annotations, this, constructor); |
| 469 if (constructor.function != null) { |
| 470 constructor.function = constructor.function.accept(this); |
| 471 constructor.function?.parent = constructor; |
| 472 } |
| 473 |
| 474 context?.inInitializer = true; |
| 475 transformList(constructor.initializers, this, constructor); |
| 476 context?.inInitializer = false; |
| 477 |
| 478 if (context != null) { |
| 479 if (context.runtimeTypeStorage == RuntimeTypeStorage.field) { |
| 480 // Initialize the runtime type field with value given in the additional |
| 481 // constructor parameter. |
| 482 assert(context.parameter != null); |
| 483 Initializer initializer = new FieldInitializer( |
| 484 context.runtimeTypeField, new VariableGet(context.parameter)); |
| 485 initializer.parent = constructor; |
| 486 constructor.initializers.insert(0, initializer); |
| 487 } |
| 488 if (context.initializers != null) { |
| 489 // For each field that needed a static initializer function, initialize |
| 490 // the field by calling the function. |
| 491 List<Initializer> fieldInitializers = <Initializer>[]; |
| 492 context.initializers.forEach((Field field, Procedure initializer) { |
| 493 assert(context.parameter != null); |
| 494 Arguments argument = |
| 495 new Arguments(<Expression>[new VariableGet(context.parameter)]); |
| 496 fieldInitializers.add(new FieldInitializer( |
| 497 field, new StaticInvocation(initializer, argument))); |
| 498 }); |
| 499 constructor.initializers.insertAll(0, fieldInitializers); |
| 500 } |
| 501 context.parameter = null; |
| 502 } |
| 503 |
| 504 return constructor; |
| 505 } |
| 506 |
| 507 /// Returns `true` if the given type can be tested using type test tags. |
| 508 /// |
| 509 /// This implies that there are no subtypes of the [type] that are not |
| 510 /// transformed. |
| 511 bool typeSupportsTagTest(InterfaceType type) { |
| 512 return needsTypeInformation(type.classNode); |
| 513 } |
| 514 |
| 515 Expression visitIsExpression(IsExpression expression) { |
| 516 trace(expression); |
| 517 |
| 518 expression.transformChildren(this); |
| 519 |
| 520 if (getEnclosingLibrary(expression) == rtiLibrary.interceptorsLibrary) { |
| 521 // In the interceptor library we need actual is-checks at the moment. |
| 522 return expression; |
| 523 } |
| 524 |
| 525 Expression target = expression.operand; |
| 526 DartType type = expression.type; |
| 527 |
| 528 if (type is InterfaceType && typeSupportsTagTest(type)) { |
| 529 assert(knowledge.classTests.contains(type.classNode)); |
| 530 bool checkArguments = |
| 531 type.typeArguments.any((DartType type) => type is! DynamicType); |
| 532 Class declaration = type.classNode; |
| 533 VariableDeclaration typeExpression = |
| 534 new VariableDeclaration(null, initializer: createRuntimeType(type)); |
| 535 VariableDeclaration targetValue = |
| 536 new VariableDeclaration(null, initializer: target); |
| 537 Expression markerClassTest = new IsExpression( |
| 538 new VariableGet(targetValue), rtiLibrary.markerClass.rawType); |
| 539 Expression tagCheck = new PropertyGet(new VariableGet(targetValue), |
| 540 builder.getTypeTestTagName(declaration)); |
| 541 Expression check = new LogicalExpression(markerClassTest, "&&", tagCheck); |
| 542 if (checkArguments) { |
| 543 // TODO(karlklose): support a direct argument check, we already checked |
| 544 // the declaration. |
| 545 Expression uninterceptedCheck = new Let( |
| 546 typeExpression, |
| 547 builder.createIsSubtypeOf( |
| 548 new VariableGet(targetValue), new VariableGet(typeExpression), |
| 549 targetHasTypeProperty: true)); |
| 550 check = new LogicalExpression(check, "&&", uninterceptedCheck); |
| 551 } |
| 552 return new Let(targetValue, check); |
| 553 } else { |
| 554 return builder.createIsSubtypeOf(target, createRuntimeType(type)); |
| 555 } |
| 556 } |
| 557 |
| 558 Expression visitListLiteral(ListLiteral node) { |
| 559 trace(node); |
| 560 node.transformChildren(this); |
| 561 return builder.callAttachType( |
| 562 node, |
| 563 new InterfaceType( |
| 564 builder.coreTypes.listClass, <DartType>[node.typeArgument])); |
| 565 } |
| 566 |
| 567 Expression visitMapLiteral(MapLiteral node) { |
| 568 trace(node); |
| 569 node.transformChildren(this); |
| 570 return builder.callAttachType( |
| 571 node, |
| 572 new InterfaceType(builder.coreTypes.mapClass, |
| 573 <DartType>[node.keyType, node.valueType])); |
| 574 } |
| 575 } |
OLD | NEW |