Skip to content

Commit 3cddeaa

Browse files
committed
SI-7916: ScriptEngine support
Refactor the ScriptEngine support to an adaptor atop the IMain API. Allow references to resolve to context attributes. (The attributes must be defined at compilation time, though they may resolve to updated values at evaluation time.) This means that attributes are not bound statically in REPL history. In particular, we forgo the trick of binding attributes named "name: Type" as typed values. Instead, an `x` bound in dynamic context is injected into the script as a dynamic selection `$ctx.x` where `ctx` performs the look-up in the script context. When a compiled script is re-evaluated, a new instance of the script class is created and defined symbols are rebound. The context stdout writer is handled with `Console.withOut`, with bytes decoded using the default charset. Compilation errors are thrown as ScriptException with the first reported error. This commit doesn't attempt dynamic selection from objects in context. Currently, script must cast.
1 parent 15189d1 commit 3cddeaa

File tree

11 files changed

+471
-189
lines changed

11 files changed

+471
-189
lines changed

build.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1163,7 +1163,7 @@ TODO:
11631163
</pre>
11641164
<!-- JSR-223 support introduced in 2.11 -->
11651165
<jar-opts>
1166-
<service type="javax.script.ScriptEngineFactory" provider="scala.tools.nsc.interpreter.IMain$Factory"/>
1166+
<service type="javax.script.ScriptEngineFactory" provider="scala.tools.nsc.interpreter.Scripted$Factory"/>
11671167
</jar-opts>
11681168
</staged-pack>
11691169
</target>

src/repl/scala/tools/nsc/interpreter/IMain.scala

Lines changed: 41 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import scala.tools.nsc.typechecker.{StructuredTypeStrings, TypeStrings}
2020
import scala.tools.nsc.util._
2121
import ScalaClassLoader.URLClassLoader
2222
import scala.tools.nsc.util.Exceptional.unwrap
23-
import javax.script.{AbstractScriptEngine, Bindings, Compilable, CompiledScript, ScriptContext, ScriptEngine, ScriptEngineFactory, ScriptException}
2423
import java.net.URL
2524
import scala.tools.util.PathResolver
2625

@@ -56,10 +55,11 @@ import scala.tools.util.PathResolver
5655
* @author Moez A. Abdel-Gawad
5756
* @author Lex Spoon
5857
*/
59-
class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Settings, protected val out: JPrintWriter) extends AbstractScriptEngine with Compilable with Imports with PresentationCompilation {
58+
class IMain(initialSettings: Settings, protected val out: JPrintWriter) extends Imports with PresentationCompilation {
6059
imain =>
6160

62-
setBindings(createBindings, ScriptContext.ENGINE_SCOPE)
61+
def this(initialSettings: Settings) = this(initialSettings, IMain.defaultOut)
62+
6363
object replOutput extends ReplOutput(settings.Yreploutdir) { }
6464

6565
@deprecated("Use replOutput.dir instead", "2.11.0")
@@ -104,13 +104,6 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set
104104
finally if (!saved) settings.nowarn.value = false
105105
}
106106

107-
/** construct an interpreter that reports to Console */
108-
def this(settings: Settings, out: JPrintWriter) = this(null, settings, out)
109-
def this(factory: ScriptEngineFactory, settings: Settings) = this(factory, settings, new NewLinePrintWriter(new ConsoleWriter, true))
110-
def this(settings: Settings) = this(settings, new NewLinePrintWriter(new ConsoleWriter, true))
111-
def this(factory: ScriptEngineFactory) = this(factory, new Settings())
112-
def this() = this(new Settings())
113-
114107
// the expanded prompt but without color escapes and without leading newline, for purposes of indenting
115108
lazy val formatting = Formatting.forPrompt(replProps.promptText)
116109
lazy val reporter: ReplReporter = new ReplReporter(this)
@@ -464,7 +457,7 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set
464457
pos
465458
}
466459

467-
private[interpreter] def requestFromLine(line: String, synthetic: Boolean): Either[IR.Result, Request] = {
460+
private[interpreter] def requestFromLine(line: String, synthetic: Boolean = false): Either[IR.Result, Request] = {
468461
val content = line
469462

470463
val trees: List[global.Tree] = parse(content) match {
@@ -559,77 +552,8 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set
559552
*/
560553
def interpret(line: String): IR.Result = interpret(line, synthetic = false)
561554
def interpretSynthetic(line: String): IR.Result = interpret(line, synthetic = true)
562-
def interpret(line: String, synthetic: Boolean): IR.Result = compile(line, synthetic) match {
563-
case Left(result) => result
564-
case Right(req) => new WrappedRequest(req).loadAndRunReq
565-
}
566-
567-
private def compile(line: String, synthetic: Boolean): Either[IR.Result, Request] = {
568-
if (global == null) Left(IR.Error)
569-
else requestFromLine(line, synthetic) match {
570-
case Left(result) => Left(result)
571-
case Right(req) =>
572-
// null indicates a disallowed statement type; otherwise compile and
573-
// fail if false (implying e.g. a type error)
574-
if (req == null || !req.compile) Left(IR.Error) else Right(req)
575-
}
576-
}
577-
578-
var code = ""
579-
var bound = false
580-
def compiled(script: String): CompiledScript = {
581-
if (!bound) {
582-
quietBind("engine" -> this.asInstanceOf[ScriptEngine])
583-
bound = true
584-
}
585-
val cat = code + script
586-
compile(cat, false) match {
587-
case Left(result) => result match {
588-
case IR.Incomplete => {
589-
code = cat + "\n"
590-
new CompiledScript {
591-
def eval(context: ScriptContext): Object = null
592-
def getEngine: ScriptEngine = IMain.this
593-
}
594-
}
595-
case _ => {
596-
code = ""
597-
throw new ScriptException("compile-time error")
598-
}
599-
}
600-
case Right(req) => {
601-
code = ""
602-
new WrappedRequest(req)
603-
}
604-
}
605-
}
606-
607-
private class WrappedRequest(val req: Request) extends CompiledScript {
608-
var recorded = false
609-
610-
/** In Java we would have to wrap any checked exception in the declared
611-
* ScriptException. Runtime exceptions and errors would be ok and would
612-
* not need to be caught. So let us do the same in Scala : catch and
613-
* wrap any checked exception, and let runtime exceptions and errors
614-
* escape. We could have wrapped runtime exceptions just like other
615-
* exceptions in ScriptException, this is a choice.
616-
*/
617-
@throws[ScriptException]
618-
def eval(context: ScriptContext): Object = {
619-
val result = req.lineRep.evalEither match {
620-
case Left(e: RuntimeException) => throw e
621-
case Left(e: Exception) => throw new ScriptException(e)
622-
case Left(e) => throw e
623-
case Right(result) => result.asInstanceOf[Object]
624-
}
625-
if (!recorded) {
626-
recordRequest(req)
627-
recorded = true
628-
}
629-
result
630-
}
631-
632-
def loadAndRunReq = classLoader.asContext {
555+
def interpret(line: String, synthetic: Boolean): IR.Result = {
556+
def loadAndRunReq(req: Request) = classLoader.asContext {
633557
val (result, succeeded) = req.loadAndRun
634558

635559
/** To our displeasure, ConsoleReporter offers only printMessage,
@@ -654,35 +578,55 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set
654578
}
655579
}
656580

657-
def getEngine: ScriptEngine = IMain.this
581+
compile(line, synthetic) match {
582+
case Left(result) => result
583+
case Right(req) => loadAndRunReq(req)
584+
}
585+
}
586+
587+
// create a Request and compile it
588+
private[interpreter] def compile(line: String, synthetic: Boolean): Either[IR.Result, Request] = {
589+
if (global == null) Left(IR.Error)
590+
else requestFromLine(line, synthetic) match {
591+
case Right(null) => Left(IR.Error) // disallowed statement type
592+
case Right(req) if !req.compile => Left(IR.Error) // compile error
593+
case ok @ Right(req) => ok
594+
case err @ Left(result) => err
595+
}
658596
}
659597

660598
/** Bind a specified name to a specified value. The name may
661599
* later be used by expressions passed to interpret.
662600
*
601+
* A fresh `ReadEvalPrint`, which defines a `line` package, is used to compile
602+
* a custom `eval` object that wraps the bound value.
603+
*
604+
* If the bound value is successfully installed, then bind the name
605+
* by interpreting `val name = $line42.$eval.value`.
606+
*
663607
* @param name the variable name to bind
664608
* @param boundType the type of the variable, as a string
665609
* @param value the object value to bind to it
666610
* @return an indication of whether the binding succeeded
667611
*/
668612
def bind(name: String, boundType: String, value: Any, modifiers: List[String] = Nil): IR.Result = {
669613
val bindRep = new ReadEvalPrint()
670-
bindRep.compile("""
671-
|object %s {
672-
| var value: %s = _
673-
| def set(x: Any) = value = x.asInstanceOf[%s]
614+
bindRep.compile(s"""
615+
|object ${bindRep.evalName} {
616+
| var value: $boundType = _
617+
| def set(x: Any) = value = x.asInstanceOf[$boundType]
674618
|}
675-
""".stripMargin.format(bindRep.evalName, boundType, boundType)
676-
)
619+
""".stripMargin
620+
)
677621
bindRep.callEither("set", value) match {
678622
case Left(ex) =>
679623
repldbg("Set failed in bind(%s, %s, %s)".format(name, boundType, value))
680624
repldbg(util.stackTraceString(ex))
681625
IR.Error
682-
683626
case Right(_) =>
684-
val line = "%sval %s = %s.value".format(modifiers map (_ + " ") mkString, name, bindRep.evalPath)
685-
repldbg("Interpreting: " + line)
627+
val mods = if (modifiers.isEmpty) "" else modifiers.mkString("", " ", " ")
628+
val line = s"${mods}val $name = ${ bindRep.evalPath }.value"
629+
repldbg(s"Interpreting: $line")
686630
interpret(line)
687631
}
688632
}
@@ -1046,31 +990,6 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set
1046990
override def toString = "Request(line=%s, %s trees)".format(line, trees.size)
1047991
}
1048992

1049-
def createBindings: Bindings = new IBindings {
1050-
override def put(name: String, value: Object): Object = {
1051-
val n = name.indexOf(":")
1052-
val p: NamedParam = if (n < 0) (name, value) else {
1053-
val nme = name.substring(0, n).trim
1054-
val tpe = name.substring(n + 1).trim
1055-
NamedParamClass(nme, tpe, value)
1056-
}
1057-
if (!p.name.startsWith("javax.script")) bind(p)
1058-
null
1059-
}
1060-
}
1061-
1062-
@throws[ScriptException]
1063-
def compile(script: String): CompiledScript = eval("new javax.script.CompiledScript { def eval(context: javax.script.ScriptContext): Object = { " + script + " }.asInstanceOf[Object]; def getEngine: javax.script.ScriptEngine = engine }").asInstanceOf[CompiledScript]
1064-
1065-
@throws[ScriptException]
1066-
def compile(reader: java.io.Reader): CompiledScript = compile(stringFromReader(reader))
1067-
1068-
@throws[ScriptException]
1069-
def eval(script: String, context: ScriptContext): Object = compiled(script).eval(context)
1070-
1071-
@throws[ScriptException]
1072-
def eval(reader: java.io.Reader, context: ScriptContext): Object = eval(stringFromReader(reader), context)
1073-
1074993
override def finalize = close
1075994

1076995
/** Returns the name of the most recent interpreter result.
@@ -1267,54 +1186,9 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set
12671186

12681187
/** Utility methods for the Interpreter. */
12691188
object IMain {
1270-
import java.util.Arrays.{ asList => asJavaList }
12711189
/** Dummy identifier fragement inserted at the cursor before presentation compilation. Needed to support completion of `global.def<TAB>` */
12721190
val DummyCursorFragment = "_CURSOR_"
12731191

1274-
class Factory extends ScriptEngineFactory {
1275-
@BeanProperty
1276-
val engineName = "Scala Interpreter"
1277-
1278-
@BeanProperty
1279-
val engineVersion = "1.0"
1280-
1281-
@BeanProperty
1282-
val extensions: JList[String] = asJavaList("scala")
1283-
1284-
@BeanProperty
1285-
val languageName = "Scala"
1286-
1287-
@BeanProperty
1288-
val languageVersion = scala.util.Properties.versionString
1289-
1290-
def getMethodCallSyntax(obj: String, m: String, args: String*): String = null
1291-
1292-
@BeanProperty
1293-
val mimeTypes: JList[String] = asJavaList("application/x-scala")
1294-
1295-
@BeanProperty
1296-
val names: JList[String] = asJavaList("scala")
1297-
1298-
def getOutputStatement(toDisplay: String): String = null
1299-
1300-
def getParameter(key: String): Object = key match {
1301-
case ScriptEngine.ENGINE => engineName
1302-
case ScriptEngine.ENGINE_VERSION => engineVersion
1303-
case ScriptEngine.LANGUAGE => languageName
1304-
case ScriptEngine.LANGUAGE_VERSION => languageVersion
1305-
case ScriptEngine.NAME => names.get(0)
1306-
case _ => null
1307-
}
1308-
1309-
def getProgram(statements: String*): String = null
1310-
1311-
def getScriptEngine: ScriptEngine = {
1312-
val settings = new Settings()
1313-
settings.usemanifestcp.value = true
1314-
new IMain(this, settings)
1315-
}
1316-
}
1317-
13181192
// The two name forms this is catching are the two sides of this assignment:
13191193
//
13201194
// $line3.$read.$iw.$iw.Bippy =
@@ -1366,5 +1240,10 @@ object IMain {
13661240

13671241
def stripImpl(str: String): String = naming.unmangle(str)
13681242
}
1243+
private[interpreter] def defaultSettings = new Settings()
1244+
private[scala] def defaultOut = new NewLinePrintWriter(new ConsoleWriter, true)
1245+
1246+
/** construct an interpreter that reports to Console */
1247+
def apply(initialSettings: Settings = defaultSettings, out: JPrintWriter = defaultOut) = new IMain(initialSettings, out)
13691248
}
13701249

0 commit comments

Comments
 (0)