Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(552)

Side by Side Diff: pkg/front_end/lib/src/fasta/kernel/kernel_shadow_ast.dart

Issue 2926763003: Add type inference for complex assignments whose LHS is an index expression. (Closed)
Patch Set: Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2017, 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 /// This file declares a "shadow hierarchy" of concrete classes which extend 5 /// This file declares a "shadow hierarchy" of concrete classes which extend
6 /// the kernel class hierarchy, adding methods and fields needed by the 6 /// the kernel class hierarchy, adding methods and fields needed by the
7 /// BodyBuilder. 7 /// BodyBuilder.
8 /// 8 ///
9 /// Instances of these classes may be created using the factory methods in 9 /// Instances of these classes may be created using the factory methods in
10 /// `ast_factory.dart`. 10 /// `ast_factory.dart`.
(...skipping 214 matching lines...) Expand 10 before | Expand all | Expand 10 after
225 while (true) { 225 while (true) {
226 inferrer.inferExpression(section.variable.initializer, null, false); 226 inferrer.inferExpression(section.variable.initializer, null, false);
227 if (section.body is! Let) break; 227 if (section.body is! Let) break;
228 section = section.body; 228 section = section.body;
229 } 229 }
230 inferrer.listener.cascadeExpressionExit(this, lhsType); 230 inferrer.listener.cascadeExpressionExit(this, lhsType);
231 return lhsType; 231 return lhsType;
232 } 232 }
233 } 233 }
234 234
235 /// Concrete shadow object representing a complex assignment in kernel form.
ahe 2017/06/08 14:16:33 What is a complex assignment?
Paul Berry 2017/06/08 16:17:04 I'm still figuring out exactly where to draw the l
236 class KernelComplexAssign extends Let implements KernelExpression {
237 KernelComplexAssign(VariableDeclaration variable, Expression body)
238 : super(variable, body);
239
240 @override
241 void _collectDependencies(KernelDependencyCollector collector) {
242 // Assignment expressions are not immediately evident expressions.
243 collector.recordNotImmediatelyEvident(fileOffset);
244 }
245
246 @override
247 DartType _inferExpression(
248 KernelTypeInferrer inferrer, DartType typeContext, bool typeNeeded) {
249 return inferrer.inferIndexAssign(this, typeContext, typeNeeded);
250 }
251 }
252
235 /// Concrete shadow object representing a conditional expression in kernel form. 253 /// Concrete shadow object representing a conditional expression in kernel form.
236 /// Shadow object for [ConditionalExpression]. 254 /// Shadow object for [ConditionalExpression].
237 class KernelConditionalExpression extends ConditionalExpression 255 class KernelConditionalExpression extends ConditionalExpression
238 implements KernelExpression { 256 implements KernelExpression {
257 /// Indicates whether this conditional expression is associated with a `??=`
258 /// in a null-aware compound assignment.
259 final bool _isNullAwareCombiner;
260
239 KernelConditionalExpression( 261 KernelConditionalExpression(
240 Expression condition, Expression then, Expression otherwise) 262 Expression condition, Expression then, Expression otherwise,
241 : super(condition, then, otherwise, const DynamicType()); 263 {bool isNullAwareCombiner: false})
264 : _isNullAwareCombiner = isNullAwareCombiner,
265 super(condition, then, otherwise, const DynamicType());
242 266
243 @override 267 @override
244 void _collectDependencies(KernelDependencyCollector collector) { 268 void _collectDependencies(KernelDependencyCollector collector) {
245 // Inference dependencies are the union of the inference dependencies of the 269 // Inference dependencies are the union of the inference dependencies of the
246 // two returned sub-expressions. 270 // two returned sub-expressions.
247 collector.collectDependencies(then); 271 collector.collectDependencies(then);
248 collector.collectDependencies(otherwise); 272 collector.collectDependencies(otherwise);
249 } 273 }
250 274
251 @override 275 @override
252 DartType _inferExpression( 276 DartType _inferExpression(
253 KernelTypeInferrer inferrer, DartType typeContext, bool typeNeeded) { 277 KernelTypeInferrer inferrer, DartType typeContext, bool typeNeeded) {
254 typeNeeded = 278 typeNeeded =
255 inferrer.listener.conditionalExpressionEnter(this, typeContext) || 279 inferrer.listener.conditionalExpressionEnter(this, typeContext) ||
256 typeNeeded; 280 typeNeeded;
257 if (!inferrer.isTopLevel) { 281 if (!inferrer.isTopLevel) {
258 inferrer.inferExpression( 282 inferrer.inferExpression(
259 condition, inferrer.coreTypes.boolClass.rawType, false); 283 condition, inferrer.coreTypes.boolClass.rawType, false);
260 } 284 }
261 DartType thenType = inferrer.inferExpression(then, typeContext, true); 285 DartType thenType = inferrer.inferExpression(then, typeContext, true);
262 DartType otherwiseType = 286 DartType otherwiseType =
263 inferrer.inferExpression(otherwise, typeContext, true); 287 inferrer.inferExpression(otherwise, typeContext, true);
264 DartType type = inferrer.typeSchemaEnvironment 288 DartType type = inferrer.typeSchemaEnvironment
265 .getLeastUpperBound(thenType, otherwiseType); 289 .getLeastUpperBound(thenType, otherwiseType);
266 staticType = type; 290 staticType = type;
267 var inferredType = typeNeeded ? type : null; 291 var inferredType = typeNeeded ? type : null;
268 inferrer.listener.conditionalExpressionExit(this, inferredType); 292 inferrer.listener.conditionalExpressionExit(this, inferredType);
269 return inferredType; 293 return inferredType;
270 } 294 }
295
296 /// Helper method allowing [_isNullAwareCombiner] to be checked from outside
297 /// this library without adding a public member to the class.
298 static bool isNullAwareCombiner(KernelConditionalExpression e) =>
299 e._isNullAwareCombiner;
271 } 300 }
272 301
273 /// Shadow object for [ConstructorInvocation]. 302 /// Shadow object for [ConstructorInvocation].
274 class KernelConstructorInvocation extends ConstructorInvocation 303 class KernelConstructorInvocation extends ConstructorInvocation
275 implements KernelExpression { 304 implements KernelExpression {
276 final Member _initialTarget; 305 final Member _initialTarget;
277 306
278 KernelConstructorInvocation( 307 KernelConstructorInvocation(
279 Constructor target, this._initialTarget, Arguments arguments, 308 Constructor target, this._initialTarget, Arguments arguments,
280 {bool isConst: false}) 309 {bool isConst: false})
(...skipping 750 matching lines...) Expand 10 before | Expand all | Expand 10 after
1031 } 1060 }
1032 } 1061 }
1033 1062
1034 /// Shadow object for [MethodInvocation]. 1063 /// Shadow object for [MethodInvocation].
1035 class KernelMethodInvocation extends MethodInvocation 1064 class KernelMethodInvocation extends MethodInvocation
1036 implements KernelExpression { 1065 implements KernelExpression {
1037 /// Indicates whether this method invocation is a call to a `call` method 1066 /// Indicates whether this method invocation is a call to a `call` method
1038 /// resulting from the invocation of a function expression. 1067 /// resulting from the invocation of a function expression.
1039 final bool _isImplicitCall; 1068 final bool _isImplicitCall;
1040 1069
1070 /// Indicates whether this method invocation invokes the operator that
1071 /// combines old and new values in a compound assignment.
1072 final bool _isCombiner;
1073
1041 KernelMethodInvocation(Expression receiver, Name name, Arguments arguments, 1074 KernelMethodInvocation(Expression receiver, Name name, Arguments arguments,
1042 {bool isImplicitCall: false, Procedure interfaceTarget}) 1075 {bool isImplicitCall: false,
1076 Procedure interfaceTarget,
1077 bool isCombiner: false})
1043 : _isImplicitCall = isImplicitCall, 1078 : _isImplicitCall = isImplicitCall,
1079 _isCombiner = isCombiner,
1044 super(receiver, name, arguments, interfaceTarget); 1080 super(receiver, name, arguments, interfaceTarget);
1045 1081
1046 @override 1082 @override
1047 void _collectDependencies(KernelDependencyCollector collector) { 1083 void _collectDependencies(KernelDependencyCollector collector) {
1048 // The inference dependencies are the inference dependencies of the 1084 // The inference dependencies are the inference dependencies of the
1049 // receiver. 1085 // receiver.
1050 collector.collectDependencies(receiver); 1086 collector.collectDependencies(receiver);
1051 } 1087 }
1052 1088
1053 @override 1089 @override
1054 DartType _inferExpression( 1090 DartType _inferExpression(
1055 KernelTypeInferrer inferrer, DartType typeContext, bool typeNeeded) { 1091 KernelTypeInferrer inferrer, DartType typeContext, bool typeNeeded) {
1092 if (identical(name.name, '[]=')) {
1093 return inferrer.inferIndexAssign(this, typeContext, typeNeeded);
1094 }
1056 typeNeeded = inferrer.listener.methodInvocationEnter(this, typeContext) || 1095 typeNeeded = inferrer.listener.methodInvocationEnter(this, typeContext) ||
1057 typeNeeded; 1096 typeNeeded;
1058 // First infer the receiver so we can look up the method that was invoked. 1097 // First infer the receiver so we can look up the method that was invoked.
1059 var receiverType = inferrer.inferExpression(receiver, null, true); 1098 var receiverType = inferrer.inferExpression(receiver, null, true);
1060 bool isOverloadedArithmeticOperator = false; 1099 bool isOverloadedArithmeticOperator = false;
1061 Member interfaceMember; 1100 Member interfaceMember =
1062 if (receiverType is InterfaceType) { 1101 inferrer.findMethodInvocationMember(receiverType, this);
1063 interfaceMember = inferrer.classHierarchy 1102 if (interfaceMember is Procedure) {
1064 .getInterfaceMember(receiverType.classNode, name); 1103 isOverloadedArithmeticOperator = inferrer.typeSchemaEnvironment
1065 // Our non-strong golden files currently don't include interface 1104 .isOverloadedArithmeticOperator(interfaceMember);
1066 // targets, so we can't store the interface target without causing tests
1067 // to fail. TODO(paulberry): fix this.
1068 if (inferrer.strongMode) {
1069 if (interfaceMember != null) {
1070 inferrer.instrumentation?.record(Uri.parse(inferrer.uri), fileOffset,
1071 'target', new InstrumentationValueForMember(interfaceMember));
1072 }
1073 // interfaceTarget is currently required to be a procedure, so we skip
1074 // if it's anything else. TODO(paulberry): fix this - see
1075 // https://codereview.chromium.org/2923653003/.
1076 if (interfaceMember is Procedure) {
1077 interfaceTarget = interfaceMember;
1078 }
1079 }
1080 if (interfaceMember is Procedure) {
1081 isOverloadedArithmeticOperator = inferrer.typeSchemaEnvironment
1082 .isOverloadedArithmeticOperator(interfaceMember);
1083 }
1084 } 1105 }
1085 var calleeType = inferrer.getCalleeFunctionType( 1106 var calleeType = inferrer.getCalleeFunctionType(
1086 interfaceMember, receiverType, name, !_isImplicitCall); 1107 interfaceMember, receiverType, name, !_isImplicitCall);
1087 var inferredType = inferrer.inferInvocation(typeContext, typeNeeded, 1108 var inferredType = inferrer.inferInvocation(typeContext, typeNeeded,
1088 fileOffset, calleeType, calleeType.returnType, arguments, 1109 fileOffset, calleeType, calleeType.returnType, arguments,
1089 isOverloadedArithmeticOperator: isOverloadedArithmeticOperator, 1110 isOverloadedArithmeticOperator: isOverloadedArithmeticOperator,
1090 receiverType: receiverType); 1111 receiverType: receiverType);
1091 inferrer.listener.methodInvocationExit(this, inferredType); 1112 inferrer.listener.methodInvocationExit(this, inferredType);
1092 return inferredType; 1113 return inferredType;
1093 } 1114 }
1115
1116 /// Helper method allowing [_isCombiner] to be checked from outside this
1117 /// library without adding a public member to the class.
1118 static bool isCombiner(KernelMethodInvocation e) => e._isCombiner;
1094 } 1119 }
1095 1120
1096 /// Shadow object for [Not]. 1121 /// Shadow object for [Not].
1097 class KernelNot extends Not implements KernelExpression { 1122 class KernelNot extends Not implements KernelExpression {
1098 KernelNot(Expression operand) : super(operand); 1123 KernelNot(Expression operand) : super(operand);
1099 1124
1100 @override 1125 @override
1101 void _collectDependencies(KernelDependencyCollector collector) { 1126 void _collectDependencies(KernelDependencyCollector collector) {
1102 // No inference dependencies. 1127 // No inference dependencies.
1103 } 1128 }
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after
1156 // field dependencies. So we don't need to do anything here. 1181 // field dependencies. So we don't need to do anything here.
1157 } 1182 }
1158 1183
1159 @override 1184 @override
1160 DartType _inferExpression( 1185 DartType _inferExpression(
1161 KernelTypeInferrer inferrer, DartType typeContext, bool typeNeeded) { 1186 KernelTypeInferrer inferrer, DartType typeContext, bool typeNeeded) {
1162 typeNeeded = 1187 typeNeeded =
1163 inferrer.listener.propertyGetEnter(this, typeContext) || typeNeeded; 1188 inferrer.listener.propertyGetEnter(this, typeContext) || typeNeeded;
1164 // First infer the receiver so we can look up the getter that was invoked. 1189 // First infer the receiver so we can look up the getter that was invoked.
1165 var receiverType = inferrer.inferExpression(receiver, null, true); 1190 var receiverType = inferrer.inferExpression(receiver, null, true);
1166 Member interfaceMember; 1191 Member interfaceMember =
1167 if (receiverType is InterfaceType) { 1192 inferrer.findInterfaceMember(receiverType, name, fileOffset);
1168 interfaceMember = inferrer.classHierarchy 1193 if (inferrer.isTopLevel &&
1169 .getInterfaceMember(receiverType.classNode, name); 1194 ((interfaceMember is Procedure &&
1170 if (inferrer.isTopLevel && 1195 interfaceMember.kind == ProcedureKind.Getter) ||
1171 ((interfaceMember is Procedure && 1196 interfaceMember is Field)) {
1172 interfaceMember.kind == ProcedureKind.Getter) || 1197 // References to fields and getters can't be relied upon for top level
1173 interfaceMember is Field)) { 1198 // inference.
1174 // References to fields and getters can't be relied upon for top level 1199 inferrer.recordNotImmediatelyEvident(fileOffset);
1175 // inference.
1176 inferrer.recordNotImmediatelyEvident(fileOffset);
1177 }
1178 // Our non-strong golden files currently don't include interface targets,
1179 // so we can't store the interface target without causing tests to fail.
1180 // TODO(paulberry): fix this.
1181 if (inferrer.strongMode) {
1182 inferrer.instrumentation?.record(Uri.parse(inferrer.uri), fileOffset,
1183 'target', new InstrumentationValueForMember(interfaceMember));
1184 interfaceTarget = interfaceMember;
1185 }
1186 } 1200 }
1201 interfaceTarget = interfaceMember;
1187 var inferredType = 1202 var inferredType =
1188 inferrer.getCalleeType(interfaceMember, receiverType, name); 1203 inferrer.getCalleeType(interfaceMember, receiverType, name);
1189 // TODO(paulberry): Infer tear-off type arguments if appropriate. 1204 // TODO(paulberry): Infer tear-off type arguments if appropriate.
1190 inferrer.listener.propertyGetExit(this, inferredType); 1205 inferrer.listener.propertyGetExit(this, inferredType);
1191 return typeNeeded ? inferredType : null; 1206 return typeNeeded ? inferredType : null;
1192 } 1207 }
1193 } 1208 }
1194 1209
1195 /// Shadow object for [PropertyGet]. 1210 /// Shadow object for [PropertyGet].
1196 class KernelPropertySet extends PropertySet implements KernelExpression { 1211 class KernelPropertySet extends PropertySet implements KernelExpression {
(...skipping 11 matching lines...) Expand all
1208 collector.recordNotImmediatelyEvident(fileOffset); 1223 collector.recordNotImmediatelyEvident(fileOffset);
1209 } 1224 }
1210 1225
1211 @override 1226 @override
1212 DartType _inferExpression( 1227 DartType _inferExpression(
1213 KernelTypeInferrer inferrer, DartType typeContext, bool typeNeeded) { 1228 KernelTypeInferrer inferrer, DartType typeContext, bool typeNeeded) {
1214 typeNeeded = 1229 typeNeeded =
1215 inferrer.listener.propertySetEnter(this, typeContext) || typeNeeded; 1230 inferrer.listener.propertySetEnter(this, typeContext) || typeNeeded;
1216 // First infer the receiver so we can look up the setter that was invoked. 1231 // First infer the receiver so we can look up the setter that was invoked.
1217 var receiverType = inferrer.inferExpression(receiver, null, true); 1232 var receiverType = inferrer.inferExpression(receiver, null, true);
1218 Member interfaceMember; 1233 Member interfaceMember = inferrer
1219 if (receiverType is InterfaceType) { 1234 .findInterfaceMember(receiverType, name, fileOffset, setter: true);
1220 interfaceMember = inferrer.classHierarchy 1235 interfaceTarget = interfaceMember;
1221 .getInterfaceMember(receiverType.classNode, name, setter: true);
1222 // Our non-strong golden files currently don't include interface targets,
1223 // so we can't store the interface target without causing tests to fail.
1224 // TODO(paulberry): fix this.
1225 if (inferrer.strongMode) {
1226 inferrer.instrumentation?.record(Uri.parse(inferrer.uri), fileOffset,
1227 'target', new InstrumentationValueForMember(interfaceMember));
1228 interfaceTarget = interfaceMember;
1229 }
1230 }
1231 var setterType = inferrer.getSetterType(interfaceMember, receiverType); 1236 var setterType = inferrer.getSetterType(interfaceMember, receiverType);
1232 var inferredType = inferrer.inferExpression(value, setterType, typeNeeded); 1237 var inferredType = inferrer.inferExpression(value, setterType, typeNeeded);
1233 inferrer.listener.propertySetExit(this, inferredType); 1238 inferrer.listener.propertySetExit(this, inferredType);
1234 return typeNeeded ? inferredType : null; 1239 return typeNeeded ? inferredType : null;
1235 } 1240 }
1236 } 1241 }
1237 1242
1238 /// Concrete shadow object representing a redirecting initializer in kernel 1243 /// Concrete shadow object representing a redirecting initializer in kernel
1239 /// form. 1244 /// form.
1240 class KernelRedirectingInitializer extends RedirectingInitializer 1245 class KernelRedirectingInitializer extends RedirectingInitializer
(...skipping 549 matching lines...) Expand 10 before | Expand all | Expand 10 after
1790 final bool _implicitlyTyped; 1795 final bool _implicitlyTyped;
1791 1796
1792 final int _functionNestingLevel; 1797 final int _functionNestingLevel;
1793 1798
1794 bool _mutatedInClosure = false; 1799 bool _mutatedInClosure = false;
1795 1800
1796 bool _mutatedAnywhere = false; 1801 bool _mutatedAnywhere = false;
1797 1802
1798 final bool _isLocalFunction; 1803 final bool _isLocalFunction;
1799 1804
1805 /// Indicates whether this variable declaration exists for the sole purpose of
1806 /// discarding a return value in a complex desugared expression.
1807 final bool _isDiscarding;
ahe 2017/06/08 14:16:33 It would be easier for for me to understand the me
Paul Berry 2017/06/08 16:17:04 I'm open to changing this, but I think "voidContex
1808
1800 KernelVariableDeclaration(String name, this._functionNestingLevel, 1809 KernelVariableDeclaration(String name, this._functionNestingLevel,
1801 {Expression initializer, 1810 {Expression initializer,
1802 DartType type, 1811 DartType type,
1803 bool isFinal: false, 1812 bool isFinal: false,
1804 bool isConst: false, 1813 bool isConst: false,
1805 bool isLocalFunction: false}) 1814 bool isLocalFunction: false})
1806 : _implicitlyTyped = type == null, 1815 : _implicitlyTyped = type == null,
1807 _isLocalFunction = isLocalFunction, 1816 _isLocalFunction = isLocalFunction,
1817 _isDiscarding = false,
1808 super(name, 1818 super(name,
1809 initializer: initializer, 1819 initializer: initializer,
1810 type: type ?? const DynamicType(), 1820 type: type ?? const DynamicType(),
1811 isFinal: isFinal, 1821 isFinal: isFinal,
1812 isConst: isConst); 1822 isConst: isConst);
1813 1823
1814 KernelVariableDeclaration.forValue( 1824 KernelVariableDeclaration.forValue(
1815 Expression initializer, this._functionNestingLevel) 1825 Expression initializer, this._functionNestingLevel,
1826 {bool isDiscarding: false})
1816 : _implicitlyTyped = true, 1827 : _implicitlyTyped = true,
1817 _isLocalFunction = false, 1828 _isLocalFunction = false,
1829 _isDiscarding = isDiscarding,
1818 super.forValue(initializer); 1830 super.forValue(initializer);
1819 1831
1820 @override 1832 @override
1821 void _inferStatement(KernelTypeInferrer inferrer) { 1833 void _inferStatement(KernelTypeInferrer inferrer) {
1822 inferrer.listener.variableDeclarationEnter(this); 1834 inferrer.listener.variableDeclarationEnter(this);
1823 var declaredType = _implicitlyTyped ? null : type; 1835 var declaredType = _implicitlyTyped ? null : type;
1824 if (initializer != null) { 1836 if (initializer != null) {
1825 var inferredType = inferrer.inferDeclarationType(inferrer.inferExpression( 1837 var inferredType = inferrer.inferDeclarationType(inferrer.inferExpression(
1826 initializer, declaredType, _implicitlyTyped)); 1838 initializer, declaredType, _implicitlyTyped));
1827 if (inferrer.strongMode && _implicitlyTyped) { 1839 if (inferrer.strongMode && _implicitlyTyped) {
1828 inferrer.instrumentation?.record(Uri.parse(inferrer.uri), fileOffset, 1840 inferrer.instrumentation?.record(Uri.parse(inferrer.uri), fileOffset,
1829 'type', new InstrumentationValueForType(inferredType)); 1841 'type', new InstrumentationValueForType(inferredType));
1830 type = inferredType; 1842 type = inferredType;
1831 } 1843 }
1832 } 1844 }
1833 inferrer.listener.variableDeclarationExit(this); 1845 inferrer.listener.variableDeclarationExit(this);
1834 } 1846 }
1835 1847
1848 /// Helper method allowing [_isDiscarding] to be checked from outside this
1849 /// library without adding a public member to the class.
1850 static bool isDiscarding(VariableDeclaration v) =>
1851 v is KernelVariableDeclaration && v._isDiscarding;
1852
1836 /// Determine whether the given [KernelVariableDeclaration] had an implicit 1853 /// Determine whether the given [KernelVariableDeclaration] had an implicit
1837 /// type. 1854 /// type.
1838 /// 1855 ///
1839 /// This is static to avoid introducing a method that would be visible to 1856 /// This is static to avoid introducing a method that would be visible to
1840 /// the kernel. 1857 /// the kernel.
1841 static bool isImplicitlyTyped(KernelVariableDeclaration variable) => 1858 static bool isImplicitlyTyped(KernelVariableDeclaration variable) =>
1842 variable._implicitlyTyped; 1859 variable._implicitlyTyped;
1843 } 1860 }
1844 1861
1845 /// Concrete shadow object representing a read from a variable in kernel form. 1862 /// Concrete shadow object representing a read from a variable in kernel form.
(...skipping 95 matching lines...) Expand 10 before | Expand all | Expand 10 after
1941 } 1958 }
1942 1959
1943 transformChildren(v) { 1960 transformChildren(v) {
1944 return internalError("Internal error: Unsupported operation."); 1961 return internalError("Internal error: Unsupported operation.");
1945 } 1962 }
1946 1963
1947 visitChildren(v) { 1964 visitChildren(v) {
1948 return internalError("Internal error: Unsupported operation."); 1965 return internalError("Internal error: Unsupported operation.");
1949 } 1966 }
1950 } 1967 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698