| OLD | NEW |
| (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 tree_ir.optimization.variable_merger; |
| 6 |
| 7 import 'optimization.dart' show Pass, PassMixin; |
| 8 import '../tree_ir_nodes.dart'; |
| 9 import '../../elements/elements.dart' show Local, ParameterElement; |
| 10 |
| 11 /// Merges variables based on liveness and source variable information. |
| 12 /// |
| 13 /// This phase cleans up artifacts introduced by the translation through CPS, |
| 14 /// where each source variable is translated into several copies. The copies |
| 15 /// are merged again when they are not live simultaneously. |
| 16 class VariableMerger extends RecursiveVisitor with PassMixin { |
| 17 String get passName => 'Variable merger'; |
| 18 |
| 19 @override |
| 20 void rewriteExecutableDefinition(ExecutableDefinition node) { |
| 21 visitExecutableDefinition(node); |
| 22 } |
| 23 |
| 24 /// Rewrites the given function. |
| 25 /// This is called for the outermost function and inner functions. |
| 26 void rewriteFunction(ExecutableDefinition node) { |
| 27 BlockGraphBuilder builder = new BlockGraphBuilder(); |
| 28 builder.visitExecutableDefinition(node); |
| 29 _computeLiveness(builder.blocks); |
| 30 Map<Variable, Variable> subst = _computeRegisterAllocation(builder.blocks); |
| 31 new SubstituteVariables(subst).visitExecutableDefinition(node); |
| 32 } |
| 33 |
| 34 visitFunctionDefinition(FunctionDefinition node) { |
| 35 super.visitFunctionDefinition(node); // Recurse to visit inner functions. |
| 36 rewriteFunction(node); |
| 37 } |
| 38 |
| 39 visitFieldDefinition(FieldDefinition node) { |
| 40 super.visitFieldDefinition(node); |
| 41 rewriteFunction(node); |
| 42 } |
| 43 |
| 44 visitConstructorDefinition(ConstructorDefinition node) { |
| 45 super.visitConstructorDefinition(node); |
| 46 rewriteFunction(node); |
| 47 } |
| 48 } |
| 49 |
| 50 /// A read or write access to a variable. |
| 51 class VariableAccess { |
| 52 Variable variable; |
| 53 bool isRead; |
| 54 bool get isWrite => !isRead; |
| 55 |
| 56 VariableAccess.read(this.variable) : isRead = true; |
| 57 VariableAccess.write(this.variable) : isRead = false; |
| 58 } |
| 59 |
| 60 /// Basic block in a control-flow graph. |
| 61 class Block { |
| 62 /// List of predecessors in the control-flow graph. |
| 63 final List<Block> predecessors = <Block>[]; |
| 64 |
| 65 /// Entry to the catch block for the enclosing try, or `null`. |
| 66 final Block catchBlock; |
| 67 |
| 68 /// List of nodes with this block as [catchBlock]. |
| 69 final List<Block> catchPredecessors = <Block>[]; |
| 70 |
| 71 /// Sequence of read and write accesses in the block. |
| 72 final List<VariableAccess> accesses = <VariableAccess>[]; |
| 73 |
| 74 /// Auxiliary fields used by the liveness analysis. |
| 75 bool inWorklist = true; |
| 76 Set<Variable> liveIn; |
| 77 Set<Variable> liveOut = new Set<Variable>(); |
| 78 Set<Variable> gen = new Set<Variable>(); |
| 79 Set<Variable> kill = new Set<Variable>(); |
| 80 |
| 81 /// Adds a read operation to the block and updates gen/kill sets accordingly. |
| 82 void addRead(Variable variable) { |
| 83 // Operations are seen in forward order. |
| 84 // If the read is not preceded by a write, then add it to the GEN set. |
| 85 if (!kill.contains(variable)) { |
| 86 gen.add(variable); |
| 87 } |
| 88 accesses.add(new VariableAccess.read(variable)); |
| 89 } |
| 90 |
| 91 /// Adds a write operation to the block and updates gen/kill sets accordingly. |
| 92 void addWrite(Variable variable) { |
| 93 // If the write is not preceded by a read, then add it to the KILL set. |
| 94 if (!gen.contains(variable)) { |
| 95 kill.add(variable); |
| 96 } |
| 97 accesses.add(new VariableAccess.write(variable)); |
| 98 } |
| 99 |
| 100 Block(this.catchBlock) { |
| 101 if (catchBlock != null) { |
| 102 catchBlock.catchPredecessors.add(this); |
| 103 } |
| 104 } |
| 105 } |
| 106 |
| 107 /// Builds a control-flow graph suitable for performing liveness analysis. |
| 108 class BlockGraphBuilder extends RecursiveVisitor { |
| 109 Map<Label, Block> _jumpTarget = <Label, Block>{}; |
| 110 Block _currentBlock; |
| 111 List<Block> blocks = <Block>[]; |
| 112 |
| 113 /// Variables with an assignment that should be treated as final. |
| 114 /// |
| 115 /// Such variables cannot be merged with any other variables, so we exclude |
| 116 /// them from the control-flow graph entirely. |
| 117 Set<Variable> _ignoredVariables = new Set<Variable>(); |
| 118 |
| 119 BlockGraphBuilder() { |
| 120 _currentBlock = newBlock(); |
| 121 } |
| 122 |
| 123 /// Creates a new block with the current exception handler or [catchBlock] |
| 124 /// if provided. |
| 125 Block newBlock({Block catchBlock}) { |
| 126 if (catchBlock == null && _currentBlock != null) { |
| 127 catchBlock = _currentBlock.catchBlock; |
| 128 } |
| 129 Block block = new Block(catchBlock); |
| 130 blocks.add(block); |
| 131 return block; |
| 132 } |
| 133 |
| 134 /// Starts a new block after the end of [block]. |
| 135 void branchFrom(Block block, {Block catchBlock}) { |
| 136 _currentBlock = newBlock(catchBlock: catchBlock)..predecessors.add(block); |
| 137 } |
| 138 |
| 139 /// Called when reading from [variable]. |
| 140 /// |
| 141 /// Appends a read operation to the current basic block. |
| 142 void read(Variable variable) { |
| 143 if (variable.isCaptured) return; |
| 144 if (_ignoredVariables.contains(variable)) return; |
| 145 _currentBlock.addRead(variable); |
| 146 } |
| 147 |
| 148 /// Called when writing to [variable]. |
| 149 /// |
| 150 /// Appends a write operation to the current basic block. |
| 151 void write(Variable variable) { |
| 152 if (variable.isCaptured) return; |
| 153 if (_ignoredVariables.contains(variable)) return; |
| 154 _currentBlock.addWrite(variable); |
| 155 } |
| 156 |
| 157 /// Called to indicate that [variable] should not be merged, and therefore |
| 158 /// be excluded from the control-flow graph. |
| 159 /// Subsequent calls to [read] and [write] will ignore it. |
| 160 void ignoreVariable(Variable variable) { |
| 161 _ignoredVariables.add(variable); |
| 162 } |
| 163 |
| 164 visitVariableUse(VariableUse node) { |
| 165 read(node.variable); |
| 166 } |
| 167 |
| 168 visitAssign(Assign node) { |
| 169 visitExpression(node.value); |
| 170 write(node.variable); |
| 171 visitStatement(node.next); |
| 172 } |
| 173 |
| 174 visitIf(If node) { |
| 175 visitExpression(node.condition); |
| 176 Block afterCondition = _currentBlock; |
| 177 branchFrom(afterCondition); |
| 178 visitStatement(node.thenStatement); |
| 179 branchFrom(afterCondition); |
| 180 visitStatement(node.elseStatement); |
| 181 } |
| 182 |
| 183 visitLabeledStatement(LabeledStatement node) { |
| 184 Block join = _jumpTarget[node.label] = newBlock(); |
| 185 visitStatement(node.body); // visitBreak will add predecessors to join. |
| 186 _currentBlock = join; |
| 187 visitStatement(node.next); |
| 188 } |
| 189 |
| 190 visitBreak(Break node) { |
| 191 _jumpTarget[node.target].predecessors.add(_currentBlock); |
| 192 } |
| 193 |
| 194 visitContinue(Continue node) { |
| 195 _jumpTarget[node.target].predecessors.add(_currentBlock); |
| 196 } |
| 197 |
| 198 visitWhileTrue(WhileTrue node) { |
| 199 Block join = _jumpTarget[node.label] = newBlock(); |
| 200 join.predecessors.add(_currentBlock); |
| 201 _currentBlock = join; |
| 202 visitStatement(node.body); // visitContinue will add predecessors to join. |
| 203 } |
| 204 |
| 205 visitWhileCondition(WhileCondition node) { |
| 206 Block join = _jumpTarget[node.label] = newBlock(); |
| 207 join.predecessors.add(_currentBlock); |
| 208 _currentBlock = join; |
| 209 visitExpression(node.condition); |
| 210 Block afterCondition = _currentBlock; |
| 211 branchFrom(afterCondition); |
| 212 visitStatement(node.body); // visitContinue will add predecessors to join. |
| 213 branchFrom(afterCondition); |
| 214 visitStatement(node.next); |
| 215 } |
| 216 |
| 217 visitTry(Try node) { |
| 218 Block catchBlock = newBlock(); |
| 219 branchFrom(_currentBlock, catchBlock: catchBlock); |
| 220 visitStatement(node.tryBody); |
| 221 _currentBlock = catchBlock; |
| 222 // Catch parameters cannot be hoisted to the top of the function, so to |
| 223 // avoid complications with scoping, we do not attempt to merge them. |
| 224 node.catchParameters.forEach(ignoreVariable); |
| 225 visitStatement(node.catchBody); |
| 226 } |
| 227 |
| 228 visitConditional(Conditional node) { |
| 229 visitExpression(node.condition); |
| 230 // TODO(asgerf): When assignment expressions are added, this is no longer |
| 231 // sound; then we need to handle as a branch. |
| 232 visitExpression(node.thenExpression); |
| 233 visitExpression(node.elseExpression); |
| 234 } |
| 235 |
| 236 visitLogicalOperator(LogicalOperator node) { |
| 237 visitExpression(node.left); |
| 238 // TODO(asgerf): When assignment expressions are added, this is no longer |
| 239 // sound; then we need to handle as a branch. |
| 240 visitExpression(node.right); |
| 241 } |
| 242 |
| 243 visitFunctionDeclaration(FunctionDeclaration node) { |
| 244 // The function variable is final, hence cannot be merged. |
| 245 ignoreVariable(node.variable); |
| 246 visitStatement(node.next); |
| 247 // Do not traverse inner function. |
| 248 } |
| 249 |
| 250 visitFunctionExpression(FunctionExpression node) { |
| 251 // Do not traverse inner function. |
| 252 } |
| 253 |
| 254 visitFunctionDefinition(FunctionDefinition node) { |
| 255 // Function parameters are treated as write operations at the entry point, |
| 256 // so they can potentially be merged with other copies of the parameter. |
| 257 // Note that function parameters always have distinct source variables, |
| 258 // so we don't risk accidentally merging two parameters. |
| 259 node.parameters.forEach(write); |
| 260 visitStatement(node.body); |
| 261 } |
| 262 |
| 263 visitConstructorDefinition(ConstructorDefinition node) { |
| 264 node.parameters.forEach(write); |
| 265 node.initializers.forEach(visitInitializer); |
| 266 visitStatement(node.body); |
| 267 } |
| 268 } |
| 269 |
| 270 /// Computes liveness information of the given control-flow graph. |
| 271 /// |
| 272 /// The results are stored in [Block.liveIn] and [Block.liveOut]. |
| 273 void _computeLiveness(List<Block> blocks) { |
| 274 // We use a LIFO queue as worklist. Blocks are given in AST order, so by |
| 275 // inserting them in this order, we initially visit them backwards, which |
| 276 // is a good ordering. |
| 277 // The choice of LIFO for re-inserted blocks is currently arbitrary, |
| 278 List<Block> worklist = new List<Block>.from(blocks); |
| 279 while (!worklist.isEmpty) { |
| 280 Block block = worklist.removeLast(); |
| 281 block.inWorklist = false; |
| 282 |
| 283 bool changed = false; |
| 284 |
| 285 // The liveIn set is computed as: |
| 286 // |
| 287 // liveIn = (liveOut - kill) + gen |
| 288 // |
| 289 // We do the computation in two steps: |
| 290 // |
| 291 // 1. liveIn = gen |
| 292 // 2. liveIn += (liveOut - kill) |
| 293 // |
| 294 // However, since liveIn only grows, and gen never changes, we only have |
| 295 // to do the first step at the first iteration. Moreover, the gen set is |
| 296 // not needed anywhere else, so we don't even need to copy it. |
| 297 if (block.liveIn == null) { |
| 298 block.liveIn = block.gen; |
| 299 block.gen = null; |
| 300 changed = true; |
| 301 } |
| 302 |
| 303 // liveIn += (liveOut - kill) |
| 304 for (Variable variable in block.liveOut) { |
| 305 if (!block.kill.contains(variable)) { |
| 306 if (block.liveIn.add(variable)) { |
| 307 changed = true; |
| 308 } |
| 309 } |
| 310 } |
| 311 |
| 312 // If anything changed, propagate liveness backwards. |
| 313 if (changed) { |
| 314 // Propagate live variables to predecessors. |
| 315 for (Block predecessor in block.predecessors) { |
| 316 int lengthBeforeChange = predecessor.liveOut.length; |
| 317 predecessor.liveOut.addAll(block.liveIn); |
| 318 if (!predecessor.inWorklist && |
| 319 predecessor.liveOut.length != lengthBeforeChange) { |
| 320 worklist.add(predecessor); |
| 321 predecessor.inWorklist = true; |
| 322 } |
| 323 } |
| 324 |
| 325 // Propagate live variables to catch predecessors. |
| 326 for (Block pred in block.catchPredecessors) { |
| 327 bool changed = false; |
| 328 int lengthBeforeChange = pred.liveOut.length; |
| 329 pred.liveOut.addAll(block.liveIn); |
| 330 if (pred.liveOut.length != lengthBeforeChange) { |
| 331 changed = true; |
| 332 } |
| 333 // Assigning to a variable that is live in the catch block, does not |
| 334 // kill the variable, because we conservatively assume that an exception |
| 335 // could be thrown immediately before the assignment. |
| 336 // Therefore remove live variables from all kill sets inside the try. |
| 337 // Since the kill set is only used to subtract live variables from a |
| 338 // set, the analysis remains monotone. |
| 339 lengthBeforeChange = pred.kill.length; |
| 340 pred.kill.removeAll(block.liveIn); |
| 341 if (pred.kill.length != lengthBeforeChange) { |
| 342 changed = true; |
| 343 } |
| 344 if (changed && !pred.inWorklist) { |
| 345 worklist.add(pred); |
| 346 pred.inWorklist = true; |
| 347 } |
| 348 } |
| 349 } |
| 350 } |
| 351 } |
| 352 |
| 353 /// For testing purposes, this flag can be passed to merge variables that |
| 354 /// originated from different source variables. |
| 355 /// |
| 356 /// Correctness should not depend on the fact that we only merge variable |
| 357 /// originating from the same source variable. Setting this flag makes a bug |
| 358 /// more likely to provoke a test case failure. |
| 359 const bool NO_PRESERVE_VARS = const bool.fromEnvironment('NO_PRESERVE_VARS'); |
| 360 |
| 361 /// Based on liveness information, computes a map of variable substitutions to |
| 362 /// merge variables. |
| 363 /// |
| 364 /// Constructs a register interference graph. This is an undirected graph of |
| 365 /// variables, with an edge between two variables if they cannot be merged |
| 366 /// (because they are live simultaneously). |
| 367 /// |
| 368 /// We then compute a graph coloring, where the color of a node denotes which |
| 369 /// variable it will be substituted by. |
| 370 /// |
| 371 /// We never merge variables that originated from distinct source variables, |
| 372 /// so we build a separate register interference graph for each source variable. |
| 373 Map<Variable, Variable> _computeRegisterAllocation(List<Block> blocks) { |
| 374 Map<Variable, Set<Variable>> interference = <Variable, Set<Variable>>{}; |
| 375 |
| 376 /// Group for the given variable. We attempt to merge variables in the same |
| 377 /// group. |
| 378 /// By default, variables are grouped based on their source variable, but |
| 379 /// this can be disabled for testing purposes. |
| 380 Local group(Variable variable) { |
| 381 if (NO_PRESERVE_VARS) { |
| 382 // Parameters may not occur more than once in a parameter list, |
| 383 // so except for parameters, we try to merge all variables. |
| 384 return variable.element is ParameterElement ? variable.element : null; |
| 385 } |
| 386 return variable.element; |
| 387 } |
| 388 |
| 389 Set<Variable> empty = new Set<Variable>(); |
| 390 |
| 391 // At the assignment to a variable x, add an edge to every variable that is |
| 392 // live after the assignment (if it came from the same source variable). |
| 393 for (Block block in blocks) { |
| 394 // Group the liveOut set by source variable. |
| 395 Map<Local, Set<Variable>> liveOut = <Local, Set<Variable>>{}; |
| 396 for (Variable variable in block.liveOut) { |
| 397 liveOut.putIfAbsent( |
| 398 group(variable), |
| 399 () => new Set<Variable>()).add(variable); |
| 400 interference.putIfAbsent(variable, () => new Set<Variable>()); |
| 401 } |
| 402 // Get variables that are live at the catch block. |
| 403 Set<Variable> liveCatch = block.catchBlock != null |
| 404 ? block.catchBlock.liveIn |
| 405 : empty; |
| 406 // Add edges for each variable being assigned here. |
| 407 for (VariableAccess access in block.accesses.reversed) { |
| 408 Variable variable = access.variable; |
| 409 interference.putIfAbsent(variable, () => new Set<Variable>()); |
| 410 Set<Variable> live = |
| 411 liveOut.putIfAbsent(group(variable), () => new Set<Variable>()); |
| 412 if (access.isRead) { |
| 413 live.add(variable); |
| 414 } else { |
| 415 if (!liveCatch.contains(variable)) { |
| 416 // Assignment to a variable that is not live in the catch block. |
| 417 live.remove(variable); |
| 418 } |
| 419 for (Variable other in live) { |
| 420 interference[variable].add(other); |
| 421 interference[other].add(variable); |
| 422 } |
| 423 } |
| 424 } |
| 425 } |
| 426 |
| 427 // Sort the variables by descending degree. |
| 428 // The most constrained variables will be assigned a color first. |
| 429 List<Variable> variables = interference.keys.toList(); |
| 430 variables.sort((x, y) => interference[y].length - interference[x].length); |
| 431 |
| 432 Map<Local, List<Variable>> registers = <Local, List<Variable>>{}; |
| 433 Map<Variable, Variable> subst = <Variable, Variable>{}; |
| 434 |
| 435 for (Variable v1 in variables) { |
| 436 List<Variable> register = registers[group(v1)]; |
| 437 |
| 438 // Optimization: For the first variable in a group, allocate a new color |
| 439 // without iterating over its interference edges. |
| 440 if (register == null) { |
| 441 registers[group(v1)] = <Variable>[v1]; |
| 442 subst[v1] = v1; |
| 443 continue; |
| 444 } |
| 445 |
| 446 // Optimization: If there are no inteference edges for this variable, |
| 447 // assign it the first color without copying the register list. |
| 448 Set<Variable> interferenceSet = interference[v1]; |
| 449 if (interferenceSet.isEmpty) { |
| 450 subst[v1] = register[0]; |
| 451 continue; |
| 452 } |
| 453 |
| 454 // Find an unused color. |
| 455 Set<Variable> potential = new Set<Variable>.from(register); |
| 456 for (Variable v2 in interferenceSet) { |
| 457 Variable v2subst = subst[v2]; |
| 458 if (v2subst != null) { |
| 459 potential.remove(v2subst); |
| 460 if (potential.isEmpty) break; |
| 461 } |
| 462 } |
| 463 |
| 464 if (potential.isEmpty) { |
| 465 // If no free color was found, add this variable as a new color. |
| 466 register.add(v1); |
| 467 subst[v1] = v1; |
| 468 } else { |
| 469 subst[v1] = potential.first; |
| 470 } |
| 471 } |
| 472 |
| 473 return subst; |
| 474 } |
| 475 |
| 476 /// Performs variable substitution and removes redundant assignments. |
| 477 class SubstituteVariables extends RecursiveVisitor { |
| 478 |
| 479 Map<Variable, Variable> mapping; |
| 480 |
| 481 SubstituteVariables(this.mapping); |
| 482 |
| 483 Variable replaceRead(Variable variable) { |
| 484 Variable w = mapping[variable]; |
| 485 if (w == null) return variable; // Skip ignored variables. |
| 486 w.readCount++; |
| 487 variable.readCount--; |
| 488 return w; |
| 489 } |
| 490 |
| 491 Variable replaceWrite(Variable variable) { |
| 492 Variable w = mapping[variable]; |
| 493 if (w == null) return variable; // Skip ignored variables. |
| 494 w.writeCount++; |
| 495 variable.writeCount--; |
| 496 return w; |
| 497 } |
| 498 |
| 499 void replaceParameters(List<Variable> parameters) { |
| 500 for (int i = 0; i < parameters.length; ++i) { |
| 501 parameters[i] = replaceWrite(parameters[i]); |
| 502 } |
| 503 } |
| 504 |
| 505 visitVariableUse(VariableUse node) { |
| 506 node.variable = replaceRead(node.variable); |
| 507 } |
| 508 |
| 509 visitFunctionDefinition(FunctionDefinition node) { |
| 510 replaceParameters(node.parameters); |
| 511 node.body = visitStatement(node.body); |
| 512 } |
| 513 |
| 514 visitConstructorDefinition(ConstructorDefinition node) { |
| 515 replaceParameters(node.parameters); |
| 516 node.initializers.forEach(visitInitializer); |
| 517 node.body = visitStatement(node.body); |
| 518 } |
| 519 |
| 520 visitFieldInitializer(FieldInitializer node) { |
| 521 node.body = visitStatement(node.body); |
| 522 } |
| 523 |
| 524 visitSuperInitializer(SuperInitializer node) { |
| 525 for (int i = 0; i<node.arguments.length; ++i) { |
| 526 node.arguments[i] = visitStatement(node.arguments[i]); |
| 527 } |
| 528 } |
| 529 |
| 530 // Statement visitors should return the transformed statement so we |
| 531 // can remove redundant assignments. |
| 532 Statement visitStatement(Statement node) => super.visitStatement(node); |
| 533 |
| 534 Statement visitAssign(Assign node) { |
| 535 node.variable = replaceWrite(node.variable); |
| 536 |
| 537 visitExpression(node.value); |
| 538 node.next = visitStatement(node.next); |
| 539 |
| 540 // Remove assignments of form "x := x" |
| 541 if (node.value is VariableUse) { |
| 542 VariableUse value = node.value; |
| 543 if (value.variable == node.variable) { |
| 544 value.variable.readCount--; |
| 545 node.variable.writeCount--; |
| 546 return node.next; |
| 547 } |
| 548 } |
| 549 |
| 550 return node; |
| 551 } |
| 552 |
| 553 Statement visitLabeledStatement(LabeledStatement node) { |
| 554 node.body = visitStatement(node.body); |
| 555 node.next = visitStatement(node.next); |
| 556 return node; |
| 557 } |
| 558 |
| 559 Statement visitReturn(Return node) { |
| 560 visitExpression(node.value); |
| 561 return node; |
| 562 } |
| 563 |
| 564 Statement visitBreak(Break node) { |
| 565 return node; |
| 566 } |
| 567 |
| 568 Statement visitContinue(Continue node) { |
| 569 return node; |
| 570 } |
| 571 |
| 572 Statement visitIf(If node) { |
| 573 visitExpression(node.condition); |
| 574 node.thenStatement = visitStatement(node.thenStatement); |
| 575 node.elseStatement = visitStatement(node.elseStatement); |
| 576 return node; |
| 577 } |
| 578 |
| 579 Statement visitWhileTrue(WhileTrue node) { |
| 580 node.body = visitStatement(node.body); |
| 581 return node; |
| 582 } |
| 583 |
| 584 Statement visitWhileCondition(WhileCondition node) { |
| 585 visitExpression(node.condition); |
| 586 node.body = visitStatement(node.body); |
| 587 node.next = visitStatement(node.next); |
| 588 return node; |
| 589 } |
| 590 |
| 591 Statement visitFunctionDeclaration(FunctionDeclaration node) { |
| 592 node.next = visitStatement(node.next); |
| 593 return node; |
| 594 } |
| 595 |
| 596 Statement visitExpressionStatement(ExpressionStatement node) { |
| 597 visitExpression(node.expression); |
| 598 node.next = visitStatement(node.next); |
| 599 return node; |
| 600 } |
| 601 |
| 602 Statement visitTry(Try node) { |
| 603 node.tryBody = visitStatement(node.tryBody); |
| 604 node.catchBody = visitStatement(node.catchBody); |
| 605 return node; |
| 606 } |
| 607 |
| 608 Statement visitSetField(SetField node) { |
| 609 visitExpression(node.object); |
| 610 visitExpression(node.value); |
| 611 node.next = visitStatement(node.next); |
| 612 return node; |
| 613 } |
| 614 } |
| OLD | NEW |