-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Implement @main functions #6898
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
Changes from all commits
a5df0e7
fce621c
97c1cd2
e1b10c8
ef16db7
a2b6023
c53f465
eae2382
c53772c
ffea9f4
4df2db1
622e8c3
eb374a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
package dotty.tools.dotc | ||
package ast | ||
|
||
import core._ | ||
import Symbols._, Types._, Contexts._, Decorators._, util.Spans._, Flags._, Constants._ | ||
import StdNames.nme | ||
import ast.Trees._ | ||
|
||
/** Generate proxy classes for @main functions. | ||
* A function like | ||
* | ||
* @main def f(x: S, ys: T*) = ... | ||
* | ||
* would be translated to something like | ||
* | ||
* import CommandLineParser._ | ||
* class f { | ||
* @static def main(args: Array[String]): Unit = | ||
* try | ||
* f( | ||
* parseArgument[S](args, 0), | ||
* parseRemainingArguments[T](args, 1): _* | ||
* ) | ||
* catch case err: ParseError => showError(err) | ||
* } | ||
*/ | ||
object MainProxies { | ||
|
||
def mainProxies(stats: List[tpd.Tree]) given Context: List[untpd.Tree] = { | ||
import tpd._ | ||
def mainMethods(stats: List[Tree]): List[Symbol] = stats.flatMap { | ||
case stat: DefDef if stat.symbol.hasAnnotation(defn.MainAnnot) => | ||
stat.symbol :: Nil | ||
case stat @ TypeDef(name, impl: Template) if stat.symbol.is(Module) => | ||
mainMethods(impl.body) | ||
case _ => | ||
Nil | ||
} | ||
mainMethods(stats).flatMap(mainProxy) | ||
} | ||
|
||
import untpd._ | ||
def mainProxy(mainFun: Symbol) given (ctx: Context): List[TypeDef] = { | ||
val mainAnnotSpan = mainFun.getAnnotation(defn.MainAnnot).get.tree.span | ||
def pos = mainFun.sourcePos | ||
val argsRef = Ident(nme.args) | ||
|
||
def addArgs(call: untpd.Tree, mt: MethodType, idx: Int): untpd.Tree = { | ||
if (mt.isImplicitMethod) { | ||
ctx.error(s"@main method cannot have implicit parameters", pos) | ||
call | ||
} | ||
else { | ||
val args = mt.paramInfos.zipWithIndex map { | ||
(formal, n) => | ||
val (parserSym, formalElem) = | ||
if (formal.isRepeatedParam) (defn.CLP_parseRemainingArguments, formal.argTypes.head) | ||
else (defn.CLP_parseArgument, formal) | ||
val arg = Apply( | ||
TypeApply(ref(parserSym.termRef), TypeTree(formalElem) :: Nil), | ||
argsRef :: Literal(Constant(idx + n)) :: Nil) | ||
if (formal.isRepeatedParam) repeated(arg) else arg | ||
} | ||
val call1 = Apply(call, args) | ||
mt.resType match { | ||
case restpe: MethodType => | ||
if (mt.paramInfos.lastOption.getOrElse(NoType).isRepeatedParam) | ||
ctx.error(s"varargs parameter of @main method must come last", pos) | ||
addArgs(call1, restpe, idx + args.length) | ||
case _ => | ||
call1 | ||
} | ||
} | ||
} | ||
|
||
var result: List[TypeDef] = Nil | ||
if (!mainFun.owner.isStaticOwner) | ||
ctx.error(s"@main method is not statically accessible", pos) | ||
else { | ||
var call = ref(mainFun.termRef) | ||
mainFun.info match { | ||
case _: ExprType => | ||
case mt: MethodType => | ||
call = addArgs(call, mt, 0) | ||
case _: PolyType => | ||
ctx.error(s"@main method cannot have type parameters", pos) | ||
case _ => | ||
ctx.error(s"@main can only annotate a method", pos) | ||
} | ||
val errVar = Ident(nme.error) | ||
val handler = CaseDef( | ||
Typed(errVar, TypeTree(defn.CLP_ParseError.typeRef)), | ||
EmptyTree, | ||
Apply(ref(defn.CLP_showError.termRef), errVar :: Nil)) | ||
val body = Try(call, handler :: Nil, EmptyTree) | ||
val mainArg = ValDef(nme.args, TypeTree(defn.ArrayType.appliedTo(defn.StringType)), EmptyTree) | ||
.withFlags(Param) | ||
val mainMeth = DefDef(nme.main, Nil, (mainArg :: Nil) :: Nil, TypeTree(defn.UnitType), body) | ||
.withFlags(JavaStatic) | ||
val mainTempl = Template(emptyConstructor, Nil, Nil, EmptyValDef, mainMeth :: Nil) | ||
val mainCls = TypeDef(mainFun.name.toTypeName, mainTempl) | ||
.withFlags(Final) | ||
if (!ctx.reporter.hasErrors) result = mainCls.withSpan(mainAnnotSpan) :: Nil | ||
} | ||
result | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
--- | ||
layout: doc-page | ||
title: "Main Methods" | ||
--- | ||
|
||
Scala 3 offers a new way to define programs that can be invoked from the command line: | ||
A `@main` annotation on a method turns this method into an executable program. | ||
Example: | ||
```scala | ||
@main def happyBirthday(age: Int, name: String, others: String*) = { | ||
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 encourage the use of |
||
val suffix = | ||
(age % 100) match { | ||
case 11 | 12 | 13 => "th" | ||
case _ => | ||
(age % 10) match { | ||
case 1 => "st" | ||
case 2 => "nd" | ||
case 3 => "rd" | ||
case _ => "th" | ||
} | ||
} | ||
val bldr = new StringBuilder(s"Happy $age$suffix birthday, $name") | ||
for other <- others do bldr.append(" and ").append(other) | ||
bldr.toString | ||
} | ||
``` | ||
This would generate a main program `happyBirthday` that could be called like this | ||
``` | ||
> scala happyBirthday 23 Lisa Peter | ||
Happy 23rd Birthday, Lisa and Peter! | ||
``` | ||
A `@main` annotated method can be written either at the top-level or in a statically accessible object. The name of the program is in each case the name of the method, without any object prefixes. The `@main` method can have an arbitrary number of parameters. | ||
For each parameter type there must be an instance of the `scala.util.FromString` typeclass | ||
that is used to convert an argument string to the required parameter type. | ||
The parameter list of a main method can end in a repeated parameter that then | ||
takes all remaining arguments given on the command line. | ||
|
||
The program implemented from a `@main` method checks that there are enough arguments on | ||
the command line to fill in all parameters, and that argument strings are convertible to | ||
the required types. If a check fails, the program is terminated with an error message. | ||
Examples: | ||
``` | ||
> scala happyBirthday 22 | ||
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. What is the exact lifecycle of such a program here? What's |
||
Illegal command line after first argument: more arguments expected | ||
> scala happyBirthday sixty Fred | ||
Illegal command line: java.lang.NumberFormatException: For input string: "sixty" | ||
``` | ||
The Scala compiler generates a program from a `@main` method `f` as follows: | ||
|
||
- It creates a class named `f` in the package where the `@main` method was found | ||
- The class has a static method `main` with the usual signature. It takes an `Array[String]` | ||
as argument and returns `Unit`. | ||
- The generated `main` method calls method `f` with arguments converted using | ||
methods in the `scala.util.CommandLineParser` object. | ||
|
||
For instance, the `happyBirthDay` method above would generate additional code equivalent to the following class: | ||
```scala | ||
final class happyBirthday { | ||
import scala.util.{CommndLineParser => CLP} | ||
<static> def main(args: Array[String]): Unit = | ||
try | ||
happyBirthday( | ||
CLP.parseArgument[Int](args, 0), | ||
CLP.parseArgument[String](args, 1), | ||
CLP.parseRemainingArguments[String](args, 2)) | ||
catch { | ||
case error: CLP.ParseError => CLP.showError(error) | ||
} | ||
} | ||
``` | ||
**Note**: The `<static>` modifier above expresses that the `main` method is generated | ||
as a static method of class `happyBirthDay`. It is not available for user programs in Scala. Regular "static" members are generated in Scala using objects instead. | ||
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. Why do we make this design decision here? Looks like a limitation to me. E.g. I may want to write a library of utility functions that may depend on each other: @main def disableFile(path: Path): Unit =
// Change the `path`'s extension from `scala` to `disabled`
@main def enableFile(path: Path): Unit =
// Change the `path`'s extension from `disabled` to `scala`
@main def filterFolder(folder: Path, fileNamePattern: Regex): Unit = {
// Disable all the files except those the names of which match the given regex pattern
val allFiles: List[Path] = // get all the files present in the folder
allFiles.foreach(enableFile) // Make sure we start with a clean folder, unaffected by previous runs of this or sister programs
allFiles.filterNot(file => fileNamePattern.matches(file.name)).foreach(disableFile)
} 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.
Also, it doesn't seem that this limitation is true: object Main {
def main(args: Array[String]): Unit = { f(); println("foo") }
@main def f(): Unit = {
println("Hello world!")
println(msg)
}
@main def g(): Unit = f()
def msg = "I was compiled by dotty :)"
} Compiles and runs fine. 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. Doesn't that mean that the synthetic method |
||
|
||
`@main` methods are the recommended scheme to generate programs that can be invoked from the command line in Scala 3. They replace the previous scheme to write program as objects with a special `App` parent class. In Scala 2, `happyBirthday` could be written also like this: | ||
```scala | ||
object happyBirthday extends App { | ||
// needs by-hand parsing of arguments vector | ||
... | ||
} | ||
``` | ||
The previous functionality of `App`, which relied on the "magic" `DelayedInit` trait, is no longer available. `App` still exists in limited form for now, but it does not support command line arguments and will be deprecated in the future. If programs need to cross-build | ||
between Scala 2 and Scala 3, it is recommended to use an explicit `main` method with an `Array[String]` argument instead. |
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.
I think you need to drop
sym.isStatic
to allow classes to havemain
method.