Skip to content

Record like HK definitions that compile fine under 3.6.4 crash under 3.7.0 #23293

Open
@rcano

Description

@rcano

Compiler version

3.7.0

Minimized code

I tried to minimize this by commenting parts out, but no luck (also most definitions depend on each other).

import scala.compiletime.ops.int.S
import scala.compiletime.ops.string.+
import scala.deriving.Mirror
import scala.annotation.{implicitNotFound}

object genrec {
  /** Fold a tuple just like Tuple.Fold but with typed upper bounds */
  type TupleFold[T <: Tuple, UpperBound, Z <: UpperBound, F[_ <: UpperBound, _ <: UpperBound] <: UpperBound] <: UpperBound = T match
    case EmptyTuple => Z
    case h *: t => F[Z, TupleFold[t, UpperBound, h, F]]

  
  type TupleReduce[Tup <: Tuple, UpperBound, F[_ <: UpperBound, _ <: UpperBound] <: UpperBound] <: UpperBound = Tup match {
    case h *: t => TupleFold[t, UpperBound, h, F]
  }

  /** Map a tuple just like Tuple.Map but with typed upper bounds */
  type TupleMap[T <: Tuple, UpperBound, F[_ <: UpperBound]] <: Tuple = T match
    case EmptyTuple => EmptyTuple
    case h *: t => F[h] *: TupleMap[t, UpperBound, F]


  /*erased*/ trait TypeWitness[N]:
    type T = N
  /*erased*/ given [T]: TypeWitness[T] = null
  object opaques {
    @scala.annotation.showAsInfix
    opaque type @@[S <: String & Singleton, +T] >: T = T

    extension [T <: String & Singleton](t: T) def @@[V](v: V): T @@ V = v
  }
  export opaques.*
  type FieldName[T <: @@[String & Singleton, Any]] <: String = T match {
    case @@[nme, _] => nme
  }
  type FieldNames[T <: Tuple] = TupleMap[T, @@[String & Singleton, Any], FieldName]
  type FieldType[T <: @@[String & Singleton, Any]] = T match {
    case @@[_, tpe] => tpe
  }

  type FieldToString[T <: @@[String & Singleton, Any]] <: String = T match {
    case @@[name, tpe] => name + ": " + tpe
  }

  extension [K <: String & Singleton, T](f: K @@ T) inline def value: T = f.asInstanceOf[T]

  type IndexOf[Rec <: Tuple, Field] <: Int = Rec match {
    case h *: t => h match { //use invariant type comparison by abusing Set trick
      case Field => 0
      case _ => S[IndexOf[t, Field]]
    }
  }
  type FieldToTuple[T <: @@[String & Singleton, Any]] <: Tuple = T match {
    case @@[nme, tpe] => (nme, tpe)
  }

  type IndexOfField[Rec <: Tuple, Field <: @@[String & Singleton, Any]] = IndexOf[Rec, Field]

  type WidenTuple[T <: Tuple] <: Tuple = T match {
    case EmptyTuple => EmptyTuple
    case h *: tail => h *: tail
  }

  given ProdOps: AnyRef with {
    extension [P <: Product](p: P)(using gen: Generic[P]) {
      def delField(field: String & Singleton)
          (using /*erased*/ w: TypeWitness[IndexOf[FieldNames[gen.Out], field.type]])
          (using i: ValueOf[w.T]): Tuple.Concat[Tuple.Take[gen.Out, w.T], Tuple.Drop[gen.Out, S[w.T]]] =
        val arr = scala.runtime.Tuples.toIArray(gen.toGen(p))
        val res = new Array[Object](arr.length - 1)
        System.arraycopy(arr, 0, res, 0, i.value)
        System.arraycopy(arr, i.value + 1, res, i.value, arr.length - i.value - 1)
        scala.runtime.Tuples.fromArray(res).asInstanceOf[Tuple.Concat[Tuple.Take[gen.Out, w.T], Tuple.Drop[gen.Out, S[w.T]]]]

      def renameField(field: String & Singleton, to: String & Singleton)
          (using /*erased*/ w: TypeWitness[IndexOf[FieldNames[gen.Out], field.type]])
          (using i: ValueOf[w.T]): Tuple.Concat[Tuple.Take[gen.Out, w.T], (to.type @@ FieldType[Tuple.Elem[gen.Out, w.T]]) *: Tuple.Drop[gen.Out, S[w.T]]] =
        gen.toGen(p).asInstanceOf[Tuple.Concat[Tuple.Take[gen.Out, w.T], (to.type @@ FieldType[Tuple.Elem[gen.Out, w.T]]) *: Tuple.Drop[gen.Out, S[w.T]]]]
      
      def +[K <: String & Singleton, V](field: K @@ V): Tuple.Concat[gen.Out, (K @@ V) *: EmptyTuple] =
        gen.toGen(p) ++ (field *: EmptyTuple)

      def replaceField[NewValueType](field: String & Singleton)
          (using /*erased*/ w: TypeWitness[IndexOf[FieldNames[gen.Out], field.type]])
          (using i: ValueOf[w.T])
          (f: FieldType[Tuple.Elem[gen.Out, w.T]] => NewValueType): Tuple.Concat[Tuple.Take[gen.Out, w.T], (field.type @@ NewValueType) *: Tuple.Drop[gen.Out, S[w.T]]] =
        val res = scala.runtime.Tuples.toArray(gen.toGen(p))
        val prev = res(i.value).asInstanceOf[FieldType[Tuple.Elem[gen.Out, w.T]]]
        val newv = f(prev)
        res(i.value) = newv.asInstanceOf[Object]
        scala.runtime.Tuples.fromArray(res).asInstanceOf[Tuple.Concat[Tuple.Take[gen.Out, w.T], (field.type @@ NewValueType) *: Tuple.Drop[gen.Out, S[w.T]]]]

      def toProd[Prd <: Product](using m: Mirror.ProductOf[Prd]): m.MirroredMonoType = m.fromProduct(gen.toGen(p))

      def select(fieldName: String & Singleton)(using s: SelectByName[fieldName.type, gen.Out]): s.Out = s.apply(gen.toGen(p))[fieldName.type]

      def field(fieldName: String & Singleton)
          (using /*erased*/ w: TypeWitness[IndexOf[FieldNames[gen.Out], fieldName.type]])
          (using i: ValueOf[w.T]): Tuple.Elem[gen.Out, w.T] =
        p.productElement(i.value).asInstanceOf[Tuple.Elem[gen.Out, w.T]]

      def pruneTo[Tup2 <: Tuple](using sr: SubRow[gen.Out, Tup2]): Tup2 = sr.asSubRecord(gen.toGen(p))

      def transmogrify[P2 <: Product](using gen2: Generic[P2], sr: SubRow[gen.Out, gen2.Out], m: Mirror.ProductOf[P2]): P2 = m.fromProduct(sr.asSubRecord(gen.toGen(p)))
    }
  }

  trait Generic[Prod <: Product] {
    type Out <: Tuple
    def toGen(prod: Prod): Out

    extension (prod: Prod) def gen: Out = toGen(prod)
  }
  object Generic extends GenericLowPriority {
    type Aux[Prod <: Product, _Out <: Tuple] = Generic[Prod] { type Out = _Out}

    transparent inline def of[Prod <: Product] = scala.compiletime.summonInline[Generic[Prod]]

    type IsRecord[Rec <: Tuple] = Rec match {
      case EmptyTuple => true
      case a *: tail => a match {
        case @@[?, ?] => IsRecord[tail]
        case _ => false
      }
    }

    private val identityGeneric = new Generic[Tuple] {
      type Out = Tuple
      def toGen(t: Tuple) = t
    }

    given tuplesGeneric[Rec <: Tuple](using IsRecord[Rec] =:= true): Generic.Aux[Rec, Rec] = identityGeneric.asInstanceOf[Generic.Aux[Rec, Rec]]
  }
  trait GenericLowPriority { self: Generic.type =>
    given productsGeneric[Prod <: Product](using m: Mirror.ProductOf[Prod]): Generic.Aux[
      Prod,
      WidenTuple[Tuple.Map[Tuple.Zip[m.MirroredElemLabels, m.MirroredElemTypes], [Entry] =>> Entry match {
        case (nme, tpe) => nme @@ tpe
      }]]
    ] = new Generic[Prod] {
      type Out = WidenTuple[Tuple.Map[Tuple.Zip[m.MirroredElemLabels, m.MirroredElemTypes], [Entry] =>> Entry match {
        case (nme, tpe) => nme @@ tpe
      }]]
      def toGen(prod: Prod): Out =
        val r = prod.productIterator.map(v => ("s" @@ v).asInstanceOf[Object]).toArray
        scala.runtime.Tuples.fromArray(r).asInstanceOf[Out]
    }
  }

  type FieldSet[Rec <: Tuple] = Tuple.Union[Tuple.Map[Rec, FieldToTuple]]
  type MissingFields[Rec <: Tuple, AvailableFields] <: Tuple = Rec match {
    case EmptyTuple => EmptyTuple
    case h *: t => FieldToTuple[h] match {
      case AvailableFields => MissingFields[t, AvailableFields]
      case _ => h *: MissingFields[t, AvailableFields]
    }
  }


  /** Read field [[Field]]  from [[Rec]] */
  @implicitNotFound("Field with name ${Field} is not present in ${Rec}")
  trait SelectByName[Field <: String & Singleton, Rec <: Tuple] {
    type Out
    extension (r: Rec) def apply[F <: Field]: Out
  }
  object SelectByName {
    type Aux[Field <: String & Singleton, Rec <: Tuple, O] = SelectByName[Field, Rec] { type Out = O }

    given [T, Field <: String & Singleton, Rec <: Tuple]
        // (using /*erased*/ w: TypeWitness[IndexOf[TupleMap[Rec, @@[String & Singleton, Any], FieldName], Field]])
        (using /*erased*/ w: TypeWitness[IndexOf[Tuple.Map[Rec, FieldName], Field]])
        (using v: ValueOf[w.T]): Aux[Field, Rec, FieldType[Tuple.Elem[Rec, w.T]]] = new SelectByName[Field, Rec] {
      type Out = FieldType[Tuple.Elem[Rec, w.T]]
      extension (r: Rec) def apply[F <: Field] = r.productElement(v.value).asInstanceOf[@@[Field, Out]].value
    }
  }

  /** Typeclass denoting that [[T]] is a subrow of [[Rec]], this allows to select every field from [[Rec]] in [[T]] */
  @implicitNotFound("can't prove that ${T} is a subrow of ${Rec}")
  trait SubRow[T, Rec <: Tuple] {

    /** Read [[FieldName] value
      * Note: the SelectByName is /*erased*/, it's only there to ensure via the compiler that you're trying to read one of the
      * fields defined in the SubRow
      */
    extension (r: T)
      def apply[FieldName <: String & Singleton](using ValueOf[FieldName])(using /*erased*/ s: SelectByName[FieldName, Rec]): s.Out
      def asSubRecord: Rec
  }
  object SubRow {
    import scala.compiletime.*
    type Of[Rec <: Tuple] = [T] =>> SubRow[T, Rec]

    /** Developer API, don't use directly */
    class SubRowImpl[T <: Product, Rec <: Tuple](val fieldIndices: collection.immutable.SeqMap[String, Int], val recFields: IArray[String]) extends SubRow[T, Rec] {
      extension (r: T)
        def apply[FieldName <: String & Singleton](using ValueOf[FieldName])(using /*erased*/ s: SelectByName[FieldName, Rec]): s.Out =
          val i = fieldIndices(valueOf[FieldName])
          r.productElement(i).asInstanceOf[s.Out]

        def asSubRecord: Rec =
          val resContent = new Array[Object](recFields.size)
          recFields.zipWithIndex `foreach` ((f, i) => resContent(i) = r.productElement(fieldIndices(f)).asInstanceOf[Object])
          scala.runtime.Tuples.fromArray(resContent).asInstanceOf[Rec]
    }

    transparent inline given isSubRow[T <: Product, Rec <: Tuple]
      (using gen: Generic[T]): SubRow[T, Rec] = {
      inline erasedValue[MissingFields[Rec, FieldSet[gen.Out]]] match {
        case t: NonEmptyTuple =>
          inline val mf = valueOf[TupleReduce[FieldNames[t.type], String, [a <: String, b <: String] =>> a + "\n  " + b]]
          error("You have missing fields:\n  " + mf + "\nEnsure that these fields are present and that their types match")
          // error("You have missing fields:\nEnsure that these fields are present and that their types match")
        case _ =>
      }

      val labels = summonAll[TupleMap[gen.Out, @@[String & Singleton, Any], [x <: @@[String & Singleton, Any]] =>> ValueOf[FieldName[x]]]].toArray
      val recFields = summonAll[TupleMap[Rec, @@[String & Singleton, Any], [x <: @@[String & Singleton, Any]] =>> ValueOf[FieldName[x]]]].toIArray.map(_.asInstanceOf[ValueOf[String]].value)
      (SubRowImpl[T, Rec](labels.map(_.asInstanceOf[ValueOf[String]].value).zipWithIndex.to(collection.immutable.TreeSeqMap), recFields): SubRow[T, Rec])
    }

  }
}

Output (click arrow to expand)

unhandled exception while running MegaPhase{crossVersionChecks, firstTransform, checkReentrant, elimPackagePrefixes, cookComments, checkLoopingImplicits, betaReduce, inlineVals, expandSAMs, elimRepeated, refchecks, dropForMap} on /tmp/genrec/genrec.scala

An unhandled exception was thrown in the compiler.
Please file a crash report here:
https://github.com/scala/scala3/issues/new/choose
For non-enriched exceptions, compile with -Xno-enrich-error-messages.

 while compiling: /tmp/genrec/genrec.scala
    during phase: MegaPhase{crossVersionChecks, firstTransform, checkReentrant, elimPackagePrefixes, cookComments, checkLoopingImplicits, betaReduce, inlineVals, expandSAMs, elimRepeated, refchecks, dropForMap}
            mode: Mode(ImplicitsEnabled)
 library version: version 2.13.16
compiler version: version 3.7.0
        settings: -classpath /home/user/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scala-lang/scala3-library_3/3.7.0/scala3-library_3-3.7.0.jar:/home/user/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scala-lang/scala-library/2.13.16/scala-library-2.13.16.jar -d /tmp/genrec/.scala-build/genrec_71ae519d80/classes/main -sourceroot /tmp/genrec

Exception in thread "main" java.lang.AssertionError: assertion failed: TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Nothing),TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),trait Tuple))
at scala.runtime.Scala3RunTime$.assertFailed(Scala3RunTime.scala:8)
at dotty.tools.dotc.core.Types$TypeBounds.(Types.scala:5554)
at dotty.tools.dotc.core.Types$RealTypeBounds.(Types.scala:5631)
at dotty.tools.dotc.core.Types$TypeBounds$.apply(Types.scala:5672)
at dotty.tools.dotc.core.Types$TypeBounds.derivedTypeBounds(Types.scala:5562)
at dotty.tools.dotc.core.Types$ApproximatingTypeMap.derivedTypeBounds(Types.scala:6503)
at dotty.tools.dotc.core.Types$TypeMap.mapOver(Types.scala:6215)
at dotty.tools.dotc.core.TypeOps$AsSeenFromMap.apply(TypeOps.scala:111)
at dotty.tools.dotc.core.TypeOps$AsSeenFromMap.apply(TypeOps.scala:69)
at scala.collection.immutable.List.mapConserve(List.scala:473)
at dotty.tools.dotc.core.Types$TypeMap.mapOverLambda(Types.scala:6158)
at dotty.tools.dotc.core.TypeOps$AsSeenFromMap.apply(TypeOps.scala:105)
at dotty.tools.dotc.core.TypeOps$.asSeenFrom(TypeOps.scala:55)
at dotty.tools.dotc.core.Types$Type.asSeenFrom(Types.scala:1113)
at dotty.tools.dotc.core.Denotations$SingleDenotation.derived$1(Denotations.scala:1107)
at dotty.tools.dotc.core.Denotations$SingleDenotation.computeAsSeenFrom(Denotations.scala:1134)
at dotty.tools.dotc.core.Denotations$SingleDenotation.computeAsSeenFrom(Denotations.scala:1087)
at dotty.tools.dotc.core.Denotations$PreDenotation.asSeenFrom(Denotations.scala:137)
at dotty.tools.dotc.core.SymDenotations$ClassDenotation.findMember(SymDenotations.scala:2194)
at dotty.tools.dotc.core.Types$Type.go$1(Types.scala:778)
at dotty.tools.dotc.core.Types$Type.findMember(Types.scala:959)
at dotty.tools.dotc.core.Types$Type.memberBasedOnFlags(Types.scala:751)
at dotty.tools.dotc.core.Types$Type.nonPrivateMember(Types.scala:741)
at dotty.tools.dotc.typer.RefChecks$.hidden$1(RefChecks.scala:1190)
at dotty.tools.dotc.typer.RefChecks$.checkExtensionMethods(RefChecks.scala:1202)
at dotty.tools.dotc.typer.RefChecks.transformDefDef(RefChecks.scala:1356)
at dotty.tools.dotc.typer.RefChecks.transformDefDef(RefChecks.scala:1351)
at dotty.tools.dotc.transform.MegaPhase.goDefDef(MegaPhase.scala:1041)
at dotty.tools.dotc.transform.MegaPhase.goDefDef(MegaPhase.scala:1042)
at dotty.tools.dotc.transform.MegaPhase.goDefDef(MegaPhase.scala:1042)
at dotty.tools.dotc.transform.MegaPhase.goDefDef(MegaPhase.scala:1042)
at dotty.tools.dotc.transform.MegaPhase.goDefDef(MegaPhase.scala:1042)
at dotty.tools.dotc.transform.MegaPhase.transformNamed$1(MegaPhase.scala:268)
at dotty.tools.dotc.transform.MegaPhase.transformTree(MegaPhase.scala:452)
at dotty.tools.dotc.transform.MegaPhase.loop$1(MegaPhase.scala:465)
at dotty.tools.dotc.transform.MegaPhase.transformStats(MegaPhase.scala:465)
at dotty.tools.dotc.transform.MegaPhase.transformUnnamed$1(MegaPhase.scala:376)
at dotty.tools.dotc.transform.MegaPhase.transformTree(MegaPhase.scala:454)
at dotty.tools.dotc.transform.MegaPhase.transformNamed$1(MegaPhase.scala:272)
at dotty.tools.dotc.transform.MegaPhase.transformTree(MegaPhase.scala:452)
at dotty.tools.dotc.transform.MegaPhase.loop$1(MegaPhase.scala:465)
at dotty.tools.dotc.transform.MegaPhase.transformStats(MegaPhase.scala:465)
at dotty.tools.dotc.transform.MegaPhase.transformUnnamed$1(MegaPhase.scala:376)
at dotty.tools.dotc.transform.MegaPhase.transformTree(MegaPhase.scala:454)
at dotty.tools.dotc.transform.MegaPhase.transformNamed$1(MegaPhase.scala:272)
at dotty.tools.dotc.transform.MegaPhase.transformTree(MegaPhase.scala:452)
at dotty.tools.dotc.transform.MegaPhase.loop$1(MegaPhase.scala:465)
at dotty.tools.dotc.transform.MegaPhase.transformStats(MegaPhase.scala:465)
at dotty.tools.dotc.transform.MegaPhase.mapPackage$1(MegaPhase.scala:396)
at dotty.tools.dotc.transform.MegaPhase.transformUnnamed$1(MegaPhase.scala:399)
at dotty.tools.dotc.transform.MegaPhase.transformTree(MegaPhase.scala:454)
at dotty.tools.dotc.transform.MegaPhase.transformUnit(MegaPhase.scala:481)
at dotty.tools.dotc.transform.MegaPhase.run(MegaPhase.scala:493)
at dotty.tools.dotc.core.Phases$Phase.runOn$$anonfun$1(Phases.scala:383)
at scala.runtime.function.JProcedure1.apply(JProcedure1.java:15)
at scala.runtime.function.JProcedure1.apply(JProcedure1.java:10)
at scala.collection.immutable.List.foreach(List.scala:334)
at dotty.tools.dotc.core.Phases$Phase.runOn(Phases.scala:376)
at dotty.tools.dotc.Run.runPhases$1$$anonfun$1(Run.scala:367)
at scala.runtime.function.JProcedure1.apply(JProcedure1.java:15)
at scala.runtime.function.JProcedure1.apply(JProcedure1.java:10)
at scala.collection.ArrayOps$.foreach$extension(ArrayOps.scala:1324)
at dotty.tools.dotc.Run.runPhases$1(Run.scala:360)
at dotty.tools.dotc.Run.compileUnits$$anonfun$1$$anonfun$2(Run.scala:407)
at dotty.tools.dotc.Run.compileUnits$$anonfun$1$$anonfun$adapted$1(Run.scala:407)
at scala.Function0.apply$mcV$sp(Function0.scala:42)
at dotty.tools.dotc.Run.showProgress(Run.scala:469)
at dotty.tools.dotc.Run.compileUnits$$anonfun$1(Run.scala:407)
at dotty.tools.dotc.Run.compileUnits$$anonfun$adapted$1(Run.scala:419)
at dotty.tools.dotc.util.Stats$.maybeMonitored(Stats.scala:69)
at dotty.tools.dotc.Run.compileUnits(Run.scala:419)
at dotty.tools.dotc.Run.compileSources(Run.scala:306)
at dotty.tools.dotc.Run.compile(Run.scala:291)
at dotty.tools.dotc.Driver.doCompile(Driver.scala:37)
at dotty.tools.dotc.Driver.process(Driver.scala:201)
at dotty.tools.dotc.Driver.process(Driver.scala:169)
at dotty.tools.dotc.Driver.process(Driver.scala:181)
at dotty.tools.dotc.Driver.main(Driver.scala:211)
at dotty.tools.dotc.Main.main(Main.scala)
Compilation failed

Activity

rcano

rcano commented on May 31, 2025

@rcano
Author

Actually, there's a warning emitted by 3.6.4:

-- [E194] Potential Issue Warning: /tmp/genrec/genrec.scala:164:27 -------------
164 |    extension (r: Rec) def apply[F <: Field]: Out
    |                           ^
    |Extension method apply will never be selected from type Rec
    |because Rec already has a member with the same name and compatible parameter types.
    |
    | longer explanation available when compiling with `-explain`
1 warning found

turns out addressing that warning by renaming that extension method makes this compile under 3.7.0 too. Still would be nice for the compiler not to crash.

som-snytt

som-snytt commented on May 31, 2025

@som-snytt
Contributor

Thanks for the hint. The check is looking up the member in the receiver.

The failing check is for the apply in trait SelectByName on Rec <: Tuple.

self-assigned this
on May 31, 2025
added and removed
stat:needs triageEvery issue needs to have an "area" and "itype" label
on May 31, 2025
linked a pull request that will close this issue on May 31, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

    Development

    Participants

    @som-snytt@rcano

    Issue actions

      Record like HK definitions that compile fine under 3.6.4 crash under 3.7.0 · Issue #23293 · scala/scala3