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

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: Address comments. 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(ClassElement cls, Send node) {
161 SourceString constructor = node.selector.asIdentifier().source;
162 if (node.receiver !== null) {
163 return new SourceString('${cls.name}.$constructor');
164 } else {
165 return cls.name;
166 }
167 }
168
169 void resolveSuperOrThis(Send call) {
170 noConstructor(e) {
171 if (e !== null) error(call, MessageKind.NO_CONSTRUCTOR, [e.name, e.kind]);
172 }
173
174 ClassElement lookupTarget = constructor.enclosingElement;
175 bool validTarget = true;
176 if (isSuperConstructorCall(call)) {
177 // Check for invalid initializers.
178 if (hasSuper) {
179 error(call, MessageKind.DUPLICATE_SUPER_INITIALIZER);
180 }
181 hasSuper = true;
182 // Calculate correct lookup target and constructor name.
183 if (lookupTarget.name == Types.OBJECT) {
184 error(call, MessageKind.SUPER_INITIALIZER_IN_OBJECT);
185 } else {
186 lookupTarget = lookupTarget.supertype.element;
187 }
188 } else if (isConstructorRedirect(call)) {
189 // Check that there are no other initializers.
190 if (!initializers.tail.isEmpty()) {
191 error(call, MessageKind.REDIRECTING_CTOR_HAS_INITIALIZER);
192 }
193 } else {
194 visitor.error(call, MessageKind.CONSTRUCTOR_CALL_EXPECTED);
195 validTarget = false;
196 }
197
198 if (validTarget) {
199 final SourceString name = getConstructorName(lookupTarget, call);
200 FunctionElement target =
201 lookupTarget.lookupConstructor(name, noConstructor);
202 if (target === null && call.arguments.isEmpty()) {
203 target = lookupTarget.getSynthesizedConstructor();
204 }
205 if (target === null) {
206 error(call, MessageKind.CANNOT_RESOLVE_CONSTRUCTOR, [name]);
207 } else {
208 final Compiler compiler = visitor.compiler;
209 // TODO(karlklose): support optional arguments.
210 if (target.parameterCount(compiler) != call.argumentCount()) {
211 error(call, MessageKind.NO_MATCHING_CONSTRUCTOR);
212 }
213 }
214 visitor.useElement(call, target);
215 }
216 // Resolve the arguments of the call.
217 for (Link<Node> arguments = call.arguments;
218 !arguments.isEmpty();
219 arguments = arguments.tail) {
220 visitor.visitInStaticContext(arguments.head);
221 }
222 }
223
224 void resolveInitializers(FunctionExpression node) {
225 if (node.initializers === null) return;
226 initializers = node.initializers.nodes;
227 Compiler compiler = visitor.compiler;
228 for (Link<Node> link = initializers;
229 !link.isEmpty();
230 link = link.tail) {
231 if (link.head.asSendSet() != null) {
232 final SendSet init = link.head.asSendSet();
233 resolveFieldInitializer(init);
234 } else if (link.head.asSend() !== null) {
235 final Send call = link.head.asSend();
236 resolveSuperOrThis(call);
237 } else {
238 visitor.compiler.cancel('internal error: invalid initializer',
239 node: link.head);
240 }
241 }
242 }
243 }
244
245
137 // TODO(ahe): Frog cannot handle generic types. 246 // TODO(ahe): Frog cannot handle generic types.
138 class ResolverVisitor extends AbstractVisitor/*<Element>*/ { 247 class ResolverVisitor extends AbstractVisitor/*<Element>*/ {
139 final Compiler compiler; 248 final Compiler compiler;
140 final TreeElements mapping; 249 final TreeElements mapping;
141 final Element enclosingElement; 250 final Element enclosingElement;
142 bool inInstanceContext; 251 bool inInstanceContext;
143 Scope context; 252 Scope context;
144 ClassElement currentClass; 253 ClassElement currentClass;
145 bool typeRequired = false; 254 bool typeRequired = false;
146 255
(...skipping 26 matching lines...) Expand all
173 compiler.reportWarning(node, warning); 282 compiler.reportWarning(node, warning);
174 } 283 }
175 284
176 cancel(Node node, String message) { 285 cancel(Node node, String message) {
177 compiler.cancel(message, node: node); 286 compiler.cancel(message, node: node);
178 } 287 }
179 288
180 Element lookup(Node node, SourceString name) { 289 Element lookup(Node node, SourceString name) {
181 Element result = context.lookup(name); 290 Element result = context.lookup(name);
182 if (!inInstanceContext && result != null && result.isInstanceMember()) { 291 if (!inInstanceContext && result != null && result.isInstanceMember()) {
183 error(node, MessageKind.NOT_STATIC, [node]); 292 error(node, MessageKind.NO_INSTANCE_AVAILABLE, [node]);
184 } 293 }
185 return result; 294 return result;
186 } 295 }
187 296
188 visitInStaticContext(Node node) { 297 visitInStaticContext(Node node) {
189 bool wasInstanceContext = inInstanceContext; 298 bool wasInstanceContext = inInstanceContext;
190 inInstanceContext = false; 299 inInstanceContext = false;
191 visit(node); 300 visit(node);
192 inInstanceContext = wasInstanceContext; 301 inInstanceContext = wasInstanceContext;
193 } 302 }
194 303
195 visit(Node node) { 304 visit(Node node) {
196 if (node == null) return null; 305 if (node == null) return null;
197 return node.accept(this); 306 return node.accept(this);
198 } 307 }
199 308
200 visitIdentifier(Identifier node) { 309 visitIdentifier(Identifier node) {
201 if (node.isThis()) { 310 if (node.isThis()) {
202 if (!inInstanceContext) error(node, MessageKind.NO_THIS_IN_STATIC); 311 if (!inInstanceContext) {
312 error(node, MessageKind.NO_INSTANCE_AVAILABLE, [node]);
313 }
203 return null; 314 return null;
204 } else if (node.isSuper()) { 315 } else if (node.isSuper()) {
205 if (!inInstanceContext) error(node, MessageKind.NO_SUPER_IN_STATIC); 316 if (!inInstanceContext) error(node, MessageKind.NO_SUPER_IN_STATIC);
206 return null; 317 return null;
207 } else { 318 } else {
208 Element element = lookup(node, node.source); 319 Element element = lookup(node, node.source);
209 if (element == null) { 320 if (element == null) {
210 error(node, MessageKind.CANNOT_RESOLVE, [node]); 321 error(node, MessageKind.CANNOT_RESOLVE, [node]);
211 } 322 }
212 return useElement(node, element); 323 return useElement(node, element);
213 } 324 }
214 } 325 }
215 326
216 visitTypeAnnotation(TypeAnnotation node) { 327 visitTypeAnnotation(TypeAnnotation node) {
217 Identifier name = node.typeName.asIdentifier(); 328 SourceString className;
218 if (name === null) { 329 if (node.typeName.asSend() !== null) {
219 // TODO(karlklose): In progress. 330 // In new and const expressions, the type name can be a Send to
220 cancel(node.typeName, "not implemented"); 331 // denote named constructors or library prefixes.
332 Send send = node.typeName.asSend();
333 className = send.receiver.asIdentifier().source;
334 } else {
335 className = node.typeName.asIdentifier().source;
221 } 336 }
222 if (name.source == const SourceString('var')) return null; 337 if (className == const SourceString('var')) return null;
223 if (name.source == const SourceString('void')) return null; 338 if (className == const SourceString('void')) return null;
224 Element element = context.lookup(name.source); 339 Element element = context.lookup(className);
225 if (element === null) { 340 if (element === null) {
226 if (typeRequired) { 341 if (typeRequired) {
227 error(node, MessageKind.CANNOT_RESOLVE_TYPE, [name]); 342 error(node, MessageKind.CANNOT_RESOLVE_TYPE, [className]);
228 } else { 343 } else {
229 warning(node, MessageKind.CANNOT_RESOLVE_TYPE, [name]); 344 warning(node, MessageKind.CANNOT_RESOLVE_TYPE, [className]);
230 } 345 }
231 } else if (element.kind !== ElementKind.CLASS) { 346 } else if (element.kind !== ElementKind.CLASS) {
232 if (typeRequired) { 347 if (typeRequired) {
233 error(node, MessageKind.NOT_A_TYPE, [name]); 348 error(node, MessageKind.NOT_A_TYPE, [className]);
234 } else { 349 } else {
235 warning(node, MessageKind.NOT_A_TYPE, [name]); 350 warning(node, MessageKind.NOT_A_TYPE, [className]);
236 } 351 }
237 } else { 352 } else {
238 ClassElement cls = element; 353 ClassElement cls = element;
239 compiler.resolver.toResolve.add(element); 354 compiler.resolver.toResolve.add(element);
240 // TODO(ahe): This should be a Type. 355 // TODO(ahe): This should be a Type.
241 useElement(node, element); 356 useElement(node, element);
242 } 357 }
243 return element; 358 return element;
244 } 359 }
245 360
(...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after
326 visitFor(For node) { 441 visitFor(For node) {
327 Scope scope = new BlockScope(context); 442 Scope scope = new BlockScope(context);
328 visitIn(node.initializer, scope); 443 visitIn(node.initializer, scope);
329 visitIn(node.condition, scope); 444 visitIn(node.condition, scope);
330 visitIn(node.update, scope); 445 visitIn(node.update, scope);
331 visitIn(node.body, scope); 446 visitIn(node.body, scope);
332 } 447 }
333 448
334 visitFunctionExpression(FunctionExpression node) { 449 visitFunctionExpression(FunctionExpression node) {
335 visit(node.returnType); 450 visit(node.returnType);
451 SourceString name;
336 if (node.name === null) { 452 if (node.name === null) {
337 cancel(node, "anonymous functions are not implemented"); 453 cancel(node, "anonymous functions are not implemented");
338 } 454 }
339 if (node.name.asIdentifier() === null) { 455 name = node.name.asIdentifier().source;
340 cancel(node.name, "named constructors are not implemented");
341 }
342 FunctionElement enclosingElement = new FunctionElement.node( 456 FunctionElement enclosingElement = new FunctionElement.node(
343 node, ElementKind.FUNCTION, null, context.element); 457 name, node, ElementKind.FUNCTION, null, context.element);
344 defineElement(node, enclosingElement); 458 defineElement(node, enclosingElement);
345 context = new MethodScope(context, enclosingElement); 459 context = new MethodScope(context, enclosingElement);
346 460
347 // TODO(ahe): Can this be cleaned up to use resolveSignature? 461 // TODO(ahe): Can this be cleaned up to use resolveSignature?
348 ParametersVisitor visitor = new ParametersVisitor(this); 462 ParametersVisitor visitor = new ParametersVisitor(this);
349 visitor.visit(node.parameters); 463 visitor.visit(node.parameters);
350 enclosingElement.parameters = visitor.elements.toLink(); 464 enclosingElement.parameters = visitor.elements.toLink();
351 465
352 visit(node.body); 466 visit(node.body);
353 context = context.parent; 467 context = context.parent;
(...skipping 156 matching lines...) Expand 10 before | Expand all | Expand 10 after
510 624
511 visitParenthesizedExpression(ParenthesizedExpression node) { 625 visitParenthesizedExpression(ParenthesizedExpression node) {
512 visit(node.expression); 626 visit(node.expression);
513 } 627 }
514 628
515 visitNewExpression(NewExpression node) { 629 visitNewExpression(NewExpression node) {
516 if (node.isConst()) cancel(node, 'const expressions are not implemented'); 630 if (node.isConst()) cancel(node, 'const expressions are not implemented');
517 631
518 visit(node.send.argumentsNode); 632 visit(node.send.argumentsNode);
519 633
634 SourceString constructorName;
635 Node typeName = node.send.selector.asTypeAnnotation().typeName;
636 if (typeName.asSend() !== null) {
637 Identifier receiver = typeName.asSend().receiver.asIdentifier();
638 Identifier selector = typeName.asSend().selector.asIdentifier();
639 SourceString className = receiver.source;
640 SourceString name = selector.source;
641 constructorName = new SourceString('$className.$name');
642 } else {
643 constructorName = typeName.asIdentifier().source;
644 }
520 ClassElement cls = resolveTypeRequired(node.send.selector); 645 ClassElement cls = resolveTypeRequired(node.send.selector);
521 Element constructor = null; 646 Element constructor = null;
522 if (cls !== null) { 647 if (cls !== null) {
523 // TODO(ngeoffray): set constructor-name correctly. 648 constructor = cls.resolve(compiler).lookupConstructor(constructorName);
524 SourceString name = cls.name; 649 if (constructorName == cls.name
525 constructor = cls.resolve(compiler).lookupConstructor(name);
526 if (name == cls.name
527 && constructor === null 650 && constructor === null
528 && node.send.argumentsNode.isEmpty()) { 651 && node.send.argumentsNode.isEmpty()) {
529 constructor = cls.getSynthesizedConstructor(); 652 constructor = cls.getSynthesizedConstructor();
530 } 653 }
531 if (constructor === null) { 654 if (constructor === null) {
532 error(node, MessageKind.CANNOT_FIND_CONSTRUCTOR, [node]); 655 error(node.send, MessageKind.CANNOT_FIND_CONSTRUCTOR, [node.send]);
656 } else {
657 FunctionElement function = constructor;
658 // TODO(karlklose): handle optional arguments.
659 if (node.send.argumentCount() != function.parameterCount(compiler)) {
660 error(node.send, MessageKind.CANNOT_FIND_CONSTRUCTOR, [node.send]);
661 }
533 } 662 }
663 } else {
664 Node selector = node.send.selector;
665 error(selector, MessageKind.CANNOT_RESOLVE_TYPE, [selector]);
534 } 666 }
535
536 useElement(node.send, constructor); 667 useElement(node.send, constructor);
537 return null; 668 return null;
538 } 669 }
539 670
540 ClassElement resolveTypeRequired(Node node) { 671 ClassElement resolveTypeRequired(Node node) {
541 bool old = typeRequired; 672 bool old = typeRequired;
542 typeRequired = true; 673 typeRequired = true;
543 ClassElement cls = visit(node); 674 ClassElement cls = visit(node);
544 typeRequired = old; 675 typeRequired = old;
545 return cls; 676 return cls;
(...skipping 280 matching lines...) Expand 10 before | Expand all | Expand 10 after
826 class TopScope extends Scope { 957 class TopScope extends Scope {
827 Universe universe; 958 Universe universe;
828 959
829 TopScope(Universe this.universe) : super(null, null); 960 TopScope(Universe this.universe) : super(null, null);
830 Element lookup(SourceString name) => universe.find(name); 961 Element lookup(SourceString name) => universe.find(name);
831 962
832 Element add(Element element) { 963 Element add(Element element) {
833 throw "Cannot add an element in the top scope"; 964 throw "Cannot add an element in the top scope";
834 } 965 }
835 } 966 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698