Skip to content
Draft
41 changes: 32 additions & 9 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 |
|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|--------|
Expand All @@ -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 |
Expand Down Expand Up @@ -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 |
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
}
}
52 changes: 34 additions & 18 deletions core/src/main/scala/com/avsystem/commons/misc/SealedUtils.scala
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -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),
)
Expand All @@ -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]
Expand Down
32 changes: 30 additions & 2 deletions core/src/main/scala/com/avsystem/commons/misc/SourceInfo.scala
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand All @@ -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("<no content>")
)
},
${ Expr(enclosingLoop(Symbol.spliceOwner.owner, Vector.empty).toList) },
)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
13 changes: 13 additions & 0 deletions core/src/test/scala/com/avsystem/commons/misc/NamedEnumTest.scala
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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)
}
}
53 changes: 48 additions & 5 deletions core/src/test/scala/com/avsystem/commons/misc/SealedEnumTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,69 @@ 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
case object Second extends SomeEnum
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
}
}
Loading
Loading