Search code examples
scalacompiler-constructionclassloaderinterpreter

Make a Scala interpreter oblivious between interpret calls


Is it possible to configure a Scala interpreter (tools.nsc.IMain) so that it "forgets" the previously executed code, whenever I run the next interpret() call?

Normally when it compiles the sources, it wraps them in nested objects, so all the previously defined variables, functions and bindings are available.

It would suffice to not generate the nested objects (or to throw them away), although I would prefer a solution which would even remove the previously compiled classes from the class loader again.

Is there a setting, or a method, or something I can overwrite, or an alternative to IMain that would accomplish this? I need to be able to still access the resulting objects / classes from the host VM.


Basically I want to isolate subsequent interpret() calls without something as heavy weight as creating a new IMain for each iteration.


Solution

  • Here is one possible answer. Basically there is method reset() which calls the following things (mostly private, so either you buy the whole package or not):

    clearExecutionWrapper()
    resetClassLoader()
    resetAllCreators()
    prevRequests.clear()
    referencedNameMap.clear()
    definedNameMap.clear()
    virtualDirectory.clear()
    

    In my case, I am using a custom execution wrapper, so that needs to be set up again, and also imports are handled through a regular interpret cycle, so either add them again, or—better—just prepend them with the execution wrapper.

    I would like to keep my bindings, they are also gone:

    import tools.nsc._
    import interpreter.IMain
    
    object Test {
      private final class Intp(cset: nsc.Settings)
        extends IMain(cset, new NewLinePrintWriter(new ConsoleWriter, autoFlush = true)) {
    
        override protected def parentClassLoader = Test.getClass.getClassLoader
      }
    
      object Foo {
        def bar() { println("BAR" )}
      }
    
      def run() {
        val cset = new nsc.Settings()
        cset.classpath.value += java.io.File.pathSeparator + sys.props("java.class.path")
        val i = new Intp(cset)
        i.initializeSynchronous()
        i.bind[Foo.type]("foo", Foo)
        val res0 = i.interpret("foo.bar(); val x = 33")
        println(s"res0: $res0")
        i.reset()
        val res1 = i.interpret("println(x)")
        println(s"res1: $res1")
        i.reset()
        val res2 = i.interpret("foo.bar()")
        println(s"res2: $res2")
      }
    }
    

    This will find Foo in the first iteration, correctly forget x in the second iteration, but then in the third iteration, it can be seen that the foo binding is also lost:

    foo: Test.Foo.type = Test$Foo$@8bf223
    BAR
    x: Int = 33
    res0: Success
    <console>:8: error: not found: value x
                  println(x)
                          ^
    res1: Error
    <console>:8: error: not found: value foo
                  foo.bar()
                  ^
    res2: Error
    

    The following seems to be fine:

    for(j <- 0 until 3) {
      val user  = "foo.bar()"
      val synth =  """import Test.{Foo => foo}
                   """.stripMargin + user
      val res = i.interpret(synth)
      println(s"res$j: $res")
      i.reset()
    }