From 4145eda87165f684b76b1d1c59dc6dc1ff447176 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 23 Apr 2020 01:17:35 +0200 Subject: [PATCH 01/14] Prototype for proposed main method generation scheme --- tests/pos/main-method-scheme.scala | 170 +++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 tests/pos/main-method-scheme.scala diff --git a/tests/pos/main-method-scheme.scala b/tests/pos/main-method-scheme.scala new file mode 100644 index 000000000000..82a2039b2086 --- /dev/null +++ b/tests/pos/main-method-scheme.scala @@ -0,0 +1,170 @@ +import annotation.StaticAnnotation +import collection.mutable + +trait MainAnnotation extends StaticAnnotation: + + type ArgumentParser[T] + + // get single argument + def getArg[T](argName: String, fromString: ArgumentParser[T], defaultValue: Option[T] = None): () => T + + // get varargs argument + def getArgs[T](argName: String, fromString: ArgumentParser[T]): () => List[T] + + // check that everything is parsed + def done(): Boolean + +end MainAnnotation + +//Sample main class, can be freely implemented: + +class _main(progName: String, args: Array[String], docComment: String) extends MainAnnotation: + + def this() = this("", Array(), "") + + type ArgumentParser = util.FromString + + /** A buffer of demanded argument names, plus + * "?" if it has a default + * "*" if it is a vararg + * "" otherwise + */ + private var argInfos = new mutable.ListBuffer[(String, String)] + + /** A buffer for all errors */ + private var errors = new mutable.ListBuffer[String] + + /** The next argument index */ + private var n: Int = 0 + + private def error(msg: String): () => Nothing = + errors += msg + () => ??? + + private def argAt(idx: Int): Option[String] = + if idx < args.length then Some(args(idx)) else None + + private def nextPositionalArg(): Option[String] = + while n < args.length && args(n).startsWith("--") do n += 2 + val result = argAt(n) + n += 1 + result + + private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = + p.fromStringOption(arg) match + case Some(t) => () => t + case None => error(s"invalid argument for $argName: $arg") + + def getArg[T](argName: String, p: ArgumentParser[T], defaultValue: Option[T] = None): () => T = + argInfos += ((argName, if defaultValue.isDefined then "?" else "")) + val idx = args.indexOf(s"--$argName") + val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() + argOpt match + case Some(arg) => convert(argName, arg, p) + case None => defaultValue match + case Some(t) => () => t + case None => error(s"missing argument for $argName") + + def getArgs[T](argName: String, fromString: ArgumentParser[T]): () => List[T] = + argInfos += ((argName, "*")) + def recur(): List[() => T] = nextPositionalArg() match + case Some(arg) => convert(arg, argName, fromString) :: recur() + case None => Nil + val fns = recur() + () => fns.map(_()) + + def usage(): Boolean = + println(s"Usage: $progName ${argInfos.map(_ + _).mkString(" ")}") + if docComment.nonEmpty then + println(docComment) // todo: process & format doc comment + false + + def showUnused(): Unit = nextPositionalArg() match + case Some(arg) => + error(s"unused argument: $arg") + showUnused() + case None => + for + arg <- args + if arg.startsWith("--") && !argInfos.map(_._1).contains(arg.drop(2)) + do + error(s"unknown argument name: $arg") + end showUnused + + def done(): Boolean = + if args.contains("--help") then + usage() + else + showUnused() + if errors.nonEmpty then + for msg <- errors do println(s"Error: $msg") + usage() + else + true + end done +end _main + +// Sample main method + +object myProgram: + + /** Adds two numbers */ + @_main def add(num: Int, inc: Int = 1) = + println(s"$num + $inc = ${num + inc}") + +end myProgram + +// Compiler generated code: + +object add: + def main(args: Array[String]) = + val cmd = new _main("add", args, "Adds two numbers") + val arg1 = cmd.getArg[Int]("num", summon[cmd.ArgumentParser[Int]]) + val arg2 = cmd.getArg[Int]("inc", summon[cmd.ArgumentParser[Int]], Some(1)) + if cmd.done() then myProgram.add(arg1(), arg2()) +end add + +/** --- Some scenarios ---------------------------------------- + +> java add 2 3 +2 + 3 = 5 +> java add 2 3 +2 + 3 = 5 +> java add 4 +4 + 1 = 5 +> java add --num 10 --inc -2 +10 + -2 = 8 +> java add --num 10 +10 + 1 = 11 +> java add --help +Usage: add num inc? +Adds two numbers +> java add +error: missing argument for num +Usage: add num inc? +Adds two numbers +> java add 1 2 3 +error: unused argument: 3 +Usage: add num inc? +Adds two numbers +> java add --num 1 --incr 2 +error: unknown argument name: --incr +Usage: add num inc? +Adds two numbers +> java add 1 true +error: invalid argument for inc: true +Usage: add num inc? +Adds two numbers +> java add true false +error: invalid argument for num: true +error: invalid argument for inc: false +Usage: add num inc? +Adds two numbers +> java add true false --foo 33 +Error: invalid argument for num: true +Error: invalid argument for inc: false +Error: unknown argument name: --foo +Usage: add num inc? +Adds two numbers + +*/ \ No newline at end of file From 7c1719bc30a899334d2f9a91c9c45033595c418d Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 23 Apr 2020 01:51:02 +0200 Subject: [PATCH 02/14] Rename annotation to `main` I first thought there'd be a conflict with the current compiler-generated version, but there isn't after all. --- tests/pos/main-method-scheme.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/pos/main-method-scheme.scala b/tests/pos/main-method-scheme.scala index 82a2039b2086..5245fe65d4c1 100644 --- a/tests/pos/main-method-scheme.scala +++ b/tests/pos/main-method-scheme.scala @@ -18,7 +18,7 @@ end MainAnnotation //Sample main class, can be freely implemented: -class _main(progName: String, args: Array[String], docComment: String) extends MainAnnotation: +class main(progName: String, args: Array[String], docComment: String) extends MainAnnotation: def this() = this("", Array(), "") @@ -102,7 +102,7 @@ class _main(progName: String, args: Array[String], docComment: String) extends M else true end done -end _main +end main // Sample main method @@ -118,7 +118,7 @@ end myProgram object add: def main(args: Array[String]) = - val cmd = new _main("add", args, "Adds two numbers") + val cmd = new main("add", args, "Adds two numbers") val arg1 = cmd.getArg[Int]("num", summon[cmd.ArgumentParser[Int]]) val arg2 = cmd.getArg[Int]("inc", summon[cmd.ArgumentParser[Int]], Some(1)) if cmd.done() then myProgram.add(arg1(), arg2()) From d7f76078aa48874f93557634a6df29cee64f155e Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 23 Apr 2020 02:01:25 +0200 Subject: [PATCH 03/14] Fix type alias We forgot the type parameters, which caused from-tasty and pickling tests to fail. We should probably disallow these forms of aliases and check for this. --- tests/pos/main-method-scheme.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pos/main-method-scheme.scala b/tests/pos/main-method-scheme.scala index 5245fe65d4c1..05ba085f1a07 100644 --- a/tests/pos/main-method-scheme.scala +++ b/tests/pos/main-method-scheme.scala @@ -22,7 +22,7 @@ class main(progName: String, args: Array[String], docComment: String) extends Ma def this() = this("", Array(), "") - type ArgumentParser = util.FromString + type ArgumentParser[T] = util.FromString[T] /** A buffer of demanded argument names, plus * "?" if it has a default From 9ca7e5c681bdf82fc24070dafe9f7c765c1fb008 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 23 Apr 2020 12:04:34 +0200 Subject: [PATCH 04/14] An even more flexible scheme --- .../pos/main-method-scheme-class-based.scala | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 tests/pos/main-method-scheme-class-based.scala diff --git a/tests/pos/main-method-scheme-class-based.scala b/tests/pos/main-method-scheme-class-based.scala new file mode 100644 index 000000000000..b139c0edd965 --- /dev/null +++ b/tests/pos/main-method-scheme-class-based.scala @@ -0,0 +1,205 @@ +import annotation.StaticAnnotation +import collection.mutable + +/** MainAnnotation provides the functionality for a compiler-generated main class. + * It links a compiler-generated main method (call it compiler-main) to a user + * written main method (user-main). + * The protocol of calls from compiler-main is as follows: + * + * - create a `command` with the command line arguments, + * - for each parameter of user-main, a call to `command.argGetter`, + * or `command.argsGetter` if is a final varargs parameter, + * - a call to `command.run` with the closure of user-main applied to all arguments. + */ +trait MainAnnotation extends StaticAnnotation: + + /** The class used for argument string parsing. E.g. `scala.util.FromString`, + * but could be something else + */ + type ArgumentParser[T] + + /** The required result type of the main function */ + type ResultType + + /** A class representing a command to run */ + abstract class Command: + + /** The getter for the next simple argument */ + def argGetter[T](argName: String, fromString: ArgumentParser[T], defaultValue: Option[T] = None): () => T + + /** The getter for a final varargs argument */ + def argsGetter[T](argName: String, fromString: ArgumentParser[T]): () => List[T] + + /** Run function `f()` if all arguments are valid, + * or print usage information and/or error messages. + */ + def run(f: => ResultType, progName: String, docComment: String): Unit + end Command + + /** A new command with arguments from `args` */ + def command(args: Array[String]): Command +end MainAnnotation + +//Sample main class, can be freely implemented: + +class main extends MainAnnotation: + + type ArgumentParser[T] = util.FromString[T] + type ResultType = Any + + def command(args: Array[String]): Command = new Command: + + /** A buffer of demanded argument names, plus + * "?" if it has a default + * "*" if it is a vararg + * "" otherwise + */ + var argInfos = new mutable.ListBuffer[(String, String)] + + /** A buffer for all errors */ + var errors = new mutable.ListBuffer[String] + + /** Issue an error, and return an uncallable getter */ + def error(msg: String): () => Nothing = + errors += msg + () => assertFail("trying to get invalid argument") + + /** The next argument index */ + var argIdx: Int = 0 + + def argAt(idx: Int): Option[String] = + if idx < args.length then Some(args(idx)) else None + + def nextPositionalArg(): Option[String] = + while argIdx < args.length && args(argIdx).startsWith("--") do argIdx += 2 + val result = argAt(argIdx) + argIdx += 1 + result + + def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = + p.fromStringOption(arg) match + case Some(t) => () => t + case None => error(s"invalid argument for $argName: $arg") + + def argGetter[T](argName: String, p: ArgumentParser[T], defaultValue: Option[T] = None): () => T = + argInfos += ((argName, if defaultValue.isDefined then "?" else "")) + val idx = args.indexOf(s"--$argName") + val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() + argOpt match + case Some(arg) => convert(argName, arg, p) + case None => defaultValue match + case Some(t) => () => t + case None => error(s"missing argument for $argName") + + def argsGetter[T](argName: String, fromString: ArgumentParser[T]): () => List[T] = + argInfos += ((argName, "*")) + def recur(): List[() => T] = nextPositionalArg() match + case Some(arg) => convert(arg, argName, fromString) :: recur() + case None => Nil + val fns = recur() + () => fns.map(_()) + + def run(f: => ResultType, progName: String, docComment: String): Unit = + def usage(): Unit = + println(s"Usage: $progName ${argInfos.map(_ + _).mkString(" ")}") + + def explain(): Unit = + if docComment.nonEmpty then println(docComment) // todo: process & format doc comment + + def flagUnused(): Unit = nextPositionalArg() match + case Some(arg) => + error(s"unused argument: $arg") + flagUnused() + case None => + for + arg <- args + if arg.startsWith("--") && !argInfos.map(_._1).contains(arg.drop(2)) + do + error(s"unknown argument name: $arg") + end flagUnused + + if args.isEmpty || args.contains("--help") then + usage() + explain() + else + flagUnused() + if errors.nonEmpty then + for msg <- errors do println(s"Error: $msg") + usage() + else f match + case n: Int if n < 0 => System.exit(-n) + case _ => + end run + end command +end main + +// Sample main method + +object myProgram: + + /** Adds two numbers */ + @main def add(num: Int, inc: Int = 1): Unit = + println(s"$num + $inc = ${num + inc}") + +end myProgram + +// Compiler generated code: + +object add extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) + val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]], Some(1)) + cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") +end add + +/** --- Some scenarios ---------------------------------------- + +> java add 2 3 +2 + 3 = 5 +> java add 4 +4 + 1 = 5 +> java add --num 10 --inc -2 +10 + -2 = 8 +> java add --num 10 +10 + 1 = 11 +> java add --help +Usage: add num inc? +Adds two numbers +> java add +Usage: add num inc? +Adds two numbers +> java add 1 2 3 4 +Error: unused argument: 3 +Error: unused argument: 4 +Usage: add num inc? +> java add -n 1 -i 10 +Error: invalid argument for num: -n +Error: unused argument: -i +Error: unused argument: 10 +Usage: add num inc? +> java add --n 1 --i 10 +Error: missing argument for num +Error: unknown argument name: --n +Error: unknown argument name: --i +Usage: add num inc? +> java add true 10 +Error: invalid argument for num: true +Usage: add num inc? +> java add true false +Error: invalid argument for num: true +Error: invalid argument for inc: false +Usage: add num inc? +> java add true false 10 +Error: invalid argument for num: true +Error: invalid argument for inc: false +Error: unused argument: 10 +Usage: add num inc? +> java add --inc 10 --num 20 +20 + 10 = 30 +> java add binary 10 01 +Error: invalid argument for num: binary +Error: unused argument: 01 +Usage: add num inc? + +*/ \ No newline at end of file From 3e3ce5be257021ffee5ef97c03747673793bfb98 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 23 Apr 2020 12:25:35 +0200 Subject: [PATCH 05/14] Reorder MainAnnot members --- tests/pos/main-method-scheme-class-based.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/pos/main-method-scheme-class-based.scala b/tests/pos/main-method-scheme-class-based.scala index b139c0edd965..79970fd89c09 100644 --- a/tests/pos/main-method-scheme-class-based.scala +++ b/tests/pos/main-method-scheme-class-based.scala @@ -21,6 +21,9 @@ trait MainAnnotation extends StaticAnnotation: /** The required result type of the main function */ type ResultType + /** A new command with arguments from `args` */ + def command(args: Array[String]): Command + /** A class representing a command to run */ abstract class Command: @@ -35,9 +38,6 @@ trait MainAnnotation extends StaticAnnotation: */ def run(f: => ResultType, progName: String, docComment: String): Unit end Command - - /** A new command with arguments from `args` */ - def command(args: Array[String]): Command end MainAnnotation //Sample main class, can be freely implemented: From 08a42b27356c97979521d4a03ab8c189db907b7d Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 23 Apr 2020 12:43:47 +0200 Subject: [PATCH 06/14] Fix typos and improve namings --- .../pos/main-method-scheme-class-based.scala | 40 +++++++++---------- tests/pos/main-method-scheme.scala | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/pos/main-method-scheme-class-based.scala b/tests/pos/main-method-scheme-class-based.scala index 79970fd89c09..4dece92c9817 100644 --- a/tests/pos/main-method-scheme-class-based.scala +++ b/tests/pos/main-method-scheme-class-based.scala @@ -19,7 +19,7 @@ trait MainAnnotation extends StaticAnnotation: type ArgumentParser[T] /** The required result type of the main function */ - type ResultType + type MainResultType /** A new command with arguments from `args` */ def command(args: Array[String]): Command @@ -27,16 +27,16 @@ trait MainAnnotation extends StaticAnnotation: /** A class representing a command to run */ abstract class Command: - /** The getter for the next simple argument */ + /** The getter for the next argument of type `T` */ def argGetter[T](argName: String, fromString: ArgumentParser[T], defaultValue: Option[T] = None): () => T - /** The getter for a final varargs argument */ - def argsGetter[T](argName: String, fromString: ArgumentParser[T]): () => List[T] + /** The getter for a final varargs argument of type `T*` */ + def argsGetter[T](argName: String, fromString: ArgumentParser[T]): () => Seq[T] - /** Run function `f()` if all arguments are valid, + /** Run `program` if all arguments are valid, * or print usage information and/or error messages. */ - def run(f: => ResultType, progName: String, docComment: String): Unit + def run(program: => MainResultType, progName: String, docComment: String): Unit end Command end MainAnnotation @@ -45,7 +45,7 @@ end MainAnnotation class main extends MainAnnotation: type ArgumentParser[T] = util.FromString[T] - type ResultType = Any + type MainResultType = Any def command(args: Array[String]): Command = new Command: @@ -54,29 +54,29 @@ class main extends MainAnnotation: * "*" if it is a vararg * "" otherwise */ - var argInfos = new mutable.ListBuffer[(String, String)] + private var argInfos = new mutable.ListBuffer[(String, String)] /** A buffer for all errors */ - var errors = new mutable.ListBuffer[String] + private var errors = new mutable.ListBuffer[String] /** Issue an error, and return an uncallable getter */ - def error(msg: String): () => Nothing = + private def error(msg: String): () => Nothing = errors += msg () => assertFail("trying to get invalid argument") /** The next argument index */ - var argIdx: Int = 0 + private var argIdx: Int = 0 - def argAt(idx: Int): Option[String] = + private def argAt(idx: Int): Option[String] = if idx < args.length then Some(args(idx)) else None - def nextPositionalArg(): Option[String] = + private def nextPositionalArg(): Option[String] = while argIdx < args.length && args(argIdx).startsWith("--") do argIdx += 2 val result = argAt(argIdx) argIdx += 1 result - def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = + private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = p.fromStringOption(arg) match case Some(t) => () => t case None => error(s"invalid argument for $argName: $arg") @@ -91,15 +91,15 @@ class main extends MainAnnotation: case Some(t) => () => t case None => error(s"missing argument for $argName") - def argsGetter[T](argName: String, fromString: ArgumentParser[T]): () => List[T] = + def argsGetter[T](argName: String, p: ArgumentParser[T]): () => Seq[T] = argInfos += ((argName, "*")) - def recur(): List[() => T] = nextPositionalArg() match - case Some(arg) => convert(arg, argName, fromString) :: recur() + def remainingArgGetters(): List[() => T] = nextPositionalArg() match + case Some(arg) => convert(arg, argName, p) :: remainingArgGetters() case None => Nil - val fns = recur() - () => fns.map(_()) + val getters = remainingArgGetters() + () => getters.map(_()) - def run(f: => ResultType, progName: String, docComment: String): Unit = + def run(f: => MainResultType, progName: String, docComment: String): Unit = def usage(): Unit = println(s"Usage: $progName ${argInfos.map(_ + _).mkString(" ")}") diff --git a/tests/pos/main-method-scheme.scala b/tests/pos/main-method-scheme.scala index 05ba085f1a07..ce8ea3ac3c19 100644 --- a/tests/pos/main-method-scheme.scala +++ b/tests/pos/main-method-scheme.scala @@ -109,7 +109,7 @@ end main object myProgram: /** Adds two numbers */ - @_main def add(num: Int, inc: Int = 1) = + @main def add(num: Int, inc: Int = 1) = println(s"$num + $inc = ${num + inc}") end myProgram From a929be5e7448d5c04d2ef09569f3233e30cd604e Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 23 Apr 2020 14:37:10 +0200 Subject: [PATCH 07/14] Add customization of wrapper class generation --- tests/pos/main-method-scheme-generic.scala | 247 +++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 tests/pos/main-method-scheme-generic.scala diff --git a/tests/pos/main-method-scheme-generic.scala b/tests/pos/main-method-scheme-generic.scala new file mode 100644 index 000000000000..77bc70480ba2 --- /dev/null +++ b/tests/pos/main-method-scheme-generic.scala @@ -0,0 +1,247 @@ +import annotation.StaticAnnotation +import collection.mutable + +/** MainAnnotation provides the functionality for a compiler-generated wrapper class. + * It links a compiler-generated main method (call it compiler-main) to a user + * written main method (user-main). + * The protocol of calls from compiler-main is as follows: + * + * - create a `command` with the command line arguments, + * - for each parameter of user-main, a call to `command.argGetter`, + * or `command.argsGetter` if is a final varargs parameter, + * - a call to `command.run` with the closure of user-main applied to all arguments. + * + * The wrapper class has this outline: + * + * object : + * def (args: ) = + * ... + * + * Here the `` name is the result of an inline call to `wrapperClassName` + * and `` is the result of an inline call to `wrapperMethodName` + * + * The result type of `
` is the same as the result type of `run` + * in the concrete implementation of `MainAnnotation`. + */ +trait MainAnnotation extends StaticAnnotation: + + /** The class used for argument string parsing. E.g. `scala.util.FromString`, + * but could be something else + */ + type ArgumentParser[T] + + /** The required result type of the user-defined main function */ + type MainResultType + + /** The type of the command line arguments. E.g., for Java main methods: `Array[String]` */ + type CommandLineArgs + + /** The return type of the generated command. E.g., for Java main methods: `Unit` */ + type CommandResult + + /** A new command with arguments from `args` */ + def command(args: CommandLineArgs): Command + + /** A class representing a command to run */ + abstract class Command: + + /** The getter for the next argument of type `T` */ + def argGetter[T](argName: String, fromString: ArgumentParser[T], defaultValue: Option[T] = None): () => T + + /** The getter for a final varargs argument of type `T*` */ + def argsGetter[T](argName: String, fromString: ArgumentParser[T]): () => Seq[T] + + /** Run `program` if all arguments are valid, + * or print usage information and/or error messages. + * @param program the program to run + * @param mainName the fully qualified name of the user-defined main method + * @param docComment the doc comment of the user-defined main method + */ + def run(program: => MainResultType, mainName: String, docComment: String): CommandResult + end Command + + // Compile-time abstractions to control wrapper class: + + /** The name to use for the static wrapper class + * @param mainName the fully qualified name of the user-defined main method + */ + inline def wrapperClassName(mainName: String): String + + /** The name to use for the main method in the static wrapper class + * @param mainName the fully qualified name of the user-defined main method + */ + inline def wrapperMethodName(mainName: String): String + +end MainAnnotation + +//Sample main class, can be freely implemented: + +class main extends MainAnnotation: + + type ArgumentParser[T] = util.FromString[T] + type MainResultType = Any + type CommandLineArgs = Array[String] + type CommandResult = Unit + + def command(args: Array[String]) = new Command: + + /** A buffer of demanded argument names, plus + * "?" if it has a default + * "*" if it is a vararg + * "" otherwise + */ + private var argInfos = new mutable.ListBuffer[(String, String)] + + /** A buffer for all errors */ + private var errors = new mutable.ListBuffer[String] + + /** Issue an error, and return an uncallable getter */ + private def error(msg: String): () => Nothing = + errors += msg + () => assertFail("trying to get invalid argument") + + /** The next argument index */ + private var argIdx: Int = 0 + + private def argAt(idx: Int): Option[String] = + if idx < args.length then Some(args(idx)) else None + + private def nextPositionalArg(): Option[String] = + while argIdx < args.length && args(argIdx).startsWith("--") do argIdx += 2 + val result = argAt(argIdx) + argIdx += 1 + result + + private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = + p.fromStringOption(arg) match + case Some(t) => () => t + case None => error(s"invalid argument for $argName: $arg") + + def argGetter[T](argName: String, p: ArgumentParser[T], defaultValue: Option[T] = None): () => T = + argInfos += ((argName, if defaultValue.isDefined then "?" else "")) + val idx = args.indexOf(s"--$argName") + val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() + argOpt match + case Some(arg) => convert(argName, arg, p) + case None => defaultValue match + case Some(t) => () => t + case None => error(s"missing argument for $argName") + + def argsGetter[T](argName: String, p: ArgumentParser[T]): () => Seq[T] = + argInfos += ((argName, "*")) + def remainingArgGetters(): List[() => T] = nextPositionalArg() match + case Some(arg) => convert(arg, argName, p) :: remainingArgGetters() + case None => Nil + val getters = remainingArgGetters() + () => getters.map(_()) + + def run(f: => MainResultType, mainName: String, docComment: String): Unit = + def usage(): Unit = + println(s"Usage: java ${wrapperClassName(mainName)} ${argInfos.map(_ + _).mkString(" ")}") + + def explain(): Unit = + if docComment.nonEmpty then println(docComment) // todo: process & format doc comment + + def flagUnused(): Unit = nextPositionalArg() match + case Some(arg) => + error(s"unused argument: $arg") + flagUnused() + case None => + for + arg <- args + if arg.startsWith("--") && !argInfos.map(_._1).contains(arg.drop(2)) + do + error(s"unknown argument name: $arg") + end flagUnused + + if args.isEmpty || args.contains("--help") then + usage() + explain() + else + flagUnused() + if errors.nonEmpty then + for msg <- errors do println(s"Error: $msg") + usage() + else f match + case n: Int if n < 0 => System.exit(-n) + case _ => + end run + end command + + inline def wrapperClassName(mainName: String): String = + mainName.drop(mainName.lastIndexOf('.') + 1) + + inline def wrapperMethodName(mainName: String): String = "main" + +end main + +// Sample main method + +object myProgram: + + /** Adds two numbers */ + @main def add(num: Int, inc: Int = 1): Unit = + println(s"$num + $inc = ${num + inc}") + +end myProgram + +// Compiler generated code: + +object add extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) + val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]], Some(1)) + cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") +end add + +/** --- Some scenarios ---------------------------------------- + +> java add 2 3 +2 + 3 = 5 +> java add 4 +4 + 1 = 5 +> java add --num 10 --inc -2 +10 + -2 = 8 +> java add --num 10 +10 + 1 = 11 +> java add --help +Usage: java add num inc? +Adds two numbers +> java add +Usage: java add num inc? +Adds two numbers +> java add 1 2 3 4 +Error: unused argument: 3 +Error: unused argument: 4 +Usage: java add num inc? +> java add -n 1 -i 10 +Error: invalid argument for num: -n +Error: unused argument: -i +Error: unused argument: 10 +Usage: java add num inc? +> java add --n 1 --i 10 +Error: missing argument for num +Error: unknown argument name: --n +Error: unknown argument name: --i +Usage: java add num inc? +> java add true 10 +Error: invalid argument for num: true +Usage: java add num inc? +> java add true false +Error: invalid argument for num: true +Error: invalid argument for inc: false +Usage: java add num inc? +> java add true false 10 +Error: invalid argument for num: true +Error: invalid argument for inc: false +Error: unused argument: 10 +Usage: java add num inc? +> java add --inc 10 --num 20 +20 + 10 = 30 +> java add binary 10 01 +Error: invalid argument for num: binary +Error: unused argument: 01 +Usage: java add num inc? + +*/ \ No newline at end of file From 3a09496ec54c1144ee56abc8dee89e868b80fd75 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 23 Apr 2020 16:40:04 +0200 Subject: [PATCH 08/14] Two refinements - Communicate a single fully qualified wrapper method name instead of classname/methodname - Allow to add an annotation to a wrapper method --- tests/pos/main-method-scheme-generic.scala | 28 ++++++++++++---------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/tests/pos/main-method-scheme-generic.scala b/tests/pos/main-method-scheme-generic.scala index 77bc70480ba2..c5236dca6958 100644 --- a/tests/pos/main-method-scheme-generic.scala +++ b/tests/pos/main-method-scheme-generic.scala @@ -1,4 +1,4 @@ -import annotation.StaticAnnotation +import annotation.{Annotation, StaticAnnotation} import collection.mutable /** MainAnnotation provides the functionality for a compiler-generated wrapper class. @@ -60,17 +60,15 @@ trait MainAnnotation extends StaticAnnotation: def run(program: => MainResultType, mainName: String, docComment: String): CommandResult end Command - // Compile-time abstractions to control wrapper class: - - /** The name to use for the static wrapper class + /** The fully qualified name to use for the static wrapper method * @param mainName the fully qualified name of the user-defined main method */ - inline def wrapperClassName(mainName: String): String + inline def wrapperName(mainName: String): String - /** The name to use for the main method in the static wrapper class - * @param mainName the fully qualified name of the user-defined main method + /** An annotation type with which the wrapper method is decorated. + * No annotation is generated of the type is left abstract. */ - inline def wrapperMethodName(mainName: String): String + type WrapperAnnotation <: Annotation end MainAnnotation @@ -137,7 +135,9 @@ class main extends MainAnnotation: def run(f: => MainResultType, mainName: String, docComment: String): Unit = def usage(): Unit = - println(s"Usage: java ${wrapperClassName(mainName)} ${argInfos.map(_ + _).mkString(" ")}") + val cmd = mainName.dropRight(".main".length) + val params = argInfos.map(_ + _).mkString(" ") + println(s"Usage: java $cmd $params") def explain(): Unit = if docComment.nonEmpty then println(docComment) // todo: process & format doc comment @@ -168,13 +168,15 @@ class main extends MainAnnotation: end run end command - inline def wrapperClassName(mainName: String): String = - mainName.drop(mainName.lastIndexOf('.') + 1) + inline def wrapperName(mainName: String): String = + s"${mainName.drop(mainName.lastIndexOf('.') + 1)}.main" - inline def wrapperMethodName(mainName: String): String = "main" + override type WrapperAnnotation = EntryPoint end main +class EntryPoint extends Annotation + // Sample main method object myProgram: @@ -188,7 +190,7 @@ end myProgram // Compiler generated code: object add extends main: - def main(args: Array[String]) = + @EntryPoint def main(args: Array[String]) = val cmd = command(args) val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]], Some(1)) From 41a483393fee2ae1f4cf9ee6ab93889191fa72c3 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 23 Apr 2020 18:05:07 +0200 Subject: [PATCH 09/14] Rename argGetters --- tests/pos/main-method-scheme-generic.scala | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/pos/main-method-scheme-generic.scala b/tests/pos/main-method-scheme-generic.scala index c5236dca6958..65bb17b1da63 100644 --- a/tests/pos/main-method-scheme-generic.scala +++ b/tests/pos/main-method-scheme-generic.scala @@ -7,8 +7,8 @@ import collection.mutable * The protocol of calls from compiler-main is as follows: * * - create a `command` with the command line arguments, - * - for each parameter of user-main, a call to `command.argGetter`, - * or `command.argsGetter` if is a final varargs parameter, + * - for each parameter of user-main, a call to `command.nextArgGetter`, + * or `command.finalArgsGetter` if is a final varargs parameter, * - a call to `command.run` with the closure of user-main applied to all arguments. * * The wrapper class has this outline: @@ -46,10 +46,10 @@ trait MainAnnotation extends StaticAnnotation: abstract class Command: /** The getter for the next argument of type `T` */ - def argGetter[T](argName: String, fromString: ArgumentParser[T], defaultValue: Option[T] = None): () => T + def nextArgGetter[T](argName: String, fromString: ArgumentParser[T], defaultValue: Option[T] = None): () => T /** The getter for a final varargs argument of type `T*` */ - def argsGetter[T](argName: String, fromString: ArgumentParser[T]): () => Seq[T] + def finalArgsGetter[T](argName: String, fromString: ArgumentParser[T]): () => Seq[T] /** Run `program` if all arguments are valid, * or print usage information and/or error messages. @@ -66,7 +66,7 @@ trait MainAnnotation extends StaticAnnotation: inline def wrapperName(mainName: String): String /** An annotation type with which the wrapper method is decorated. - * No annotation is generated of the type is left abstract. + * No annotation is generated if the type is left abstract. */ type WrapperAnnotation <: Annotation @@ -115,7 +115,7 @@ class main extends MainAnnotation: case Some(t) => () => t case None => error(s"invalid argument for $argName: $arg") - def argGetter[T](argName: String, p: ArgumentParser[T], defaultValue: Option[T] = None): () => T = + def nextArgGetter[T](argName: String, p: ArgumentParser[T], defaultValue: Option[T] = None): () => T = argInfos += ((argName, if defaultValue.isDefined then "?" else "")) val idx = args.indexOf(s"--$argName") val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() @@ -125,7 +125,7 @@ class main extends MainAnnotation: case Some(t) => () => t case None => error(s"missing argument for $argName") - def argsGetter[T](argName: String, p: ArgumentParser[T]): () => Seq[T] = + def finalArgsGetter[T](argName: String, p: ArgumentParser[T]): () => Seq[T] = argInfos += ((argName, "*")) def remainingArgGetters(): List[() => T] = nextPositionalArg() match case Some(arg) => convert(arg, argName, p) :: remainingArgGetters() @@ -192,8 +192,8 @@ end myProgram object add extends main: @EntryPoint def main(args: Array[String]) = val cmd = command(args) - val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) - val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]], Some(1)) + val arg1 = cmd.nextArgGetter[Int]("num", summon[ArgumentParser[Int]]) + val arg2 = cmd.nextArgGetter[Int]("inc", summon[ArgumentParser[Int]], Some(1)) cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") end add From 59a04ebbba0055126f5339c0f7350a74c1363346 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 23 Apr 2020 19:35:00 +0200 Subject: [PATCH 10/14] Change nomenclature Change nomenclature from main methods and commands to entry points and wrappers, since the scheme is now more general than just main method generation. --- tests/pos/main-method-scheme-generic.scala | 122 ++++++++++----------- 1 file changed, 60 insertions(+), 62 deletions(-) diff --git a/tests/pos/main-method-scheme-generic.scala b/tests/pos/main-method-scheme-generic.scala index 65bb17b1da63..5aa9cfa6a404 100644 --- a/tests/pos/main-method-scheme-generic.scala +++ b/tests/pos/main-method-scheme-generic.scala @@ -1,49 +1,62 @@ import annotation.{Annotation, StaticAnnotation} import collection.mutable -/** MainAnnotation provides the functionality for a compiler-generated wrapper class. - * It links a compiler-generated main method (call it compiler-main) to a user - * written main method (user-main). - * The protocol of calls from compiler-main is as follows: +/** This class provides a framework for compiler-generated wrappers + * of "entry-point" methods. It routes and transforms parameters and results + * between a compiler-generated wrapper method that has calling conventions + * fixed by a framework and a user-written entry-point method that can have + * flexible argument lists. It allows the wrapper to provide help and usage + * information as well as customised error messages if the actual wrapper arguments + * do not match the expected entry-point parameters. * - * - create a `command` with the command line arguments, - * - for each parameter of user-main, a call to `command.nextArgGetter`, - * or `command.finalArgsGetter` if is a final varargs parameter, - * - a call to `command.run` with the closure of user-main applied to all arguments. + * The protocol of calls from the wrapper method is as follows: + * + * 1. Create a `call` instance with the wrapper argument. + * 2. For each parameter of the entry-point, invoke `call.nextArgGetter`, + * or `call.finalArgsGetter` if is a final varargs parameter. + * 3. Invoke `call.run` with the closure of entry-point applied to all arguments. * * The wrapper class has this outline: * * object : - * def (args: ) = + * @WrapperAnnotation def (args: ) = * ... * - * Here the `` name is the result of an inline call to `wrapperClassName` - * and `` is the result of an inline call to `wrapperMethodName` - * - * The result type of `
` is the same as the result type of `run` - * in the concrete implementation of `MainAnnotation`. + * Here `` and `` are obtained from an + * inline call to the `wrapperName` method. */ -trait MainAnnotation extends StaticAnnotation: +trait EntryPointAnnotation extends StaticAnnotation: - /** The class used for argument string parsing. E.g. `scala.util.FromString`, - * but could be something else + /** The class used for argument parsing. E.g. `scala.util.FromString`, if + * arguments are strings, but it could be something else. */ type ArgumentParser[T] /** The required result type of the user-defined main function */ - type MainResultType + type EntryPointResult + + /** The type of the wrapper argument. E.g., for Java main methods: `Array[String]` */ + type WrapperArgs - /** The type of the command line arguments. E.g., for Java main methods: `Array[String]` */ - type CommandLineArgs + /** The return type of the generated wrapper. E.g., for Java main methods: `Unit` */ + type WrapperResult - /** The return type of the generated command. E.g., for Java main methods: `Unit` */ - type CommandResult + /** An annotation type with which the wrapper method is decorated. + * No annotation is generated if the type is left abstract. + */ + type WrapperAnnotation <: Annotation + + /** The fully qualified name (relative to enclosing package) to + * use for the static wrapper method. + * @param mainName the fully qualified name of the user-defined main method + */ + inline def wrapperName(mainName: String): String - /** A new command with arguments from `args` */ - def command(args: CommandLineArgs): Command + /** A new wrapper call with arguments from `args` */ + def call(args: WrapperArgs): Call - /** A class representing a command to run */ - abstract class Command: + /** A class representing a wrapper call */ + abstract class Call: /** The getter for the next argument of type `T` */ def nextArgGetter[T](argName: String, fromString: ArgumentParser[T], defaultValue: Option[T] = None): () => T @@ -51,37 +64,26 @@ trait MainAnnotation extends StaticAnnotation: /** The getter for a final varargs argument of type `T*` */ def finalArgsGetter[T](argName: String, fromString: ArgumentParser[T]): () => Seq[T] - /** Run `program` if all arguments are valid, + /** Run `entryPoint` if all arguments are valid, * or print usage information and/or error messages. - * @param program the program to run - * @param mainName the fully qualified name of the user-defined main method - * @param docComment the doc comment of the user-defined main method + * @param entryPointApply the applied entry-point to run + * @param entryPointName the fully qualified name of the entry-point method + * @param docComment the doc comment of the entry-point method */ - def run(program: => MainResultType, mainName: String, docComment: String): CommandResult - end Command - - /** The fully qualified name to use for the static wrapper method - * @param mainName the fully qualified name of the user-defined main method - */ - inline def wrapperName(mainName: String): String - - /** An annotation type with which the wrapper method is decorated. - * No annotation is generated if the type is left abstract. - */ - type WrapperAnnotation <: Annotation - -end MainAnnotation + def run(entryPointApply: => EntryPointResult, entryPointName: String, docComment: String): WrapperResult + end Call +end EntryPointAnnotation //Sample main class, can be freely implemented: -class main extends MainAnnotation: +class main extends EntryPointAnnotation: type ArgumentParser[T] = util.FromString[T] - type MainResultType = Any - type CommandLineArgs = Array[String] - type CommandResult = Unit + type EntryPointResult = Unit + type WrapperArgs = Array[String] + type WrapperResult = Unit - def command(args: Array[String]) = new Command: + def call(args: Array[String]) = new Call: /** A buffer of demanded argument names, plus * "?" if it has a default @@ -133,9 +135,9 @@ class main extends MainAnnotation: val getters = remainingArgGetters() () => getters.map(_()) - def run(f: => MainResultType, mainName: String, docComment: String): Unit = + def run(entryPointApply: => EntryPointResult, entryPointName: String, docComment: String): Unit = def usage(): Unit = - val cmd = mainName.dropRight(".main".length) + val cmd = entryPointName.dropRight(".main".length) val params = argInfos.map(_ + _).mkString(" ") println(s"Usage: java $cmd $params") @@ -162,11 +164,9 @@ class main extends MainAnnotation: if errors.nonEmpty then for msg <- errors do println(s"Error: $msg") usage() - else f match - case n: Int if n < 0 => System.exit(-n) - case _ => + else f end run - end command + end call inline def wrapperName(mainName: String): String = s"${mainName.drop(mainName.lastIndexOf('.') + 1)}.main" @@ -175,8 +175,6 @@ class main extends MainAnnotation: end main -class EntryPoint extends Annotation - // Sample main method object myProgram: @@ -190,11 +188,11 @@ end myProgram // Compiler generated code: object add extends main: - @EntryPoint def main(args: Array[String]) = - val cmd = command(args) - val arg1 = cmd.nextArgGetter[Int]("num", summon[ArgumentParser[Int]]) - val arg2 = cmd.nextArgGetter[Int]("inc", summon[ArgumentParser[Int]], Some(1)) - cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") + def main(args: Array[String]) = + val cll = call(args) + val arg1 = cll.nextArgGetter[Int]("num", summon[ArgumentParser[Int]]) + val arg2 = cll.nextArgGetter[Int]("inc", summon[ArgumentParser[Int]], Some(1)) + cll.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") end add /** --- Some scenarios ---------------------------------------- From 89ad95fe7578c5c312b1e6eadfc73acfa4c5875b Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 26 Apr 2020 19:07:06 +0200 Subject: [PATCH 11/14] Prototype for stackable entry point wrappers --- tests/pos/decorators/DocComment.scala | 26 +++ tests/pos/decorators/EntryPoint.scala | 183 +++++++++++++++++++++ tests/pos/decorators/Test.scala | 30 ++++ tests/pos/decorators/WordSplitter.scala | 30 ++++ tests/pos/decorators/main.scala | 124 ++++++++++++++ tests/pos/decorators/sample-adapters.scala | 34 ++++ tests/pos/decorators/sample-program.scala | 51 ++++++ 7 files changed, 478 insertions(+) create mode 100644 tests/pos/decorators/DocComment.scala create mode 100644 tests/pos/decorators/EntryPoint.scala create mode 100644 tests/pos/decorators/Test.scala create mode 100644 tests/pos/decorators/WordSplitter.scala create mode 100644 tests/pos/decorators/main.scala create mode 100644 tests/pos/decorators/sample-adapters.scala create mode 100644 tests/pos/decorators/sample-program.scala diff --git a/tests/pos/decorators/DocComment.scala b/tests/pos/decorators/DocComment.scala new file mode 100644 index 000000000000..85b30fbce393 --- /dev/null +++ b/tests/pos/decorators/DocComment.scala @@ -0,0 +1,26 @@ +/** Represents a doc comment, splitting it into `body` and `tags` + * `tags` are all lines starting with an `@`, where the tag thats starts + * with `@` is paired with the text that follows, up to the next + * tagged line. + * `body` what comes before the first tagged line + */ +case class DocComment(body: String, tags: Map[String, List[String]]) +object DocComment: + def fromString(str: String): DocComment = + val lines = str.linesIterator.toList + def tagged(line: String): Option[(String, String)] = + val ws = WordSplitter(line) + val tag = ws.next() + if tag.startsWith("@") then Some(tag, line.drop(ws.nextOffset)) + else None + val (bodyLines, taggedLines) = lines.span(tagged(_).isEmpty) + def tagPairs(lines: List[String]): List[(String, String)] = lines match + case line :: lines1 => + val (tag, descPrefix) = tagged(line).get + val (untaggedLines, lines2) = lines1.span(tagged(_).isEmpty) + val following = untaggedLines.map(_.dropWhile(_ <= ' ')) + (tag, (descPrefix :: following).mkString("\n")) :: tagPairs(lines2) + case _ => + Nil + DocComment(bodyLines.mkString("\n"), tagPairs(taggedLines).groupMap(_._1)(_._2)) +end DocComment \ No newline at end of file diff --git a/tests/pos/decorators/EntryPoint.scala b/tests/pos/decorators/EntryPoint.scala new file mode 100644 index 000000000000..f87519fabee0 --- /dev/null +++ b/tests/pos/decorators/EntryPoint.scala @@ -0,0 +1,183 @@ +import collection.mutable + +/** A framework for defining stackable entry point wrappers */ +object EntryPoint: + + /** A base trait for wrappers of entry points. + * Sub-traits: Annotation#Wrapper + * Adapter#Wrapper + */ + sealed trait Wrapper + + /** This class provides a framework for compiler-generated wrappers + * of "entry-point" methods. It routes and transforms parameters and results + * between a compiler-generated wrapper method that has calling conventions + * fixed by a framework and a user-written entry-point method that can have + * flexible argument lists. It allows the wrapper to provide help and usage + * information as well as customised error messages if the actual wrapper arguments + * do not match the expected entry-point parameters. + * + * The protocol of calls from the wrapper method is as follows: + * + * 1. Create a `call` instance with the wrapper argument. + * 2. For each parameter of the entry-point, invoke `call.nextArgGetter`, + * or `call.finalArgsGetter` if is a final varargs parameter. + * 3. Invoke `call.run` with the closure of entry-point applied to all arguments. + * + * The wrapper class has this outline: + * + * object : + * @WrapperAnnotation def (args: ) = + * ... + * + * Here `` and `` are obtained from an + * inline call to the `wrapperName` method. + */ + trait Annotation extends annotation.StaticAnnotation: + + /** The class used for argument parsing. E.g. `scala.util.FromString`, if + * arguments are strings, but it could be something else. + */ + type ArgumentParser[T] + + /** The required result type of the user-defined main function */ + type EntryPointResult + + /** The fully qualified name (relative to enclosing package) to + * use for the static wrapper method. + * @param entryPointName the fully qualified name of the user-defined entry point method + */ + inline def wrapperName(entryPointName: String): String + + /** Create an entry point wrapper. + * @param entryPointName the fully qualified name of the user-defined entry point method + * @param docComment the doc comment of the user-defined entry point method + */ + def wrapper(entryPointName: String, docComment: String): Wrapper + + /** Base class for descriptions of an entry point wrappers */ + abstract class Wrapper extends EntryPoint.Wrapper: + + /** The type of the wrapper argument. E.g., for Java main methods: `Array[String]` */ + type Argument + + /** The return type of the generated wrapper. E.g., for Java main methods: `Unit` */ + type Result + + /** An annotation type with which the wrapper method is decorated. + * No annotation is generated if the type is left abstract. + * Multiple annotations are generated if the type is an intersection of annotations. + */ + type WrapperAnnotation <: annotation.Annotation + + /** The fully qualified name of the user-defined entry point method that is wrapped */ + val entryPointName: String + + /** The doc comment of the user-defined entry point method that is wrapped */ + val docComment: String + + /** A new wrapper call with arguments from `args` */ + def call(arg: Argument): Call + + /** A class representing a wrapper call */ + abstract class Call: + + /** The getter for the next argument of type `T` */ + def nextArgGetter[T](argName: String, fromString: ArgumentParser[T], defaultValue: Option[T] = None): () => T + + /** The getter for a final varargs argument of type `T*` */ + def finalArgsGetter[T](argName: String, fromString: ArgumentParser[T]): () => Seq[T] + + /** Run `entryPointWithArgs` if all arguments are valid, + * or print usage information and/or error messages. + * @param entryPointWithArgs the applied entry-point to run + */ + def run(entryPointWithArgs: => EntryPointResult): Result + end Call + end Wrapper + end Annotation + + /** An annotation that generates an adapter of an entry point wrapper. + * An `EntryPoint.Adapter` annotation should always be written together + * with an `EntryPoint.Annotation` and the adapter should be given first. + * If several adapters are present, they are applied right to left. + * Example: + * + * @logged @transactional @main def f(...) + * + * This wraps the main method generated by @main first in a `transactional` + * wrapper and then in a `logged` wrapper. The result would look like this: + * + * $logged$wrapper.adapt { y => + * $transactional$wrapper.adapt { z => + * val cll = $main$wrapper.call(z) + * val arg1 = ... + * ... + * val argN = ... + * cll.run(...) + * } (y) + * } (x) + * + * where + * + * - $logged$wrapper, $transactional$wrapper, $main$wrapper are the wrappers + * created from @logged, @transactional, and @main, respectively. + * - `x` is the argument of the outer $logged$wrapper. + */ + trait Adapter extends annotation.StaticAnnotation: + + /** Creates a new wrapper around `wrapped` */ + def wrapper(wrapped: EntryPoint.Wrapper): Wrapper + + /** The wrapper class. A wrapper class must define a method `adapt` + * that maps unary functions to unary functions. A typical definition + * of `adapt` is: + * + * def adapt(f: A1 => B1)(a: A2): B2 = toB2(f(toA1(a))) + * + * This version of `adapt` converts its argument `a` to the wrapped + * function's argument type `A1`, applies the function, and converts + * the application's result back to type `B2`. `adapt` can also call + * the wrapped function only under a condition or call it multiple times. + * + * `adapt` can also be polymorphic. For instance: + * + * def adapt[R](f: A1 => R)(a: A2): R = f(toA1(a)) + * + * or + * + * def adapt[A, R](f: A => R)(a: A): R = { log(a); f(a) } + * + * Since `adapt` can be of many forms, the base class does not provide + * an abstract method that needs to be implemented in concrete wrapper + * classes. Instead it is checked that the types line up when adapt chains + * are assembled. + * + * I investigated an explicitly typed approach, but could not arrive at a + * solution that was clean and simple enough. If Scala had more dependent + * type support, it would be quite straightforward, i.e. generic `Wrapper` + * could be defined like this: + * + * class Wrapper(wrapped: EntryPoint.Wrapper): + * type Argument + * type Result + * def adapt(f: wrapped.Argument => wrapped.Result)(x: Argument): Result + * + * But to get this to work, we'd need support for types depending on their + * arguments, e.g. a type of the form `Wrapper(wrapped)`. That's an interesting + * avenue to pursue. Until that materializes I think it's preferable to + * keep the `adapt` type contract implicit (types are still checked when adapts + * are generated, of course). + */ + abstract class Wrapper extends EntryPoint.Wrapper: + /** The wrapper that this wrapped in turn by this wrapper */ + val wrapped: EntryPoint.Wrapper + + /** The wrapper of the entry point annotation that this wrapper + * wraps directly or indirectly + */ + def finalWrapped: EntryPoint.Annotation#Wrapper = wrapped match + case wrapped: EntryPoint.Adapter#Wrapper => wrapped.finalWrapped + case wrapped: EntryPoint.Annotation#Wrapper => wrapped + end Wrapper + end Adapter diff --git a/tests/pos/decorators/Test.scala b/tests/pos/decorators/Test.scala new file mode 100644 index 000000000000..1d821d2109d2 --- /dev/null +++ b/tests/pos/decorators/Test.scala @@ -0,0 +1,30 @@ +object Test: + def main(args: Array[String]) = + def testAdd(args: String) = + println(s"> java add $args") + add.main(args.split(" ")) + println() + def testAddAll(args: String) = + println(s"> java addAll $args") + addAll.main(args.split(" ")) + println() + + testAdd("2 3") + testAdd("4") + testAdd("--num 10 --inc -2") + testAdd("--num 10") + testAdd("--help") + testAdd("") + testAdd("1 2 3 4") + testAdd("-n 1 -i 2") + testAdd("true 10") + testAdd("true false") + testAdd("true false 10") + testAdd("--inc 10 --num 20") + testAdd("binary 10 01") + testAddAll("1 2 3 4 5") + testAddAll("--nums") + testAddAll("--nums 33 44") + testAddAll("true 1 2 3") + testAddAll("--help") +end Test \ No newline at end of file diff --git a/tests/pos/decorators/WordSplitter.scala b/tests/pos/decorators/WordSplitter.scala new file mode 100644 index 000000000000..4195fa73a220 --- /dev/null +++ b/tests/pos/decorators/WordSplitter.scala @@ -0,0 +1,30 @@ +/** An iterator to return words in a string while keeping tarck of their offsets */ +class WordSplitter(str: String, start: Int = 0, isSeparator: Char => Boolean = _ <= ' ') +extends Iterator[String]: + private var idx: Int = start + private var lastIdx: Int = start + private var word: String = _ + + private def skipSeparators() = + while idx < str.length && isSeparator(str(idx)) do + idx += 1 + + def lastWord = word + def lastOffset = lastIdx + + def nextOffset = + skipSeparators() + idx + + def next(): String = + skipSeparators() + lastIdx = idx + val b = new StringBuilder + while idx < str.length && !isSeparator(str(idx)) do + b += str(idx) + idx += 1 + word = b.toString + word + + def hasNext: Boolean = nextOffset < str.length +end WordSplitter \ No newline at end of file diff --git a/tests/pos/decorators/main.scala b/tests/pos/decorators/main.scala new file mode 100644 index 000000000000..a73760252049 --- /dev/null +++ b/tests/pos/decorators/main.scala @@ -0,0 +1,124 @@ +import collection.mutable + +/** A sample @main entry point annotation. + * Generates a main function. + */ +class main extends EntryPoint.Annotation: + + type ArgumentParser[T] = util.FromString[T] + type EntryPointResult = Unit + + inline def wrapperName(entryPointName: String): String = + s"${entryPointName.drop(entryPointName.lastIndexOf('.') + 1)}.main" + + def wrapper(name: String, doc: String): MainWrapper = new MainWrapper(name, doc) + + class MainWrapper(val entryPointName: String, val docComment: String) extends Wrapper: + type Argument = Array[String] + type Result = Unit + + def call(args: Array[String]) = new Call: + + /** A buffer of demanded argument names, plus + * "?" if it has a default + * "*" if it is a vararg + * "" otherwise + */ + private var argInfos = new mutable.ListBuffer[(String, String)] + + /** A buffer for all errors */ + private var errors = new mutable.ListBuffer[String] + + /** Issue an error, and return an uncallable getter */ + private def error(msg: String): () => Nothing = + errors += msg + () => assertFail("trying to get invalid argument") + + /** The next argument index */ + private var argIdx: Int = 0 + + private def argAt(idx: Int): Option[String] = + if idx < args.length then Some(args(idx)) else None + + private def nextPositionalArg(): Option[String] = + while argIdx < args.length && args(argIdx).startsWith("--") do argIdx += 2 + val result = argAt(argIdx) + argIdx += 1 + result + + private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = + p.fromStringOption(arg) match + case Some(t) => () => t + case None => error(s"invalid argument for $argName: $arg") + + def nextArgGetter[T](argName: String, p: ArgumentParser[T], defaultValue: Option[T] = None): () => T = + argInfos += ((argName, if defaultValue.isDefined then "?" else "")) + val idx = args.indexOf(s"--$argName") + val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() + argOpt match + case Some(arg) => convert(argName, arg, p) + case None => defaultValue match + case Some(t) => () => t + case None => error(s"missing argument for $argName") + + def finalArgsGetter[T](argName: String, p: ArgumentParser[T]): () => Seq[T] = + argInfos += ((argName, "*")) + def remainingArgGetters(): List[() => T] = nextPositionalArg() match + case Some(arg) => convert(argName, arg, p) :: remainingArgGetters() + case None => Nil + val getters = remainingArgGetters() + () => getters.map(_()) + + def run(entryPointWithArgs: => EntryPointResult): Unit = + lazy val DocComment(explanation, docTags) = DocComment.fromString(docComment) + + def usageString = + docTags.get("@usage") match + case Some(s :: _) => s + case _ => + val cmd = wrapperName(entryPointName).stripSuffix(".main") + val params = argInfos.map(_ + _).mkString(" ") + s"java $cmd $params" + + def printUsage() = println(s"Usage: $usageString") + + def explain(): Unit = + if explanation.nonEmpty then println(explanation) + printUsage() + docTags.get("@param") match + case Some(paramInfos) => + println("where") + for paramInfo <- paramInfos do + val ws = WordSplitter(paramInfo) + val name = ws.next() + val desc = paramInfo.drop(ws.nextOffset) + println(s" $name $desc") + case None => + end explain + + def flagUnused(): Unit = nextPositionalArg() match + case Some(arg) => + error(s"unused argument: $arg") + flagUnused() + case None => + for + arg <- args + if arg.startsWith("--") && !argInfos.map(_._1).contains(arg.drop(2)) + do + error(s"unknown argument name: $arg") + end flagUnused + + if args.contains("--help") then + explain() + else + flagUnused() + if errors.nonEmpty then + for msg <- errors do println(s"Error: $msg") + printUsage() + if explanation.nonEmpty || docTags.contains("@param") then + println("--help gives more information") + else entryPointWithArgs + end run + end call + end MainWrapper +end main diff --git a/tests/pos/decorators/sample-adapters.scala b/tests/pos/decorators/sample-adapters.scala new file mode 100644 index 000000000000..77622261d16e --- /dev/null +++ b/tests/pos/decorators/sample-adapters.scala @@ -0,0 +1,34 @@ +// Sample adapters: + +class logged extends EntryPoint.Adapter: + + def wrapper(wrapped: EntryPoint.Wrapper): LoggedWrapper = LoggedWrapper(wrapped) + + class LoggedWrapper(val wrapped: EntryPoint.Wrapper) extends Wrapper: + def adapt[A, R](op: A => R)(args: A): R = + val argsString: String = args match + case args: Array[_] => args.mkString(", ") + case args: Seq[_] => args.mkString(", ") + case args: Unit => "()" + case args => args.toString + val result = op(args) + println(s"[log] ${finalWrapped.entryPointName}($argsString) -> $result") + result + end LoggedWrapper +end logged + +class split extends EntryPoint.Adapter: + + def wrapper(wrapped: EntryPoint.Wrapper): SplitWrapper = SplitWrapper(wrapped) + + class SplitWrapper(val wrapped: EntryPoint.Wrapper) extends Wrapper: + def adapt[R](op: Array[String] => R)(args: String): R = op(args.split(" ")) +end split + +class join extends EntryPoint.Adapter: + + def wrapper(wrapped: EntryPoint.Wrapper): JoinWrapper = JoinWrapper(wrapped) + + class JoinWrapper(val wrapped: EntryPoint.Wrapper) extends Wrapper: + def adapt[R](op: String => R)(args: Array[String]): R = op(args.mkString(" ")) +end join diff --git a/tests/pos/decorators/sample-program.scala b/tests/pos/decorators/sample-program.scala new file mode 100644 index 000000000000..1f58f4cd3260 --- /dev/null +++ b/tests/pos/decorators/sample-program.scala @@ -0,0 +1,51 @@ +object myProgram: + + /** Adds two numbers + * @param num the first number + * @param inc the second number + */ + @main def add(num: Int, inc: Int = 1): Unit = + println(s"$num + $inc = ${num + inc}") + + /** @usage java addAll --nums */ + @join @logged @split @main def addAll(nums: Int*): Unit = + println(nums.sum) + +end myProgram + +// Compiler generated code: + +object add: + private val $main = new main() + private val $main$wrapper = $main.wrapper( + "MyProgram.add", + """Adds two numbers + |@param num the first number + |@param inc the second number""".stripMargin) + def main(args: Array[String]): Unit = + val cll = $main$wrapper.call(args) + val arg1 = cll.nextArgGetter[Int]("num", summon[$main.ArgumentParser[Int]]) + val arg2 = cll.nextArgGetter[Int]("inc", summon[$main.ArgumentParser[Int]], Some(1)) + cll.run(myProgram.add(arg1(), arg2())) +end add + +object addAll: + private val $main = new main() + private val $split = new split() + private val $logged = new logged() + private val $join = new join() + private val $main$wrapper = $main.wrapper("MyProgram.addAll", "@usage java addAll --nums ") + private val $split$wrapper = $split.wrapper($main$wrapper) + private val $logged$wrapper = $logged.wrapper($split$wrapper) + private val $join$wrapper = $join.wrapper($logged$wrapper) + def main(args: Array[String]): Unit = + $join$wrapper.adapt { (args: String) => + $logged$wrapper.adapt { (args: String) => + $split$wrapper.adapt { (args: Array[String]) => + val cll = $main$wrapper.call(args) + val arg1 = cll.finalArgsGetter[Int]("nums", summon[$main.ArgumentParser[Int]]) + cll.run(myProgram.addAll(arg1(): _*)) + } (args) + } (args) + } (args) +end addAll \ No newline at end of file From f8847e01b6f0df3ef3c8185ccbd3cef0e653f7de Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 26 Apr 2020 19:07:27 +0200 Subject: [PATCH 12/14] Better error messages for illegal type tests --- .../src/dotty/tools/dotc/transform/TypeTestsCasts.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index 722867215adc..4a7be54cfc28 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -209,6 +209,7 @@ object TypeTestsCasts { * can be true in some cases. Issues a warning or an error otherwise. */ def checkSensical(foundClasses: List[Symbol])(using Context): Boolean = + def exprType = i"type ${expr.tpe.widen.stripAnnots}" def check(foundCls: Symbol): Boolean = if (!isCheckable(foundCls)) true else if (!foundCls.derivesFrom(testCls)) { @@ -216,9 +217,9 @@ object TypeTestsCasts { testCls.is(Final) || !testCls.is(Trait) && !foundCls.is(Trait) ) if (foundCls.is(Final)) - unreachable(i"type ${expr.tpe.widen} is not a subclass of $testCls") + unreachable(i"$exprType is not a subclass of $testCls") else if (unrelated) - unreachable(i"type ${expr.tpe.widen} and $testCls are unrelated") + unreachable(i"$exprType and $testCls are unrelated") else true } else true @@ -227,7 +228,7 @@ object TypeTestsCasts { val foundEffectiveClass = effectiveClass(expr.tpe.widen) if foundEffectiveClass.isPrimitiveValueClass && !testCls.isPrimitiveValueClass then - ctx.error("cannot test if value types are references", tree.sourcePos) + ctx.error(i"cannot test if value of $exprType is a reference of $testCls", tree.sourcePos) false else foundClasses.exists(check) end checkSensical From 8acc6da3f5649e2a2ea2271fe704fc1eb91f7b67 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 26 Apr 2020 19:10:47 +0200 Subject: [PATCH 13/14] Make decorators a run test --- tests/pos/main-method-scheme-generic.scala | 4 +- tests/run/decorators.check | 85 +++++++++++++++++++ .../{pos => run}/decorators/DocComment.scala | 0 .../{pos => run}/decorators/EntryPoint.scala | 0 tests/{pos => run}/decorators/Test.scala | 0 .../decorators/WordSplitter.scala | 0 tests/{pos => run}/decorators/main.scala | 0 .../decorators/sample-adapters.scala | 0 .../decorators/sample-program.scala | 0 9 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 tests/run/decorators.check rename tests/{pos => run}/decorators/DocComment.scala (100%) rename tests/{pos => run}/decorators/EntryPoint.scala (100%) rename tests/{pos => run}/decorators/Test.scala (100%) rename tests/{pos => run}/decorators/WordSplitter.scala (100%) rename tests/{pos => run}/decorators/main.scala (100%) rename tests/{pos => run}/decorators/sample-adapters.scala (100%) rename tests/{pos => run}/decorators/sample-program.scala (100%) diff --git a/tests/pos/main-method-scheme-generic.scala b/tests/pos/main-method-scheme-generic.scala index 5aa9cfa6a404..f56b3df9ca5e 100644 --- a/tests/pos/main-method-scheme-generic.scala +++ b/tests/pos/main-method-scheme-generic.scala @@ -164,15 +164,13 @@ class main extends EntryPointAnnotation: if errors.nonEmpty then for msg <- errors do println(s"Error: $msg") usage() - else f + else entryPointApply end run end call inline def wrapperName(mainName: String): String = s"${mainName.drop(mainName.lastIndexOf('.') + 1)}.main" - override type WrapperAnnotation = EntryPoint - end main // Sample main method diff --git a/tests/run/decorators.check b/tests/run/decorators.check new file mode 100644 index 000000000000..ffa7186e3093 --- /dev/null +++ b/tests/run/decorators.check @@ -0,0 +1,85 @@ +> java add 2 3 +2 + 3 = 5 + +> java add 4 +4 + 1 = 5 + +> java add --num 10 --inc -2 +10 + -2 = 8 + +> java add --num 10 +10 + 1 = 11 + +> java add --help +Adds two numbers +Usage: java add num inc? +where + num the first number + inc the second number + +> java add +Error: invalid argument for num: +Usage: java add num inc? +--help gives more information + +> java add 1 2 3 4 +Error: unused argument: 3 +Error: unused argument: 4 +Usage: java add num inc? +--help gives more information + +> java add -n 1 -i 2 +Error: invalid argument for num: -n +Error: unused argument: -i +Error: unused argument: 2 +Usage: java add num inc? +--help gives more information + +> java add true 10 +Error: invalid argument for num: true +Usage: java add num inc? +--help gives more information + +> java add true false +Error: invalid argument for num: true +Error: invalid argument for inc: false +Usage: java add num inc? +--help gives more information + +> java add true false 10 +Error: invalid argument for num: true +Error: invalid argument for inc: false +Error: unused argument: 10 +Usage: java add num inc? +--help gives more information + +> java add --inc 10 --num 20 +20 + 10 = 30 + +> java add binary 10 01 +Error: invalid argument for num: binary +Error: unused argument: 01 +Usage: java add num inc? +--help gives more information + +> java addAll 1 2 3 4 5 +15 +[log] MyProgram.addAll(1 2 3 4 5) -> () + +> java addAll --nums +0 +[log] MyProgram.addAll(--nums) -> () + +> java addAll --nums 33 44 +44 +[log] MyProgram.addAll(--nums 33 44) -> () + +> java addAll true 1 2 3 +Error: invalid argument for nums: true +Usage: java addAll --nums +[log] MyProgram.addAll(true 1 2 3) -> () + +> java addAll --help +Usage: java addAll --nums +[log] MyProgram.addAll(--help) -> () + diff --git a/tests/pos/decorators/DocComment.scala b/tests/run/decorators/DocComment.scala similarity index 100% rename from tests/pos/decorators/DocComment.scala rename to tests/run/decorators/DocComment.scala diff --git a/tests/pos/decorators/EntryPoint.scala b/tests/run/decorators/EntryPoint.scala similarity index 100% rename from tests/pos/decorators/EntryPoint.scala rename to tests/run/decorators/EntryPoint.scala diff --git a/tests/pos/decorators/Test.scala b/tests/run/decorators/Test.scala similarity index 100% rename from tests/pos/decorators/Test.scala rename to tests/run/decorators/Test.scala diff --git a/tests/pos/decorators/WordSplitter.scala b/tests/run/decorators/WordSplitter.scala similarity index 100% rename from tests/pos/decorators/WordSplitter.scala rename to tests/run/decorators/WordSplitter.scala diff --git a/tests/pos/decorators/main.scala b/tests/run/decorators/main.scala similarity index 100% rename from tests/pos/decorators/main.scala rename to tests/run/decorators/main.scala diff --git a/tests/pos/decorators/sample-adapters.scala b/tests/run/decorators/sample-adapters.scala similarity index 100% rename from tests/pos/decorators/sample-adapters.scala rename to tests/run/decorators/sample-adapters.scala diff --git a/tests/pos/decorators/sample-program.scala b/tests/run/decorators/sample-program.scala similarity index 100% rename from tests/pos/decorators/sample-program.scala rename to tests/run/decorators/sample-program.scala From 165dc16e88c5063adfc58da63ca53a0253521394 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 26 Apr 2020 20:01:09 +0200 Subject: [PATCH 14/14] Drop a test file This one is subsumed by run/decorators now. --- tests/pos/main-method-scheme-generic.scala | 245 --------------------- 1 file changed, 245 deletions(-) delete mode 100644 tests/pos/main-method-scheme-generic.scala diff --git a/tests/pos/main-method-scheme-generic.scala b/tests/pos/main-method-scheme-generic.scala deleted file mode 100644 index f56b3df9ca5e..000000000000 --- a/tests/pos/main-method-scheme-generic.scala +++ /dev/null @@ -1,245 +0,0 @@ -import annotation.{Annotation, StaticAnnotation} -import collection.mutable - -/** This class provides a framework for compiler-generated wrappers - * of "entry-point" methods. It routes and transforms parameters and results - * between a compiler-generated wrapper method that has calling conventions - * fixed by a framework and a user-written entry-point method that can have - * flexible argument lists. It allows the wrapper to provide help and usage - * information as well as customised error messages if the actual wrapper arguments - * do not match the expected entry-point parameters. - * - * The protocol of calls from the wrapper method is as follows: - * - * 1. Create a `call` instance with the wrapper argument. - * 2. For each parameter of the entry-point, invoke `call.nextArgGetter`, - * or `call.finalArgsGetter` if is a final varargs parameter. - * 3. Invoke `call.run` with the closure of entry-point applied to all arguments. - * - * The wrapper class has this outline: - * - * object : - * @WrapperAnnotation def (args: ) = - * ... - * - * Here `` and `` are obtained from an - * inline call to the `wrapperName` method. - */ -trait EntryPointAnnotation extends StaticAnnotation: - - /** The class used for argument parsing. E.g. `scala.util.FromString`, if - * arguments are strings, but it could be something else. - */ - type ArgumentParser[T] - - /** The required result type of the user-defined main function */ - type EntryPointResult - - /** The type of the wrapper argument. E.g., for Java main methods: `Array[String]` */ - type WrapperArgs - - /** The return type of the generated wrapper. E.g., for Java main methods: `Unit` */ - type WrapperResult - - /** An annotation type with which the wrapper method is decorated. - * No annotation is generated if the type is left abstract. - */ - type WrapperAnnotation <: Annotation - - /** The fully qualified name (relative to enclosing package) to - * use for the static wrapper method. - * @param mainName the fully qualified name of the user-defined main method - */ - inline def wrapperName(mainName: String): String - - /** A new wrapper call with arguments from `args` */ - def call(args: WrapperArgs): Call - - /** A class representing a wrapper call */ - abstract class Call: - - /** The getter for the next argument of type `T` */ - def nextArgGetter[T](argName: String, fromString: ArgumentParser[T], defaultValue: Option[T] = None): () => T - - /** The getter for a final varargs argument of type `T*` */ - def finalArgsGetter[T](argName: String, fromString: ArgumentParser[T]): () => Seq[T] - - /** Run `entryPoint` if all arguments are valid, - * or print usage information and/or error messages. - * @param entryPointApply the applied entry-point to run - * @param entryPointName the fully qualified name of the entry-point method - * @param docComment the doc comment of the entry-point method - */ - def run(entryPointApply: => EntryPointResult, entryPointName: String, docComment: String): WrapperResult - end Call -end EntryPointAnnotation - -//Sample main class, can be freely implemented: - -class main extends EntryPointAnnotation: - - type ArgumentParser[T] = util.FromString[T] - type EntryPointResult = Unit - type WrapperArgs = Array[String] - type WrapperResult = Unit - - def call(args: Array[String]) = new Call: - - /** A buffer of demanded argument names, plus - * "?" if it has a default - * "*" if it is a vararg - * "" otherwise - */ - private var argInfos = new mutable.ListBuffer[(String, String)] - - /** A buffer for all errors */ - private var errors = new mutable.ListBuffer[String] - - /** Issue an error, and return an uncallable getter */ - private def error(msg: String): () => Nothing = - errors += msg - () => assertFail("trying to get invalid argument") - - /** The next argument index */ - private var argIdx: Int = 0 - - private def argAt(idx: Int): Option[String] = - if idx < args.length then Some(args(idx)) else None - - private def nextPositionalArg(): Option[String] = - while argIdx < args.length && args(argIdx).startsWith("--") do argIdx += 2 - val result = argAt(argIdx) - argIdx += 1 - result - - private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = - p.fromStringOption(arg) match - case Some(t) => () => t - case None => error(s"invalid argument for $argName: $arg") - - def nextArgGetter[T](argName: String, p: ArgumentParser[T], defaultValue: Option[T] = None): () => T = - argInfos += ((argName, if defaultValue.isDefined then "?" else "")) - val idx = args.indexOf(s"--$argName") - val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() - argOpt match - case Some(arg) => convert(argName, arg, p) - case None => defaultValue match - case Some(t) => () => t - case None => error(s"missing argument for $argName") - - def finalArgsGetter[T](argName: String, p: ArgumentParser[T]): () => Seq[T] = - argInfos += ((argName, "*")) - def remainingArgGetters(): List[() => T] = nextPositionalArg() match - case Some(arg) => convert(arg, argName, p) :: remainingArgGetters() - case None => Nil - val getters = remainingArgGetters() - () => getters.map(_()) - - def run(entryPointApply: => EntryPointResult, entryPointName: String, docComment: String): Unit = - def usage(): Unit = - val cmd = entryPointName.dropRight(".main".length) - val params = argInfos.map(_ + _).mkString(" ") - println(s"Usage: java $cmd $params") - - def explain(): Unit = - if docComment.nonEmpty then println(docComment) // todo: process & format doc comment - - def flagUnused(): Unit = nextPositionalArg() match - case Some(arg) => - error(s"unused argument: $arg") - flagUnused() - case None => - for - arg <- args - if arg.startsWith("--") && !argInfos.map(_._1).contains(arg.drop(2)) - do - error(s"unknown argument name: $arg") - end flagUnused - - if args.isEmpty || args.contains("--help") then - usage() - explain() - else - flagUnused() - if errors.nonEmpty then - for msg <- errors do println(s"Error: $msg") - usage() - else entryPointApply - end run - end call - - inline def wrapperName(mainName: String): String = - s"${mainName.drop(mainName.lastIndexOf('.') + 1)}.main" - -end main - -// Sample main method - -object myProgram: - - /** Adds two numbers */ - @main def add(num: Int, inc: Int = 1): Unit = - println(s"$num + $inc = ${num + inc}") - -end myProgram - -// Compiler generated code: - -object add extends main: - def main(args: Array[String]) = - val cll = call(args) - val arg1 = cll.nextArgGetter[Int]("num", summon[ArgumentParser[Int]]) - val arg2 = cll.nextArgGetter[Int]("inc", summon[ArgumentParser[Int]], Some(1)) - cll.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") -end add - -/** --- Some scenarios ---------------------------------------- - -> java add 2 3 -2 + 3 = 5 -> java add 4 -4 + 1 = 5 -> java add --num 10 --inc -2 -10 + -2 = 8 -> java add --num 10 -10 + 1 = 11 -> java add --help -Usage: java add num inc? -Adds two numbers -> java add -Usage: java add num inc? -Adds two numbers -> java add 1 2 3 4 -Error: unused argument: 3 -Error: unused argument: 4 -Usage: java add num inc? -> java add -n 1 -i 10 -Error: invalid argument for num: -n -Error: unused argument: -i -Error: unused argument: 10 -Usage: java add num inc? -> java add --n 1 --i 10 -Error: missing argument for num -Error: unknown argument name: --n -Error: unknown argument name: --i -Usage: java add num inc? -> java add true 10 -Error: invalid argument for num: true -Usage: java add num inc? -> java add true false -Error: invalid argument for num: true -Error: invalid argument for inc: false -Usage: java add num inc? -> java add true false 10 -Error: invalid argument for num: true -Error: invalid argument for inc: false -Error: unused argument: 10 -Usage: java add num inc? -> java add --inc 10 --num 20 -20 + 10 = 30 -> java add binary 10 01 -Error: invalid argument for num: binary -Error: unused argument: 01 -Usage: java add num inc? - -*/ \ No newline at end of file