optimizer can optimize filtered for loops now

--HG--
branch : trunk
diff --git a/examples/test_loop_filter.py b/examples/test_loop_filter.py
index 64f32d3..49c2efc 100644
--- a/examples/test_loop_filter.py
+++ b/examples/test_loop_filter.py
@@ -2,11 +2,11 @@
 
 tmpl = Environment().from_string("""\
 <ul>
-{% for item in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] if item % 2 == 0 %}
+{%- for item in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] if item % 2 == 0 %}
     <li>{{ loop.index }} / {{ loop.length }}: {{ item }}</li>
-{% endfor %}
+{%- endfor %}
 </ul>
-{{ 1 if foo else 0 }}
+if condition: {{ 1 if foo else 0 }}
 """)
 
 print tmpl.render(foo=True)
diff --git a/jinja2/compiler.py b/jinja2/compiler.py
index e16617e..3c2347d 100644
--- a/jinja2/compiler.py
+++ b/jinja2/compiler.py
@@ -240,16 +240,18 @@
     def outdent(self, step=1):
         self.indentation -= step
 
-    def blockvisit(self, nodes, frame, force_generator=False):
-        self.indent()
-        if force_generator and frame.buffer is None:
+    def blockvisit(self, nodes, frame, indent=True, force_generator=True):
+        if indent:
+            self.indent()
+        if frame.buffer is None and force_generator:
             self.writeline('if 0: yield None')
         try:
             for node in nodes:
                 self.visit(node, frame)
         except CompilerExit:
             pass
-        self.outdent()
+        if indent:
+            self.outdent()
 
     def write(self, x):
         if self.new_lines:
@@ -297,8 +299,8 @@
             self.write('**')
             self.visit(node.dyn_kwargs, frame)
 
-    def pull_locals(self, frame, no_indent=False):
-        if not no_indent:
+    def pull_locals(self, frame, indent=True):
+        if indent:
             self.indent()
         for name in frame.identifiers.undeclared:
             self.writeline('l_%s = context[%r]' % (name, name))
@@ -306,7 +308,7 @@
             self.writeline('f_%s = environment.filters[%r]' % (name, name))
         for name in frame.identifiers.tests:
             self.writeline('t_%s = environment.tests[%r]' % (name, name))
-        if not no_indent:
+        if indent:
             self.outdent()
 
     def collect_shadowed(self, frame):
@@ -394,11 +396,11 @@
         frame = Frame()
         frame.inspect(node.body)
         frame.toplevel = frame.rootlevel = True
-        self.pull_locals(frame)
         self.indent()
+        self.pull_locals(frame, indent=False)
         self.writeline('yield context')
+        self.blockvisit(node.body, frame, indent=False)
         self.outdent()
-        self.blockvisit(node.body, frame)
 
         # make sure that the parent root is called.
         if have_extends:
@@ -421,7 +423,7 @@
             self.writeline('def block_%s(context, environment=environment):'
                            % name, block, 1)
             self.pull_locals(block_frame)
-            self.blockvisit(block.body, block_frame, True)
+            self.blockvisit(block.body, block_frame)
 
         self.writeline('blocks = {%s}' % ', '.join('%r: block_%s' % (x, x)
                                                    for x in self.blocks), extra=1)
@@ -526,7 +528,7 @@
             loop_frame.identifiers.add_special('loop')
 
         aliases = self.collect_shadowed(loop_frame)
-        self.pull_locals(loop_frame, True)
+        self.pull_locals(loop_frame, indent=False)
         if node.else_:
             self.writeline('l_loop = None')
 
@@ -576,14 +578,14 @@
             self.writeline('continue')
             self.outdent(2)
 
-        self.blockvisit(node.body, loop_frame)
+        self.blockvisit(node.body, loop_frame, force_generator=False)
 
         if node.else_:
             self.writeline('if l_loop is None:')
             self.indent()
             self.writeline('l_loop = ' + parent_loop)
             self.outdent()
-            self.blockvisit(node.else_, loop_frame)
+            self.blockvisit(node.else_, loop_frame, force_generator=False)
 
         # reset the aliases if there are any.
         for name, alias in aliases.iteritems():
@@ -603,8 +605,13 @@
         macro_frame = self.function_scoping(node, frame)
         args = macro_frame.arguments
         self.writeline('def macro(%s):' % ', '.join(args), node)
-        self.pull_locals(macro_frame)
-        self.blockvisit(node.body, macro_frame, True)
+        macro_frame.buffer = buf = self.temporary_identifier()
+        self.indent()
+        self.pull_locals(macro_frame, indent=False)
+        self.writeline('%s = []' % buf)
+        self.blockvisit(node.body, macro_frame, indent=False)
+        self.writeline("return TemplateData(u''.join(%s))" % buf)
+        self.outdent()
         self.newline()
         if frame.toplevel:
             self.write('context[%r] = ' % node.name)
@@ -614,7 +621,7 @@
         self.write('l_%s = Macro(macro, %r, (%s), (' % (node.name, node.name,
                                                        arg_tuple))
         for arg in node.defaults:
-            self.visit(arg)
+            self.visit(arg, macro_frame)
             self.write(', ')
         self.write('), %s, %s)' % (
             macro_frame.accesses_arguments and '1' or '0',
@@ -625,7 +632,13 @@
         call_frame = self.function_scoping(node, frame)
         args = call_frame.arguments
         self.writeline('def call(%s):' % ', '.join(args), node)
-        self.blockvisit(node.body, call_frame, node)
+        call_frame.buffer = buf = self.temporary_identifier()
+        self.indent()
+        self.pull_locals(call_frame, indent=False)
+        self.writeline('%s = []' % buf)
+        self.blockvisit(node.body, call_frame, indent=False)
+        self.writeline("return TemplateData(u''.join(%s))" % buf)
+        self.outdent()
         arg_tuple = ', '.join(repr(x.name) for x in node.args)
         if len(node.args) == 1:
             arg_tuple += ','
@@ -647,7 +660,7 @@
         filter_frame.inspect(node.iter_child_nodes())
 
         aliases = self.collect_shadowed(filter_frame)
-        self.pull_locals(filter_frame, True)
+        self.pull_locals(filter_frame, indent=False)
         filter_frame.buffer = buf = self.temporary_identifier()
 
         self.writeline('%s = []' % buf, node)
diff --git a/jinja2/nodes.py b/jinja2/nodes.py
index 89c6fa2..ecc3f1e 100644
--- a/jinja2/nodes.py
+++ b/jinja2/nodes.py
@@ -35,6 +35,17 @@
     '-':        operator.neg
 }
 
+_cmpop_to_func = {
+    'eq':       operator.eq,
+    'ne':       operator.ne,
+    'gt':       operator.gt,
+    'gteq':     operator.ge,
+    'lt':       operator.lt,
+    'lteq':     operator.le,
+    'in':       operator.contains,
+    'notin':    lambda a, b: not operator.contains(a, b)
+}
+
 
 class Impossible(Exception):
     """Raised if the node could not perform a requested action."""
@@ -484,6 +495,14 @@
     """{{ foo == bar }}, {{ foo >= bar }} etc."""
     fields = ('expr', 'ops')
 
+    def as_const(self):
+        result = value = self.expr.as_const()
+        for op in self.ops:
+            new_value = op.expr.as_const()
+            result = _cmpop_to_func[op.op](value, new_value)
+            value = new_value
+        return result
+
 
 class Operand(Helper):
     """Operator + expression."""
diff --git a/jinja2/optimizer.py b/jinja2/optimizer.py
index 5877c6f..4dbd5d9 100644
--- a/jinja2/optimizer.py
+++ b/jinja2/optimizer.py
@@ -123,6 +123,7 @@
 
     def visit_For(self, node, context):
         """Loop unrolling for iterable constant values."""
+        fallback = self.generic_visit(node.copy(), context)
         try:
             iterable = self.visit(node.iter, context).as_const()
             # we only unroll them if they have a length and are iterable
@@ -131,11 +132,8 @@
             # we also don't want unrolling if macros are defined in it
             if node.find(nodes.Macro) is not None:
                 raise TypeError()
-            # XXX: add support for loop test clauses in the optimizer
-            if node.test is not None:
-                raise TypeError()
         except (nodes.Impossible, TypeError):
-            return self.generic_visit(node, context)
+            return fallback
 
         parent = context.get('loop')
         context.push()
@@ -157,6 +155,20 @@
             else:
                 raise AssertionError('unexpected assignable node')
 
+        if node.test is not None:
+            filtered_sequence = []
+            for item in iterable:
+                context.push()
+                assign(node.target, item)
+                try:
+                    rv = self.visit(node.test.copy(), context).as_const()
+                except:
+                    return fallback
+                context.pop()
+                if rv:
+                    filtered_sequence.append(item)
+            iterable = filtered_sequence
+
         try:
             try:
                 for item, loop in LoopContext(iterable, parent, True):
diff --git a/jinja2/runtime.py b/jinja2/runtime.py
index 676c1f5..fb2802e 100644
--- a/jinja2/runtime.py
+++ b/jinja2/runtime.py
@@ -15,7 +15,7 @@
 
 
 __all__ = ['subscribe', 'LoopContext', 'StaticLoopContext', 'TemplateContext',
-           'Macro', 'IncludedTemplate', 'Undefined']
+           'Macro', 'IncludedTemplate', 'Undefined', 'TemplateData']
 
 
 def subscribe(obj, argument):
@@ -190,6 +190,9 @@
             self.parent
         )
 
+    def __len__(self):
+        return self._length
+
     def make_static(self):
         return self
 
@@ -231,7 +234,7 @@
             arguments['l_caller'] = caller
         if self.catch_all:
             arguments['l_arguments'] = kwargs
-        return TemplateData(u''.join(self._func(**arguments)))
+        return self._func(**arguments)
 
     def __repr__(self):
         return '<%s %s>' % (