Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions compiler/src/dotty/tools/repl/ReplDriver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,12 @@ class ReplDriver(settings: Array[String],
* observable outside of the CLI, for this reason, most helper methods are
* `protected final` to facilitate testing.
*/
def runUntilQuit(using initialState: State = initialState)(): State = {
def runUntilQuit(using initialState: State = initialState)(hardcodedInput: java.io.InputStream = null): State = {
val terminal = new JLineTerminal

val hardcodedInputLines =
if (hardcodedInput == null) null
else new java.io.BufferedReader(new java.io.InputStreamReader(hardcodedInput))
out.println(
s"""Welcome to Scala $simpleVersionString ($javaVersion, Java $javaVmName).
|Type in expressions for evaluation. Or try :help.""".stripMargin)
Expand Down Expand Up @@ -208,8 +211,12 @@ class ReplDriver(settings: Array[String],
}

try {
val line = terminal.readLine(completer)
ParseResult(line)
val line =
if (hardcodedInputLines != null) hardcodedInputLines.readLine()
else terminal.readLine(completer)

if (line == null) Quit
else ParseResult(line)
Copy link
Contributor

@WojciechMazur WojciechMazur Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lower in this file there is defined final def bind(name: String, value: Any) which is a stub. We can either try to adapt logic to take advantage of this API which might have been used by existing tooling, or we can remove it. It's used in ConsoleInterface.java

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also good to explore significance with the java scripting api (JSR 223)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current API is modelled after the Ammonite API, which is Configure(...flags...).run(...bindings...). There are other ways we could design the API (builders, setters, etc.) but I think the Ammonite API is a reasonable starting point. IMO if def bind doesn't do anything, and anyway has the wrong signature (we need to capture the type name), we can just remove it.

JSR223 support could be done, but I think that would be a very different use case than the programmatic use case supported here, and can come in a separate PR if anyone asks for it

} catch {
case _: EndOfFileException => // Ctrl+D
Quit
Expand Down
60 changes: 60 additions & 0 deletions compiler/src/dotty/tools/repl/ReplMain.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package dotty.tools.repl

import java.io.PrintStream

class ReplMain(
settings: Array[String] = Array.empty,
out: PrintStream = Console.out,
classLoader: Option[ClassLoader] = Some(getClass.getClassLoader),
predefCode: String = "",
testCode: String = ""
):
def run(bindings: ReplMain.Bind[_]*): Any =
try
ReplMain.currentBindings.set(bindings.map{bind => bind.name -> bind.value}.toMap)

val bindingsPredef = bindings
.map { case bind =>
s"def ${bind.name}: ${bind.typeName.value} = dotty.tools.repl.ReplMain.currentBinding[${bind.typeName.value}](\"${bind.name}\")"
}
.mkString("\n")

val fullPredef =
ReplDriver.pprintImport +
(if bindingsPredef.nonEmpty then s"\n$bindingsPredef\n" else "") +
(if predefCode.nonEmpty then s"\n$predefCode\n" else "")

val driver = new ReplDriver(settings, out, classLoader, fullPredef)

if (testCode == "") driver.tryRunning
else driver.runUntilQuit(using driver.initialState)(
new java.io.ByteArrayInputStream(testCode.getBytes())
)
()
finally
ReplMain.currentBindings.set(null)


object ReplMain:
final case class TypeName[A](value: String)
object TypeName extends TypeNamePlatform

import scala.quoted._

trait TypeNamePlatform:
inline given [A]: TypeName[A] = ${TypeNamePlatform.impl[A]}

object TypeNamePlatform:
def impl[A](using t: Type[A], ctx: Quotes): Expr[TypeName[A]] =
'{TypeName[A](${Expr(Type.show[A])})}


case class Bind[T](name: String, value: () => T)(implicit val typeName: TypeName[T])
object Bind:
implicit def ammoniteReplArrowBinder[T](t: (String, T))(implicit typeName: TypeName[T]): Bind[T] = {
Bind(t._1, () => t._2)(typeName)
}

def currentBinding[T](s: String): T = currentBindings.get().apply(s).apply().asInstanceOf[T]

private val currentBindings = new ThreadLocal[Map[String, () => Any]]()
73 changes: 73 additions & 0 deletions compiler/test/dotty/tools/repl/ReplMainTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package dotty.tools
package repl

import scala.language.unsafeNulls

import java.io.{ByteArrayOutputStream, PrintStream}
import java.nio.charset.StandardCharsets

import vulpix.TestConfiguration
import org.junit.Test
import org.junit.Assert._

/** Tests for the programmatic REPL API (ReplMain) */
class ReplMainTest:

private val defaultOptions = Array("-classpath", TestConfiguration.withCompilerClasspath)

private def captureOutput(body: PrintStream => Unit): String =
val out = new ByteArrayOutputStream()
val ps = new PrintStream(out, true, StandardCharsets.UTF_8.name)
body(ps)
dotty.shaded.fansi.Str(out.toString(StandardCharsets.UTF_8.name)).plainText

@Test def basicBinding(): Unit =
val output = captureOutput { out =>
val replMain = new ReplMain(
settings = defaultOptions,
out = out,
testCode = "test"
)

replMain.run("test" -> 42)
}

assertTrue(output.contains("val res0: Int = 42"))

@Test def multipleBindings(): Unit =
val output = captureOutput { out =>
val replMain = new ReplMain(
settings = defaultOptions,
out = out,
testCode = "x\ny\nz"
)

replMain.run(
"x" -> 1,
"y" -> "hello",
"z" -> true
)
}

assertTrue(output.contains("val res0: Int = 1"))
assertTrue(output.contains("val res1: String = \"hello\""))
assertTrue(output.contains("val res2: Boolean = true"))

@Test def bindingTypes(): Unit =
val output = captureOutput { out =>
val replMain = new ReplMain(
settings = defaultOptions ++ Array("-repl-quit-after-init"),
out = out,
testCode = "list\nmap"
)

replMain.run(
"list" -> List(1, 2, 3),
"map" -> Map(1 -> "hello")
)
}

assertTrue(output.contains("val res0: List[Int] = List(1, 2, 3)"))
assertTrue(output.contains("val res1: Map[Int, String] = Map(1 -> \"hello\")"))

end ReplMainTest
2 changes: 1 addition & 1 deletion sbt-bridge/src/xsbt/ConsoleInterface.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public void run(

state = driver.run(initialCommands, state);
// TODO handle failure during initialisation
state = driver.runUntilQuit(state);
state = driver.runUntilQuit(state, null);
driver.run(cleanupCommands, state);
}
}
Loading