diff --git a/MIGRATION.md b/MIGRATION.md index b99395701..7c1ee0844 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -46,15 +46,37 @@ the bottom of this file. Restoration ships incrementally per feature area. - `SelfInstance` HKT wildcard parameter narrowed to `C[Any]` (see `core/.../misc/SelfInstance.scala`). - `Timestamp` no longer extends `Comparable[Timestamp]` — Scala 3 forbids `AnyVal` inheriting Object-derived traits. Use the explicit comparator. -- `SealedEnumCompanion.values` is `lazy val` instead of `val` (initialization order under Scala 3 sealed-children - enumeration). - `Tag` companion has an explicit `unapply` (Scala 3 case-class extractor inference changed). -- `SealedUtils.instancesFor` return type widened to `TC[T]`. - `GenCodec.bseqCodec` / `iseqCodec` use the explicit `using` keyword (Scala 2 second-implicit-arg-list syntax no longer compiles). - `enum` was renamed to `e` at one call site in `GenKeyCodec` (`enum` is reserved in Scala 3). - `@targetName` annotation added to `CloseableIterator` overloaded methods. +### core — misc SealedUtils (slice 5.6) + +- `misc/SealedUtils.scala` ported from `origin/master:core/src/main/scala-3/com/avsystem/commons/misc/SealedUtils.scala` + per fork commit `3ec8c125`. Pattern 4 — pure inline / no quoted macro: uses `compiletime.{summonAll, summonFrom, + erasedValue}` + `Mirror.SumOf` + `scala.ValueOf` for compile-time case-object listing. +- `SealedUtils.caseObjectsFor[T]` REMOVED (zero internal callers per pre-port audit). Downstream replacement is + `SealedUtils.caseObjects[T: Mirror.SumOf]` — recorded in §1. +- `SealedEnumCompanion.values` widened from `lazy val values: ISeq[T]` to `def values: ISeq[T]`; subclasses overriding + with `lazy val` continue to compile. +- `SealedEnumCompanion.caseObjects` is now `inline protected def caseObjects(using Mirror.SumOf[T]): List[T]` + delegating to `SealedUtils.caseObjects[T]` (was Scala 2 macro stubbed to `???`). +- `SealedEnumCompanion.evidence: this.type = this` REMAINS COMMENTED OUT (matches fork). Uncommenting triggers + `illegal inheritance: self type X.type does not conform to self type scala.deriving.Mirror.Sum` on every companion + object extending `SealedEnumCompanion` (the auto-derived `Mirror.Sum` for the companion clashes with the + same-type `given`). Downstream consumers needing the typeclass-from-companion idiom can do + `summon[SomeEnum.type]` only if they re-introduce the `given` locally. +- `NamedEnumCompanion`: `implicit lazy val keyCodec` / `implicit lazy val codec` replaced by named + `given keyCodec: GenKeyCodec[T]` / `given codec: GenCodec[T]`. Public names preserved — downstream + `MyEnum.codec` / `MyEnum.keyCodec` continues to work. +- `OrderedEnum.ordering[T]` `implicit def` replaced by named `given ordering: [T <: OrderedEnum] => Ordering[T]`. +- `SealedEnumTest` + `NamedEnumTest` un-wrapped and green (6/6). +- Compat traits `OrderedEnumCompat` / `NamedEnumCompanionCompat` from fork `compat.scala` (pure deprecation + wrappers) are intentionally NOT ported in this slice — own future slice once we batch the rest of + `compat.scala`. + ### mongo - `BsonRef.Creator.ref`, `DataTypeDsl.{ref, as, is, isNot}`, `TypedMongoUtils.optionalizeFirstArg` are stubbed with @@ -84,6 +106,12 @@ the bottom of this file. Restoration ships incrementally per feature area. | `spring` | Deleted outright — spring-context wiring deprecated upstream. | n/a (will-not-migrate) | | `comprof` | `scalac-profiling` is Scala 2 only. | TBD | +### sbt plugins disabled + +| Plugin | Reason | Restore effort | +|------------------|-----------------------------------------------------------------------------------------------------------------|-------------------------------| +| `sbt-ci-release` | Transitively pulls `sbt-git` whose JGit fails with `NoWorkTreeException` on linked git worktrees. Disabled to keep per-branch worktree builds green; release plumbing unaffected outside CI. | S — re-enable once releasing. | + ### Test sources commented per-file 38 test classes commented across 38 files (whole-file `/* ... */` wraps) — every wrapped file had ALL classes broken @@ -107,7 +135,7 @@ Full per-file list with locations is in the Backlog table below (filter rows whe ## Backlog -*Auto-derived from `git grep -nE 'TODO\[scala3-port\]'` on this PR's tip. Total tags: 155.* +*Auto-derived from `git grep -nE 'TODO\[scala3-port\]'` on this PR's tip. Total tags: 154.* | Location | Description | Effort | |---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|--------| @@ -129,7 +157,6 @@ Full per-file list with locations is in the Backlog table below (filter rows whe | `core/src/main/scala/com/avsystem/commons/SharedExtensions.scala:145` | sourceCode (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/SharedExtensions.scala:147` | withSourceCode (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/annotation/AnnotationAggregate.scala:52` | reifyAggregated (Scala 2 macro def) | L | -| `core/src/main/scala/com/avsystem/commons/annotation/positioned.scala:12` | here (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/di/Components.scala:18` | component (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/di/Components.scala:21` | asyncComponent (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/di/Components.scala:25` | singleton (Scala 2 macro def) | L | @@ -167,13 +194,9 @@ Full per-file list with locations is in the Backlog table below (filter rows whe | `core/src/main/scala/com/avsystem/commons/misc/Sam.scala:9` | Sam.apply (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/misc/SamCompanion.scala:11` | SamCompanion.apply (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/misc/SamCompanion.scala:19` | isValidSam (Scala 2 macro def) | L | -| `core/src/main/scala/com/avsystem/commons/misc/SealedUtils.scala:12` | instancesFor (Scala 2 macro def; return type widened to TC[T]) | L | -| `core/src/main/scala/com/avsystem/commons/misc/SealedUtils.scala:52` | caseObjects (Scala 2 macro def) | L | -| `core/src/main/scala/com/avsystem/commons/misc/SealedUtils.scala:8` | caseObjectsFor (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/misc/SelfInstance.scala:4` | C[_] existential narrowed to C[Any] (Scala 3 forbids HKT wildcard application) | S | | `core/src/main/scala/com/avsystem/commons/misc/SelfInstance.scala:7` | SelfInstance.materialize (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/misc/SimpleClassName.scala:8` | SimpleClassName.materialize (Scala 2 macro def) | L | -| `core/src/main/scala/com/avsystem/commons/misc/SourceInfo.scala:28` | SourceInfo.here (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/misc/Timestamp.scala:13` | Comparable[Timestamp] (Scala 3 forbids AnyVal inheriting Object-derived traits) | S | | `core/src/main/scala/com/avsystem/commons/misc/TypeString.scala:31` | TypeString.materialize (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/misc/TypeString.scala:90` | JavaClassName.materialize (Scala 2 macro def) | L | diff --git a/core/src/main/scala/com/avsystem/commons/annotation/positioned.scala b/core/src/main/scala/com/avsystem/commons/annotation/positioned.scala index 92d1194e2..204c3affb 100644 --- a/core/src/main/scala/com/avsystem/commons/annotation/positioned.scala +++ b/core/src/main/scala/com/avsystem/commons/annotation/positioned.scala @@ -1,6 +1,8 @@ package com.avsystem.commons package annotation +import scala.quoted.* + /** Annotate a symbol (i.e. class, method, parameter, etc.) with `@positioned(positioned.here)` to retain source * position information for that symbol to be available in macro implementations which inspect that symbol. This is * necessary e.g. for determining declaration order of subtypes of sealed hierarchies in macro implementations. This @@ -9,6 +11,10 @@ package annotation */ class positioned(val point: Int) extends StaticAnnotation object positioned { - // TODO[scala3-port]: here (Scala 2 macro def) (L) - def here: Int = ??? + inline def here: Int = ${ hereImpl } + + private def hereImpl(using Quotes): Expr[Int] = { + import quotes.reflect.* + Expr(Position.ofMacroExpansion.start) + } } diff --git a/core/src/main/scala/com/avsystem/commons/misc/SealedUtils.scala b/core/src/main/scala/com/avsystem/commons/misc/SealedUtils.scala index 917b4be4a..30007c724 100644 --- a/core/src/main/scala/com/avsystem/commons/misc/SealedUtils.scala +++ b/core/src/main/scala/com/avsystem/commons/misc/SealedUtils.scala @@ -1,17 +1,30 @@ package com.avsystem.commons package misc -import com.avsystem.commons.annotation.explicitGenerics import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} -object SealedUtils { - // TODO[scala3-port]: caseObjectsFor (Scala 2 macro def) (L) - @explicitGenerics - def caseObjectsFor[T]: List[T] = ??? +import scala.compiletime +import scala.deriving.Mirror - // TODO[scala3-port]: instancesFor (Scala 2 macro def; return type widened to TC[T]) (L) - @explicitGenerics - def instancesFor[TC[_], T]: List[TC[T]] = ??? +object SealedUtils { + inline def instancesFor[TC[_], T: Mirror.SumOf as m]: List[TC[T]] = + compiletime.summonAll[Tuple.Map[m.MirroredElemTypes, TC]].toList.asInstanceOf[List[TC[T]]] + + inline def caseObjects[T: Mirror.SumOf as m]: List[T] = + collectCaseObjects[T, m.MirroredElemTypes] + + inline private def collectCaseObjects[T, Tup <: Tuple]: List[T] = inline compiletime.erasedValue[Tup] match { + case _: (h *: t) => + compiletime.summonFrom { + case vo: scala.ValueOf[`h`] => + vo.value.asInstanceOf[T] :: Nil + case m: Mirror.SumOf[`h`] => + collectCaseObjects[T, m.MirroredElemTypes] // todo: check if required to recurse into nested sum type + case _ => + Nil + } ::: collectCaseObjects[T, t] + case _: EmptyTuple => Nil + } } /** Base trait for companion objects of sealed traits that serve as enums, i.e. their only values are case objects. For @@ -33,7 +46,7 @@ trait SealedEnumCompanion[T] { /** Thanks to this implicit, [[SealedEnumCompanion]] and its subtraits can be used as typeclasses. */ - implicit def evidence: this.type = this +// given evidence: this.type = this /** Holds a list of all case objects of a sealed trait or class `T`. This must be implemented separately for every * sealed enum, but can be implemented simply by using the [[caseObjects]] macro. It's important to *always* state @@ -46,11 +59,14 @@ trait SealedEnumCompanion[T] { * Also, be aware that [[caseObjects]] macro guarantees well-defined order of elements only for * [[com.avsystem.commons.misc.OrderedEnum OrderedEnum]]. */ - // lazy to allow lazy-val override in ValueEnumCompanion (Scala 3 disallows lazy overriding non-lazy) - lazy val values: ISeq[T] + def values: ISeq[T] - // TODO[scala3-port]: caseObjects (Scala 2 macro def) (L) - protected def caseObjects: List[T] = ??? + /** A macro which reifies a list of all case objects of the sealed trait or class `T`. WARNING: the order of case + * objects in the resulting list is well defined only for enums that extend [[OrderedEnum]]. In such case, the order + * is consistent with declaration order in source file. However, if the enum is not an [[OrderedEnum]], the order may + * be arbitrary. + */ + inline protected def caseObjects(using Mirror.SumOf[T]): List[T] = SealedUtils.caseObjects[T] } abstract class AbstractSealedEnumCompanion[T] extends SealedEnumCompanion[T] @@ -123,8 +139,8 @@ trait NamedEnumCompanion[T <: NamedEnum] extends SealedEnumCompanion[T] { ), ) - implicit lazy val keyCodec: GenKeyCodec[T] = GenKeyCodec.create(decode, _.name) - implicit lazy val codec: GenCodec[T] = GenCodec.nullableSimple[T]( + given keyCodec: GenKeyCodec[T] = GenKeyCodec.create(decode, _.name) + given codec: GenCodec[T] = GenCodec.nullableSimple[T]( input => decode(input.readString()), (output, value) => output.writeString(value.name), ) @@ -151,10 +167,10 @@ trait OrderedEnum { } object OrderedEnum { private object reusableOrdering extends Ordering[OrderedEnum] { - def compare(x: OrderedEnum, y: OrderedEnum) = Integer.compare(x.sourceInfo.offset, y.sourceInfo.offset) + def compare(x: OrderedEnum, y: OrderedEnum): Int = Integer.compare(x.sourceInfo.offset, y.sourceInfo.offset) } - implicit def ordering[T <: OrderedEnum]: Ordering[T] = - reusableOrdering.asInstanceOf[Ordering[T]] + + given ordering: [T <: OrderedEnum] => Ordering[T] = reusableOrdering.asInstanceOf[Ordering[T]] } abstract class AbstractNamedEnumCompanion[T <: NamedEnum] diff --git a/core/src/main/scala/com/avsystem/commons/misc/SourceInfo.scala b/core/src/main/scala/com/avsystem/commons/misc/SourceInfo.scala index bf418c3a2..14a4dce62 100644 --- a/core/src/main/scala/com/avsystem/commons/misc/SourceInfo.scala +++ b/core/src/main/scala/com/avsystem/commons/misc/SourceInfo.scala @@ -1,6 +1,9 @@ package com.avsystem.commons package misc +import scala.annotation.tailrec +import scala.quoted.* + /** Macro-materialized implicit value that provides information about callsite source file position. It can be used in * runtime for logging and debugging purposes. Similar to Scalactic's `Position`, but contains more information. */ @@ -25,6 +28,31 @@ case class SourceInfo( object SourceInfo { def apply()(implicit si: SourceInfo): SourceInfo = si - // TODO[scala3-port]: SourceInfo.here (Scala 2 macro def) (L) - implicit def here: SourceInfo = ??? + inline implicit def here: SourceInfo = ${ hereImpl } + + private def hereImpl(using quotes: Quotes): Expr[SourceInfo] = { + import quotes.reflect.* + val pos = Position.ofMacroExpansion + + @tailrec + def enclosingLoop(sym: Symbol, acc: Vector[String]): Seq[String] = + if (sym == defn.RootClass) acc + else enclosingLoop(sym.owner, acc :+ sym.name) + + '{ + SourceInfo( + ${ Expr(pos.sourceFile.path) }, + ${ Expr(pos.sourceFile.name) }, + ${ Expr(pos.start) }, + ${ Expr(pos.startLine + 1) }, + ${ Expr(pos.startColumn + 1) }, + ${ + Expr( + pos.sourceFile.content.flatMap(_.linesIterator.drop(pos.startLine).nextOption).getOrElse("") + ) + }, + ${ Expr(enclosingLoop(Symbol.spliceOwner.owner, Vector.empty).toList) }, + ) + } + } } diff --git a/core/src/test/scala/com/avsystem/commons/annotation/PositionedTest.scala b/core/src/test/scala/com/avsystem/commons/annotation/PositionedTest.scala new file mode 100644 index 000000000..491cb3d9e --- /dev/null +++ b/core/src/test/scala/com/avsystem/commons/annotation/PositionedTest.scala @@ -0,0 +1,21 @@ +package com.avsystem.commons +package annotation + +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class PositionedTest extends AnyFunSuite with Matchers { + val point: Int = positioned.here + + test("positioned.here yields distinct positive offsets at distinct call sites") { + val a = positioned.here + val b = positioned.here + assert(a > 0) + assert(b > 0) + assert(a != b) + } + + test("positioned.here offset is the start offset of the `positioned.here` term") { + point shouldBe 214 + } +} diff --git a/core/src/test/scala/com/avsystem/commons/misc/NamedEnumTest.scala b/core/src/test/scala/com/avsystem/commons/misc/NamedEnumTest.scala index 23f0a0f02..74c70f386 100644 --- a/core/src/test/scala/com/avsystem/commons/misc/NamedEnumTest.scala +++ b/core/src/test/scala/com/avsystem/commons/misc/NamedEnumTest.scala @@ -1,6 +1,7 @@ package com.avsystem.commons package misc +import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -72,4 +73,16 @@ class NamedEnumTest extends AnyFunSuite with Matchers { test("top level objects") { assert(SomeNamedEnum.byName.contains("I am toplvl")) } + + test("keyCodec round-trip via NamedEnumCompanion given") { + val codec = summon[GenKeyCodec[SomeNamedEnum]] + for (v <- SomeNamedEnum.values) { + codec.read(codec.write(v)) shouldEqual v + } + } + + test("codec given resolves to a NamedEnumCompanion-backed instance") { + val codec = summon[GenCodec[SomeNamedEnum]] + (codec should be).theSameInstanceAs(SomeNamedEnum.codec) + } } diff --git a/core/src/test/scala/com/avsystem/commons/misc/SealedEnumTest.scala b/core/src/test/scala/com/avsystem/commons/misc/SealedEnumTest.scala index 9a9456e80..f83f39336 100644 --- a/core/src/test/scala/com/avsystem/commons/misc/SealedEnumTest.scala +++ b/core/src/test/scala/com/avsystem/commons/misc/SealedEnumTest.scala @@ -2,8 +2,9 @@ package com.avsystem.commons package misc import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers -class SealedEnumTest extends AnyFunSuite { +class SealedEnumTest extends AnyFunSuite with Matchers { sealed abstract class SomeEnum(implicit val sourceInfo: SourceInfo) extends OrderedEnum object SomeEnum extends SealedEnumCompanion[SomeEnum] { case object First extends SomeEnum @@ -11,17 +12,59 @@ class SealedEnumTest extends AnyFunSuite { case object Third extends SomeEnum case object Fourth extends SomeEnum - lazy val values: List[SomeEnum] = caseObjects - val classTags: List[ClassTag[_ <: SomeEnum]] = SealedUtils.instancesFor[ClassTag, SomeEnum] + val values: List[SomeEnum] = caseObjects + val classTags: List[ClassTag[? <: SomeEnum]] = SealedUtils.instancesFor[ClassTag, SomeEnum] } + // Bare sum type used by direct SealedUtils.caseObjects / instancesFor tests (no companion shell). + sealed trait Color + case object Red extends Color + case object Green extends Color + case object Blue extends Color + + // Nested sum — outer has a case object and a sealed sub-branch with its own case objects. + sealed trait Shape + case object Circle extends Shape + sealed trait Polygon extends Shape + case object Square extends Polygon + case object Triangle extends Polygon + + // Mixed: case objects + case classes. caseObjects must skip the case classes. + sealed trait Mixed + case object MixedA extends Mixed + case object MixedB extends Mixed + case class MixedClass(value: Int) extends Mixed + test("case objects listing") { - import SomeEnum._ + import SomeEnum.* assert(values == List(First, Second, Third, Fourth)) } test("typeclass instance listing") { - import SomeEnum._ + import SomeEnum.* assert(classTags.map(_.runtimeClass) == List(First.getClass, Second.getClass, Third.getClass, Fourth.getClass)) } + + test("SealedUtils.caseObjects works standalone (no companion shell)") { + SealedUtils.caseObjects[Color] should contain theSameElementsAs List(Red, Green, Blue) + } + + test("SealedUtils.caseObjects recurses into nested sealed sub-branches") { + SealedUtils.caseObjects[Shape] should contain theSameElementsAs List(Circle, Square, Triangle) + } + + test("SealedUtils.caseObjects skips case classes in mixed hierarchies") { + SealedUtils.caseObjects[Mixed] should contain theSameElementsAs List(MixedA, MixedB) + } + + test("OrderedEnum.ordering sorts by source declaration order") { + import SomeEnum.* + val shuffled = List(Third, First, Fourth, Second) + shuffled.sorted shouldEqual List(First, Second, Third, Fourth) + } + + test("instancesFor / caseObjects do not compile for non-sealed types") { + "SealedUtils.caseObjects[String]" shouldNot typeCheck + "SealedUtils.instancesFor[ClassTag, String]" shouldNot typeCheck + } } diff --git a/core/src/test/scala/com/avsystem/commons/misc/SourceInfoTest.scala b/core/src/test/scala/com/avsystem/commons/misc/SourceInfoTest.scala index 18983d80e..55dec68d9 100644 --- a/core/src/test/scala/com/avsystem/commons/misc/SourceInfoTest.scala +++ b/core/src/test/scala/com/avsystem/commons/misc/SourceInfoTest.scala @@ -8,13 +8,16 @@ class SourceInfoTest extends AnyFunSuite with Matchers { val srcInfo = SourceInfo.here test("simple") { + // Scala 3 macro `Position.ofMacroExpansion` points to the receiver (start of + // `SourceInfo.here`) rather than the method name itself, hence different + // offset/column from the upstream Scala 2.13 macro. srcInfo should matchPattern { case SourceInfo( _, "SourceInfoTest.scala", - 216, + 205, 8, - 28, + 17, " val srcInfo = SourceInfo.here", List("srcInfo", "SourceInfoTest", "misc", "commons", "avsystem", "com"), ) =>