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

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

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


##########
src/test/groovy/groovy/InstanceofTest.groovy:
##########
@@ -223,4 +223,279 @@ final class InstanceofTest {
         }
         assert y == 'foobar'
     }
+
+    // GROOVY-12242: Java-aligned flow scoping for negated instanceof (JEP 394)
+    @Test
+    void testVariableScopeNegatedElse() {
+        def f = { Object o ->
+            if (!(o instanceof String s)) {
+                return 'not'
+            } else {
+                return s.toUpperCase()
+            }
+        }
+        assert f('hi') == 'HI'
+        assert f(1) == 'not'
+    }
+
+    // GROOVY-12242: pattern variable remains in scope after abrupt then-branch
+    @Test
+    void testVariableScopeEarlyReturn() {
+        def f = { Object o ->
+            if (!(o instanceof String s)) return 'early'
+            return s.toUpperCase()
+        }
+        assert f('hi') == 'HI'
+        assert f(42) == 'early'
+    }
+
+    // GROOVY-12242: pattern variable remains after else that cannot complete 
normally
+    @Test
+    void testVariableScopeAfterAbruptElse() {
+        def f = { Object o ->
+            if (o instanceof String s) {
+                // matched
+            } else {
+                return 'no'
+            }
+            return s.toUpperCase()
+        }
+        assert f('ab') == 'AB'
+        assert f(9) == 'no'
+    }
+
+    // GROOVY-12242: pattern variable must not leak after a declaration 
statement
+    @Test
+    void testVariableNoLeakAfterDeclaration() {
+        def err = shouldFail MissingPropertyException, '''
+            class C {
+                Object m(Object o) {
+                    boolean b = (o instanceof String s)
+                    return s
+                }
+            }
+            new C().m('hi')
+        '''
+        assert err.message =~ /No such property: s/
+    }
+
+    // GROOVY-12242: pattern variable must not leak after an expression 
statement
+    @Test
+    void testVariableNoLeakAfterExpressionStatement() {
+        def err = shouldFail MissingPropertyException, '''
+            class C {
+                Object m(Object o) {
+                    o instanceof String s && s.length() > 0
+                    return s
+                }
+            }
+            new C().m('hi')
+        '''
+        assert err.message =~ /No such property: s/
+    }
+
+    // GROOVY-12242: true branch of negated instanceof must not see the 
pattern local
+    // (CompileStack polarity must match VariableScope — no silent null ALOAD)
+    @Test
+    void testVariableNegatedIfBranchNotInScope() {
+        def err = shouldFail MissingPropertyException, '''
+            class C {
+                Object m(Object o) {
+                    if (!(o instanceof String s)) {
+                        return s
+                    }
+                    return 'matched'
+                }
+            }
+            new C().m(1)
+        '''
+        assert err.message =~ /No such property: s/
+    }
+
+    // GROOVY-12242: true-path binding of left of || is not in scope on the 
right (Java)
+    @Test
+    void testVariableOrRightHandSideNotInScope() {
+        def shell = GroovyShell.withConfig {
+            ast groovy.transform.TypeChecked
+        }
+        def err = shouldFail shell, '''
+            @groovy.transform.TypeChecked
+            class C {
+                static void m(Object o) {
+                    if (o instanceof String s || s.length() > 0) {
+                    }
+                }
+            }
+        '''
+        assert err.message =~ /The variable .s. is undeclared|Apparent 
variable .s./
+    }
+
+    // GROOVY-12242: false-path binding is in scope on the right of || (Java)
+    @Test
+    void testVariableOrRightHandSideFalsePathInScope() {
+        def f = { Object o ->
+            // when o is String, left is false, right sees s
+            return (!(o instanceof String s) || s.isEmpty())
+        }
+        assert f('') == true
+        assert f('x') == false
+        assert f(1) == true // left true → short-circuit, s not needed
+    }
+
+    // GROOVY-12242: ternary false branch must not see true-path pattern 
variable
+    @Test
+    void testVariableTernaryFalseBranchNotInScope() {
+        def shell = GroovyShell.withConfig {
+            ast groovy.transform.TypeChecked
+        }
+        def err = shouldFail shell, '''
+            @groovy.transform.TypeChecked
+            class C {
+                static Object m(Object o) {
+                    return o instanceof String s ? 'yes' : s
+                }
+            }
+        '''
+        assert err.message =~ /The variable .s. is undeclared|Apparent 
variable .s./
+    }
+
+    // GROOVY-12242: dynamic ternary false branch must not load a pattern local
+    @Test
+    void testVariableTernaryFalseBranchNotInScopeDynamic() {
+        def err = shouldFail MissingPropertyException, '''
+            class C {
+                Object m(Object o) {
+                    return o instanceof String s ? 'yes' : s
+                }
+            }
+            new C().m(1)
+        '''
+        assert err.message =~ /No such property: s/
+    }
+
+    // GROOVY-12242: ternary true branch sees pattern variable
+    @Test
+    void testVariableTernaryTrueBranch() {
+        def f = { Object o -> o instanceof String s ? s.toUpperCase() : 'no' }
+        assert f('ab') == 'AB'
+        assert f(1) == 'no'
+    }
+
+    // GROOVY-12242: reassignment of pattern variable (not implicitly final, 
JEP 394)
+    @Test
+    void testVariableReassignment() {
+        Object o = 'hi'
+        if (o instanceof String s) {
+            s = s + '!'
+            assert s == 'hi!'
+        } else {
+            assert false
+        }
+    }
+
+    // GROOVY-12242: pattern variable shadows a field only where in scope
+    @Test
+    void testVariableFieldShadowing() {
+        def obj = new Object() {
+            String s = 'field'
+            def test(Object o) {
+                if (o instanceof String s) {
+                    return "pv=$s"
+                }
+                return "field=$s"
+            }
+        }
+        assert obj.test('x') == 'pv=x'
+        assert obj.test(1) == 'field=field'
+    }
+
+    // GROOVY-12242: && chain uses pattern variable on subsequent operands
+    @Test
+    void testVariableAndChain() {
+        Object o = 'hello'
+        assert (o instanceof String s && s.length() > 3 && s.startsWith('h'))
+        assert !(o instanceof String s && s.length() > 99)
+    }
+
+    // GROOVY-12242: while body can use true-path pattern variable
+    @Test
+    void testVariableWhileBody() {
+        Object o = 'ab'
+        def n = 0
+        while (o instanceof String s && s.length() > 0) {
+            n += 1
+            o = s.substring(1)
+        }
+        assert n == 2
+        assert o == ''
+    }
+
+    // GROOVY-12242: reuse the same pattern variable name in successive 
statements
+    @Test
+    void testVariableNameReuse() {
+        Object a = 'x', b = 1
+        def r = []
+        if (a instanceof String s) r << s
+        if (b instanceof Integer s) r << s
+        assert r == ['x', 1]
+    }
+
+    // GROOVY-12242: type-checked flow scoping for early return
+    @Test
+    void testVariableScopeEarlyReturnTypeChecked() {
+        def shell = GroovyShell.withConfig {
+            ast groovy.transform.TypeChecked
+        }
+        assert shell.evaluate('''
+            @groovy.transform.TypeChecked
+            class C {
+                static String m(Object o) {
+                    if (!(o instanceof String s)) return 'early'
+                    return s.toUpperCase()
+                }
+            }
+            assert C.m('hi') == 'HI'
+            assert C.m(1) == 'early'
+            true
+        ''')
+    }
+
+    // GROOVY-12242: type-checked — positive instanceof still not in else
+    @Test
+    void testVariableScopePositiveNotInElseTypeChecked() {
+        def shell = GroovyShell.withConfig {
+            ast groovy.transform.TypeChecked
+        }
+        def err = shouldFail shell, '''
+            Number n = 12345
+            if (n instanceof Integer i) {
+            } else {
+                i.toString()
+            }
+        '''
+        assert err.message =~ /The variable .i. is undeclared/
+    }
+
+    // GROOVY-12242: type-checked — negated instanceof is in else
+    @Test
+    void testVariableScopeNegatedInElseTypeChecked() {
+        def shell = GroovyShell.withConfig {
+            ast groovy.transform.TypeChecked
+        }
+        assert shell.evaluate('''
+            @groovy.transform.TypeChecked
+            class C {
+                static String m(Object o) {
+                    if (!(o instanceof String s)) {
+                        return 'not'
+                    } else {
+                        return s.toUpperCase()
+                    }
+                }
+            }
+            assert C.m('hi') == 'HI'
+            assert C.m(1) == 'not'
+            true
+        ''')
+    }

Review Comment:
   [...]
   >     * **`InstanceofFlowSlotPublisher`** in `StatementWriter` solves a 
distinct
   >       **bytecode-level problem**: `evaluateInstanceof` allocates a 
CompileStack slot
   >       during condition evaluation (required for `&&` short-circuit RHS). 
Without the
   >       publisher, that slot would be visible to both branches in the 
bytecode, even
   >       though `VariableScopeVisitor` has already excluded it from the 
else-branch's
   >       scope. The publisher hides and re-exposes slots to keep 
_CompileStack slot
   >       visibility_ consistent with the _VariableScope declarations_ — it 
does not
   >       independently decide which variables are in scope.
   
   my problem with this is  that it describes what it does, but it does not 
postulate why it has to be like that. And then it sounds like trying to solve a 
tooling problem with more tooling instead of fixing the original tool in the 
first place. There is then a number of things to mention as result of that.
   
   * if the variable is visible via compile stack even though it should not via 
scoping, does it matter? 
     - If the variable is not used then I guess the line number table would be 
wrong for it, though it is not proofed yet, that the current solution does this 
correctly. My guess would be that not.
     - If there is another variable of the same name in the block or we 
reference a former hidden variable. Here I do see that we have to do something.
   * Entry point for should be CompileStack.
     -  InstanceofFlowSlotPublisher seems to exist parallel to it. Assume we 
give the compile stack the ability to hide a variable, meaning name becomes 
free, but index is unchanged, plush pushing the state and poping when done. 
Does this mean more code that is using compile stack? Probably yes, but I don´t 
think we need the bindings or the publisher here anymore. 
   *  CompileStack is the administrator for handling bytecode tables and 
variable slots and the design idea is to have something like a stack for the 
different states. Those do not exist in bytecode but they are reflected by what 
slots are free and how the tables are written.Bypassing the push/pop logic as 
the additions of removeVariable and putVariable bend that idea. But putVariable 
basically tells me compile stack is no longer producing the variables in all 
cases (it comes originally from compile stack in this code, but nobody 
guarantees that) and removeVariable is supposed to be handled by push/pop
   * I am not against adding helper classes, but the asm part should not have 
to redo things that had been done before already. Which means any classgen.asm 
class referencing something from even just classgen, that redoes something 
non-trivial already done by a previous visitor, that was supposed to enrich the 
AST with that kind of information, is looking really really problematic to me. 
It would be different if in a asm class we suddenly need to expand the AST, 
maybe create a parallel AST, and need to change information in there. But this 
is not the case here and also sounds like a bad idea frankly.
   
   
   > In short: `VariableScopeVisitor` says _which names are in scope_; 
`InstanceofFlowSlotPublisher` says _which bytecode slots are exposed_ on each 
branch. Both are driven by the same `InstanceofFlowBindings` analysis to stay 
in sync.
   
   as explained before, InstanceofFlowBindings should not be required concept 
wise.
    
   > The Javadoc on `VariableScopeVisitor` (class level and `visitIfElse`) now 
makes this split explicit and is the canonical description of the design.
   
   Mentioning InstanceofFlowSlotPublisher in VariableScoppeVisitor is imho not 
good. If there is a need for that, then there is a problem. You should never 
have to name a specific class in a classgen transform that is specific to a 
later classgen transform, especially not in classgen.asm. 
    
   > Longer term, if the dynamic compiler were changed to allocate pattern 
slots lazily (only after the full condition has been evaluated, with initial 
visibility controlled by the taken branch), the slot publisher could be removed 
entirely. That would require changes to `CompileStack` / `evaluateInstanceof` 
and is left as a tracked future improvement.
   
   how does the lazy approach solve the problem? Also as long as this code is a 
break of the design it should not wait... Unless you convince me that it is not 
breaking, not even bending strongly.
   
   > `InstanceofScopeTest.groovy`       **New** — AST-level scope assertions 
via `VariableExpression.getAccessedVariable()`, full visibility matrix
   [...]
   > `VariableScopeVisitor.java`        Class-level and `visitIfElse` Javadoc 
clarified: authoritative-source role; relationship with 
`InstanceofFlowSlotPublisher`; JLS §6.3.2.2 rule labels on inline comments
   
   I think the testing has now improved a lot... but I am still missing 
something InstanceofScopeTest does only positive testing, VariableScopeVisitor 
does some negative and positive testing. I think we are missing still two tings 
related to additional variable declarations. (1) some shouldNotCompileTests 
that check you cannot declare a variable s where s is supposed to be visible 
through the instance-of. (2) more tests in InstanceOfScopeTest that use 
variable declarations of s where s is not visible through the instance-of and 
asserting they are local. And we have to ensure those examples actually are 
done correctly in class generation as well, thus similar tests should also be 
added to InstanceOfTest
   





> 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