Skip to content

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

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

Open
rcano opened this issue May 31, 2025 · 2 comments · May be fixed by #23294
Open

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

rcano opened this issue May 31, 2025 · 2 comments · May be fixed by #23294

Comments

@rcano
Copy link

rcano commented May 31, 2025

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

@rcano rcano added itype:bug itype:crash stat:needs triage Every issue needs to have an "area" and "itype" label labels May 31, 2025
@rcano
Copy link
Author

rcano commented May 31, 2025

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
Copy link
Contributor

som-snytt commented May 31, 2025

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.

@som-snytt som-snytt self-assigned this May 31, 2025
@som-snytt som-snytt added area:refchecks and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels May 31, 2025
@som-snytt som-snytt linked a pull request May 31, 2025 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants