diff --git a/pom.xml b/pom.xml index 5bfe916..70652f8 100644 --- a/pom.xml +++ b/pom.xml @@ -1,16 +1,16 @@ - + 4.0.0 org.scijava pom-scijava - 27.0.1 + 34.0.0 scripting-scala - 0.2.3-SNAPSHOT + 0.3.0-SNAPSHOT SciJava Scripting: Scala JSR-223-compliant Scala scripting language plugin. @@ -53,6 +53,10 @@ Keith Schulze keithschulze + + Jarek Sacha + jpsacha + @@ -82,6 +86,7 @@ org.scijava.plugins.scripting.scala + org.scijava.plugins.scripting.scala.Main bsd_2 Board of Regents of the University of @@ -91,22 +96,62 @@ Wisconsin-Madison. sign,deploy-to-scijava - 2.12.1 + 3.2.2 - - - - org.scijava - scijava-common - - - - org.scala-lang - scala-compiler - ${scala.version} - + + + + net.alchim31.maven + scala-maven-plugin + 4.8.0 + + + -unchecked + -deprecation + -explain + -explain-types + -release + 8 + + + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + + + + + + + + org.scijava + scijava-common + + + + + org.scala-lang + scala3-compiler_3 + ${scala.version} + + + diff --git a/src/main/java/org/scijava/plugins/scripting/scala/ScalaScriptEngine.java b/src/main/java/org/scijava/plugins/scripting/scala/Main.java similarity index 51% rename from src/main/java/org/scijava/plugins/scripting/scala/ScalaScriptEngine.java rename to src/main/java/org/scijava/plugins/scripting/scala/Main.java index ec97c5d..7647249 100644 --- a/src/main/java/org/scijava/plugins/scripting/scala/ScalaScriptEngine.java +++ b/src/main/java/org/scijava/plugins/scripting/scala/Main.java @@ -1,19 +1,18 @@ -/* +/*- * #%L * JSR-223-compliant Scala scripting language plugin. * %% - * Copyright (C) 2013 - 2016 Board of Regents of the University of - * Wisconsin-Madison. + * Copyright (C) 2014 - 2023 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -27,45 +26,13 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ - package org.scijava.plugins.scripting.scala; -import javax.script.ScriptEngine; -import javax.script.ScriptException; - -import org.scijava.script.AdaptedScriptEngine; - -/** - * Scala interpreter - * - * @author Keith Schulze - * @see ScriptEngine - */ -public class ScalaScriptEngine extends AdaptedScriptEngine { - - public ScalaScriptEngine(ScriptEngine engine) { - super(engine); - } - - @Override - public Object get(String key) { - // First try to get value from bindings - Object value = super.get(key); +import org.scijava.script.ScriptREPL; - // NB: Extracting values from Scala Script Engine are a little tricky. - // Values (variables) initialised or computed in the script are - // not added to the bindings of the CompiledScript AFAICT. Therefore - // the only way to extract them is to evaluate the variable and - // capture the return. If it evaluates to null or throws a - // a ScriptException, we simply return null. - if (value == null) try { - value = super.eval(key); - } catch (ScriptException ignored) { - // HACK: Explicitly ignore ScriptException, which arises if - // key is not found. This feels bad because it fails silently - // for the user, but it mimics behaviour in other script langs. - } +public class Main { - return value; - } + public static void main(String... args) throws Exception { + ScriptREPL.main(args); + } } diff --git a/src/main/java/org/scijava/plugins/scripting/scala/ScalaScriptLanguage.java b/src/main/java/org/scijava/plugins/scripting/scala/ScalaScriptLanguage.java index b71abef..2566bc6 100644 --- a/src/main/java/org/scijava/plugins/scripting/scala/ScalaScriptLanguage.java +++ b/src/main/java/org/scijava/plugins/scripting/scala/ScalaScriptLanguage.java @@ -30,10 +30,6 @@ package org.scijava.plugins.scripting.scala; -import java.net.URLClassLoader; -import java.util.Arrays; -import java.util.stream.Collectors; - import javax.script.ScriptEngine; import org.scijava.log.LogService; @@ -42,17 +38,13 @@ import org.scijava.script.AdaptedScriptLanguage; import org.scijava.script.ScriptLanguage; -import scala.tools.nsc.ConsoleWriter; -import scala.tools.nsc.NewLinePrintWriter; -import scala.tools.nsc.Settings; -import scala.tools.nsc.interpreter.Scripted; - /** * An adapter of the Scala interpreter to the SciJava scripting interface. * * @author Curtis Rueden * @author Keith Schulze * @author Johannes Schindelin + * @author Jarek Sacha * @see ScriptEngine */ @Plugin(type = ScriptLanguage.class, name = "Scala") @@ -67,25 +59,7 @@ public ScalaScriptLanguage() { @Override public ScriptEngine getScriptEngine() { - final Settings settings = new Settings(); - settings.classpath().value_$eq(getClasspath()); - - Scripted eng = Scripted.apply(new Scripted.Factory(), settings, - new NewLinePrintWriter(new ConsoleWriter(), true)); - - return new ScalaScriptEngine(eng); - } - - /** Retrieves the current classpath as a string. */ - private String getClasspath() { - final ClassLoader cl = ClassLoader.getSystemClassLoader(); - if (!(cl instanceof URLClassLoader)) { - log.warn("Cannot retrieve classpath from class loader of type '" + - cl.getClass().getName() + "'"); - return System.getProperty("java.class.path"); - } - return Arrays.stream(((URLClassLoader) cl).getURLs()).map(// - url -> url.getPath() // - ).collect(Collectors.joining(System.getProperty("path.separator"))); + final ScriptEngine eng = new dotty.tools.repl.ScriptEngine(); + return new ScalaAdaptedScriptEngine(eng); } } diff --git a/src/main/scala/org/scijava/plugins/scripting/scala/ScalaAdaptedScriptEngine.scala b/src/main/scala/org/scijava/plugins/scripting/scala/ScalaAdaptedScriptEngine.scala new file mode 100644 index 0000000..8f39b4e --- /dev/null +++ b/src/main/scala/org/scijava/plugins/scripting/scala/ScalaAdaptedScriptEngine.scala @@ -0,0 +1,135 @@ +package org.scijava.plugins.scripting.scala + +import java.io.{OutputStream, Reader, StringWriter, Writer} +import javax.script.* +import scala.collection.mutable +import scala.jdk.CollectionConverters.* +import scala.util.Try + +/** + * Adapted Scala ScriptEngine + * + * @author Jarek Sacha + * @author Keith Schulze + * @see ScriptEngine + */ +class ScalaAdaptedScriptEngine(engine: ScriptEngine) extends AbstractScriptEngine: + + import ScalaAdaptedScriptEngine.* + + private val buffer = new Array[Char](8192) + + @throws[ScriptException] + override def eval(reader: Reader, context: ScriptContext): AnyRef = eval(stringFromReader(reader), context) + + @throws[ScriptException] + override def eval(script: String, context: ScriptContext): AnyRef = + emulateBinding(context) + val r = evalInner(script, context) + // Scala returns `Unit` when no value is returned. Script Engine (or the + // Java side) expects `null` when no value was returned. + // Anything else return as is. + r match + case _: Unit => null + case x => x + + private def emulateBinding(context: ScriptContext): Unit = + + // Scala 3.2.2 ignores bindings, emulate binding using setup script + // Create a line with variable declaration for each binding item + val lines = + for + scope <- context.getScopes.asScala + bindings <- Option(context.getBindings(scope)).map(_.asScala) // bindings in context can be null + yield { + for (name, value) <- bindings yield { + value match + case v: Double => s"val $name : Double = ${v}d" + case v: Float => s"val $name : Float = ${v}f" + case v: Long => s"val $name : Long = ${v}L" + case v: Int => s"val $name : Int = $v" + case v: Char => s"val $name : Char = '$v'" + case v: Short => s"val $name : Short = $v" + case v: Byte => s"val $name : Byte = $v" + case v: Boolean => s"val $name : Int = $v" + case o: AnyRef if isValidVariableName(name) => + _transfer = o + val typeName = Option(o).map(_.getClass.getCanonicalName).getOrElse("AnyRef") + s""" + |val $name : $typeName = { + | val t = org.scijava.plugins.scripting.scala.ScalaAdaptedScriptEngine._transfer + | t.asInstanceOf[$typeName] + |}""".stripMargin + case _: AnyRef => "" // ignore if name is not a variable + case v: Unit => + throw ScriptException(s"Unsupported type for bind variable $name: ${v.getClass}") + } + } + + val script = lines + .flatten + .filter(_.nonEmpty) + .mkString("\n") + + if script.nonEmpty then + evalInner(script, context) + + end emulateBinding + + private def evalInner(script: String, context: ScriptContext) = + class WriterOutputStream(w: Writer) extends OutputStream: + override def write(b: Int): Unit = w.write(b) + + // Redirect output to writes provided by context + Console.withOut(WriterOutputStream(context.getWriter)) { + Console.withErr(WriterOutputStream(context.getErrorWriter)) { + engine.eval(script, context) + } + } + + private def stringFromReader(in: Reader) = + val out = new StringWriter() + var n = in.read(buffer) + while n > -1 do + out.write(buffer, 0, n) + n = in.read(buffer) + in.close() + out.toString + + override def createBindings(): Bindings = engine.createBindings + + override def getFactory: ScriptEngineFactory = engine.getFactory + + override def get(key: String): AnyRef = + // First try to get value from bindings + var value = super.get(key) + + // NB: Extracting values from Scala Script Engine are a little tricky. + // Values (variables) initialised or computed in the script are + // not added to the bindings of the CompiledScript AFAICT. Therefore + // the only way to extract them is to evaluate the variable and + // capture the return. If it evaluates to null or throws a + // a ScriptException, we simply return null. + if value == null then + try + value = evalInner(key, getContext) + catch + case _: ScriptException => + // HACK: Explicitly ignore ScriptException, which arises if + // key is not found. This feels bad because it fails silently + // for the user, but it mimics behaviour in other script langs. + + value + end get + +end ScalaAdaptedScriptEngine + +object ScalaAdaptedScriptEngine: + private lazy val variableNamePattern = """^[a-zA-Z_$][a-zA-Z_$0-9]*$""".r + + /** Do not use externally despite it is declared public. IT is public so it is accessible from scripts */ + // noinspection ScalaWeakerAccess + var _transfer: Object = _ + + private def isValidVariableName(name: String): Boolean = variableNamePattern.matches(name) +end ScalaAdaptedScriptEngine diff --git a/src/test/java/org/scijava/plugins/scripting/scala/ScalaTest.java b/src/test/java/org/scijava/plugins/scripting/scala/ScalaTest.java index 49e0cd5..eefe925 100644 --- a/src/test/java/org/scijava/plugins/scripting/scala/ScalaTest.java +++ b/src/test/java/org/scijava/plugins/scripting/scala/ScalaTest.java @@ -7,13 +7,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -30,41 +30,241 @@ package org.scijava.plugins.scripting.scala; -import static org.junit.Assert.assertEquals; - -import java.io.StringWriter; - -import javax.script.ScriptEngine; -import javax.script.SimpleScriptContext; - import org.junit.Test; import org.scijava.Context; import org.scijava.script.ScriptLanguage; +import org.scijava.script.ScriptModule; import org.scijava.script.ScriptService; +import javax.script.ScriptEngine; +import javax.script.SimpleScriptContext; +import java.io.StringWriter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + /** * Scala unit tests. * * @author Johannes Schindelin * @author Keith Schulze + * @author Jarek Sacha */ public class ScalaTest { - @Test - public void testBasic() throws Exception { - final Context context = new Context(ScriptService.class); - final ScriptService scriptService = context.getService(ScriptService.class); + private ScriptEngine getEngine(Context context) { + final ScriptService scriptService = context.getService(ScriptService.class); + final ScriptLanguage language = scriptService.getLanguageByExtension("scala"); + return language.getScriptEngine(); + } + + @Test + public void testBasic() throws Exception { + try (final Context context = new Context(ScriptService.class)) { + + final ScriptEngine engine = getEngine(context); + + final SimpleScriptContext ssc = new SimpleScriptContext(); + final StringWriter writer = new StringWriter(); + ssc.setWriter(writer); + + final String script = "print(\"3\")"; + engine.eval(script, ssc); + assertEquals("3", writer.toString()); + } + } + + @Test + public void testEmptyReturnValue() throws Exception { + try (final Context context = new Context(ScriptService.class)) { + final ScriptService scriptService = context.getService(ScriptService.class); + final ScriptModule m = scriptService.run("hello.scala", "print(\"3\")", true).get(); + final Void expected = null; + final Object actual = m.getReturnValue(); + assertEquals(expected, actual); + } + } + + @Test + public void testPutDouble() throws Exception { + try (final Context context = new Context(ScriptService.class)) { + final ScriptEngine engine = getEngine(context); + + final double expected = 7.1d; + engine.put("v", expected); + final String script = "val v1:Double = v"; + engine.eval(script); + final Object actual = engine.get("v1"); + assertEquals(expected, actual); + } + } + + @Test + public void testPutFloat() throws Exception { + try (final Context context = new Context(ScriptService.class)) { + final ScriptEngine engine = getEngine(context); + + final float expected = 7.1f; + engine.put("v", expected); + final String script = "val v1:Float = v"; + engine.eval(script); + final Object actual = engine.get("v1"); + assertEquals(expected, actual); + } + } + + @Test + public void testPutLong() throws Exception { + try (final Context context = new Context(ScriptService.class)) { + final ScriptEngine engine = getEngine(context); + + final long expected = 7L; + engine.put("v", expected); + final String script = "val v1:Long = v"; + engine.eval(script); + final Object actual = engine.get("v1"); + assertEquals(expected, actual); + } + } + + @Test + public void testPutChar() throws Exception { + try (final Context context = new Context(ScriptService.class)) { + final ScriptEngine engine = getEngine(context); + + final char expected = 'q'; + engine.put("v", expected); + final String script = "val v1:Char = v"; + engine.eval(script); + final Object actual = engine.get("v1"); + assertEquals(expected, actual); + } + } + + @Test + public void testPutShort() throws Exception { + try (final Context context = new Context(ScriptService.class)) { + final ScriptEngine engine = getEngine(context); + + final short expected = 512; + engine.put("v", expected); + final String script = "val v1:Short = v"; + engine.eval(script); + final Object actual = engine.get("v1"); + assertEquals(expected, actual); + } + } + + @Test + public void testPutByte() throws Exception { + try (final Context context = new Context(ScriptService.class)) { + final ScriptEngine engine = getEngine(context); + + final byte expected = -127; + engine.put("v", expected); + final String script = "val v1:Byte = v"; + engine.eval(script); + final Object actual = engine.get("v1"); + assertEquals(expected, actual); + } + } + + @Test + public void testPutString() throws Exception { + try (final Context context = new Context(ScriptService.class)) { + final ScriptEngine engine = getEngine(context); + + final String expected = "Ala ma kota"; + engine.put("v", expected); + final String script = "val v1:String = v"; + engine.eval(script); + final Object actual = engine.get("v1"); + assertEquals(expected, actual); + } + } + + + @Test + public void testPutInt() throws Exception { + try (final Context context = new Context(ScriptService.class)) { + final ScriptEngine engine = getEngine(context); + + final int expected = 7; + engine.put("v", expected); + final String script = "val v1:Int = v"; + engine.eval(script); + final Object actual = engine.get("v1"); + assertEquals(expected, actual); + } + } + + + @Test + public void testLocals() throws Exception { + try (final Context context = new Context(ScriptService.class)) { + final ScriptService scriptService = context.getService(ScriptService.class); + + final ScriptLanguage language = scriptService.getLanguageByExtension("scala"); + final ScriptEngine engine = language.getScriptEngine(); + assertEquals("org.scijava.plugins.scripting.scala.ScalaAdaptedScriptEngine", engine.getClass().getName()); + engine.put("hello", 17); + assertEquals("17", engine.eval("hello").toString()); + assertEquals("17", engine.get("hello").toString()); + +// With Scala 3.2.2 cannot reset bindings correctly, will skip the ret of the test +// final Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE); +// bindings.clear(); +// assertNull(engine.get("hello")); + } + } + + @Test + public void testParameters() throws Exception { + try (final Context context = new Context(ScriptService.class)) { + final ScriptService scriptService = context.getService(ScriptService.class); + + final String script = "" + // + "#@ScriptService ss\n" + // + "#@OUTPUT String language\n" + // + "val language = ss.getLanguageByName(\"scala\").getLanguageName()\n"; + final ScriptModule m = scriptService.run("hello.scala", script, true).get(); + + final Object actual = m.getOutput("language"); + final String expected = + scriptService.getLanguageByName("scala").getLanguageName(); + assertEquals(expected, actual); + } + } + + @Test + public void test2Inputs() throws Exception { + try (final Context context = new Context(ScriptService.class)) { + final ScriptEngine engine = getEngine(context); - final ScriptLanguage language = - scriptService.getLanguageByExtension("scala"); - final ScriptEngine engine = language.getScriptEngine(); + engine.put("a", 2); + engine.put("b", 5); + engine.eval("val c = a + b"); + final Object actual = engine.get("c"); + assertEquals(7, actual); + } + } - final SimpleScriptContext ssc = new SimpleScriptContext(); - final StringWriter writer = new StringWriter(); - ssc.setWriter(writer); + @Test + public void testImportsRetained() throws Exception { + try (final Context context = new Context(ScriptService.class)) { + final ScriptService scriptService = context.getService(ScriptService.class); + final ScriptEngine engine = scriptService.getLanguageByName("scala").getScriptEngine(); + final String script = "" + + "import org.scijava.util.VersionUtils\n" + + "VersionUtils.getVersion(classOf[VersionUtils])\n"; + final Object result = engine.eval(script); + assertTrue(result instanceof String); + final String version = (String) result; + assertTrue(version, version.matches("\\d+\\.\\d+\\.\\d")); - final String script = "print(\"3\");"; - engine.eval(script, ssc); - assertEquals("3", writer.toString()); - } + final String script2 = "VersionUtils.getVersion(classOf[VersionUtils])\n"; + final Object result2 = engine.eval(script2); + assertEquals(result, result2); + } + } }