optimized variable access when possible
authorFabien Potencier <fabien.potencier@gmail.com>
Fri, 30 Sep 2011 15:04:06 +0000 (17:04 +0200)
committerFabien Potencier <fabien.potencier@gmail.com>
Sat, 8 Oct 2011 12:27:18 +0000 (14:27 +0200)
lib/Twig/Node/Body.php [new file with mode: 0644]
lib/Twig/Node/Expression/Name.php
lib/Twig/Node/Expression/TempName.php [new file with mode: 0644]
lib/Twig/Node/Module.php
lib/Twig/Node/SetTemp.php [new file with mode: 0644]
lib/Twig/NodeVisitor/Optimizer.php
lib/Twig/Parser.php
lib/Twig/TokenParser/Macro.php
test/Twig/Tests/ExpressionParserTest.php
test/Twig/Tests/NodeVisitor/OptimizerTest.php

diff --git a/lib/Twig/Node/Body.php b/lib/Twig/Node/Body.php
new file mode 100644 (file)
index 0000000..f72bf50
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2011 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a body node.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien@symfony.com>
+ */
+class Twig_Node_Body extends Twig_Node
+{
+}
index 1dacc87..94d6135 100644 (file)
  */
 class Twig_Node_Expression_Name extends Twig_Node_Expression
 {
+    protected $specialVars = array(
+        '_self'    => '$this',
+        '_context' => '$context',
+        '_charset' => '$this->env->getCharset()',
+    );
+
     public function __construct($name, $lineno)
     {
-        parent::__construct(array(), array('name' => $name, 'output' => false), $lineno);
+        parent::__construct(array(), array('name' => $name), $lineno);
     }
 
     public function compile(Twig_Compiler $compiler)
     {
-        static $specialVars = array(
-            '_self'    => '$this',
-            '_context' => '$context',
-            '_charset' => '$this->env->getCharset()',
-        );
-
         $name = $this->getAttribute('name');
 
         if ($this->hasAttribute('is_defined_test')) {
-            if (isset($specialVars[$name])) {
+            if ($this->isSpecial()) {
                 $compiler->repr(true);
             } else {
                 $compiler->raw('array_key_exists(')->repr($name)->raw(', $context)');
             }
-        } elseif (isset($specialVars[$name])) {
-            $compiler->raw($specialVars[$name]);
-        } elseif ($this->getAttribute('output')) {
-            $compiler
-                ->addDebugInfo($this)
-                ->write('if (isset($context[')
-                ->string($name)
-                ->raw("])) {\n")
-                ->indent()
-                ->write('echo $context[')
-                ->string($name)
-                ->raw("];\n")
-                ->outdent()
-                ->write("}\n")
-            ;
+        } elseif ($this->isSpecial()) {
+            $compiler->raw($this->specialVars[$name]);
         } else {
             $compiler
                 ->raw('$this->getContext($context, ')
@@ -55,4 +42,14 @@ class Twig_Node_Expression_Name extends Twig_Node_Expression
             ;
         }
     }
+
+    public function isSpecial()
+    {
+        return isset($this->specialVars[$this->getAttribute('name')]);
+    }
+
+    public function isSimple()
+    {
+        return !$this->isSpecial() && !$this->hasAttribute('is_defined_test');
+    }
 }
diff --git a/lib/Twig/Node/Expression/TempName.php b/lib/Twig/Node/Expression/TempName.php
new file mode 100644 (file)
index 0000000..eea9d47
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2011 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_TempName extends Twig_Node_Expression
+{
+    public function __construct($name, $lineno)
+    {
+        parent::__construct(array(), array('name' => $name), $lineno);
+    }
+
+    public function compile(Twig_Compiler $compiler)
+    {
+        $compiler->raw('$_')->raw($this->getAttribute('name'))->raw('_');
+    }
+}
index bf7d4c1..7321e06 100644 (file)
@@ -257,8 +257,14 @@ class Twig_Node_Module extends Twig_Node
         // only contains blocks and use statements.
         $traitable = null === $this->getNode('parent') && 0 === count($this->getNode('macros'));
         if ($traitable) {
-            if (!count($nodes = $this->getNode('body'))) {
-                $nodes = new Twig_Node(array($this->getNode('body')));
+            if ($this->getNode('body') instanceof Twig_Node_Body) {
+                $nodes = $this->getNode('body')->getNode(0);
+            } else {
+                $nodes = $this->getNode('body');
+            }
+
+            if (!count($nodes)) {
+                $nodes = new Twig_Node(array($nodes));
             }
 
             foreach ($nodes as $node) {
diff --git a/lib/Twig/Node/SetTemp.php b/lib/Twig/Node/SetTemp.php
new file mode 100644 (file)
index 0000000..3bdd1cb
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2011 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Node_SetTemp extends Twig_Node
+{
+    public function __construct($name, $lineno)
+    {
+        parent::__construct(array(), array('name' => $name), $lineno);
+    }
+
+    public function compile(Twig_Compiler $compiler)
+    {
+        $name = $this->getAttribute('name');
+        $compiler
+            ->addDebugInfo($this)
+            ->write('if (isset($context[')
+            ->string($name)
+            ->raw('])) { $_')
+            ->raw($name)
+            ->raw('_ = $context[')
+            ->repr($name)
+            ->raw(']; } else { $_')
+            ->raw($name)
+            ->raw("_ = null; }\n")
+        ;
+    }
+}
index 0d0ff84..196c5d7 100644 (file)
@@ -26,9 +26,12 @@ class Twig_NodeVisitor_Optimizer implements Twig_NodeVisitorInterface
     const OPTIMIZE_NONE        = 0;
     const OPTIMIZE_FOR         = 2;
     const OPTIMIZE_RAW_FILTER  = 4;
+    const OPTIMIZE_VAR_ACCESS  = 8;
 
     protected $loops = array();
     protected $optimizers;
+    protected $prependedNodes = array();
+    protected $inABody = false;
 
     /**
      * Constructor.
@@ -53,6 +56,20 @@ class Twig_NodeVisitor_Optimizer implements Twig_NodeVisitorInterface
             $this->enterOptimizeFor($node, $env);
         }
 
+        if (self::OPTIMIZE_VAR_ACCESS === (self::OPTIMIZE_VAR_ACCESS & $this->optimizers) && !$env->isStrictVariables() && !$env->hasExtension('sandbox')) {
+            if ($this->inABody) {
+                if (!$node instanceof Twig_Node_Expression) {
+                    if (get_class($node) !== 'Twig_Node') {
+                        array_unshift($this->prependedNodes, array());
+                    }
+                } else {
+                    $node = $this->optimizeVariables($node, $env);
+                }
+            } elseif ($node instanceof Twig_Node_Body) {
+                $this->inABody = true;
+            }
+        }
+
         return $node;
     }
 
@@ -61,6 +78,8 @@ class Twig_NodeVisitor_Optimizer implements Twig_NodeVisitorInterface
      */
     public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env)
     {
+        $expression = $node instanceof Twig_Node_Expression;
+
         if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) {
             $this->leaveOptimizeFor($node, $env);
         }
@@ -71,6 +90,28 @@ class Twig_NodeVisitor_Optimizer implements Twig_NodeVisitorInterface
 
         $node = $this->optimizePrintNode($node, $env);
 
+        if (self::OPTIMIZE_VAR_ACCESS === (self::OPTIMIZE_VAR_ACCESS & $this->optimizers) && !$env->isStrictVariables() && !$env->hasExtension('sandbox')) {
+            if ($node instanceof Twig_Node_Body) {
+                $this->inABody = false;
+            } elseif ($this->inABody) {
+                if (!$expression && get_class($node) !== 'Twig_Node' && $prependedNodes = array_shift($this->prependedNodes)) {
+                    $prependedNodes[] = $node;
+                    $node = new Twig_Node($prependedNodes);
+                }
+            }
+        }
+
+        return $node;
+    }
+
+    protected function optimizeVariables($node, $env)
+    {
+        if ('Twig_Node_Expression_Name' === get_class($node) && $node->isSimple()) {
+            $this->prependedNodes[0][] = new Twig_Node_SetTemp($node->getAttribute('name'), $node->getLine());
+
+            return new Twig_Node_Expression_TempName($node->getAttribute('name'), $node->getLine());
+        }
+
         return $node;
     }
 
@@ -80,7 +121,6 @@ class Twig_NodeVisitor_Optimizer implements Twig_NodeVisitorInterface
      * It replaces:
      *
      *   * "echo $this->render(Parent)Block()" with "$this->display(Parent)Block()"
-     *   * "echo $this->getContext('...')" with "if (isset('...')) { echo '...' }"
      *
      * @param Twig_NodeInterface $node A Node
      * @param Twig_Environment   $env  The current Twig environment
@@ -93,8 +133,7 @@ class Twig_NodeVisitor_Optimizer implements Twig_NodeVisitorInterface
 
         if (
             $node->getNode('expr') instanceof Twig_Node_Expression_BlockReference ||
-            $node->getNode('expr') instanceof Twig_Node_Expression_Parent ||
-            ($node->getNode('expr') instanceof Twig_Node_Expression_Name && !$env->hasExtension('sandbox') && !$env->isStrictVariables())
+            $node->getNode('expr') instanceof Twig_Node_Expression_Parent
         ) {
             $node->getNode('expr')->setAttribute('output', true);
 
index dc5b42c..0fc11cb 100644 (file)
@@ -93,7 +93,7 @@ class Twig_Parser implements Twig_ParserInterface
             throw $e;
         }
 
-        $node = new Twig_Node_Module($body, $this->parent, new Twig_Node($this->blocks), new Twig_Node($this->macros), new Twig_Node($this->traits), $this->stream->getFilename());
+        $node = new Twig_Node_Module(new Twig_Node_Body(array($body)), $this->parent, new Twig_Node($this->blocks), new Twig_Node($this->macros), new Twig_Node($this->traits), $this->stream->getFilename());
 
         $traverser = new Twig_NodeTraverser($this->env, $this->visitors);
 
@@ -204,7 +204,7 @@ class Twig_Parser implements Twig_ParserInterface
 
     public function setBlock($name, $value)
     {
-        $this->blocks[$name] = $value;
+        $this->blocks[$name] = new Twig_Node_Body(array($value));
     }
 
     public function hasMacro($name)
index da1ac55..64f2ea5 100644 (file)
@@ -47,7 +47,7 @@ class Twig_TokenParser_Macro extends Twig_TokenParser
         $this->parser->popLocalScope();
         $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
 
-        $this->parser->setMacro($name, new Twig_Node_Macro($name, $body, $arguments, $lineno, $this->getTag()));
+        $this->parser->setMacro($name, new Twig_Node_Macro($name, new Twig_Node_Body(array($body)), $arguments, $lineno, $this->getTag()));
 
         return null;
     }
index 4e43652..d36c9fd 100644 (file)
@@ -45,7 +45,7 @@ class Twig_Tests_ExpressionParserTest extends PHPUnit_Framework_TestCase
         $stream = $env->tokenize($template, 'index');
         $parser = new Twig_Parser($env);
 
-        $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode('expr'));
+        $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode(0)->getNode('expr'));
     }
 
     /**
index b6aa979..edf6c51 100644 (file)
@@ -17,7 +17,7 @@ class Twig_Tests_NodeVisitor_OptimizerTest extends PHPUnit_Framework_TestCase
 
         $stream = $env->parse($env->tokenize('{{ block("foo") }}', 'index'));
 
-        $node = $stream->getNode('body');
+        $node = $stream->getNode('body')->getNode(0);
 
         $this->assertInstanceOf('Twig_Node_Expression_BlockReference', $node);
         $this->assertTrue($node->getAttribute('output'));
@@ -30,7 +30,7 @@ class Twig_Tests_NodeVisitor_OptimizerTest extends PHPUnit_Framework_TestCase
 
         $stream = $env->parse($env->tokenize('{% extends "foo" %}{% block content %}{{ parent() }}{% endblock %}', 'index'));
 
-        $node = $stream->getNode('blocks')->getNode('content')->getNode('body');
+        $node = $stream->getNode('blocks')->getNode('content')->getNode(0)->getNode('body');
 
         $this->assertInstanceOf('Twig_Node_Expression_Parent', $node);
         $this->assertTrue($node->getAttribute('output'));
@@ -42,24 +42,12 @@ class Twig_Tests_NodeVisitor_OptimizerTest extends PHPUnit_Framework_TestCase
         $env->addExtension(new Twig_Extension_Optimizer());
         $stream = $env->parse($env->tokenize('{{ block(name|lower) }}', 'index'));
 
-        $node = $stream->getNode('body');
+        $node = $stream->getNode('body')->getNode(0)->getNode(1);
 
         $this->assertInstanceOf('Twig_Node_Expression_BlockReference', $node);
         $this->assertTrue($node->getAttribute('output'));
     }
 
-    public function testRenderNameOptimizer()
-    {
-        $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
-        $env->addExtension(new Twig_Extension_Optimizer());
-        $stream = $env->parse($env->tokenize('{{ name }}', 'index'));
-
-        $node = $stream->getNode('body');
-
-        $this->assertInstanceOf('Twig_Node_Expression_Name', $node);
-        $this->assertTrue($node->getAttribute('output'));
-    }
-
     /**
      * @dataProvider getTestsForForOptimizer
      */