Skip to content

Commit

Permalink
Refactor settings & improve dx (#19766)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
szymon-rd authored Mar 18, 2024
2 parents 943efdc + 9a16617 commit 397da20
Show file tree
Hide file tree
Showing 18 changed files with 526 additions and 422 deletions.
39 changes: 21 additions & 18 deletions compiler/src/dotty/tools/backend/jvm/BackendUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,7 @@ class BackendUtils(val postProcessor: PostProcessor) {
import bTypes.*
import coreBTypes.jliLambdaMetaFactoryAltMetafactoryHandle

// Keep synchronized with `minTargetVersion` and `maxTargetVersion` in ScalaSettings
lazy val classfileVersion: Int = compilerSettings.target match {
case "8" => asm.Opcodes.V1_8
case "9" => asm.Opcodes.V9
case "10" => asm.Opcodes.V10
case "11" => asm.Opcodes.V11
case "12" => asm.Opcodes.V12
case "13" => asm.Opcodes.V13
case "14" => asm.Opcodes.V14
case "15" => asm.Opcodes.V15
case "16" => asm.Opcodes.V16
case "17" => asm.Opcodes.V17
case "18" => asm.Opcodes.V18
case "19" => asm.Opcodes.V19
case "20" => asm.Opcodes.V20
case "21" => asm.Opcodes.V21
case "22" => asm.Opcodes.V22
}
lazy val classfileVersion: Int = BackendUtils.classfileVersionMap(compilerSettings.target.toInt)

lazy val extraProc: Int = {
import GenBCodeOps.addFlagIf
Expand Down Expand Up @@ -184,3 +167,23 @@ class BackendUtils(val postProcessor: PostProcessor) {
}
}
}

object BackendUtils {
lazy val classfileVersionMap: Map[Int, Int] = Map(
8 -> asm.Opcodes.V1_8,
9 -> asm.Opcodes.V9,
10 -> asm.Opcodes.V10,
11 -> asm.Opcodes.V11,
12 -> asm.Opcodes.V12,
13 -> asm.Opcodes.V13,
14 -> asm.Opcodes.V14,
15 -> asm.Opcodes.V15,
16 -> asm.Opcodes.V16,
17 -> asm.Opcodes.V17,
18 -> asm.Opcodes.V18,
19 -> asm.Opcodes.V19,
20 -> asm.Opcodes.V20,
21 -> asm.Opcodes.V21,
22 -> asm.Opcodes.V22,
)
}
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/config/CliCommand.scala
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ trait CliCommand:
protected def isVerbose(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =
s.name.startsWith("-V") && s.name != "-V"
protected def isWarning(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =
s.name.startsWith("-W") && s.name != "-W" || s.name == "-Xlint"
s.name.startsWith("-W") && s.name != "-W"
protected def isAdvanced(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =
s.name.startsWith("-X") && s.name != "-X"
protected def isPrivate(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/config/CompilerCommand.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import core.Contexts.*
abstract class CompilerCommand extends CliCommand:
type ConcreteSettings = ScalaSettings

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

final def isHelpFlag(using settings: ScalaSettings)(using SettingsState): Boolean =
final def isHelpFlag(using settings: ConcreteSettings)(using SettingsState): Boolean =
import settings.*
val flags = Set(help, Vhelp, Whelp, Xhelp, Yhelp, showPlugins, XshowPhases)
flags.exists(_.value) || allSettings.exists(isHelping)
485 changes: 242 additions & 243 deletions compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions compiler/src/dotty/tools/dotc/config/ScalaSettingsProperties.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package dotty.tools.dotc
package config

import dotty.tools.backend.jvm.BackendUtils.classfileVersionMap
import dotty.tools.io.{AbstractFile, Directory, JDK9Reflectors, PlainDirectory, NoAbstractFile}
import scala.language.unsafeNulls

object ScalaSettingsProperties:

private lazy val minTargetVersion = classfileVersionMap.keysIterator.min
private lazy val maxTargetVersion = classfileVersionMap.keysIterator.max

def supportedTargetVersions: List[String] =
(minTargetVersion to maxTargetVersion).toList.map(_.toString)

def supportedReleaseVersions: List[String] =
if scala.util.Properties.isJavaAtLeast("9") then
val jdkVersion = JDK9Reflectors.runtimeVersionMajor(JDK9Reflectors.runtimeVersion()).intValue()
val maxVersion = Math.min(jdkVersion, maxTargetVersion)
(minTargetVersion to maxVersion).toList.map(_.toString)
else List(minTargetVersion).map(_.toString)

def supportedScalaReleaseVersions: List[String] =
ScalaRelease.values.toList.map(_.show)

def supportedSourceVersions: List[String] =
SourceVersion.values.toList.map(_.toString)

def defaultClasspath: String = sys.env.getOrElse("CLASSPATH", ".")

def defaultPageWidth: Int = {
val defaultWidth = 80
val columnsVar = System.getenv("COLUMNS")
if columnsVar != null then columnsVar.toInt
else if Properties.isWin then
val ansiconVar = System.getenv("ANSICON") // eg. "142x32766 (142x26)"
if ansiconVar != null && ansiconVar.matches("[0-9]+x.*") then
ansiconVar.substring(0, ansiconVar.indexOf("x")).toInt
else defaultWidth
else defaultWidth
}
Loading

0 comments on commit 397da20

Please sign in to comment.