improved loop unrolling

--HG--
branch : trunk
diff --git a/jinja2/optimizer.py b/jinja2/optimizer.py
index 191ec58..ee2969c 100644
--- a/jinja2/optimizer.py
+++ b/jinja2/optimizer.py
@@ -83,7 +83,10 @@
     def visit_For(self, node, context):
         """Loop unrolling for iterable constant values."""
         try:
-            iterable = iter(self.visit(node.iter, context).as_const())
+            iterable = self.visit(node.iter, context).as_const()
+            # we only unroll them if they have a length and are iterable
+            iter(iterable)
+            len(iterable)
         except (nodes.Impossible, TypeError):
             return self.generic_visit(node, context)
 
@@ -107,12 +110,10 @@
             else:
                 raise AssertionError('unexpected assignable node')
 
-        # XXX: not covered cases:
-        #       - item is accessed by dynamic part in the iteration
         try:
             try:
-                for loop, item in LoopContext(iterable, parent):
-                    context['loop'] = loop
+                for loop, item in LoopContext(iterable, parent, True):
+                    context['loop'] = loop.make_static()
                     assign(node.target, item)
                     result.extend(self.visit(n, context)
                                   for n in deepcopy(node.body))
diff --git a/jinja2/runtime.py b/jinja2/runtime.py
index a79fac3..fd22395 100644
--- a/jinja2/runtime.py
+++ b/jinja2/runtime.py
@@ -14,7 +14,10 @@
     defaultdict = None
 
 
-__all__ = ['extends', 'subscribe', 'LoopContext', 'TemplateContext', 'Macro']
+# contains only the variables the template will import automatically, not the
+# objects injected by the evaluation loop (such as undefined objects)
+__all__ = ['extends', 'subscribe', 'LoopContext', 'StaticLoopContext',
+           'TemplateContext', 'Macro']
 
 
 def extends(template, namespace):
@@ -74,36 +77,74 @@
             return self.undefined_factory(key)
 
 
-class LoopContext(object):
+class LoopContextBase(object):
     """Helper for extended iteration."""
 
     def __init__(self, iterable, parent=None):
         self._iterable = iterable
+        self._length = None
         self.index0 = 0
         self.parent = parent
 
-    def __iter__(self):
-        for item in self._iterable:
-            yield self, item
-            self.index0 += 1
-
     first = property(lambda x: x.index0 == 0)
     last = property(lambda x: x.revindex0 == 0)
     index = property(lambda x: x.index0 + 1)
     revindex = property(lambda x: x.length)
     revindex0 = property(lambda x: x.length - 1)
+    length = property(lambda x: len(x))
 
-    @property
-    def length(self):
-        if not hasattr(self, '_length'):
+
+class LoopContext(LoopContextBase):
+
+    def __init__(self, iterable, parent=None, enforce_length=False):
+        self._iterable = iterable
+        self._length = None
+        self.index0 = 0
+        self.parent = parent
+        if enforce_length:
+            len(self)
+
+    def make_static(self):
+        """Return a static loop context for the optimizer."""
+        parent = None
+        if self.parent is not None:
+            parent = self.parent.make_static()
+        return StaticLoopContext(self.index0, self.length, parent)
+
+    def __iter__(self):
+        for item in self._iterable:
+            yield self, item
+            self.index0 += 1
+
+    def __len__(self):
+        if self._length is None:
             try:
                 length = len(self._iterable)
             except TypeError:
-                length = len(tuple(self._iterable))
+                self._iterable = tuple(self._iterable)
+                length = self.index0 + len(tuple(self._iterable))
             self._length = length
         return self._length
 
 
+class StaticLoopContext(LoopContextBase):
+
+    def __init__(self, index0, length, parent):
+        self.index0 = index0
+        self.parent = parent
+        self._length = length
+
+    def __repr__(self):
+        return 'StaticLoopContext(%r, %r, %r)' % (
+            self.index0,
+            self._length,
+            self.parent
+        )
+
+    def make_static(self):
+        return self
+
+
 class Macro(object):
     """
     Wraps a macor
@@ -139,3 +180,35 @@
         if self.catch_all:
             arguments['l_arguments'] = kwargs
         return u''.join(self.func(**arguments))
+
+
+class Undefined(object):
+    """The default undefined behavior."""
+
+    def __init__(self, name=None, attr=None):
+        if attr is None:
+            self._undefined_hint = '%r is undefined' % attr
+        elif name is None:
+            self._undefined_hint = 'attribute %r is undefined' % name
+        else:
+            self._undefined_hint = 'attribute %r of %r is undefined' \
+                                   % (attr, name)
+
+    def fail(self, *args, **kwargs):
+        raise TypeError(self._undefined_hint)
+    __getattr__ = __getitem__ = __add__ = __mul__ = __div__ = \
+    __realdiv__ = __floordiv__ = __mod__ = __pos__ = __neg__ = fail
+    del fail
+
+    def __unicode__(self):
+        return ''
+
+    def __repr__(self):
+        return 'Undefined'
+
+    def __len__(self):
+        return 0
+
+    def __iter__(self):
+        if 0:
+            yield None
diff --git a/test_optimizer.py b/test_optimizer.py
index ce0cb75..36f2011 100644
--- a/test_optimizer.py
+++ b/test_optimizer.py
@@ -20,7 +20,7 @@
     {% navigation = [('#foo', 'Foo'), ('#bar', 'Bar'), ('#baz', 42 * 2 + 23)] %}
     <ul>
     {% for key, value in navigation %}
-        <li>{{ loop.index }}: <a href="{{ key|e }}">{{ value|e }}</a></li>
+        <li>{{ test(loop) }}: <a href="{{ key|e }}">{{ value|e }}</a></li>
     {% endfor %}
     </ul>
 """)