-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Refactor settings & improve dx #19766
Conversation
name: String, | ||
description: String, | ||
default: T, | ||
helpArg: String = "", | ||
choices: Option[Seq[?]] = None, | ||
prefix: String = "", | ||
prefix: Option[String] = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the difference between Some("") and None?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the prefix of a setting exists, then this setting behaves differently; it is a different type of setting. API users can choose not to provide this prefix, thus not using this feature.
Sole String
does not express the property of being optional. String would mean that it either has an empty prefix (so that would include all possible settings or some other feature) or that there is no prefix. Option type makes it clear that the prefix can be not present. It is similar to the conflict of None
vsSome(null)
. Ideally, we'd like an Option[NonEmptyString] there. But now, Some("") just is the most general prefix that includes all options.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't follow you. Can you give 3 reasonable examples, with None, Some(""), and Some("-Whatever")? Might be easier to follow your reasoning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally in the compiler we prefer sentinels over Option
. That's the adopted code style. Option
can be useful sometimes, but as Dale says we should not reach for it blindly.
27d7ff6
to
b4c918d
Compare
@@ -23,7 +23,20 @@ object ScalaSettingCategories: | |||
val AdvancedSetting = "X" | |||
val VerboseSetting = "V" | |||
|
|||
object ScalaSettings: | |||
type ScalaSettings = ScalaSettings.type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't want to have .type
everywhere ScalaSettings are stored, I'd rather have it as a type. But it kind of gives a false impression of being instantiable and is not a widely used pattern here. And is not really necessary in the end, we don't need an instance of it as val/field, as it's now immutable. But it's still a draft, a final shape will be there when I convert it to a full PR.
@@ -861,7 +861,7 @@ object Contexts { | |||
with Phases.PhasesBase | |||
with Plugins { | |||
|
|||
val settings: ScalaSettings = ScalaSettings | |||
val settings: ScalaSettings.type = ScalaSettings |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that this is a singleton we could use ScalaSettings
instead of settings
. Not sure what this val gives us. Same for def settings: ScalaSettings.type = base.settings
.
I know that would imply a massive refactoring of all ctx.settings.XYZ.value
to ScalaSettings.XYZ.value
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know that would imply a massive refactoring of all ctx.settings.XYZ.value to ScalaSettings.XYZ.value.
That's the reason. There is no cost to keeping it a setting right now, and the PR is getting quite big (a change in params parsing is coming here too). This refactor will be done in next PR, so we don't block the ScalaCLI integration with it.
@szymon-rd could you add a PR description (what changes for users, in the settings API, what is being fixed, etc)? |
06ae7db
to
c0bd89a
Compare
@lrytz I will write detailed description before making it |
Thanks, what you added is already really helpful! |
Nice description, @szymon-rd ! |
04c0976
to
7994d02
Compare
4d613f4
to
f5a6376
Compare
TODO: Run full open CB on this |
Some projects in CB failed. After investigation, I found the cause: Some macro libraries, especially Enumeratum, use ScalaSettings directly from the compiler. I think we should be able to have a ScalaSettings trait & companion object implementing it and have it fix it. |
e751d5a
to
9b4db52
Compare
|
||
object BackendUtils { | ||
lazy val classfileVersionMap = Map( | ||
"8" -> asm.Opcodes.V1_8, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might transform this into a Map[Int, ...]
. This way we would simplify classfileVersionMap.keysIterator.map(_.toInt).min
to classfileVersionMap.keysIterator.min
.
object ScalaSettingCategories: | ||
val RootSetting = "" | ||
val WarningSetting = "W" | ||
val ForkSetting = "Y" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would add some documentation to all these settings.
val WarningSetting = "W" | ||
val ForkSetting = "Y" | ||
val AdvancedSetting = "X" | ||
val VerboseSetting = "V" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be an enumeration. It seems too easy to pass an arbitrary string to the category
of BooleanSetting
(or similar method).
|
||
import scala.util.chaining.* | ||
|
||
import java.util.zip.Deflater | ||
|
||
class ScalaSettings extends SettingGroup with AllScalaSettings | ||
object ScalaSettingCategories: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This concept deserves its own file.
|
||
// Kept as seperate type to avoid breaking backward compatibility | ||
abstract class ScalaSettings extends SettingGroup, AllScalaSettings: | ||
val settingsByCategory: Map[String, List[Setting[_]]] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
val settingsByCategory: Map[String, List[Setting[_]]] = | |
private val settingsByCategory: Map[String, List[Setting[_]]] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kept it intentionally as public, It may be useful in cli tooling
allSettings.groupBy(_.category) | ||
.view.mapValues(_.toList).toMap | ||
.withDefaultValue(Nil) | ||
def categories: List[String] = settingsByCategory.keys.toList |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The order of keys is not deterministic, but it should probably be deterministic.
def BooleanSetting(name: String, descr: String, initialValue: Boolean = false, aliases: List[String] = Nil): Setting[Boolean] = | ||
publish(Setting(name, descr, initialValue, aliases = aliases)) | ||
@unshared | ||
val settingCharacters = "[a-zA-Z0-9_\\-]*".r |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would accept the empty name and names that start with _
or -
. Maybe the regex should be [a-zA-Z][a-zA-Z0-9_\\-]*
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would also accept a name that starts with X
, Y
, V
, .... Maybe we the regex can be [a-z][a-zA-Z0-9_\\-]*
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kept the prefix in the name to avoid too much magic (people will likely search for e.g. "Wunused" in the ScalaSettings.scala file)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as for the first comment, aliases are also validated, and they are kept as they were for now for back compat. They contain one or two -
in prefix. In general, whether a setting starts with - or has an appropriate prefix is validated in the Setting itself. I will put what I can in the Setting class.
"-" + validateSetting(name) | ||
|
||
def BooleanSetting(category: String, name: String, descr: String, initialValue: Boolean = false, aliases: List[String] = Nil): Setting[Boolean] = | ||
publish(Setting(category, validateAndPrependName(name), descr, initialValue, aliases = aliases.map(validateSetting))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be simpler to pass the name
as is and validate it in Setting
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It prepends the "-" for backward compatibility, so the names don't change after this PR. But we don't pass "-" anymore in the config. Little magic, but seems to be optimal out of all possibilities. I will put what I can in the Setting class.
2dd4408
to
f46eb74
Compare
f146ca7
to
73d57ac
Compare
73d57ac
to
2b3c05a
Compare
Please do not merge yet, I am still validating community builds. |
There seems to be a last problem: Some project in CB B failed with |
thorough writeup in PR description much appreciated 👍 |
Conflict between scala#18783 and scala#19766
This PR aims to:
-W
("-Wall") (2)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 thisScalaSettings
instance, the settings did not expose API with the required access paths: (provided by ScalaCLI team):
or space?)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:
These problems include:
--
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
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. NowSetting
describes the structure of settings, and the whole state is represented bySettingsState
. 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
--
or-
. There are some exceptions that come from aliases kept for backward compatibility.[a-zA-Z0-9_\\-]*
-
at the beginning and contains category prefix (likeW
):
.-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:String
parameters that accept a set ofchoice
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 passMain.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.-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.