Skip to content

Commit 74ea1af

Browse files
authored
Merge pull request #12589 from som-snytt/issue/12588
processArgs is final for some tailrec
2 parents 778e37c + f1df8f3 commit 74ea1af

File tree

4 files changed

+113
-49
lines changed

4 files changed

+113
-49
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ trait CliCommand:
6363
}
6464

6565
sg.processArguments(expandedArguments, processAll = true, settingsState = ss)
66+
end distill
6667

6768
/** Creates a help message for a subset of options based on cond */
6869
protected def availableOptionsMsg(cond: Setting[?] => Boolean)(using settings: ConcreteSettings)(using SettingsState): String =
@@ -108,6 +109,7 @@ trait CliCommand:
108109
""
109110
s"${formatName(s.name)} ${formatDescription(s.description)}${formatSetting("Default", defaultValue)}${formatSetting("Choices", s.legalChoices)}"
110111
ss.map(helpStr).mkString("", "\n", s"\n${formatName("@<file>")} ${formatDescription("A text file containing compiler arguments (options and source files).")}\n")
112+
end availableOptionsMsg
111113

112114
protected def shortUsage: String = s"Usage: $cmdName <options> <source files>"
113115

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

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,22 @@ object Settings {
2121
val OptionTag: ClassTag[Option[?]] = ClassTag(classOf[Option[?]])
2222
val OutputTag: ClassTag[AbstractFile] = ClassTag(classOf[AbstractFile])
2323

24-
class SettingsState(initialValues: Seq[Any]) {
24+
class SettingsState(initialValues: Seq[Any]):
2525
private val values = ArrayBuffer(initialValues: _*)
2626
private var _wasRead: Boolean = false
2727

2828
override def toString: String = s"SettingsState(values: ${values.toList})"
2929

30-
def value(idx: Int): Any = {
30+
def value(idx: Int): Any =
3131
_wasRead = true
3232
values(idx)
33-
}
3433

3534
def update(idx: Int, x: Any): SettingsState =
36-
if (_wasRead)
37-
new SettingsState(values.toSeq).update(idx, x)
38-
else {
35+
if (_wasRead) then SettingsState(values.toSeq).update(idx, x)
36+
else
3937
values(idx) = x
4038
this
41-
}
42-
}
39+
end SettingsState
4340

4441
case class ArgsSummary(
4542
sstate: SettingsState,
@@ -67,18 +64,11 @@ object Settings {
6764

6865
private var changed: Boolean = false
6966

70-
def valueIn(state: SettingsState): T =
71-
state.value(idx).asInstanceOf[T]
67+
def valueIn(state: SettingsState): T = state.value(idx).asInstanceOf[T]
7268

73-
def updateIn(state: SettingsState, x: Any): SettingsState = x match {
69+
def updateIn(state: SettingsState, x: Any): SettingsState = x match
7470
case _: T => state.update(idx, x)
75-
case _ =>
76-
// would like to do:
77-
// throw new ClassCastException(s"illegal argument, found: $x of type ${x.getClass}, required: ${implicitly[ClassTag[T]]}")
78-
// but this runs afoul of primitive types. Concretely: if T is Boolean, then x is a boxed Boolean and the test will fail.
79-
// Maybe this is a bug in Scala 2.10?
80-
state.update(idx, x.asInstanceOf[T])
81-
}
71+
case _ => throw IllegalArgumentException(s"found: $x of type ${x.getClass.getName}, required: ${implicitly[ClassTag[T]]}")
8272

8373
def isDefaultIn(state: SettingsState): Boolean = valueIn(state) == default
8474

@@ -94,7 +84,7 @@ object Settings {
9484

9585
def tryToSet(state: ArgsSummary): ArgsSummary = {
9686
val ArgsSummary(sstate, arg :: args, errors, warnings) = state
97-
def update(value: Any, args: List[String]) =
87+
def update(value: Any, args: List[String]): ArgsSummary =
9888
var dangers = warnings
9989
val value1 =
10090
if changed && isMultivalue then
@@ -107,6 +97,7 @@ object Settings {
10797
value
10898
changed = true
10999
ArgsSummary(updateIn(sstate, value1), args, errors, dangers)
100+
end update
110101
def fail(msg: String, args: List[String]) =
111102
ArgsSummary(sstate, args, errors :+ msg, warnings)
112103
def missingArg =
@@ -209,7 +200,7 @@ object Settings {
209200
/** Iterates over the arguments applying them to settings where applicable.
210201
* Then verifies setting dependencies are met.
211202
*
212-
* This temporarily takes a boolean indicating whether to keep
203+
* This takes a boolean indicating whether to keep
213204
* processing if an argument is seen which is not a command line option.
214205
* This is an expedience for the moment so that you can say
215206
*
@@ -221,28 +212,27 @@ object Settings {
221212
*
222213
* to get their arguments.
223214
*/
224-
def processArguments(state: ArgsSummary, processAll: Boolean, skipped: List[String]): ArgsSummary = {
215+
@tailrec
216+
final def processArguments(state: ArgsSummary, processAll: Boolean, skipped: List[String]): ArgsSummary =
225217
def stateWithArgs(args: List[String]) = ArgsSummary(state.sstate, args, state.errors, state.warnings)
226-
state.arguments match {
218+
state.arguments match
227219
case Nil =>
228220
checkDependencies(stateWithArgs(skipped))
229221
case "--" :: args =>
230222
checkDependencies(stateWithArgs(skipped ++ args))
231223
case x :: _ if x startsWith "-" =>
232-
@tailrec def loop(settings: List[Setting[?]]): ArgsSummary = settings match {
224+
@tailrec def loop(settings: List[Setting[?]]): ArgsSummary = settings match
233225
case setting :: settings1 =>
234226
val state1 = setting.tryToSet(state)
235-
if (state1 ne state) processArguments(state1, processAll, skipped)
227+
if state1 ne state then state1
236228
else loop(settings1)
237229
case Nil =>
238-
processArguments(state.warn(s"bad option '$x' was ignored"), processAll, skipped)
239-
}
240-
loop(allSettings.toList)
230+
state.warn(s"bad option '$x' was ignored")
231+
processArguments(loop(allSettings.toList), processAll, skipped)
241232
case arg :: args =>
242-
if (processAll) processArguments(stateWithArgs(args), processAll, skipped :+ arg)
233+
if processAll then processArguments(stateWithArgs(args), processAll, skipped :+ arg)
243234
else state
244-
}
245-
}
235+
end processArguments
246236

247237
def processArguments(arguments: List[String], processAll: Boolean, settingsState: SettingsState = defaultState): ArgsSummary =
248238
processArguments(ArgsSummary(settingsState, arguments, Nil, Nil), processAll, Nil)

compiler/test/dotty/tools/dotc/SettingsTests.scala

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,14 @@ class SettingsTests {
2121
assertEquals(1, reporter.errorCount)
2222
assertEquals("'not_here' does not exist or is not a directory or .jar file", reporter.allErrors.head.message)
2323

24-
@Test def jarOutput: Unit = {
24+
@Test def jarOutput: Unit =
2525
val source = "tests/pos/Foo.scala"
2626
val out = Paths.get("out/jaredFoo.jar").normalize
2727
if (Files.exists(out)) Files.delete(out)
2828
val options = Array("-classpath", TestConfiguration.basicClasspath, "-d", out.toString, source)
2929
val reporter = Main.process(options)
3030
assertEquals(0, reporter.errorCount)
3131
assertTrue(Files.exists(out))
32-
}
3332

3433
@Test def `t8124 Don't crash on missing argument`: Unit =
3534
val source = Paths.get("tests/pos/Foo.scala").normalize
@@ -45,15 +44,70 @@ class SettingsTests {
4544
val foo = StringSetting("-foo", "foo", "Foo", "a")
4645
val bar = IntSetting("-bar", "Bar", 0)
4746

48-
inContext {
49-
val args = List("-foo", "b", "-bar", "1")
50-
val summary = Settings.processArguments(args, true)
51-
assertTrue(summary.errors.isEmpty)
52-
given SettingsState = summary.sstate
47+
val args = List("-foo", "b", "-bar", "1")
48+
val summary = Settings.processArguments(args, true)
49+
assertTrue(summary.errors.isEmpty)
50+
withProcessedArgs(summary) {
5351
assertEquals("b", Settings.foo.value)
5452
assertEquals(1, Settings.bar.value)
5553
}
5654

55+
@Test def `workaround dont crash on many files`: Unit =
56+
object Settings extends SettingGroup
57+
58+
val args = "--" :: List.fill(6000)("file.scala")
59+
val summary = Settings.processArguments(args, processAll = true)
60+
assertTrue(summary.errors.isEmpty)
61+
assertEquals(6000, summary.arguments.size)
62+
63+
@Test def `dont crash on many files`: Unit =
64+
object Settings extends SettingGroup
65+
66+
val args = List.fill(6000)("file.scala")
67+
val summary = Settings.processArguments(args, processAll = true)
68+
assertTrue(summary.errors.isEmpty)
69+
assertEquals(6000, summary.arguments.size)
70+
71+
@Test def `dont crash on many options`: Unit =
72+
object Settings extends SettingGroup:
73+
val option = BooleanSetting("-option", "Some option")
74+
75+
val limit = 6000
76+
val args = List.fill(limit)("-option")
77+
val summary = Settings.processArguments(args, processAll = true)
78+
assertTrue(summary.errors.isEmpty)
79+
assertEquals(limit-1, summary.warnings.size)
80+
assertTrue(summary.warnings.head.contains("repeatedly"))
81+
assertEquals(0, summary.arguments.size)
82+
withProcessedArgs(summary) {
83+
assertTrue(Settings.option.value)
84+
}
85+
86+
@Test def `bad option warning consumes an arg`: Unit =
87+
object Settings extends SettingGroup:
88+
val option = BooleanSetting("-option", "Some option")
89+
90+
val args = List("-adoption", "dogs", "cats")
91+
val summary = Settings.processArguments(args, processAll = true)
92+
assertTrue(summary.errors.isEmpty)
93+
assertFalse(summary.warnings.isEmpty)
94+
assertEquals(2, summary.arguments.size)
95+
96+
@Test def `bad option settings throws`: Unit =
97+
object Settings extends SettingGroup:
98+
val option = BooleanSetting("-option", "Some option")
99+
100+
def checkMessage(s: String): (Throwable => Boolean) = t =>
101+
if t.getMessage == s then true
102+
else
103+
println(s"Expected: $s, Actual: ${t.getMessage}")
104+
false
105+
106+
val default = Settings.defaultState
107+
assertThrows[IllegalArgumentException](checkMessage("found: not an option of type java.lang.String, required: Boolean")) {
108+
Settings.option.updateIn(default, "not an option")
109+
}
110+
57111
@Test def validateChoices: Unit =
58112
object Settings extends SettingGroup:
59113
val foo = ChoiceSetting("-foo", "foo", "Foo", List("a", "b"), "a")
@@ -63,25 +117,27 @@ class SettingsTests {
63117
val quux = ChoiceSetting("-quux", "quux", "Quux", List(), "")
64118
val quuz = IntChoiceSetting("-quuz", "Quuz", List(), 0)
65119

66-
inContext {
120+
locally {
67121
val args = List("-foo", "b", "-bar", "1", "-baz", "5")
68122
val summary = Settings.processArguments(args, true)
69123
assertTrue(summary.errors.isEmpty)
70-
given SettingsState = summary.sstate
71-
assertEquals("b", Settings.foo.value)
72-
assertEquals(1, Settings.bar.value)
73-
assertEquals(5, Settings.baz.value)
124+
withProcessedArgs(summary) {
125+
assertEquals("b", Settings.foo.value)
126+
assertEquals(1, Settings.bar.value)
127+
assertEquals(5, Settings.baz.value)
128+
}
74129
}
75130

76-
inContext {
131+
locally {
77132
val args = List("-foo:b")
78133
val summary = Settings.processArguments(args, true)
79134
assertTrue(summary.errors.isEmpty)
80-
given SettingsState = summary.sstate
81-
assertEquals("b", Settings.foo.value)
135+
withProcessedArgs(summary) {
136+
assertEquals("b", Settings.foo.value)
137+
}
82138
}
83139

84-
inContext {
140+
locally {
85141
val args = List("-foo", "c", "-bar", "3", "-baz", "-1")
86142
val summary = Settings.processArguments(args, true)
87143
val expectedErrors = List(
@@ -92,14 +148,14 @@ class SettingsTests {
92148
assertEquals(expectedErrors, summary.errors)
93149
}
94150

95-
inContext {
151+
locally {
96152
val args = List("-foo:c")
97153
val summary = Settings.processArguments(args, true)
98154
val expectedErrors = List("c is not a valid choice for -foo")
99155
assertEquals(expectedErrors, summary.errors)
100156
}
101157

102-
inContext {
158+
locally {
103159
val args = List("-quux", "a", "-quuz", "0")
104160
val summary = Settings.processArguments(args, true)
105161
val expectedErrors = List(
@@ -109,7 +165,7 @@ class SettingsTests {
109165
assertEquals(expectedErrors, summary.errors)
110166
}
111167

112-
private def inContext(f: Context ?=> Unit) = f(using (new ContextBase).initialCtx.fresh)
168+
private def withProcessedArgs(summary: ArgsSummary)(f: SettingsState ?=> Unit) = f(using summary.sstate)
113169

114170
extension [T](setting: Setting[T])
115171
private def value(using ss: SettingsState): T = setting.valueIn(ss)

compiler/test/dotty/tools/utils.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import java.io.File
44
import java.nio.charset.StandardCharsets.UTF_8
55

66
import scala.io.Source
7+
import scala.reflect.ClassTag
78
import scala.util.Using.resource
9+
import scala.util.chaining.given
10+
import scala.util.control.{ControlThrowable, NonFatal}
811

912
def scripts(path: String): Array[File] = {
1013
val dir = new File(getClass.getResource(path).getPath)
@@ -17,3 +20,16 @@ private def withFile[T](file: File)(action: Source => T): T =
1720

1821
def readLines(f: File): List[String] = withFile(f)(_.getLines.toList)
1922
def readFile(f: File): String = withFile(f)(_.mkString)
23+
24+
private object Unthrown extends ControlThrowable
25+
26+
def assertThrows[T <: Throwable: ClassTag](p: T => Boolean)(body: => Any): Unit =
27+
try
28+
body
29+
throw Unthrown
30+
catch
31+
case Unthrown => throw AssertionError("Expression did not throw!")
32+
case e: T if p(e) => ()
33+
case failed: T => throw AssertionError(s"Exception failed check: $failed").tap(_.addSuppressed(failed))
34+
case NonFatal(other) => throw AssertionError(s"Wrong exception: expected ${implicitly[ClassTag[T]]} but was ${other.getClass.getName}").tap(_.addSuppressed(other))
35+
end assertThrows

0 commit comments

Comments
 (0)