From a745311c7000471be4eb5e2f733279190f5d51a7 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Wed, 9 Apr 2025 04:32:43 -0700 Subject: [PATCH 1/3] Exit on main arg parse error --- .../src/dotty/tools/dotc/ast/MainProxies.scala | 9 +++++++-- library/src/scala/util/CommandLineParser.scala | 17 ++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 9ed19c93d1ba..5730bf2386cd 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -5,7 +5,7 @@ import core.* import Symbols.*, Types.*, Contexts.*, Decorators.*, util.Spans.*, Flags.*, Constants.* import StdNames.{nme, tpnme} import ast.Trees.* -import Names.Name +import Names.{Name, termName} import Comments.Comment import NameKinds.DefaultGetterName import Annotations.Annotation @@ -94,7 +94,12 @@ object MainProxies { val handler = CaseDef( Typed(errVar, TypeTree(defn.CLP_ParseError.typeRef)), EmptyTree, - Apply(ref(defn.CLP_showError.termRef), errVar :: Nil)) + Block( + List(Apply(ref(defn.CLP_showError.termRef), errVar :: Nil)), + Apply(Select(Select(Select(Ident(termName("java")), termName("lang")), termName("System")), termName("exit")), + List(Literal(Constant(1)))) + ) + ) val body = Try(call, handler :: Nil, EmptyTree) val mainArg = ValDef(nme.args, TypeTree(defn.ArrayType.appliedTo(defn.StringType)), EmptyTree) .withFlags(Param) diff --git a/library/src/scala/util/CommandLineParser.scala b/library/src/scala/util/CommandLineParser.scala index fd239ef231c5..4d799388b0ec 100644 --- a/library/src/scala/util/CommandLineParser.scala +++ b/library/src/scala/util/CommandLineParser.scala @@ -1,23 +1,22 @@ package scala.util +import scala.util.control.ControlThrowable + /** A utility object to support command line parsing for @main methods */ object CommandLineParser { /** An exception raised for an illegal command line - * @param idx The index of the argument that's faulty (starting from 0) - * @param msg The error message - */ - class ParseError(val idx: Int, val msg: String) extends Exception + * @param idx The index of the argument that's faulty (starting from 0) + * @param msg The error message + */ + class ParseError(val idx: Int, val msg: String) extends Exception(msg) /** Parse command line argument `s`, which has index `n`, as a value of type `T` * @throws ParseError if argument cannot be converted to type `T`. */ - def parseString[T](str: String, n: Int)(using fs: FromString[T]): T = { + def parseString[T](str: String, n: Int)(using fs: FromString[T]): T = try fs.fromString(str) - catch { - case ex: IllegalArgumentException => throw ParseError(n, ex.toString) - } - } + catch case ex: IllegalArgumentException => throw ParseError(n, ex.toString) /** Parse `n`'th argument in `args` (counting from 0) as a value of type `T` * @throws ParseError if argument does not exist or cannot be converted to type `T`. From 28a0417019126978f7cfead2494380b48d85b840 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Wed, 9 Apr 2025 11:49:41 -0700 Subject: [PATCH 2/3] Run test takes java args Uses `// java:` syntax as a stopgap. Args are appended to child message line. --- compiler/test/dotty/tools/utils.scala | 27 +++++++++++-------- .../test/dotty/tools/vulpix/ChildJVMMain.java | 21 +++++++++------ .../tools/vulpix/RunnerOrchestration.scala | 17 ++++++------ tests/run/Pouring.check | 3 ++- tests/run/Pouring.scala | 2 ++ 5 files changed, 42 insertions(+), 28 deletions(-) diff --git a/compiler/test/dotty/tools/utils.scala b/compiler/test/dotty/tools/utils.scala index c33310acf06e..9e9972025735 100644 --- a/compiler/test/dotty/tools/utils.scala +++ b/compiler/test/dotty/tools/utils.scala @@ -12,10 +12,10 @@ import scala.io.Source import scala.jdk.StreamConverters._ import scala.reflect.ClassTag import scala.util.Using.resource -import scala.util.chaining.given import scala.util.control.{ControlThrowable, NonFatal} import dotc.config.CommandLineParser +import dotc.util.chaining.* object Dummy @@ -76,7 +76,11 @@ object ToolName: def named(s: String): ToolName = values.find(_.toString.equalsIgnoreCase(s)).getOrElse(throw IllegalArgumentException(s)) type ToolArgs = Map[ToolName, List[String]] +object ToolArgs: + def empty = Map.empty[ToolName, List[String]] type PlatformFiles = Map[TestPlatform, List[String]] +object PlatformFiles: + def empty = Map.empty[TestPlatform, List[String]] /** Take a prefix of each file, extract tool args, parse, and combine. * Arg parsing respects quotation marks. Result is a map from ToolName to the combined tokens. @@ -90,21 +94,23 @@ def toolArgsFor(files: List[JPath], charset: Charset = UTF_8): ToolArgs = * If the ToolName is Target, then also accumulate the file name associated with the given platform. */ def platformAndToolArgsFor(files: List[JPath], charset: Charset = UTF_8): (PlatformFiles, ToolArgs) = - files.foldLeft(Map.empty[TestPlatform, List[String]] -> Map.empty[ToolName, List[String]]) { (res, path) => + files.foldLeft((PlatformFiles.empty, ToolArgs.empty)) { (res, path) => val toolargs = toolArgsParse(resource(Files.lines(path, charset))(_.limit(10).toScala(List)), Some(path.toString)) toolargs.foldLeft(res) { - case ((plat, acc), (tool, args)) => + case ((plats, tools), (tool, args)) => val name = ToolName.named(tool) val tokens = CommandLineParser.tokenize(args) - val plat1 = if name eq ToolName.Target then + val plats1 = if name eq ToolName.Target then val testPlatform = TestPlatform.named(tokens.head) val fileName = path.toString - plat.updatedWith(testPlatform)(_.map(fileName :: _).orElse(Some(fileName :: Nil))) + plats.updatedWith(testPlatform)(_.map(fileName :: _).orElse(Some(fileName :: Nil))) else - plat + plats - plat1 -> acc.updatedWith(name)(v0 => v0.map(_ ++ tokens).orElse(Some(tokens))) + val tools1 = tools.updatedWith(name)(v0 => v0.map(_ ++ tokens).orElse(Some(tokens))) + + (plats1, tools1) } } @@ -131,19 +137,18 @@ private val directiveUnknown = raw"//> using (.*)".r.unanchored // If args string ends in close comment, stop at the `*` `/`. // Returns all the matches by the regex. def toolArgsParse(lines: List[String], filename: Option[String]): List[(String,String)] = - lines.flatMap { + lines.flatMap: case toolArg("scalac", _) => sys.error(s"`// scalac: args` not supported. Please use `//> using options args`${filename.fold("")(f => s" in file $f")}") case toolArg("javac", _) => sys.error(s"`// javac: args` not supported. Please use `//> using javacOpt args`${filename.fold("")(f => s" in file $f")}") case toolArg(name, args) => List((name, args)) case _ => Nil - } ++ - lines.flatMap { + ++ + lines.flatMap: case directiveOptionsArg(args) => List(("scalac", args)) case directiveJavacOptions(args) => List(("javac", args)) case directiveTargetOptions(platform) => List(("target", platform)) case directiveUnknown(rest) => sys.error(s"Unknown directive: `//> using ${CommandLineParser.tokenize(rest).headOption.getOrElse("''")}`${filename.fold("")(f => s" in file $f")}") case _ => Nil - } import org.junit.Test import org.junit.Assert._ diff --git a/compiler/test/dotty/tools/vulpix/ChildJVMMain.java b/compiler/test/dotty/tools/vulpix/ChildJVMMain.java index a6873ead1968..009a44cbc699 100644 --- a/compiler/test/dotty/tools/vulpix/ChildJVMMain.java +++ b/compiler/test/dotty/tools/vulpix/ChildJVMMain.java @@ -12,10 +12,15 @@ public class ChildJVMMain { static final String MessageStart = "##THIS IS THE START FOR ME, HELLO##"; static final String MessageEnd = "##THIS IS THE END FOR ME, GOODBYE##"; - private static void runMain(String dir) throws Exception { + private static void runMain(String line) throws Exception { Method meth = null; - Object[] args = new Object[]{ new String[]{ } }; + String[] args = new String[]{ }; try { + String[] tokens = line.split("\\s+", 2); + String dir = tokens[0]; + if (tokens.length > 1) { + args = tokens[1].split("\\s+"); + } String jcp = System.getProperty("java.class.path"); String sep = File.pathSeparator; System.setProperty("java.class.path", jcp == null ? dir : dir + sep + jcp); @@ -37,15 +42,15 @@ private static void runMain(String dir) throws Exception { } System.out.println(MessageStart); - meth.invoke(null, args); + meth.invoke(null, new Object[] { args }); } public static void main(String[] args) throws Exception { - BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); + BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); - while (true) { - runMain(stdin.readLine()); - System.out.println(MessageEnd); - } + while (true) { + runMain(stdin.readLine()); + System.out.println(MessageEnd); + } } } diff --git a/compiler/test/dotty/tools/vulpix/RunnerOrchestration.scala b/compiler/test/dotty/tools/vulpix/RunnerOrchestration.scala index a91b0de3e238..59cd0ba8f2f3 100644 --- a/compiler/test/dotty/tools/vulpix/RunnerOrchestration.scala +++ b/compiler/test/dotty/tools/vulpix/RunnerOrchestration.scala @@ -53,7 +53,7 @@ trait RunnerOrchestration { /** Running a `Test` class's main method from the specified `classpath` */ def runMain(classPath: String, toolArgs: ToolArgs)(implicit summaryReport: SummaryReporting): Status = - monitor.runMain(classPath) + monitor.runMain(classPath, toolArgs.getOrElse(ToolName.Java, Nil)) /** Each method of Debuggee can be called only once, in the order of definition.*/ trait Debuggee: @@ -86,8 +86,8 @@ trait RunnerOrchestration { */ private class RunnerMonitor { - def runMain(classPath: String)(implicit summaryReport: SummaryReporting): Status = - withRunner(_.runMain(classPath)) + def runMain(classPath: String, args: List[String])(implicit summaryReport: SummaryReporting): Status = + withRunner(_.runMain(classPath, args)) def debugMain(classPath: String)(f: Debuggee => Unit)(implicit summaryReport: SummaryReporting): Unit = withRunner(_.debugMain(classPath)(f)) @@ -132,9 +132,9 @@ trait RunnerOrchestration { process = null /** Blocks less than `maxDuration` while running `Test.main` from `dir` */ - def runMain(classPath: String): Status = + def runMain(classPath: String, args: List[String]): Status = assert(process ne null, "Runner was killed and then reused without setting a new process") - awaitStatusOrRespawn(startMain(classPath)) + awaitStatusOrRespawn(startMain(classPath, args)) def debugMain(classPath: String)(f: Debuggee => Unit): Unit = assert(process ne null, "Runner was killed and then reused without setting a new process") @@ -142,7 +142,7 @@ trait RunnerOrchestration { val debuggee = new Debuggee: private var mainFuture: Future[Status] = null def readJdiPort(): Int = process.getJdiPort() - def launch(): Unit = mainFuture = startMain(classPath) + def launch(): Unit = mainFuture = startMain(classPath, Nil) def exit(): Status = awaitStatusOrRespawn(mainFuture) @@ -153,9 +153,10 @@ trait RunnerOrchestration { throw e end debugMain - private def startMain(classPath: String): Future[Status] = + private def startMain(classPath: String, args: List[String]): Future[Status] = + val line = if args.isEmpty then classPath else s"$classPath ${args.mkString(" ")}" // pass classpath to running process - process.printLine(classPath) + process.printLine(line) // Create a future reading the object: Future: diff --git a/tests/run/Pouring.check b/tests/run/Pouring.check index c9ab84a226bb..05f61720967c 100644 --- a/tests/run/Pouring.check +++ b/tests/run/Pouring.check @@ -1 +1,2 @@ -Illegal command line: more arguments expected +Moves: Vector(Empty(0), Empty(1), Fill(0), Fill(1), Pour(0,1), Pour(1,0)) +Solution: Some(Fill(1) Pour(1,0) Empty(0) Pour(1,0) Fill(1) Pour(1,0) --> Vector(4, 6)) diff --git a/tests/run/Pouring.scala b/tests/run/Pouring.scala index 5bb2a92ff8e3..e82627d6e8da 100644 --- a/tests/run/Pouring.scala +++ b/tests/run/Pouring.scala @@ -1,3 +1,5 @@ +// java: 6 4 7 + type Glass = Int type Levels = Vector[Int] From ac712962885f4f285a00899a88e885ae326bdbde Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Mon, 14 Apr 2025 14:10:51 -0700 Subject: [PATCH 3/3] Rethrow stackless parse error --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 5 +++-- library/src/scala/util/CommandLineParser.scala | 2 +- tests/printing/infix-operations.check | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 5730bf2386cd..6dd35e67124b 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -96,8 +96,9 @@ object MainProxies { EmptyTree, Block( List(Apply(ref(defn.CLP_showError.termRef), errVar :: Nil)), - Apply(Select(Select(Select(Ident(termName("java")), termName("lang")), termName("System")), termName("exit")), - List(Literal(Constant(1)))) + Throw(errVar) + //Apply(Select(Select(Select(Ident(termName("java")), termName("lang")), termName("System")), termName("exit")), + // List(Literal(Constant(1)))) ) ) val body = Try(call, handler :: Nil, EmptyTree) diff --git a/library/src/scala/util/CommandLineParser.scala b/library/src/scala/util/CommandLineParser.scala index 4d799388b0ec..ac6cd511f579 100644 --- a/library/src/scala/util/CommandLineParser.scala +++ b/library/src/scala/util/CommandLineParser.scala @@ -9,7 +9,7 @@ object CommandLineParser { * @param idx The index of the argument that's faulty (starting from 0) * @param msg The error message */ - class ParseError(val idx: Int, val msg: String) extends Exception(msg) + class ParseError(val idx: Int, val msg: String) extends Exception(msg, null, false, false) /** Parse command line argument `s`, which has index `n`, as a value of type `T` * @throws ParseError if argument cannot be converted to type `T`. diff --git a/tests/printing/infix-operations.check b/tests/printing/infix-operations.check index 3de3cca6423f..c4d33f7940ee 100644 --- a/tests/printing/infix-operations.check +++ b/tests/printing/infix-operations.check @@ -31,6 +31,7 @@ package { { case error @ _:scala.util.CommandLineParser.ParseError => scala.util.CommandLineParser.showError(error) + throw error } } }