Skip to content

Commit 397da20

Browse files
authored
Refactor settings & improve dx (#19766)
This PR aims to: - Expose Settings for ScalaCLI so it does not need to instantiate Context and can discover settings & categories. (1) - Get rid of mutable state within Settings, so we can create an immutable DSL for implementing options like `-W` ("-Wall") (2) - Standardise parsing & structure of the settings. (3) - Refactor the settings to prevent problems in the future. (4) ## Goals Here is the additional context for each of the goals: ### (1) Exposing Settings ScalaSettings was a class that was instantiated for each compiler context. Each time it was the same - the state was (almost) separated from it and put in `SettingsState`. Due to it being a class, tooling needed an artificial compilation context to access the fields. Even after acquiring this `ScalaSettings` instance, the settings did not expose API with the required access paths: (provided by ScalaCLI team) - List of all keys accepted by the Compiler - List of categories prefixes (-V, -W, etc.) - List of keys not requiring source inputs - List of alises for keys / access to keys via aliases - Way to determine how a setting accepts options (after `:` or space?) - Way to see if settings are a flag. - Way to see if a setting can be provided multiple times. Additionally, the ScalaSettings were the same each time and represented a static structure - it would lower complexity & limit potential problems if made an immutable object instead. ### (2) Mutable state Settings still had a piece of mutable state inside of them - it was used to store a flag denoting whether the setting was set repeatedly. It could be stored in SettingsState instead, allowing an immutable DSL on Settings. ### (3) Standarization Settings contain some behaviors that may be hard to get right without preexisting knowledge. They do not have a standard enforced upon them. That causes two problems: - It may be misleading to users & worsen the DX - It was reported as a cause of problems in integration with ScalaCLI and may cause similar problems in the future for other tools These problems include: - Some options support only passing parameters after a semicolon, and some allow passing them after space: Strings/Ints allow after space, VersionTags/Files/Lists do not, etc, and there's no way to know which is which without trying it out - Some have `--` aliases, some don't ### (4) Refactor Mutable state in Settings, various ways of handling Setting values (like `-Ykind-projector`), no validation of settings names/states, no way of enforcing categories in settings were all problems that caused a handful of constructs that made it harder to work with settings. I recognized that it was a good moment to refactor the code & enforce some rules and patterns. ## Solutions ### (1) Exposing Settings - Made ScalaSettings an immutable public object (made Setting immutable alongside) - Added methods allowing access to required data - Added categories to Setting and made prefix Optional for discoverability The requirement of checking if provided options run help instead of requiring source can be done by passing now immutable ScalaSettings and SettingsState to ScalacCommand `isHelpArg` ### (2) Mutable state `changed` field was moved to SettingsState as a Set instead of storing it as a mutable field in the Setting. Now `Setting` describes the structure of settings, and the whole state is represented by `SettingsState`. The plan is to use this property for a similar goal as the "post set hooks" in Scala 2 (aside from exposing the API) ### (3) Standarization - All options can now be passed with either `--` or `-`. There are some exceptions that come from aliases kept for backward compatibility. - Setting names are now validated against simple regex `[a-zA-Z0-9_\\-]*` - Setting names are now validated so they do not contain `-` at the beginning and contains category prefix (like `W`) - All arguments can now be passed after space or `:`. `-Ykind-projector` is an exception that was kept for backward compatibility. Set of additional rules was introduced for the settings to make sure that nothing breaks and compatibility is kept: - Empty strings are not supported as choice arguments. They were not supported for any other parameter type, only for `String` parameters that accept a set of `choice` values. If an empty choice setting was allowed, passing no value would result in empty value being chosen. Example: `-Ykind-projector` accepts parameters: `-Ykind-projector:underscores`. But if it was passed as just `-Ykind-projector`, then the value would be set to an empty string. This feature made it hard to keep backward compatibility after making it possible to pass arguments for every type of setting after a space: One could write `-Ykind-projector Main.scala` and mistakenly pass `Main.scala` as an arg for the setting. Luckily, `Ykind-projector` is the only setting that used this feature and is kept as legacy, no new settings with this behavior are allowed. - List settings cannot accept ignore invalid args. Luckily, no setting was doing that. If possible, an existing command that works but produces a warning like `-foo Main.scala` (for`-foo` accepting list of string args) would stop working. String commands did accept passing arguments after space before, but this is a new behavior for the list settings. If ignoring invalid args was possible, commands like `-foo Main.scala` could work unexpectedly. ### (4) Refactor It's mostly visible in the sources. For example, there is no need to change `classfileVersion` in two places every time. ### Additional changes -Xlint was deprecated in favor of -Wshadow, as it was only used for shadow warnings in the current shape. It's TBD, but from my perspective, there is no point in keeping some warnings as args for -Xlint, and some as separate -W settings.
2 parents 943efdc + 9a16617 commit 397da20

File tree

18 files changed

+526
-422
lines changed

18 files changed

+526
-422
lines changed

compiler/src/dotty/tools/backend/jvm/BackendUtils.scala

+21-18
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,7 @@ class BackendUtils(val postProcessor: PostProcessor) {
2020
import bTypes.*
2121
import coreBTypes.jliLambdaMetaFactoryAltMetafactoryHandle
2222

23-
// Keep synchronized with `minTargetVersion` and `maxTargetVersion` in ScalaSettings
24-
lazy val classfileVersion: Int = compilerSettings.target match {
25-
case "8" => asm.Opcodes.V1_8
26-
case "9" => asm.Opcodes.V9
27-
case "10" => asm.Opcodes.V10
28-
case "11" => asm.Opcodes.V11
29-
case "12" => asm.Opcodes.V12
30-
case "13" => asm.Opcodes.V13
31-
case "14" => asm.Opcodes.V14
32-
case "15" => asm.Opcodes.V15
33-
case "16" => asm.Opcodes.V16
34-
case "17" => asm.Opcodes.V17
35-
case "18" => asm.Opcodes.V18
36-
case "19" => asm.Opcodes.V19
37-
case "20" => asm.Opcodes.V20
38-
case "21" => asm.Opcodes.V21
39-
case "22" => asm.Opcodes.V22
40-
}
23+
lazy val classfileVersion: Int = BackendUtils.classfileVersionMap(compilerSettings.target.toInt)
4124

4225
lazy val extraProc: Int = {
4326
import GenBCodeOps.addFlagIf
@@ -184,3 +167,23 @@ class BackendUtils(val postProcessor: PostProcessor) {
184167
}
185168
}
186169
}
170+
171+
object BackendUtils {
172+
lazy val classfileVersionMap: Map[Int, Int] = Map(
173+
8 -> asm.Opcodes.V1_8,
174+
9 -> asm.Opcodes.V9,
175+
10 -> asm.Opcodes.V10,
176+
11 -> asm.Opcodes.V11,
177+
12 -> asm.Opcodes.V12,
178+
13 -> asm.Opcodes.V13,
179+
14 -> asm.Opcodes.V14,
180+
15 -> asm.Opcodes.V15,
181+
16 -> asm.Opcodes.V16,
182+
17 -> asm.Opcodes.V17,
183+
18 -> asm.Opcodes.V18,
184+
19 -> asm.Opcodes.V19,
185+
20 -> asm.Opcodes.V20,
186+
21 -> asm.Opcodes.V21,
187+
22 -> asm.Opcodes.V22,
188+
)
189+
}

compiler/src/dotty/tools/dotc/config/CliCommand.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ trait CliCommand:
8686
protected def isVerbose(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =
8787
s.name.startsWith("-V") && s.name != "-V"
8888
protected def isWarning(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =
89-
s.name.startsWith("-W") && s.name != "-W" || s.name == "-Xlint"
89+
s.name.startsWith("-W") && s.name != "-W"
9090
protected def isAdvanced(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =
9191
s.name.startsWith("-X") && s.name != "-X"
9292
protected def isPrivate(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =

compiler/src/dotty/tools/dotc/config/CompilerCommand.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import core.Contexts.*
77
abstract class CompilerCommand extends CliCommand:
88
type ConcreteSettings = ScalaSettings
99

10-
final def helpMsg(using settings: ScalaSettings)(using SettingsState, Context): String =
10+
final def helpMsg(using settings: ConcreteSettings)(using SettingsState, Context): String =
1111
settings.allSettings.find(isHelping) match
1212
case Some(s) => s.description
1313
case _ =>
@@ -20,7 +20,7 @@ abstract class CompilerCommand extends CliCommand:
2020
else if (settings.XshowPhases.value) phasesMessage
2121
else ""
2222

23-
final def isHelpFlag(using settings: ScalaSettings)(using SettingsState): Boolean =
23+
final def isHelpFlag(using settings: ConcreteSettings)(using SettingsState): Boolean =
2424
import settings.*
2525
val flags = Set(help, Vhelp, Whelp, Xhelp, Yhelp, showPlugins, XshowPhases)
2626
flags.exists(_.value) || allSettings.exists(isHelping)

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

+242-243
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package dotty.tools.dotc
2+
package config
3+
4+
import dotty.tools.backend.jvm.BackendUtils.classfileVersionMap
5+
import dotty.tools.io.{AbstractFile, Directory, JDK9Reflectors, PlainDirectory, NoAbstractFile}
6+
import scala.language.unsafeNulls
7+
8+
object ScalaSettingsProperties:
9+
10+
private lazy val minTargetVersion = classfileVersionMap.keysIterator.min
11+
private lazy val maxTargetVersion = classfileVersionMap.keysIterator.max
12+
13+
def supportedTargetVersions: List[String] =
14+
(minTargetVersion to maxTargetVersion).toList.map(_.toString)
15+
16+
def supportedReleaseVersions: List[String] =
17+
if scala.util.Properties.isJavaAtLeast("9") then
18+
val jdkVersion = JDK9Reflectors.runtimeVersionMajor(JDK9Reflectors.runtimeVersion()).intValue()
19+
val maxVersion = Math.min(jdkVersion, maxTargetVersion)
20+
(minTargetVersion to maxVersion).toList.map(_.toString)
21+
else List(minTargetVersion).map(_.toString)
22+
23+
def supportedScalaReleaseVersions: List[String] =
24+
ScalaRelease.values.toList.map(_.show)
25+
26+
def supportedSourceVersions: List[String] =
27+
SourceVersion.values.toList.map(_.toString)
28+
29+
def defaultClasspath: String = sys.env.getOrElse("CLASSPATH", ".")
30+
31+
def defaultPageWidth: Int = {
32+
val defaultWidth = 80
33+
val columnsVar = System.getenv("COLUMNS")
34+
if columnsVar != null then columnsVar.toInt
35+
else if Properties.isWin then
36+
val ansiconVar = System.getenv("ANSICON") // eg. "142x32766 (142x26)"
37+
if ansiconVar != null && ansiconVar.matches("[0-9]+x.*") then
38+
ansiconVar.substring(0, ansiconVar.indexOf("x")).toInt
39+
else defaultWidth
40+
else defaultWidth
41+
}

0 commit comments

Comments
 (0)