Skip to content

Commit

Permalink
require @unchecked for effect casts, support effect ascriptions.
Browse files Browse the repository at this point in the history
fixes #5
  • Loading branch information
lrytz committed May 12, 2013
1 parent ac86c46 commit 998e74a
Show file tree
Hide file tree
Showing 14 changed files with 215 additions and 44 deletions.
4 changes: 4 additions & 0 deletions src/main/scala/scala/tools/nsc/effects/EffectDomain.scala
Expand Up @@ -66,6 +66,10 @@ abstract class EffectDomain extends Infer with RelEffects with DefaultEffects wi
final def existsEffectAnnotation(annotations: List[AnnotationInfo]) = {
annotations.exists(ann => allEffectAnnots.contains(ann.atp.typeSymbol))
}

final def hasUncheckedAnnotation(annotations: List[AnnotationInfo]) = {
annotations.exists(ann => ann.atp.typeSymbol == definitions.UncheckedClass)
}

final lazy val pureClass = rootMirror.getClassByName(newTypeName("scala.annotation.effects.pure"))

Expand Down
37 changes: 29 additions & 8 deletions src/main/scala/scala/tools/nsc/effects/Infer.scala
Expand Up @@ -151,20 +151,41 @@ trait Infer { self: EffectDomain =>
* is defined. Instead of overriding this methods, effect domains should override `computeEffectImpl`.
*/
final def computeEffect(tree: Tree, ctx: EffectContext): Effect = {
def collectAnnots(typed: Typed): List[AnnotationInfo] ={
typed.tpt.tpe.annotations ::: (typed.expr match {
case typed1: Typed => collectAnnots(typed1)
case _ => Nil
})

def collectAnnots(typed: Typed): (List[AnnotationInfo], Tree) = {
val annots = typed.tpt.tpe.annotations
typed.expr match {
case typed1: Typed =>
val (restAnnots, expr) = collectAnnots(typed1)
(annots ::: restAnnots, expr)
case expr =>
(annots, expr)
}
}

if (tree.isErroneous) bottom
else {

val e = tree match {
/*** Type Ascriptions: if they contain effect annotations, they are treated as effect casts ***/
case typed: Typed if existsEffectAnnotation(collectAnnots(typed)) =>
fromAnnotationList(collectAnnots(typed))
/* Type ascriptions containing effect annotations AND the `@unchecked` annotations are effect casts,
* the ascripted tree is not effect-checked.
*
* Type ascriptions with effect annotations, but without `@unchecked` are effect ascriptions. They
* change the expected effect, which allows users to check the effect of a subtree.
*/
case typed: Typed =>
val (annots, expr) = collectAnnots(typed)
val hasEffAnnots = existsEffectAnnotation(annots)
if (hasUncheckedAnnotation(annots) && hasEffAnnots) {
fromAnnotationList(annots)
} else if (hasEffAnnots) {
val newExpected = fromAnnotationList(annots)
val newExpOpt = Some(newExpected) // or rather ctx.expected.map(_ => newExpected) ?
computeEffectImpl(tree, ctx.copy(expected = newExpOpt))
newExpected
} else {
computeEffectImpl(tree, ctx)
}

case _ =>
computeEffectImpl(tree, ctx)
Expand Down
48 changes: 32 additions & 16 deletions src/main/scala/scala/tools/nsc/effects/TypeCheckerPlugin.scala
Expand Up @@ -211,7 +211,26 @@ trait TypeCheckerPlugin extends TypeUtils { // self: EffectChecker =>
case tt @ TypeTree() => tt.wasEmpty
case _ => false
})

def expectedIfNotUnchecked(annots: List[AnnotationInfo]): Option[Effect] = {
if (hasUncheckedAnnotation(annots) && existsEffectAnnotation(annots)) {
None
} else {
Some(fromAnnotationList(annots))
}
}

def constructorEffectAnnotations(constrSym: Symbol, typeDefAnnots: List[AnnotationInfo]): List[AnnotationInfo] = {
val annotatedSym = if (constrSym.isPrimaryConstructor) {
val clazz = constrSym.owner
if (clazz.isModuleClass) clazz.sourceModule else clazz
} else constrSym
val symAnnots = annotatedSym.annotations
if (existsEffectAnnotation(symAnnots))
symAnnots
else
typeDefAnnots
}

/**
* Returns the annotated effect of a constructor, if an effect annotation exists.
Expand All @@ -220,16 +239,9 @@ trait TypeCheckerPlugin extends TypeUtils { // self: EffectChecker =>
* are annotated on the constructor symbol
*/
def annotatedConstrEffect(constrSym: Symbol, typeDefAnnots: List[AnnotationInfo]): Option[(Effect, List[RelEffect])] = {
val annotatedSym = if (constrSym.isPrimaryConstructor) {
val clazz = constrSym.owner
if (clazz.isModuleClass) clazz.sourceModule else clazz
} else constrSym
val symAnnots = annotatedSym.annotations

if (existsEffectAnnotation(symAnnots))
Some((fromAnnotationList(symAnnots), relFromAnnotationList(symAnnots)))
else if (existsEffectAnnotation(typeDefAnnots))
Some(fromAnnotationList(typeDefAnnots), relFromAnnotationList(typeDefAnnots))
val annots = constructorEffectAnnotations(constrSym, typeDefAnnots)
if (existsEffectAnnotation(annots))
Some(fromAnnotationList(annots), relFromAnnotationList(annots))
else
None
}
Expand Down Expand Up @@ -277,7 +289,7 @@ trait TypeCheckerPlugin extends TypeUtils { // self: EffectChecker =>
}

case None => constrDef.rhs match {
case DefDef(_, _, _, _, _, Block((td @ TypeDef(_, ConstrEffTypeDefName, _, _)) :: _, _)) =>
case Block(thisCall :: (td @ TypeDef(_, ConstrEffTypeDefName, _, _)) :: _, _) =>
if (!alreadyTyped) {
typer.namer.enterSym(td)
}
Expand Down Expand Up @@ -811,13 +823,16 @@ trait TypeCheckerPlugin extends TypeUtils { // self: EffectChecker =>
None // primary constr effes are handled separately, see case ClassDef/ModuleDef below
} else if (meth.isConstructor) {
val typeDefAnnots = constrEffTypeDefAnnots(ddef, None, typer, alreadyTyped = true)
val annots = constructorEffectAnnotations(meth, typeDefAnnots)
// we use `annotatedConstrEffect` to test if the constructor ahs an annotated effect.
// if that's the case, we read the expected effect from the constructor's return type.
annotatedConstrEffect(meth, typeDefAnnots).map(_ => fromAnnotation(meth.tpe))
} else if (!tpt.wasEmpty)
Some(fromAnnotation(meth.tpe))
else
expectedIfNotUnchecked(annots).map(_ => fromAnnotation(meth.tpe))
} else if (!tpt.wasEmpty) {
val annots = meth.tpe.finalResultType.annotations
expectedIfNotUnchecked(annots)
} else {
None
}
}

expectedEffect foreach (annotEff => {
Expand All @@ -839,7 +854,8 @@ trait TypeCheckerPlugin extends TypeUtils { // self: EffectChecker =>
val (_, templTyper) = templates.get(constrSym.owner)
val constrDefTyper = analyzer.newTyper(templTyper.context.makeNewScope(constrDef, constrSym))
val typeDefAnnots = constrEffTypeDefAnnots(constrDef, Some(templ), templTyper, alreadyTyped = true)
for (annotEff <- annotatedConstrEffect(constrSym, typeDefAnnots)) {
val annots = constructorEffectAnnotations(constrSym, typeDefAnnots)
for (_ <- expectedIfNotUnchecked(annots)) {
// as expected effect we use the one on the return type of the constructor, not the one on the
// constructor symbol or the typeDef
val expected = Some(adaptExpectedMethodEffect(constrSym, fromAnnotation(constrSym.tpe)))
Expand Down
@@ -0,0 +1,94 @@
import annotation.effects._

class C {
def no: Unit @noIo = ()
def yo: Unit @io = ()


// pos ascription
def f1 = {
if (true) (no: @noIo) else (yo: @io)
(no: @io)
}

// pos cast
def f4: Unit @noIo = {
(yo: @noIo @unchecked)
}

// pos cast
def f5: Unit @noIo = {
(yo: @unchecked @noIo)
}

// pos cast
def f6: Unit @unchecked @noIo = {
yo
}

// pos cast
@unchecked @pure
class C1 {
println()
}
def t1: C1 @pure = new C1

// pos cast
class C2 {
@unchecked @pure type constructorEffect
println()
}
def t2: C2 @pure = new C2

// pos cast
class C3 {
@unchecked @pure def this(x: Int) = {
this()
println()
}
}
def t3: C3 @pure = new C3(2)

// pos cast
class C4 {
def this(x: Int) = {
this()
@unchecked @pure type constructorEffect = Nothing
println()
}
}
def t4: C4 @pure = new C4(2)
}

class C1 {
import scala.{unchecked => un}

def no: Unit @noIo = ()
def yo: Unit @io = ()

def f4: Unit @noIo = {
(yo: @un @noIo)
}
}

class C2 {
import scala.{unchecked => !}

def no: Unit @noIo = ()
def yo: Unit @io = ()

def f4: Unit @noIo = {
(yo: @`!` @noIo)
}
}

class C3 {
type un = scala.unchecked

def no: Unit @noIo = ()
def yo: Unit @io = ()

def f4: Unit @noIo = {
(yo: @un @noIo)
}
}
@@ -0,0 +1,10 @@
newSource:9: error: effect type mismatch;
found : @io
required: @noIo
(no: @io)
^
newSource:14: error: effect type mismatch;
found : @io
required: @noIo
(yo: @noIo)
^
@@ -0,0 +1,16 @@
import annotation.effects._

class C {
def no: Unit @noIo = ()
def yo: Unit @io = ()

// neg ascription
def f2: Unit @noIo = {
(no: @io)
}

// neg ascription
def f3: Unit @noIo = {
(yo: @noIo)
}
}
Expand Up @@ -10,9 +10,9 @@ import annotation.effects._
* ******** */

object ioPrimitives {
def show(a: Any): Unit @pure @io = {
// inner cast required due to ANF transform
println(a.toString: @pure): @pure @io
// cast (@unchecked) required for `println`
def show(a: Any): Unit @unchecked @pure(a.toString()) @io = {
println(a.toString)
}
}

Expand Down Expand Up @@ -41,7 +41,7 @@ trait Optn[+A] {
def isEmpty: Boolean @pure

def getOrElse[B >: A](default: => B): B @pure(default) =
if (isEmpty) default else (get: @pure) // effect cast
if (isEmpty) default else (get: @unchecked @pure) // effect cast

def get: A @pure @throws[NoSuchElem]
}
Expand Down Expand Up @@ -182,7 +182,7 @@ trait Itor[+A] {
def next(): A @pure @mod(this) @throws[NoSuchElem]
def isEmpty: Boolean @pure = !hasNext
def foreach[U](f: A => U): Unit @mod(this) @pure(f) = {
while (hasNext) f(next(): @pure @mod(this)) // effect cast for call to `next`
while (hasNext) f(next(): @unchecked @pure @mod(this)) // effect cast for call to `next`
()
}
}
Expand Down Expand Up @@ -290,7 +290,7 @@ sealed abstract class Lst[+A] extends Sq[A] with GenTravTmpl[A, Lst] with SqLk[A

def length: Int @pure = {
if (isEmpty) 0
else 1 + (tail: @pure).length // effect cast for `@throws[NoSuchElem]` of `tail`
else 1 + (tail: @unchecked @pure).length // effect cast for `@throws[NoSuchElem]` of `tail`
}

override def companion: GenCpn[Lst] @pure = Lst
Expand All @@ -314,7 +314,7 @@ object Lst extends SqFct[Lst] {

def apply[A](elems: A*): Lst[A] @pure = {
// cast because it uses the scala.Seq
elems.foldRight(nl: Lst[A])((a, res) => cns(a, res)): @pure
elems.foldRight(nl: Lst[A])((a, res) => cns(a, res)): @unchecked @pure
}

def newBuilder[A]: Bldr[A, Lst[A]] @pure @loc() = new LstBldr[A]
Expand Down
Expand Up @@ -33,21 +33,21 @@ class C {


def f7: Unit @pure = {
(f(): @pure)
(f(): @unchecked @pure)
}

def f8a: Unit @pure @throws[E1] = {
{
throw new E1
f()
}: @noIo @throws[E1]
}: @unchecked @noIo @throws[E1]
}

def f8b: Unit @pure @throws[E1] = {
{
throw new E1
f()
}: Unit @noIo @throws[E1]
}: Unit @unchecked @noIo @throws[E1]
}

def f9a[B](default: () => B): B @pure(default.apply()) =
Expand Down
Expand Up @@ -40,13 +40,13 @@ class C {
}

def f7: Unit @pure = {
(f(): @noIo)
(f(): @unchecked @noIo)
}

def f8: Unit @pure = {
{
throw new E1
f()
}: @noIo
}: @unchecked @noIo
}
}
Expand Up @@ -51,11 +51,11 @@ newSource:43: error: effect type mismatch;
found : @throws[Throwable] @noIo
required: @throws[Nothing] @noIo
@throws[Throwable] does not conform to @throws[Nothing]
(f(): @noIo)
^
(f(): @unchecked @noIo)
^
newSource:50: error: effect type mismatch;
found : @throws[Throwable] @noIo
required: @throws[Nothing] @noIo
@throws[Throwable] does not conform to @throws[Nothing]
}: @noIo
^
}: @unchecked @noIo
^
@@ -1,9 +1,13 @@
import annotation.effects.{io, pure}

class C {
def show(a: Any): Unit @pure @io = {
class A {
def s: String = ""
}

def show(a: A): Unit @pure @io = {
// need an additional cast because the argument is ANF-lifted to a local variable,
// and it ends up outside the other ascription.
println(a.toString: @pure): @pure @io
println(a.s: @unchecked @pure): @unchecked @pure @io
}
}

0 comments on commit 998e74a

Please sign in to comment.