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 library dart2js.cps_ir.type_propagation; | 4 library dart2js.cps_ir.type_propagation; |
5 | 5 |
6 import 'optimizers.dart'; | 6 import 'optimizers.dart'; |
7 | 7 |
8 import '../closure.dart' show | 8 import '../closure.dart' show |
9 ClosureClassElement; | 9 ClosureClassElement; |
10 import '../common.dart'; | 10 import '../common.dart'; |
(...skipping 665 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
676 /// reachable method matching the selector may be targeted. | 676 /// reachable method matching the selector may be targeted. |
677 AbstractConstantValue getInvokeReturnType(Selector selector, TypeMask mask) { | 677 AbstractConstantValue getInvokeReturnType(Selector selector, TypeMask mask) { |
678 return fromMask(typeSystem.getInvokeReturnType(selector, mask)); | 678 return fromMask(typeSystem.getInvokeReturnType(selector, mask)); |
679 } | 679 } |
680 | 680 |
681 AbstractConstantValue fromMask(TypeMask mask) { | 681 AbstractConstantValue fromMask(TypeMask mask) { |
682 ConstantValue constantValue = typeSystem.getConstantOf(mask); | 682 ConstantValue constantValue = typeSystem.getConstantOf(mask); |
683 if (constantValue != null) return constant(constantValue, mask); | 683 if (constantValue != null) return constant(constantValue, mask); |
684 return nonConstant(mask); | 684 return nonConstant(mask); |
685 } | 685 } |
686 | |
687 AbstractConstantValue nonNullable(AbstractConstantValue value) { | |
688 if (value.isNullConstant) return nothing; | |
689 if (!value.isNullable) return value; | |
690 return nonConstant(value.type.nonNullable()); | |
691 } | |
686 } | 692 } |
687 | 693 |
688 /** | 694 /** |
689 * Propagates types (including value types for constants) throughout the IR, and | 695 * Propagates types (including value types for constants) throughout the IR, and |
690 * replaces branches with fixed jumps as well as side-effect free expressions | 696 * replaces branches with fixed jumps as well as side-effect free expressions |
691 * with known constant results. | 697 * with known constant results. |
692 * | 698 * |
693 * Should be followed by the [ShrinkingReducer] pass. | 699 * Should be followed by the [ShrinkingReducer] pass. |
694 * | 700 * |
695 * Implemented according to 'Constant Propagation with Conditional Branches' | 701 * Implemented according to 'Constant Propagation with Conditional Branches' |
(...skipping 385 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1081 /// if (typeof x !== 'number') return x.$lt(); | 1087 /// if (typeof x !== 'number') return x.$lt(); |
1082 /// | 1088 /// |
1083 /// The [guard] must accept all possible non-null values for the receiver, | 1089 /// The [guard] must accept all possible non-null values for the receiver, |
1084 /// but should be as specific as possible so the VM gets more information | 1090 /// but should be as specific as possible so the VM gets more information |
1085 /// from the check. | 1091 /// from the check. |
1086 Primitive guardReceiver(CpsFragment cps, BuiltinOperator guard) { | 1092 Primitive guardReceiver(CpsFragment cps, BuiltinOperator guard) { |
1087 if (guard == null || getValue(node.dartReceiver).isDefinitelyNotNull) { | 1093 if (guard == null || getValue(node.dartReceiver).isDefinitelyNotNull) { |
1088 return node.dartReceiver; | 1094 return node.dartReceiver; |
1089 } | 1095 } |
1090 if (!trustPrimitives) { | 1096 if (!trustPrimitives) { |
1097 // TODO(asgerf): Perhaps a separate optimization should decide that | |
1098 // the guarded check is better based on the type? | |
sra1
2015/12/11 03:34:06
We should also consider generating something like
asgerf
2015/12/11 10:15:40
I see. NullCheck would certainly not be an approp
| |
1091 Primitive check = cps.applyBuiltin(guard, [node.dartReceiver]); | 1099 Primitive check = cps.applyBuiltin(guard, [node.dartReceiver]); |
1092 cps.ifFalsy(check) | 1100 return cps.letPrim(new NullCheck.guarded(check, node.dartReceiver, |
1093 ..invokeMethod(node.dartReceiver, node.selector, | 1101 node.selector, node.sourceInformation)); |
1094 typeSystem.nullType, []) | 1102 } else { |
1095 ..put(new Unreachable()); | 1103 // Refine the receiver to be non-null for use in the operator. |
1104 // This restricts code motion and improves the type computed for the | |
1105 // built-in operator that depends on it. | |
1106 // This must be done even if trusting primitives. | |
1107 return cps.letPrim( | |
1108 new Refinement(node.dartReceiver, typeSystem.nonNullType)); | |
1096 } | 1109 } |
1097 // Refine the receiver to be non-null for use in the operator. | |
1098 // This restricts code motion and improves the type computed for the | |
1099 // built-in operator that depends on it. | |
1100 // This must be done even if trusting primitives. | |
1101 Primitive refined = cps.letPrim( | |
1102 new Refinement(node.dartReceiver, typeSystem.nonNullType)); | |
1103 return refined; | |
1104 } | 1110 } |
1105 | 1111 |
1106 /// Replaces the call with [operator], using the receiver and first argument | 1112 /// Replaces the call with [operator], using the receiver and first argument |
1107 /// as operands (in that order). | 1113 /// as operands (in that order). |
1108 /// | 1114 /// |
1109 /// If [guard] is given, the receiver is checked using [guardReceiver], | 1115 /// If [guard] is given, the receiver is checked using [guardReceiver], |
1110 /// unless it is known kot to be null. | 1116 /// unless it is known kot to be null. |
1111 CpsFragment makeBinary(BuiltinOperator operator, {BuiltinOperator guard}) { | 1117 CpsFragment makeBinary(BuiltinOperator operator, {BuiltinOperator guard}) { |
1112 CpsFragment cps = new CpsFragment(node.sourceInformation); | 1118 CpsFragment cps = new CpsFragment(node.sourceInformation); |
1113 Primitive left = guardReceiver(cps, guard); | 1119 Primitive left = guardReceiver(cps, guard); |
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1157 } | 1163 } |
1158 if (behavesLikeIdentical) { | 1164 if (behavesLikeIdentical) { |
1159 return makeBinary(BuiltinOperator.Identical); | 1165 return makeBinary(BuiltinOperator.Identical); |
1160 } | 1166 } |
1161 } else { | 1167 } else { |
1162 if (lattice.isDefinitelyNum(left, allowNull: true) && | 1168 if (lattice.isDefinitelyNum(left, allowNull: true) && |
1163 lattice.isDefinitelyNum(right, allowNull: trustPrimitives)) { | 1169 lattice.isDefinitelyNum(right, allowNull: trustPrimitives)) { |
1164 // Try to insert a numeric operator. | 1170 // Try to insert a numeric operator. |
1165 BuiltinOperator operator = NumBinaryBuiltins[opname]; | 1171 BuiltinOperator operator = NumBinaryBuiltins[opname]; |
1166 if (operator != null) { | 1172 if (operator != null) { |
1167 return makeBinary(operator, guard: BuiltinOperator.IsNumber); | 1173 return makeBinary(operator, guard: BuiltinOperator.IsNotNumber); |
1168 } | 1174 } |
1169 // Shift operators are not in [NumBinaryBuiltins] because Dart shifts | 1175 // Shift operators are not in [NumBinaryBuiltins] because Dart shifts |
1170 // behave different to JS shifts, especially in the handling of the | 1176 // behave different to JS shifts, especially in the handling of the |
1171 // shift count. | 1177 // shift count. |
1172 // Try to insert a shift-left operator. | 1178 // Try to insert a shift-left operator. |
1173 if (opname == '<<' && | 1179 if (opname == '<<' && |
1174 lattice.isDefinitelyInt(left, allowNull: true) && | 1180 lattice.isDefinitelyInt(left, allowNull: true) && |
1175 lattice.isDefinitelyIntInRange(right, min: 0, max: 31)) { | 1181 lattice.isDefinitelyIntInRange(right, min: 0, max: 31)) { |
1176 return makeBinary(BuiltinOperator.NumShl, | 1182 return makeBinary(BuiltinOperator.NumShl, |
1177 guard: BuiltinOperator.IsNumber); | 1183 guard: BuiltinOperator.IsNotNumber); |
1178 } | 1184 } |
1179 // Try to insert a shift-right operator. JavaScript's right shift is | 1185 // Try to insert a shift-right operator. JavaScript's right shift is |
1180 // consistent with Dart's only for left operands in the unsigned | 1186 // consistent with Dart's only for left operands in the unsigned |
1181 // 32-bit range. | 1187 // 32-bit range. |
1182 if (opname == '>>' && | 1188 if (opname == '>>' && |
1183 lattice.isDefinitelyUint32(left, allowNull: true) && | 1189 lattice.isDefinitelyUint32(left, allowNull: true) && |
1184 lattice.isDefinitelyIntInRange(right, min: 0, max: 31)) { | 1190 lattice.isDefinitelyIntInRange(right, min: 0, max: 31)) { |
1185 return makeBinary(BuiltinOperator.NumShr, | 1191 return makeBinary(BuiltinOperator.NumShr, |
1186 guard: BuiltinOperator.IsNumber); | 1192 guard: BuiltinOperator.IsNotNumber); |
1187 } | 1193 } |
1188 // Try to use remainder for '%'. Both operands must be non-negative | 1194 // Try to use remainder for '%'. Both operands must be non-negative |
1189 // and the divisor must be non-zero. | 1195 // and the divisor must be non-zero. |
1190 if (opname == '%' && | 1196 if (opname == '%' && |
1191 lattice.isDefinitelyUint(left, allowNull: true) && | 1197 lattice.isDefinitelyUint(left, allowNull: true) && |
1192 lattice.isDefinitelyUint(right) && | 1198 lattice.isDefinitelyUint(right) && |
1193 lattice.isDefinitelyIntInRange(right, min: 1)) { | 1199 lattice.isDefinitelyIntInRange(right, min: 1)) { |
1194 return makeBinary(BuiltinOperator.NumRemainder, | 1200 return makeBinary(BuiltinOperator.NumRemainder, |
1195 guard: BuiltinOperator.IsNumber); | 1201 guard: BuiltinOperator.IsNotNumber); |
1196 } | 1202 } |
1197 | 1203 |
1198 if (opname == '~/' && | 1204 if (opname == '~/' && |
1199 lattice.isDefinitelyUint32(left, allowNull: true) && | 1205 lattice.isDefinitelyUint32(left, allowNull: true) && |
1200 lattice.isDefinitelyIntInRange(right, min: 2)) { | 1206 lattice.isDefinitelyIntInRange(right, min: 2)) { |
1201 return makeBinary(BuiltinOperator.NumTruncatingDivideToSigned32, | 1207 return makeBinary(BuiltinOperator.NumTruncatingDivideToSigned32, |
1202 guard: BuiltinOperator.IsNumber); | 1208 guard: BuiltinOperator.IsNotNumber); |
1203 } | 1209 } |
1204 } | 1210 } |
1205 if (lattice.isDefinitelyString(left, allowNull: trustPrimitives) && | 1211 if (lattice.isDefinitelyString(left, allowNull: trustPrimitives) && |
1206 lattice.isDefinitelyString(right, allowNull: trustPrimitives) && | 1212 lattice.isDefinitelyString(right, allowNull: trustPrimitives) && |
1207 opname == '+') { | 1213 opname == '+') { |
1208 // TODO(asgerf): Add IsString builtin so we can use a guard here. | 1214 // TODO(asgerf): Add IsString builtin so we can use a guard here. |
1209 return makeBinary(BuiltinOperator.StringConcatenate); | 1215 return makeBinary(BuiltinOperator.StringConcatenate); |
1210 } | 1216 } |
1211 } | 1217 } |
1212 } | 1218 } |
1213 if (node.selector.isOperator && node.arguments.length == 1) { | 1219 if (node.selector.isOperator && node.arguments.length == 1) { |
1214 Primitive argument = node.dartReceiver; | 1220 Primitive argument = node.dartReceiver; |
1215 AbstractConstantValue value = getValue(argument); | 1221 AbstractConstantValue value = getValue(argument); |
1216 | 1222 |
1217 if (lattice.isDefinitelyNum(value, allowNull: true)) { | 1223 if (lattice.isDefinitelyNum(value, allowNull: true)) { |
1218 String opname = node.selector.name; | 1224 String opname = node.selector.name; |
1219 if (opname == '~') { | 1225 if (opname == '~') { |
1220 return makeUnary(BuiltinOperator.NumBitNot, | 1226 return makeUnary(BuiltinOperator.NumBitNot, |
1221 guard: BuiltinOperator.IsNumber); | 1227 guard: BuiltinOperator.IsNotNumber); |
1222 } | 1228 } |
1223 if (opname == 'unary-') { | 1229 if (opname == 'unary-') { |
1224 return makeUnary(BuiltinOperator.NumNegate, | 1230 return makeUnary(BuiltinOperator.NumNegate, |
1225 guard: BuiltinOperator.IsNumber); | 1231 guard: BuiltinOperator.IsNotNumber); |
1226 } | 1232 } |
1227 } | 1233 } |
1228 } | 1234 } |
1229 if (node.selector.isCall) { | 1235 if (node.selector.isCall) { |
1230 String name = node.selector.name; | 1236 String name = node.selector.name; |
1231 Primitive receiver = node.dartReceiver; | 1237 Primitive receiver = node.dartReceiver; |
1232 AbstractConstantValue receiverValue = getValue(receiver); | 1238 AbstractConstantValue receiverValue = getValue(receiver); |
1233 if (name == 'remainder') { | 1239 if (name == 'remainder') { |
1234 if (node.arguments.length == 2) { | 1240 if (node.arguments.length == 2) { |
1235 Primitive arg = node.dartArgument(0); | 1241 Primitive arg = node.dartArgument(0); |
1236 AbstractConstantValue argValue = getValue(arg); | 1242 AbstractConstantValue argValue = getValue(arg); |
1237 if (lattice.isDefinitelyInt(receiverValue, allowNull: true) && | 1243 if (lattice.isDefinitelyInt(receiverValue, allowNull: true) && |
1238 lattice.isDefinitelyInt(argValue) && | 1244 lattice.isDefinitelyInt(argValue) && |
1239 isIntNotZero(argValue)) { | 1245 isIntNotZero(argValue)) { |
1240 return makeBinary(BuiltinOperator.NumRemainder, | 1246 return makeBinary(BuiltinOperator.NumRemainder, |
1241 guard: BuiltinOperator.IsNumber); | 1247 guard: BuiltinOperator.IsNotNumber); |
1242 } | 1248 } |
1243 } | 1249 } |
1244 } else if (name == 'codeUnitAt') { | 1250 } else if (name == 'codeUnitAt') { |
1245 if (node.arguments.length == 2) { | 1251 if (node.arguments.length == 2) { |
1246 Primitive index = node.dartArgument(0); | 1252 Primitive index = node.dartArgument(0); |
1247 if (lattice.isDefinitelyString(receiverValue) && | 1253 if (lattice.isDefinitelyString(receiverValue) && |
1248 lattice.isDefinitelyInt(getValue(index))) { | 1254 lattice.isDefinitelyInt(getValue(index))) { |
1249 SourceInformation sourceInfo = node.sourceInformation; | 1255 SourceInformation sourceInfo = node.sourceInformation; |
1250 CpsFragment cps = makeBoundsCheck(receiver, index, sourceInfo); | 1256 CpsFragment cps = makeBoundsCheck(receiver, index, sourceInfo); |
1251 ApplyBuiltinOperator get = | 1257 ApplyBuiltinOperator get = |
(...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1315 Primitive isTooSmall = cps.applyBuiltin( | 1321 Primitive isTooSmall = cps.applyBuiltin( |
1316 BuiltinOperator.NumLt, | 1322 BuiltinOperator.NumLt, |
1317 <Primitive>[index, cps.makeZero()]); | 1323 <Primitive>[index, cps.makeZero()]); |
1318 cps.ifTruthy(isTooSmall).invokeContinuation(fail); | 1324 cps.ifTruthy(isTooSmall).invokeContinuation(fail); |
1319 } | 1325 } |
1320 Primitive isTooLarge = cps.applyBuiltin( | 1326 Primitive isTooLarge = cps.applyBuiltin( |
1321 BuiltinOperator.NumGe, | 1327 BuiltinOperator.NumGe, |
1322 <Primitive>[index, cps.letPrim(new GetLength(list))]); | 1328 <Primitive>[index, cps.letPrim(new GetLength(list))]); |
1323 cps.ifTruthy(isTooLarge).invokeContinuation(fail); | 1329 cps.ifTruthy(isTooLarge).invokeContinuation(fail); |
1324 cps.insideContinuation(fail).invokeStaticThrower( | 1330 cps.insideContinuation(fail).invokeStaticThrower( |
1325 helpers.throwIndexOutOfBoundsError, | 1331 helpers.throwIndexOutOfRangeException, |
1326 <Primitive>[list, index]); | 1332 <Primitive>[list, index]); |
1327 return cps; | 1333 return cps; |
1328 } | 1334 } |
1329 | 1335 |
1330 /// Create a check that throws if the length of [list] is not equal to | 1336 /// Create a check that throws if the length of [list] is not equal to |
1331 /// [originalLength]. | 1337 /// [originalLength]. |
1332 /// | 1338 /// |
1333 /// Returns a CPS fragment whose context is the branch where no error | 1339 /// Returns a CPS fragment whose context is the branch where no error |
1334 /// was thrown. | 1340 /// was thrown. |
1335 CpsFragment makeConcurrentModificationCheck(Primitive list, | 1341 CpsFragment makeConcurrentModificationCheck(Primitive list, |
(...skipping 94 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1430 return null; | 1436 return null; |
1431 } | 1437 } |
1432 if (!isExtendable) return null; | 1438 if (!isExtendable) return null; |
1433 CpsFragment cps = new CpsFragment(sourceInfo); | 1439 CpsFragment cps = new CpsFragment(sourceInfo); |
1434 Primitive length = cps.letPrim(new GetLength(list)); | 1440 Primitive length = cps.letPrim(new GetLength(list)); |
1435 Primitive isEmpty = cps.applyBuiltin( | 1441 Primitive isEmpty = cps.applyBuiltin( |
1436 BuiltinOperator.StrictEq, | 1442 BuiltinOperator.StrictEq, |
1437 [length, cps.makeZero()]); | 1443 [length, cps.makeZero()]); |
1438 CpsFragment fail = cps.ifTruthy(isEmpty); | 1444 CpsFragment fail = cps.ifTruthy(isEmpty); |
1439 fail.invokeStaticThrower( | 1445 fail.invokeStaticThrower( |
1440 helpers.throwIndexOutOfBoundsError, | 1446 helpers.throwIndexOutOfRangeException, |
1441 [list, fail.makeConstant(new IntConstantValue(-1))]); | 1447 [list, fail.makeConstant(new IntConstantValue(-1))]); |
1442 Primitive removedItem = cps.invokeBuiltin(BuiltinMethod.Pop, | 1448 Primitive removedItem = cps.invokeBuiltin(BuiltinMethod.Pop, |
1443 list, | 1449 list, |
1444 <Primitive>[]); | 1450 <Primitive>[]); |
1445 removedItem.hint = node.hint; | 1451 removedItem.hint = node.hint; |
1446 node.replaceUsesWith(removedItem); | 1452 node.replaceUsesWith(removedItem); |
1447 return cps; | 1453 return cps; |
1448 | 1454 |
1449 case 'addAll': | 1455 case 'addAll': |
1450 if (!node.selector.isCall || | 1456 if (!node.selector.isCall || |
(...skipping 939 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
2390 functionCompiler.glue.getInterceptedClassesOn(invoke.selector); | 2396 functionCompiler.glue.getInterceptedClassesOn(invoke.selector); |
2391 if (interceptedClasses.contains(helpers.jsDoubleClass)) return false; | 2397 if (interceptedClasses.contains(helpers.jsDoubleClass)) return false; |
2392 if (interceptedClasses.contains(helpers.jsIntClass)) return false; | 2398 if (interceptedClasses.contains(helpers.jsIntClass)) return false; |
2393 continue; | 2399 continue; |
2394 } | 2400 } |
2395 // Other uses need full distinction. | 2401 // Other uses need full distinction. |
2396 return false; | 2402 return false; |
2397 } | 2403 } |
2398 return true; | 2404 return true; |
2399 } | 2405 } |
2406 | |
2407 visitNullCheck(NullCheck node) { | |
2408 if (!getValue(node.value.definition).isNullable) { | |
2409 node.replaceUsesWith(node.value.definition); | |
2410 return new CpsFragment(); | |
2411 } | |
2412 return null; | |
2413 } | |
2400 } | 2414 } |
2401 | 2415 |
2402 /** | 2416 /** |
2403 * Runs an analysis pass on the given function definition in order to detect | 2417 * Runs an analysis pass on the given function definition in order to detect |
2404 * const-ness as well as reachability, both of which are used in the subsequent | 2418 * const-ness as well as reachability, both of which are used in the subsequent |
2405 * transformation pass. | 2419 * transformation pass. |
2406 */ | 2420 */ |
2407 class TypePropagationVisitor implements Visitor { | 2421 class TypePropagationVisitor implements Visitor { |
2408 // The node worklist stores nodes that are both reachable and need to be | 2422 // The node worklist stores nodes that are both reachable and need to be |
2409 // processed, but have not been processed yet. Using a worklist avoids deep | 2423 // processed, but have not been processed yet. Using a worklist avoids deep |
(...skipping 767 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
3177 if (value.isNothing || | 3191 if (value.isNothing || |
3178 typeSystem.areDisjoint(value.type, node.refineType)) { | 3192 typeSystem.areDisjoint(value.type, node.refineType)) { |
3179 setValue(node, nothing); | 3193 setValue(node, nothing); |
3180 } else if (value.isConstant) { | 3194 } else if (value.isConstant) { |
3181 setValue(node, value); | 3195 setValue(node, value); |
3182 } else { | 3196 } else { |
3183 setValue(node, | 3197 setValue(node, |
3184 nonConstant(value.type.intersection(node.refineType, classWorld))); | 3198 nonConstant(value.type.intersection(node.refineType, classWorld))); |
3185 } | 3199 } |
3186 } | 3200 } |
3201 | |
3202 @override | |
3203 void visitNullCheck(NullCheck node) { | |
3204 setValue(node, lattice.nonNullable(getValue(node.value.definition))); | |
3205 } | |
3187 } | 3206 } |
3188 | 3207 |
3189 /// Represents the abstract value of a primitive value at some point in the | 3208 /// Represents the abstract value of a primitive value at some point in the |
3190 /// program. Abstract values of all kinds have a type [T]. | 3209 /// program. Abstract values of all kinds have a type [T]. |
3191 /// | 3210 /// |
3192 /// The different kinds of abstract values represents the knowledge about the | 3211 /// The different kinds of abstract values represents the knowledge about the |
3193 /// constness of the value: | 3212 /// constness of the value: |
3194 /// NOTHING: cannot have any value | 3213 /// NOTHING: cannot have any value |
3195 /// CONSTANT: is a constant. The value is stored in the [constant] field, | 3214 /// CONSTANT: is a constant. The value is stored in the [constant] field, |
3196 /// and the type of the constant is in the [type] field. | 3215 /// and the type of the constant is in the [type] field. |
(...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
3289 processLetPrim(LetPrim node) { | 3308 processLetPrim(LetPrim node) { |
3290 node.primitive.type = null; | 3309 node.primitive.type = null; |
3291 values[node.primitive] = null; | 3310 values[node.primitive] = null; |
3292 } | 3311 } |
3293 | 3312 |
3294 processLetMutable(LetMutable node) { | 3313 processLetMutable(LetMutable node) { |
3295 node.variable.type = null; | 3314 node.variable.type = null; |
3296 values[node.variable] = null; | 3315 values[node.variable] = null; |
3297 } | 3316 } |
3298 } | 3317 } |
OLD | NEW |