Skip to content

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

Open
@neko-kai

Description

@neko-kai

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.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions