Skip to content
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

Make -release more useful, deprecate -target, align with Scala 3 #9982

Merged
merged 4 commits into from Jun 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -186,7 +186,7 @@ object PostProcessorFrontendAccess {

@inline def debug: Boolean = s.isDebug

val target: String = s.target.value
val target: String = s.targetValue

private val singleOutDir = s.outputDirs.getSingleOutput
// the call to `outputDirFor` should be frontendSynch'd, but we assume that the setting is not mutated during the backend
Expand Down
26 changes: 8 additions & 18 deletions src/compiler/scala/tools/nsc/settings/AbsSettings.scala
Expand Up @@ -20,7 +20,7 @@ package settings

trait AbsSettings extends scala.reflect.internal.settings.AbsSettings {
type Setting <: AbsSetting // Fix to the concrete Setting type
type ResultOfTryToSet // List[String] in mutable, (Settings, List[String]) in immutable
type ResultOfTryToSet // List[String] in MutableSettings
def errorFn: String => Unit
protected def allSettings: scala.collection.Map[String, Setting]

Expand Down Expand Up @@ -66,14 +66,6 @@ trait AbsSettings extends scala.reflect.internal.settings.AbsSettings {
/* For tools which need to populate lists of available choices */
def choices : List[String] = Nil

/** In mutable Settings, these return the same object with a var set.
* In immutable, of course they will return a new object, which means
* we can't use "this.type", at least not in a non-casty manner, which
* is unfortunate because we lose type information without it.
*
* ...but now they're this.type because of scala/bug#3462. The immutable
* side doesn't exist yet anyway.
*/
def withAbbreviation(name: String): this.type
def withHelpSyntax(help: String): this.type
def withDeprecationMessage(msg: String): this.type
Expand Down Expand Up @@ -101,19 +93,17 @@ trait AbsSettings extends scala.reflect.internal.settings.AbsSettings {
/** The help message to be printed if [[isHelping]]. */
def help: String = ""

/** After correct Setting has been selected, tryToSet is called with the
* remainder of the command line. It consumes any applicable arguments and
* returns the unconsumed ones.
/** Setting is presented the remaining command line arguments.
* It should consume any applicable args and return the rest,
* or `None` on error.
*/
protected[nsc] def tryToSet(args: List[String]): Option[ResultOfTryToSet]

/** Commands which can take lists of arguments in form -Xfoo:bar,baz override
* this method and accept them as a list. It returns List[String] for
* consistency with tryToSet, and should return its incoming arguments
* unmodified on failure, and Nil on success.
/** Setting is presented arguments in form -Xfoo:bar,baz.
* It should consume all the arguments and return an empty list,
* or `None` on error. Unconsumed args may error.
*/
protected[nsc] def tryToSetColon(args: List[String]): Option[ResultOfTryToSet] =
errorAndValue(s"'$name' does not accept multiple arguments", None)
protected[nsc] def tryToSetColon(args: List[String]): Option[ResultOfTryToSet]

/** Attempt to set from a properties file style property value.
* Currently used by Eclipse SDT only.
Expand Down
42 changes: 30 additions & 12 deletions src/compiler/scala/tools/nsc/settings/MutableSettings.scala
Expand Up @@ -398,11 +398,19 @@ class MutableSettings(val errorFn: String => Unit, val pathFactory: PathFactory)

def errorMsg() = errorFn(s"invalid setting for $name $getValidText")

def tryToSet(args: List[String]) =
if (args.isEmpty) errorAndValue("missing argument", None)
else parseArgument(args.head) match {
case Some(i) => value = i ; Some(args.tail)
case None => errorMsg() ; None
def tryToSet(args: List[String]): Option[ResultOfTryToSet] =
args match {
case h :: rest =>
parseArgument(h) match {
case Some(i) => value = i; Some(rest)
case None => errorMsg(); None
}
case Nil => errorAndValue("missing argument", None)
}
def tryToSetColon(args: List[String]): Option[ResultOfTryToSet] =
args match {
case Nil | _ :: Nil => tryToSet(args)
case _ => errorAndValue("too many arguments", None)
}

def unparse: List[String] =
Expand Down Expand Up @@ -452,6 +460,7 @@ class MutableSettings(val errorFn: String => Unit, val pathFactory: PathFactory)
case _ =>
None
}
def tryToSetColon(args: List[String]): Option[ResultOfTryToSet] = errorAndValue(s"bad argument for $name", None)
override def respondsTo(token: String) = token startsWith prefix
def unparse: List[String] = value
}
Expand All @@ -475,6 +484,11 @@ class MutableSettings(val errorFn: String => Unit, val pathFactory: PathFactory)
case "help" :: rest if helpText.nonEmpty => sawHelp = true ; Some(rest)
case h :: rest => value = h ; Some(rest)
}
def tryToSetColon(args: List[String]): Option[ResultOfTryToSet] =
args match {
case Nil | _ :: Nil => tryToSet(args)
case _ => errorAndValue("too many arguments", None)
}
def unparse: List[String] = if (value == default) Nil else List(name, value)

override def isHelping: Boolean = sawHelp
Expand All @@ -498,15 +512,15 @@ class MutableSettings(val errorFn: String => Unit, val pathFactory: PathFactory)

// This method is invoked if there are no colonated args. In this case the default value is
// used. No arguments are consumed.
override def tryToSet(args: List[String]) = {
def tryToSet(args: List[String]) = {
default match {
case Some(d) => value = d
case None => errorFn(s"$name requires an argument, the syntax is $helpSyntax")
}
Some(args)
}

override def tryToSetColon(args: List[String]) = args match {
def tryToSetColon(args: List[String]) = args match {
case x :: xs => value = ScalaVersion(x, errorFn); Some(xs)
case nil => Some(nil)
}
Expand Down Expand Up @@ -678,7 +692,7 @@ class MutableSettings(val errorFn: String => Unit, val pathFactory: PathFactory)
}

def tryToSet(args: List[String]) = tryToSetArgs(args, halting = true)
override def tryToSetColon(args: List[String]) = tryToSetArgs(args, halting = false)
def tryToSetColon(args: List[String]) = tryToSetArgs(args, halting = false)
override def tryToSetFromPropertyValue(s: String) = tryToSet(s.trim.split(',').toList) // used from ide

/** Try to set args, handling "help" and default.
Expand Down Expand Up @@ -805,7 +819,7 @@ class MutableSettings(val errorFn: String => Unit, val pathFactory: PathFactory)
Some(rest)
}
def tryToSet(args: List[String]) = tryToSetArgs(args, halting = true)
override def tryToSetColon(args: List[String]) = tryToSetArgs(args, halting = false)
def tryToSetColon(args: List[String]) = tryToSetArgs(args, halting = false)
override def tryToSetFromPropertyValue(s: String) = tryToSet(s.trim.split(',').toList) // used from ide

def clear(): Unit = (v = Nil)
Expand Down Expand Up @@ -851,9 +865,13 @@ class MutableSettings(val errorFn: String => Unit, val pathFactory: PathFactory)
override def isHelping = sawHelp
override def help = usageErrorMessage

def tryToSet(args: List[String]) = errorAndValue(usageErrorMessage, None)
def tryToSet(args: List[String]) =
args match {
case Nil | Optionlike() :: _ => errorAndValue(usageErrorMessage, None)
case arg :: rest => tryToSetColon(List(arg)).map(_ => rest)
}

override def tryToSetColon(args: List[String]) = args map _preSetHook match {
def tryToSetColon(args: List[String]) = args map _preSetHook match {
case Nil => errorAndValue(usageErrorMessage, None)
case List("help") => sawHelp = true; SomeOfNil
case List(x) if choices contains x => value = x ; SomeOfNil
Expand Down Expand Up @@ -916,7 +934,7 @@ class MutableSettings(val errorFn: String => Unit, val pathFactory: PathFactory)

private def splitDefault = default.split(',').toList

override def tryToSetColon(args: List[String]) = try {
def tryToSetColon(args: List[String]) = try {
args match {
case Nil => if (default == "") errorAndValue("missing phase", None)
else tryToSetColon(splitDefault)
Expand Down
9 changes: 0 additions & 9 deletions src/compiler/scala/tools/nsc/settings/ScalaSettings.scala
Expand Up @@ -82,15 +82,6 @@ trait ScalaSettings extends StandardScalaSettings with Warnings { _: MutableSett
domain = languageFeatures
) withAbbreviation "--language"
}
val release = StringSetting("-release", "release", "Compile for a specific version of the Java platform. Supported targets: 8, 9, ..., 17, 18", "").withPostSetHook { (value: StringSetting) =>
if (value.value != "" && !scala.util.Properties.isJavaAtLeast("9")) {
errorFn.apply("-release is only supported on Java 9 and higher")
} else {
// TODO validate numeric value
// TODO validate release <= java.specification.version
}
} withAbbreviation "--release"
def releaseValue: Option[String] = Option(release.value).filter(_ != "")

/**
* -X "Advanced" settings
Expand Down
45 changes: 38 additions & 7 deletions src/compiler/scala/tools/nsc/settings/StandardScalaSettings.scala
Expand Up @@ -14,11 +14,10 @@ package scala.tools.nsc
package settings

import scala.tools.util.PathResolver.Defaults
import scala.util.Properties.{isJavaAtLeast, javaSpecVersion}

/** Settings which aren't behind a -V, -W, -X, -Y, or -P option.
* When possible, the val and the option have identical names.
* The abstract settings are commented as to why they are as yet
* implemented in MutableSettings rather than mutation-generically.
*/
trait StandardScalaSettings { _: MutableSettings =>

Expand Down Expand Up @@ -52,7 +51,29 @@ trait StandardScalaSettings { _: MutableSettings =>
val nowarn = BooleanSetting ("-nowarn", "Generate no warnings.") withAbbreviation "--no-warnings" withPostSetHook { s => if (s) maxwarns.value = 0 }
val optimise: BooleanSetting // depends on post hook which mutates other settings
val print = BooleanSetting ("-print", "Print program with Scala-specific features removed.") withAbbreviation "--print"
val target = ChoiceSetting ("-target", "target", "Target platform for object files.", AllTargetVersions, "8") withPreSetHook normalizeTarget _ withAbbreviation "--target"
val release =
ChoiceSetting("-release", "release", "Compile for a version of the Java API and target class file.", AllTargetVersions, normalizeTarget(javaSpecVersion))
.withPostSetHook { setting =>
val current = setting.value.toInt
if (!isJavaAtLeast("9") && current > 8) errorFn.apply("-release is only supported on JVM 9 and higher")
if (target.valueSetByUser.map(_.toInt > current).getOrElse(false)) errorFn("-release cannot be less than -target")
som-snytt marked this conversation as resolved.
Show resolved Hide resolved
//target.value = setting.value // this would trigger deprecation
}
som-snytt marked this conversation as resolved.
Show resolved Hide resolved
.withAbbreviation("--release")
// .withAbbreviation("-java-output-version")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Scala 3, -java-output-version is the preferred flag name (with the alias of -release still permitted) - would it be reasonable to uncomment this line, and allow Scala 2.13 to use -java-output-version as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we'd accept a PR that did that — at least I can't think why not

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the dotty project is really bad at choosing option names.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(to be clear: not suggesting we make it canonical in Scala 2, just an accepted alternative)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see also -Xunchecked-java-output-version. I didn't want to encourage them (the folks inventing these names). I came close to enabling them (the alt names) on this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we'd accept a PR that did that — at least I can't think why not

Thanks! Opened with #10654.

Because the dotty project is really bad at choosing option names.

This comment reminded me of my favourite video on software development, the 2012 talk by Andrew Dupont How to Argue About Code (sadly, as it's 'Not Yet Rated' on Vimeo, you have to login to view it in UK & EU, but at least after that there's no ads).

Regarding the option name, for myself, when I'm trying to help junior devs understand that if they want to run their code on Java 11, they need to specify -java-output-version 11, the conversation is a little easier than it is if I say -release 11. The first name, because it includes 'java', is more self-explanatory. The junior dev is aware of Scala and Java, but doesn't automatically realise that when Scala says -release, it's referring to a Java-related concern.

def releaseValue: Option[String] = release.valueSetByUser
val target =
ChoiceSetting("-target", "target", "Target platform for object files.", AllTargetVersions, "8")
.withPreSetHook(normalizeTarget)
.withPostSetHook { setting =>
if (releaseValue.map(_.toInt < setting.value.toInt).getOrElse(false)) errorFn("-release cannot be less than -target")
}
.withAbbreviation("--target")
som-snytt marked this conversation as resolved.
Show resolved Hide resolved
// .withAbbreviation("--Xtarget")
// .withAbbreviation("-Xtarget")
// .withAbbreviation("-Xunchecked-java-output-version")
.withDeprecationMessage("Use -release instead to compile against the correct platform API.")
def targetValue: String = releaseValue.getOrElse(target.value)
val unchecked = BooleanSetting ("-unchecked", "Enable additional warnings where generated code depends on assumptions. See also -Wconf.") withAbbreviation "--unchecked" withPostSetHook { s =>
if (s.value) Wconf.tryToSet(List(s"cat=unchecked:w"))
else Wconf.tryToSet(List(s"cat=unchecked:s"))
Expand All @@ -66,14 +87,24 @@ trait StandardScalaSettings { _: MutableSettings =>
// Support passe prefixes of -target values:
// - `jvm-` (from back when we also had `msil`)
// - `1.` (from back when Java 2 was a possibility)
// `-target:1.jvm-13` is ridiculous, though.
private[this] def normalizeTarget(in: String): String = in.stripPrefix("jvm-").stripPrefix("1.")
// Otherwise, `-release` could be `IntSetting`.
private def normalizeTarget(in: String): String = {
val jvmish = raw"jvm-(\d*)".r
in match {
case "1.8" | "jvm-1.8" => "8"
som-snytt marked this conversation as resolved.
Show resolved Hide resolved
case jvmish(n) => n
case n => n
}
}
}

object StandardScalaSettings {
// not final in case some separately compiled client code wanted to depend on updated values
val MinTargetVersion = 8
val MaxTargetVersion = 19
val MaxTargetVersion = ScalaVersion(javaSpecVersion) match {
case SpecificScalaVersion(1, minor, _, _) => minor
case SpecificScalaVersion(major, _, _, _) => major
case _ => 19
}

private val AllTargetVersions = (MinTargetVersion to MaxTargetVersion).map(_.toString).to(List)
}
4 changes: 4 additions & 0 deletions test/files/neg/t12543.check
@@ -0,0 +1,4 @@
t12543.scala:10: error: value of is not a member of object java.util.List
val ss = java.util.List.of("Hello", who)
^
1 error
17 changes: 17 additions & 0 deletions test/files/neg/t12543.scala
@@ -0,0 +1,17 @@

// scalac: -Werror -release 8

import scala.jdk.CollectionConverters._
//import jdk.CollectionConverters._ // scala/bug/issues/12566
import sun._

object HelloWorld {
def main(args: Array[String]) = {
val ss = java.util.List.of("Hello", who)
println(ss.asScala.mkString("", ", ", "!"))
}
}

object sun {
val who = "world"
}
15 changes: 10 additions & 5 deletions test/junit/scala/tools/nsc/settings/TargetTest.scala
Expand Up @@ -18,12 +18,19 @@ import org.junit.runner.RunWith
import org.junit.runners.JUnit4

import scala.collection.mutable.ListBuffer
import scala.util.Properties.javaSpecVersion

@RunWith(classOf[JUnit4])
class TargetTest {

@Test def testSettingTargetSetting(): Unit = {
def check(in: String, expect: String) = {
def check(in: String, expect: String) =
javaSpecVersion match {
case "1.8" => if (expect == "8") checkSuccess(in, expect) else checkFail(in)
case jdk if jdk.toInt >= expect.toInt => checkSuccess(in, expect)
case _ => checkFail(in)
}
def checkSuccess(in: String, expect: String) = {
val settings = new Settings(err => fail(s"Error output: $err"))
val (ok, _) = settings.processArgumentString(in)
assertTrue(ok)
Expand All @@ -34,7 +41,7 @@ class TargetTest {
val settings = new Settings(messages.addOne)
val (ok, _) = settings.processArgumentString(in)
assertFalse(ok)
assertTrue(messages.nonEmpty)
assertFalse(messages.isEmpty)
assertEquals(2, messages.size) // bad choice + bad option
assertTrue(messages.exists(_.startsWith("bad option")))
}
Expand All @@ -46,7 +53,7 @@ class TargetTest {

check("-target:jvm-9", "9")
check("-target:9", "9")
// it's not Java 1.9, you reprobates!
checkFail("-target:1.9") // it's not Java 1.9, you reprobates!

check("-target:jvm-10", "10")
check("-target:10", "10")
Expand Down Expand Up @@ -76,7 +83,5 @@ class TargetTest {
checkFail("-target:jvm-20") // not yet...
checkFail("-target:jvm-3000") // not in our lifetime
checkFail("-target:msil") // really?

}

}