daniellansun commented on code in PR #3259:
URL: https://github.com/apache/groovy/pull/3259#discussion_r4550574203
##########
src/main/java/org/codehaus/groovy/ast/tools/GenericsUtils.java:
##########
@@ -368,6 +368,89 @@ public static ClassNode erasure(final ClassNode type) {
return t.getPlainNodeReference();
}
+ /**
+ * Type arguments safe to store on {@code owner}. A class that declares no
+ * type parameters gets none. Each argument is a reference type.
+ *
+ * @param owner the parameterized type
+ * @param arguments the arguments about to be stored
+ * @return the arguments to store, or {@code null} when the class declares
none
+ */
+ public static GenericsType[] legalTypeArguments(final ClassNode owner,
final GenericsType[] arguments) {
+ if (arguments == null) return null;
+ if (arguments.length > 0 && declaresNoTypeParameters(owner)) return
null;
+ GenericsType[] legal = new GenericsType[arguments.length];
+ boolean changed = false;
+ for (int i = 0; i < arguments.length; i += 1) {
+ legal[i] = asReferenceTypeArgument(arguments[i]);
+ changed |= legal[i] != arguments[i];
+ }
+ return changed ? legal : arguments;
+ }
+
+ private static boolean declaresNoTypeParameters(final ClassNode type) {
+ if (type == null || type.isGenericsPlaceHolder()) return false;
+ ClassNode declared = type.redirect();
Review Comment:
Agreed. `ClassNode.redirect()` returns this node when it is not a proxy, so
the null check is gone.
##########
src/main/java/org/codehaus/groovy/ast/tools/GenericsUtils.java:
##########
@@ -368,6 +368,89 @@ public static ClassNode erasure(final ClassNode type) {
return t.getPlainNodeReference();
}
+ /**
+ * Type arguments safe to store on {@code owner}. A class that declares no
+ * type parameters gets none. Each argument is a reference type.
+ *
+ * @param owner the parameterized type
+ * @param arguments the arguments about to be stored
+ * @return the arguments to store, or {@code null} when the class declares
none
+ */
+ public static GenericsType[] legalTypeArguments(final ClassNode owner,
final GenericsType[] arguments) {
+ if (arguments == null) return null;
+ if (arguments.length > 0 && declaresNoTypeParameters(owner)) return
null;
+ GenericsType[] legal = new GenericsType[arguments.length];
+ boolean changed = false;
+ for (int i = 0; i < arguments.length; i += 1) {
+ legal[i] = asReferenceTypeArgument(arguments[i]);
+ changed |= legal[i] != arguments[i];
+ }
+ return changed ? legal : arguments;
+ }
+
+ private static boolean declaresNoTypeParameters(final ClassNode type) {
+ if (type == null || type.isGenericsPlaceHolder()) return false;
+ ClassNode declared = type.redirect();
+ if (declared == null || !declared.isResolved() || declared.isArray())
return false;
Review Comment:
Whether a class declares type parameters now comes from the declaration, not
from whether the use site has already been resolved. A primary source class
uses the formals on that node. A loaded class uses `Class.getTypeParameters()`
when the formals were not copied onto the node. An unresolved use site is not
treated as a declaration, so this method does not strip its arguments;
`ResolveVisitor` applies `legalTypeArguments` after the redirect is set.
##########
src/main/java/org/codehaus/groovy/classgen/asm/BytecodeHelper.java:
##########
@@ -493,19 +493,26 @@ static void addSubTypes(StringBuilder ret, GenericsType[]
types, String start, S
if (type.getType().isArray()) {
ret.append("[");
addSubTypes(ret, new GenericsType[]{new
GenericsType(type.getType().getComponentType())}, "", "");
+ } else if ("<".equals(start) && isPrimitiveType(type.getType()) &&
!isPrimitiveVoid(type.getType())) {
+ // Last resort (JVMS 4.7.9.1): a type argument that is still a
+ // primitive is written as its wrapper, which is what javac
writes
+ // for the legal reference type. Parameters and return types
use a
+ // different start marker and stay primitive.
+ writeGenericsBoundType(ret,
ClassHelper.getWrapper(type.getType()), false);
Review Comment:
Agreed — this was mixing concerns. `BytecodeHelper` no longer boxes a
primitive type argument or a `? extends int` bound. Boxing is done in
`GenericsUtils.asReferenceTypeArgument` when the `GenericsType` is constructed.
The inferred call type for `of(1, 2)` and `ext(1)` is `Integer`, and the method
signatures do not contain `<I` or `+I`.
##########
src/main/java/org/codehaus/groovy/classgen/asm/TypeSignatureWriter.java:
##########
@@ -46,7 +47,47 @@ static void writeParameterizedClass(final StringBuilder ret,
final ClassNode pri
return;
}
ret.append(BytecodeHelper.getTypeDescription(printType, false));
- BytecodeHelper.addSubTypes(ret, printType.getGenericsTypes(), "<",
">");
+ BytecodeHelper.addSubTypes(ret, typeArguments(printType), "<", ">");
+ }
+
+ /**
+ * Type arguments written for {@code printType}. Last resort for JVMS
+ * 4.7.9.1: if a use site still names arguments a class does not declare,
+ * write the raw type javac would emit for the legal type. The type
+ * checker is what must keep {@code Serializable<T>} and {@code
Queryable<int>}
+ * out of the AST.
+ */
+ private static GenericsType[] typeArguments(final ClassNode printType) {
+ GenericsType[] useSite = printType.getGenericsTypes();
+ if (useSite == null || useSite.length == 0) {
+ return useSite;
+ }
+ int declared = declaredTypeParameters(printType);
+ if (declared >= 0 && declared != useSite.length) {
+ return null;
+ }
+ return useSite;
+ }
+
+ /**
+ * Number of type parameters the class actually declares, or {@code -1}
+ * when that is not known yet. An unresolved {@code java.*} name is loaded
+ * so a use site cannot attach arguments to {@code Serializable}.
+ */
+ private static int declaredTypeParameters(final ClassNode printType) {
+ ClassNode resolved = printType.isResolved() ? printType :
printType.redirect();
Review Comment:
Agreed. At signature write time the declaration is read from the class node.
Unresolved names are not loaded here.
##########
src/main/java/org/codehaus/groovy/classgen/asm/TypeSignatureWriter.java:
##########
@@ -46,7 +47,47 @@ static void writeParameterizedClass(final StringBuilder ret,
final ClassNode pri
return;
}
ret.append(BytecodeHelper.getTypeDescription(printType, false));
- BytecodeHelper.addSubTypes(ret, printType.getGenericsTypes(), "<",
">");
+ BytecodeHelper.addSubTypes(ret, typeArguments(printType), "<", ">");
+ }
+
+ /**
+ * Type arguments written for {@code printType}. Last resort for JVMS
+ * 4.7.9.1: if a use site still names arguments a class does not declare,
+ * write the raw type javac would emit for the legal type. The type
+ * checker is what must keep {@code Serializable<T>} and {@code
Queryable<int>}
+ * out of the AST.
+ */
+ private static GenericsType[] typeArguments(final ClassNode printType) {
+ GenericsType[] useSite = printType.getGenericsTypes();
+ if (useSite == null || useSite.length == 0) {
+ return useSite;
+ }
+ int declared = declaredTypeParameters(printType);
+ if (declared >= 0 && declared != useSite.length) {
+ return null;
+ }
+ return useSite;
+ }
+
+ /**
+ * Number of type parameters the class actually declares, or {@code -1}
+ * when that is not known yet. An unresolved {@code java.*} name is loaded
+ * so a use site cannot attach arguments to {@code Serializable}.
+ */
+ private static int declaredTypeParameters(final ClassNode printType) {
+ ClassNode resolved = printType.isResolved() ? printType :
printType.redirect();
+ if (resolved != null && resolved.isResolved() && !resolved.isArray()) {
+ return resolved.getTypeClass().getTypeParameters().length;
+ }
+ String name = printType.getName();
+ if (name != null && (name.startsWith("java.") ||
name.startsWith("javax.") || name.startsWith("groovy."))) {
+ try {
+ return Class.forName(name, false,
TypeSignatureWriter.class.getClassLoader()).getTypeParameters().length;
Review Comment:
Agreed. That belongs in `ResolveVisitor`. Source use sites such as
`Serializable<T>` and `Raw<String>` are corrected once the class is known, and
the field signature is the raw type. The writer no longer uses `Class.forName`,
and unresolved names are not loaded there.
A type invented later is also kept legal in the AST. The lowest upper bound
of a multi-key group value is still named `java.io.Serializable` (the
compile-time class), but it is no longer parameterized: `Comparable`'s type
argument stays on `Comparable`. The signature is the raw type because the AST
type is raw, not because the writer drops an illegal argument.
##########
subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/Queryable.java:
##########
@@ -516,6 +539,52 @@ default Queryable<T> union(Queryable<? extends T>
queryable) {
*/
List<T> toList();
+ /**
+ * Iterates the materialized elements. A static {@code for} loop calls this
+ * rather than wrapping the query as a single value.
+ *
+ * @return an iterator over the elements
+ * @since 7.0.0
+ */
+ default Iterator<T> iterator() {
+ return toList().iterator();
+ }
+
+ /**
+ * Coerces this query. {@link Queryable}, {@link List}, {@link Collection}
and
+ * {@link Iterable} are answered here. Every other target is delegated to
+ * {@link DefaultGroovyMethods#asType(Object, Class)}, which throws
+ * {@link GroovyCastException} when the value is not compatible with the
target.
+ *
+ * @param type the requested type
+ * @return the coerced value
+ * @throws GroovyCastException when {@code type} is null or the value
cannot be coerced
+ * @since 7.0.0
+ */
+ default Object asType(Class<?> type) {
+ if (type == null) {
+ throw new GroovyCastException("Cannot cast '" +
getClass().getName() + "' to null");
+ }
+ if (type == Queryable.class) {
+ return this;
Review Comment:
`as List` is `toList()`. `Queryable` does not implement `Iterable` or
`Collection`, so `DefaultGroovyMethods.asType(Object, Class)` cannot walk the
elements and would throw for `Collection`, `Iterable`, and `Set`. Those go
through `DefaultGroovyMethods.asType(toList(), type)`. `as Queryable` returns
this query, and `as Foo` still throws `GroovyCastException`.
##########
subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/QueryableCollection.java:
##########
@@ -684,12 +690,128 @@ public long size() {
}
/**
- * Returns a reusable stream over the current elements.
+ * Returns a new stream over the current elements. Creating the stream
does not
+ * pull. An iterable source is read again by every stream. A one-shot
stream is
+ * pulled into a buffer as it is consumed, and a later call replays that
buffer
+ * instead of returning the stream that was already handed out.
*
- * @return the element stream
+ * @return a stream that has not been consumed
*/
@Override
public Stream<T> stream() {
+ writeLock.lock();
+ try {
+ if (sourceIterable != null) {
+ Stream<T> fresh = toStream(sourceIterable);
+ if (!fresh.isParallel() && isParallel()) {
+ fresh = fresh.parallel();
+ }
+ return fresh;
+ }
+ bindUpstream();
+ return StreamSupport.stream(ReplaySpliterator::new,
upstreamCharacteristics, false);
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ /**
+ * Binds the one-shot source. The size is recorded before any element is
pulled,
+ * so a later {@code count} can use it without running projections.
+ */
+ private void bindUpstream() {
+ if (upstreamBound) {
+ return;
+ }
+ upstreamBound = true;
+ upstream = openStream().spliterator();
+ long exact = upstream.getExactSizeIfKnown();
+ upstreamExactSize = exact;
+ upstreamCharacteristics = Spliterator.ORDERED;
+ if (exact >= 0) {
+ upstreamCharacteristics |= Spliterator.SIZED |
Spliterator.SUBSIZED;
+ }
+ }
+
+ /**
+ * One cursor over a one-shot source. Elements are appended to {@link
#replay}
+ * as they are pulled, so another stream can read them from the start.
+ * A sized source reports that size, and {@code Stream.count} then does
not pull.
+ */
+ private final class ReplaySpliterator implements Spliterator<T> {
+ private int cursor;
+
+ @Override
+ public boolean tryAdvance(Consumer<? super T> action) {
+ writeLock.lock();
Review Comment:
Elements already in the replay buffer are served under the read lock. The
callback runs after that lock is released. The write lock is taken only when
the cursor is at the end of the buffer and the next element has to be pulled
from upstream. Two concurrent readers of one stream see the same sequence,
including nulls, with no lost or duplicated elements.
--
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]