Skip to content

Commit 1bd7c77

Browse files
Merge pull request #14558 from dotty-staging/add-scala.annotation.MainAnnotation
Add `scala.annotation.MainAnnotation`
2 parents bdddd41 + 813e059 commit 1bd7c77

27 files changed

+1266
-29
lines changed

compiler/src/dotty/tools/dotc/ast/MainProxies.scala

Lines changed: 351 additions & 23 deletions
Large diffs are not rendered by default.

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,8 @@ class Definitions {
528528
@tu lazy val Seq_lengthCompare: Symbol = SeqClass.requiredMethod(nme.lengthCompare, List(IntType))
529529
@tu lazy val Seq_length : Symbol = SeqClass.requiredMethod(nme.length)
530530
@tu lazy val Seq_toSeq : Symbol = SeqClass.requiredMethod(nme.toSeq)
531+
@tu lazy val SeqModule: Symbol = requiredModule("scala.collection.immutable.Seq")
532+
531533

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

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

858+
@tu lazy val MainAnnotationClass: ClassSymbol = requiredClass("scala.annotation.MainAnnotation")
859+
@tu lazy val MainAnnotationInfo: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.Info")
860+
@tu lazy val MainAnnotationParameter: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.Parameter")
861+
@tu lazy val MainAnnotationParameterAnnotation: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.ParameterAnnotation")
862+
@tu lazy val MainAnnotationCommand: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.Command")
863+
856864
@tu lazy val CommandLineParserModule: Symbol = requiredModule("scala.util.CommandLineParser")
857865
@tu lazy val CLP_ParseError: ClassSymbol = CommandLineParserModule.requiredClass("ParseError").typeRef.symbol.asClass
858866
@tu lazy val CLP_parseArgument: Symbol = CommandLineParserModule.requiredMethod("parseArgument")

compiler/src/dotty/tools/dotc/core/StdNames.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ object StdNames {
397397
val applyOrElse: N = "applyOrElse"
398398
val args : N = "args"
399399
val argv : N = "argv"
400+
val argGetter : N = "argGetter"
400401
val arrayClass: N = "arrayClass"
401402
val arrayElementClass: N = "arrayElementClass"
402403
val arrayType: N = "arrayType"
@@ -427,6 +428,8 @@ object StdNames {
427428
val classOf: N = "classOf"
428429
val classType: N = "classType"
429430
val clone_ : N = "clone"
431+
val cmd: N = "cmd"
432+
val command: N = "command"
430433
val common: N = "common"
431434
val compiletime : N = "compiletime"
432435
val conforms_ : N = "$conforms"
@@ -540,6 +543,7 @@ object StdNames {
540543
val ordinalDollar: N = "$ordinal"
541544
val ordinalDollar_ : N = "_$ordinal"
542545
val origin: N = "origin"
546+
val parameters: N = "parameters"
543547
val parts: N = "parts"
544548
val postfixOps: N = "postfixOps"
545549
val prefix : N = "prefix"
@@ -613,6 +617,7 @@ object StdNames {
613617
val fromOrdinal: N = "fromOrdinal"
614618
val values: N = "values"
615619
val view_ : N = "view"
620+
val varargGetter : N = "varargGetter"
616621
val wait_ : N = "wait"
617622
val wildcardType: N = "wildcardType"
618623
val withFilter: N = "withFilter"

compiler/src/dotty/tools/dotc/typer/Checking.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,12 +1351,13 @@ trait Checking {
13511351
def checkAnnotApplicable(annot: Tree, sym: Symbol)(using Context): Boolean =
13521352
!ctx.reporter.reportsErrorsFor {
13531353
val annotCls = Annotations.annotClass(annot)
1354+
val concreteAnnot = Annotations.ConcreteAnnotation(annot)
13541355
val pos = annot.srcPos
1355-
if (annotCls == defn.MainAnnot) {
1356+
if (annotCls == defn.MainAnnot || concreteAnnot.matches(defn.MainAnnotationClass)) {
13561357
if (!sym.isRealMethod)
1357-
report.error(em"@main annotation cannot be applied to $sym", pos)
1358+
report.error(em"main annotation cannot be applied to $sym", pos)
13581359
if (!sym.owner.is(Module) || !sym.owner.isStatic)
1359-
report.error(em"$sym cannot be a @main method since it cannot be accessed statically", pos)
1360+
report.error(em"$sym cannot be a main method since it cannot be accessed statically", pos)
13601361
}
13611362
// TODO: Add more checks here
13621363
}

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2602,7 +2602,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
26022602
pkg.moduleClass.info.decls.lookup(topLevelClassName).ensureCompleted()
26032603
var stats1 = typedStats(tree.stats, pkg.moduleClass)._1
26042604
if (!ctx.isAfterTyper)
2605-
stats1 = stats1 ++ typedBlockStats(MainProxies.mainProxies(stats1))._1
2605+
stats1 = stats1 ++ typedBlockStats(MainProxies.proxies(stats1))._1
26062606
cpy.PackageDef(tree)(pid1, stats1).withType(pkg.termRef)
26072607
}
26082608
case _ =>
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
layout: doc-page
3+
title: "MainAnnotation"
4+
---
5+
6+
`MainAnnotation` provides a generic way to define main annotations such as `@main`.
7+
8+
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.
9+
10+
```scala
11+
/** Sum all the numbers
12+
*
13+
* @param first Fist number to sum
14+
* @param rest The rest of the numbers to sum
15+
*/
16+
@myMain def sum(first: Int, second: Int = 0, rest: Int*): Int = first + second + rest.sum
17+
```
18+
19+
```scala
20+
object foo {
21+
def main(args: Array[String]): Unit = {
22+
val mainAnnot = new myMain()
23+
val info = new Info(
24+
name = "foo.main",
25+
documentation = "Sum all the numbers",
26+
parameters = Seq(
27+
new Parameter("first", "scala.Int", hasDefault=false, isVarargs=false, "Fist number to sum", Seq()),
28+
new Parameter("second", "scala.Int", hasDefault=true, isVarargs=false, "", Seq()),
29+
new Parameter("rest", "scala.Int" , hasDefault=false, isVarargs=true, "The rest of the numbers to sum", Seq())
30+
)
31+
)
32+
val mainArgsOpt = mainAnnot.command(info, args)
33+
if mainArgsOpt.isDefined then
34+
val mainArgs = mainArgsOpt.get
35+
val args0 = mainAnnot.argGetter[Int](info.parameters(0), mainArgs(0), None) // using a parser of Int
36+
val args1 = mainAnnot.argGetter[Int](info.parameters(1), mainArgs(1), Some(() => sum$default$1())) // using a parser of Int
37+
val args2 = mainAnnot.varargGetter[Int](info.parameters(2), mainArgs.drop(2)) // using a parser of Int
38+
mainAnnot.run(() => sum(args0(), args1(), args2()*))
39+
}
40+
}
41+
```
42+
43+
The implementation of the `main` method first instantiates the annotation and then call `command`.
44+
When calling the `command`, the arguments can be checked and preprocessed.
45+
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.
46+
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`).
47+
48+
49+
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.
50+
51+
```scala
52+
// Parser used to parse command line arguments
53+
import scala.util.CommandLineParser.FromString[T]
54+
55+
// Result type of the annotated method is Int and arguments are parsed using FromString
56+
@experimental class myMain extends MainAnnotation[FromString, Int]:
57+
import MainAnnotation.{ Info, Parameter }
58+
59+
def command(info: Info, args: Seq[String]): Option[Seq[String]] =
60+
if args.contains("--help") then
61+
println(info.documentation)
62+
None // do not parse or run the program
63+
else if info.parameters.exists(_.hasDefault) then
64+
println("Default arguments are not supported")
65+
None
66+
else if info.hasVarargs then
67+
val numPlainArgs = info.parameters.length - 1
68+
if numPlainArgs <= args.length then
69+
println("Not enough arguments")
70+
None
71+
else
72+
Some(args)
73+
else
74+
if info.parameters.length <= args.length then
75+
println("Not enough arguments")
76+
None
77+
else if info.parameters.length >= args.length then
78+
println("Too many arguments")
79+
None
80+
else
81+
Some(args)
82+
83+
def argGetter[T](param: Parameter, arg: String, defaultArgument: Option[() => T])(using parser: FromString[T]): () => T =
84+
() => parser.fromString(arg)
85+
86+
def varargGetter[T](param: Parameter, args: Seq[String])(using parser: FromString[T]): () => Seq[T] =
87+
() => args.map(arg => parser.fromString(arg))
88+
89+
def run(program: () => Int): Unit =
90+
println("executing program")
91+
92+
try {
93+
val result = program()
94+
println("result: " + result)
95+
println("executed program")
96+
end myMain
97+
```

docs/sidebar.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ subsection:
147147
- page: reference/experimental/named-typeargs-spec.md
148148
- page: reference/experimental/numeric-literals.md
149149
- page: reference/experimental/explicit-nulls.md
150+
- page: reference/experimental/main-annotation.md
150151
- page: reference/experimental/cc.md
151152
- page: reference/experimental/tupled-function.md
152153
- page: reference/syntax.md
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package scala.annotation
2+
3+
/** MainAnnotation provides the functionality for a compiler-generated main class.
4+
* It links a compiler-generated main method (call it compiler-main) to a user
5+
* written main method (user-main).
6+
* The protocol of calls from compiler-main is as follows:
7+
*
8+
* - create a `command` with the command line arguments,
9+
* - for each parameter of user-main, a call to `command.argGetter`,
10+
* or `command.varargGetter` if is a final varargs parameter,
11+
* - a call to `command.run` with the closure of user-main applied to all arguments.
12+
*
13+
* Example:
14+
* ```scala
15+
* /** Sum all the numbers
16+
* *
17+
* * @param first Fist number to sum
18+
* * @param rest The rest of the numbers to sum
19+
* */
20+
* @myMain def sum(first: Int, second: Int = 0, rest: Int*): Int = first + second + rest.sum
21+
* ```
22+
* generates
23+
* ```scala
24+
* object foo {
25+
* def main(args: Array[String]): Unit = {
26+
* val mainAnnot = new myMain()
27+
* val info = new Info(
28+
* name = "foo.main",
29+
* documentation = "Sum all the numbers",
30+
* parameters = Seq(
31+
* new Parameter("first", "scala.Int", hasDefault=false, isVarargs=false, "Fist number to sum"),
32+
* new Parameter("rest", "scala.Int" , hasDefault=false, isVarargs=true, "The rest of the numbers to sum")
33+
* )
34+
* )
35+
* val mainArgsOpt = mainAnnot.command(info, args)
36+
* if mainArgsOpt.isDefined then
37+
* val mainArgs = mainArgsOpt.get
38+
* val args0 = mainAnnot.argGetter[Int](info.parameters(0), mainArgs(0), None) // using parser Int
39+
* val args1 = mainAnnot.argGetter[Int](info.parameters(1), mainArgs(1), Some(() => sum$default$1())) // using parser Int
40+
* val args2 = mainAnnot.varargGetter[Int](info.parameters(2), mainArgs.drop(2)) // using parser Int
41+
* mainAnnot.run(() => sum(args0(), args1(), args2()*))
42+
* }
43+
* }
44+
* ```
45+
*
46+
* @param Parser The class used for argument string parsing and arguments into a `T`
47+
* @param Result The required result type of the main method.
48+
* If this type is Any or Unit, any type will be accepted.
49+
*/
50+
@experimental
51+
trait MainAnnotation[Parser[_], Result] extends StaticAnnotation:
52+
import MainAnnotation.{Info, Parameter}
53+
54+
/** Process the command arguments before parsing them.
55+
*
56+
* Return `Some` of the sequence of arguments that will be parsed to be passed to the main method.
57+
* This sequence needs to have the same length as the number of parameters of the main method (i.e. `info.parameters.size`).
58+
* If there is a varags parameter, then the sequence must be at least of length `info.parameters.size - 1`.
59+
*
60+
* Returns `None` if the arguments are invalid and parsing and run should be stopped.
61+
*
62+
* @param info The information about the command (name, documentation and info about parameters)
63+
* @param args The command line arguments
64+
*/
65+
def command(info: Info, args: Seq[String]): Option[Seq[String]]
66+
67+
/** The getter for the `idx`th argument of type `T`
68+
*
69+
* @param idx The index of the argument
70+
* @param defaultArgument Optional lambda to instantiate the default argument
71+
*/
72+
def argGetter[T](param: Parameter, arg: String, defaultArgument: Option[() => T])(using Parser[T]): () => T
73+
74+
/** The getter for a final varargs argument of type `T*` */
75+
def varargGetter[T](param: Parameter, args: Seq[String])(using Parser[T]): () => Seq[T]
76+
77+
/** Run `program` if all arguments are valid if all arguments are valid
78+
*
79+
* @param program A function containing the call to the main method and instantiation of its arguments
80+
*/
81+
def run(program: () => Result): Unit
82+
83+
end MainAnnotation
84+
85+
@experimental
86+
object MainAnnotation:
87+
88+
/** Information about the main method
89+
*
90+
* @param name The name of the main method
91+
* @param documentation The documentation of the main method without the `@param` documentation (see Parameter.documentaion)
92+
* @param parameters Information about the parameters of the main method
93+
*/
94+
final class Info(
95+
val name: String,
96+
val documentation: String,
97+
val parameters: Seq[Parameter],
98+
):
99+
100+
/** If the method ends with a varargs parameter */
101+
def hasVarargs: Boolean = parameters.nonEmpty && parameters.last.isVarargs
102+
103+
end Info
104+
105+
/** Information about a parameter of a main method
106+
*
107+
* @param name The name of the parameter
108+
* @param typeName The name of the parameter's type
109+
* @param hasDefault If the parameter has a default argument
110+
* @param isVarargs If the parameter is a varargs parameter (can only be true for the last parameter)
111+
* @param documentation The documentation of the parameter (from `@param` documentation in the main method)
112+
* @param annotations The annotations of the parameter that extend `ParameterAnnotation`
113+
*/
114+
final class Parameter(
115+
val name: String,
116+
val typeName: String,
117+
val hasDefault: Boolean,
118+
val isVarargs: Boolean,
119+
val documentation: String,
120+
val annotations: Seq[ParameterAnnotation],
121+
)
122+
123+
/** Marker trait for annotations that will be included in the Parameter annotations. */
124+
trait ParameterAnnotation extends StaticAnnotation
125+
126+
end MainAnnotation

project/MiMaFilters.scala

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@ import com.typesafe.tools.mima.core._
33

44
object MiMaFilters {
55
val Library: Seq[ProblemFilter] = Seq(
6-
7-
// Those are OK because user code is not allowed to inherit from Quotes:
6+
// Experimental APIs that can be added in 3.2.0 or later
7+
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.append"),
88
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.asQuotes"),
99
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#ClassDefModule.apply"),
1010
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolModule.newClass"),
1111
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.typeRef"),
1212
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.termRef"),
1313
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#TypeTreeModule.ref"),
14+
15+
// Experimental `MainAnnotation` APIs. Can be added in 3.3.0 or later.
16+
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation"),
17+
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$"),
18+
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$Command"),
19+
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$CommandInfo"),
20+
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$ParameterInfo"),
21+
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$ParameterAnnotation"),
1422
)
1523
}

project/resources/referenceReplacements/sidebar.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ subsection:
127127
- page: reference/experimental/named-typeargs-spec.md
128128
- page: reference/experimental/numeric-literals.md
129129
- page: reference/experimental/explicit-nulls.md
130+
- page: reference/experimental/main-annotation.md
130131
- page: reference/experimental/cc.md
131132
- page: reference/syntax.md
132133
- title: Language Versions

project/scripts/expected-links/reference-expected-links.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
./experimental/erased-defs.html
6969
./experimental/explicit-nulls.html
7070
./experimental/index.html
71+
./experimental/main-annotation.html
7172
./experimental/named-typeargs-spec.html
7273
./experimental/named-typeargs.html
7374
./experimental/numeric-literals.html
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import scala.annotation.MainAnnotation
2+
3+
@MainAnnotation def f(i: Int, n: Int) = () // error
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
executing program
2+
result: 28
3+
executed program

0 commit comments

Comments
 (0)