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

Side by Side Diff: frog/leg/resolver.dart

Issue 9243011: Implement named constructors and resolving of redirecting constructors and super-initializers. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Move constructor name creation to lookup function. Created 8 years, 11 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 | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2011, 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 class TreeElements { 5 class TreeElements {
6 Map<Node, Element> map; 6 Map<Node, Element> map;
7 TreeElements() : map = new LinkedHashMap<Node, Element>(); 7 TreeElements() : map = new LinkedHashMap<Node, Element>();
8 operator []=(Node node, Element element) => map[node] = element; 8 operator []=(Node node, Element element) => map[node] = element;
9 operator [](Node node) => map[node]; 9 operator [](Node node) => map[node];
10 } 10 }
(...skipping 24 matching lines...) Expand all
35 } 35 }
36 36
37 TreeElements resolveMethodElement(FunctionElement element) { 37 TreeElements resolveMethodElement(FunctionElement element) {
38 FunctionExpression tree = element.parseNode(compiler, compiler); 38 FunctionExpression tree = element.parseNode(compiler, compiler);
39 // TODO(ahe): Can this be cleaned up to use resolveSignature? 39 // TODO(ahe): Can this be cleaned up to use resolveSignature?
40 ResolverVisitor visitor = new SignatureResolverVisitor(compiler, element); 40 ResolverVisitor visitor = new SignatureResolverVisitor(compiler, element);
41 visitor.visit(tree); 41 visitor.visit(tree);
42 42
43 visitor = new FullResolverVisitor.from(visitor); 43 visitor = new FullResolverVisitor.from(visitor);
44 if (tree.initializers != null) { 44 if (tree.initializers != null) {
45 resolveInitializers(element, tree, visitor); 45 new InitializerResolver(visitor, element).resolveInitializers(tree);
46 } 46 }
47 visitor.visit(tree.body); 47 visitor.visit(tree.body);
48 48
49 // Resolve the type annotations encountered in the method. 49 // Resolve the type annotations encountered in the method.
50 while (!toResolve.isEmpty()) { 50 while (!toResolve.isEmpty()) {
51 toResolve.removeFirst().resolve(compiler); 51 toResolve.removeFirst().resolve(compiler);
52 } 52 }
53 return visitor.mapping; 53 return visitor.mapping;
54 } 54 }
55 55
56 TreeElements resolveFieldElement(Element element) { 56 TreeElements resolveFieldElement(Element element) {
57 Node tree = element.parseNode(compiler, compiler); 57 Node tree = element.parseNode(compiler, compiler);
58 ResolverVisitor visitor = new FullResolverVisitor(compiler, element); 58 ResolverVisitor visitor = new FullResolverVisitor(compiler, element);
59 if (tree is SendSet) { 59 if (tree is SendSet) {
60 SendSet send = tree; 60 SendSet send = tree;
61 visitor.visit(send.arguments.head); 61 visitor.visit(send.arguments.head);
62 } 62 }
63 return visitor.mapping; 63 return visitor.mapping;
64 } 64 }
65 65
66 bool isInitializer(SendSet node) {
67 if (node.selector.asIdentifier() == null) return false;
68 if (node.receiver == null) return true;
69 if (node.receiver.asIdentifier() == null) return false;
70 return node.receiver.asIdentifier().isThis();
71 }
72
73 SourceString getInitializerFieldName(SendSet node, onError(node)) {
74 if (!isInitializer(node)) onError(node);
75 return node.selector.asIdentifier().source;
76 }
77
78 void resolveInitializers(Element element, FunctionExpression node,
79 ResolverVisitor visitor) {
80 void onError(node) {
81 visitor.error(node, MessageKind.INVALID_RECEIVER_IN_INITIALIZER);
82 }
83 Map<SourceString, Node> initialized = new Map<SourceString, Node>();
84 for (Link<Node> link = node.initializers.nodes;
85 !link.isEmpty();
86 link = link.tail) {
87 if (link.head.asSendSet() != null) {
88 SendSet init = link.head;
89 SourceString name = getInitializerFieldName(init, onError);
90 ClassElement classElement = element.enclosingElement;
91 Element target = classElement.lookupLocalMember(name);
92 Node selector = init.selector;
93 if (target == null) {
94 visitor.error(selector, MessageKind.CANNOT_RESOLVE, [name]);
95 } else if (target.kind != ElementKind.FIELD) {
96 visitor.error(selector, MessageKind.NOT_A_FIELD, [name]);
97 } else if (!target.isInstanceMember()) {
98 visitor.error(selector, MessageKind.INIT_STATIC_FIELD, [name]);
99 }
100 visitor.useElement(init, target);
101 if (initialized.containsKey(name)) {
102 visitor.error(init, MessageKind.DUPLICATE_INITIALIZER, [name]);
103 visitor.warning(initialized[name], MessageKind.ALREADY_INITIALIZED,
104 [name]);
105 }
106 initialized[name] = init;
107 Node value = init.arguments.head;
108 visitor.visitInStaticContext(value);
109 } else if (link.head.asSend() !== null) {
110 // TODO(karlklose): super(...), this(...).
111 compiler.cancel('uniplemented', node:link.head);
112 } else {
113 compiler.cancel('internal error: invalid initializer',
114 node: link.head);
115 }
116 }
117 }
118
119 void resolveType(ClassElement element) { 66 void resolveType(ClassElement element) {
120 measure(() { 67 measure(() {
121 ClassNode tree = element.parseNode(compiler, compiler); 68 ClassNode tree = element.parseNode(compiler, compiler);
122 ClassResolverVisitor visitor = new ClassResolverVisitor(compiler); 69 ClassResolverVisitor visitor = new ClassResolverVisitor(compiler);
123 visitor.visit(tree); 70 visitor.visit(tree);
124 }); 71 });
125 } 72 }
126 73
127 void resolveSignature(FunctionElement element) { 74 void resolveSignature(FunctionElement element) {
128 measure(() { 75 measure(() {
129 FunctionExpression node = element.parseNode(compiler, compiler); 76 FunctionExpression node = element.parseNode(compiler, compiler);
130 SignatureResolverVisitor visitor = 77 SignatureResolverVisitor visitor =
131 new SignatureResolverVisitor(compiler, element); 78 new SignatureResolverVisitor(compiler, element);
132 visitor.visitFunctionExpression(node); 79 visitor.visitFunctionExpression(node);
133 }); 80 });
134 } 81 }
135 } 82 }
136 83
84
85 class InitializerResolver {
86 final ResolverVisitor visitor;
87 final FunctionElement constructor;
88 final Map<SourceString, Node> initialized;
89 Link<Node> initializers;
90 bool hasSuper;
91
92 bool isSuperConstructorCall(Send node) {
93 return (node.receiver === null &&
94 node.selector.asIdentifier() !== null &&
95 node.selector.asIdentifier().isSuper()) ||
96 (node.receiver !== null &&
97 node.receiver.asIdentifier() !== null &&
98 node.receiver.asIdentifier().isSuper() &&
99 node.selector.asIdentifier() !== null);
100 }
101
102 bool isConstructorRedirect(Send node) {
103 return (node.receiver === null &&
104 node.selector.asIdentifier() !== null &&
105 node.selector.asIdentifier().isThis()) ||
106 (node.receiver !== null &&
107 node.receiver.asIdentifier() !== null &&
108 node.receiver.asIdentifier().isThis() &&
109 node.selector.asIdentifier() !== null);
110 }
111
112 InitializerResolver(this.visitor, this.constructor)
113 : initialized = new Map<SourceString, Node>(), hasSuper = false;
114
115 error(Node node, MessageKind kind, [arguments = const []]) {
116 visitor.error(node, kind, arguments);
117 }
118
119 warning(Node node, MessageKind kind, [arguments = const []]) {
120 visitor.warning(node, kind, arguments);
121 }
122
123 bool isFieldInitializer(SendSet node) {
124 if (node.selector.asIdentifier() == null) return false;
125 if (node.receiver == null) return true;
126 if (node.receiver.asIdentifier() == null) return false;
127 return node.receiver.asIdentifier().isThis();
128 }
129
130 void resolveFieldInitializer(SendSet init) {
131 // init is of the form [this.]field = value.
132 final Node selector = init.selector;
133 final SourceString name = selector.asIdentifier().source;
134 // Lookup target field.
135 Element target;
136 if (isFieldInitializer(init)) {
137 final ClassElement classElement = constructor.enclosingElement;
138 target = classElement.lookupLocalMember(name);
139 if (target === null) {
140 error(selector, MessageKind.CANNOT_RESOLVE, [name]);
141 } else if (target.kind != ElementKind.FIELD) {
142 error(selector, MessageKind.NOT_A_FIELD, [name]);
143 } else if (!target.isInstanceMember()) {
144 error(selector, MessageKind.INIT_STATIC_FIELD, [name]);
145 }
146 } else {
147 error(init, MessageKind.INVALID_RECEIVER_IN_INITIALIZER);
148 }
149 visitor.useElement(init, target);
150 // Check for duplicate initializers.
151 if (initialized.containsKey(name)) {
152 error(init, MessageKind.DUPLICATE_INITIALIZER, [name]);
153 warning(initialized[name], MessageKind.ALREADY_INITIALIZED, [name]);
154 }
155 initialized[name] = init;
156 // Resolve initializing value.
157 visitor.visitInStaticContext(init.arguments.head);
158 }
159
160 SourceString getConstructorName(Send node) {
161 if (node.receiver !== null) {
162 return node.selector.asIdentifier().source;
163 } else {
164 return const SourceString('');
165 }
166 }
167
168 void resolveSuperOrThis(Send call) {
169 noConstructor(e) {
170 if (e !== null) error(call, MessageKind.NO_CONSTRUCTOR, [e.name, e.kind]);
171 }
172
173 ClassElement lookupTarget = constructor.enclosingElement;
174 bool validTarget = true;
175 if (isSuperConstructorCall(call)) {
176 // Check for invalid initializers.
177 if (hasSuper) {
178 error(call, MessageKind.DUPLICATE_SUPER_INITIALIZER);
179 }
180 hasSuper = true;
181 // Calculate correct lookup target and constructor name.
182 if (lookupTarget.name == Types.OBJECT) {
183 error(call, MessageKind.SUPER_INITIALIZER_IN_OBJECT);
184 } else {
185 lookupTarget = lookupTarget.supertype.element;
186 }
187 } else if (isConstructorRedirect(call)) {
188 // Check that there are no other initializers.
189 if (!initializers.tail.isEmpty()) {
190 error(call, MessageKind.REDIRECTING_CTOR_HAS_INITIALIZER);
191 }
192 } else {
193 visitor.error(call, MessageKind.CONSTRUCTOR_CALL_EXPECTED);
194 validTarget = false;
195 }
196
197 if (validTarget) {
198 final SourceString className = lookupTarget.name;
199 final SourceString constructorName = getConstructorName(call);
200 FunctionElement target =
201 lookupTarget.lookupConstructor(className, constructorName,
202 noConstructor);
203 if (target === null && call.arguments.isEmpty()) {
204 target = lookupTarget.getSynthesizedConstructor();
205 }
206 if (target === null) {
207 String name = (constructorName === const SourceString(''))
208 ? className.stringValue
209 : "$className.$constructorName";
210 error(call, MessageKind.CANNOT_RESOLVE_CONSTRUCTOR, [name]);
211 } else {
212 final Compiler compiler = visitor.compiler;
213 // TODO(karlklose): support optional arguments.
214 if (target.parameterCount(compiler) != call.argumentCount()) {
215 error(call, MessageKind.NO_MATCHING_CONSTRUCTOR);
216 }
217 }
218 visitor.useElement(call, target);
219 }
220 // Resolve the arguments of the call.
221 for (Link<Node> arguments = call.arguments;
222 !arguments.isEmpty();
223 arguments = arguments.tail) {
224 visitor.visitInStaticContext(arguments.head);
225 }
226 }
227
228 void resolveInitializers(FunctionExpression node) {
229 if (node.initializers === null) return;
230 initializers = node.initializers.nodes;
231 Compiler compiler = visitor.compiler;
232 for (Link<Node> link = initializers;
233 !link.isEmpty();
234 link = link.tail) {
235 if (link.head.asSendSet() != null) {
236 final SendSet init = link.head.asSendSet();
237 resolveFieldInitializer(init);
238 } else if (link.head.asSend() !== null) {
239 final Send call = link.head.asSend();
240 resolveSuperOrThis(call);
241 } else {
242 visitor.compiler.cancel('internal error: invalid initializer',
243 node: link.head);
244 }
245 }
246 }
247 }
248
249
137 // TODO(ahe): Frog cannot handle generic types. 250 // TODO(ahe): Frog cannot handle generic types.
138 class ResolverVisitor extends AbstractVisitor/*<Element>*/ { 251 class ResolverVisitor extends AbstractVisitor/*<Element>*/ {
139 final Compiler compiler; 252 final Compiler compiler;
140 final TreeElements mapping; 253 final TreeElements mapping;
141 final Element enclosingElement; 254 final Element enclosingElement;
142 bool inInstanceContext; 255 bool inInstanceContext;
143 Scope context; 256 Scope context;
144 ClassElement currentClass; 257 ClassElement currentClass;
145 bool typeRequired = false; 258 bool typeRequired = false;
146 259
(...skipping 26 matching lines...) Expand all
173 compiler.reportWarning(node, warning); 286 compiler.reportWarning(node, warning);
174 } 287 }
175 288
176 cancel(Node node, String message) { 289 cancel(Node node, String message) {
177 compiler.cancel(message, node: node); 290 compiler.cancel(message, node: node);
178 } 291 }
179 292
180 Element lookup(Node node, SourceString name) { 293 Element lookup(Node node, SourceString name) {
181 Element result = context.lookup(name); 294 Element result = context.lookup(name);
182 if (!inInstanceContext && result != null && result.isInstanceMember()) { 295 if (!inInstanceContext && result != null && result.isInstanceMember()) {
183 error(node, MessageKind.NOT_STATIC, [node]); 296 error(node, MessageKind.NO_INSTANCE_AVAILABLE, [node]);
184 } 297 }
185 return result; 298 return result;
186 } 299 }
187 300
188 visitInStaticContext(Node node) { 301 visitInStaticContext(Node node) {
189 bool wasInstanceContext = inInstanceContext; 302 bool wasInstanceContext = inInstanceContext;
190 inInstanceContext = false; 303 inInstanceContext = false;
191 visit(node); 304 visit(node);
192 inInstanceContext = wasInstanceContext; 305 inInstanceContext = wasInstanceContext;
193 } 306 }
194 307
195 visit(Node node) { 308 visit(Node node) {
196 if (node == null) return null; 309 if (node == null) return null;
197 return node.accept(this); 310 return node.accept(this);
198 } 311 }
199 312
200 visitIdentifier(Identifier node) { 313 visitIdentifier(Identifier node) {
201 if (node.isThis()) { 314 if (node.isThis()) {
202 if (!inInstanceContext) error(node, MessageKind.NO_THIS_IN_STATIC); 315 if (!inInstanceContext) {
316 error(node, MessageKind.NO_INSTANCE_AVAILABLE, [node]);
317 }
203 return null; 318 return null;
204 } else if (node.isSuper()) { 319 } else if (node.isSuper()) {
205 if (!inInstanceContext) error(node, MessageKind.NO_SUPER_IN_STATIC); 320 if (!inInstanceContext) error(node, MessageKind.NO_SUPER_IN_STATIC);
206 return null; 321 return null;
207 } else { 322 } else {
208 Element element = lookup(node, node.source); 323 Element element = lookup(node, node.source);
209 if (element == null) { 324 if (element == null) {
210 error(node, MessageKind.CANNOT_RESOLVE, [node]); 325 error(node, MessageKind.CANNOT_RESOLVE, [node]);
211 } 326 }
212 return useElement(node, element); 327 return useElement(node, element);
213 } 328 }
214 } 329 }
215 330
216 visitTypeAnnotation(TypeAnnotation node) { 331 visitTypeAnnotation(TypeAnnotation node) {
217 Identifier name = node.typeName.asIdentifier(); 332 SourceString className;
218 if (name === null) { 333 if (node.typeName.asSend() !== null) {
219 // TODO(karlklose): In progress. 334 // In new and const expressions, the type name can be a Send to
220 cancel(node.typeName, "not implemented"); 335 // denote named constructors or library prefixes.
336 Send send = node.typeName.asSend();
337 className = send.receiver.asIdentifier().source;
338 } else {
339 className = node.typeName.asIdentifier().source;
221 } 340 }
222 if (name.source == const SourceString('var')) return null; 341 if (className == const SourceString('var')) return null;
223 if (name.source == const SourceString('void')) return null; 342 if (className == const SourceString('void')) return null;
224 Element element = context.lookup(name.source); 343 Element element = context.lookup(className);
225 if (element === null) { 344 if (element === null) {
226 if (typeRequired) { 345 if (typeRequired) {
227 error(node, MessageKind.CANNOT_RESOLVE_TYPE, [name]); 346 error(node, MessageKind.CANNOT_RESOLVE_TYPE, [className]);
228 } else { 347 } else {
229 warning(node, MessageKind.CANNOT_RESOLVE_TYPE, [name]); 348 warning(node, MessageKind.CANNOT_RESOLVE_TYPE, [className]);
230 } 349 }
231 } else if (element.kind !== ElementKind.CLASS) { 350 } else if (element.kind !== ElementKind.CLASS) {
232 if (typeRequired) { 351 if (typeRequired) {
233 error(node, MessageKind.NOT_A_TYPE, [name]); 352 error(node, MessageKind.NOT_A_TYPE, [className]);
234 } else { 353 } else {
235 warning(node, MessageKind.NOT_A_TYPE, [name]); 354 warning(node, MessageKind.NOT_A_TYPE, [className]);
236 } 355 }
237 } else { 356 } else {
238 ClassElement cls = element; 357 ClassElement cls = element;
239 compiler.resolver.toResolve.add(element); 358 compiler.resolver.toResolve.add(element);
240 // TODO(ahe): This should be a Type. 359 // TODO(ahe): This should be a Type.
241 useElement(node, element); 360 useElement(node, element);
242 } 361 }
243 return element; 362 return element;
244 } 363 }
245 364
(...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after
326 visitFor(For node) { 445 visitFor(For node) {
327 Scope scope = new BlockScope(context); 446 Scope scope = new BlockScope(context);
328 visitIn(node.initializer, scope); 447 visitIn(node.initializer, scope);
329 visitIn(node.condition, scope); 448 visitIn(node.condition, scope);
330 visitIn(node.update, scope); 449 visitIn(node.update, scope);
331 visitIn(node.body, scope); 450 visitIn(node.body, scope);
332 } 451 }
333 452
334 visitFunctionExpression(FunctionExpression node) { 453 visitFunctionExpression(FunctionExpression node) {
335 visit(node.returnType); 454 visit(node.returnType);
455 SourceString name;
336 if (node.name === null) { 456 if (node.name === null) {
337 cancel(node, "anonymous functions are not implemented"); 457 cancel(node, "anonymous functions are not implemented");
338 } 458 }
339 if (node.name.asIdentifier() === null) { 459 name = node.name.asIdentifier().source;
340 cancel(node.name, "named constructors are not implemented");
341 }
342 FunctionElement enclosingElement = new FunctionElement.node( 460 FunctionElement enclosingElement = new FunctionElement.node(
343 node, ElementKind.FUNCTION, null, context.element); 461 name, node, ElementKind.FUNCTION, null, context.element);
344 defineElement(node, enclosingElement); 462 defineElement(node, enclosingElement);
345 context = new MethodScope(context, enclosingElement); 463 context = new MethodScope(context, enclosingElement);
346 464
347 // TODO(ahe): Can this be cleaned up to use resolveSignature? 465 // TODO(ahe): Can this be cleaned up to use resolveSignature?
348 ParametersVisitor visitor = new ParametersVisitor(this); 466 ParametersVisitor visitor = new ParametersVisitor(this);
349 visitor.visit(node.parameters); 467 visitor.visit(node.parameters);
350 enclosingElement.parameters = visitor.elements.toLink(); 468 enclosingElement.parameters = visitor.elements.toLink();
351 469
352 visit(node.body); 470 visit(node.body);
353 context = context.parent; 471 context = context.parent;
(...skipping 156 matching lines...) Expand 10 before | Expand all | Expand 10 after
510 628
511 visitParenthesizedExpression(ParenthesizedExpression node) { 629 visitParenthesizedExpression(ParenthesizedExpression node) {
512 visit(node.expression); 630 visit(node.expression);
513 } 631 }
514 632
515 visitNewExpression(NewExpression node) { 633 visitNewExpression(NewExpression node) {
516 if (node.isConst()) cancel(node, 'const expressions are not implemented'); 634 if (node.isConst()) cancel(node, 'const expressions are not implemented');
517 635
518 visit(node.send.argumentsNode); 636 visit(node.send.argumentsNode);
519 637
638 SourceString constructorName;
639 Node typeName = node.send.selector.asTypeAnnotation().typeName;
640 if (typeName.asSend() !== null) {
641 Identifier receiver = typeName.asSend().receiver.asIdentifier();
642 Identifier selector = typeName.asSend().selector.asIdentifier();
643 SourceString className = receiver.source;
644 SourceString name = selector.source;
645 constructorName = new SourceString('$className.$name');
646 } else {
647 constructorName = typeName.asIdentifier().source;
648 }
520 ClassElement cls = resolveTypeRequired(node.send.selector); 649 ClassElement cls = resolveTypeRequired(node.send.selector);
521 Element constructor = null; 650 Element constructor = null;
522 if (cls !== null) { 651 if (cls !== null) {
523 // TODO(ngeoffray): set constructor-name correctly. 652 constructor = cls.resolve(compiler).lookupConstructor(constructorName);
524 SourceString name = cls.name; 653 if (constructorName == cls.name
525 constructor = cls.resolve(compiler).lookupConstructor(name);
526 if (name == cls.name
527 && constructor === null 654 && constructor === null
528 && node.send.argumentsNode.isEmpty()) { 655 && node.send.argumentsNode.isEmpty()) {
529 constructor = cls.getSynthesizedConstructor(); 656 constructor = cls.getSynthesizedConstructor();
530 } 657 }
531 if (constructor === null) { 658 if (constructor === null) {
532 error(node, MessageKind.CANNOT_FIND_CONSTRUCTOR, [node]); 659 error(node.send, MessageKind.CANNOT_FIND_CONSTRUCTOR, [node.send]);
660 } else {
661 FunctionElement function = constructor;
662 // TODO(karlklose): handle optional arguments.
663 if (node.send.argumentCount() != function.parameterCount(compiler)) {
664 error(node.send, MessageKind.CANNOT_FIND_CONSTRUCTOR, [node.send]);
665 }
533 } 666 }
667 } else {
668 Node selector = node.send.selector;
669 error(selector, MessageKind.CANNOT_RESOLVE_TYPE, [selector]);
534 } 670 }
535
536 useElement(node.send, constructor); 671 useElement(node.send, constructor);
537 return null; 672 return null;
538 } 673 }
539 674
540 ClassElement resolveTypeRequired(Node node) { 675 ClassElement resolveTypeRequired(Node node) {
541 bool old = typeRequired; 676 bool old = typeRequired;
542 typeRequired = true; 677 typeRequired = true;
543 ClassElement cls = visit(node); 678 ClassElement cls = visit(node);
544 typeRequired = old; 679 typeRequired = old;
545 return cls; 680 return cls;
(...skipping 280 matching lines...) Expand 10 before | Expand all | Expand 10 after
826 class TopScope extends Scope { 961 class TopScope extends Scope {
827 Universe universe; 962 Universe universe;
828 963
829 TopScope(Universe this.universe) : super(null, null); 964 TopScope(Universe this.universe) : super(null, null);
830 Element lookup(SourceString name) => universe.find(name); 965 Element lookup(SourceString name) => universe.find(name);
831 966
832 Element add(Element element) { 967 Element add(Element element) {
833 throw "Cannot add an element in the top scope"; 968 throw "Cannot add an element in the top scope";
834 } 969 }
835 } 970 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698