Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ val scalaCheckVersion = "1.19.0"
val scalazVersion = "7.3.9"
val scodecVersion = "1.11.11"
val scoptVersion = "4.1.0"
val hearthVersion = "0.4.1"

def macroParadise(configuration: Configuration): Def.Initialize[Seq[ModuleID]] =
Def.setting {
Expand Down Expand Up @@ -191,7 +192,10 @@ lazy val core = myCrossProject("core")
libraryDependencies ++=
macroParadise(Compile).value ++ (
if (isScala3Setting.value)
Seq()
Seq(
"com.kubuszok" %%% "hearth" % hearthVersion,
"com.kubuszok" % "hearth-cross-quotes_3" % hearthVersion % Provided
)
else
Seq(
scalaOrganization.value % "scala-reflect" % scalaVersion.value,
Expand All @@ -205,6 +209,21 @@ lazy val core = myCrossProject("core")
) ++ Seq(
"org.scalacheck" %%% "scalacheck" % scalaCheckVersion % Test
),
// On Scala 3 the macros use Hearth's cross-quotes DSL (`Expr.quote`/`Expr.splice`/`Expr.upcast`),
// which is desugared by the `hearth-cross-quotes` *compiler plugin*. The plugin is published for
// JVM only and reused across all platforms, so it is pulled in as a `Provided` dependency (see
// above) and wired into the compiler here via `-Xplugin`, resolved from the compile classpath.
scalacOptions ++= {
if (isScala3Setting.value)
Seq(
"-Xplugin:" + (Compile / dependencyClasspath).value
.map(_.data.getAbsolutePath)
.find(_.contains("hearth-cross-quotes"))
.getOrElse(sys.error("hearth-cross-quotes jar not found on classpath"))
)
else
Seq.empty
},
buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion),
buildInfoPackage := s"$rootPkg.internal"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ trait RefType[F[_, _]] extends Serializable {
def refine[P]: RefinePartiallyApplied[F, P] =
new RefinePartiallyApplied(this)

/**
* Macro that returns a value of type `T` refined as `F[T, P]` if it
* satisfies the predicate `P`, or fails to compile otherwise.
*
* Example: {{{
* scala> import eu.timepit.refined.api.{ Refined, RefType }
* | import eu.timepit.refined.numeric.Positive
*
* scala> RefType[Refined].refineM[Positive](10)
* res0: Refined[Int, Positive] = 10
* }}}
*
* Note: `M` stands for '''m'''acro.
*/
def refineM[P]: RefineMPartiallyApplied[F, P] =
new RefineMPartiallyApplied

def mapRefine[T, P, U](
tp: F[T, P]
)(f: T => U)(implicit v: Validate[U, P]): Either[String, F[U, P]] =
Expand Down Expand Up @@ -77,6 +94,24 @@ object RefType {
def applyRef[FTP]: ApplyRefPartiallyApplied[FTP] =
new ApplyRefPartiallyApplied

/**
* Macro that returns a value of type `T` refined as `FTP` if it
* satisfies the predicate in `FTP`, or fails to compile otherwise.
*
* Example: {{{
* scala> import eu.timepit.refined.api.{ Refined, RefType }
* | import eu.timepit.refined.numeric.Positive
*
* scala> type PosInt = Int Refined Positive
* scala> RefType.applyRefM[PosInt](10)
* res0: PosInt = 10
* }}}
*
* Note: `M` stands for '''m'''acro.
*/
def applyRefM[FTP]: ApplyRefMPartiallyApplied[FTP] =
new ApplyRefMPartiallyApplied

implicit val refinedRefType: RefType[Refined] =
new RefType[Refined] {
override def unsafeWrap[T, P](t: T): Refined[T, P] =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package eu.timepit.refined.api

import eu.timepit.refined.macros.RefinedTypeOpsM

/**
* Provides functions to create values of the refined type `FTP` from
* values of the base type `T`. It is intended to simplify the definition
Expand All @@ -20,7 +22,9 @@ package eu.timepit.refined.api
* res1: PosInt = 2
* }}}
*/
class RefinedTypeOps[FTP, T](implicit rt: RefinedType.AuxT[FTP, T]) extends Serializable {
class RefinedTypeOps[FTP, T](implicit rt: RefinedType.AuxT[FTP, T])
extends RefinedTypeOpsM[FTP, T]
with Serializable {

def from(t: T): Either[String, FTP] =
rt.refine(t)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,25 @@
package eu.timepit.refined

import eu.timepit.refined.api.RefType
import eu.timepit.refined.api.{Inference, RefType, Refined, Validate}
import eu.timepit.refined.macros.Macros

import scala.language.implicitConversions
import scala.quoted.*

/**
* Module that provides automatic refinements and automatic conversions
* between refined types (refinement subtyping) at compile-time.
*/
object auto {

/**
* Implicitly unwraps the `T` from a value of type `F[T, P]` using the
* `[[api.RefType]]` instance of `F`. This allows a `F[T, P]` to be
* used as it were a subtype of `T`.
*
* Example: {{{
* scala> import eu.timepit.refined.auto.autoUnwrap
* | import eu.timepit.refined.types.numeric.PosInt
*
* scala> def plusOne(i: Int): Int = i + 1
* | val x = PosInt.unsafeFrom(42)
*
* // converts x implicitly to an Int:
* scala> plusOne(x)
* res0: Int = 43
* }}}
*
* Note: This conversion is not needed if `F[T, _] <: T` holds (which
* is the case for `shapeless.tag.@@`, for example).
*/
implicit inline def autoRefineV[T, P](inline t: T)(implicit
inline v: Validate[T, P]
): Refined[T, P] = ${
Macros.autoRefineV[T, P]('t, 'v)
}

implicit inline def autoInfer[T, A, B](inline ta: Refined[T, A])(implicit
inline ir: Inference[A, B]
): Refined[T, B] = ${
Macros.autoInfer[T, A, B]('ta, 'ir)
}

implicit def autoUnwrap[F[_, _], T, P](tp: F[T, P])(implicit rt: RefType[F]): T =
rt.unwrap(tp)
}
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,16 @@ private[refined] trait BooleanInference2 extends BooleanInference3 {
implicit def conjunctionEliminationL[A, B, C](implicit p1: A ==> C): (A And B) ==> C =
p1.adapt("conjunctionEliminationL(%s)")

implicit def hypotheticalSyllogism[A, B, C](implicit p1: A ==> B, p2: B ==> C): A ==> C =
// NOTE: `hypotheticalSyllogism` (transitivity: `A ==> B`, `B ==> C` ⟹ `A ==> C`) from the Scala 2
// sources is intentionally omitted here. Its intermediate `B` appears only in the premises, never
// the conclusion, so resolving a goal through it spawns a free-RHS subgoal `A ==> ?B` that unifies
// with several always-valid rules at once (`minimalTautology`, `disjunctionIntroduction{L,R}`, ...),
// which Scala 3's implicit search reports as an ambiguity that aborts the whole search — including
// otherwise-derivable goals such as `Size[Interval.Closed[1, n]] ==> NonEmpty`. Dropping it keeps the
// common single-step and conjunction-elimination inferences working reliably; the price is that
// purely transitive two-hop chains (e.g. `Last[P] ==> NonEmpty`, via `Exists[P]`) are not derived.
// kept non implicit version for bin-compat
def hypotheticalSyllogism[A, B, C](implicit p1: A ==> B, p2: B ==> C): A ==> C =
Inference.combine(p1, p2, "hypotheticalSyllogism(%s, %s)")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package eu.timepit.refined.internal

import eu.timepit.refined.api.{Refined, Validate}
import eu.timepit.refined.macros.Macros

/**
* Helper class that allows the types `T` and `P` to be inferred from calls
* like `[[api.RefType.applyRefM]][F[T, P]](t)`.
*
* See [[http://tpolecat.github.io/2015/07/30/infer.html]] for a detailed
* explanation of this trick.
*/
final class ApplyRefMPartiallyApplied[FTP] {

inline def apply[T, P](inline t: T)(implicit
inline ev: Refined[T, P] =:= FTP,
inline v: Validate[T, P]
): FTP =
${ Macros.applyRef[FTP, T, P]('t, 'v) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package eu.timepit.refined.internal

import eu.timepit.refined.api.{RefType, Validate}
import eu.timepit.refined.macros.Macros

/**
* Helper class that allows the type `T` to be inferred from calls like
* `[[api.RefType.refineM]][P](t)`.
*
* See [[http://tpolecat.github.io/2015/07/30/infer.html]] for a detailed
* explanation of this trick.
*/
final class RefineMPartiallyApplied[F[_, _], P] {

// The macro only validates `t` against `P` at compile time (returning `t`); wrapping into `F[T, P]`
// is the zero-cost runtime `unsafeWrap`. This keeps the macro carrier-agnostic, so no higher-kinded
// macro over `F` is needed. `apply` is inline (not itself a macro) so it may combine the macro call
// with `unsafeWrap` — a macro's splice must be the entire right-hand side, which `validated` is.
inline def apply[T](inline t: T)(implicit rt: RefType[F], inline v: Validate[T, P]): F[T, P] =
rt.unsafeWrap[T, P](validated[T](t))

private inline def validated[T](inline t: T)(implicit inline v: Validate[T, P]): T =
${ Macros.refineM[T, P]('t, 'v) }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package eu.timepit.refined.internal

import scala.compiletime.{constValue, error}
import scala.compiletime.{constValue, error, summonFrom, summonInline}

/**
* `WitnessAs[A, B]` provides the singleton value of type `A` in `fst`
Expand Down Expand Up @@ -35,9 +35,18 @@ object WitnessAs extends WitnessAs1 {
): WitnessAs[A, B] =
WitnessAs(wa.value, nb.fromInt(ta.apply()))

inline given singletonWitnessAs[B, A <: B]: WitnessAs[A, B] = {
inline val a = constValue[A]
WitnessAs(a, a)
// Route by whether the base type `B` is a singleton (i.e. has a `ValueOf`):
// - object singletons (`B = Foo.type`) can't be witnessed by `constValue` ("not a constant type"),
// so use `ValueOf` — exercised only at runtime (`isValid`), matching Scala 2's `Equal[Foo.type]`;
// - everything else (`Int`, `Char`, `String`, ... literal witnesses) uses `constValue`, which the
// compile-time macros' `semiEval` reduces natively as a plain literal.
inline given singletonWitnessAs[B, A <: B]: WitnessAs[A, B] = summonFrom {
case _: ValueOf[B] =>
val v = summonInline[ValueOf[A]]
WitnessAs[A, B](v.value, v.value)
case _ =>
inline val a = constValue[A]
WitnessAs(a, a)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package eu.timepit.refined.macros

import hearth.*
import eu.timepit.refined.api.{Inference, Refined, Validate}

import scala.quoted.*

private[refined] class RefinedMacros(q: Quotes) extends MacroCommonsScala3(using q), RefinedMacro

private[refined] object Macros {

def autoRefineV[T: Type, P: Type](
t: Expr[T],
v: Expr[Validate[T, P]]
)(using q: Quotes): Expr[Refined[T, P]] =
new RefinedMacros(q).autoRefineImpl[T, P](t, v)

def autoInfer[T: Type, A: Type, B: Type](
ta: Expr[Refined[T, A]],
ir: Expr[Inference[A, B]]
)(using q: Quotes): Expr[Refined[T, B]] =
new RefinedMacros(q).autoInferImpl[T, A, B](ta, ir)

def refineMV[T: Type, P: Type](
t: Expr[T],
v: Expr[Validate[T, P]]
)(using q: Quotes): Expr[Refined[T, P]] =
new RefinedMacros(q).refineMVImpl[T, P](t, v)

def refineM[T: Type, P: Type](
t: Expr[T],
v: Expr[Validate[T, P]]
)(using q: Quotes): Expr[T] =
new RefinedMacros(q).refineMImpl[T, P](t, v)

def applyRef[FTP: Type, T: Type, P: Type](
t: Expr[T],
v: Expr[Validate[T, P]]
)(using q: Quotes): Expr[FTP] =
new RefinedMacros(q).applyRefImpl[FTP, T, P](t, v)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package eu.timepit.refined.macros

import hearth.*
import eu.timepit.refined.api.{Inference, RefType, Refined, Validate}

trait RefinedMacro { this: MacroCommons =>

def autoRefineImpl[T: Type, P: Type](
t: Expr[T],
v: Expr[Validate[T, P]]
): Expr[Refined[T, P]] = {
validateAtCompileTime(t, v)
Expr.quote(Refined.unsafeApply[T, P](Expr.splice(t)))
}

def autoInferImpl[T: Type, A: Type, B: Type](
ta: Expr[Refined[T, A]],
ir: Expr[Inference[A, B]]
): Expr[Refined[T, B]] = {
val inference = ir.semiEval match {
case Right(value) => value
case Left(errors) =>
Environment.reportErrorAndAbort(
s"Cannot evaluate Inference[${Type[A].plainPrint}, ${Type[B].plainPrint}] at compile time: ${errors.mkString(", ")}. "
)
}
if (!inference.isValid)
Environment.reportErrorAndAbort(
s"Inference failed: ${inference.show}"
)
Expr.quote(Refined.unsafeApply[T, B](Expr.splice(ta).value))
}

def refineMVImpl[T: Type, P: Type](
t: Expr[T],
v: Expr[Validate[T, P]]
): Expr[Refined[T, P]] = autoRefineImpl[T, P](t, v)

/**
* Validates `t` against `P` at compile time and returns `t` unchanged. Used by the carrier-generic
* `RefType.refineM`, whose wrapping into `F[T, P]` is done by the (zero-cost) runtime `unsafeWrap`,
* so no higher-kinded macro over `F` is needed.
*/
def refineMImpl[T: Type, P: Type](
t: Expr[T],
v: Expr[Validate[T, P]]
): Expr[T] = {
validateAtCompileTime(t, v)
t
}

def applyRefImpl[FTP: Type, T: Type, P: Type](
t: Expr[T],
v: Expr[Validate[T, P]]
): Expr[FTP] = {
validateAtCompileTime(t, v)
val refined: Expr[Refined[T, P]] = Expr.quote(Refined.unsafeApply[T, P](Expr.splice(t)))
Expr.upcast[Refined[T, P], FTP](refined)(using Type.of[Refined[T, P]], Type[FTP])
}

private def validateAtCompileTime[T: Type, P: Type](
t: Expr[T],
v: Expr[Validate[T, P]]
): Unit = {
val tValue = t.semiEval match {
case Right(value) => value
case Left(errors) =>
Environment.reportErrorAndAbort(
s"Cannot evaluate expression at compile time: ${errors.mkString(", ")}"
)
}
val validate = v.semiEval match {
case Right(value) => value
case Left(errors) =>
Environment.reportErrorAndAbort(
s"Cannot evaluate Validate[${Type[T].plainPrint}, ${Type[P].plainPrint}] at compile time: ${errors.mkString(", ")}. " +
s"Use refineV for runtime validation instead."
)
}
val result = validate.validate(tValue)
if (!result.isPassed)
Environment.reportErrorAndAbort(s"Predicate failed: ${validate.showResult(tValue, result)}")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package eu.timepit.refined.macros

import hearth.*
import eu.timepit.refined.api.{Refined, Validate}

import scala.quoted.*

trait RefinedTypeOpsM[FTP, T] {

inline def apply[P](
inline t: T
)(implicit inline ev: Refined[T, P] =:= FTP, inline v: Validate[T, P]): FTP =
${ Macros.applyRef[FTP, T, P]('t, 'v) }
}
Loading