Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add scala.annotation.MainAnnotation #14558

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
374 changes: 351 additions & 23 deletions compiler/src/dotty/tools/dotc/ast/MainProxies.scala

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,8 @@ class Definitions {
@tu lazy val Seq_lengthCompare: Symbol = SeqClass.requiredMethod(nme.lengthCompare, List(IntType))
@tu lazy val Seq_length : Symbol = SeqClass.requiredMethod(nme.length)
@tu lazy val Seq_toSeq : Symbol = SeqClass.requiredMethod(nme.toSeq)
@tu lazy val SeqModule: Symbol = requiredModule("scala.collection.immutable.Seq")


@tu lazy val StringOps: Symbol = requiredClass("scala.collection.StringOps")
@tu lazy val StringOps_format: Symbol = StringOps.requiredMethod(nme.format)
Expand Down Expand Up @@ -853,6 +855,12 @@ class Definitions {

@tu lazy val XMLTopScopeModule: Symbol = requiredModule("scala.xml.TopScope")

@tu lazy val MainAnnotationClass: ClassSymbol = requiredClass("scala.annotation.MainAnnotation")
@tu lazy val MainAnnotationInfo: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.Info")
@tu lazy val MainAnnotationParameter: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.Parameter")
@tu lazy val MainAnnotationParameterAnnotation: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.ParameterAnnotation")
@tu lazy val MainAnnotationCommand: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.Command")

@tu lazy val CommandLineParserModule: Symbol = requiredModule("scala.util.CommandLineParser")
@tu lazy val CLP_ParseError: ClassSymbol = CommandLineParserModule.requiredClass("ParseError").typeRef.symbol.asClass
@tu lazy val CLP_parseArgument: Symbol = CommandLineParserModule.requiredMethod("parseArgument")
Expand Down
5 changes: 5 additions & 0 deletions compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ object StdNames {
val applyOrElse: N = "applyOrElse"
val args : N = "args"
val argv : N = "argv"
val argGetter : N = "argGetter"
val arrayClass: N = "arrayClass"
val arrayElementClass: N = "arrayElementClass"
val arrayType: N = "arrayType"
Expand Down Expand Up @@ -427,6 +428,8 @@ object StdNames {
val classOf: N = "classOf"
val classType: N = "classType"
val clone_ : N = "clone"
val cmd: N = "cmd"
val command: N = "command"
val common: N = "common"
val compiletime : N = "compiletime"
val conforms_ : N = "$conforms"
Expand Down Expand Up @@ -540,6 +543,7 @@ object StdNames {
val ordinalDollar: N = "$ordinal"
val ordinalDollar_ : N = "_$ordinal"
val origin: N = "origin"
val parameters: N = "parameters"
val parts: N = "parts"
val postfixOps: N = "postfixOps"
val prefix : N = "prefix"
Expand Down Expand Up @@ -613,6 +617,7 @@ object StdNames {
val fromOrdinal: N = "fromOrdinal"
val values: N = "values"
val view_ : N = "view"
val varargGetter : N = "varargGetter"
val wait_ : N = "wait"
val wildcardType: N = "wildcardType"
val withFilter: N = "withFilter"
Expand Down
7 changes: 4 additions & 3 deletions compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1351,12 +1351,13 @@ trait Checking {
def checkAnnotApplicable(annot: Tree, sym: Symbol)(using Context): Boolean =
!ctx.reporter.reportsErrorsFor {
val annotCls = Annotations.annotClass(annot)
val concreteAnnot = Annotations.ConcreteAnnotation(annot)
val pos = annot.srcPos
if (annotCls == defn.MainAnnot) {
if (annotCls == defn.MainAnnot || concreteAnnot.matches(defn.MainAnnotationClass)) {
if (!sym.isRealMethod)
report.error(em"@main annotation cannot be applied to $sym", pos)
report.error(em"main annotation cannot be applied to $sym", pos)
if (!sym.owner.is(Module) || !sym.owner.isStatic)
report.error(em"$sym cannot be a @main method since it cannot be accessed statically", pos)
report.error(em"$sym cannot be a main method since it cannot be accessed statically", pos)
}
// TODO: Add more checks here
}
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2602,7 +2602,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
pkg.moduleClass.info.decls.lookup(topLevelClassName).ensureCompleted()
var stats1 = typedStats(tree.stats, pkg.moduleClass)._1
if (!ctx.isAfterTyper)
stats1 = stats1 ++ typedBlockStats(MainProxies.mainProxies(stats1))._1
stats1 = stats1 ++ typedBlockStats(MainProxies.proxies(stats1))._1
cpy.PackageDef(tree)(pid1, stats1).withType(pkg.termRef)
}
case _ =>
Expand Down
97 changes: 97 additions & 0 deletions docs/_docs/reference/experimental/main-annotation.md
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()),
Copy link
Contributor

@bjornregnell bjornregnell Apr 18, 2022

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"

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
```
1 change: 1 addition & 0 deletions docs/sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ subsection:
- page: reference/experimental/named-typeargs-spec.md
- page: reference/experimental/numeric-literals.md
- page: reference/experimental/explicit-nulls.md
- page: reference/experimental/main-annotation.md
- page: reference/experimental/cc.md
- page: reference/experimental/tupled-function.md
- page: reference/syntax.md
Expand Down
126 changes: 126 additions & 0 deletions library/src/scala/annotation/MainAnnotation.scala
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The 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 command.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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],
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we override equals and hashCode to get structural comparisons? (And same for other classes defined here)

Also, should we make the primary constructor private, to support possible evolutions in the future?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should we override equals and hashCode to get structural comparisons? (And same for other classes defined here)

That is a good idea

Also, should we make the primary constructor private, to support possible evolutions in the future?

We should


/** Marker trait for annotations that will be included in the Parameter annotations. */
trait ParameterAnnotation extends StaticAnnotation
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we just use StaticAnnotation instead of ParameterAnnotation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
12 changes: 10 additions & 2 deletions project/MiMaFilters.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@ import com.typesafe.tools.mima.core._

object MiMaFilters {
val Library: Seq[ProblemFilter] = Seq(

// Those are OK because user code is not allowed to inherit from Quotes:
// Experimental APIs that can be added in 3.2.0 or later
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.append"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.asQuotes"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#ClassDefModule.apply"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolModule.newClass"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.typeRef"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.termRef"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#TypeTreeModule.ref"),

// Experimental `MainAnnotation` APIs. Can be added in 3.3.0 or later.
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation"),
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$"),
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$Command"),
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$CommandInfo"),
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$ParameterInfo"),
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$ParameterAnnotation"),
)
}
1 change: 1 addition & 0 deletions project/resources/referenceReplacements/sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ subsection:
- page: reference/experimental/named-typeargs-spec.md
- page: reference/experimental/numeric-literals.md
- page: reference/experimental/explicit-nulls.md
- page: reference/experimental/main-annotation.md
- page: reference/experimental/cc.md
- page: reference/syntax.md
- title: Language Versions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
./experimental/erased-defs.html
./experimental/explicit-nulls.html
./experimental/index.html
./experimental/main-annotation.html
./experimental/named-typeargs-spec.html
./experimental/named-typeargs.html
./experimental/numeric-literals.html
Expand Down
3 changes: 3 additions & 0 deletions tests/neg/main-annotation-mainannotation.scala
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
3 changes: 3 additions & 0 deletions tests/run/main-annotation-example.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
executing program
result: 28
executed program
Loading