Skip to content

Commit 8fd5964

Browse files
committed
Make possible to bind/transfer multiple AnyRef variables
AnyRef bound variables maintain reference to transfer variable. Even if they are `val`'s. Need to use separate transfer variable for each bound variable. This implementation is not production ready. It never releases transfer variables and limits their number. Good enough for scripts that are called only limited times.
1 parent 730a93b commit 8fd5964

File tree

2 files changed

+122
-21
lines changed

2 files changed

+122
-21
lines changed

src/main/scala/org/scijava/plugins/scripting/scala/ScalaAdaptedScriptEngine.scala

Lines changed: 81 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import javax.script.*
3434
import scala.collection.mutable
3535
import scala.jdk.CollectionConverters.*
3636
import scala.util.Try
37+
import scala.util.control.NonFatal
3738
import scala.util.matching.Regex
3839

3940
/**
@@ -67,12 +68,12 @@ class ScalaAdaptedScriptEngine(engine: ScriptEngine) extends AbstractScriptEngin
6768

6869
// Scala 3.2.2 ignores bindings, emulate binding using setup script
6970
// Create a line with variable declaration for each binding item
70-
val lines =
71+
val transfers: mutable.Seq[Seq[String]] =
7172
for
7273
scope <- context.getScopes.asScala
7374
bindings <- Option(context.getBindings(scope)).map(_.asScala) // bindings in context can be null
7475
yield {
75-
for (name, value) <- bindings yield {
76+
for (name, value) <- bindings.toSeq yield {
7677
if isValidVariableName(name) then
7778
val validName = addBackticksIfNeeded(name)
7879
value match
@@ -85,12 +86,22 @@ class ScalaAdaptedScriptEngine(engine: ScriptEngine) extends AbstractScriptEngin
8586
case v: Byte => s"val $validName : Byte = $v"
8687
case v: Boolean => s"val $validName : Int = $v"
8788
case v: AnyRef =>
88-
_transfer = v
89-
val typeName = Option(v).map(_.getClass.getCanonicalName).getOrElse("AnyRef")
89+
val transferIndex = BindingSupport.nextTransferIndex
90+
BindingSupport.__transfer(transferIndex) = v
91+
val typeName = Option(v)
92+
.map { oo =>
93+
val tt: Array[_] = oo.getClass.getTypeParameters
94+
tt.foreach(t => log(s"${oo.getClass.getCanonicalName} TYPE PARAM: ${t.getClass.getName}"))
95+
val p = tt.map(_ => "_").mkString("[", ",", "]")
96+
val n = oo.getClass.getCanonicalName
97+
if tt.nonEmpty then n + p else n
98+
}
99+
.getOrElse("AnyRef")
90100
val validTypeName = addBackticksIfNeeded(typeName)
91101
s"""
92102
|val $validName : $validTypeName = {
93-
| val t = org.scijava.plugins.scripting.scala.ScalaAdaptedScriptEngine._transfer
103+
| val t = org.scijava.plugins.scripting.scala.ScalaAdaptedScriptEngine.BindingSupport
104+
| ._transfer($transferIndex)
94105
| t.asInstanceOf[$validTypeName]
95106
|}""".stripMargin
96107
case v: Unit =>
@@ -100,26 +111,45 @@ class ScalaAdaptedScriptEngine(engine: ScriptEngine) extends AbstractScriptEngin
100111
}
101112
}
102113

103-
val script = lines
114+
val script = transfers
104115
.flatten
105116
.filter(_.nonEmpty)
106117
.mkString("\n")
107118

108-
if script.nonEmpty then
109-
evalInner(script, context)
119+
evalInner(script, context)
110120

111121
end emulateBinding
112122

113-
private def evalInner(script: String, context: ScriptContext) =
114-
class WriterOutputStream(w: Writer) extends OutputStream:
115-
override def write(b: Int): Unit = w.write(b)
123+
private def evalInner(script: String, context: ScriptContext): AnyRef =
124+
log(
125+
s"""
126+
|LOG[evalInner] script
127+
|BEGIN
128+
|---------------------------
129+
|$script
130+
|---------------------------
131+
|END
132+
|""".stripMargin
133+
)
134+
if script.trim.isEmpty then
135+
log("LOG[evalInner] script is empty, skipping evaluation")
136+
null
137+
else
138+
class WriterOutputStream(w: Writer) extends OutputStream:
139+
override def write(b: Int): Unit = w.write(b)
116140

117-
// Redirect output to writes provided by context
118-
Console.withOut(WriterOutputStream(context.getWriter)) {
119-
Console.withErr(WriterOutputStream(context.getErrorWriter)) {
120-
engine.eval(script, context)
121-
}
122-
}
141+
try
142+
// Redirect output to writes provided by context
143+
Console.withOut(WriterOutputStream(context.getWriter)) {
144+
Console.withErr(WriterOutputStream(context.getErrorWriter)) {
145+
engine.eval(script, context)
146+
}
147+
}
148+
catch
149+
case NonFatal(t) =>
150+
log(s"LOG[evalInner] in eval: $t")
151+
t.printStackTrace()
152+
throw t
123153

124154
private def stringFromReader(in: Reader) =
125155
val out = new StringWriter()
@@ -156,9 +186,15 @@ class ScalaAdaptedScriptEngine(engine: ScriptEngine) extends AbstractScriptEngin
156186
value
157187
end get
158188

189+
private def log(msg: String): Unit = {
190+
if ScalaAdaptedScriptEngine.DEBUG then
191+
Console.out.println(msg)
192+
}
193+
159194
end ScalaAdaptedScriptEngine
160195

161196
object ScalaAdaptedScriptEngine:
197+
private val DEBUG: Boolean = false
162198
private lazy val variableNamePattern = """^[a-zA-Z_$][a-zA-Z_$0-9]*$""".r
163199
private val scala3Keywords = Seq(
164200
"abstract",
@@ -204,10 +240,6 @@ object ScalaAdaptedScriptEngine:
204240
"yield"
205241
)
206242

207-
/** Do not use externally despite it is declared public. IT is public so it is accessible from scripts */
208-
// noinspection ScalaWeakerAccess
209-
var _transfer: Object = _
210-
211243
private def isValidVariableName(name: String): Boolean = variableNamePattern.matches(name)
212244

213245
private[scala] def addBackticksIfNeeded(referenceName: String): String =
@@ -216,4 +248,32 @@ object ScalaAdaptedScriptEngine:
216248
.map(n => if scala3Keywords.contains(n) then s"`$n`" else n)
217249
.mkString(".")
218250

251+
/**
252+
* Temporary support for implementing binding in the script engine.
253+
* It has limited capacity and does not free memory.
254+
* Access to storage is public, so it is visible from scripts.
255+
*/
256+
//noinspection ScalaWeakerAccess
257+
object BindingSupport:
258+
private val MaxTransfers: Int = 1024 * 1024
259+
260+
/**
261+
* Do not use externally despite it is declared public.
262+
* It is public so it is accessible from scripts that are used to emulate variable binding
263+
*/
264+
// noinspection ScalaWeakerAccess,ScalaUnusedSymbol
265+
def _transfer: Seq[AnyRef] = __transfer.toSeq
266+
267+
private[scala] val __transfer: mutable.ListBuffer[AnyRef] = mutable.ListBuffer.empty[AnyRef]
268+
269+
private var lastTransferIndex = -1
270+
271+
private[scala] def nextTransferIndex: Int =
272+
if lastTransferIndex + 1 >= MaxTransfers then
273+
throw new IllegalStateException("ScalaAdaptedScriptEngine: maximum transfer limit reached")
274+
275+
lastTransferIndex += 1
276+
__transfer.append(null)
277+
lastTransferIndex
278+
219279
end ScalaAdaptedScriptEngine

src/test/java/org/scijava/plugins/scripting/scala/ScalaTest.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
import javax.script.ScriptException;
4141
import javax.script.SimpleScriptContext;
4242
import java.io.StringWriter;
43+
import java.util.ArrayList;
44+
import java.util.List;
4345

4446
import static org.junit.Assert.*;
4547

@@ -183,6 +185,28 @@ public void testPutString() throws Exception {
183185
}
184186
}
185187

188+
@Test
189+
public void testPut3Strings() throws Exception {
190+
// Check that multiple AnyRef variable are bound correctly
191+
try (final Context context = new Context(ScriptService.class)) {
192+
final ScriptEngine engine = getEngine(context);
193+
final String expected1 = "Ala ma kota";
194+
final String expected2 = "Kot ma Ale";
195+
final String expected3 = "Reksio nie ma butow";
196+
engine.put("v1", expected1);
197+
engine.put("v2", expected2);
198+
engine.put("v3", expected3);
199+
final String script = "\n" +
200+
"val o1:String = v1\n" +
201+
"val o2:String = v2\n" +
202+
"val o3:String = v3\n";
203+
engine.eval(script);
204+
assertEquals(expected1, engine.get("o1"));
205+
assertEquals(expected2, engine.get("o2"));
206+
assertEquals(expected3, engine.get("o3"));
207+
}
208+
}
209+
186210

187211
@Test
188212
public void testPutInt() throws Exception {
@@ -198,6 +222,23 @@ public void testPutInt() throws Exception {
198222
}
199223
}
200224

225+
@Test
226+
public void testPutSeqInt() throws Exception {
227+
try (final Context context = new Context(ScriptService.class)) {
228+
final ScriptEngine engine = getEngine(context);
229+
230+
final List<Integer> expected = new ArrayList<>();
231+
expected.add(7);
232+
expected.add(13);
233+
expected.add(-1);
234+
engine.put("v", expected);
235+
final String script = "val v1 = v";
236+
engine.eval(script);
237+
final Object actual = engine.get("v1");
238+
assertEquals(expected, actual);
239+
}
240+
}
241+
201242

202243
@Test
203244
public void testLocals() throws Exception {

0 commit comments

Comments
 (0)