-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Open
Labels
area:metaprogramming:otherIssues tied to metaprogramming/macros not covered by the other labels.Issues tied to metaprogramming/macros not covered by the other labels.area:metaprogramming:quotesIssues related to quotes and splicesIssues related to quotes and splicesitype:bugregressionThis worked in a previous version but doesn't anymoreThis worked in a previous version but doesn't anymore
Description
Based on the OpenCB failure in getkyo/kyo - build logs
Apparently some runtime semantics have changes since 3.7.4
Issue previously hidden by compiletime regressions: #24097 and #24547
Issue might require further minimization. Changing logic in def immediateParents can workaround this specific problem, but causes new problems elsewhere
Compiler version
Last good release: 3.8.0-RC1-bin-20250817-8c3f1a6-NIGHTLY
First bad release: 3.8.0-RC1-bin-20250818-aaa39c5-NIGHTLY
Bisect does not point to changes that can be an actual cause, more likely regression started to be present after switching to new Scala 3 artifacts (new stdlib)
Minimized code
// Uncomment if testing against original library
// //> using dep "io.getkyo::kyo-data:1.0-RC1"
import kyo.*
@main def Test = {
val record = ("name" ~ "test")
.asInstanceOf[Record["name" ~ String]]
val name: String = record.name
assert(name == "test")
}package kyo
import scala.language.dynamics
opaque type Tag[A] = String | Tag.internal.Dynamic
object Tag:
import internal.*
import Type.*
import Type.Entry.*
inline given derive[A]: Tag[A] = ${ kyo.internal.TagMacro.deriveImpl[A] }
private[kyo] object internal:
import Type.*
import Type.Entry.*
final case class Type[A](staticDB: Map[Type.Entry.Id, Type.Entry], dynamicDB: Map[Type.Entry.Id, Tag[Any]])
object Type:
sealed abstract class Entry extends Serializable with Product derives CanEqual
object Entry:
type Id = String
case object AnyEntry extends Entry
case object NothingEntry extends Entry
case object NullEntry extends Entry
final case class ClassEntry(
className: String,
params: Seq[Id],
parents: Seq[Id]
) extends Entry
end Type
final case class Dynamic(tag: String, map: Map[Entry.Id, Any]):
lazy val tpe = Type(decode(tag).staticDB, map.asInstanceOf[Map[Type.Entry.Id, Tag[Any]]])
def decode(encoded: String): Type[Any] =
val lines = encoded.split("\n")
val staticDB = scala.collection.mutable.Map.empty[Entry.Id, Entry]
var i = 0
while i < lines.length do
val line = lines(i)
if line.nonEmpty && line.contains(":") then
val colonIdx = line.indexOf(':')
val id = line.substring(0, colonIdx)
val rest = line.substring(colonIdx + 1)
staticDB(id) = parseEntry(rest, staticDB)
i += 1
Type(staticDB.toMap, Map.empty)
private def parseEntry(encoded: String, db: scala.collection.mutable.Map[Entry.Id, Entry]): Entry =
if encoded.startsWith("A") then Entry.AnyEntry
else if encoded.startsWith("N") then Entry.NothingEntry
else if encoded.startsWith("U") && encoded.length == 1 then Entry.NullEntry
else if encoded.startsWith("C:") then
val parts = encoded.substring(2).split(":")
val className = parts(0).replace("_colon_", ":")
val paramCount = if parts.length > 1 then parts(1).toInt else 0
val paramStart = 2 + paramCount
val params = (paramStart until paramStart + paramCount).map(i => if i < parts.length then parts(i) else "").filter(_.nonEmpty).toSeq
val parents = (paramStart + paramCount until parts.length).map(i => parts(i)).filter(_.nonEmpty).toSeq
Entry.ClassEntry(className, params, parents)
else
Entry.AnyEntry
def encode[A](staticDB: Map[Type.Entry.Id, Type.Entry]): String =
val concreteFlag = staticDB.get("0") match
case Some(Entry.ClassEntry(_, params, _)) if params.size == 0 => "*"
case _ => "."
concreteFlag + staticDB.map { (id, entry) =>
s"$id:${encodeEntry(entry)}"
}.mkString("\n")
private def encodeEntry(entry: Entry): String =
entry match
case Entry.AnyEntry => "A"
case Entry.NothingEntry => "N"
case Entry.NullEntry => "U"
case Entry.ClassEntry(className, params, parents) =>
val sanitized = className.replaceAll(":", "_colon_")
s"C:$sanitized:${params.size}:${params.mkString(":")}:${parents.mkString(":")}"
case _ => "A"
end internal
end Tag
case class Field[Name <: String, Value](name: Name, tag: Tag[Value])
final class Record[+Fields] private (val toMap: Map[Field[?, ?], Any]) extends AnyVal with Dynamic:
def selectDynamic[Name <: String & Singleton, Value](name: Name)(using
ev: Fields <:< Name ~ Value,
tag: Tag[Value]
): Value =
val key = Field(name, tag)
toMap.get(key).map(_.asInstanceOf[Value]).getOrElse {
sys.error(s"Not found $key, got: ${toMap.keys.mkString(", ")}")
}.asInstanceOf[Value]
object Record:
final infix class ~[Name <: String, Value] private () extends Serializable
extension (self: String)
def ~[Value](value: Value)(using tag: Tag[Value]): Record[self.type ~ Value] =
Record(Map(Field(self, tag) -> value))
export Record.`~`package kyo.internal
import kyo.*
import kyo.Tag.*
import kyo.Tag.internal.*
import kyo.Tag.internal.Type.*
import kyo.Tag.internal.Type.Entry.*
import scala.collection.immutable.HashMap
import scala.quoted.{Type as SType, *}
object TagMacro:
def deriveImpl[A: SType](using Quotes): Expr[String | Tag.internal.Dynamic] =
import quotes.reflect.*
val (staticDB, dynamicDB) = deriveDB[A]
Expr(Tag.internal.encode(staticDB))
private def deriveDB[A: SType](using q: Quotes): (Map[Type.Entry.Id, Type.Entry], Map[Type.Entry.Id, (q.reflect.TypeRepr, Expr[Tag[Any]])]) =
import quotes.reflect.*
var nextId = 0
var seen = Map.empty[TypeRepr | Symbol, (TypeRepr, String)]
var static = HashMap.empty[Type.Entry.Id, Type.Entry]
var dynamic = HashMap.empty[Type.Entry.Id, (TypeRepr, Expr[Tag[Any]])]
def visit(t: TypeRepr): Type.Entry.Id =
val tpe = t.dealiasKeepOpaques.simplified.dealiasKeepOpaques
val key =
tpe.typeSymbol.isNoSymbol match
case true => tpe
case false =>
seen.get(tpe.typeSymbol) match
case None => tpe.typeSymbol
case Some((t, _)) if t.equals(tpe) => tpe.typeSymbol
case _ => tpe
if seen.contains(key) then
seen(key)._2
else
val id = nextId.toString
nextId += 1
seen += key -> (tpe, id)
def loop(tpe: TypeRepr): Entry =
tpe match
case tpe if tpe =:= TypeRepr.of[Any] => AnyEntry
case tpe if tpe =:= TypeRepr.of[Nothing] => NothingEntry
case tpe if tpe =:= TypeRepr.of[Null] => NullEntry
case tpe if tpe.typeSymbol.isClassDef =>
val symbol = tpe.typeSymbol
val name = symbol.fullName
val params = tpe.typeArgs.map(visit)
// Only parents differ between versions - this causes the bug
ClassEntry(
name,
params,
immediateParents(tpe).map(visit)
)
case tpe =>
tpe.asType match
case '[t] =>
Expr.summon[Tag[t]] match
case Some(tag) =>
dynamic = dynamic.updated(id, tpe -> '{ $tag.asInstanceOf[Tag[Any]] })
null
case None =>
report.errorAndAbort(s"Please provide an implicit kyo.Tag[${tpe.show}] parameter.")
val entry = loop(tpe)
if entry != null then
static = static.updated(id, loop(tpe))
id
end if
end visit
visit(TypeRepr.of[A])
(static, dynamic)
end deriveDB
private def immediateParents(using Quotes)(tpe: quotes.reflect.TypeRepr): List[quotes.reflect.TypeRepr] =
import quotes.reflect.*
// removing `.tail` would fix the problem here, but would lead to other runtime failure in other places
val all = tpe.baseClasses.tail.map(tpe.baseType)
all.filter { parent =>
!all.exists { otherAncestor =>
!otherAncestor.equals(parent) && otherAncestor.baseClasses.contains(parent.typeSymbol)
}
}Output
Exception in thread "main" java.lang.RuntimeException: Not found Field(name,*8:C:java.lang.String:0::1:5:6:7:9
4:A
9:C:java.io.Serializable:0::4
5:C:java.lang.constant.Constable:0::2
0:C:java.lang.String:0::1:5:6:7:9
2:C:java.lang.Object:0::3
7:C:java.lang.Comparable:1:8:2
3:C:scala.Matchable:0::4
6:C:java.lang.CharSequence:0::2
1:C:java.lang.constant.ConstantDesc:0::2), got: Field(name,*8:C:java.io.Serializable:0::4
4:A
5:C:java.lang.constant.Constable:0::2
0:C:java.lang.String:0::1:5:6:7:8
2:C:java.lang.Object:0::3
7:C:java.lang.Comparable:1:0:2
3:C:scala.Matchable:0::4
6:C:java.lang.CharSequence:0::2
1:C:java.lang.constant.ConstantDesc:0::2)
at scala.sys.package$.error(package.scala:28)
at kyo.Record$.selectDynamic$extension$$anonfun$2(repro.scala:97)
at scala.Option.getOrElse(Option.scala:203)
at kyo.Record$.selectDynamic$extension(repro.scala:98)
at kyo.repro$u002Eusage$package$.Test(repro.usage.scala:11)
at kyo.Test.main(repro.usage.scala:5)Expectation
Metadata
Metadata
Assignees
Labels
area:metaprogramming:otherIssues tied to metaprogramming/macros not covered by the other labels.Issues tied to metaprogramming/macros not covered by the other labels.area:metaprogramming:quotesIssues related to quotes and splicesIssues related to quotes and splicesitype:bugregressionThis worked in a previous version but doesn't anymoreThis worked in a previous version but doesn't anymore