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.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to