Skip to content

Scripting solution #10491

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Dec 11, 2020
Merged
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
22 changes: 22 additions & 0 deletions compiler/src/dotty/tools/scripting/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package dotty.tools.scripting

import java.io.File

/** Main entry point to the Scripting execution engine */
object Main:
/** All arguments before -script <target_script> are compiler arguments.
All arguments afterwards are script arguments.*/
def distinguishArgs(args: Array[String]): (Array[String], File, Array[String]) =
val (compilerArgs, rest) = args.splitAt(args.indexOf("-script"))
val file = File(rest(1))
val scriptArgs = rest.drop(2)
(compilerArgs, file, scriptArgs)
end distinguishArgs

def main(args: Array[String]): Unit =
val (compilerArgs, scriptFile, scriptArgs) = distinguishArgs(args)
try ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun()
catch
case ScriptingException(msg) =>
println(s"Error: $msg")
sys.exit(1)
86 changes: 86 additions & 0 deletions compiler/src/dotty/tools/scripting/ScriptingDriver.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package dotty.tools.scripting

import java.nio.file.{ Files, Path }
import java.io.File
import java.net.{ URL, URLClassLoader }
import java.lang.reflect.{ Modifier, Method }

import scala.jdk.CollectionConverters._

import dotty.tools.dotc.{ Driver, Compiler }
import dotty.tools.dotc.core.Contexts, Contexts.{ Context, ContextBase, ctx }
import dotty.tools.dotc.config.CompilerCommand
import dotty.tools.io.{ PlainDirectory, Directory }
import dotty.tools.dotc.reporting.Reporter
import dotty.tools.dotc.config.Settings.Setting._

import sys.process._

class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: Array[String]) extends Driver:
def compileAndRun(): Unit =
val outDir = Files.createTempDirectory("scala3-scripting")
val (toCompile, rootCtx) = setup(compilerArgs :+ scriptFile.getAbsolutePath, initCtx.fresh)
given Context = rootCtx.fresh.setSetting(rootCtx.settings.outputDir,
new PlainDirectory(Directory(outDir)))

if doCompile(newCompiler, toCompile).hasErrors then
throw ScriptingException("Errors encountered during compilation")

try detectMainMethod(outDir, ctx.settings.classpath.value).invoke(null, scriptArgs)
catch
case e: java.lang.reflect.InvocationTargetException =>
throw e.getCause
finally
deleteFile(outDir.toFile)
end compileAndRun

private def deleteFile(target: File): Unit =
if target.isDirectory then
for member <- target.listFiles.toList
do deleteFile(member)
target.delete()
end deleteFile

private def detectMainMethod(outDir: Path, classpath: String): Method =
val outDirURL = outDir.toUri.toURL
val classpathUrls = classpath.split(":").map(File(_).toURI.toURL)
val cl = URLClassLoader(classpathUrls :+ outDirURL)

def collectMainMethods(target: File, path: String): List[Method] =
val nameWithoutExtension = target.getName.takeWhile(_ != '.')
val targetPath =
if path.nonEmpty then s"${path}.${nameWithoutExtension}"
else nameWithoutExtension

if target.isDirectory then
for
packageMember <- target.listFiles.toList
membersMainMethod <- collectMainMethods(packageMember, targetPath)
yield membersMainMethod
else if target.getName.endsWith(".class") then
val cls = cl.loadClass(targetPath)
try
val method = cls.getMethod("main", classOf[Array[String]])
if Modifier.isStatic(method.getModifiers) then List(method) else Nil
catch
case _: java.lang.NoSuchMethodException => Nil
else Nil
end collectMainMethods

val candidates = for
file <- outDir.toFile.listFiles.toList
method <- collectMainMethods(file, "")
yield method

candidates match
case Nil =>
throw ScriptingException("No main methods detected in your script")
case _ :: _ :: _ =>
throw ScriptingException("A script must contain only one main method. " +
s"Detected the following main methods:\n${candidates.mkString("\n")}")
case m :: Nil => m
end match
end detectMainMethod
end ScriptingDriver

case class ScriptingException(msg: String) extends RuntimeException(msg)
1 change: 1 addition & 0 deletions compiler/test-resources/scripting/basic-math.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@main def Test = assert(2 + 2 == 4)
1 change: 1 addition & 0 deletions compiler/test-resources/scripting/say-hi.args
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
World
2 changes: 2 additions & 0 deletions compiler/test-resources/scripting/say-hi.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@main def Test(name: String) =
assert(name == "World")
11 changes: 2 additions & 9 deletions compiler/test/dotty/tools/repl/ReplTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,6 @@ class ReplTest(withStaging: Boolean = false, out: ByteArrayOutputStream = new By
extension [A](state: State)
def andThen(op: State => A): A = op(state)

def scripts(path: String): Array[JFile] = {
val dir = new JFile(getClass.getResource(path).getPath)
assert(dir.exists && dir.isDirectory, "Couldn't load scripts dir")
dir.listFiles
}

def testFile(f: JFile): Unit = {
val prompt = "scala>"

Expand All @@ -77,12 +71,11 @@ class ReplTest(withStaging: Boolean = false, out: ByteArrayOutputStream = new By
case nonEmptyLine => nonEmptyLine :: Nil
}

val expectedOutput =
Using(Source.fromFile(f, StandardCharsets.UTF_8.name))(_.getLines().flatMap(filterEmpties).toList).get
val expectedOutput = readLines(f).flatMap(filterEmpties)
val actualOutput = {
resetToInitial()

val lines = Using(Source.fromFile(f, StandardCharsets.UTF_8.name))(_.getLines.toList).get
val lines = readLines(f)
assert(lines.head.startsWith(prompt),
s"""Each file has to start with the prompt: "$prompt"""")
val inputRes = lines.filter(_.startsWith(prompt))
Expand Down
39 changes: 39 additions & 0 deletions compiler/test/dotty/tools/scripting/ScriptingTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package dotty
package tools
package scripting

import java.io.File

import org.junit.Test

import vulpix.TestConfiguration


/** Runs all tests contained in `compiler/test-resources/repl/` */
class ScriptingTests:
extension (str: String) def dropExtension =
str.reverse.dropWhile(_ != '.').drop(1).reverse

@Test def scriptingTests =
val testFiles = scripts("/scripting")

val argss: Map[String, Array[String]] = (
for
argFile <- testFiles
if argFile.getName.endsWith(".args")
name = argFile.getName.dropExtension
scriptArgs = readLines(argFile).toArray
yield name -> scriptArgs).toMap

for
scriptFile <- testFiles
if scriptFile.getName.endsWith(".scala")
name = scriptFile.getName.dropExtension
scriptArgs = argss.getOrElse(name, Array.empty[String])
do
ScriptingDriver(
compilerArgs = Array(
"-classpath", TestConfiguration.basicClasspath),
scriptFile = scriptFile,
scriptArgs = scriptArgs
).compileAndRun()
19 changes: 19 additions & 0 deletions compiler/test/dotty/tools/utils.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dotty.tools

import java.io.File
import java.nio.charset.StandardCharsets.UTF_8

import scala.io.Source
import scala.util.Using.resource

def scripts(path: String): Array[File] = {
val dir = new File(getClass.getResource(path).getPath)
assert(dir.exists && dir.isDirectory, "Couldn't load scripts dir")
dir.listFiles
}

private def withFile[T](file: File)(action: Source => T): T =
resource(Source.fromFile(file, UTF_8.name))(action)

def readLines(f: File): List[String] = withFile(f)(_.getLines.toList)
def readFile(f: File): String = withFile(f)(_.mkString)
21 changes: 19 additions & 2 deletions dist/bin/scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ addDotcOptions () {
source "$PROG_HOME/bin/common"

declare -a residual_args
declare -a script_args
execute_repl=false
execute_run=false
execute_script=false
with_compiler=false
class_path_count=0
CLASS_PATH=""
Expand Down Expand Up @@ -79,13 +81,28 @@ while [[ $# -gt 0 ]]; do
addDotcOptions "${1}"
shift ;;
*)
residual_args+=("$1")
if [ $execute_script == false ]; then
if [[ "$1" == *.scala ]]; then
execute_script=true
target_script="$1"
else
residual_args+=("$1")
fi
else
script_args+=("$1")
fi
shift
;;

esac
done
if [ $execute_repl == true ] || ([ $execute_run == false ] && [ $options_indicator == 0 ]); then

if [ $execute_script == true ]; then
if [ "$CLASS_PATH" ]; then
cp_arg="-classpath \"$CLASS_PATH\""
fi
eval "\"$PROG_HOME/bin/scalac\" $cp_arg ${java_options[@]} ${residual_args[@]} -script $target_script ${script_args[@]}"
elif [ $execute_repl == true ] || ([ $execute_run == false ] && [ $options_indicator == 0 ]); then
if [ "$CLASS_PATH" ]; then
cp_arg="-classpath \"$CLASS_PATH\""
fi
Expand Down
21 changes: 19 additions & 2 deletions dist/bin/scalac
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ withCompiler=true
CompilerMain=dotty.tools.dotc.Main
DecompilerMain=dotty.tools.dotc.decompiler.Main
ReplMain=dotty.tools.repl.Main
ScriptingMain=dotty.tools.scripting.Main

PROG_NAME=$CompilerMain

Expand All @@ -45,6 +46,9 @@ addScala () {
addResidual () {
residual_args+=("'$1'")
}
addScripting () {
scripting_args+=("'$1'")
}

classpathArgs () {
# echo "dotty-compiler: $DOTTY_COMP"
Expand Down Expand Up @@ -74,6 +78,7 @@ classpathArgs () {
jvm_cp_args="-classpath \"$toolchain\""
}

in_scripting_args=false
while [[ $# -gt 0 ]]; do
case "$1" in
--) shift; for arg; do addResidual "$arg"; done; set -- ;;
Expand All @@ -85,6 +90,7 @@ case "$1" in
# Optimize for short-running applications, see https://github.com/lampepfl/dotty/issues/222
-Oshort) addJava "-XX:+TieredCompilation -XX:TieredStopAtLevel=1" && shift ;;
-repl) PROG_NAME="$ReplMain" && shift ;;
-script) PROG_NAME="$ScriptingMain" && target_script="$2" && in_scripting_args=true && shift && shift ;;
-compile) PROG_NAME="$CompilerMain" && shift ;;
-decompile) PROG_NAME="$DecompilerMain" && shift ;;
-print-tasty) PROG_NAME="$DecompilerMain" && addScala "-print-tasty" && shift ;;
Expand All @@ -98,12 +104,22 @@ case "$1" in
# will be available as system properties.
-D*) addJava "$1" && shift ;;
-J*) addJava "${1:2}" && shift ;;
*) addResidual "$1" && shift ;;
*) if [ $in_scripting_args == false ]; then
addResidual "$1"
else
addScripting "$1"
fi
shift
;;
esac
done

classpathArgs

if [ "$PROG_NAME" == "$ScriptingMain" ]; then
scripting_string="-script $target_script ${scripting_args[@]}"
fi

eval exec "\"$JAVACMD\"" \
${JAVA_OPTS:-$default_java_opts} \
"$DEBUG" \
Expand All @@ -112,5 +128,6 @@ eval exec "\"$JAVACMD\"" \
-Dscala.usejavacp=true \
"$PROG_NAME" \
"${scala_args[@]}" \
"${residual_args[@]}"
"${residual_args[@]}" \
"$scripting_string"
exit $?
16 changes: 16 additions & 0 deletions docs/docs/usage/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,19 @@ In case you have already installed Dotty via brew, you should instead update it:
```bash
brew upgrade dotty
```

### Scala 3 for Scripting
If you have followed the steps in "Standalone Installation" section and have the `scala` executable on your `PATH`, you can run `*.scala` files as scripts. Given a source named Test.scala:

```scala
@main def Test(name: String): Unit =
println(s"Hello ${name}!")
```

You can run: `scala Test.scala World` to get an output `Hello World!`.

A "script" is an ordinary Scala file which contains a main method. The semantics of the `scala Script.scala` command is as follows:

- Compile `Script.scala` with `scalac` into a temporary directory.
- Detect the main method in the `*.class` files produced by the compilation.
- Execute the main method.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package scala.quoted.staging.repl

import dotty.BootstrappedOnlyTests
import dotty.tools.scripts
import dotty.tools.repl.ReplTest
import dotty.tools.vulpix.TestConfiguration
import org.junit.Test
Expand Down