-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add @experimental
annotation
#12102
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 @experimental
annotation
#12102
Conversation
83fb0e2
to
4aaca69
Compare
f5b4430
to
290f36d
Compare
290f36d
to
2a7d472
Compare
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.
@sjrd are there some important test cases that I may have missed?
Concrete type alias (danger of being dealiased) and opaque type alias are two good candidates. Otherwise it seems pretty exhaustive. :)
tests/neg-custom-args/no-experimental/experimentalAnnotation.scala
Outdated
Show resolved
Hide resolved
def isExperimental(using Context): Boolean = | ||
(self eq defn.ExperimentalAnnot) | ||
|| self.hasAnnotation(defn.ExperimentalAnnot) | ||
|| self.allOverriddenSymbols.nonEmpty && self.allOverriddenSymbols.forall(_.hasAnnotation(defn.ExperimentalAnnot)) // TODO infer @experimental? |
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.
I would say a method is experimental if at least one method it overrides is experimental:
|| self.allOverriddenSymbols.nonEmpty && self.allOverriddenSymbols.forall(_.hasAnnotation(defn.ExperimentalAnnot)) // TODO infer @experimental? | |
|| self.allOverriddenSymbols.exists(_.hasAnnotation(defn.ExperimentalAnnot)) // TODO infer @experimental? |
but I think more properly when we do override checks in RefChecks, we should disallow overriding an experimental method from a non-experimental method, same with subclassing an experimental class.
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.
For methods, I am not sure what we should do with the following case
trait A:
@experimental def f: Int
trait B:
def f: Int
trait C extends A, B:
def f: Int = 3
Is C.f
experimental? It implements an experimental API but at the same time it implements a stable API. If we look at it as implementing B.f
it can be considered as non-experimental.
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.
It implements both and if A.f is removed that might have binary compatibilty implications on C.f and its users, so yes.
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.
Applying the LSP, the only thing that should be forbidden is when an experimental method implements/overrides a non-experimental one, for example:
trait A:
def f: Int
def g: Int = 3
trait B extends A:
@experimental def f: Int = 4 // error
@experimental override def g: Int = 5 // error
because if that were allowed, one could call B.f
or B.g
through an A
without being detected.
It's perfectly fine to implement or override an experimental method with a non-experimental one.
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.
It's perfectly fine to implement or override an experimental method with a non-experimental one.
I'm not convinced, it's at least not source-compatible since if that method gets removed, your usage of override
is now an error. I would err on the side of caution and disallow both directions.
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.
It implements both and if A.f is removed that might have binary compatibilty implications on C.f and its users, so yes.
No, it can't. Users of C.f
will always produce bytecode that call C.f
itself, never A.f
or any of the bridges generated in C
to adapt it to A.f
.
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.
but I think more properly when we do override checks in RefChecks, we should disallow overriding an experimental method from a non-experimental method
Do you mean that we need to annotate each experimental method with @experimental
? That would imply that all members of an experimental class must be individually annotated.
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.
If the class itself is experimental then when you subclass it, the subclass should be marked experimental, but no need to mark the individual methods as experimental
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.
It's perfectly fine to implement or override an experimental method with a non-experimental one.
I'm not convinced, it's at least not source-compatible since if that method gets removed, your usage of
override
is now an error. I would err on the side of caution and disallow both directions.
Any change of public/protected API, even experimental, is always going to be source incompatible. Removing an experimental top-level class could change how some name resolution behaves, even if it did not actually resolve to that class before. You can't protect people from theoretical source incompatibilities. Don't try.
tests/neg-custom-args/no-experimental/experimentalAnnotation.scala
Outdated
Show resolved
Hide resolved
6a12e15
to
c23db64
Compare
object FromDigits { | ||
|
||
/** A subclass of `FromDigits` that also allows to convert whole number literals | ||
* with a radix other than 10 | ||
*/ | ||
@experimental |
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.
Is it necesary to mark these definitions as experimental since their enclosing object is already experimental?
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.
It is by the inheritance check. We could add an exception, but then we would need the annotation inference again or make the isExperimental
more computationally expensive.
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.
The best solution I found is to infer all @experimental
on classes nested in an @experimental
definition and then require the annotation on any class that extends an experimental class. This implies that if we have a class that is both nested in experimental class and extends an experimental class it will infer the annotation. This also implies that we only need to look at the direct parents to do the checks.
Can this be added in 3.0.1? I am wary of any additional restrictions we impose so late in the game. Anything that makes some programs not compile should be deferred. |
We could put the annotation in the library now but only give it meaning in 3.0.1, this does mean we can't rely on it for FromDigits but that's not a big deal I think. |
926a23c
to
9d75466
Compare
The `@exerimental` annotation marks definitions as _experimental_ feature. These can be used in the same situattions where `languange.experimental` can be used. * A class is experimental if * It is annotated with `@experimental` * It is a nested class of an experimental class. Annotation `@experimental` is inferred. * It extends an experimental class. An error is emitted if it does not have @experimental. * A member definition is experimental if * It is annotated with `@experimental` * Its a member of an experimental class * `class experimental` is experimental
699cb68
to
112bea7
Compare
|
||
private def annotateExperimental(sym: Symbol)(using Context): Unit = | ||
if sym.is(Enum) && sym.hasAnnotation(defn.ExperimentalAnnot) then | ||
// Add @experimental annotation to enum class definitions |
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.
Why is this needed if we also have checkExperimentalInheritance
?
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 is not needed anymore. Removed the code. Though now there is similar looking logic for case class
modules.
@@ -0,0 +1,31 @@ | |||
import scala.annotation.experimental | |||
|
|||
@experimental // error |
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.
Looks like we don't have any test for an @experimental case class
, I think we need to make sure that the generated companion is also marked @experimental
(or else just always check both the class and the companion since having one experimental but not the other isn't very useful).
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.
Added the annotation in annotateExperimental
.
dbdd1b3
to
59a2f32
Compare
62c1293
to
b5a2752
Compare
The
@exerimental
annotation marks definitions as experimental feature.These can be used in the same situations where
languange.experimental
can be used.@experimental
@experimental
is inferred.@experimental
class experimental
is experimental