Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reflection: Symbol.typeMembers has a non-deterministic order on classes with more than 5 type parameters/members #22472

Open
neko-kai opened this issue Jan 27, 2025 · 4 comments
Assignees
Labels
area:metaprogramming:reflection Issues related to the quotes reflection API itype:bug

Comments

@neko-kai
Copy link
Contributor

neko-kai commented Jan 27, 2025

Compiler version

3.3.4

Minimized code

package example

import scala.quoted.*

object Macro {
  inline def typeMembers[T <: AnyKind]: String = ${ typeMembersImpl[T] }

  def typeMembersImpl[T <: AnyKind: Type](using quotes: Quotes): Expr[String] = {
    import quotes.reflect.*
    Expr(TypeRepr.of[T].typeSymbol.typeMembers.toString)
  }
}
import example.Macro

@main def test(): Unit = {
  class FooSmall[A, B] { type C; type D }
  class FooLarge[A, B, C] { type D; type E }

  println(Macro.typeMembers[FooSmall])
  println(Macro.typeMembers[FooLarge])
}

Output

List(type A, type B, type C, type D)
List(type B, type D, type C, type E, type A)

Expectation

List(type A, type B, type C, type D)
List(type A, type B, type C, type D, type E)

The list returned by .typeMembers is ordered by declaration order, with type parameters first and type members second. BUT only if the total number of type members + type parameters is less than 5. Otherwise the order is non-deterministic due to underlying usage of Set.

I think expecting a defined order in Symbold.typeMembers method is reasonable, because:

  1. When this method was added, the tests added with it only tested naive usage .typeMembers.filter(_.isTypeParam) – indicating that this is the correct usage pattern.
  2. It's the only source of type parameter variance in public reflection API.
  3. Restoring order without relying on it being correct in .typeMembers is very hard - the only way I found is to match type parameter names with the same from .primaryConstructor - and that's hardly intuitive and I'm not even sure if all types that can have variance defined on type parameters also have a primaryConstructor, otherwise the order is not restorable.
  4. The .typeMembers call returns a List, not a Set, indicating order.

A user of izumi-reflect uncovered this undefined behavior due to getting incorrect variance in typetags generated by izumi-reflect on classes with many type parameters: zio/izumi-reflect#511

I'm not sure there's a bulletproof workaround for this issue downstream – because I'm not sure if every relevant type that may be inspected by izumi-reflect has a .primaryConstructor to match order against.

@Gedochao
Copy link
Contributor

What's interesting is that it's non-deterministic across Scala versions...

scala-cli run . -S 3.6.3
# Compiling project (Scala 3.6.3, JVM (23))
# Compiled project (Scala 3.6.3, JVM (23))
# List(type A, type B, type C, type D)
# List(type A, type E, type D, type B, type C)
scala-cli run . -S 3.3.4
# Compiling project (Scala 3.3.4, JVM (23))
# Compiled project (Scala 3.3.4, JVM (23))
# List(type A, type B, type C, type D)
# List(type A, type D, type C, type E, type B)
scala-cli run . -S 3.3.5
# Compiling project (Scala 3.3.5, JVM (23))
# Compiled project (Scala 3.3.5, JVM (23))
# List(type A, type B, type C, type D)
# List(type A, type C, type E, type D, type B)
scala-cli run . -S 3.nightly
# Compiling project (Scala 3.7.0-RC1-bin-20250129-81e057a-NIGHTLY, JVM (23))
# Compiled project (Scala 3.7.0-RC1-bin-20250129-81e057a-NIGHTLY, JVM (23))
# List(type A, type B, type C, type D)
# List(type B, type D, type A, type C, type E)

@Gedochao Gedochao added area:metaprogramming:reflection Issues related to the quotes reflection API and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels Jan 30, 2025
@prolativ
Copy link
Contributor

Indeed, List instead of Set might suggest relevance of order, but changing the method signature to Set is not an option at this point.
But for this particular use case .declaredTypes.filter(_.isTypeParam) seems to return actual type parameters in the expected order, even for bigger arities. If you also need information about type parameters of supertypes, then you'll have to traverse the inheritance tree explicitly by yourself and handle the parameters of the supertypes explicitly, because variance of a type parameter might slightly vary between the sub- and the supertype, e.g.

trait Foo[+A]
trait Bar[A] extends Foo[A]

@pshirshov
Copy link
Contributor

but changing the method signature to Set is not an option at this point.

This is misleading and, at the very least, should be documented with a docstring. I think the underlying collection should either be changed to an ordered set or the signature updated.

@neko-kai
Copy link
Contributor Author

@prolativ declaredTypes worked, thank you very much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:metaprogramming:reflection Issues related to the quotes reflection API itype:bug
Projects
None yet
Development

No branches or pull requests

5 participants