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

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

Copilot commented on code in PR #2489:
URL: https://github.com/apache/groovy/pull/2489#discussion_r3142882307


##########
src/main/java/org/codehaus/groovy/runtime/MultipleAssignmentSupport.java:
##########
@@ -0,0 +1,161 @@
+/*
+ *  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.runtime;
+
+import groovy.lang.IntRange;
+
+import java.lang.reflect.Array;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * Runtime helpers used by the GEP-20 multi-assignment destructuring bytecode.
+ *
+ * <p>These are called from generated bytecode only. User code should not rely 
on them.</p>
+ */
+public final class MultipleAssignmentSupport {
+
+    private MultipleAssignmentSupport() {}
+
+    private static final Class<?>[] INT_RANGE_PARAM = { IntRange.class };
+
+    /**
+     * Resolve the value for a tail rest binding ({@code *t}) at declarator 
index
+     * {@code fromIndex}. The GEP-20 dispatch model has three paths:
+     *
+     * <ul>
+     *   <li><b>Path A</b> — {@code Stream<T>}: wrap the already-advanced 
iterator
+     *       in a fresh sequential Stream and chain {@code onClose} to the
+     *       original. The new Stream reports no spliterator characteristics
+     *       (preserving them would require reading the original spliterator,
+     *       which is mutually exclusive with the iterator path used for head
+     *       extraction).</li>
+     *   <li><b>Path B</b> — receiver with a resolvable {@code 
getAt(IntRange)}.
+     *       Implemented as fast-path {@code instanceof} branches for
+     *       {@code List}, {@code CharSequence}, and Java arrays (size-aware so
+     *       out-of-bounds {@code fromIndex} returns the canonical empty value
+     *       without calling user code), with an MOP-dispatched fallback for
+     *       any other Path B receiver — {@link java.util.BitSet} (returns
+     *       {@code BitSet}), user custom classes, etc.</li>
+     *   <li><b>Path C</b> — anything else iterable: return the 
already-advanced
+     *       iterator.</li>
+     * </ul>
+     *
+     * <p>The actual {@code instanceof} checks are ordered for performance
+     * (List/CharSequence/array fast paths first, then Stream, then MOP
+     * fallback, then iterator) but the conceptual dispatch order is A, then
+     * B, then C.</p>
+     *
+     * <p>Primitive streams ({@code IntStream}, {@code LongStream},
+     * {@code DoubleStream}) are not subtypes of {@code Stream<T>} and 
therefore
+     * fall through to Path B (if they expose {@code getAt(IntRange)}) or
+     * Path C; their elements are boxed by the underlying iterator.</p>
+     *
+     * @param rhs       the original RHS value
+     * @param fromIndex the declarator position of the rest binding
+     * @param seq       the iterator that has already yielded the preceding 
fixed-slot values
+     */
+    public static Object tailRest(final Object rhs, final int fromIndex, final 
Iterator<?> seq) {
+        if (rhs instanceof List) {
+            if (fromIndex >= ((List<?>) rhs).size()) return 
Collections.emptyList();

Review Comment:
   In the List fast-path, returning Collections.emptyList() when fromIndex is 
past the end makes the rest binding immutable. This is inconsistent with 
Groovy's List range slicing (getAt(EmptyRange) / getAt(Range)) which returns a 
new, mutable list (often of a similar concrete type). Consider returning a 
mutable empty list (or delegating to List#getAt with an empty range) so `*t` 
behaves the same whether it absorbs 0 or >0 elements.
   ```suggestion
               if (fromIndex >= ((List<?>) rhs).size()) return new 
java.util.ArrayList<>();
   ```



##########
src/main/java/org/codehaus/groovy/runtime/MultipleAssignmentSupport.java:
##########
@@ -0,0 +1,161 @@
+/*
+ *  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.runtime;
+
+import groovy.lang.IntRange;
+
+import java.lang.reflect.Array;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * Runtime helpers used by the GEP-20 multi-assignment destructuring bytecode.
+ *
+ * <p>These are called from generated bytecode only. User code should not rely 
on them.</p>
+ */
+public final class MultipleAssignmentSupport {
+
+    private MultipleAssignmentSupport() {}
+
+    private static final Class<?>[] INT_RANGE_PARAM = { IntRange.class };
+
+    /**
+     * Resolve the value for a tail rest binding ({@code *t}) at declarator 
index
+     * {@code fromIndex}. The GEP-20 dispatch model has three paths:
+     *
+     * <ul>
+     *   <li><b>Path A</b> — {@code Stream<T>}: wrap the already-advanced 
iterator
+     *       in a fresh sequential Stream and chain {@code onClose} to the
+     *       original. The new Stream reports no spliterator characteristics
+     *       (preserving them would require reading the original spliterator,
+     *       which is mutually exclusive with the iterator path used for head
+     *       extraction).</li>
+     *   <li><b>Path B</b> — receiver with a resolvable {@code 
getAt(IntRange)}.
+     *       Implemented as fast-path {@code instanceof} branches for
+     *       {@code List}, {@code CharSequence}, and Java arrays (size-aware so
+     *       out-of-bounds {@code fromIndex} returns the canonical empty value
+     *       without calling user code), with an MOP-dispatched fallback for
+     *       any other Path B receiver — {@link java.util.BitSet} (returns
+     *       {@code BitSet}), user custom classes, etc.</li>
+     *   <li><b>Path C</b> — anything else iterable: return the 
already-advanced
+     *       iterator.</li>
+     * </ul>
+     *
+     * <p>The actual {@code instanceof} checks are ordered for performance
+     * (List/CharSequence/array fast paths first, then Stream, then MOP
+     * fallback, then iterator) but the conceptual dispatch order is A, then
+     * B, then C.</p>
+     *
+     * <p>Primitive streams ({@code IntStream}, {@code LongStream},
+     * {@code DoubleStream}) are not subtypes of {@code Stream<T>} and 
therefore
+     * fall through to Path B (if they expose {@code getAt(IntRange)}) or
+     * Path C; their elements are boxed by the underlying iterator.</p>
+     *
+     * @param rhs       the original RHS value
+     * @param fromIndex the declarator position of the rest binding
+     * @param seq       the iterator that has already yielded the preceding 
fixed-slot values
+     */
+    public static Object tailRest(final Object rhs, final int fromIndex, final 
Iterator<?> seq) {
+        if (rhs instanceof List) {
+            if (fromIndex >= ((List<?>) rhs).size()) return 
Collections.emptyList();
+            IntRange range = new IntRange(true, fromIndex, -1);
+            return InvokerHelper.invokeMethod(rhs, "getAt", range);
+        }
+        if (rhs instanceof CharSequence) {
+            if (fromIndex >= ((CharSequence) rhs).length()) return "";
+            IntRange range = new IntRange(true, fromIndex, -1);
+            return InvokerHelper.invokeMethod(rhs, "getAt", range);
+        }
+        if (rhs != null && rhs.getClass().isArray()) {
+            // Match the non-empty branch: ArrayGroovyMethods.getAt(T[], 
IntRange) returns List<T>,
+            // so the empty case must be Collections.emptyList() — not an 
empty T[] — for typed
+            // rest binders like `List<T> *t` to round-trip consistently.
+            if (fromIndex >= Array.getLength(rhs)) return 
Collections.emptyList();
+            IntRange range = new IntRange(true, fromIndex, -1);
+            return InvokerHelper.invokeMethod(rhs, "getAt", range);
+        }
+        if (rhs instanceof Stream) {
+            Spliterator<?> spliterator = 
Spliterators.spliteratorUnknownSize(seq, 0);
+            Stream<?> tail = StreamSupport.stream(spliterator, false);
+            return tail.onClose(((Stream<?>) rhs)::close);
+        }
+        // MOP fallback: any other receiver with getAt(IntRange) — BitSet, 
user classes, etc.
+        // Picked deliberately AFTER the Stream branch so a Stream RHS never 
materialises via
+        // StreamGroovyMethods.getAt(Stream, IntRange).
+        if (rhs != null && InvokerHelper.getMetaClass(rhs).pickMethod("getAt", 
INT_RANGE_PARAM) != null) {
+            IntRange range = new IntRange(true, fromIndex, -1);
+            return InvokerHelper.invokeMethod(rhs, "getAt", range);
+        }
+        return seq;
+    }
+
+    /**
+     * Resolve the value for a head or middle rest binding. Requires a sized, 
indexable RHS
+     * (List, CharSequence, or array — or any other receiver with a 
non-reversing
+     * {@code getAt(IntRange)}); for other RHS types this routes through the 
MOP so that:
+     *
+     * <ul>
+     *   <li>Truly non-indexable sources (iterators, sets, custom Iterables 
without a
+     *       {@code getAt(IntRange)} extension) fail fast with
+     *       {@code MissingMethodException} — preventing silent 
materialisation of
+     *       unbounded sources.</li>
+     *   <li>{@code Stream} RHS resolves {@code 
StreamGroovyMethods.getAt(Stream, IntRange)},
+     *       which rejects the destructuring slice's reverse range with
+     *       {@code IllegalArgumentException("reverse range")} — also 
fail-fast, also
+     *       no materialisation. (Head/middle rest from {@code Stream} is 
rejected at
+     *       compile time under {@code @CompileStatic}; the IAE only surfaces 
in
+     *       dynamic mode.)</li>
+     * </ul>
+     *
+     * <p>When the RHS is shorter than the sum of fixed slots around the rest, 
the computed range
+     * is inverted. DGM's {@code getAt(IntRange)} would reverse an inverted 
range; for destructuring
+     * we want an empty slice instead, so this helper short-circuits that case 
for the sized fast
+     * paths (List/CharSequence/array). Other indexable receivers are 
responsible for their own
+     * bounds handling.</p>
+     *
+     * @param rhs       the original RHS value
+     * @param fromIndex positive index where the slice begins
+     * @param toIndex   negative index where the slice ends (inclusive)
+     */
+    public static Object nonTailRestSlice(final Object rhs, final int 
fromIndex, final int toIndex) {
+        int size = -1;
+        if (rhs instanceof List) size = ((List<?>) rhs).size();
+        else if (rhs instanceof CharSequence) size = ((CharSequence) 
rhs).length();
+        else if (rhs != null && rhs.getClass().isArray()) size = 
Array.getLength(rhs);
+
+        if (size >= 0) {
+            int effectiveTo = toIndex < 0 ? size + toIndex : toIndex;
+            if (fromIndex > effectiveTo || fromIndex >= size || effectiveTo < 
0) {
+                // Array empty-slice falls through to Collections.emptyList(): 
the non-empty
+                // path delegates to getAt(IntRange) which returns List<T> for 
arrays, so the
+                // empty path must agree — see tailRest for the same alignment.
+                if (rhs instanceof CharSequence) return "";
+                return Collections.emptyList();
+            }

Review Comment:
   nonTailRestSlice returns Collections.emptyList() for sized non-CharSequence 
RHS when the computed slice is empty/inverted. For List/array receivers, 
Groovy's getAt(EmptyRange) returns a new mutable list; returning an immutable 
empty list here makes the rest binding's mutability depend on whether it 
captured 0 elements. Consider returning a mutable empty list (and ideally 
matching Groovy's usual empty-slice behavior) to avoid 
UnsupportedOperationException surprises.





> Additional multi-assignment forms
> ---------------------------------
>
>                 Key: GROOVY-11964
>                 URL: https://issues.apache.org/jira/browse/GROOVY-11964
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Priority: Major
>
> Three idioms are common in modern languages but unavailable in Groovy:
> * Tail rest binding: {{def (h, *t) = list}} - separate the head from the 
> remainder. Common in functional code (recursive list traversal, 
> pipeline-style processing of streams and iterators).
> * Map-style destructuring: {{def (name: n, age: a) = person}} - extract named 
> properties or map keys directly, without intermediate variables.
> * Rest in non-tail positions: {{def (*f, last) = list, def (l, *m, r) = 
> list}} - useful when the boundaries are at the ends rather than the middle.



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

Reply via email to