-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add scala.annotation.MainAnnotation
#14558
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
--- | ||
layout: doc-page | ||
title: "MainAnnotation" | ||
--- | ||
|
||
`MainAnnotation` provides a generic way to define main annotations such as `@main`. | ||
|
||
When a users annotates a method with an annotation that extends `MainAnnotation` a class with a `main` method will be generated. The main method will contain the code needed to parse the command line arguments and run the application. | ||
|
||
```scala | ||
/** Sum all the numbers | ||
* | ||
* @param first Fist number to sum | ||
* @param rest The rest of the numbers to sum | ||
*/ | ||
@myMain def sum(first: Int, second: Int = 0, rest: Int*): Int = first + second + rest.sum | ||
``` | ||
|
||
```scala | ||
object foo { | ||
def main(args: Array[String]): Unit = { | ||
val mainAnnot = new myMain() | ||
val info = new Info( | ||
name = "foo.main", | ||
documentation = "Sum all the numbers", | ||
parameters = Seq( | ||
new Parameter("first", "scala.Int", hasDefault=false, isVarargs=false, "Fist number to sum", Seq()), | ||
new Parameter("second", "scala.Int", hasDefault=true, isVarargs=false, "", Seq()), | ||
new Parameter("rest", "scala.Int" , hasDefault=false, isVarargs=true, "The rest of the numbers to sum", Seq()) | ||
) | ||
) | ||
val mainArgsOpt = mainAnnot.command(info, args) | ||
if mainArgsOpt.isDefined then | ||
val mainArgs = mainArgsOpt.get | ||
val args0 = mainAnnot.argGetter[Int](info.parameters(0), mainArgs(0), None) // using a parser of Int | ||
val args1 = mainAnnot.argGetter[Int](info.parameters(1), mainArgs(1), Some(() => sum$default$1())) // using a parser of Int | ||
val args2 = mainAnnot.varargGetter[Int](info.parameters(2), mainArgs.drop(2)) // using a parser of Int | ||
mainAnnot.run(() => sum(args0(), args1(), args2()*)) | ||
} | ||
} | ||
``` | ||
|
||
The implementation of the `main` method first instantiates the annotation and then call `command`. | ||
When calling the `command`, the arguments can be checked and preprocessed. | ||
Then it defines a series of argument getters calling `argGetter` for each parameter and `varargGetter` for the last one if it is a varargs. `argGetter` gets an optional lambda that computes the default argument. | ||
Finally, the `run` method is called to run the application. It receives a by-name argument that contains the call the annotated method with the instantiations arguments (using the lambdas from `argGetter`/`varargGetter`). | ||
|
||
|
||
Example of implementation of `myMain` that takes all arguments positionally. It used `util.CommandLineParser.FromString` and expects no default arguments. For simplicity, any errors in preprocessing or parsing results in crash. | ||
|
||
```scala | ||
// Parser used to parse command line arguments | ||
import scala.util.CommandLineParser.FromString[T] | ||
|
||
// Result type of the annotated method is Int and arguments are parsed using FromString | ||
@experimental class myMain extends MainAnnotation[FromString, Int]: | ||
import MainAnnotation.{ Info, Parameter } | ||
|
||
def command(info: Info, args: Seq[String]): Option[Seq[String]] = | ||
if args.contains("--help") then | ||
println(info.documentation) | ||
None // do not parse or run the program | ||
else if info.parameters.exists(_.hasDefault) then | ||
println("Default arguments are not supported") | ||
None | ||
else if info.hasVarargs then | ||
val numPlainArgs = info.parameters.length - 1 | ||
if numPlainArgs <= args.length then | ||
println("Not enough arguments") | ||
None | ||
else | ||
Some(args) | ||
else | ||
if info.parameters.length <= args.length then | ||
println("Not enough arguments") | ||
None | ||
else if info.parameters.length >= args.length then | ||
println("Too many arguments") | ||
None | ||
else | ||
Some(args) | ||
|
||
def argGetter[T](param: Parameter, arg: String, defaultArgument: Option[() => T])(using parser: FromString[T]): () => T = | ||
() => parser.fromString(arg) | ||
|
||
def varargGetter[T](param: Parameter, args: Seq[String])(using parser: FromString[T]): () => Seq[T] = | ||
() => args.map(arg => parser.fromString(arg)) | ||
|
||
def run(program: () => Int): Unit = | ||
println("executing program") | ||
|
||
try { | ||
val result = program() | ||
println("result: " + result) | ||
println("executed program") | ||
end myMain | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
package scala.annotation | ||
|
||
/** 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.varargGetter` if is a final varargs parameter, | ||
* - a call to `command.run` with the closure of user-main applied to all arguments. | ||
* | ||
* Example: | ||
* ```scala | ||
* /** Sum all the numbers | ||
* * | ||
* * @param first Fist number to sum | ||
* * @param rest The rest of the numbers to sum | ||
* */ | ||
* @myMain def sum(first: Int, second: Int = 0, rest: Int*): Int = first + second + rest.sum | ||
* ``` | ||
* generates | ||
* ```scala | ||
* object foo { | ||
* def main(args: Array[String]): Unit = { | ||
* val mainAnnot = new myMain() | ||
* val info = new Info( | ||
* name = "foo.main", | ||
* documentation = "Sum all the numbers", | ||
* parameters = Seq( | ||
* new Parameter("first", "scala.Int", hasDefault=false, isVarargs=false, "Fist number to sum"), | ||
* new Parameter("rest", "scala.Int" , hasDefault=false, isVarargs=true, "The rest of the numbers to sum") | ||
* ) | ||
* ) | ||
* val mainArgsOpt = mainAnnot.command(info, args) | ||
* if mainArgsOpt.isDefined then | ||
* val mainArgs = mainArgsOpt.get | ||
* val args0 = mainAnnot.argGetter[Int](info.parameters(0), mainArgs(0), None) // using parser Int | ||
* val args1 = mainAnnot.argGetter[Int](info.parameters(1), mainArgs(1), Some(() => sum$default$1())) // using parser Int | ||
* val args2 = mainAnnot.varargGetter[Int](info.parameters(2), mainArgs.drop(2)) // using parser Int | ||
* mainAnnot.run(() => sum(args0(), args1(), args2()*)) | ||
* } | ||
* } | ||
* ``` | ||
* | ||
* @param Parser The class used for argument string parsing and arguments into a `T` | ||
* @param Result The required result type of the main method. | ||
* If this type is Any or Unit, any type will be accepted. | ||
*/ | ||
@experimental | ||
trait MainAnnotation[Parser[_], Result] extends StaticAnnotation: | ||
import MainAnnotation.{Info, Parameter} | ||
|
||
/** Process the command arguments before parsing them. | ||
* | ||
* Return `Some` of the sequence of arguments that will be parsed to be passed to the main method. | ||
* This sequence needs to have the same length as the number of parameters of the main method (i.e. `info.parameters.size`). | ||
* If there is a varags parameter, then the sequence must be at least of length `info.parameters.size - 1`. | ||
* | ||
* Returns `None` if the arguments are invalid and parsing and run should be stopped. | ||
* | ||
* @param info The information about the command (name, documentation and info about parameters) | ||
* @param args The command line arguments | ||
*/ | ||
def command(info: Info, args: Seq[String]): Option[Seq[String]] | ||
|
||
/** The getter for the `idx`th argument of type `T` | ||
* | ||
* @param idx The index of the argument | ||
* @param defaultArgument Optional lambda to instantiate the default argument | ||
*/ | ||
def argGetter[T](param: Parameter, arg: String, defaultArgument: Option[() => T])(using Parser[T]): () => T | ||
|
||
/** The getter for a final varargs argument of type `T*` */ | ||
def varargGetter[T](param: Parameter, args: Seq[String])(using Parser[T]): () => Seq[T] | ||
|
||
/** Run `program` if all arguments are valid if all arguments are valid | ||
* | ||
* @param program A function containing the call to the main method and instantiation of its arguments | ||
*/ | ||
def run(program: () => Result): Unit | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should pass the info to this method as well def run(info: Info, program: () => Result): Unit It might be useful for error reporting if the program fails There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the user really needs it, they can always set a private mutable variable from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, they can. |
||
|
||
end MainAnnotation | ||
|
||
@experimental | ||
object MainAnnotation: | ||
|
||
/** Information about the main method | ||
* | ||
* @param name The name of the main method | ||
* @param documentation The documentation of the main method without the `@param` documentation (see Parameter.documentaion) | ||
* @param parameters Information about the parameters of the main method | ||
*/ | ||
final class Info( | ||
val name: String, | ||
val documentation: String, | ||
val parameters: Seq[Parameter], | ||
): | ||
|
||
/** If the method ends with a varargs parameter */ | ||
def hasVarargs: Boolean = parameters.nonEmpty && parameters.last.isVarargs | ||
|
||
end Info | ||
|
||
/** Information about a parameter of a main method | ||
* | ||
* @param name The name of the parameter | ||
* @param typeName The name of the parameter's type | ||
* @param hasDefault If the parameter has a default argument | ||
* @param isVarargs If the parameter is a varargs parameter (can only be true for the last parameter) | ||
* @param documentation The documentation of the parameter (from `@param` documentation in the main method) | ||
* @param annotations The annotations of the parameter that extend `ParameterAnnotation` | ||
*/ | ||
final class Parameter( | ||
val name: String, | ||
val typeName: String, | ||
val hasDefault: Boolean, | ||
val isVarargs: Boolean, | ||
val documentation: String, | ||
val annotations: Seq[ParameterAnnotation], | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we override Also, should we make the primary constructor private, to support possible evolutions in the future? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That is a good idea
We should |
||
|
||
/** Marker trait for annotations that will be included in the Parameter annotations. */ | ||
trait ParameterAnnotation extends StaticAnnotation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we just use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a way to filter the annotation that need to be reified and instantiated at runtime |
||
|
||
end MainAnnotation |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import scala.annotation.MainAnnotation | ||
|
||
@MainAnnotation def f(i: Int, n: Int) = () // error |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
executing program | ||
result: 28 | ||
executed program |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo here: Should be "First" not "Fist"