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

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

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


##########
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;
+    }
+
+    private static Set<String> names(final List<VariableExpression> vars) {
+        if (vars.isEmpty()) return Collections.emptySet();
+        Set<String> result = new LinkedHashSet<>(vars.size());
+        for (VariableExpression ve : vars) {
+            result.add(ve.getName());
+        }
+        return result;
+    }
+
+    /**
+     * Analyses {@code expression} for definite {@code instanceof} pattern 
bindings.
+     *
+     * @param expression a boolean condition (may be a {@link 
BooleanExpression} wrapper)
+     * @return the true/false binding sets; never {@code null}
+     */
+    public static InstanceofFlowBindings of(final Expression expression) {
+        if (expression == null) {
+            return EMPTY;
+        }
+        return analyse(expression);
+    }
+
+    /**
+     * Returns {@code true} if {@code expression} contains any JEP&nbsp;394 
type
+     * pattern ({@code e instanceof T t} or {@code e !instanceof T t}), 
including
+     * nested subexpressions. Used to decide whether expression-statement
+     * CompileStack isolation is required.
+     *
+     * @param expression any expression; {@code null} yields {@code false}
+     */
+    public static boolean containsPattern(final Expression expression) {
+        if (expression == null) return false;
+        boolean[] found = {false};
+        expression.visit(new CodeVisitorSupport() {
+            @Override
+            public void visitBinaryExpression(final BinaryExpression be) {
+                if (found[0]) return;
+                int op = be.getOperation().getType();
+                if ((op == Types.KEYWORD_INSTANCEOF || op == 
Types.COMPARE_NOT_INSTANCEOF)
+                        && isTypePattern(be.getRightExpression())) {
+                    found[0] = true;
+                    return;
+                }
+                super.visitBinaryExpression(be);
+            }
+        });
+        return found[0];
+    }
+
+    private static boolean isTypePattern(final Expression right) {
+        return right instanceof DeclarationExpression decl
+                && !decl.isMultipleAssignmentDeclaration()
+                && decl.getVariableExpression() != null;
+    }
+
+    private static InstanceofFlowBindings analyse(final Expression expression) 
{
+        Expression expr = expression;
+
+        // Unwrap BooleanExpression wrappers; NotExpression is handled below 
so that
+        // nested negations compose correctly.
+        while (expr instanceof BooleanExpression && !(expr instanceof 
NotExpression)) {
+            expr = ((BooleanExpression) expr).getExpression();
+        }
+
+        if (expr instanceof NotExpression not) {
+            return analyse(not.getExpression()).negated();
+        }
+
+        if (expr instanceof BinaryExpression binary) {
+            int op = binary.getOperation().getType();
+            if (op == Types.KEYWORD_INSTANCEOF) {
+                return ofInstanceof(binary);
+            }
+            if (op == Types.COMPARE_NOT_INSTANCEOF) {
+                // AST may still carry !instanceof before codegen rewrites it 
to !(… instanceof …).
+                return ofInstanceof(binary).negated();
+            }
+            if (op == Types.LOGICAL_AND) {
+                InstanceofFlowBindings left = 
analyse(binary.getLeftExpression());
+                InstanceofFlowBindings right = 
analyse(binary.getRightExpression());
+                // True path evaluates both; false path is not definite for 
either side alone.
+                return new InstanceofFlowBindings(
+                        union(left.whenTrue, right.whenTrue),
+                        List.of());
+            }
+            if (op == Types.LOGICAL_OR) {
+                InstanceofFlowBindings left = 
analyse(binary.getLeftExpression());
+                InstanceofFlowBindings right = 
analyse(binary.getRightExpression());
+                // False path evaluates both; true path is not definite for 
either side alone.
+                return new InstanceofFlowBindings(
+                        List.of(),
+                        union(left.whenFalse, right.whenFalse));
+            }
+        }

Review Comment:
   There are two methods with deliberately different traversal strategies:
   
   **`containsPattern(Expression)` — intentionally traverses the full subtree.**
   This method answers a conservative question: *"does any descendant node in 
this expression
   tree contain a type pattern?"* It is used for expression-statement isolation 
(deciding
   whether the statement's value needs CompileStack sandboxing). Even a pattern 
buried
   arbitrarily deep (e.g. inside a method-call argument: `print(o instanceof 
String s)`) may
   have caused `evaluateInstanceof` to define a CompileStack slot. That 
worst-case question
   requires a full subtree scan. Short-circuit optimisation (`found[0]` 
early-return) keeps
   the typical cost proportional to the depth of the first match, not the full 
tree size.
   
   **`analyse(Expression)` — does *not* traverse the full tree.** It is a 
recursive structural
   descent that follows only the operators that propagate *definite-assignment* 
(`instanceof`,
   `!instanceof`, `&&`, `||`, `!`) and returns `EMPTY` conservatively for any 
other shape.
   For a typical condition `o instanceof String s && s.length() > 0` the 
descent visits exactly
   three nodes.
   
   Both methods now have explicit **"Why a full subtree walk?"** / **"Why not a 
full subtree
   walk?"** Javadoc paragraphs to document this distinction for future 
contributors.
   





> 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