-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Add compiletime Error type #7951
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello, and thank you for opening this PR! 🎉
All contributors have signed the CLA, thank you! ❤️
Have an awesome day! ☀️
I think that under |
I don't think there is a problem here. I doubt the two will need to interact, and even if so, then people can just refer to |
As I've discovered from my experience with the singleton-ops library, compile-time errors have a few issues that need to be discussed. Implicit Error RedirectionThere are many cases where the error will be used implicitly, so we need to make sure to redirect it to the enclosing implicit symbol's annotation. For example: import scala.compiletime.ops.string.{+, Error}
import scala.compiletime.ops.int.{>=, ToString}
object Test {
opaque type Positive[T <: Int] = T
object Positive {
def apply[T <: Int](value: T)(given pos : Positive[T]): Positive[T] = pos
given [T <: Int](given pos : ValueOf[RequirePositive[T]]) : Positive[T] = pos.value
type RequirePositive[T <: Int] <: Int= (T >= 0) match {
case true => T
case false => Error["The provided value (" + ToString[T] + ") isn't positive"]
}
}
val compiles = Positive(1)
val doesNotCompile = Positive(-1)
val compiles2 = implicitly[Positive[1]]
val doesNotCompile2 = implicitly[Positive[-1]] //What error is generated here?
} I don't have time to test your fork, but I'm guessing the Therefore, I propose you do exactly what I do in the singleton-ops library's macro. Before throwing the error I also check the enclosing implicit. If there are enclosing implicits then I set the annotation of its last implicit's symbol. This is how it looks in Scala 2 macro code: val defaultAnnotatedSym : Option[TypeSymbol] =
if (c.enclosingImplicits.isEmpty) None else c.enclosingImplicits.last.pt match {
case TypeRef(_,sym,_) => Some(sym.asType)
case x => Some(x.typeSymbol.asType)
}
//This is the method I call every time I wish to throw a compile-time error (expected or otherwise)
def abort(msg: String, annotatedSym : Option[TypeSymbol] = defaultAnnotatedSym): Nothing = {
if (annotatedSym.isDefined) setAnnotation(msg, annotatedSym.get)
c.abort(c.enclosingPosition, msg)
}
////////////////////////////////////////////////////////////////////
// Code thanks to Shapeless
// https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/lazy.scala
////////////////////////////////////////////////////////////////////
def setAnnotation(msg: String, annotatedSym : TypeSymbol): Unit = {
import c.internal._
import decorators._
val tree0 =
c.typecheck(
q"""
new _root_.scala.annotation.implicitNotFound("dummy")
""",
silent = false
)
class SubstMessage extends Transformer {
val global = c.universe.asInstanceOf[scala.tools.nsc.Global]
override def transform(tree: Tree): Tree = {
super.transform {
tree match {
case Literal(Constant("dummy")) => Literal(Constant(msg))
case t => t
}
}
}
}
val tree = new SubstMessage().transform(tree0)
annotatedSym.setAnnotations(Annotation(tree))
()
}
////////////////////////////////////////////////////////////////////
|
Non-Literal Value HandlingWhat do we do with non-literal values in errors? In your example, what does the following code do: val one : Int = 1
val negOne : Int = -1
val pos = Positive(one) //what happens here?
val neg = Positive(negOne) //what happens here? In the singleton-ops library, there is a compile-time error if my Workaround Scalac Eager WideningA problem will surface when we add a runtime fallback. Scalac (and also dotty is no different, I assume) is very eager to widen types to get things to compile. It's great in most situations, but can cause unexpected results here: type RequirePositive[T <: Int] <: Boolean = ConstOps[T] match {
case Some[t] => (t > 0) match {
case true => false //return false to indicate no runtime check is needed
case false => Error["The provided value (" + ToString[t] + ") isn't positive"]
case None => true //return true to indicate a runtime check is needed
}
def check[T <: Int](t : T)(given checkPos : ValueOf[RequirePositive[T]]) : Unit =
if (checkPos.value) assert(checkPos.value > 0, s"The provided value (${checkPos.value}) isn't positive")
val check1 = check(1) //should compile and run (not doing any runtime checks)
val checkNeg1 = check(-1) //should fail at compile-time
val one : Int = 1
val negOne : Int = -1
val checkOne = check(one) //should compile and run
val checkNegOne= check(negOne) //should compile and throw an exception The problem (I'm assuming) you will see in the example above without working around Scalac's widening is that the compiler will know that it can widen |
Warn[T]In addition to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import scala.compiletime.ops.string.{+, Error} import scala.compiletime.ops.int.{>=, ToString} object Test { opaque type Positive = Int object Positive { def apply[T <: Int](value: T)(given RequirePositive[T]): Positive = value type RequirePositive[T <: Int] <: Any = (T >= 0) match { case true => Any case false => Error["The provided value (" + ToString[T] + ") isn't positive"] } } val compiles: Positive = Positive[1](1) val doesNotCompile: Positive = Positive[-1](-1) // error: ^ The provided value (-1) isn't positive }
I'm confused from my syntax of singleton-ops, but shouldn't this code look like follows:
import scala.compiletime.ops.string.{+, Error}
import scala.compiletime.ops.int.{>=, ToString}
object Test {
opaque type Positive[T <: Int] = Positive.RequirePositive[T]
object Positive {
def apply[T <: Int](value : RequirePositive[T]) : Positive[T] = value
type RequirePositive[T <: Int] <: Int = (T > 0) match {
case true => T
case false => Error["The provided value (" + ToString[T] + ") isn't positive"]
}
}
val compiles: Positive = Positive[1](1)
val doesNotCompile: Positive = Positive[-1](-1)
// error: ^ The provided value (-1) isn't positive
}
|
||
object Positive { | ||
type RequirePositive[T <: Int] = Require[T >= 0, "The provided value (" + ToString[T] + ") isn't positive"] | ||
def apply[T <: Int](value: T)(given RequirePositive[T]): Positive = value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After thinking about this, I'm actually surprised this is working. This style is similar to the singleton-ops library and assumes that there is an implicit for RequirePositive[T <: Int]
, but why should there be? If there is no error, RequirePositive[T <: Int]
is actually Any
, so how come it works?
opaque type Positive = Int | ||
|
||
object Positive { | ||
type RequirePositive[T <: Int] = Require[T >= 0, "The provided value (" + ToString[T] + ") isn't positive"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be Require[T > 0, ...
Thank you for the feedback @soronpo! It will take another couple of weeks before I have some free time to invest into this PR, but I will get to it. |
Hey @MaximeKjaer, what is the current status on this PR? |
I still think this feature would be nice to have. But it proved to be a bit too big of a challenge for me to implement, both in terms of time and of my knowledge of the compiler. So if anyone else is interested in taking this on, you can go ahead if you'd like! A year on, the main conclusion I've come to is that I think this feature shouldn't be a compiletime op, but it should instead be tied exclusively to match types. This is for two reasons:
For these reasons, I think the feature should only be allowed in the right-hand side of a match type case; using it anywhere else should be disallowed (or at least, ignored). I've spoken a bit about this with @OlivierBlanvillain, and I think he agrees with the above (but do correct me if I'm wrong, @OlivierBlanvillain!). |
I may take this on later this year, trying to port singleton-ops to Scala 3 (or removing the need for it in by doing everything in Scala 3). |
Thanks @MaximeKjaer! I am closing this PR then for the time being. Maybe @soronpo can bookmark it to pick it up later. |
Hi @b-studios, @soronpo. I wanted to see if there were any updates regarding this PR? Such a feature would be useful for a project I am currently working on 🙂. |
@dorranh Unfortunately, I do not have time for this. There is also an issue of how to get this API addition approved, even if there was a PR. Currently there is no committee for the Scala standard library (which compiletime-ops is part of). |
Hi @soronpo, thanks for your response. No worries - it's good to know that this is not planned for the time being. |
This PR adds support for custom compile-time type error messages, making it possible to write the following:
Background
#7628 added compile-time singleton operations. With the ability to write more complex type-level computations comes the need for reporting errors neatly to the user. The
Error
type is implemented as another compile-time singleton op that reports a compilation error when evaluated by the compiler.Note that Dotty already has mechanisms for custom errors during implicit search through
@scala.annotation.implicitNotFound
, and during inlining throughscala.compiletime.error
.Discussion
This is a draft PR: it's not ready to be merged as is, as I would also like to discuss a few things:
Should
Error
be somewhere else thanscala.compiletime.ops.string
? Though it is currenlly implemented as such, it isn't really an operation on strings. Should it perhaps be inscala.compiletime.ops
, orscala.compiletime
?Should
Error
have another name? It clashes withjava.Error
.I think the current implementation of throwing an exception when the type is normalized is wrong. For instance, the following code does not compile:
What would be a better way to implement this?