Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
|
ngeoffray
2013/12/10 17:11:02
I plan to split this file into different files in
| |
| 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 type_graph_inferrer; | 5 part of type_graph_inferrer; |
| 6 | 6 |
| 7 /** | 7 /** |
| 8 * A set of selector names that [List] implements, that we know do not | 8 * A set of selector names that [List] implements, that we know do not |
| 9 * change the element type of the list, or let the list escape to code | 9 * change the element type of the list, or let the list escape to code |
| 10 * that might change the element type. | 10 * that might change the element type. |
| 11 */ | 11 */ |
| (...skipping 150 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 162 'shuffle', | 162 'shuffle', |
| 163 '[]=', | 163 '[]=', |
| 164 | 164 |
| 165 // From JSArray. | 165 // From JSArray. |
| 166 'checkMutable', | 166 'checkMutable', |
| 167 'checkGrowable', | 167 'checkGrowable', |
| 168 ]); | 168 ]); |
| 169 | 169 |
| 170 bool _VERBOSE = false; | 170 bool _VERBOSE = false; |
| 171 | 171 |
| 172 class ContainerTracerVisitor implements TypeInformationVisitor { | 172 abstract class TracerVisitor implements TypeInformationVisitor { |
| 173 final ListTypeInformation container; | 173 final TypeInformation tracedType; |
| 174 final TypeGraphInferrerEngine inferrer; | 174 final TypeGraphInferrerEngine inferrer; |
| 175 final Compiler compiler; | 175 final Compiler compiler; |
| 176 | 176 |
| 177 static const int MAX_ANALYSIS_COUNT = 16; | |
| 178 final Setlet<Element> analyzedElements = new Setlet<Element>(); | |
| 179 | |
| 180 TracerVisitor(this.tracedType, inferrer) | |
| 181 : this.inferrer = inferrer, this.compiler = inferrer.compiler; | |
| 177 | 182 |
| 178 // Work list that gets populated with [TypeInformation] that could | 183 // Work list that gets populated with [TypeInformation] that could |
| 179 // contain the container. | 184 // contain the container. |
| 180 final List<TypeInformation> workList = <TypeInformation>[]; | 185 final List<TypeInformation> workList = <TypeInformation>[]; |
| 181 | 186 |
| 182 // Work list of containers to analyze after analyzing the users of a | 187 // Work list of containers to analyze after analyzing the users of a |
| 183 // [TypeInformation] that may be [container]. We know [container] | 188 // [TypeInformation] that may be [container]. We know [container] |
| 184 // has been stored in these containers and we must check how | 189 // has been stored in these containers and we must check how |
| 185 // [container] escapes from these containers. | 190 // [container] escapes from these containers. |
| 186 final List<ListTypeInformation> containersToAnalyze = | 191 final List<ListTypeInformation> containersToAnalyze = |
| 187 <ListTypeInformation>[]; | 192 <ListTypeInformation>[]; |
| 188 | 193 |
| 194 final Setlet<TypeInformation> flowsInto = new Setlet<TypeInformation>(); | |
| 195 | |
| 189 // The current [TypeInformation] in the analysis. | 196 // The current [TypeInformation] in the analysis. |
| 190 TypeInformation currentUser; | 197 TypeInformation currentUser; |
| 191 | |
| 192 // The list of found assignments to the container. | |
| 193 final List<TypeInformation> assignments = <TypeInformation>[]; | |
| 194 | |
| 195 bool callsGrowableMethod = false; | |
| 196 bool continueAnalyzing = true; | 198 bool continueAnalyzing = true; |
| 197 | |
| 198 static const int MAX_ANALYSIS_COUNT = 16; | |
| 199 final Setlet<Element> analyzedElements = new Setlet<Element>(); | |
| 200 | |
| 201 ContainerTracerVisitor(this.container, inferrer) | |
| 202 : this.inferrer = inferrer, this.compiler = inferrer.compiler; | |
| 203 | 199 |
| 204 void addNewEscapeInformation(TypeInformation info) { | 200 void addNewEscapeInformation(TypeInformation info) { |
| 205 if (container.flowsInto.contains(info)) return; | 201 if (flowsInto.contains(info)) return; |
| 206 container.flowsInto.add(info); | 202 flowsInto.add(info); |
| 207 workList.add(info); | 203 workList.add(info); |
| 208 } | 204 } |
| 209 | 205 |
| 210 List<TypeInformation> run() { | 206 void analyze() { |
| 211 // Collect the [TypeInformation] where the container can flow in, | 207 // Collect the [TypeInformation] where the container can flow in, |
| 212 // as well as the operations done on all these [TypeInformation]s. | 208 // as well as the operations done on all these [TypeInformation]s. |
| 213 addNewEscapeInformation(container); | 209 addNewEscapeInformation(tracedType); |
| 214 while (!workList.isEmpty) { | 210 while (!workList.isEmpty) { |
| 215 currentUser = workList.removeLast(); | 211 currentUser = workList.removeLast(); |
| 216 currentUser.users.forEach((TypeInformation info) { | 212 currentUser.users.forEach((TypeInformation info) { |
| 217 analyzedElements.add(info.owner); | 213 analyzedElements.add(info.owner); |
| 218 info.accept(this); | 214 info.accept(this); |
| 219 }); | 215 }); |
| 220 while (!containersToAnalyze.isEmpty) { | 216 while (!containersToAnalyze.isEmpty) { |
| 221 analyzeStoredIntoContainer(containersToAnalyze.removeLast()); | 217 analyzeStoredIntoContainer(containersToAnalyze.removeLast()); |
| 222 } | 218 } |
| 223 if (!continueAnalyzing) break; | 219 if (!continueAnalyzing) break; |
| 224 if (analyzedElements.length > MAX_ANALYSIS_COUNT) { | 220 if (analyzedElements.length > MAX_ANALYSIS_COUNT) { |
| 225 bailout('Too many users'); | 221 bailout('Too many users'); |
| 226 break; | 222 break; |
| 227 } | 223 } |
| 228 } | 224 } |
| 229 | |
| 230 if (continueAnalyzing) { | |
| 231 if (!callsGrowableMethod && container.inferredLength == null) { | |
| 232 container.inferredLength = container.originalLength; | |
| 233 } | |
| 234 return assignments; | |
| 235 } | |
| 236 return null; | |
| 237 } | 225 } |
| 238 | 226 |
| 239 void bailout(String reason) { | 227 void bailout(String reason) { |
| 240 if (_VERBOSE) { | 228 if (_VERBOSE) { |
| 241 ContainerTypeMask mask = container.type; | 229 print('Bailing out on $tracedType because: $reason'); |
| 242 print('Bailing out on ${mask.allocationNode} ${mask.allocationElement} ' | |
| 243 'because: $reason'); | |
| 244 } | 230 } |
| 245 continueAnalyzing = false; | 231 continueAnalyzing = false; |
| 246 callsGrowableMethod = true; | |
| 247 } | 232 } |
| 248 | 233 |
| 249 visitNarrowTypeInformation(NarrowTypeInformation info) { | 234 void visitNarrowTypeInformation(NarrowTypeInformation info) { |
| 250 addNewEscapeInformation(info); | 235 addNewEscapeInformation(info); |
| 251 } | 236 } |
| 252 | 237 |
| 253 visitPhiElementTypeInformation(PhiElementTypeInformation info) { | 238 void visitPhiElementTypeInformation(PhiElementTypeInformation info) { |
| 254 addNewEscapeInformation(info); | 239 addNewEscapeInformation(info); |
| 255 } | 240 } |
| 256 | 241 |
| 257 visitElementInContainerTypeInformation( | 242 void visitElementInContainerTypeInformation( |
| 258 ElementInContainerTypeInformation info) { | 243 ElementInContainerTypeInformation info) { |
| 259 addNewEscapeInformation(info); | 244 addNewEscapeInformation(info); |
| 260 } | 245 } |
| 261 | 246 |
| 262 visitListTypeInformation(ListTypeInformation info) { | 247 visitListTypeInformation(ListTypeInformation info) { |
| 263 containersToAnalyze.add(info); | 248 containersToAnalyze.add(info); |
| 264 } | 249 } |
| 265 | 250 |
| 266 visitMapTypeInformation(MapTypeInformation info) { | 251 void visitConcreteTypeInformation(ConcreteTypeInformation info) {} |
| 267 bailout('Stored in a map'); | |
| 268 } | |
| 269 | 252 |
| 270 visitConcreteTypeInformation(ConcreteTypeInformation info) {} | 253 void visitClosureTypeInformation(ConcreteTypeInformation info) {} |
| 271 | |
| 272 visitClosureCallSiteTypeInformation(ClosureCallSiteTypeInformation info) { | |
| 273 bailout('Passed to a closure'); | |
| 274 } | |
| 275 | 254 |
| 276 visitStaticCallSiteTypeInformation(StaticCallSiteTypeInformation info) { | 255 visitStaticCallSiteTypeInformation(StaticCallSiteTypeInformation info) { |
| 277 Element called = info.calledElement; | 256 Element called = info.calledElement; |
| 278 if (called.isForeign(compiler) && called.name == 'JS') { | |
| 279 bailout('Used in JS ${info.call}'); | |
| 280 } | |
| 281 if (inferrer.types.getInferredTypeOf(called) == currentUser) { | 257 if (inferrer.types.getInferredTypeOf(called) == currentUser) { |
| 282 addNewEscapeInformation(info); | 258 addNewEscapeInformation(info); |
| 283 } | 259 } |
| 284 } | 260 } |
| 285 | 261 |
| 286 void analyzeStoredIntoContainer(ListTypeInformation container) { | 262 void analyzeStoredIntoContainer(ListTypeInformation container) { |
| 287 inferrer.analyzeContainer(container); | 263 inferrer.analyzeContainer(container); |
| 288 if (container.bailedOut) { | 264 if (container.bailedOut) { |
| 289 bailout('Stored in a container that bailed out'); | 265 bailout('Stored in a container that bailed out'); |
| 290 } else { | 266 } else { |
| 291 container.flowsInto.forEach((flow) { | 267 container.flowsInto.forEach((flow) { |
| 292 flow.users.forEach((user) { | 268 flow.users.forEach((user) { |
| 293 if (user is !DynamicCallSiteTypeInformation) return; | 269 if (user is !DynamicCallSiteTypeInformation) return; |
| 294 if (user.receiver != flow) return; | 270 if (user.receiver != flow) return; |
| 295 if (returnsElementTypeSet.contains(user.selector)) { | 271 if (returnsElementTypeSet.contains(user.selector)) { |
| 296 addNewEscapeInformation(user); | 272 addNewEscapeInformation(user); |
| 297 } else if (!doesNotEscapeElementSet.contains(user.selector.name)) { | 273 } else if (!doesNotEscapeElementSet.contains(user.selector.name)) { |
| 298 bailout('Escape from a container'); | 274 bailout('Escape from a container'); |
| 299 } | 275 } |
| 300 }); | 276 }); |
| 301 }); | 277 }); |
| 302 } | 278 } |
| 303 } | 279 } |
| 304 | 280 |
| 305 bool isAddedToContainer(DynamicCallSiteTypeInformation info) { | 281 bool isAddedToContainer(DynamicCallSiteTypeInformation info) { |
| 282 if (info.arguments == null) return false; | |
| 306 var receiverType = info.receiver.type; | 283 var receiverType = info.receiver.type; |
| 307 if (!receiverType.isContainer) return false; | 284 if (!receiverType.isContainer) return false; |
| 308 String selectorName = info.selector.name; | 285 String selectorName = info.selector.name; |
| 309 List<TypeInformation> arguments = info.arguments.positional; | 286 List<TypeInformation> arguments = info.arguments.positional; |
| 310 return (selectorName == '[]=' && currentUser == arguments[1]) | 287 return (selectorName == '[]=' && currentUser == arguments[1]) |
| 311 || (selectorName == 'insert' && currentUser == arguments[0]) | 288 || (selectorName == 'insert' && currentUser == arguments[0]) |
| 312 || (selectorName == 'add' && currentUser == arguments[0]); | 289 || (selectorName == 'add' && currentUser == arguments[0]); |
| 313 } | 290 } |
| 314 | 291 |
| 292 void visitDynamicCallSiteTypeInformation( | |
| 293 DynamicCallSiteTypeInformation info) { | |
| 294 if (isAddedToContainer(info)) { | |
| 295 ContainerTypeMask mask = info.receiver.type; | |
| 296 if (mask.allocationNode != null) { | |
| 297 ListTypeInformation container = | |
| 298 inferrer.types.allocatedLists[mask.allocationNode]; | |
| 299 containersToAnalyze.add(container); | |
| 300 } else { | |
| 301 // The [ContainerTypeMask] is a union of two containers, and | |
| 302 // we lose track of where these containers have been allocated | |
| 303 // at this point. | |
| 304 bailout('Stored in too many containers'); | |
| 305 } | |
| 306 } | |
| 307 | |
| 308 if (info.targets | |
|
kasperl
2013/12/11 07:35:35
Consider using a local variable for the result of
ngeoffray
2013/12/16 11:47:43
Done.
| |
| 309 .map((element) => inferrer.types.getInferredTypeOf(element)) | |
| 310 .any((other) => other == currentUser)) { | |
| 311 addNewEscapeInformation(info); | |
| 312 } | |
| 313 } | |
| 314 | |
| 315 bool isParameterOfListAddingMethod(Element element) { | |
| 316 if (!element.isParameter()) return false; | |
| 317 if (element.getEnclosingClass() != compiler.backend.listImplementation) { | |
| 318 return false; | |
| 319 } | |
| 320 Element method = element.enclosingElement; | |
| 321 return (method.name == '[]=') | |
| 322 || (method.name == 'add') | |
| 323 || (method.name == 'insert'); | |
| 324 } | |
| 325 | |
| 326 void visitElementTypeInformation(ElementTypeInformation info) { | |
| 327 if (isParameterOfListAddingMethod(info.element)) { | |
| 328 // These elements are being handled in | |
| 329 // [visitDynamicCallSiteTypeInformation]. | |
| 330 return; | |
| 331 } | |
| 332 addNewEscapeInformation(info); | |
| 333 } | |
| 334 } | |
| 335 | |
| 336 class ContainerTracerVisitor extends TracerVisitor { | |
| 337 // The list of found assignments to the container. | |
| 338 final List<TypeInformation> assignments = <TypeInformation>[]; | |
| 339 bool callsGrowableMethod = false; | |
| 340 | |
| 341 ContainerTracerVisitor(tracedType, inferrer) : super(tracedType, inferrer); | |
| 342 | |
| 343 List<TypeInformation> run() { | |
| 344 analyze(); | |
| 345 ListTypeInformation container = tracedType; | |
| 346 if (continueAnalyzing) { | |
| 347 if (!callsGrowableMethod && container.inferredLength == null) { | |
| 348 container.inferredLength = container.originalLength; | |
| 349 } | |
| 350 container.flowsInto.addAll(flowsInto); | |
| 351 return assignments; | |
| 352 } else { | |
| 353 callsGrowableMethod = true; | |
| 354 return null; | |
| 355 } | |
| 356 } | |
| 357 | |
| 358 visitMapTypeInformation(MapTypeInformation info) { | |
| 359 bailout('Stored in a map'); | |
| 360 } | |
| 361 | |
| 362 visitClosureCallSiteTypeInformation(ClosureCallSiteTypeInformation info) { | |
| 363 bailout('Passed to a closure'); | |
| 364 } | |
| 365 | |
| 366 visitStaticCallSiteTypeInformation(StaticCallSiteTypeInformation info) { | |
| 367 Element called = info.calledElement; | |
| 368 if (called.isForeign(compiler) && called.name == 'JS') { | |
| 369 bailout('Used in JS ${info.call}'); | |
| 370 } | |
| 371 super.visitStaticCallSiteTypeInformation(info); | |
|
kasperl
2013/12/11 07:35:35
Is it important to call the super method at the bo
ngeoffray
2013/12/16 11:47:43
Done.
| |
| 372 } | |
| 373 | |
| 315 visitDynamicCallSiteTypeInformation(DynamicCallSiteTypeInformation info) { | 374 visitDynamicCallSiteTypeInformation(DynamicCallSiteTypeInformation info) { |
| 316 Selector selector = info.selector; | 375 Selector selector = info.selector; |
| 317 String selectorName = selector.name; | 376 String selectorName = selector.name; |
| 318 if (currentUser == info.receiver) { | 377 if (currentUser == info.receiver) { |
| 319 if (!okSelectorsSet.contains(selectorName)) { | 378 if (!okSelectorsSet.contains(selectorName)) { |
| 320 if (selector.isCall()) { | 379 if (selector.isCall()) { |
| 321 int positionalLength = info.arguments.positional.length; | 380 int positionalLength = info.arguments.positional.length; |
| 322 if (selectorName == 'add') { | 381 if (selectorName == 'add') { |
| 323 if (positionalLength == 1) { | 382 if (positionalLength == 1) { |
| 324 assignments.add(info.arguments.positional[0]); | 383 assignments.add(info.arguments.positional[0]); |
| (...skipping 17 matching lines...) Expand all Loading... | |
| 342 callsGrowableMethod = true; | 401 callsGrowableMethod = true; |
| 343 } | 402 } |
| 344 if (selectorName == 'length' && selector.isSetter()) { | 403 if (selectorName == 'length' && selector.isSetter()) { |
| 345 callsGrowableMethod = true; | 404 callsGrowableMethod = true; |
| 346 assignments.add(inferrer.types.nullType); | 405 assignments.add(inferrer.types.nullType); |
| 347 } | 406 } |
| 348 } else if (selector.isCall() | 407 } else if (selector.isCall() |
| 349 && !info.targets.every((element) => element.isFunction())) { | 408 && !info.targets.every((element) => element.isFunction())) { |
| 350 bailout('Passed to a closure'); | 409 bailout('Passed to a closure'); |
| 351 return; | 410 return; |
| 352 } else if (isAddedToContainer(info)) { | |
| 353 ContainerTypeMask mask = info.receiver.type; | |
| 354 if (mask.allocationNode != null) { | |
| 355 ListTypeInformation container = | |
| 356 inferrer.types.allocatedLists[mask.allocationNode]; | |
| 357 containersToAnalyze.add(container); | |
| 358 } else { | |
| 359 // The [ContainerTypeMask] is a union of two containers, and | |
| 360 // we lose track of where these containers have been allocated | |
| 361 // at this point. | |
| 362 bailout('Stored in too many containers'); | |
| 363 } | |
| 364 } | 411 } |
| 365 | 412 super.visitDynamicCallSiteTypeInformation(info); |
| 366 if (info.targets | |
| 367 .map((element) => inferrer.types.getInferredTypeOf(element)) | |
| 368 .any((other) => other == currentUser)) { | |
| 369 addNewEscapeInformation(info); | |
| 370 } | |
| 371 } | 413 } |
| 372 | 414 |
| 373 bool isClosure(Element element) { | 415 bool isClosure(Element element) { |
| 374 if (!element.isFunction()) return false; | 416 if (!element.isFunction()) return false; |
| 375 Element outermost = element.getOutermostEnclosingMemberOrTopLevel(); | 417 Element outermost = element.getOutermostEnclosingMemberOrTopLevel(); |
| 376 return outermost.declaration != element.declaration; | 418 return outermost.declaration != element.declaration; |
| 377 } | 419 } |
| 378 | 420 |
| 379 bool isParameterOfListAddingMethod(Element element) { | 421 visitElementTypeInformation(ElementTypeInformation info) { |
| 380 if (!element.isParameter()) return false; | 422 if (isClosure(info.element)) { |
| 381 if (element.getEnclosingClass() != compiler.backend.listImplementation) { | 423 bailout('Returned from a closure'); |
| 382 return false; | |
| 383 } | 424 } |
| 384 Element method = element.enclosingElement; | 425 if (compiler.backend.isNeededForReflection(info.element)) { |
| 385 return (method.name == '[]=') | 426 bailout('Escape in reflection'); |
| 386 || (method.name == 'add') | 427 } |
| 387 || (method.name == 'insert'); | 428 super.visitElementTypeInformation(info); |
| 429 } | |
| 430 } | |
| 431 | |
| 432 class ClosureTracerVisitor extends TracerVisitor { | |
| 433 ClosureTracerVisitor(tracedType, inferrer) : super(tracedType, inferrer); | |
| 434 | |
| 435 void run() { | |
| 436 ClosureTypeInformation closure = tracedType; | |
| 437 FunctionElement element = closure.element; | |
| 438 element.functionSignature.forEachParameter((Element parameter) { | |
| 439 ElementTypeInformation info = inferrer.types.getInferredTypeOf(parameter); | |
| 440 info.abandonInferencing = false; | |
| 441 }); | |
| 442 analyze(); | |
| 443 element.functionSignature.forEachParameter((Element parameter) { | |
| 444 ElementTypeInformation info = inferrer.types.getInferredTypeOf(parameter); | |
| 445 if (continueAnalyzing) { | |
| 446 info.turnSpecialNonSpecial = true; | |
| 447 } else { | |
| 448 info.giveUp(inferrer); | |
| 449 } | |
| 450 }); | |
| 451 } | |
| 452 | |
| 453 visitMapTypeInformation(MapTypeInformation info) { | |
| 454 bailout('Stored in a map'); | |
| 455 } | |
| 456 | |
| 457 void analyzeCall(CallSiteTypeInformation info) { | |
| 458 ClosureTypeInformation closure = tracedType; | |
| 459 FunctionElement element = closure.element; | |
| 460 Selector selector = info.selector; | |
| 461 if (!selector.appliesUntyped(element, compiler)) return; | |
| 462 inferrer.updateParameterAssignments( | |
| 463 info, element, info.arguments, selector, remove: false, | |
| 464 addToQueue: false); | |
| 465 } | |
| 466 | |
| 467 visitClosureCallSiteTypeInformation(ClosureCallSiteTypeInformation info) { | |
| 468 if (info.closure == currentUser) { | |
| 469 analyzeCall(info); | |
| 470 } else { | |
| 471 bailout('Passed to a closure'); | |
| 472 } | |
| 473 } | |
| 474 | |
| 475 visitStaticCallSiteTypeInformation(StaticCallSiteTypeInformation info) { | |
| 476 Element called = info.calledElement; | |
| 477 if (called.isForeign(compiler) && called.name == 'JS') { | |
| 478 bailout('Used in JS ${info.call}'); | |
| 479 } | |
| 480 super.visitStaticCallSiteTypeInformation(info); | |
| 481 } | |
| 482 | |
| 483 bool checkIfCurrentUser(element) { | |
| 484 return inferrer.types.getInferredTypeOf(element) == currentUser; | |
| 485 } | |
| 486 | |
| 487 visitDynamicCallSiteTypeInformation(DynamicCallSiteTypeInformation info) { | |
| 488 if (info.selector.isCall()) { | |
| 489 if (info.arguments.contains(currentUser) | |
| 490 && !info.targets.every((element) => element.isFunction())) { | |
| 491 bailout('Passed to a closure'); | |
| 492 } else if (info.targets.any((element) => checkIfCurrentUser(element))) { | |
| 493 analyzeCall(info); | |
| 494 } | |
| 495 } | |
| 496 super.visitDynamicCallSiteTypeInformation(info); | |
| 497 } | |
| 498 | |
| 499 bool isClosure(Element element) { | |
| 500 if (!element.isFunction()) return false; | |
| 501 Element outermost = element.getOutermostEnclosingMemberOrTopLevel(); | |
| 502 return outermost.declaration != element.declaration; | |
| 388 } | 503 } |
| 389 | 504 |
| 390 visitElementTypeInformation(ElementTypeInformation info) { | 505 visitElementTypeInformation(ElementTypeInformation info) { |
| 391 if (isClosure(info.element)) { | 506 if (isClosure(info.element)) { |
| 392 bailout('Returned from a closure'); | 507 bailout('Returned from a closure'); |
| 393 } | 508 } |
| 394 if (compiler.backend.isNeededForReflection(info.element)) { | 509 if (compiler.backend.isNeededForReflection(info.element)) { |
| 395 bailout('Escape in reflection'); | 510 bailout('Escape in reflection'); |
| 396 } | 511 } |
| 397 if (isParameterOfListAddingMethod(info.element)) { | 512 super.visitElementTypeInformation(info); |
| 398 // These elements are being handled in | |
| 399 // [visitDynamicCallSiteTypeInformation]. | |
| 400 return; | |
| 401 } | |
| 402 addNewEscapeInformation(info); | |
| 403 } | 513 } |
| 404 } | 514 } |
| OLD | NEW |