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

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

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


##########
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:
   Thank you for the careful design review. Your points on *why* slot visibility
   matters, and on keeping **CompileStack** as the single administrator of 
name↔slot
   state, were correct. The earlier `InstanceofFlowSlotPublisher` +
   `putVariable`/`removeVariable` design bent that model; the current code 
follows
   the direction you described.
   
   ---
   
   ## 1. When does CompileStack visibility matter?
   
   > *if the variable is visible via compile stack even though it should not via
   > scoping, does it matter?*
   
   | Case | Does it matter? | Approach now |
   |------|-----------------|--------------|
   | Name unused on that path | Mostly LVT / debug ranges | Improved as a side 
effect of hide + push/pop; not the primary driver |
   | **Same name redeclared** where the pattern is not live | **Yes** — name 
must be free | `CompileStack.hideVariable` (name free, index unchanged) |
   | **Reference that should be dynamic** | **Yes** — `AsmClassGenerator` looks 
up locals **by name** | (1) path hide; (2) if `accessedVariable instanceof 
DynamicVariable`, never load a same-named local (also covers mid-condition 
`\|\|` RHS) |
   
   So action is required exactly for the two cases you flagged: 
**redeclaration** and
   **reference to a name that scoping has already rejected**.
   
   ---
   
   ## 2. Entry point is CompileStack; publisher removed
   
   > *Entry point should be CompileStack… hide… name free, index unchanged… 
push/pop…*
   > *I don’t think we need the bindings or the publisher here anymore.*
   
   **Done:**
   
   - `InstanceofFlowSlotPublisher` **deleted**.
   - `putVariable` / `removeVariable` **deleted** (they bypassed push/pop and 
implied
     CompileStack was no longer the sole producer of locals).
   - CompileStack now owns:
     - `recordPatternVariable` — slots produced only by `evaluateInstanceof`;
     - `hideVariable` — name free, index kept;
     - `hidePatternVariablesExcept(candidates, live)` — hide among names
       **introduced by this condition** (outer pattern names untouched);
     - `patternVariablesIntroducedSince(before)` — **identity**-based 
introduced set
       (same name re-bound by a later condition still path-hides correctly).
   
   **Control flow (if/else):**
   
   ```text
   // condition on outer frame → slots defined & recorded
   pushBreakable()                    // arms only (GROOVY-7463)
     pushState / hide(except whenTrue)  / then / pop
     pushState / hide(except whenFalse) / else / pop
   pop()                              // outer still holds the slots
   hidePatternVariablesExcept(introduced, survivors)  // permanent on outer
   ```
   
   Then/else use ordinary **push → hide → pop**. Survivors need no put-back: the
   condition runs **before** `pushBreakable`, so after `pop` the outer map still
   holds those slots and we only hide non-survivors.
   
   ---
   
   ## 3. asm must not re-run flow analysis
   
   > *the asm part should not have to redo things that had been done before 
already*  
   > *InstanceofFlowBindings should not be required concept wise* [in classgen]
   
   **Agreed.** Layering now:
   
   | Phase | Role |
   |-------|------|
   | `VariableScopeVisitor` + `InstanceofFlowBindings` (internal analysis) | 
Declare names into scopes; attach **`InstanceofPathLiveNames`** (path-live 
*name* sets) as AST metadata |
   | classgen.asm | Read metadata only; drive CompileStack hide/push/pop — 
**no** `InstanceofFlowBindings.of(...)` |
   
   `InstanceofFlowBindings` remains a private analysis helper of the visitor 
(and its
   unit tests). It is not a cross-phase concept that asm re-executes. Classgen 
only
   consumes the **enriched** name sets on the AST — the same idea as using
   `VariableScope` after the visitor, adapted to flow-sensitive bindings.
   
   ---
   
   ## 4. No SlotPublisher in VariableScopeVisitor
   
   > *Mentioning InstanceofFlowSlotPublisher in VariableScopeVisitor is imho 
not good*
   
   **Done.** That class is gone; VSV Javadoc no longer names any classgen.asm 
type.
   The visitor documents only its own products: scopes + 
`InstanceofPathLiveNames`.
   
   ---
   
   ## 5. Lazy allocation
   
   > *how does the lazy approach solve the problem? … as long as this is a 
break of
   > the design it should not wait*
   
   Agreed that a design break should not be deferred. The redesign above is 
meant to
   **remove the layering break now**, not postpone it.
   
   Lazy define (slot only after the condition, on the taken path) could still
   simplify `evaluateInstanceof` later, but it is **not** required to restore
   CompileStack’s push/pop ownership. Early define remains for short-circuit 
`&&`
   RHS; path visibility is administered by CompileStack hide/push/pop + 
metadata.
   Lazy allocation is therefore an optional micro-optimisation, not a fix for a
   broken design.
   
   ---
   
   ## 6. Tests you asked for
   
   > *(1) shouldNotCompile where s is visible*  
   > *(2) ScopeTest: declare s where not visible, assert local*  
   > *(3) same for classgen / InstanceofTest*
   
   **Added:**
   
   - **`InstanceofScopeTest`:** redeclare where pattern is *not* live → new 
local
     (`accessedVariable` local and ≠ pattern); where pattern *is* live →
     `already contains a variable of the name s`.
   - **`InstanceofTest`:** matching runtime/classgen cases; successive ifs 
reusing
     the same pattern name; isolated pattern expression then later if.
   
   Visibility matrix and flow-bindings tests remain green; main-module `:test` 
is green
   on this change set.
   
   ---
   
   ## 7. Point → change (summary)
   
   | Your point | Response |
   |------------|----------|
   | Explain *why*, not only *what* | Free name for redeclare + correct load vs 
dynamic access |
   | CompileStack entry; hide + push/pop; drop publisher | Done; publisher 
deleted |
   | put/remove bend the model | Removed; no put-back API |
   | asm must not redo visitor work | Metadata only; no re-analysis |
   | Don’t name later asm types from VSV | Done |
   | Design break must not wait on “lazy” | Layering fixed without lazy |
   | Redeclare tests (visible / not) | Scope + runtime tests added |
   
   Happy to adjust further if any of the above still looks like a strong bend 
of the
   CompileStack model from your point of view.





> 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