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

Side by Side Diff: lib/compiler/implementation/resolver.dart

Issue 11085092: Revert "Made dart2js constructor lookup logic "private"-aware, fixed 4740 bug." and 5 subsequent at… (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 8 years, 2 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) 2012, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2012, 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 abstract class TreeElements { 5 abstract class TreeElements {
6 Element operator[](Node node); 6 Element operator[](Node node);
7 Selector getSelector(Send send); 7 Selector getSelector(Send send);
8 DartType getType(TypeAnnotation annotation); 8 DartType getType(TypeAnnotation annotation);
9 bool isParameterChecked(Element element); 9 bool isParameterChecked(Element element);
10 } 10 }
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after
80 if (kind === ElementKind.PARAMETER || 80 if (kind === ElementKind.PARAMETER ||
81 kind === ElementKind.FIELD_PARAMETER) { 81 kind === ElementKind.FIELD_PARAMETER) {
82 return resolveParameter(element); 82 return resolveParameter(element);
83 } 83 }
84 84
85 compiler.unimplemented("resolve($element)", 85 compiler.unimplemented("resolve($element)",
86 node: element.parseNode(compiler)); 86 node: element.parseNode(compiler));
87 }); 87 });
88 } 88 }
89 89
90 bool isNamedConstructor(Send node) => node.receiver !== null;
91
92 SourceString getConstructorName(Send node) { 90 SourceString getConstructorName(Send node) {
93 return node.selector.asIdentifier().source; 91 if (node.receiver !== null) {
92 return node.selector.asIdentifier().source;
93 } else {
94 return const SourceString('');
95 }
94 } 96 }
95 97
96 String constructorNameForDiagnostics(SourceString className, 98 FunctionElement resolveConstructorRedirection(FunctionElement constructor) {
97 SourceString constructorName) {
98 String classNameString = className.slowToString();
99 String constructorNameString = constructorName.slowToString();
100 return (constructorName === const SourceString(''))
101 ? classNameString
102 : "$classNameString.$constructorNameString";
103 }
104
105 FunctionElement resolveConstructorRedirection(InitializerResolver resolver,
106 FunctionElement constructor) {
107 if (constructor.isPatched) { 99 if (constructor.isPatched) {
108 checkMatchingPatchSignatures(constructor, constructor.patch); 100 checkMatchingPatchSignatures(constructor, constructor.patch);
109 constructor = constructor.patch; 101 constructor = constructor.patch;
110 } 102 }
111 FunctionExpression node = constructor.parseNode(compiler); 103 FunctionExpression node = constructor.parseNode(compiler);
112 104
113 // A synthetic constructor does not have a node. 105 // A synthetic constructor does not have a node.
114 if (node === null) return null; 106 if (node === null) return null;
115 if (node.initializers === null) return null; 107 if (node.initializers === null) return null;
116 Link<Node> initializers = node.initializers.nodes; 108 Link<Node> initializers = node.initializers.nodes;
117 if (!initializers.isEmpty() && 109 if (!initializers.isEmpty() &&
118 Initializers.isConstructorRedirect(initializers.head)) { 110 Initializers.isConstructorRedirect(initializers.head)) {
119 final ClassElement classElement = constructor.getEnclosingClass(); 111 final ClassElement classElement = constructor.getEnclosingClass();
120 Selector selector; 112 final SourceString constructorName =
121 if (isNamedConstructor(initializers.head)) { 113 getConstructorName(initializers.head);
122 SourceString constructorName = getConstructorName(initializers.head); 114 final SourceString className = classElement.name;
123 selector = new Selector.callConstructor( 115 return classElement.lookupConstructor(className, constructorName);
124 constructorName,
125 resolver.visitor.enclosingElement.getLibrary());
126 } else {
127 selector = new Selector.callDefaultConstructor(
128 resolver.visitor.enclosingElement.getLibrary());
129 }
130 return classElement.lookupConstructor(selector);
131 } 116 }
132 return null; 117 return null;
133 } 118 }
134 119
135 void resolveRedirectingConstructor(InitializerResolver resolver, 120 void resolveRedirectingConstructor(InitializerResolver resolver,
136 Node node, 121 Node node,
137 FunctionElement constructor, 122 FunctionElement constructor,
138 FunctionElement redirection) { 123 FunctionElement redirection) {
139 Set<FunctionElement> seen = new Set<FunctionElement>(); 124 Set<FunctionElement> seen = new Set<FunctionElement>();
140 seen.add(constructor); 125 seen.add(constructor);
141 while (redirection !== null) { 126 while (redirection !== null) {
142 if (seen.contains(redirection)) { 127 if (seen.contains(redirection)) {
143 resolver.visitor.error(node, MessageKind.REDIRECTING_CONSTRUCTOR_CYCLE); 128 resolver.visitor.error(node, MessageKind.REDIRECTING_CONSTRUCTOR_CYCLE);
144 return; 129 return;
145 } 130 }
146 seen.add(redirection); 131 seen.add(redirection);
147 redirection = resolveConstructorRedirection(resolver, redirection); 132 redirection = resolveConstructorRedirection(redirection);
148 } 133 }
149 } 134 }
150 135
151 void checkMatchingPatchParameters(FunctionElement origin, 136 void checkMatchingPatchParameters(FunctionElement origin,
152 Link<Element> originParameters, 137 Link<Element> originParameters,
153 Link<Element> patchParameters) { 138 Link<Element> patchParameters) {
154 while (!originParameters.isEmpty()) { 139 while (!originParameters.isEmpty()) {
155 Element originParameter = originParameters.head; 140 Element originParameter = originParameters.head;
156 Element patchParameter = patchParameters.head; 141 Element patchParameter = patchParameters.head;
157 // Hack: Use unparser to test parameter equality. This only works because 142 // Hack: Use unparser to test parameter equality. This only works because
(...skipping 140 matching lines...) Expand 10 before | Expand all | Expand 10 after
298 assert(defaultClass.resolutionState == STATE_DONE); 283 assert(defaultClass.resolutionState == STATE_DONE);
299 assert(defaultClass.supertypeLoadState == STATE_DONE); 284 assert(defaultClass.supertypeLoadState == STATE_DONE);
300 if (defaultClass.isInterface()) { 285 if (defaultClass.isInterface()) {
301 error(node, MessageKind.CANNOT_INSTANTIATE_INTERFACE, 286 error(node, MessageKind.CANNOT_INSTANTIATE_INTERFACE,
302 [defaultClass.name]); 287 [defaultClass.name]);
303 } 288 }
304 // We have now established the following: 289 // We have now established the following:
305 // [intrface] is an interface, let's say "MyInterface". 290 // [intrface] is an interface, let's say "MyInterface".
306 // [defaultClass] is a class, let's say "MyClass". 291 // [defaultClass] is a class, let's say "MyClass".
307 292
308 Selector selector;
309 // If the default class implements the interface then we must use the 293 // If the default class implements the interface then we must use the
310 // default class' name. Otherwise we look for a factory with the name 294 // default class' name. Otherwise we look for a factory with the name
311 // of the interface. 295 // of the interface.
296 SourceString name;
312 if (defaultClass.implementsInterface(intrface)) { 297 if (defaultClass.implementsInterface(intrface)) {
313 var constructorNameString = constructor.name.slowToString(); 298 // TODO(ahe): Don't use string replacement here.
314 // Create selector based on constructor.name but where interface 299 name = new SourceString(constructor.name.slowToString().replaceFirst(
315 // is replaced with default class name. 300 intrface.name.slowToString(),
316 // TODO(ahe): Don't use string manipulations here. 301 defaultClass.name.slowToString()));
317 int classNameSeparatorIndex = constructorNameString.indexOf('\$');
318 if (classNameSeparatorIndex < 0) {
319 selector = new Selector.callDefaultConstructor(
320 defaultClass.getLibrary());
321 } else {
322 selector = new Selector.callConstructor(
323 new SourceString(
324 constructorNameString.substring(classNameSeparatorIndex + 1)),
325 defaultClass.getLibrary());
326 }
327 constructor.defaultImplementation =
328 defaultClass.lookupConstructor(selector);
329 } else { 302 } else {
330 selector = 303 name = constructor.name;
331 new Selector.callConstructor(constructor.name,
332 defaultClass.getLibrary());
333 constructor.defaultImplementation =
334 defaultClass.lookupFactoryConstructor(selector);
335 } 304 }
305 constructor.defaultImplementation = defaultClass.lookupConstructor(name);
306
336 if (constructor.defaultImplementation === null) { 307 if (constructor.defaultImplementation === null) {
337 // We failed to find a constructor named either 308 // We failed to find a constructor named either
338 // "MyInterface.name" or "MyClass.name". 309 // "MyInterface.name" or "MyClass.name".
339 // TODO(aprelev@gmail.com): Use constructorNameForDiagnostics in
340 // the error message below.
341 error(node, 310 error(node,
342 MessageKind.CANNOT_FIND_CONSTRUCTOR2, 311 MessageKind.CANNOT_FIND_CONSTRUCTOR2,
343 [selector.name, defaultClass.name]); 312 [name, defaultClass.name]);
344 } 313 }
345 } 314 }
346 315
347 TreeElements resolveField(VariableElement element) { 316 TreeElements resolveField(VariableElement element) {
348 Node tree = element.parseNode(compiler); 317 Node tree = element.parseNode(compiler);
349 if(element.modifiers.isStatic() && element.variables.isTopLevel()) { 318 if(element.modifiers.isStatic() && element.variables.isTopLevel()) {
350 error(element.modifiers.getStatic(), MessageKind.TOP_LEVEL_VARIABLE_DECLAR ED_STATIC); 319 error(element.modifiers.getStatic(), MessageKind.TOP_LEVEL_VARIABLE_DECLAR ED_STATIC);
351 } 320 }
352 ResolverVisitor visitor = new ResolverVisitor(compiler, element); 321 ResolverVisitor visitor = new ResolverVisitor(compiler, element);
353 initializerDo(tree, visitor.visit); 322 initializerDo(tree, visitor.visit);
(...skipping 352 matching lines...) Expand 10 before | Expand all | Expand 10 after
706 } else { 675 } else {
707 error(init, MessageKind.INVALID_RECEIVER_IN_INITIALIZER); 676 error(init, MessageKind.INVALID_RECEIVER_IN_INITIALIZER);
708 } 677 }
709 visitor.useElement(init, target); 678 visitor.useElement(init, target);
710 visitor.world.registerStaticUse(target); 679 visitor.world.registerStaticUse(target);
711 checkForDuplicateInitializers(name, init); 680 checkForDuplicateInitializers(name, init);
712 // Resolve initializing value. 681 // Resolve initializing value.
713 visitor.visitInStaticContext(init.arguments.head); 682 visitor.visitInStaticContext(init.arguments.head);
714 } 683 }
715 684
716 ClassElement getSuperOrThisLookupTarget(FunctionElement constructor,
717 bool isSuperCall,
718 Node diagnosticNode) {
719 ClassElement lookupTarget = constructor.getEnclosingClass();
720 if (isSuperCall) {
721 // Calculate correct lookup target and constructor name.
722 if (lookupTarget === visitor.compiler.objectClass) {
723 error(diagnosticNode, MessageKind.SUPER_INITIALIZER_IN_OBJECT);
724 } else {
725 return lookupTarget.supertype.element;
726 }
727 }
728 return lookupTarget;
729 }
730
731 Element resolveSuperOrThisForSend(FunctionElement constructor, 685 Element resolveSuperOrThisForSend(FunctionElement constructor,
732 FunctionExpression functionNode, 686 FunctionExpression functionNode,
733 Send call) { 687 Send call) {
734 // Resolve the selector and the arguments. 688 // Resolve the selector and the arguments.
735 ResolverTask resolver = visitor.compiler.resolver; 689 ResolverTask resolver = visitor.compiler.resolver;
736 visitor.inStaticContext(() { 690 visitor.inStaticContext(() {
737 visitor.resolveSelector(call); 691 visitor.resolveSelector(call);
738 visitor.resolveArguments(call.argumentsNode); 692 visitor.resolveArguments(call.argumentsNode);
739 }); 693 });
740 Selector selector = visitor.mapping.getSelector(call); 694 Selector selector = visitor.mapping.getSelector(call);
741 bool isSuperCall = Initializers.isSuperConstructorCall(call); 695 bool isSuperCall = Initializers.isSuperConstructorCall(call);
742 696 SourceString constructorName = resolver.getConstructorName(call);
743 ClassElement lookupTarget = getSuperOrThisLookupTarget(constructor, 697 Element result = resolveSuperOrThis(
744 isSuperCall, 698 constructor, isSuperCall, false, constructorName, selector, call);
745 call); 699 visitor.useElement(call, result);
746 final SourceString className = lookupTarget.name; 700 visitor.world.registerStaticUse(result);
747 701 return result;
748 SourceString constructorName;
749 Selector lookupSelector;
750 if (resolver.isNamedConstructor(call)) {
751 constructorName = resolver.getConstructorName(call);
752 lookupSelector = new Selector.callConstructor(
753 constructorName,
754 visitor.enclosingElement.getLibrary());
755 } else {
756 constructorName = const SourceString('');
757 lookupSelector = new Selector.callDefaultConstructor(
758 visitor.enclosingElement.getLibrary());
759 }
760
761 FunctionElement lookedupConstructor =
762 lookupTarget.lookupConstructor(lookupSelector);
763
764 final bool isImplicitSuperCall = false;
765 verifyThatConstructorMatchesCall(lookedupConstructor,
766 selector,
767 isImplicitSuperCall,
768 call,
769 constructorName,
770 className);
771
772 visitor.useElement(call, lookedupConstructor);
773 visitor.world.registerStaticUse(lookedupConstructor);
774 return lookedupConstructor;
775 } 702 }
776 703
777 void resolveImplicitSuperConstructorSend(FunctionElement constructor, 704 void resolveImplicitSuperConstructorSend(FunctionElement constructor,
778 FunctionExpression functionNode) { 705 FunctionExpression functionNode) {
779 // If the class has a super resolve the implicit super call. 706 // If the class has a super resolve the implicit super call.
780 ClassElement classElement = constructor.getEnclosingClass(); 707 ClassElement classElement = constructor.getEnclosingClass();
781 ClassElement superClass = classElement.superclass; 708 ClassElement superClass = classElement.superclass;
782 if (classElement != visitor.compiler.objectClass) { 709 if (classElement != visitor.compiler.objectClass) {
783 assert(superClass !== null); 710 assert(superClass !== null);
784 assert(superClass.resolutionState == STATE_DONE); 711 assert(superClass.resolutionState == STATE_DONE);
785 SourceString constructorName = const SourceString(''); 712 SourceString name = const SourceString('');
786 Selector callToMatch = new Selector.call( 713 Selector call = new Selector.call(name, classElement.getLibrary(), 0);
787 constructorName, 714 var element = resolveSuperOrThis(constructor, true, true,
788 classElement.getLibrary(), 715 name, call, functionNode);
789 0); 716 visitor.world.registerStaticUse(element);
790
791 final bool isSuperCall = true;
792 ClassElement lookupTarget = getSuperOrThisLookupTarget(constructor,
793 isSuperCall,
794 functionNode);
795 final SourceString className = lookupTarget.name;
796 Element calledConstructor = lookupTarget.lookupConstructor(
797 new Selector.callDefaultConstructor(
798 visitor.enclosingElement.getLibrary()));
799
800 final bool isImplicitSuperCall = true;
801 verifyThatConstructorMatchesCall(calledConstructor,
802 callToMatch,
803 isImplicitSuperCall,
804 functionNode,
805 className,
806 const SourceString(''));
807
808 visitor.world.registerStaticUse(calledConstructor);
809 } 717 }
810 } 718 }
811 719
812 void verifyThatConstructorMatchesCall( 720 Element resolveSuperOrThis(FunctionElement constructor,
813 FunctionElement lookedupConstructor, 721 bool isSuperCall,
814 Selector call, 722 bool isImplicitSuperCall,
815 bool isImplicitSuperCall, 723 SourceString constructorName,
816 Node diagnosticNode, 724 Selector selector,
817 SourceString className, 725 Node diagnosticNode) {
818 SourceString constructorName) { 726 ClassElement lookupTarget = constructor.getEnclosingClass();
819 if (lookedupConstructor === null 727 bool validTarget = true;
820 || !lookedupConstructor.isGenerativeConstructor()) { 728 FunctionElement result;
821 var fullConstructorName = 729 if (isSuperCall) {
822 visitor.compiler.resolver.constructorNameForDiagnostics(className, 730 // Calculate correct lookup target and constructor name.
823 constructorName); 731 if (lookupTarget === visitor.compiler.objectClass) {
732 error(diagnosticNode, MessageKind.SUPER_INITIALIZER_IN_OBJECT);
733 } else {
734 lookupTarget = lookupTarget.supertype.element;
735 }
736 }
737
738 // Lookup constructor and try to match it to the selector.
739 ResolverTask resolver = visitor.compiler.resolver;
740 final SourceString className = lookupTarget.name;
741 result = lookupTarget.lookupConstructor(className, constructorName);
742 if (result === null || !result.isGenerativeConstructor()) {
743 String classNameString = className.slowToString();
744 String constructorNameString = constructorName.slowToString();
745 String name = (constructorName === const SourceString(''))
746 ? classNameString
747 : "$classNameString.$constructorNameString";
824 MessageKind kind = isImplicitSuperCall 748 MessageKind kind = isImplicitSuperCall
825 ? MessageKind.CANNOT_RESOLVE_CONSTRUCTOR_FOR_IMPLICIT 749 ? MessageKind.CANNOT_RESOLVE_CONSTRUCTOR_FOR_IMPLICIT
826 : MessageKind.CANNOT_RESOLVE_CONSTRUCTOR; 750 : MessageKind.CANNOT_RESOLVE_CONSTRUCTOR;
827 error(diagnosticNode, kind, [fullConstructorName]); 751 error(diagnosticNode, kind, [name]);
828 } else { 752 } else {
829 if (!call.applies(lookedupConstructor, visitor.compiler)) { 753 if (!selector.applies(result, visitor.compiler)) {
830 MessageKind kind = isImplicitSuperCall 754 MessageKind kind = isImplicitSuperCall
831 ? MessageKind.NO_MATCHING_CONSTRUCTOR_FOR_IMPLICIT 755 ? MessageKind.NO_MATCHING_CONSTRUCTOR_FOR_IMPLICIT
832 : MessageKind.NO_MATCHING_CONSTRUCTOR; 756 : MessageKind.NO_MATCHING_CONSTRUCTOR;
833 error(diagnosticNode, kind); 757 error(diagnosticNode, kind);
834 } 758 }
835 } 759 }
760 return result;
836 } 761 }
837 762
838 FunctionElement resolveRedirection(FunctionElement constructor, 763 FunctionElement resolveRedirection(FunctionElement constructor,
839 FunctionExpression functionNode) { 764 FunctionExpression functionNode) {
840 if (functionNode.initializers === null) return null; 765 if (functionNode.initializers === null) return null;
841 Link<Node> link = functionNode.initializers.nodes; 766 Link<Node> link = functionNode.initializers.nodes;
842 if (!link.isEmpty() && Initializers.isConstructorRedirect(link.head)) { 767 if (!link.isEmpty() && Initializers.isConstructorRedirect(link.head)) {
843 return resolveSuperOrThisForSend(constructor, functionNode, link.head); 768 return resolveSuperOrThisForSend(constructor, functionNode, link.head);
844 } 769 }
845 return null; 770 return null;
(...skipping 2031 matching lines...) Expand 10 before | Expand all | Expand 10 after
2877 if (inConstContext) { 2802 if (inConstContext) {
2878 error(diagnosticNode, kind, arguments); 2803 error(diagnosticNode, kind, arguments);
2879 } else { 2804 } else {
2880 ResolutionWarning warning = new ResolutionWarning(kind, arguments); 2805 ResolutionWarning warning = new ResolutionWarning(kind, arguments);
2881 compiler.reportWarning(diagnosticNode, warning); 2806 compiler.reportWarning(diagnosticNode, warning);
2882 return new ErroneousFunctionElement(kind, arguments, targetName, 2807 return new ErroneousFunctionElement(kind, arguments, targetName,
2883 enclosing); 2808 enclosing);
2884 } 2809 }
2885 } 2810 }
2886 2811
2887 Selector createConstructorSelector(SourceString constructorName) {
2888 return constructorName == const SourceString('')
2889 ? new Selector.callDefaultConstructor(
2890 resolver.enclosingElement.getLibrary())
2891 : new Selector.callConstructor(
2892 constructorName,
2893 resolver.enclosingElement.getLibrary());
2894 }
2895
2896 // TODO(ngeoffray): method named lookup should not report errors. 2812 // TODO(ngeoffray): method named lookup should not report errors.
2897 FunctionElement lookupConstructor(ClassElement cls, 2813 FunctionElement lookupConstructor(ClassElement cls,
2898 Node diagnosticNode, 2814 Node diagnosticNode,
2899 SourceString constructorName) { 2815 SourceString constructorName) {
2900 cls.ensureResolved(compiler); 2816 cls.ensureResolved(compiler);
2901 Selector selector = createConstructorSelector(constructorName); 2817 Element result = cls.lookupConstructor(cls.name, constructorName);
2902 Element result = cls.lookupConstructor(selector);
2903 if (result === null) { 2818 if (result === null) {
2904 String fullConstructorName = 2819 String fullConstructorName = cls.name.slowToString();
2905 resolver.compiler.resolver.constructorNameForDiagnostics( 2820 if (constructorName !== const SourceString('')) {
2906 cls.name, 2821 fullConstructorName = '$fullConstructorName'
2907 constructorName); 2822 '.${constructorName.slowToString()}';
2908 return failOrReturnErroneousElement( 2823 }
2909 cls, 2824 return failOrReturnErroneousElement(cls, diagnosticNode,
2910 diagnosticNode, 2825 new SourceString(fullConstructorName),
2911 new SourceString(fullConstructorName), 2826 MessageKind.CANNOT_FIND_CONSTRUCTOR,
2912 MessageKind.CANNOT_FIND_CONSTRUCTOR, 2827 [fullConstructorName]);
2913 [fullConstructorName]);
2914 } else if (inConstContext && !result.modifiers.isConst()) { 2828 } else if (inConstContext && !result.modifiers.isConst()) {
2915 error(diagnosticNode, MessageKind.CONSTRUCTOR_IS_NOT_CONST); 2829 error(diagnosticNode, MessageKind.CONSTRUCTOR_IS_NOT_CONST);
2916 } 2830 }
2917 return result; 2831 return result;
2918 } 2832 }
2919 2833
2920 visitNewExpression(NewExpression node) { 2834 visitNewExpression(NewExpression node) {
2921 Node selector = node.send.selector; 2835 Node selector = node.send.selector;
2922 Element e = visit(selector); 2836 Element e = visit(selector);
2923 if (!Elements.isUnresolved(e) && e.kind === ElementKind.CLASS) { 2837 if (!Elements.isUnresolved(e) && e.kind === ElementKind.CLASS) {
(...skipping 314 matching lines...) Expand 10 before | Expand all | Expand 10 after
3238 return result; 3152 return result;
3239 } 3153 }
3240 Element lookup(SourceString name) => localLookup(name); 3154 Element lookup(SourceString name) => localLookup(name);
3241 Element lexicalLookup(SourceString name) => localLookup(name); 3155 Element lexicalLookup(SourceString name) => localLookup(name);
3242 3156
3243 Element add(Element newElement) { 3157 Element add(Element newElement) {
3244 throw "Cannot add an element in a patch library scope"; 3158 throw "Cannot add an element in a patch library scope";
3245 } 3159 }
3246 String toString() => 'PatchLibraryScope($origin,$patch)'; 3160 String toString() => 'PatchLibraryScope($origin,$patch)';
3247 } 3161 }
OLDNEW
« no previous file with comments | « lib/compiler/implementation/elements/elements.dart ('k') | lib/compiler/implementation/ssa/builder.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698