Skip to content

Commit 67625b0

Browse files
Merge pull request #10491 from dotty-staging/scripting
Scripting solution
2 parents bf87cd1 + 246b5a1 commit 67625b0

File tree

12 files changed

+227
-13
lines changed

12 files changed

+227
-13
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package dotty.tools.scripting
2+
3+
import java.io.File
4+
5+
/** Main entry point to the Scripting execution engine */
6+
object Main:
7+
/** All arguments before -script <target_script> are compiler arguments.
8+
All arguments afterwards are script arguments.*/
9+
def distinguishArgs(args: Array[String]): (Array[String], File, Array[String]) =
10+
val (compilerArgs, rest) = args.splitAt(args.indexOf("-script"))
11+
val file = File(rest(1))
12+
val scriptArgs = rest.drop(2)
13+
(compilerArgs, file, scriptArgs)
14+
end distinguishArgs
15+
16+
def main(args: Array[String]): Unit =
17+
val (compilerArgs, scriptFile, scriptArgs) = distinguishArgs(args)
18+
try ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun()
19+
catch
20+
case ScriptingException(msg) =>
21+
println(s"Error: $msg")
22+
sys.exit(1)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package dotty.tools.scripting
2+
3+
import java.nio.file.{ Files, Path }
4+
import java.io.File
5+
import java.net.{ URL, URLClassLoader }
6+
import java.lang.reflect.{ Modifier, Method }
7+
8+
import scala.jdk.CollectionConverters._
9+
10+
import dotty.tools.dotc.{ Driver, Compiler }
11+
import dotty.tools.dotc.core.Contexts, Contexts.{ Context, ContextBase, ctx }
12+
import dotty.tools.dotc.config.CompilerCommand
13+
import dotty.tools.io.{ PlainDirectory, Directory }
14+
import dotty.tools.dotc.reporting.Reporter
15+
import dotty.tools.dotc.config.Settings.Setting._
16+
17+
import sys.process._
18+
19+
class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: Array[String]) extends Driver:
20+
def compileAndRun(): Unit =
21+
val outDir = Files.createTempDirectory("scala3-scripting")
22+
val (toCompile, rootCtx) = setup(compilerArgs :+ scriptFile.getAbsolutePath, initCtx.fresh)
23+
given Context = rootCtx.fresh.setSetting(rootCtx.settings.outputDir,
24+
new PlainDirectory(Directory(outDir)))
25+
26+
if doCompile(newCompiler, toCompile).hasErrors then
27+
throw ScriptingException("Errors encountered during compilation")
28+
29+
try detectMainMethod(outDir, ctx.settings.classpath.value).invoke(null, scriptArgs)
30+
catch
31+
case e: java.lang.reflect.InvocationTargetException =>
32+
throw e.getCause
33+
finally
34+
deleteFile(outDir.toFile)
35+
end compileAndRun
36+
37+
private def deleteFile(target: File): Unit =
38+
if target.isDirectory then
39+
for member <- target.listFiles.toList
40+
do deleteFile(member)
41+
target.delete()
42+
end deleteFile
43+
44+
private def detectMainMethod(outDir: Path, classpath: String): Method =
45+
val outDirURL = outDir.toUri.toURL
46+
val classpathUrls = classpath.split(":").map(File(_).toURI.toURL)
47+
val cl = URLClassLoader(classpathUrls :+ outDirURL)
48+
49+
def collectMainMethods(target: File, path: String): List[Method] =
50+
val nameWithoutExtension = target.getName.takeWhile(_ != '.')
51+
val targetPath =
52+
if path.nonEmpty then s"${path}.${nameWithoutExtension}"
53+
else nameWithoutExtension
54+
55+
if target.isDirectory then
56+
for
57+
packageMember <- target.listFiles.toList
58+
membersMainMethod <- collectMainMethods(packageMember, targetPath)
59+
yield membersMainMethod
60+
else if target.getName.endsWith(".class") then
61+
val cls = cl.loadClass(targetPath)
62+
try
63+
val method = cls.getMethod("main", classOf[Array[String]])
64+
if Modifier.isStatic(method.getModifiers) then List(method) else Nil
65+
catch
66+
case _: java.lang.NoSuchMethodException => Nil
67+
else Nil
68+
end collectMainMethods
69+
70+
val candidates = for
71+
file <- outDir.toFile.listFiles.toList
72+
method <- collectMainMethods(file, "")
73+
yield method
74+
75+
candidates match
76+
case Nil =>
77+
throw ScriptingException("No main methods detected in your script")
78+
case _ :: _ :: _ =>
79+
throw ScriptingException("A script must contain only one main method. " +
80+
s"Detected the following main methods:\n${candidates.mkString("\n")}")
81+
case m :: Nil => m
82+
end match
83+
end detectMainMethod
84+
end ScriptingDriver
85+
86+
case class ScriptingException(msg: String) extends RuntimeException(msg)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@main def Test = assert(2 + 2 == 4)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
World
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@main def Test(name: String) =
2+
assert(name == "World")

compiler/test/dotty/tools/repl/ReplTest.scala

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,6 @@ class ReplTest(withStaging: Boolean = false, out: ByteArrayOutputStream = new By
5050
extension [A](state: State)
5151
def andThen(op: State => A): A = op(state)
5252

53-
def scripts(path: String): Array[JFile] = {
54-
val dir = new JFile(getClass.getResource(path).getPath)
55-
assert(dir.exists && dir.isDirectory, "Couldn't load scripts dir")
56-
dir.listFiles
57-
}
58-
5953
def testFile(f: JFile): Unit = {
6054
val prompt = "scala>"
6155

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

80-
val expectedOutput =
81-
Using(Source.fromFile(f, StandardCharsets.UTF_8.name))(_.getLines().flatMap(filterEmpties).toList).get
74+
val expectedOutput = readLines(f).flatMap(filterEmpties)
8275
val actualOutput = {
8376
resetToInitial()
8477

85-
val lines = Using(Source.fromFile(f, StandardCharsets.UTF_8.name))(_.getLines.toList).get
78+
val lines = readLines(f)
8679
assert(lines.head.startsWith(prompt),
8780
s"""Each file has to start with the prompt: "$prompt"""")
8881
val inputRes = lines.filter(_.startsWith(prompt))
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package dotty
2+
package tools
3+
package scripting
4+
5+
import java.io.File
6+
7+
import org.junit.Test
8+
9+
import vulpix.TestConfiguration
10+
11+
12+
/** Runs all tests contained in `compiler/test-resources/repl/` */
13+
class ScriptingTests:
14+
extension (str: String) def dropExtension =
15+
str.reverse.dropWhile(_ != '.').drop(1).reverse
16+
17+
@Test def scriptingTests =
18+
val testFiles = scripts("/scripting")
19+
20+
val argss: Map[String, Array[String]] = (
21+
for
22+
argFile <- testFiles
23+
if argFile.getName.endsWith(".args")
24+
name = argFile.getName.dropExtension
25+
scriptArgs = readLines(argFile).toArray
26+
yield name -> scriptArgs).toMap
27+
28+
for
29+
scriptFile <- testFiles
30+
if scriptFile.getName.endsWith(".scala")
31+
name = scriptFile.getName.dropExtension
32+
scriptArgs = argss.getOrElse(name, Array.empty[String])
33+
do
34+
ScriptingDriver(
35+
compilerArgs = Array(
36+
"-classpath", TestConfiguration.basicClasspath),
37+
scriptFile = scriptFile,
38+
scriptArgs = scriptArgs
39+
).compileAndRun()

compiler/test/dotty/tools/utils.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package dotty.tools
2+
3+
import java.io.File
4+
import java.nio.charset.StandardCharsets.UTF_8
5+
6+
import scala.io.Source
7+
import scala.util.Using.resource
8+
9+
def scripts(path: String): Array[File] = {
10+
val dir = new File(getClass.getResource(path).getPath)
11+
assert(dir.exists && dir.isDirectory, "Couldn't load scripts dir")
12+
dir.listFiles
13+
}
14+
15+
private def withFile[T](file: File)(action: Source => T): T =
16+
resource(Source.fromFile(file, UTF_8.name))(action)
17+
18+
def readLines(f: File): List[String] = withFile(f)(_.getLines.toList)
19+
def readFile(f: File): String = withFile(f)(_.mkString)

dist/bin/scala

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ addDotcOptions () {
3838
source "$PROG_HOME/bin/common"
3939

4040
declare -a residual_args
41+
declare -a script_args
4142
execute_repl=false
4243
execute_run=false
44+
execute_script=false
4345
with_compiler=false
4446
class_path_count=0
4547
CLASS_PATH=""
@@ -79,13 +81,28 @@ while [[ $# -gt 0 ]]; do
7981
addDotcOptions "${1}"
8082
shift ;;
8183
*)
82-
residual_args+=("$1")
84+
if [ $execute_script == false ]; then
85+
if [[ "$1" == *.scala ]]; then
86+
execute_script=true
87+
target_script="$1"
88+
else
89+
residual_args+=("$1")
90+
fi
91+
else
92+
script_args+=("$1")
93+
fi
8394
shift
8495
;;
8596

8697
esac
8798
done
88-
if [ $execute_repl == true ] || ([ $execute_run == false ] && [ $options_indicator == 0 ]); then
99+
100+
if [ $execute_script == true ]; then
101+
if [ "$CLASS_PATH" ]; then
102+
cp_arg="-classpath \"$CLASS_PATH\""
103+
fi
104+
eval "\"$PROG_HOME/bin/scalac\" $cp_arg ${java_options[@]} ${residual_args[@]} -script $target_script ${script_args[@]}"
105+
elif [ $execute_repl == true ] || ([ $execute_run == false ] && [ $options_indicator == 0 ]); then
89106
if [ "$CLASS_PATH" ]; then
90107
cp_arg="-classpath \"$CLASS_PATH\""
91108
fi

dist/bin/scalac

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ withCompiler=true
3333
CompilerMain=dotty.tools.dotc.Main
3434
DecompilerMain=dotty.tools.dotc.decompiler.Main
3535
ReplMain=dotty.tools.repl.Main
36+
ScriptingMain=dotty.tools.scripting.Main
3637

3738
PROG_NAME=$CompilerMain
3839

@@ -45,6 +46,9 @@ addScala () {
4546
addResidual () {
4647
residual_args+=("'$1'")
4748
}
49+
addScripting () {
50+
scripting_args+=("'$1'")
51+
}
4852

4953
classpathArgs () {
5054
# echo "dotty-compiler: $DOTTY_COMP"
@@ -74,6 +78,7 @@ classpathArgs () {
7478
jvm_cp_args="-classpath \"$toolchain\""
7579
}
7680

81+
in_scripting_args=false
7782
while [[ $# -gt 0 ]]; do
7883
case "$1" in
7984
--) shift; for arg; do addResidual "$arg"; done; set -- ;;
@@ -85,6 +90,7 @@ case "$1" in
8590
# Optimize for short-running applications, see https://github.com/lampepfl/dotty/issues/222
8691
-Oshort) addJava "-XX:+TieredCompilation -XX:TieredStopAtLevel=1" && shift ;;
8792
-repl) PROG_NAME="$ReplMain" && shift ;;
93+
-script) PROG_NAME="$ScriptingMain" && target_script="$2" && in_scripting_args=true && shift && shift ;;
8894
-compile) PROG_NAME="$CompilerMain" && shift ;;
8995
-decompile) PROG_NAME="$DecompilerMain" && shift ;;
9096
-print-tasty) PROG_NAME="$DecompilerMain" && addScala "-print-tasty" && shift ;;
@@ -98,12 +104,22 @@ case "$1" in
98104
# will be available as system properties.
99105
-D*) addJava "$1" && shift ;;
100106
-J*) addJava "${1:2}" && shift ;;
101-
*) addResidual "$1" && shift ;;
107+
*) if [ $in_scripting_args == false ]; then
108+
addResidual "$1"
109+
else
110+
addScripting "$1"
111+
fi
112+
shift
113+
;;
102114
esac
103115
done
104116

105117
classpathArgs
106118

119+
if [ "$PROG_NAME" == "$ScriptingMain" ]; then
120+
scripting_string="-script $target_script ${scripting_args[@]}"
121+
fi
122+
107123
eval exec "\"$JAVACMD\"" \
108124
${JAVA_OPTS:-$default_java_opts} \
109125
"$DEBUG" \
@@ -112,5 +128,6 @@ eval exec "\"$JAVACMD\"" \
112128
-Dscala.usejavacp=true \
113129
"$PROG_NAME" \
114130
"${scala_args[@]}" \
115-
"${residual_args[@]}"
131+
"${residual_args[@]}" \
132+
"$scripting_string"
116133
exit $?

docs/docs/usage/getting-started.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,19 @@ In case you have already installed Dotty via brew, you should instead update it:
7474
```bash
7575
brew upgrade dotty
7676
```
77+
78+
### Scala 3 for Scripting
79+
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:
80+
81+
```scala
82+
@main def Test(name: String): Unit =
83+
println(s"Hello ${name}!")
84+
```
85+
86+
You can run: `scala Test.scala World` to get an output `Hello World!`.
87+
88+
A "script" is an ordinary Scala file which contains a main method. The semantics of the `scala Script.scala` command is as follows:
89+
90+
- Compile `Script.scala` with `scalac` into a temporary directory.
91+
- Detect the main method in the `*.class` files produced by the compilation.
92+
- Execute the main method.

staging/test/scala/quoted/staging/repl/StagingScriptedReplTests.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package scala.quoted.staging.repl
22

33
import dotty.BootstrappedOnlyTests
4+
import dotty.tools.scripts
45
import dotty.tools.repl.ReplTest
56
import dotty.tools.vulpix.TestConfiguration
67
import org.junit.Test

0 commit comments

Comments
 (0)