Skip to content

Commit dac1470

Browse files
authored
split, fix and cleanup predef and runBefore handling (#176)
Prior to 0.1.92 we concatenated and executed all predef code. That was very simple but had quite a few downsides, including incorrect line number reporting and some edge cases when handling complex compilation units, e.g. ordering of imports etc. #157 changed this behaviour to instead compile the given files and add the results to the classpath. At the same time we unintendedly broke the ability to execute code on startup of the repl - that's reintroduced as a new parameter `--runBefore` now.
1 parent c2ba41c commit dac1470

File tree

14 files changed

+145
-153
lines changed

14 files changed

+145
-153
lines changed

README.md

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,22 @@ srp
2020
Prerequisite: jdk11+
2121

2222
## TOC
23-
<!-- markdown-toc --maxdepth 3 README.md|tail -n +4 -->
2423
- [Benefits over / comparison with](#benefits-over--comparison-with)
2524
* [Regular Scala REPL](#regular-scala-repl)
2625
* [Ammonite](#ammonite)
2726
* [scala-cli](#scala-cli)
2827
- [Prerequisite for all of the below: run `sbt stage` or download the latest release](#prerequisite-for-all-of-the-below-run-sbt-stage-or-download-the-latest-release)
2928
- [REPL](#repl)
3029
* [run with defaults](#run-with-defaults)
31-
* [customize prompt, greeting and exit code](#customize-prompt-greeting-and-exit-code)
32-
* [execute some predef code](#execute-some-predef-code)
30+
* [execute code at the start with `--runBefore`](#execute-code-at-the-start-with---runbefore)
31+
* [`--predef`: code that is compiled but not executed](#--predef-code-that-is-compiled-but-not-executed)
3332
* [Operators: Redirect to file, pipe to external command](#operators-redirect-to-file-pipe-to-external-command)
3433
* [Add dependencies with maven coordinates](#add-dependencies-with-maven-coordinates)
3534
* [Importing additional script files interactively](#importing-additional-script-files-interactively)
3635
* [Adding classpath entries](#adding-classpath-entries)
3736
* [Rendering of output](#rendering-of-output)
3837
* [Exiting the REPL](#exiting-the-repl)
38+
* [customize prompt, greeting and exit code](#customize-prompt-greeting-and-exit-code)
3939
* [Looking up the current terminal width](#looking-up-the-current-terminal-width)
4040
- [Scripting](#scripting)
4141
* [Simple "Hello world" script](#simple-hello-world-script)
@@ -49,7 +49,6 @@ Prerequisite: jdk11+
4949
* [Attach a debugger (remote jvm debug)](#attach-a-debugger-remote-jvm-debug)
5050
- [Server mode](#server-mode)
5151
- [Embed into your own project](#embed-into-your-own-project)
52-
- [Global predef file: `~/.scala-repl-pp.sc`](#global-predef-file-scala-repl-ppsc)
5352
- [Verbose mode](#verbose-mode)
5453
- [Inherited classpath](#inherited-classpath)
5554
- [Parameters cheat sheet: the most important ones](#parameters-cheat-sheet-the-most-important-ones)
@@ -62,7 +61,8 @@ Prerequisite: jdk11+
6261
* [How can I get a new binary (bootstrapped) release?](#how-can-i-get-a-new-binary-bootstrapped-release)
6362
* [Updating the Scala version](#updating-the-scala-version)
6463
* [Updating the shaded libraries](#updating-the-shaded-libraries)
65-
- [Fineprint](#fineprint)
64+
- [Fineprint](#fineprint)
65+
6666

6767
## Benefits over / comparison with
6868

@@ -100,10 +100,28 @@ scala-cli wraps and invokes the regular Scala REPL (by default; or optionally Am
100100
./srp
101101
```
102102

103-
### customize prompt, greeting and exit code
104-
./srp --prompt myprompt --greeting 'hey there!' --onExitCode 'println("see ya!")'
103+
### execute code at the start with `--runBefore`
104+
```
105+
./srp --runBefore 'import Byte.MaxValue'
106+
107+
scala> MaxValue
108+
val res0: Int = 127
109+
```
105110

106-
### execute some predef code
111+
You can specify this parameter multiple times, the given statements will be executed in the given order.
112+
113+
If you want to execute some code _every single time_ you start a session, just write it to `~/.scala-repl-pp.sc`
114+
```
115+
echo 'import Short.MaxValue' > ~/.scala-repl-pp.sc
116+
117+
./srp
118+
119+
scala> MaxValue
120+
val res0: Int = 32767
121+
```
122+
123+
### `--predef`: add source files to the classpath
124+
Additional source files that are compiled added to the classpath, but unlike `runBefore` not executed straight away can be provided via `--predef`.
107125
```
108126
echo 'def foo = 42' > foo.sc
109127
@@ -112,6 +130,12 @@ scala> foo
112130
val res0: Int = 42
113131
```
114132

133+
You can specify this parameter multiple times (`--predef one.sc --predef two.sc`).
134+
135+
Why not use `runBefore` instead? For simple examples like the one above, you can do so. If it gets more complicated and you have multiple files referencing each others, `predef` allows you to treat it as one compilation unit, which isn't possible with `runBefore`. And as you add more code it's get's easier to manage in files rather than command line arguments.
136+
137+
Note that predef files may not contain toplevel statements like `println("foo")` - instead, these either belong into your main script (if you're executing one) and/or can be passed to the repl via `runBefore`.
138+
115139
### Operators: Redirect to file, pipe to external command
116140
Inspired by unix shell redirection and pipe operators (`>`, `>>` and `|`) you can redirect output into files with `#>` (overrides existing file) and `#>>` (create or append to file), and use `#|` to pipe the output to a command, such as `less`:
117141
```scala
@@ -274,6 +298,15 @@ $
274298
```
275299
Context: we'd prefer to cancel the long-running operation, but that's not so easy on the JVM.
276300

301+
### customize prompt, greeting and exit code
302+
```
303+
./srp --prompt myprompt --greeting 'hey there!' --onExitCode 'println("see ya!")'
304+
305+
hey there!
306+
myprompt> :exit
307+
see ya!
308+
```
309+
277310
### Looking up the current terminal width
278311
In case you want to adjust your output rendering to the available terminal size, you can look it up:
279312

@@ -487,26 +520,6 @@ stringcalc> add(One, Two)
487520
val res0: stringcalc.Number = Number(3)
488521
```
489522

490-
## Global predef file: `~/.scala-repl-pp.sc`
491-
Code that should be available across all srp sessions can be written into your local `~/.scala-repl-pp.sc`.
492-
493-
```
494-
echo 'def bar = 90' > ~/.scala-repl-pp.sc
495-
echo 'def baz = 91' > script1.sc
496-
echo 'def bam = 92' > script2.sc
497-
498-
./srp --predef script1.sc --predef script2.sc
499-
500-
scala> bar
501-
val res0: Int = 90
502-
503-
scala> baz
504-
val res1: Int = 91
505-
506-
scala> bam
507-
val res2: Int = 92
508-
```
509-
510523
## Verbose mode
511524
If verbose mode is enabled, you'll get additional information about classpaths and complete scripts etc.
512525
To enable it, you can either pass `--verbose` or set the environment variable `SCALA_REPL_PP_VERBOSE=true`.

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ lazy val core = project.in(file("core"))
2626
executableScriptName := "srp",
2727
libraryDependencies ++= Seq(
2828
"org.scala-lang" %% "scala3-compiler" % scalaVersion.value,
29-
"org.slf4j" % "slf4j-simple" % "2.0.13" % Optional,
29+
"org.slf4j" % "slf4j-simple" % "2.0.16" % Optional,
3030
),
3131
assemblyJarName := "srp.jar", // TODO remove the '.jar' suffix - when doing so, it doesn't work any longer
3232
)

core/src/main/scala/replpp/Config.scala

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import replpp.shaded.scopt.OParserBuilder
88
import java.nio.file.Path
99

1010
case class Config(
11-
predefFiles: Seq[Path] = Nil,
11+
predefFiles: Seq[Path] = Nil, // these files will be precompiled and added to the classpath
12+
runBefore: Seq[String] = Nil, // these statements will be interpreted on startup
1213
nocolors: Boolean = false,
1314
verbose: Boolean = false,
1415
classpathConfig: Config.ForClasspath = Config.ForClasspath(),
@@ -41,6 +42,10 @@ case class Config(
4142
add("--predef", predefFile.toString)
4243
}
4344

45+
runBefore.foreach { runBefore =>
46+
add("--runBefore", runBefore)
47+
}
48+
4449
if (nocolors) add("--nocolors")
4550
if (verbose) add("--verbose")
4651

@@ -103,6 +108,7 @@ object Config {
103108
OParser.sequence(
104109
programName("scala-repl-pp"),
105110
opts.predef((x, c) => c.copy(predefFiles = c.predefFiles :+ x)),
111+
opts.runBefore((x, c) => c.copy(runBefore = c.runBefore :+ x)),
106112
opts.nocolors((_, c) => c.copy(nocolors = true)),
107113
opts.verbose((_, c) => c.copy(verbose = true)),
108114
opts.classpathEntry((x, c) => c.copy(classpathConfig = c.classpathConfig.copy(additionalClasspathEntries = c.classpathConfig.additionalClasspathEntries :+ x))),
@@ -142,7 +148,16 @@ object Config {
142148
.unbounded()
143149
.optional()
144150
.action(action)
145-
.text("import additional script files on startup - may be passed multiple times")
151+
.text("given source files will be compiled and added to classpath - this may be passed multiple times")
152+
}
153+
154+
def runBefore[C](using builder: OParserBuilder[C])(action: Action[String, C]) = {
155+
builder.opt[String]("runBefore")
156+
.valueName("'val foo = 42'")
157+
.unbounded()
158+
.optional()
159+
.action(action)
160+
.text("given code will be executed on startup - this may be passed multiple times")
146161
}
147162

148163
def nocolors[C](using builder: OParserBuilder[C])(action: Action[Unit, C]) = {

core/src/main/scala/replpp/InteractiveShell.scala

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package replpp
22

33
import dotty.tools.repl.State
44

5+
import java.lang.System.lineSeparator
56
import scala.util.control.NoStackTrace
67

78
object InteractiveShell {
@@ -21,22 +22,22 @@ object InteractiveShell {
2122
)
2223

2324
val initialState: State = replDriver.initialState
24-
val predefCode = DefaultPredef
25+
val runBeforeLines = (DefaultRunBeforeLines ++ globalRunBeforeLines ++ config.runBefore).mkString(lineSeparator)
2526
val state: State = {
2627
if (verboseEnabled(config)) {
2728
println(s"compiler arguments: ${compilerArgs.mkString(",")}")
28-
println(predefCode)
29-
replDriver.run(predefCode)(using initialState)
29+
println(runBeforeLines)
30+
replDriver.run(runBeforeLines)(using initialState)
3031
} else {
31-
replDriver.runQuietly(predefCode)(using initialState)
32+
replDriver.runQuietly(runBeforeLines)(using initialState)
3233
}
3334
}
3435

35-
if (predefCode.nonEmpty && state.objectIndex != 1) {
36+
if (runBeforeLines.nonEmpty && state.objectIndex != 1) {
3637
throw new AssertionError(s"compilation error for predef code - error should have been reported above ^") with NoStackTrace
3738
}
3839

3940
replDriver.runUntilQuit(using state)()
4041
}
4142

42-
}
43+
}

core/src/main/scala/replpp/UsingDirectives.scala

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,6 @@ object UsingDirectives {
2525
def findImportedFiles(lines: IterableOnce[String], rootPath: Path): Seq[Path] =
2626
scanFor(FileDirective, lines).iterator.map(resolveFile(rootPath, _)).toSeq
2727

28-
def findImportedFilesRecursively(lines: IterableOnce[String], rootPath: Path): Seq[Path] = {
29-
val results = Seq.newBuilder[Path]
30-
val visited = mutable.Set.empty[Path]
31-
32-
findImportedFiles(lines, rootPath).foreach { file =>
33-
results += file
34-
visited += file
35-
36-
val recursiveFiles = findImportedFilesRecursively(file, visited.toSet)
37-
results ++= recursiveFiles
38-
visited ++= recursiveFiles
39-
}
40-
41-
results.result().distinct
42-
}
43-
4428
def findDeclaredDependencies(lines: IterableOnce[String]): Seq[String] =
4529
scanFor(LibDirective, lines)
4630

core/src/main/scala/replpp/package.scala

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import replpp.util.{ClasspathHelper, SimpleDriver, linesFromFile}
22

3-
import java.lang.System.lineSeparator
43
import java.nio.file.{Files, Path, Paths}
54
import scala.collection.mutable
65

@@ -9,22 +8,21 @@ package object replpp {
98
val VerboseEnvVar = "SCALA_REPL_PP_VERBOSE"
109
lazy val pwd: Path = Paths.get(".").toAbsolutePath
1110
lazy val home: Path = Paths.get(System.getProperty("user.home"))
12-
lazy val globalPredefFile = home.resolve(".scala-repl-pp.sc")
13-
lazy val globalPredefFileMaybe = Option(globalPredefFile).filter(Files.exists(_))
11+
lazy val globalRunBeforeFile: Path = home.resolve(".scala-repl-pp.sc")
12+
lazy val globalRunBeforeFileMaybe: Option[Path] = Option(globalRunBeforeFile).filter(Files.exists(_))
13+
lazy val globalRunBeforeLines: Seq[String] = globalRunBeforeFileMaybe.map(linesFromFile).getOrElse(Seq.empty)
1414

15-
private[replpp] def DefaultPredefLines(using colors: Colors) = {
15+
private[replpp] def DefaultRunBeforeLines(using colors: Colors) = {
1616
val colorsImport = colors match {
1717
case Colors.BlackWhite => "replpp.Colors.BlackWhite"
1818
case Colors.Default => "replpp.Colors.Default"
1919
}
2020
Seq(
2121
"import replpp.Operators.*",
22-
s"given replpp.Colors = $colorsImport"
22+
s"given replpp.Colors = $colorsImport",
2323
)
2424
}
2525

26-
private[replpp] def DefaultPredef(using Colors) = DefaultPredefLines.mkString(lineSeparator)
27-
2826
/** verbose mode can either be enabled via the config, or the environment variable `SCALA_REPL_PP_VERBOSE=true` */
2927
def verboseEnabled(config: Config): Boolean = {
3028
config.verbose ||
@@ -44,14 +42,13 @@ package object replpp {
4442
/** recursively find all relevant source files from main script, global predef file,
4543
* provided predef files, other scripts that were imported with `using file` directive */
4644
def allSourceFiles(config: Config): Seq[Path] =
47-
(allPredefFiles(config) ++ config.scriptFile).distinct.sorted
45+
(allPredefFiles(config) ++ config.scriptFile ++ globalRunBeforeFileMaybe).distinct.sorted
4846

4947
def allPredefFiles(config: Config): Seq[Path] = {
5048
val allPredefFiles = mutable.Set.empty[Path]
5149
allPredefFiles ++= config.predefFiles
52-
globalPredefFileMaybe.foreach(allPredefFiles.addOne)
5350

54-
// the directly resolved predef files might reference additional files via `using` directive
51+
// the directly referenced predef files might reference additional files via `using` directive
5552
val predefFilesDirect = allPredefFiles.toSet
5653
predefFilesDirect.foreach { predefFile =>
5754
val importedFiles = UsingDirectives.findImportedFilesRecursively(predefFile, visited = allPredefFiles.toSet)
@@ -64,19 +61,18 @@ package object replpp {
6461
allPredefFiles ++= importedFiles
6562
}
6663

67-
allPredefFiles.toSeq.sorted
64+
allPredefFiles.toSeq.filter(Files.exists(_)).sorted
6865
}
6966

7067
def allSourceLines(config: Config): Seq[String] =
71-
allSourceFiles(config).flatMap(linesFromFile)
68+
allSourceFiles(config).flatMap(linesFromFile) ++ config.runBefore
7269

7370
/** precompile given predef files (if any) and update Config to include the results in the classpath */
7471
def precompilePredefFiles(config: Config): Config = {
75-
val allPredefFiles = (config.predefFiles :+ globalPredefFile).filter(Files.exists(_))
76-
if (allPredefFiles.nonEmpty) {
72+
if (config.predefFiles.nonEmpty) {
7773
val predefClassfilesDir = new SimpleDriver().compileAndGetOutputDir(
7874
replpp.compilerArgs(config),
79-
inputFiles = allPredefFiles,
75+
inputFiles = allPredefFiles(config),
8076
verbose = config.verbose
8177
).get
8278
config.withAdditionalClasspathEntry(predefClassfilesDir)

core/src/main/scala/replpp/scripting/NonForkingScriptRunner.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package replpp.scripting
22

3-
import replpp.{Config, allPredefFiles, allSourceFiles}
3+
import replpp.{Config, allPredefFiles}
44

55
import java.nio.file.Files
66
import scala.util.{Failure, Success}
@@ -36,6 +36,9 @@ object NonForkingScriptRunner {
3636
commandArgs ++ parameterArgs
3737
}
3838

39+
if (config.runBefore.nonEmpty)
40+
println(s"[WARNING] ScriptingDriver does not support `runBefore` code, the given ${config.runBefore.size} statements will be ignored")
41+
3942
val verboseEnabled = replpp.verboseEnabled(config)
4043
new ScriptingDriver(
4144
compilerArgs = replpp.compilerArgs(config) :+ "-nowarn",
@@ -52,4 +55,4 @@ object NonForkingScriptRunner {
5255
}
5356
}
5457

55-
}
58+
}

core/src/main/scala/replpp/scripting/ScriptingDriver.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import scala.util.{Failure, Try}
1717
* Runs a given script on the current JVM.
1818
*
1919
* Similar to dotty.tools.scripting.ScriptingDriver, but simpler and faster.
20-
* Main difference: we don't (need to) recursively look for main method entrypoints in the entire classpath,
20+
* Main difference: we don't (need to) recursively look for main method entry points in the entire classpath,
2121
* because we have a fixed class and method name that ScriptRunner uses when it embeds the script and predef code.
2222
* */
2323
class ScriptingDriver(compilerArgs: Array[String], predefFiles: Seq[Path], scriptFile: Path, scriptArgs: Array[String], verbose: Boolean) {

core/src/test/scala/replpp/ConfigTests.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class ConfigTests extends AnyWordSpec with Matchers {
1919
"asJavaArgs (inverse of Config.parse)" in {
2020
val config = Config(
2121
predefFiles = List(Paths.get("/some/path/predefFile1"), Paths.get("/some/path/predefFile2")),
22+
runBefore = List("val foo = 42", "println(foo)"),
2223
nocolors = true,
2324
verbose = true,
2425
classpathConfig = Config.ForClasspath(
@@ -39,6 +40,8 @@ class ConfigTests extends AnyWordSpec with Matchers {
3940
javaArgs shouldBe Seq(
4041
"--predef", Paths.get("/some/path/predefFile1").toString,
4142
"--predef", Paths.get("/some/path/predefFile2").toString,
43+
"--runBefore", "val foo = 42",
44+
"--runBefore", "println(foo)",
4245
"--nocolors",
4346
"--verbose",
4447
"--classpathEntry", "cp1",

0 commit comments

Comments
 (0)