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 import 'optimizers.dart'; | 5 import 'optimizers.dart'; |
6 | 6 |
7 import '../constants/constant_system.dart'; | 7 import '../constants/constant_system.dart'; |
8 import '../resolution/operators.dart'; | 8 import '../resolution/operators.dart'; |
9 import '../constants/values.dart'; | 9 import '../constants/values.dart'; |
10 import '../dart_types.dart' as types; | 10 import '../dart_types.dart' as types; |
(...skipping 537 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
548 */ | 548 */ |
549 class TransformingVisitor extends LeafVisitor { | 549 class TransformingVisitor extends LeafVisitor { |
550 final TypePropagationVisitor analyzer; | 550 final TypePropagationVisitor analyzer; |
551 final Map<Expression, ConstantValue> replacements; | 551 final Map<Expression, ConstantValue> replacements; |
552 final ConstantPropagationLattice lattice; | 552 final ConstantPropagationLattice lattice; |
553 final dart2js.Compiler compiler; | 553 final dart2js.Compiler compiler; |
554 | 554 |
555 JavaScriptBackend get backend => compiler.backend; | 555 JavaScriptBackend get backend => compiler.backend; |
556 TypeMaskSystem get typeSystem => lattice.typeSystem; | 556 TypeMaskSystem get typeSystem => lattice.typeSystem; |
557 types.DartTypes get dartTypes => lattice.dartTypes; | 557 types.DartTypes get dartTypes => lattice.dartTypes; |
558 Set<Node> get reachable => analyzer.reachableNodes; | |
559 Map<Node, AbstractValue> get values => analyzer.values; | 558 Map<Node, AbstractValue> get values => analyzer.values; |
560 | 559 |
561 final dart2js.InternalErrorFunction internalError; | 560 final dart2js.InternalErrorFunction internalError; |
562 | 561 |
563 final List<Node> stack = <Node>[]; | 562 final List<Node> stack = <Node>[]; |
564 | 563 |
565 TransformingVisitor(this.compiler, | 564 TransformingVisitor(this.compiler, |
566 this.lattice, | 565 this.lattice, |
567 this.analyzer, | 566 this.analyzer, |
568 this.replacements, | 567 this.replacements, |
(...skipping 875 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1444 case AbstractBool.True: | 1443 case AbstractBool.True: |
1445 // Cast always succeeds, replace it with InvokeContinuation. | 1444 // Cast always succeeds, replace it with InvokeContinuation. |
1446 InvokeContinuation invoke = | 1445 InvokeContinuation invoke = |
1447 new InvokeContinuation(cont, <Primitive>[node.value.definition]); | 1446 new InvokeContinuation(cont, <Primitive>[node.value.definition]); |
1448 replaceSubtree(node, invoke); | 1447 replaceSubtree(node, invoke); |
1449 push(invoke); | 1448 push(invoke); |
1450 return; | 1449 return; |
1451 | 1450 |
1452 case AbstractBool.False: | 1451 case AbstractBool.False: |
1453 // Cast always fails, remove unreachable continuation body. | 1452 // Cast always fails, remove unreachable continuation body. |
1454 assert(!reachable.contains(cont)); | |
1455 replaceSubtree(cont.body, new Unreachable()); | 1453 replaceSubtree(cont.body, new Unreachable()); |
1456 break; | 1454 break; |
1457 } | 1455 } |
1458 } | 1456 } |
1459 | 1457 |
1460 /// Specialize calls to internal static methods. | 1458 /// Specialize calls to internal static methods. |
1461 /// | 1459 /// |
1462 /// Returns true if the call was replaced. | 1460 /// Returns true if the call was replaced. |
1463 bool specializeInternalMethodCall(InvokeStatic node) { | 1461 bool specializeInternalMethodCall(InvokeStatic node) { |
1464 // TODO(asgerf): This is written to easily scale to more cases, | 1462 // TODO(asgerf): This is written to easily scale to more cases, |
(...skipping 161 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1626 // The node worklist stores nodes that are both reachable and need to be | 1624 // The node worklist stores nodes that are both reachable and need to be |
1627 // processed, but have not been processed yet. Using a worklist avoids deep | 1625 // processed, but have not been processed yet. Using a worklist avoids deep |
1628 // recursion. | 1626 // recursion. |
1629 // The node worklist and the reachable set operate in concert: nodes are | 1627 // The node worklist and the reachable set operate in concert: nodes are |
1630 // only ever added to the worklist when they have not yet been marked as | 1628 // only ever added to the worklist when they have not yet been marked as |
1631 // reachable, and adding a node to the worklist is always followed by marking | 1629 // reachable, and adding a node to the worklist is always followed by marking |
1632 // it reachable. | 1630 // it reachable. |
1633 // TODO(jgruber): Storing reachability per-edge instead of per-node would | 1631 // TODO(jgruber): Storing reachability per-edge instead of per-node would |
1634 // allow for further optimizations. | 1632 // allow for further optimizations. |
1635 final List<Node> nodeWorklist = <Node>[]; | 1633 final List<Node> nodeWorklist = <Node>[]; |
1636 final Set<Node> reachableNodes = new Set<Node>(); | 1634 final Set<Continuation> reachableContinuations = new Set<Continuation>(); |
1637 | 1635 |
1638 // The definition workset stores all definitions which need to be reprocessed | 1636 // The definition workset stores all definitions which need to be reprocessed |
1639 // since their lattice value has changed. | 1637 // since their lattice value has changed. |
1640 final Set<Definition> defWorkset = new Set<Definition>(); | 1638 final List<Definition> defWorklist = <Definition>[]; |
1641 | 1639 |
1642 final ConstantPropagationLattice lattice; | 1640 final ConstantPropagationLattice lattice; |
1643 final dart2js.InternalErrorFunction internalError; | 1641 final dart2js.InternalErrorFunction internalError; |
1644 | 1642 |
1645 TypeMaskSystem get typeSystem => lattice.typeSystem; | 1643 TypeMaskSystem get typeSystem => lattice.typeSystem; |
1646 | 1644 |
1647 AbstractValue get nothing => lattice.nothing; | 1645 AbstractValue get nothing => lattice.nothing; |
1648 | 1646 |
1649 AbstractValue nonConstant([TypeMask type]) => lattice.nonConstant(type); | 1647 AbstractValue nonConstant([TypeMask type]) => lattice.nonConstant(type); |
1650 | 1648 |
1651 AbstractValue constantValue(ConstantValue constant, [TypeMask type]) { | 1649 AbstractValue constantValue(ConstantValue constant, [TypeMask type]) { |
1652 return lattice.constant(constant, type); | 1650 return lattice.constant(constant, type); |
1653 } | 1651 } |
1654 | 1652 |
1655 // Stores the current lattice value for primitives and mutable variables. | 1653 // Stores the current lattice value for primitives and mutable variables. |
1656 // Access through [getValue] and [setValue]. | 1654 // Access through [getValue] and [setValue]. |
1657 final Map<Definition, AbstractValue> values; | 1655 final Map<Definition, AbstractValue> values; |
1658 | 1656 |
1659 /// Expressions that invoke their call continuation with a constant value | 1657 /// Expressions that invoke their call continuation with a constant value |
1660 /// and without any side effects. These can be replaced by the constant. | 1658 /// and without any side effects. These can be replaced by the constant. |
1661 final Map<Expression, ConstantValue> replacements; | 1659 final Map<Expression, ConstantValue> replacements; |
1662 | 1660 |
1663 TypePropagationVisitor(this.lattice, | 1661 TypePropagationVisitor(this.lattice, |
1664 this.values, | 1662 this.values, |
1665 this.replacements, | 1663 this.replacements, |
1666 this.internalError); | 1664 this.internalError); |
1667 | 1665 |
1668 void analyze(FunctionDefinition root) { | 1666 void analyze(FunctionDefinition root) { |
1669 reachableNodes.clear(); | 1667 reachableContinuations.clear(); |
1670 | 1668 |
1671 // Initially, only the root node is reachable. | 1669 // Initially, only the root node is reachable. |
1672 setReachable(root); | 1670 push(root); |
1673 | 1671 |
1674 iterateWorklist(); | 1672 iterateWorklist(); |
1675 } | 1673 } |
1676 | 1674 |
1677 void reanalyzeSubtree(Node node) { | 1675 void reanalyzeSubtree(Node node) { |
1678 new ResetAnalysisInfo(reachableNodes, values).visit(node); | 1676 new ResetAnalysisInfo(reachableContinuations, values).visit(node); |
1679 setReachable(node); | 1677 push(node); |
1680 iterateWorklist(); | 1678 iterateWorklist(); |
1681 } | 1679 } |
1682 | 1680 |
1683 void iterateWorklist() { | 1681 void iterateWorklist() { |
1684 while (true) { | 1682 while (true) { |
1685 if (nodeWorklist.isNotEmpty) { | 1683 if (nodeWorklist.isNotEmpty) { |
1686 // Process a new reachable expression. | 1684 // Process a new reachable expression. |
1687 Node node = nodeWorklist.removeLast(); | 1685 Node node = nodeWorklist.removeLast(); |
1688 visit(node); | 1686 visit(node); |
1689 } else if (defWorkset.isNotEmpty) { | 1687 } else if (defWorklist.isNotEmpty) { |
1690 // Process all usages of a changed definition. | 1688 // Process all usages of a changed definition. |
1691 Definition def = defWorkset.first; | 1689 Definition def = defWorklist.removeLast(); |
1692 defWorkset.remove(def); | |
1693 | 1690 |
1694 // Visit all uses of this definition. This might add new entries to | 1691 // Visit all uses of this definition. This might add new entries to |
1695 // [nodeWorklist], for example by visiting a newly-constant usage within | 1692 // [nodeWorklist], for example by visiting a newly-constant usage within |
1696 // a branch node. | 1693 // a branch node. |
1697 for (Reference ref = def.firstRef; ref != null; ref = ref.next) { | 1694 for (Reference ref = def.firstRef; ref != null; ref = ref.next) { |
1698 visit(ref.parent); | 1695 visit(ref.parent); |
1699 } | 1696 } |
1700 } else { | 1697 } else { |
1701 break; // Both worklists empty. | 1698 break; // Both worklists empty. |
1702 } | 1699 } |
1703 } | 1700 } |
1704 } | 1701 } |
1705 | 1702 |
| 1703 /// Adds [node] to the worklist. |
| 1704 void push(Node node) { |
| 1705 nodeWorklist.add(node); |
| 1706 } |
| 1707 |
1706 /// If the passed node is not yet reachable, mark it reachable and add it | 1708 /// If the passed node is not yet reachable, mark it reachable and add it |
1707 /// to the work list. | 1709 /// to the work list. |
1708 void setReachable(Node node) { | 1710 void setReachable(Continuation cont) { |
1709 if (!reachableNodes.contains(node)) { | 1711 if (reachableContinuations.add(cont)) { |
1710 reachableNodes.add(node); | 1712 push(cont); |
1711 nodeWorklist.add(node); | |
1712 } | 1713 } |
1713 } | 1714 } |
1714 | 1715 |
1715 /// Returns the lattice value corresponding to [node], defaulting to nothing. | 1716 /// Returns the lattice value corresponding to [node], defaulting to nothing. |
1716 /// | 1717 /// |
1717 /// Never returns null. | 1718 /// Never returns null. |
1718 AbstractValue getValue(Definition node) { | 1719 AbstractValue getValue(Definition node) { |
1719 AbstractValue value = values[node]; | 1720 AbstractValue value = values[node]; |
1720 return (value == null) ? nothing : value; | 1721 return (value == null) ? nothing : value; |
1721 } | 1722 } |
1722 | 1723 |
1723 /// Joins the passed lattice [updateValue] to the current value of [node], | 1724 /// Joins the passed lattice [updateValue] to the current value of [node], |
1724 /// and adds it to the definition work set if it has changed and [node] is | 1725 /// and adds it to the definition work set if it has changed and [node] is |
1725 /// a definition. | 1726 /// a definition. |
1726 void setValue(Definition node, AbstractValue updateValue) { | 1727 void setValue(Definition node, AbstractValue updateValue) { |
1727 AbstractValue oldValue = getValue(node); | 1728 AbstractValue oldValue = getValue(node); |
1728 AbstractValue newValue = lattice.join(oldValue, updateValue); | 1729 AbstractValue newValue = lattice.join(oldValue, updateValue); |
1729 if (oldValue == newValue) { | 1730 if (oldValue == newValue) { |
1730 return; | 1731 return; |
1731 } | 1732 } |
1732 | 1733 |
1733 // Values may only move in the direction NOTHING -> CONSTANT -> NONCONST. | 1734 // Values may only move in the direction NOTHING -> CONSTANT -> NONCONST. |
1734 assert(newValue.kind >= oldValue.kind); | 1735 assert(newValue.kind >= oldValue.kind); |
1735 | 1736 |
1736 values[node] = newValue; | 1737 values[node] = newValue; |
1737 defWorkset.add(node); | 1738 defWorklist.add(node); |
1738 } | 1739 } |
1739 | 1740 |
1740 // -------------------------- Visitor overrides ------------------------------ | 1741 // -------------------------- Visitor overrides ------------------------------ |
1741 void visit(Node node) { node.accept(this); } | 1742 void visit(Node node) { node.accept(this); } |
1742 | 1743 |
1743 void visitFunctionDefinition(FunctionDefinition node) { | 1744 void visitFunctionDefinition(FunctionDefinition node) { |
1744 if (node.thisParameter != null) { | 1745 if (node.thisParameter != null) { |
1745 setValue(node.thisParameter, | 1746 setValue(node.thisParameter, |
1746 nonConstant(typeSystem.getReceiverType(node.element))); | 1747 nonConstant(typeSystem.getReceiverType(node.element))); |
1747 } | 1748 } |
1748 node.parameters.forEach(visit); | 1749 node.parameters.forEach(visit); |
1749 setReachable(node.body); | 1750 push(node.body); |
1750 } | 1751 } |
1751 | 1752 |
1752 void visitLetPrim(LetPrim node) { | 1753 void visitLetPrim(LetPrim node) { |
1753 visit(node.primitive); // No reason to delay visits to primitives. | 1754 visit(node.primitive); // No reason to delay visits to primitives. |
1754 setReachable(node.body); | 1755 push(node.body); |
1755 } | 1756 } |
1756 | 1757 |
1757 void visitLetCont(LetCont node) { | 1758 void visitLetCont(LetCont node) { |
1758 // The continuation is only marked as reachable on use. | 1759 // The continuation is only marked as reachable on use. |
1759 setReachable(node.body); | 1760 push(node.body); |
1760 } | 1761 } |
1761 | 1762 |
1762 void visitLetHandler(LetHandler node) { | 1763 void visitLetHandler(LetHandler node) { |
1763 setReachable(node.body); | 1764 push(node.body); |
1764 // The handler is assumed to be reachable (we could instead treat it as | 1765 // The handler is assumed to be reachable (we could instead treat it as |
1765 // unreachable unless we find something reachable that might throw in the | 1766 // unreachable unless we find something reachable that might throw in the |
1766 // body --- it's not clear if we want to do that here or in some other | 1767 // body --- it's not clear if we want to do that here or in some other |
1767 // pass). The handler parameters are assumed to be unknown. | 1768 // pass). The handler parameters are assumed to be unknown. |
1768 // | 1769 // |
1769 // TODO(kmillikin): we should set the type of the exception and stack | 1770 // TODO(kmillikin): we should set the type of the exception and stack |
1770 // trace here. The way we do that depends on how we handle 'on T' catch | 1771 // trace here. The way we do that depends on how we handle 'on T' catch |
1771 // clauses. | 1772 // clauses. |
1772 setReachable(node.handler); | 1773 setReachable(node.handler); |
1773 for (Parameter param in node.handler.parameters) { | 1774 for (Parameter param in node.handler.parameters) { |
1774 setValue(param, nonConstant()); | 1775 setValue(param, nonConstant()); |
1775 } | 1776 } |
1776 } | 1777 } |
1777 | 1778 |
1778 void visitLetMutable(LetMutable node) { | 1779 void visitLetMutable(LetMutable node) { |
1779 setValue(node.variable, getValue(node.value.definition)); | 1780 setValue(node.variable, getValue(node.value.definition)); |
1780 setReachable(node.body); | 1781 push(node.body); |
1781 } | 1782 } |
1782 | 1783 |
1783 void visitInvokeStatic(InvokeStatic node) { | 1784 void visitInvokeStatic(InvokeStatic node) { |
1784 Continuation cont = node.continuation.definition; | 1785 Continuation cont = node.continuation.definition; |
1785 setReachable(cont); | 1786 setReachable(cont); |
1786 | 1787 |
1787 assert(cont.parameters.length == 1); | 1788 assert(cont.parameters.length == 1); |
1788 Parameter returnValue = cont.parameters[0]; | 1789 Parameter returnValue = cont.parameters[0]; |
1789 | 1790 |
1790 /// Sets the value of the target continuation parameter, and possibly | 1791 /// Sets the value of the target continuation parameter, and possibly |
(...skipping 295 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2086 ConstantValue value = node.value; | 2087 ConstantValue value = node.value; |
2087 if (value.isDummy || !value.isConstant) { | 2088 if (value.isDummy || !value.isConstant) { |
2088 // TODO(asgerf): Explain how this happens and why we don't want them. | 2089 // TODO(asgerf): Explain how this happens and why we don't want them. |
2089 setValue(node, nonConstant(typeSystem.getTypeOf(value))); | 2090 setValue(node, nonConstant(typeSystem.getTypeOf(value))); |
2090 } else { | 2091 } else { |
2091 setValue(node, constantValue(value, typeSystem.getTypeOf(value))); | 2092 setValue(node, constantValue(value, typeSystem.getTypeOf(value))); |
2092 } | 2093 } |
2093 } | 2094 } |
2094 | 2095 |
2095 void visitCreateFunction(CreateFunction node) { | 2096 void visitCreateFunction(CreateFunction node) { |
2096 setReachable(node.definition); | 2097 throw 'CreateFunction is not used'; |
2097 ConstantValue constant = | |
2098 new FunctionConstantValue(node.definition.element); | |
2099 setValue(node, constantValue(constant, typeSystem.functionType)); | |
2100 } | 2098 } |
2101 | 2099 |
2102 void visitGetMutable(GetMutable node) { | 2100 void visitGetMutable(GetMutable node) { |
2103 setValue(node, getValue(node.variable.definition)); | 2101 setValue(node, getValue(node.variable.definition)); |
2104 } | 2102 } |
2105 | 2103 |
2106 void visitMutableVariable(MutableVariable node) { | 2104 void visitMutableVariable(MutableVariable node) { |
2107 // [MutableVariable]s are bound either as parameters to | 2105 // [MutableVariable]s are bound either as parameters to |
2108 // [FunctionDefinition]s, by [LetMutable]. | 2106 // [FunctionDefinition]s, by [LetMutable]. |
2109 if (node.parent is FunctionDefinition) { | 2107 if (node.parent is FunctionDefinition) { |
(...skipping 26 matching lines...) Expand all Loading... |
2136 // some other abstract value than non-constant. | 2134 // some other abstract value than non-constant. |
2137 } else { | 2135 } else { |
2138 internalError(node.hint, "Unexpected parent of Parameter: ${node.parent}")
; | 2136 internalError(node.hint, "Unexpected parent of Parameter: ${node.parent}")
; |
2139 } | 2137 } |
2140 } | 2138 } |
2141 | 2139 |
2142 void visitContinuation(Continuation node) { | 2140 void visitContinuation(Continuation node) { |
2143 node.parameters.forEach(visit); | 2141 node.parameters.forEach(visit); |
2144 | 2142 |
2145 if (node.body != null) { | 2143 if (node.body != null) { |
2146 setReachable(node.body); | 2144 push(node.body); |
2147 } | 2145 } |
2148 } | 2146 } |
2149 | 2147 |
2150 void visitGetStatic(GetStatic node) { | 2148 void visitGetStatic(GetStatic node) { |
2151 if (node.element.isFunction) { | 2149 if (node.element.isFunction) { |
2152 setValue(node, nonConstant(typeSystem.functionType)); | 2150 setValue(node, nonConstant(typeSystem.functionType)); |
2153 } else { | 2151 } else { |
2154 setValue(node, nonConstant(typeSystem.getFieldType(node.element))); | 2152 setValue(node, nonConstant(typeSystem.getFieldType(node.element))); |
2155 } | 2153 } |
2156 } | 2154 } |
2157 | 2155 |
2158 void visitSetStatic(SetStatic node) {} | 2156 void visitSetStatic(SetStatic node) {} |
2159 | 2157 |
2160 void visitGetLazyStatic(GetLazyStatic node) { | 2158 void visitGetLazyStatic(GetLazyStatic node) { |
2161 Continuation cont = node.continuation.definition; | 2159 Continuation cont = node.continuation.definition; |
2162 setReachable(cont); | 2160 setReachable(cont); |
2163 | 2161 |
2164 assert(cont.parameters.length == 1); | 2162 assert(cont.parameters.length == 1); |
2165 Parameter returnValue = cont.parameters[0]; | 2163 Parameter returnValue = cont.parameters[0]; |
2166 setValue(returnValue, nonConstant(typeSystem.getFieldType(node.element))); | 2164 setValue(returnValue, nonConstant(typeSystem.getFieldType(node.element))); |
2167 } | 2165 } |
2168 | 2166 |
2169 void visitIsTrue(IsTrue node) { | 2167 void visitIsTrue(IsTrue node) { |
2170 Branch branch = node.parent; | 2168 Branch branch = node.parent; |
2171 visitBranch(branch); | 2169 visitBranch(branch); |
2172 } | 2170 } |
2173 | 2171 |
2174 void visitInterceptor(Interceptor node) { | 2172 void visitInterceptor(Interceptor node) { |
2175 setReachable(node.input.definition); | 2173 push(node.input.definition); |
2176 AbstractValue value = getValue(node.input.definition); | 2174 AbstractValue value = getValue(node.input.definition); |
2177 if (!value.isNothing) { | 2175 if (!value.isNothing) { |
2178 setValue(node, nonConstant(typeSystem.nonNullType)); | 2176 setValue(node, nonConstant(typeSystem.nonNullType)); |
2179 } | 2177 } |
2180 } | 2178 } |
2181 | 2179 |
2182 void visitGetField(GetField node) { | 2180 void visitGetField(GetField node) { |
2183 setValue(node, nonConstant(typeSystem.getFieldType(node.field))); | 2181 setValue(node, nonConstant(typeSystem.getFieldType(node.field))); |
2184 } | 2182 } |
2185 | 2183 |
(...skipping 142 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2328 String get name => 'current'; | 2326 String get name => 'current'; |
2329 } | 2327 } |
2330 | 2328 |
2331 /// Suggested name for the original length of a list, for use in checks | 2329 /// Suggested name for the original length of a list, for use in checks |
2332 /// for concurrent modification. | 2330 /// for concurrent modification. |
2333 class OriginalLengthEntity extends Entity { | 2331 class OriginalLengthEntity extends Entity { |
2334 String get name => 'length'; | 2332 String get name => 'length'; |
2335 } | 2333 } |
2336 | 2334 |
2337 class ResetAnalysisInfo extends RecursiveVisitor { | 2335 class ResetAnalysisInfo extends RecursiveVisitor { |
2338 Set<Node> reachableNodes; | 2336 Set<Continuation> reachableContinuations; |
2339 Map<Definition, AbstractValue> values; | 2337 Map<Definition, AbstractValue> values; |
2340 | 2338 |
2341 ResetAnalysisInfo(this.reachableNodes, this.values); | 2339 ResetAnalysisInfo(this.reachableContinuations, this.values); |
2342 | 2340 |
2343 visit(Node node) { | 2341 processContinuation(Continuation cont) { |
2344 reachableNodes.remove(node); | 2342 reachableContinuations.remove(cont); |
2345 if (node is Definition) values.remove(node); | 2343 cont.parameters.forEach(values.remove); |
2346 node.accept(this); | 2344 } |
| 2345 |
| 2346 processLetPrim(LetPrim node) { |
| 2347 values.remove(node.primitive); |
| 2348 } |
| 2349 |
| 2350 processLetMutable(LetMutable node) { |
| 2351 values.remove(node.variable); |
2347 } | 2352 } |
2348 } | 2353 } |
OLD | NEW |