[ 
https://issues.apache.org/jira/browse/GROOVY-12242?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18103028#comment-18103028
 ] 

ASF GitHub Bot commented on GROOVY-12242:
-----------------------------------------

daniellansun commented on code in PR #2773:
URL: https://github.com/apache/groovy/pull/2773#discussion_r3740600398


##########
src/main/java/org/codehaus/groovy/classgen/InstanceofFlowBindings.java:
##########
@@ -0,0 +1,249 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.codehaus.groovy.classgen;
+
+import org.codehaus.groovy.ast.CodeVisitorSupport;
+import org.codehaus.groovy.ast.expr.BinaryExpression;
+import org.codehaus.groovy.ast.expr.BooleanExpression;
+import org.codehaus.groovy.ast.expr.DeclarationExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.NotExpression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.syntax.Types;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Flow-sensitive analysis of JEP 394 {@code instanceof} pattern bindings
+ * (GROOVY-12242).
+ * <p>
+ * This is pure <em>semantic</em> analysis: given a boolean expression, which
+ * pattern variables are <em>definitely bound</em> when the expression is
+ * {@code true} versus {@code false}? (Same idea as compiler “flow info” /
+ * JEP 394 flow scoping — not a bytecode construct.)
+ * <ul>
+ *   <li>{@link #of(Expression)} — true/false binding sets for a condition</li>
+ *   <li>{@link #containsPattern(Expression)} — nested type-pattern presence
+ *       (e.g. whether an expression statement needs CompileStack 
isolation)</li>
+ * </ul>
+ * Covered shapes: {@code e instanceof T t}, negation / {@code !instanceof},
+ * {@code &&} (union of true bindings), {@code ||} (union of false bindings).
+ * Other shapes contribute nothing (conservative).
+ * <p>
+ * Consumers:
+ * <ul>
+ *   <li>{@link VariableScopeVisitor} — declare names on the live path</li>
+ *   <li>{@link org.codehaus.groovy.classgen.asm.InstanceofFlowSlotPublisher} —
+ *       publish/hide CompileStack slots from these bindings</li>
+ * </ul>
+ *
+ * @see org.codehaus.groovy.classgen.asm.InstanceofFlowSlotPublisher
+ * @since 6.0.0
+ */
+public final class InstanceofFlowBindings {
+
+    private static final InstanceofFlowBindings EMPTY =
+            new InstanceofFlowBindings(List.of(), List.of());
+
+    private final List<VariableExpression> whenTrue;
+    private final List<VariableExpression> whenFalse;
+
+    private InstanceofFlowBindings(final List<VariableExpression> whenTrue,
+                                      final List<VariableExpression> 
whenFalse) {
+        this.whenTrue = whenTrue;
+        this.whenFalse = whenFalse;
+    }
+
+    /**
+     * Pattern variables that are definitely assigned when the analysed 
expression
+     * evaluates to {@code true}.
+     */
+    public List<VariableExpression> whenTrue() {
+        return whenTrue;
+    }
+
+    /**
+     * Pattern variables that are definitely assigned when the analysed 
expression
+     * evaluates to {@code false}.
+     */
+    public List<VariableExpression> whenFalse() {
+        return whenFalse;
+    }
+
+    /** Whether any pattern variable is bound on either path. */
+    public boolean isEmpty() {
+        return whenTrue.isEmpty() && whenFalse.isEmpty();
+    }
+
+    /**
+     * Names of pattern variables bound when the expression is {@code true}.
+     */
+    public Set<String> whenTrueNames() {
+        return names(whenTrue);
+    }
+
+    /**
+     * Names of pattern variables bound when the expression is {@code false}.
+     */
+    public Set<String> whenFalseNames() {
+        return names(whenFalse);
+    }
+
+    /**
+     * All pattern-variable names appearing in either path (stable encounter 
order).
+     */
+    public Set<String> allNames() {
+        if (isEmpty()) return Collections.emptySet();
+        Set<String> names = new LinkedHashSet<>(whenTrue.size() + 
whenFalse.size());
+        for (VariableExpression ve : whenTrue) names.add(ve.getName());
+        for (VariableExpression ve : whenFalse) names.add(ve.getName());
+        return names;
+    }

Review Comment:
   Yes, exactly. Calling `names(whenTrue)` followed by `names(whenFalse)` would 
allocate *two*
   intermediate `LinkedHashSet` instances (one per call to `names()`), only to 
merge them into
   a third result set. The direct loop populates a single `LinkedHashSet` in 
one pass.
   
   The Javadoc for `allNames()` has been updated to document this rationale 
explicitly:
   
   ```java
   /**
    * All pattern-variable names appearing in either path (stable encounter 
order).
    * <p>
    * Implemented by iterating both lists directly rather than composing
    * {@link #whenTrueNames()} and {@link #whenFalseNames()}: that would
    * allocate two intermediate {@link Set} objects only to merge them into a
    * third, whereas the direct loop allocates only the result set.
    */
   ```
   





> instanceof pattern variable scope is not aligned with Java flow scoping (JEP 
> 394)
> ---------------------------------------------------------------------------------
>
>                 Key: GROOVY-12242
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12242
>             Project: Groovy
>          Issue Type: Bug
>            Reporter: Daniel Sun
>            Priority: Major
>
> h2. Summary
> After {{instanceof}} type patterns landed in GROOVY-11229, pattern variables 
> were still scoped with a coarse lexical approximation. That diverges from 
> Java’s *flow scoping* (JEP 394): a pattern variable must be visible only 
> where the pattern has *definitely* matched.
> The gaps appear as:
>  # variables missing where Java allows them
>  # variables leaking past the statement that introduced them
>  # name resolution and bytecode disagreeing, so an “out of scope” use can 
> still load a local slot
> h2. Background
>  * GROOVY-11229 added {{e instanceof T t}} (parser, AST, store-on-match).
>  * Java (JEP 394 / JLS): scope follows boolean flow and abrupt completion, 
> not simple block poison.
>  * Groovy initially limited leakage with push/pop around statements, but did 
> not implement true/false-path binding or CompileStack polarity.
> h2. Problems (before the fix)
> ||#||Scenario||Java||Groovy (before)||
> |1|negated {{instanceof}} — use pattern var in else|in scope|missing|
> |2|negated {{instanceof}} + early {{return}} — use pattern var after if|in 
> scope|missing|
> |3|positive {{instanceof}} + abrupt else — use pattern var after if|in 
> scope|missing|
> |4|{{boolean b = (o instanceof String s)}} then use {{s}}|not in 
> scope|CompileStack leak (local still loadable)|
> |5|expression statement with pattern, then use pattern var|not in 
> scope|CompileStack leak|
> |6|type-checked: pattern var used on RHS of logical-or|error on RHS|often 
> accepted|
> |7|type-checked ternary false arm uses pattern var|error|often accepted|
> |8|negated {{instanceof}} — use pattern var in then-branch|not in scope|could 
> ALOAD unassigned local (null)|
> h2. Steps to reproduce
> h3. A. Negated instanceof — else branch (should see {{{}s{}}})
> {code:groovy}
> def f = { Object o ->
>     if (!(o instanceof String s)) {
>         return 'not'
>     } else {
>         return s.toUpperCase()   // expected: OK when o is String
>     }
> }
> assert f('hi') == 'HI'
> {code}
> h3. B. Early return after negation (should see {{s}} after if)
> {code:groovy}
> def f = { Object o ->
>     if (!(o instanceof String s)) return 'early'
>     return s.toUpperCase()       // expected: OK when o is String
> }
> assert f('hi') == 'HI'
> {code}
> h3. C. Leak after declaration (must *not* see {{{}s{}}})
> {code:groovy}
> class C {
>     Object m(Object o) {
>         boolean b = (o instanceof String s)
>         return s                 // expected: MissingPropertyException / 
> undeclared
>     }
> }
> new C().m('hi')
> {code}
> h3. D. Type-checked {{||}} RHS must not see true-path binding
> {code:groovy}
> @groovy.transform.TypeChecked
> class C {
>     static void m(Object o) {
>         if (o instanceof String s || s.length() > 0) {
>             // expected: undeclared / apparent variable s on RHS of ||
>         }
>     }
> }
> {code}
> h2. Expected behaviour
> Align with Java JEP 394 flow scoping for the common shapes:
>  * true-path bindings (e.g. {{{}e instanceof T t{}}}) live in then-blocks, 
> {{&&}} RHS, and ternary true arm
>  * false-path bindings (e.g. {{{}!(e instanceof T t){}}}) live in 
> else-blocks, after abrupt then, and the matching ternary arm
>  * pattern variables do not leak past the introducing statement (declaration 
> RHS, expression statement, …)
>  * VariableScope (names) and CompileStack (locals) agree on which path a 
> pattern local is live
> h2. Actual behaviour (before fix)
>  * Lexical push/pop approximated “no leak past statement” but not true/false 
> path polarity.
>  * CompileStack could keep pattern slots after VariableScope had dropped the 
> name (silent local load vs property miss).
>  * Negation and abrupt-completion cases from Java were not supported.
>  



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to