Repository: quarkusio/quarkus
Issue: Qute extension method with parameters not working in loop

**Describe the bug**
Given the following extension method: 

    static Set<String> tags(Item item, String category) { return item.tagsForCategory(category); }

The following works:

    {item.tags('foo')} // This prints ['foo', 'bar']

But the following doesn't:

    {#each item.tags('foo')}
      {it}
    {/each}

The Quarkus build fails with:

    io.quarkus.qute.TemplateException: Found template problems (1):
        [1] Incorrect expression: it
	- property/method [tags('foo')] not found on class [org.acme.qute.Item] nor handled by an extension method

The same problem occurs with the {#for} and {#let} tags.

**Expected behavior**

I would expect that I can use tags such {#each} and {#for} to loop over the results of a virtual method.

**To Reproduce**

See https://github.com/eamelink/quarkus-qute-issue-reproducer , and then the /qute/items path.
**Environment 
-  Darwin MacBook-Pro.local 20.1.0 Darwin Kernel Version 20.1.0: Sat Oct 31 00:07:11 PDT 2020; root:xnu-7195.50.7~2/RELEASE_X86_64 x86_64
 - Output of `java -version`: openjdk version "15" 2020-09-15
 - Quarkus version or git rev: Quarkus 1.9.2


Changed files:
independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionBlock.java
independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionNode.java
extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java
extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java
independent-projects/qute/core/src/main/java/io/quarkus/qute/SetSectionHelper.java
extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java
extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ValidationFailuresTest.java
extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeInfos.java
extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLoopTest.java
independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java
independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java
independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java
independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java
extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLoopFailureTest.java
independent-projects/qute/core/src/main/java/io/quarkus/qute/Scope.java
independent-projects/qute/core/src/main/java/io/quarkus/qute/WhenSectionHelper.java
independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java
independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java
independent-projects/qute/core/src/main/java/io/quarkus/qute/Expression.java
independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java
independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java

diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java
index acafbda3fe1..406f97da1b5 100644
--- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java
+++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java
@@ -421,7 +421,10 @@ void validateExpressions(TemplatesAnalysisBuildItem templatesAnalysis, BeanArchi
             List<TemplateExtensionMethodBuildItem> templateExtensionMethods,
             List<TypeCheckExcludeBuildItem> excludes,
             BuildProducer<IncorrectExpressionBuildItem> incorrectExpressions,
-            BuildProducer<ImplicitValueResolverBuildItem> implicitClasses) {
+            BuildProducer<ImplicitValueResolverBuildItem> implicitClasses,
+            BeanRegistrationPhaseBuildItem registrationPhase,
+            // This producer is needed to ensure the correct ordering, ie. this build step must be executed before the ArC validation step
+            BuildProducer<BeanConfiguratorBuildItem> configurators) {
 
         IndexView index = beanArchiveIndex.getIndex();
         Function<String, String> templateIdToPathFun = new Function<String, String>() {
@@ -431,20 +434,39 @@ public String apply(String id) {
             }
         };
 
+        // IMPLEMENTATION NOTE: 
+        // We do not support injection of synthetic beans with names 
+        // Dependency on the ValidationPhaseBuildItem would result in a cycle in the build chain
+        Map<String, BeanInfo> namedBeans = registrationPhase.getContext().beans().withName()
+                .collect(toMap(BeanInfo::getName, Function.identity()));
+
         // Map implicit class -> set of used members
         Map<ClassInfo, Set<String>> implicitClassToMembersUsed = new HashMap<>();
 
-        for (TemplateAnalysis analysis : templatesAnalysis.getAnalysis()) {
+        for (TemplateAnalysis templateAnalysis : templatesAnalysis.getAnalysis()) {
             // Maps an expression generated id to the last match of an expression (i.e. the type of the last part)
             Map<Integer, Match> generatedIdsToMatches = new HashMap<>();
-            for (Expression expression : analysis.expressions) {
-                if (skip(expression)) {
+
+            for (Expression expression : templateAnalysis.expressions) {
+                if (expression.isLiteral()) {
                     continue;
                 }
-                generatedIdsToMatches.put(expression.getGeneratedId(),
-                        validateNestedExpressions(analysis, null, new HashMap<>(), templateExtensionMethods, excludes,
-                                incorrectExpressions, expression, index, implicitClassToMembersUsed, templateIdToPathFun,
-                                generatedIdsToMatches));
+                if (expression.hasNamespace()) {
+                    if (expression.getNamespace().equals(EngineProducer.INJECT_NAMESPACE)) {
+                        validateInjectExpression(templateAnalysis, expression, index, incorrectExpressions,
+                                templateExtensionMethods, excludes, namedBeans, generatedIdsToMatches,
+                                implicitClassToMembersUsed,
+                                templateIdToPathFun);
+                    } else {
+                        continue;
+                    }
+                } else {
+                    generatedIdsToMatches.put(expression.getGeneratedId(),
+                            validateNestedExpressions(templateAnalysis, null, new HashMap<>(), templateExtensionMethods,
+                                    excludes,
+                                    incorrectExpressions, expression, index, implicitClassToMembersUsed, templateIdToPathFun,
+                                    generatedIdsToMatches));
+                }
             }
         }
 
@@ -514,19 +536,23 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI
         Info root = iterator.next();
 
         if (rootClazz == null) {
-            // {foo.name} 
             if (root.isTypeInfo()) {
+                // E.g. |org.acme.Item|
                 match.clazz = root.asTypeInfo().rawClass;
                 match.type = root.asTypeInfo().resolvedType;
                 if (root.asTypeInfo().hint != null) {
-                    processHints(templateAnalysis, root.asTypeInfo().hint, match, index, expression, generatedIdsToMatches);
+                    processHints(templateAnalysis, root.asTypeInfo().hint, match, index, expression, generatedIdsToMatches,
+                            incorrectExpressions);
                 }
             } else {
                 if (root.isProperty() && root.asProperty().hint != null) {
                     // Root is not a type info but a property with hint
-                    // We need to reset the iterator
-                    processHints(templateAnalysis, root.asProperty().hint, match, index, expression, generatedIdsToMatches);
-                    iterator = parts.iterator();
+                    // E.g. 'it<loop#123>' and 'STATUS<when#123>'
+                    if (processHints(templateAnalysis, root.asProperty().hint, match, index, expression,
+                            generatedIdsToMatches, incorrectExpressions)) {
+                        // In some cases it's necessary to reset the iterator
+                        iterator = parts.iterator();
+                    }
                 } else {
                     // No type info available 
                     results.put(expression.toOriginalString(), match);
@@ -587,9 +613,6 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI
                     break;
                 } else {
                     match.type = resolveType(member, match, index);
-                    if (match.type.kind() == org.jboss.jandex.Type.Kind.PRIMITIVE) {
-                        break;
-                    }
                     if (match.type.kind() == Type.Kind.CLASS || match.type.kind() == Type.Kind.PARAMETERIZED_TYPE) {
                         match.clazz = index.getClassByName(match.type.name());
                     }
@@ -597,9 +620,13 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI
                         String hint = info.asProperty().hint;
                         if (hint != null) {
                             // For example a loop section needs to validate the type of an element
-                            processHints(templateAnalysis, hint, match, index, expression, generatedIdsToMatches);
+                            processHints(templateAnalysis, hint, match, index, expression, generatedIdsToMatches,
+                                    incorrectExpressions);
                         }
                     }
+                    if (match.type != null && match.type.kind() == org.jboss.jandex.Type.Kind.PRIMITIVE) {
+                        break;
+                    }
                 }
             } else {
                 LOGGER.debugf(
@@ -699,81 +726,43 @@ private void produceExtensionMethod(IndexView index, BuildProducer<TemplateExten
                 priority, namespace));
     }
 
-    @BuildStep
-    void validateInjectNamespaceExpressions(TemplatesAnalysisBuildItem analysis, BeanArchiveIndexBuildItem beanArchiveIndex,
+    private void validateInjectExpression(TemplateAnalysis templateAnalysis, Expression expression, IndexView index,
             BuildProducer<IncorrectExpressionBuildItem> incorrectExpressions,
-            List<TemplateExtensionMethodBuildItem> templateExtensionMethods,
-            List<TypeCheckExcludeBuildItem> excludes,
-            BeanRegistrationPhaseBuildItem registrationPhase,
-            // This producer is needed to ensure the correct ordering, ie. this build step must be executed before the ArC validation step
-            BuildProducer<BeanConfiguratorBuildItem> configurators,
-            BuildProducer<ImplicitValueResolverBuildItem> implicitClasses) {
-
-        IndexView index = beanArchiveIndex.getIndex();
-        Function<String, String> templateIdToPathFun = new Function<String, String>() {
-            @Override
-            public String apply(String id) {
-                return findTemplatePath(analysis, id);
-            }
-        };
-        Map<TemplateAnalysis, Set<Expression>> injectExpressions = collectNamespaceExpressions(analysis,
-                EngineProducer.INJECT_NAMESPACE);
-
-        if (!injectExpressions.isEmpty()) {
-            // IMPLEMENTATION NOTE: 
-            // We do not support injection of synthetic beans with names 
-            // Dependency on the ValidationPhaseBuildItem would result in a cycle in the build chain
-            Map<String, BeanInfo> namedBeans = registrationPhase.getContext().beans().withName()
-                    .collect(toMap(BeanInfo::getName, Function.identity()));
-
-            // Map implicit class -> set of used members
-            Map<ClassInfo, Set<String>> implicitClassToMembersUsed = new HashMap<>();
-
-            for (Entry<TemplateAnalysis, Set<Expression>> entry : injectExpressions.entrySet()) {
-
-                Map<Integer, Match> generatedIdsToMatches = new HashMap<>();
-
-                for (Expression expression : entry.getValue()) {
-                    Expression.Part firstPart = expression.getParts().get(0);
-                    if (firstPart.isVirtualMethod()) {
-                        incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(),
-                                "The inject: namespace must be followed by a bean name",
-                                expression.getOrigin()));
-                        continue;
-                    }
-                    String beanName;
-                    if (expression.hasNamespace()) {
-                        beanName = firstPart.getName();
-                    } else {
-                        // inject:foo.labels<for-element> => foo
-                        String firstInfoPart = Expressions.splitTypeInfoParts(firstPart.getTypeInfo()).get(0);
-                        beanName = firstInfoPart.substring(EngineProducer.INJECT_NAMESPACE.length() + 1,
-                                firstInfoPart.length());
-                    }
-
-                    BeanInfo bean = namedBeans.get(beanName);
-                    if (bean != null) {
-                        if (expression.getParts().size() == 1) {
-                            // Only the bean needs to be validated
-                            continue;
-                        }
-                        generatedIdsToMatches.put(expression.getGeneratedId(),
-                                validateNestedExpressions(entry.getKey(), bean.getImplClazz(), new HashMap<>(),
-                                        templateExtensionMethods, excludes, incorrectExpressions, expression, index,
-                                        implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches));
+            List<TemplateExtensionMethodBuildItem> templateExtensionMethods, List<TypeCheckExcludeBuildItem> excludes,
+            Map<String, BeanInfo> namedBeans, Map<Integer, Match> generatedIdsToMatches,
+            Map<ClassInfo, Set<String>> implicitClassToMembersUsed, Function<String, String> templateIdToPathFun) {
+        Expression.Part firstPart = expression.getParts().get(0);
+        if (firstPart.isVirtualMethod()) {
+            incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(),
+                    "The inject: namespace must be followed by a bean name",
+                    expression.getOrigin()));
+            return;
+        }
+        String beanName;
+        if (expression.hasNamespace()) {
+            beanName = firstPart.getName();
+        } else {
+            // inject:foo.labels<loop-element> => foo
+            String firstInfoPart = Expressions.splitTypeInfoParts(firstPart.getTypeInfo()).get(0);
+            beanName = firstInfoPart.substring(EngineProducer.INJECT_NAMESPACE.length() + 1,
+                    firstInfoPart.length());
+        }
 
-                    } else {
-                        // User is injecting a non-existing bean
-                        incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(),
-                                beanName, null, expression.getOrigin()));
-                    }
-                }
+        BeanInfo bean = namedBeans.get(beanName);
+        if (bean != null) {
+            if (expression.getParts().size() == 1) {
+                // Only the bean needs to be validated
+                return;
             }
+            generatedIdsToMatches.put(expression.getGeneratedId(),
+                    validateNestedExpressions(templateAnalysis, bean.getImplClazz(), new HashMap<>(),
+                            templateExtensionMethods, excludes, incorrectExpressions, expression, index,
+                            implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches));
 
-            for (Entry<ClassInfo, Set<String>> entry : implicitClassToMembersUsed.entrySet()) {
-                implicitClasses.produce(new ImplicitValueResolverBuildItem(entry.getKey(),
-                        new TemplateDataBuilder().addIgnore(buildIgnorePattern(entry.getValue())).build()));
-            }
+        } else {
+            // User is injecting a non-existing bean
+            incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(),
+                    beanName, null, expression.getOrigin()));
         }
     }
 
@@ -1078,35 +1067,58 @@ private static Type resolveType(AnnotationTarget member, Match match, IndexView
         return matchType;
     }
 
-    static void processHints(TemplateAnalysis templateAnalysis, String helperHint, Match match, IndexView index,
-            Expression expression, Map<Integer, Match> generatedIdsToMatches) {
-        if (LoopSectionHelper.Factory.HINT.equals(helperHint)) {
+    /**
+     * 
+     * @param templateAnalysis
+     * @param helperHint
+     * @param match
+     * @param index
+     * @param expression
+     * @param generatedIdsToMatches
+     * @param incorrectExpressions
+     * @return {@code true} if it is necessary to reset the type info part iterator
+     */
+    static boolean processHints(TemplateAnalysis templateAnalysis, String helperHint, Match match, IndexView index,
+            Expression expression, Map<Integer, Match> generatedIdsToMatches,
+            BuildProducer<IncorrectExpressionBuildItem> incorrectExpressions) {
+        if (helperHint == null || helperHint.isEmpty()) {
+            return false;
+        }
+        if (helperHint.equals(LoopSectionHelper.Factory.HINT_ELEMENT)) {
             // Iterable<Item>, Stream<Item> => Item
             // Map<String,Long> => Entry<String,Long>
-            processLoopHint(match, index, expression);
-        } else if (helperHint != null && helperHint.startsWith(WhenSectionHelper.Factory.HINT_PREFIX)) {
+            processLoopElementHint(match, index, expression, incorrectExpressions);
+        } else if (helperHint.startsWith(LoopSectionHelper.Factory.HINT_PREFIX)) {
+            Integer iterableExprId = Integer
+                    .valueOf(helperHint.substring(LoopSectionHelper.Factory.HINT_PREFIX.length(), helperHint.length() - 1));
+            Expression valueExpr = templateAnalysis.findExpression(iterableExprId);
+            if (valueExpr != null) {
+                Match valueExprMatch = generatedIdsToMatches.get(valueExpr.getGeneratedId());
+                if (valueExprMatch != null) {
+                    match.type = valueExprMatch.type;
+                    match.clazz = valueExprMatch.clazz;
+                }
+            }
+        } else if (helperHint.startsWith(WhenSectionHelper.Factory.HINT_PREFIX)) {
             // If a value expression resolves to an enum we attempt to use the enum type to validate the enum constant  
             // This basically transforms the type info "ON<when:12345>" into something like "|org.acme.Status|.ON"
             Integer valueExprId = Integer
                     .valueOf(helperHint.substring(WhenSectionHelper.Factory.HINT_PREFIX.length(), helperHint.length() - 1));
-            Expression valueExpr = null;
-            for (Expression e : templateAnalysis.expressions) {
-                if (e.getGeneratedId() == valueExprId) {
-                    valueExpr = e;
-                    break;
-                }
-            }
+            Expression valueExpr = templateAnalysis.findExpression(valueExprId);
             if (valueExpr != null) {
                 Match valueExprMatch = generatedIdsToMatches.get(valueExpr.getGeneratedId());
                 if (valueExprMatch != null && valueExprMatch.clazz.isEnum()) {
                     match.type = valueExprMatch.type;
                     match.clazz = valueExprMatch.clazz;
+                    return true;
                 }
             }
         }
+        return false;
     }
 
-    static void processLoopHint(Match match, IndexView index, Expression expression) {
+    static void processLoopElementHint(Match match, IndexView index, Expression expression,
+            BuildProducer<IncorrectExpressionBuildItem> incorrectExpressions) {
         if (match.type.name().equals(DotNames.INTEGER)) {
             return;
         }
@@ -1146,7 +1158,7 @@ static void processLoopHint(Match match, IndexView index, Expression expression)
             // CompletionStage<List<Item>> => List<Item>
             match.type = extractMatchType(closure, Names.COMPLETION_STAGE, firstParamType);
             match.clazz = index.getClassByName(match.type.name());
-            processLoopHint(match, index, expression);
+            processLoopElementHint(match, index, expression, incorrectExpressions);
             return;
         }
 
@@ -1154,10 +1166,10 @@ static void processLoopHint(Match match, IndexView index, Expression expression)
             match.type = matchType;
             match.clazz = index.getClassByName(match.type.name());
         } else {
-            throw new TemplateException(expression.getOrigin(), String.format(
-                    "Unsupported iterable type found in [%s]\\n\\t- matching type: %s \\n\\t- found in template [%s] on line %s",
-                    expression.toOriginalString(),
-                    match.type, expression.getOrigin().getTemplateId(), expression.getOrigin().getLine()));
+            incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(),
+                    "Unsupported iterable type found: " + match.type, expression.getOrigin()));
+            match.type = null;
+            match.clazz = null;
         }
     }
 
@@ -1540,16 +1552,4 @@ private static boolean isExcluded(TypeCheck check, List<TypeCheckExcludeBuildIte
         return false;
     }
 
-    private boolean skip(Expression expression) {
-        if (expression.hasNamespace() || expression.isLiteral()) {
-            return true;
-        }
-        String typeInfo = expression.getParts().get(0).getTypeInfo();
-        if (typeInfo != null && typeInfo.contains(":")) {
-            // An expression bound to a namespace expression
-            return true;
-        }
-        return false;
-    }
-
 }
diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java
index a235359a5c2..2597dde031e 100644
--- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java
+++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java
@@ -2,7 +2,6 @@
 
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 
 import io.quarkus.builder.item.SimpleBuildItem;
 import io.quarkus.qute.Expression;
@@ -27,16 +26,25 @@ static class TemplateAnalysis {
         // Path or other user-defined id; may be null
         public final String id;
         public final String generatedId;
-        public final Set<Expression> expressions;
+        public final List<Expression> expressions;
         public final String path;
 
-        public TemplateAnalysis(String id, String generatedId, Set<Expression> expressions, String path) {
+        public TemplateAnalysis(String id, String generatedId, List<Expression> expressions, String path) {
             this.id = id;
             this.generatedId = generatedId;
             this.expressions = expressions;
             this.path = path;
         }
 
+        Expression findExpression(Integer id) {
+            for (Expression expression : expressions) {
+                if (expression.getGeneratedId() == id) {
+                    return expression;
+                }
+            }
+            return null;
+        }
+
         @Override
         public int hashCode() {
             final int prime = 31;
diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeInfos.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeInfos.java
index 32312f62ace..fe04f9a76d7 100644
--- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeInfos.java
+++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeInfos.java
@@ -66,13 +66,14 @@ static Info create(String typeInfo, Expression.Part part, IndexView index, Funct
                 return new TypeInfo(typeInfo, part, helperHint(typeInfo.substring(endIdx, typeInfo.length())), resolvedType,
                         rawClass);
             }
-        } else if (part.isVirtualMethod()) {
-            return new VirtualMethodInfo(typeInfo, part.asVirtualMethod());
         } else {
             String hint = helperHint(typeInfo);
             if (hint != null) {
                 typeInfo = typeInfo.substring(0, typeInfo.indexOf(LEFT_ANGLE));
             }
+            if (part.isVirtualMethod() || Expressions.isVirtualMethod(typeInfo)) {
+                return new VirtualMethodInfo(typeInfo, part.asVirtualMethod());
+            }
             return new PropertyInfo(typeInfo, part, hint);
         }
     }
diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLoopFailureTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLoopFailureTest.java
index 732a3690568..a5f6b382faf 100644
--- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLoopFailureTest.java
+++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLoopFailureTest.java
@@ -1,5 +1,7 @@
 package io.quarkus.qute.deployment.typesafe;
 
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
 import org.jboss.shrinkwrap.api.ShrinkWrap;
@@ -22,7 +24,20 @@ public class TypeSafeLoopFailureTest {
                             + "{#for foo in list}"
                             + "{foo.name}={foo.ages}"
                             + "{/}"), "templates/foo.html"))
-            .setExpectedException(TemplateException.class);
+            .assertException(t -> {
+                Throwable e = t;
+                TemplateException te = null;
+                while (e != null) {
+                    if (e instanceof TemplateException) {
+                        te = (TemplateException) e;
+                        break;
+                    }
+                    e = e.getCause();
+                }
+                assertNotNull(te);
+                assertTrue(te.getMessage().contains("Found template problems (1)"), te.getMessage());
+                assertTrue(te.getMessage().contains("foo.ages"), te.getMessage());
+            });
 
     @Test
     public void testValidation() {
diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLoopTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLoopTest.java
index 88a847419cd..9a59463a0a8 100644
--- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLoopTest.java
+++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLoopTest.java
@@ -2,7 +2,9 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
 import javax.inject.Inject;
 
@@ -13,6 +15,7 @@
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 import io.quarkus.qute.Template;
+import io.quarkus.qute.TemplateExtension;
 import io.quarkus.qute.deployment.Foo;
 import io.quarkus.qute.deployment.MyFooList;
 import io.quarkus.test.QuarkusUnitTest;
@@ -22,7 +25,7 @@ public class TypeSafeLoopTest {
     @RegisterExtension
     static final QuarkusUnitTest config = new QuarkusUnitTest()
             .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
-                    .addClasses(Foo.class, MyFooList.class)
+                    .addClasses(Foo.class, MyFooList.class, Item.class, Extensions.class)
                     .addAsResource(new StringAsset("{@java.util.List<io.quarkus.qute.deployment.Foo> list}"
                             + "{@io.quarkus.qute.deployment.MyFooList fooList}"
                             + "{#for foo in list}"
@@ -33,16 +36,33 @@ public class TypeSafeLoopTest {
                             + "{foo.name}={foo.age}={foo.charlie.name}"
                             + "{/}"
                             + "::"
-                            + "{fooList.get(0).name}"), "templates/foo.html"));
+                            + "{fooList.get(0).name}"
+                            + "::"
+                            + "{#for item in items}{#each item.tags('foo')}{it}{/each}{/for}"), "templates/foo.html"));
 
     @Inject
     Template foo;
 
     @Test
     public void testValidation() {
-        assertEquals("bravo=10=BRAVO::alpha=1=ALPHA::alpha",
-                foo.data("list", Collections.singletonList(new Foo("bravo", 10l)))
-                        .data("fooList", new MyFooList(new Foo("alpha", 1l))).render());
+        List<Foo> foos = Collections.singletonList(new Foo("bravo", 10l));
+        MyFooList myFoos = new MyFooList(new Foo("alpha", 1l));
+        List<Item> items = Arrays.asList(new Item());
+        assertEquals("bravo=10=BRAVO::alpha=1=ALPHA::alpha::foobar",
+                foo.data("list", foos, "fooList", myFoos, "items", items).render());
+    }
+
+    static class Item {
+
+    }
+
+    @TemplateExtension
+    static class Extensions {
+
+        static List<String> tags(Item item, String foo) {
+            return Arrays.asList(foo, "bar");
+        }
+
     }
 
 }
diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ValidationFailuresTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ValidationFailuresTest.java
index df3911f9018..801b5fc4beb 100644
--- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ValidationFailuresTest.java
+++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ValidationFailuresTest.java
@@ -34,7 +34,8 @@ public class ValidationFailuresTest {
                             + "{movie.toNumber(age)}"
                             + "{#each movie.mainCharacters}{it.boom(1)}{/}"
                             // Template extension method must accept one param
-                            + "{movie.toNumber}"),
+                            + "{movie.toNumber}"
+                            + "{#each movie}{it}{/each}"),
                             "templates/movie.html"))
             .assertException(t -> {
                 Throwable e = t;
@@ -47,7 +48,7 @@ public class ValidationFailuresTest {
                     e = e.getCause();
                 }
                 assertNotNull(te);
-                assertTrue(te.getMessage().contains("Found template problems (8)"), te.getMessage());
+                assertTrue(te.getMessage().contains("Found template problems (9)"), te.getMessage());
                 assertTrue(te.getMessage().contains("movie.foo"), te.getMessage());
                 assertTrue(te.getMessage().contains("movie.getName('foo')"), te.getMessage());
                 assertTrue(te.getMessage().contains("movie.findService(age)"), te.getMessage());
@@ -56,6 +57,9 @@ public class ValidationFailuresTest {
                 assertTrue(te.getMessage().contains("movie.toNumber(age)"), te.getMessage());
                 assertTrue(te.getMessage().contains("it.boom(1)"), te.getMessage());
                 assertTrue(te.getMessage().contains("movie.toNumber"), te.getMessage());
+                assertTrue(
+                        te.getMessage().contains("Unsupported iterable type found: io.quarkus.qute.deployment.typesafe.Movie"),
+                        te.getMessage());
             });
 
     @Test
diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java
index cc8cf0fc70e..881aaa69cd5 100644
--- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java
+++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java
@@ -9,7 +9,6 @@
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Optional;
-import java.util.Set;
 import java.util.concurrent.CompletionStage;
 import java.util.function.Consumer;
 
@@ -113,7 +112,7 @@ public TemplateInstance instance() {
         }
 
         @Override
-        public Set<Expression> getExpressions() {
+        public List<Expression> getExpressions() {
             throw new UnsupportedOperationException("Injected templates do not support getExpressions()");
         }
 
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expression.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expression.java
index c1f4f55a1f1..b75c7e2da19 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expression.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expression.java
@@ -89,8 +89,8 @@ interface Part {
          * <li>type info that represents a fully qualified type name (including type parameters) -
          * {@code |TYPE_INFO|<section-hint>};
          * for example {@code |org.acme.Foo|},
-         * {@code |java.util.List<org.acme.Label>|} and {@code |org.acme.Foo|<for-element>}</li>
-         * <li>property; for example {@code foo} and {@code foo<for-element>}</li>
+         * {@code |java.util.List<org.acme.Label>|} and {@code |org.acme.Foo|<when#123>}</li>
+         * <li>property; for example {@code foo} and {@code foo<loop#123>}</li>
          * <li>virtual method; for example {@code foo.call(|org.acme.Bar|)}</li>
          * </ul>
          * 
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionNode.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionNode.java
index 8ec248def74..182afdbbc80 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionNode.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionNode.java
@@ -1,7 +1,7 @@
 package io.quarkus.qute;
 
 import java.util.Collections;
-import java.util.Set;
+import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 import java.util.function.Function;
@@ -50,8 +50,8 @@ Engine getEngine() {
         return engine;
     }
 
-    public Set<Expression> getExpressions() {
-        return Collections.singleton(expression);
+    public List<Expression> getExpressions() {
+        return Collections.singletonList(expression);
     }
 
     @Override
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java
index d448aa419a9..f89e55c7b52 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java
@@ -99,7 +99,8 @@ CompletionStage<ResultNode> nextElement(Object element, int index, boolean hasNe
 
     public static class Factory implements SectionHelperFactory<LoopSectionHelper> {
 
-        public static final String HINT = "<for-element>";
+        public static final String HINT_ELEMENT = "<loop-element>";
+        public static final String HINT_PREFIX = "<loop#";
         private static final String ALIAS = "alias";
         private static final String IN = "in";
         private static final String ITERABLE = "iterable";
@@ -130,17 +131,25 @@ public Scope initializeBlock(Scope previousScope, BlockInfo block) {
                 if (iterable == null) {
                     iterable = ValueResolvers.THIS;
                 }
+                // foo.items
+                // > |org.acme.Foo|.items<loop-element>
+                previousScope.setLastPartHint(HINT_ELEMENT);
                 Expression iterableExpr = block.addExpression(ITERABLE, iterable);
+                previousScope.setLastPartHint(null);
+
                 String alias = block.getParameters().get(ALIAS);
+
                 if (iterableExpr.hasTypeInfo()) {
+                    // it.name
+                    // > it<loop#123>.name
                     alias = alias.equals(Parameter.EMPTY) ? DEFAULT_ALIAS : alias;
                     Scope newScope = new Scope(previousScope);
-                    newScope.put(alias, iterableExpr.collectTypeInfo() + HINT);
+                    newScope.putBinding(alias, alias + HINT_PREFIX + iterableExpr.getGeneratedId() + ">");
                     return newScope;
                 } else {
                     // Make sure we do not try to validate against the parent context
                     Scope newScope = new Scope(previousScope);
-                    newScope.put(alias, null);
+                    newScope.putBinding(alias, null);
                     return newScope;
                 }
             } else {
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java
index 0840748b259..b09b5ba8360 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java
@@ -465,7 +465,7 @@ private void flushTag() {
             int spaceIdx = content.indexOf(" ");
             String key = content.substring(spaceIdx + 1, content.length());
             String value = content.substring(1, spaceIdx);
-            currentScope.put(key, Expressions.TYPE_INFO_SEPARATOR + value + Expressions.TYPE_INFO_SEPARATOR);
+            currentScope.putBinding(key, Expressions.TYPE_INFO_SEPARATOR + value + Expressions.TYPE_INFO_SEPARATOR);
             sectionBlockStack.peek().addNode(new ParameterDeclarationNode(content, origin(0)));
 
         } else {
@@ -705,8 +705,9 @@ static ExpressionImpl parseExpression(Supplier<Integer> idGenerator, String valu
         }
         List<Part> parts = new ArrayList<>(strParts.size());
         Part first = null;
-        for (String strPart : strParts) {
-            Part part = createPart(idGenerator, namespace, first, strPart, scope, origin);
+        Iterator<String> strPartsIterator = strParts.iterator();
+        while (strPartsIterator.hasNext()) {
+            Part part = createPart(idGenerator, namespace, first, strPartsIterator, scope, origin);
             if (!isValidIdentifier(part.getName())) {
                 StringBuilder builder = new StringBuilder("Invalid identifier found [");
                 builder.append(value).append("]");
@@ -724,8 +725,10 @@ static ExpressionImpl parseExpression(Supplier<Integer> idGenerator, String valu
         return new ExpressionImpl(idGenerator.get(), namespace, parts, Result.NOT_FOUND, origin);
     }
 
-    private static Part createPart(Supplier<Integer> idGenerator, String namespace, Part first, String value, Scope scope,
+    private static Part createPart(Supplier<Integer> idGenerator, String namespace, Part first,
+            Iterator<String> strPartsIterator, Scope scope,
             Origin origin) {
+        String value = strPartsIterator.next();
         if (Expressions.isVirtualMethod(value)) {
             String name = Expressions.parseVirtualMethodName(value);
             List<String> strParams = new ArrayList<>(Expressions.parseVirtualMethodParams(value));
@@ -756,10 +759,15 @@ private static Part createPart(Supplier<Integer> idGenerator, String namespace,
         if (namespace != null) {
             typeInfo = first != null ? value : namespace + NAMESPACE_SEPARATOR + value;
         } else if (first == null) {
-            typeInfo = scope.getBindingType(value);
+            // Try to find the binding type for the first part of the expression
+            typeInfo = scope.getBinding(value);
         } else if (first.getTypeInfo() != null) {
             typeInfo = value;
         }
+        if (typeInfo != null && !strPartsIterator.hasNext() && scope.getLastPartHint() != null) {
+            // If type info present then append hint to the last part
+            typeInfo += scope.getLastPartHint();
+        }
         return new ExpressionImpl.PartImpl(value, typeInfo);
     }
 
@@ -937,7 +945,7 @@ public String toString() {
     public void addParameter(String name, String type) {
         // {@org.acme.Foo foo}
         Scope currentScope = scopeStack.peek();
-        currentScope.put(name, Expressions.TYPE_INFO_SEPARATOR + type + Expressions.TYPE_INFO_SEPARATOR);
+        currentScope.putBinding(name, Expressions.TYPE_INFO_SEPARATOR + type + Expressions.TYPE_INFO_SEPARATOR);
     }
 
     private static final SectionHelper ROOT_SECTION_HELPER = new SectionHelper() {
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Scope.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Scope.java
index b6199638ee2..9f11ccc2b91 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Scope.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Scope.java
@@ -7,39 +7,41 @@ public class Scope {
 
     public static final Scope EMPTY = new Scope(null) {
         @Override
-        public void put(String binding, String type) {
+        public void putBinding(String binding, String type) {
             throw new UnsupportedOperationException("Immutable empty scope");
         }
     };
 
-    private Scope parentScope;
+    private final Scope parentScope;
     private Map<String, String> bindings;
     private Map<String, Object> attributes;
+    // TODO: add proper API to handle this
+    private String lastPartHint;
 
     public Scope(Scope parentScope) {
         this.parentScope = parentScope;
     }
 
-    public void put(String binding, String type) {
+    public void putBinding(String binding, String type) {
         if (bindings == null)
             bindings = new HashMap<>();
         bindings.put(binding, type);
     }
 
-    public String getBindingType(String binding) {
+    public String getBinding(String binding) {
         // we can contain null types to override outer scopes
         if (bindings != null
                 && bindings.containsKey(binding))
             return bindings.get(binding);
-        return parentScope != null ? parentScope.getBindingType(binding) : null;
+        return parentScope != null ? parentScope.getBinding(binding) : null;
     }
 
     public String getBindingTypeOrDefault(String binding, String defaultValue) {
-        String type = getBindingType(binding);
+        String type = getBinding(binding);
         return type != null ? type : defaultValue;
     }
 
-    public void setAttribute(String name, Object value) {
+    public void putAttribute(String name, Object value) {
         if (attributes == null) {
             attributes = new HashMap<>();
         }
@@ -50,4 +52,12 @@ public Object getAttribute(String name) {
         return attributes != null ? attributes.get(name) : null;
     }
 
+    public String getLastPartHint() {
+        return lastPartHint;
+    }
+
+    public void setLastPartHint(String lastPartHint) {
+        this.lastPartHint = lastPartHint;
+    }
+
 }
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionBlock.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionBlock.java
index 43301f7f332..7bd99bb5afb 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionBlock.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionBlock.java
@@ -4,7 +4,6 @@
 import io.quarkus.qute.TemplateNode.Origin;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -54,8 +53,8 @@ public boolean isEmpty() {
         return nodes.isEmpty();
     }
 
-    Set<Expression> getExpressions() {
-        Set<Expression> expressions = new HashSet<>();
+    List<Expression> getExpressions() {
+        List<Expression> expressions = new ArrayList<>();
         expressions.addAll(this.expressions.values());
         for (TemplateNode node : nodes) {
             expressions.addAll(node.getExpressions());
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java
index 0d83cd3f77f..3862f02e0f5 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java
@@ -89,6 +89,14 @@ default boolean hasParameter(String name) {
             return getParameters().containsKey(name);
         }
 
+        /**
+         * Parse and register an expression for the specified parameter.
+         * 
+         * @param param
+         * @param value
+         * @return a new expression
+         * @see SectionInitContext#getExpression(String)
+         */
         Expression addExpression(String param, String value);
 
     }
@@ -98,6 +106,10 @@ default boolean hasParameter(String name) {
      */
     public interface SectionInitContext extends ParserDelegate {
 
+        /**
+         * 
+         * @return the parameters of the main block
+         */
         default public Map<String, String> getParameters() {
             return getBlocks().get(0).parameters;
         }
@@ -110,8 +122,20 @@ default public String getParameter(String name) {
             return getParameters().get(name);
         }
 
+        /**
+         * 
+         * @param parameterName
+         * @return an expression registered for the specified param name, or {@code null}
+         * @see BlockInfo#addExpression(String, String)
+         */
         public Expression getExpression(String parameterName);
 
+        /**
+         * Parse the specified value. The expression is not registered in the template.
+         * 
+         * @param value
+         * @return a new expression
+         */
         public Expression parseValue(String value);
 
         public List<SectionBlock> getBlocks();
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java
index dbfe84b0beb..63ed6847c55 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java
@@ -2,7 +2,6 @@
 
 import io.quarkus.qute.SectionHelper.SectionResolutionContext;
 import java.util.ArrayList;
-import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
@@ -53,8 +52,8 @@ public String toString() {
         return builder.toString();
     }
 
-    public Set<Expression> getExpressions() {
-        Set<Expression> expressions = new HashSet<>();
+    public List<Expression> getExpressions() {
+        List<Expression> expressions = new ArrayList<>();
         for (SectionBlock block : blocks) {
             expressions.addAll(block.getExpressions());
         }
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SetSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SetSectionHelper.java
index 473bdb285e8..091eddd7343 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SetSectionHelper.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SetSectionHelper.java
@@ -68,7 +68,7 @@ public Scope initializeBlock(Scope previousScope, BlockInfo block) {
                 Scope newScope = new Scope(previousScope);
                 for (Entry<String, String> entry : block.getParameters().entrySet()) {
                     Expression expr = block.addExpression(entry.getKey(), entry.getValue());
-                    newScope.put(entry.getKey(), expr.collectTypeInfo());
+                    newScope.putBinding(entry.getKey(), expr.collectTypeInfo());
                 }
                 return newScope;
             } else {
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java
index 2de0e141f89..710a74dcd50 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java
@@ -1,7 +1,7 @@
 package io.quarkus.qute;
 
+import java.util.List;
 import java.util.Optional;
-import java.util.Set;
 
 /**
  * Represents an immutable template definition.
@@ -115,9 +115,9 @@ default String render() {
 
     /**
      * 
-     * @return an immutable set of expressions used in the template
+     * @return an immutable list of expressions used in the template
      */
-    Set<Expression> getExpressions();
+    List<Expression> getExpressions();
 
     /**
      * The id is unique for the engine instance.
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java
index 848c1711486..ae039b48365 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java
@@ -3,7 +3,6 @@
 import io.smallrye.mutiny.Multi;
 import java.util.List;
 import java.util.Optional;
-import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 import java.util.concurrent.ExecutionException;
@@ -31,7 +30,7 @@ public TemplateInstance instance() {
     }
 
     @Override
-    public Set<Expression> getExpressions() {
+    public List<Expression> getExpressions() {
         return root.getExpressions();
     }
 
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java
index ae92a72b658..2036805350e 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java
@@ -1,8 +1,8 @@
 package io.quarkus.qute;
 
 import java.util.Collections;
+import java.util.List;
 import java.util.Optional;
-import java.util.Set;
 import java.util.concurrent.CompletionStage;
 
 /**
@@ -19,10 +19,10 @@ public interface TemplateNode {
 
     /**
      * 
-     * @return a set of expressions
+     * @return a list of expressions
      */
-    default Set<Expression> getExpressions() {
-        return Collections.emptySet();
+    default List<Expression> getExpressions() {
+        return Collections.emptyList();
     }
 
     /**
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/WhenSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/WhenSectionHelper.java
index dd71707dc65..9d1a93fa5a0 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/WhenSectionHelper.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/WhenSectionHelper.java
@@ -127,24 +127,34 @@ public Scope initializeBlock(Scope previousScope, BlockInfo block) {
                 Expression valueExpr = block.addExpression(VALUE, value);
                 if (valueExpr.hasTypeInfo()) {
                     // If type info is available we do add the expression id 
-                    previousScope.setAttribute(VALUE_EXPR_ID, valueExpr.getGeneratedId());
+                    previousScope.putAttribute(VALUE_EXPR_ID, valueExpr.getGeneratedId());
                 }
             } else if (ELSE.equals(block.getLabel())) {
                 // No special handling required for "else"
             } else if (IS.equals(block.getLabel()) || CASE.equals(block.getLabel())) {
                 Object valueExprId = previousScope.getAttribute(VALUE_EXPR_ID);
-                for (String param : block.getParameters().values()) {
+                int added = 0;
+                Iterator<String> it = block.getParameters().values().iterator();
+                while (it.hasNext()) {
+                    String param = it.next();
+                    if (added == 0 && it.hasNext()) {
+                        // Skip the operator param
+                        continue;
+                    }
+                    added++;
                     if (valueExprId != null) {
                         // This could be an enum switch - we need to add a hint in order to validate the enum constants properly 
-                        String binding = previousScope.getBindingType(param);
-                        if (binding == null) {
-                            binding = param;
+                        String previousBinding = previousScope.getBinding(param);
+                        String newBinding = previousBinding;
+                        if (newBinding == null) {
+                            newBinding = param;
                         }
                         // Append hint to the existing binding if needed
                         // E.g. ON -> ON<when:12345>
-                        binding += HINT_PREFIX + valueExprId + ">";
-                        previousScope.put(param, binding);
+                        newBinding += HINT_PREFIX + valueExprId + ">";
+                        previousScope.putBinding(param, newBinding);
                         block.addExpression(param, param);
+                        previousScope.putBinding(param, previousBinding);
                     } else {
                         block.addExpression(param, param);
                     }
diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java
index 03469c82f85..178ee44761e 100644
--- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java
+++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java
@@ -14,7 +14,6 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
-import java.util.Set;
 import org.junit.jupiter.api.Test;
 
 public class ParserTest {
@@ -86,21 +85,30 @@ public void testTypeInfos() {
                 + "{/for}"
                 + "{foo.call(labels,bar)}"
                 + "{#when machine.status}{#is OK}..{#is NOK}{/when}");
-        Set<Expression> expressions = template.getExpressions();
+        List<Expression> expressions = template.getExpressions();
 
         assertExpr(expressions, "foo.name", 2, "|org.acme.Foo|.name");
-        assertExpr(expressions, "foo.items", 2, "|org.acme.Foo|.items");
-        assertExpr(expressions, "item.name", 2, "|org.acme.Foo|.items<for-element>.name");
+
+        Expression fooItems = find(expressions, "foo.items");
+        assertExpr(expressions, "foo.items", 2, "|org.acme.Foo|.items<loop-element>");
+        assertExpr(expressions, "item.name", 2, "item<loop#" + fooItems.getGeneratedId() + ">.name");
         assertExpr(expressions, "bar.name", 2, null);
-        assertExpr(expressions, "labels", 1, "|java.util.List<org.acme.Label>|");
-        assertExpr(expressions, "it.name", 2, "|java.util.List<org.acme.Label>|<for-element>.name");
+
+        Expression labels = find(expressions, "labels");
+        assertExpr(expressions, "labels", 1, "|java.util.List<org.acme.Label>|<loop-element>");
+        assertExpr(expressions, "it.name", 2, "it<loop#" + labels.getGeneratedId() + ">.name");
+
         assertExpr(expressions, "inject:bean.name", 2, "inject:bean.name");
-        assertExpr(expressions, "inject:bean.labels", 2, "inject:bean.labels");
-        assertExpr(expressions, "it.value", 2, "inject:bean.labels<for-element>.value");
+
+        Expression beanLabels = find(expressions, "inject:bean.labels");
+        assertExpr(expressions, "inject:bean.labels", 2, "inject:bean.labels<loop-element>");
+        assertExpr(expressions, "it.value", 2, "it<loop#" + beanLabels.getGeneratedId() + ">.value");
+
         assertExpr(expressions, "foo.bar", 2, "|org.acme.Foo|.bar");
         assertExpr(expressions, "baz.name", 2, "|org.acme.Foo|.bar.name");
         assertExpr(expressions, "foo.baz", 2, null);
         assertExpr(expressions, "foo.call(labels,bar)", 2, "|org.acme.Foo|.call(labels,bar)");
+
         Expression machineStatusExpr = find(expressions, "machine.status");
         assertExpr(expressions, "OK", 1, "OK<when#" + machineStatusExpr.getGeneratedId() + ">");
     }
@@ -253,6 +261,18 @@ public void testTextNodeCollapse() {
         assertEquals("next", ((TextNode) rootNodes.get(2)).getValue());
     }
 
+    @Test
+    public void testGetExpressions() {
+        Template template = Engine.builder().addDefaults().build()
+                .parse("{foo}{#each items}{it.name}{#for foo in foos}{foo.name}{/for}{/each}");
+        List<Expression> expressions = template.getExpressions();
+        assertEquals("foo", expressions.get(0).toOriginalString());
+        assertEquals("items", expressions.get(1).toOriginalString());
+        assertEquals("it.name", expressions.get(2).toOriginalString());
+        assertEquals("foos", expressions.get(3).toOriginalString());
+        assertEquals("foo.name", expressions.get(4).toOriginalString());
+    }
+
     private void assertParserError(String template, String message, int line) {
         Engine engine = Engine.builder().addDefaultSectionHelpers().build();
         try {
@@ -266,14 +286,14 @@ private void assertParserError(String template, String message, int line) {
         }
     }
 
-    private void assertExpr(Set<Expression> expressions, String value, int parts, String typeInfo) {
+    private void assertExpr(List<Expression> expressions, String value, int parts, String typeInfo) {
         Expression expr = find(expressions, value);
         assertEquals(parts, expr.getParts().size());
         assertEquals(typeInfo,
                 expr.collectTypeInfo());
     }
 
-    private Expression find(Set<Expression> expressions, String val) {
+    private Expression find(List<Expression> expressions, String val) {
         return expressions.stream().filter(e -> e.toOriginalString().equals(val)).findAny().get();
     }