Skip to content

Decouple transparent and erased properties #4860

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

Closed
Closed
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
7 changes: 5 additions & 2 deletions compiler/src/dotty/tools/dotc/core/Flags.scala
Original file line number Diff line number Diff line change
Expand Up @@ -548,8 +548,8 @@ object Flags {
/** Assumed to be pure */
final val StableOrErased = Stable | Erased

/** Labeled `private`, `final`, or `transparent` */
final val EffectivelyFinal = Private | Final | Transparent
/** Labeled `private`, `final`, `transparent`, or `erased` */
final val EffectivelyFinal = Private | Final | Transparent | Erased

/** A private method */
final val PrivateMethod = allOf(Private, Method)
Expand All @@ -563,6 +563,9 @@ object Flags {
/** A transparent parameter */
final val TransparentParam = allOf(Transparent, Param)

/** A transparent erased method */
final val TransparentErasedMethod = allOf(Transparent, Erased, Method)

/** An enum case */
final val EnumCase = allOf(Enum, Case)

Expand Down
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/core/TypeOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,10 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
/** Are we in a transparent method body? */
def inTransparentMethod = owner.ownersIterator.exists(_.isTransparentMethod)

/** Are we in a transparent method body? */
def inErasedTransparentMethod = owner.ownersIterator.exists(sym =>
sym.isTransparentMethod && sym.is(Erased))

/** Is `feature` enabled in class `owner`?
* This is the case if one of the following two alternatives holds:
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ public enum ErrorMessageID {
MatchCaseOnlyNullWarningID,
ImportRenamedTwiceID,
TypeTestAlwaysSucceedsID,
SpliceOutsideQuotesID,
;

public int errorNumber() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2105,4 +2105,12 @@ object messages {
}
val explanation = ""
}

case class SpliceOutsideQuotes() extends Message(SpliceOutsideQuotesID) {
val kind = "Syntax"
val msg = "splice outside quotes"
val explanation =
"""A splice may only appear inside quotes '{ ... },
|or else it must be the whole right hand side of a transparent method.""".stripMargin
}
}
25 changes: 5 additions & 20 deletions compiler/src/dotty/tools/dotc/transform/PostTyper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -156,25 +156,10 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
}
}

/** 1. If we are in a transparent method but not in a nested quote, mark the transparent method
* as a macro.
*
* 2. If selection is a quote or splice node, record that fact in the current compilation unit.
/** If selection is a quote or splice node, record that fact in the current compilation unit.
*/
private def handleMeta(sym: Symbol)(implicit ctx: Context): Unit = {

def markAsMacro(c: Context): Unit =
if (c.owner eq c.outer.owner) markAsMacro(c.outer)
else if (c.owner.isTransparentMethod) {
c.owner.setFlag(Macro)
}
else if (!c.outer.owner.is(Package)) markAsMacro(c.outer)

if (sym.isSplice || sym.isQuote) {
markAsMacro(ctx)
ctx.compilationUnit.containsQuotesOrSplices = true
}
}
private def reportMeta(sym: Symbol)(implicit ctx: Context): Unit =
if (sym.isSplice || sym.isQuote) ctx.compilationUnit.containsQuotesOrSplices = true

private object dropInlines extends TreeMap {
override def transform(tree: Tree)(implicit ctx: Context): Tree = tree match {
Expand All @@ -186,13 +171,13 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
override def transform(tree: Tree)(implicit ctx: Context): Tree =
try tree match {
case tree: Ident if !tree.isType =>
handleMeta(tree.symbol)
reportMeta(tree.symbol)
tree.tpe match {
case tpe: ThisType => This(tpe.cls).withPos(tree.pos)
case _ => tree
}
case tree @ Select(qual, name) =>
handleMeta(tree.symbol)
reportMeta(tree.symbol)
if (name.isTypeName) {
Checking.checkRealizable(qual.tpe, qual.pos.focus)
super.transform(tree)(ctx.addMode(Mode.Type))
Expand Down
10 changes: 7 additions & 3 deletions compiler/src/dotty/tools/dotc/transform/ReifyQuotes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import SymUtils._
import NameKinds._
import dotty.tools.dotc.ast.tpd.Tree
import typer.Implicits.SearchFailureType

import reporting.diagnostic.messages._
import scala.collection.mutable
import dotty.tools.dotc.core.StdNames._
import dotty.tools.dotc.core.quoted._
Expand Down Expand Up @@ -242,6 +242,10 @@ class ReifyQuotes extends MacroTransformWithImplicits {
!sym.is(Param) || levelOK(sym.owner)
}

/** Issue a "splice outside quote" error unless we are in the body of a transparent method */
def spliceOutsideQuotes(pos: Position)(implicit ctx: Context): Unit =
ctx.error(SpliceOutsideQuotes(), pos)

/** Try to heal phase-inconsistent reference to type `T` using a local type definition.
* @return None if successful
* @return Some(msg) if unsuccessful where `msg` is a potentially empty error message
Expand Down Expand Up @@ -300,7 +304,7 @@ class ReifyQuotes extends MacroTransformWithImplicits {
outer.checkType(pos).foldOver(acc, tp)
}
else {
if (tp.isTerm) ctx.error(i"splice outside quotes", pos)
if (tp.isTerm) spliceOutsideQuotes(pos)
tp
}
case tp: NamedType =>
Expand Down Expand Up @@ -431,7 +435,7 @@ class ReifyQuotes extends MacroTransformWithImplicits {
if (ctx.reporter.hasErrors) splice else transform(evaluatedSplice)
}
else if (!ctx.owner.is(Transparent)) { // level 0 outside a transparent definition
ctx.error(i"splice outside quotes or transparent method", splice.pos)
spliceOutsideQuotes(splice.pos)
splice
}
else if (Splicer.canBeSpliced(splice.qualifier)) { // level 0 inside a transparent definition
Expand Down
6 changes: 5 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/Inliner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,12 @@ object Inliner {
/** Should call with method `meth` be inlined in this context? */
def isInlineable(meth: Symbol)(implicit ctx: Context): Boolean = {

/** Suppress inlining of
* - non-erased methods inside a transparent method,
* - all methods inside an erased transparent method
*/
def suppressInline =
ctx.inTransparentMethod ||
ctx.inTransparentMethod && (!meth.is(Erased) || ctx.inErasedTransparentMethod) ||
ctx.settings.YnoInline.value ||
ctx.isAfterTyper ||
ctx.reporter.hasErrors
Expand Down
9 changes: 0 additions & 9 deletions compiler/src/dotty/tools/dotc/typer/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1195,15 +1195,6 @@ class Namer { typer: Typer =>
instantiateDependent(restpe, typeParams, termParamss)
ctx.methodType(tparams map symbolOfTree, termParamss, restpe, isJava = ddef.mods is JavaDefined)
}
if (sym.is(Transparent) &&
sym.unforcedAnnotation(defn.ForceInlineAnnot).isEmpty)
// Need to keep @forceInline annotated methods around to get to parity with Scala.
// This is necessary at least until we have full bootstrap. Right now
// dotty-bootstrapped involves running the Dotty compiler compiled with Scala 2 with
// a Dotty runtime library compiled with Dotty. If we erase @forceInline annotated
// methods, this means that the support methods in dotty.runtime.LazyVals vanish.
// But they are needed for running the lazy val implementations in the Scala-2 compiled compiler.
sym.setFlag(Erased)
if (isConstructor) {
// set result type tree to unit, but take the current class as result type of the symbol
typedAheadType(ddef.tpt, defn.UnitType)
Expand Down
10 changes: 10 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/PrepareTransparent.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import ProtoTypes.selectionProto
import SymDenotations.SymDenotation
import Annotations._
import transform.{ExplicitOuter, AccessProxies}
import transform.SymUtils._
import Inferencing.fullyDefinedType
import config.Printers.inlining
import ErrorReporting.errorTree
Expand Down Expand Up @@ -250,6 +251,15 @@ object PrepareTransparent {
inlined.updateAnnotation(LazyBodyAnnotation { _ =>
implicit val ctx = inlineCtx
val rawBody = treeExpr(ctx)

def markAsMacro(body: Tree): Unit = body match {
case _: Select if body.symbol.isSplice => inlined.setFlag(Erased | Macro)
case Block(Nil, expr) => markAsMacro(expr)
case Block(expr :: Nil, Literal(Constant(()))) => markAsMacro(expr)
case _ =>
}
markAsMacro(rawBody)

val typedBody =
if (ctx.reporter.hasErrors) rawBody
else ctx.compilationUnit.inlineAccessors.makeInlineable(rawBody)
Expand Down
16 changes: 4 additions & 12 deletions compiler/src/dotty/tools/dotc/typer/RefChecks.scala
Original file line number Diff line number Diff line change
Expand Up @@ -143,16 +143,14 @@ object RefChecks {
* 1.8.1 M's type is a subtype of O's type, or
* 1.8.2 M is of type []S, O is of type ()T and S <: T, or
* 1.8.3 M is of type ()S, O is of type []T and S <: T, or
* 1.9 M must not be a typelevel def or a Dotty macro def
* 1.10. If M is a 2.x macro def, O cannot be deferred unless there's a concrete method overriding O.
* 1.11. If M is not a macro def, O cannot be a macro def.
* 1.9 If M is an erased definition, it must override at least one concrete member
* 2. Check that only abstract classes have deferred members
* 3. Check that concrete classes do not have deferred definitions
* that are not implemented in a subclass.
* 4. Check that every member with an `override` modifier
* overrides some other member.
* TODO check that classes are not overridden
* TODO This still needs to be cleaned up; the current version is a staright port of what was there
* TODO This still needs to be cleaned up; the current version is a straight port of what was there
* before, but it looks too complicated and method bodies are far too large.
*/
private def checkAllOverrides(clazz: Symbol)(implicit ctx: Context): Unit = {
Expand Down Expand Up @@ -376,14 +374,8 @@ object RefChecks {
overrideError("may not override a non-lazy value")
} else if (other.is(Lazy) && !other.isRealMethod && !member.is(Lazy)) {
overrideError("must be declared lazy to override a lazy value")
} else if (member.is(Erased) && member.allOverriddenSymbols.forall(_.is(Deferred))) { // (1.9)
overrideError("is an erased method, may not override only deferred methods")
} else if (member.is(Macro, butNot = Scala2x)) { // (1.9)
overrideError("is a macro, may not override anything")
} else if (other.is(Deferred) && member.is(Scala2Macro) && member.extendedOverriddenSymbols.forall(_.is(Deferred))) { // (1.10)
overrideError("cannot be used here - term macros cannot override abstract methods")
} else if (other.is(Macro) && !member.is(Macro)) { // (1.11)
overrideError("cannot be used here - only term macros can override term macros")
} else if (member.is(Erased) && other.is(Deferred) && member.extendedOverriddenSymbols.forall(_.is(Deferred))) { // (1.9)
overrideError("is erased, cannot override only abstract methods")
} else if (!compatibleTypes(memberTp(self), otherTp(self)) &&
!compatibleTypes(memberTp(upwardsSelf), otherTp(upwardsSelf))) {
overrideError("has incompatible type" + err.whyNoMatchStr(memberTp(self), otherTp(self)))
Expand Down
7 changes: 5 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -974,8 +974,11 @@ class Typer extends Namer
val unchecked = pt.isRef(defn.PartialFunctionClass)
typed(desugar.makeCaseLambda(tree.cases, protoFormals.length, unchecked) withPos tree.pos, pt)
case id @ untpd.ImplicitScrutinee() =>
if (tree.getAttachment(PrepareTransparent.TopLevelMatch).isEmpty)
if (tree.getAttachment(PrepareTransparent.TopLevelMatch).isDefined)
ctx.owner.setFlag(Erased)
else
ctx.error(em"implicit match cannot be used here; it must occur as a toplevel match of a transparent method", tree.pos)

val sel1 = id.withType(defn.ImplicitScrutineeTypeRef)
typedMatchFinish(tree, sel1, sel1.tpe, pt)
case _ =>
Expand Down Expand Up @@ -2351,7 +2354,7 @@ class Typer extends Namer
// - the current tree is a synthetic apply which is not expandable (eta-expasion would simply undo that)
if (arity >= 0 &&
!tree.symbol.isConstructor &&
!tree.symbol.is(TransparentMethod) &&
!tree.symbol.is(TransparentErasedMethod) &&
!ctx.mode.is(Mode.Pattern) &&
!(isSyntheticApply(tree) && !isExpandableApply))
simplify(typed(etaExpand(tree, wtp, arity), pt), pt, locked)
Expand Down
35 changes: 9 additions & 26 deletions docs/docs/typelevel.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,34 +229,17 @@ The following rewrite rules are performed when simplifying inlined bodies:
Dropping a binding might make other bindings redundant. Garbage collection proceeds until no further bindings
can be dropped.

## Restrictions for Transparent and Typelevel Functions
## Restrictions for Transparent and Erased Functions

Transparent methods are effectively final; they may not be overwritten.

If a transparent
method has a toplevel match expression or a toplevel splice `~` on its right-hand side,
it is classified as a typelevel method that can _only_ be executed at compile time. For typelevel methods two more restrictions apply:
Transparent methods with a toplevel implicit match or macro splice are classified `erased` - an `erased` modifier can be given for them, but it is redundant. Erased transparent methods must be always fully applied. In addition, the restrictions on normal erased methods apply, including:

1. They must be always fully applied.
2. They may override other methods only if one of the overridden methods is concrete.
1. They may not override other methods.
2. They may not be referred to from a non-erased context.

The right hand side of a typelevel method is never invoked by dynamic dispatch. As an example consider a situation like the following:
```scala
class Iterable[T] {
def foreach(f: T => Unit): Unit = ...
}
class List[T] extends Iterable[T] {
override transparent def foreach(f: T => Unit): Unit = ...
}
val xs: Iterable[T] = ...
val ys: List[T] = ...
val zs: Iterable[T] = ys
xs.foreach(f) // calls Iterable's foreach
ys.foreach(f) // expands to the body of List's foreach
zs.foreach(f) // calls Iterable's foreach
```
It follows that an overriding typelevel method should implement exactly the same semantics as the
method it overrides (but possibly more efficiently).
**Question:** We currently set `erased` automatically for macros, i.e. methods with a
right-hand side of the form `~...`. But we require it to be written explicitly for methods that have an implicit match as RHS. Should these situations be treated in the same instead? If yes, which of the two is preferable?

## Matching on Types

Expand Down Expand Up @@ -415,7 +398,7 @@ There are some proposals to improve the situation in specific areas, for instanc
By contrast, the new `implicit match` construct makes implicit search available in a functional context. To solve
the problem of creating the right set, one would use it as follows:
```scala
transparent def setFor[T]: Set[T] = implicit match {
erased transparent def setFor[T]: Set[T] = implicit match {
case ord: Ordering[T] => new TreeSet[T]
case _ => new HashSet[T]
}
Expand All @@ -426,8 +409,8 @@ Patterns are tried in sequence. The first case with a pattern `x: T` such that a
of type `T` can be summoned is chosen. The variable `x` is then bound to the implicit value for the remainder of the case. It can in turn be used as an implicit in the right hand side of the case.
It is an error if one of the tested patterns gives rise to an ambiguous implicit search.

Implicit matches can only occur as toplevel match expressions of transparent methods. This ensures that
all implicit searches are done at compile-time.
Implicit matches can only occur as toplevel match expressions of a `transparent` method.
If a transpatent method contains implicit matches, it is classified as `erased` - an `erased` modifier can be given for it, but it is redundant. This ensures that all implicit searches are done at compile-time.

## Transparent Values

Expand Down
2 changes: 1 addition & 1 deletion tests/neg/implicitMatch-syntax.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ object Test {
case _ => new HashSet[T]
}}

def f6[T] = implicit match { // error: implicit match cannot be used here
erased def f6[T] = implicit match { // error: implicit match cannot be used here
case _ => new HashSet[T]
}
}
18 changes: 9 additions & 9 deletions tests/neg/quote-macro-splice.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@ import scala.quoted._

object Test {

transparent def foo1: Int = { // error
transparent def foo1: Int = {
println()
~impl(1.toExpr)
~impl(1.toExpr) // error: splice outside quotes
}

transparent def foo2: Int = { // error
~impl(1.toExpr)
~impl(2.toExpr)
transparent def foo2: Int = {
~impl(1.toExpr) // error: splice outside quotes
~impl(2.toExpr) // error: splice outside quotes
}

transparent def foo3: Int = { // error
transparent def foo3: Int = {
val a = 1
~impl('(a))
~impl('(a)) // error: splice outside quotes
}

transparent def foo4: Int = { // error
~impl('(1))
transparent def foo4: Int = {
~impl('(1)) // error: splice outside quotes
1
}

Expand Down
2 changes: 1 addition & 1 deletion tests/neg/transparent-override/B_2.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class B extends A {
transparent def f(x: Int): Int = x match { // error
erased transparent def f(x: Int): Int = x match { // error
case 0 => 1
case _ => x
}
Expand Down
2 changes: 1 addition & 1 deletion tests/neg/typelevel-noeta.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
object Test {
def anyValue[T]: T = ???

transparent def test(x: Int) = x match {
erased transparent def test(x: Int) = x match {
case _: Byte =>
case _: Char =>
}
Expand Down
2 changes: 1 addition & 1 deletion tests/pos/i4773.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import scala.quoted._
object Foo {
transparent def foo2(): Unit = ~foo2Impl()
def foo2Impl(): Expr[Unit] = '()
transparent def foo(): Unit = foo2()
erased transparent def foo(): Unit = foo2()
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ object Test {
}

def main(args: Array[String]): Unit = {
val q"class $name extends $parent" = new Object // error: method unapply is used
val q"class $name extends $parent" = new Object
println(name)
println(parent)
}
Expand Down
7 changes: 6 additions & 1 deletion tests/pos/typelevel-vector1.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ object Test {
val z: S[S[S[S[Z]]]] = y

transparent def concat[T, N1 <: Nat, N2 <: Nat](xs: Vec[T, N1], ys: Vec[T, N2]): Vec[T, _] = {
val length = Typed(add(erasedValue[N1], erasedValue[N2]))
erased val length = Typed(add(erasedValue[N1], erasedValue[N2]))
Vec[T, length.Type](xs.elems ++ ys.elems)
}

val xs = Vec[Int, S[S[Z]]](List(1, 2))
val ys = Vec[Int, S[Z]](List(3))
val zs = concat(xs, ys)
val zsc: Vec[Int, S[S[S[Z]]]] = zs
}

Loading