Skip to content

Commit 230e762

Browse files
committed
init
1 parent ff747dc commit 230e762

File tree

13 files changed

+991
-0
lines changed

13 files changed

+991
-0
lines changed

LICENSE.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Copyright (c) 2014-2015 Purdue University
2+
3+
All rights reserved.
4+
5+
Redistribution and use in source and binary forms, with or without modification,
6+
are permitted provided that the following conditions are met:
7+
8+
* Redistributions of source code must retain the above copyright notice,
9+
this list of conditions and the following disclaimer.
10+
* Redistributions in binary form must reproduce the above copyright notice,
11+
this list of conditions and the following disclaimer in the documentation
12+
and/or other materials provided with the distribution.
13+
* Neither the name of the EPFL nor the names of its contributors
14+
may be used to endorse or promote products derived from this software
15+
without specific prior written permission.
16+
17+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
21+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
The Great Escape
2+
================
3+
4+
First-class service is nice. But if everybody rides first class, it stops being fun.
5+
6+
This Scala compiler plug-in exposes a programming model to enforce a _no-escape_ policy for certain objects.
7+
8+
There are many potential uses:
9+
10+
- Effects:
11+
Objects can serve as capabilities. But we must limit the scope of capabilities to retain control. Compare to Monads, which do not compose well.
12+
- Region based memory:
13+
Deallocate and/or reuse memory in a timely manner (note that we do not aim to do this for _all_ allocations, just for big chunks).
14+
- Resource control:
15+
Ensure that resources such as file handles are properly closed.
16+
- Staging:
17+
Scope extrusion. We must limit the scope of binders to guarantee well-typed generated code.
18+
- Distributed systems:
19+
We do not want to serialize large data by accident (see Spores).
20+
21+
22+
A High-Level Example
23+
--------------------
24+
25+
If an expression has type `A @safe(x)` it will not leak symbol `x`.
26+
27+
If a function has type `A => B @safe(%)`, it will not leak its argument.
28+
Think of this as a dependent function type `(x:A) => B @safe(x)`.
29+
30+
// For exception handling, we would like to enforce that
31+
// exceptions can only be raised within a try/catch block.
32+
33+
def trycatch(f: (Exception => Nothing) => Unit @safe(%)): Unit
34+
35+
// The closure passed to trycatch may not leak its argument.
36+
37+
trycatch { raise =>
38+
raise(new Exception) // ok: raise cannot escape
39+
}
40+
41+
// Inside a try block we can use `raise` in safe positions,
42+
// but not in unsafe ones, where it could be leaked.
43+
44+
def safeMethod(f: () => Any): Int @safe(f)
45+
def unsafeMethod(f: () => Any): Int
46+
47+
trycatch { raise =>
48+
safeMethod { () =>
49+
raise(new Exception) // ok: closure is safe
50+
}
51+
unsafeMethod { () =>
52+
raise(new Exception) // not ok
53+
}
54+
}
55+
56+
See the [test suite](library/src/test/scala/scala/tools/escape) for complete code.
57+
58+
59+
Practical Restrictions
60+
----------------------
61+
62+
Consider the following example:
63+
64+
def map[A,B](xs: List[A])(f: A => B): List[B] @safe(f) = {
65+
val b = new Builder[B]
66+
xs foreach (x => b += f(x))
67+
b.result
68+
}
69+
70+
We are not leaking `f`, but we _are_ leaking objects returned
71+
from calling `f`.
72+
73+
It is no problem to use `map` like this:
74+
75+
map(xs) { x =>
76+
if (x > 0) x else raise(new Exception)
77+
}
78+
79+
But we need to be careful about cases where
80+
we could transitively leak `raise`, e.g. as part of
81+
another closure:
82+
83+
map(xs) { x =>
84+
() => raise(new Exception)
85+
}
86+
87+
The design decision here is to disallow this case.
88+
89+
Extensions to _n-level_ safety are conceivable.
90+
91+
92+
Rationale and Background
93+
------------------------
94+
95+
The concept of higher-order and first-class language constructs goes hand in hand. In a higher-order language, many things are first-class: functions, mutable references, etc.
96+
97+
Being first-class means that there are no restrictions on how an object may be used. Functions can be passed to other functions as arguments and returned from other functions. First class reference cells may hold functions or other reference cells.
98+
99+
Lexical scope is central to most modern programming languages. First-class functions _close over_ their defining scope. Hence they are also called closures.
100+
101+
While programming with first-class objects is incredibly useful, it is sometimes _too_ powerful, and one loses some useful guarantees.
102+
103+
For example, in a language without closures, function calls follow a strict stack discipline. A local variable's lifetime ends when the function returns and its space can be reclaimed. Contrast this with higher-order languages, which allocate closure records on the heap. The lifetime of a variable may be indefinite if it is captured by a closure.
104+
105+
Binding the lifetime of (certain -- not all) objects is important. So maybe not all objects should be first class?
106+
107+
After all, we want first class to be an exclusive club, with tightly regulates access.
108+
109+
110+
Current Status
111+
--------------
112+
113+
Early development and proof of concept. Do not expect too much, but feel free to contribute!

build.sbt

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import com.typesafe.tools.mima.plugin.{MimaPlugin, MimaKeys}
2+
import Keys.{`package` => packageTask }
3+
import com.typesafe.sbt.osgi.{OsgiKeys, SbtOsgi}
4+
5+
// plugin logic of build based on https://github.com/retronym/boxer
6+
7+
lazy val commonSettings = scalaModuleSettings ++ Seq(
8+
repoName := "scala-escape",
9+
organization := "org.scala-lang.plugins",
10+
version := "1.0.1-SNAPSHOT",
11+
scalaVersion := "2.11.4",
12+
snapshotScalaBinaryVersion := "2.11.4",
13+
scalacOptions ++= Seq(
14+
"-deprecation",
15+
"-feature")
16+
)
17+
18+
lazy val root = project.in( file(".") ).settings( publishArtifact := false ).aggregate(plugin, library).settings(commonSettings : _*)
19+
20+
lazy val plugin = project settings (scalaModuleOsgiSettings: _*) settings (
21+
name := "scala-escape-plugin",
22+
crossVersion := CrossVersion.full, // because compiler api is not binary compatible
23+
libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value,
24+
OsgiKeys.exportPackage := Seq(s"scala.tools.escape;version=${version.value}")
25+
) settings (commonSettings : _*)
26+
27+
val pluginJar = packageTask in (plugin, Compile)
28+
29+
// TODO: the library project's test are really plugin tests, but we first need that jar
30+
lazy val library = project settings (scalaModuleOsgiSettings: _*) settings (MimaPlugin.mimaDefaultSettings: _*) settings (
31+
name := "scala-escape-library",
32+
MimaKeys.previousArtifact := Some(organization.value % s"${name.value}_2.11.0-RC1" % "1.0.0"),
33+
scalacOptions ++= Seq(
34+
// add the plugin to the compiler
35+
s"-Xplugin:${pluginJar.value.getAbsolutePath}",
36+
// enable the plugin
37+
//"-P:escape:enable",
38+
// add plugin timestamp to compiler options to trigger recompile of
39+
// the library after editing the plugin. (Otherwise a 'clean' is needed.)
40+
s"-Jdummy=${pluginJar.value.lastModified}"),
41+
libraryDependencies ++= Seq(
42+
"org.scala-lang" % "scala-compiler" % scalaVersion.value % "test",
43+
"junit" % "junit" % "4.11" % "test",
44+
"com.novocode" % "junit-interface" % "0.10" % "test"),
45+
testOptions += Tests.Argument(
46+
TestFrameworks.JUnit,
47+
s"-Dscala-escape-plugin.jar=${pluginJar.value.getAbsolutePath}"
48+
),
49+
// run mima during tests
50+
/*test in Test := {
51+
MimaKeys.reportBinaryIssues.value
52+
(test in Test).value
53+
},*/
54+
OsgiKeys.exportPackage := Seq(s"scala.util.escape;version=${version.value}")
55+
) settings (commonSettings : _*)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package scala.util.escape
2+
3+
import scala.annotation.{ Annotation, StaticAnnotation, TypeConstraint }
4+
5+
class safe(xs: Any*) extends StaticAnnotation with TypeConstraint
6+
7+
case object %
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package scala.tools.escape
2+
3+
import org.junit.Test
4+
5+
class CompilerTesting {
6+
private def pluginJar: String = {
7+
val f = sys.props("scala-escape-plugin.jar")
8+
assert(new java.io.File(f).exists, f)
9+
f
10+
}
11+
def loadPlugin = s"-Xplugin:${pluginJar}"
12+
13+
// note: `code` should have a | margin
14+
def escErrorMessages(msg: String, code: String) =
15+
errorMessages(msg, loadPlugin)(s"import scala.util.escape._\nobject Test {\ndef trycatch1: Unit = {\n${code.stripMargin}\n}\n}")
16+
17+
def expectEscErrorOutput(msg: String, code: String) = {
18+
val errors = escErrorMessages(msg, code)
19+
assert((errors mkString "\n") == msg, errors mkString "\n")
20+
}
21+
22+
def expectEscError(msg: String, code: String) = {
23+
val errors = escErrorMessages(msg, code)
24+
assert(errors exists (_ contains msg), errors mkString "\n")
25+
}
26+
27+
def expectEscErrors(msgCount: Int, msg: String, code: String) = {
28+
val errors = escErrorMessages(msg, code)
29+
val errorCount = errors.filter(_ contains msg).length
30+
assert(errorCount == msgCount, s"$errorCount occurrences of \'$msg\' found -- expected $msgCount in:\n${errors mkString "\n"}")
31+
}
32+
33+
// TODO: move to scala.tools.reflect.ToolboxFactory
34+
def errorMessages(errorSnippet: String, compileOptions: String)(code: String): List[String] = {
35+
import scala.tools.reflect._
36+
val m = scala.reflect.runtime.currentMirror
37+
val tb = m.mkToolBox(options = compileOptions) //: ToolBox[m.universe.type]
38+
val fe = tb.frontEnd
39+
40+
try {
41+
tb.eval(tb.parse(code))
42+
Nil
43+
} catch {
44+
case _: ToolBoxError =>
45+
import fe._
46+
infos.toList collect { case Info(_, msg, ERROR) => msg }
47+
}
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package scala.tools.escape
2+
3+
import org.junit.Test
4+
import org.junit.Assert.assertEquals
5+
6+
import scala.annotation._
7+
import scala.collection.Seq
8+
import scala.collection.generic.CanBuildFrom
9+
import scala.language.{ implicitConversions, higherKinds }
10+
import scala.util.escape._
11+
import scala.util.control.Exception
12+
13+
14+
class Basic extends CompilerTesting {
15+
@Test def test10 = {
16+
val x = 100
17+
val y = 99
18+
19+
def f(a:Int): Int @safe(x) = a
20+
21+
def g1(a:Int): Int @safe(x) = f(a)
22+
23+
def g(a:Int): Int @safe(x) = a
24+
25+
def h(a: Int): Int @safe(x) = g(f(a))
26+
}
27+
28+
@Test def test20 = expectEscErrorOutput(
29+
"value x not safe (not declared as such by return type of f = (a: Int)Int!\n"+
30+
"couldn't prove that object returned from f(a) does not point to Set(value x)",
31+
"""
32+
val x = 100
33+
val y = 99
34+
35+
def f(a:Int): Int = a
36+
def g(a:Int): Int @safe(x) = f(a) // not safe
37+
""")
38+
39+
}
40+
41+
class TryCatch extends CompilerTesting {
42+
43+
@Test def trycatch1: Unit = { () =>
44+
45+
def trycatch(f: (Exception => Nothing) => Unit @safe(%)): Unit = ???
46+
47+
trycatch { raise =>
48+
raise(new Exception)
49+
}
50+
51+
}
52+
53+
@Test def trycatch2: Unit = { () =>
54+
55+
def trycatch(f: (Exception => Nothing) => Unit @safe(%)): Unit = ???
56+
57+
def safeMethod(f: () => Unit): Unit @ safe(f) = ???
58+
59+
trycatch { raise =>
60+
safeMethod { () =>
61+
raise(new Exception) // ok
62+
()
63+
}
64+
()
65+
}
66+
67+
}
68+
69+
@Test def trycatch3: Unit = { () =>
70+
71+
def trycatch(f: (Exception => Nothing) => Int @safe(%)): Int = ???
72+
73+
def safeMethod(f: () => Int): Int @ safe(f) = ???
74+
75+
trycatch { raise =>
76+
77+
def inner(a:Int): Int @safe(raise) = a
78+
79+
safeMethod { () =>
80+
raise(new Exception) // ok
81+
inner(7)
82+
}
83+
84+
9
85+
}
86+
87+
}
88+
89+
90+
91+
@Test def trycatch4 = expectEscErrorOutput(
92+
"value raise not safe (free in lambda)!","""
93+
94+
def trycatch(f: (Exception => Nothing) => Int @safe(%)): Int = ???
95+
96+
def safeMethod(f: () => Int): Int @ safe(f) = ???
97+
def unsafeMethod(f: () => Int): Int = ???
98+
99+
trycatch { raise =>
100+
101+
safeMethod { () =>
102+
raise(new Exception) // ok
103+
7
104+
}
105+
106+
unsafeMethod { () =>
107+
raise(new Exception) // not ok
108+
7
109+
}
110+
7
111+
}
112+
""")
113+
114+
@Test def trycatch5 = expectEscErrorOutput(
115+
"couldn't prove that object returned from inner(7) does not point to Set(value raise)","""
116+
117+
def trycatch(f: (Exception => Nothing) => Int @safe(%)): Int = ???
118+
119+
def safeMethod(f: () => Int): Int @ safe(f) = ???
120+
121+
trycatch { raise =>
122+
123+
def inner(a:Int): Int = a
124+
125+
safeMethod { () =>
126+
raise(new Exception) // ok
127+
inner(7) // not ok
128+
}
129+
130+
7
131+
}
132+
""")
133+
134+
135+
}

0 commit comments

Comments
 (0)