Skip to content

Commit b3826d0

Browse files
Merge pull request #6898 from dotty-staging/add-main-fn
Implement @main functions
2 parents 0b52037 + eb374a1 commit b3826d0

File tree

21 files changed

+411
-18
lines changed

21 files changed

+411
-18
lines changed

compiler/src/dotty/tools/backend/jvm/GenBCode.scala

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,14 @@ class GenBCodePipeline(val entryPoints: List[Symbol], val int: DottyBackendInter
162162
val (cl1, cl2) =
163163
if (classSymbol.effectiveName.toString < dupClassSym.effectiveName.toString) (classSymbol, dupClassSym)
164164
else (dupClassSym, classSymbol)
165+
val same = classSymbol.effectiveName.toString == dupClassSym.effectiveName.toString
165166
ctx.atPhase(ctx.typerPhase) {
166-
the[Context].warning(s"${cl1.show} differs only in case from ${cl2.showLocated}. " +
167-
"Such classes will overwrite one another on case-insensitive filesystems.", cl1.sourcePos)
167+
if (same)
168+
the[Context].warning( // FIXME: This should really be an error, but then FromTasty tests fail
169+
s"${cl1.show} and ${cl2.showLocated} produce classes that overwrite one another", cl1.sourcePos)
170+
else
171+
the[Context].warning(s"${cl1.show} differs only in case from ${cl2.showLocated}. " +
172+
"Such classes will overwrite one another on case-insensitive filesystems.", cl1.sourcePos)
168173
}
169174
}
170175
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package dotty.tools.dotc
2+
package ast
3+
4+
import core._
5+
import Symbols._, Types._, Contexts._, Decorators._, util.Spans._, Flags._, Constants._
6+
import StdNames.nme
7+
import ast.Trees._
8+
9+
/** Generate proxy classes for @main functions.
10+
* A function like
11+
*
12+
* @main def f(x: S, ys: T*) = ...
13+
*
14+
* would be translated to something like
15+
*
16+
* import CommandLineParser._
17+
* class f {
18+
* @static def main(args: Array[String]): Unit =
19+
* try
20+
* f(
21+
* parseArgument[S](args, 0),
22+
* parseRemainingArguments[T](args, 1): _*
23+
* )
24+
* catch case err: ParseError => showError(err)
25+
* }
26+
*/
27+
object MainProxies {
28+
29+
def mainProxies(stats: List[tpd.Tree]) given Context: List[untpd.Tree] = {
30+
import tpd._
31+
def mainMethods(stats: List[Tree]): List[Symbol] = stats.flatMap {
32+
case stat: DefDef if stat.symbol.hasAnnotation(defn.MainAnnot) =>
33+
stat.symbol :: Nil
34+
case stat @ TypeDef(name, impl: Template) if stat.symbol.is(Module) =>
35+
mainMethods(impl.body)
36+
case _ =>
37+
Nil
38+
}
39+
mainMethods(stats).flatMap(mainProxy)
40+
}
41+
42+
import untpd._
43+
def mainProxy(mainFun: Symbol) given (ctx: Context): List[TypeDef] = {
44+
val mainAnnotSpan = mainFun.getAnnotation(defn.MainAnnot).get.tree.span
45+
def pos = mainFun.sourcePos
46+
val argsRef = Ident(nme.args)
47+
48+
def addArgs(call: untpd.Tree, mt: MethodType, idx: Int): untpd.Tree = {
49+
if (mt.isImplicitMethod) {
50+
ctx.error(s"@main method cannot have implicit parameters", pos)
51+
call
52+
}
53+
else {
54+
val args = mt.paramInfos.zipWithIndex map {
55+
(formal, n) =>
56+
val (parserSym, formalElem) =
57+
if (formal.isRepeatedParam) (defn.CLP_parseRemainingArguments, formal.argTypes.head)
58+
else (defn.CLP_parseArgument, formal)
59+
val arg = Apply(
60+
TypeApply(ref(parserSym.termRef), TypeTree(formalElem) :: Nil),
61+
argsRef :: Literal(Constant(idx + n)) :: Nil)
62+
if (formal.isRepeatedParam) repeated(arg) else arg
63+
}
64+
val call1 = Apply(call, args)
65+
mt.resType match {
66+
case restpe: MethodType =>
67+
if (mt.paramInfos.lastOption.getOrElse(NoType).isRepeatedParam)
68+
ctx.error(s"varargs parameter of @main method must come last", pos)
69+
addArgs(call1, restpe, idx + args.length)
70+
case _ =>
71+
call1
72+
}
73+
}
74+
}
75+
76+
var result: List[TypeDef] = Nil
77+
if (!mainFun.owner.isStaticOwner)
78+
ctx.error(s"@main method is not statically accessible", pos)
79+
else {
80+
var call = ref(mainFun.termRef)
81+
mainFun.info match {
82+
case _: ExprType =>
83+
case mt: MethodType =>
84+
call = addArgs(call, mt, 0)
85+
case _: PolyType =>
86+
ctx.error(s"@main method cannot have type parameters", pos)
87+
case _ =>
88+
ctx.error(s"@main can only annotate a method", pos)
89+
}
90+
val errVar = Ident(nme.error)
91+
val handler = CaseDef(
92+
Typed(errVar, TypeTree(defn.CLP_ParseError.typeRef)),
93+
EmptyTree,
94+
Apply(ref(defn.CLP_showError.termRef), errVar :: Nil))
95+
val body = Try(call, handler :: Nil, EmptyTree)
96+
val mainArg = ValDef(nme.args, TypeTree(defn.ArrayType.appliedTo(defn.StringType)), EmptyTree)
97+
.withFlags(Param)
98+
val mainMeth = DefDef(nme.main, Nil, (mainArg :: Nil) :: Nil, TypeTree(defn.UnitType), body)
99+
.withFlags(JavaStatic)
100+
val mainTempl = Template(emptyConstructor, Nil, Nil, EmptyValDef, mainMeth :: Nil)
101+
val mainCls = TypeDef(mainFun.name.toTypeName, mainTempl)
102+
.withFlags(Final)
103+
if (!ctx.reporter.hasErrors) result = mainCls.withSpan(mainAnnotSpan) :: Nil
104+
}
105+
result
106+
}
107+
}

compiler/src/dotty/tools/dotc/config/JavaPlatform.scala

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@ class JavaPlatform extends Platform {
2222

2323
// The given symbol is a method with the right name and signature to be a runnable java program.
2424
def isMainMethod(sym: SymDenotation)(implicit ctx: Context): Boolean =
25-
(sym.name == nme.main) && (sym.info match {
26-
case MethodTpe(_, defn.ArrayOf(el) :: Nil, restpe) => el =:= defn.StringType && (restpe isRef defn.UnitClass)
27-
case _ => false
28-
})
25+
sym.name == nme.main &&
26+
(sym.owner.is(Module) || sym.owner.isClass && !sym.owner.is(Trait) && sym.is(JavaStatic)) && {
27+
sym.info match {
28+
case MethodTpe(_, defn.ArrayOf(el) :: Nil, restpe) => el =:= defn.StringType && (restpe isRef defn.UnitClass)
29+
case _ => false
30+
}
31+
}
2932

3033
/** Update classpath with a substituted subentry */
3134
def updateClassPath(subst: Map[ClassPath, ClassPath]): Unit = currentClassPath.get match {

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ import util.Spans.Span
1010

1111
object Annotations {
1212

13+
def annotClass(tree: Tree) given Context =
14+
if (tree.symbol.isConstructor) tree.symbol.owner
15+
else tree.tpe.typeSymbol
16+
1317
abstract class Annotation {
1418
def tree(implicit ctx: Context): Tree
1519

16-
def symbol(implicit ctx: Context): Symbol =
17-
if (tree.symbol.isConstructor) tree.symbol.owner
18-
else tree.tpe.typeSymbol
20+
def symbol(implicit ctx: Context): Symbol = annotClass(tree)
1921

2022
def matches(cls: Symbol)(implicit ctx: Context): Boolean = symbol.derivesFrom(cls)
2123

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -695,10 +695,16 @@ class Definitions {
695695

696696
@threadUnsafe lazy val ValueOfClass: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.ValueOf"))
697697
@threadUnsafe lazy val StatsModule: SymbolPerRun = perRunSym(ctx.requiredModuleRef("dotty.tools.dotc.util.Stats"))
698-
@threadUnsafe lazy val Stats_doRecord: SymbolPerRun = perRunSym(StatsModule.requiredMethodRef("doRecord"))
698+
@threadUnsafe lazy val Stats_doRecord: SymbolPerRun = perRunSym(StatsModule.requiredMethodRef("doRecord"))
699699

700700
@threadUnsafe lazy val XMLTopScopeModule: SymbolPerRun = perRunSym(ctx.requiredModuleRef("scala.xml.TopScope"))
701701

702+
@threadUnsafe lazy val CommandLineParserModule: SymbolPerRun = perRunSym(ctx.requiredModuleRef("scala.util.CommandLineParser"))
703+
@threadUnsafe lazy val CLP_ParseError: ClassSymbolPerRun = perRunClass(CommandLineParserModule.requiredClass("ParseError").typeRef)
704+
@threadUnsafe lazy val CLP_parseArgument: SymbolPerRun = perRunSym(CommandLineParserModule.requiredMethodRef("parseArgument"))
705+
@threadUnsafe lazy val CLP_parseRemainingArguments: SymbolPerRun = perRunSym(CommandLineParserModule.requiredMethodRef("parseRemainingArguments"))
706+
@threadUnsafe lazy val CLP_showError: SymbolPerRun = perRunSym(CommandLineParserModule.requiredMethodRef("showError"))
707+
702708
@threadUnsafe lazy val TupleTypeRef: TypeRef = ctx.requiredClassRef("scala.Tuple")
703709
def TupleClass(implicit ctx: Context): ClassSymbol = TupleTypeRef.symbol.asClass
704710
@threadUnsafe lazy val Tuple_cons: SymbolPerRun = perRunSym(TupleClass.requiredMethodRef("*:"))
@@ -712,8 +718,8 @@ class Definitions {
712718

713719
def TupleXXL_fromIterator(implicit ctx: Context): Symbol = TupleXXLModule.requiredMethod("fromIterator")
714720

715-
lazy val DynamicTupleModule: Symbol = ctx.requiredModule("scala.runtime.DynamicTuple")
716-
lazy val DynamicTupleModuleClass: Symbol = DynamicTupleModule.moduleClass
721+
@threadUnsafe lazy val DynamicTupleModule: Symbol = ctx.requiredModule("scala.runtime.DynamicTuple")
722+
@threadUnsafe lazy val DynamicTupleModuleClass: Symbol = DynamicTupleModule.moduleClass
717723
lazy val DynamicTuple_consIterator: Symbol = DynamicTupleModule.requiredMethod("consIterator")
718724
lazy val DynamicTuple_concatIterator: Symbol = DynamicTupleModule.requiredMethod("concatIterator")
719725
lazy val DynamicTuple_dynamicApply: Symbol = DynamicTupleModule.requiredMethod("dynamicApply")
@@ -724,10 +730,10 @@ class Definitions {
724730
lazy val DynamicTuple_dynamicToArray: Symbol = DynamicTupleModule.requiredMethod("dynamicToArray")
725731
lazy val DynamicTuple_productToArray: Symbol = DynamicTupleModule.requiredMethod("productToArray")
726732

727-
lazy val TupledFunctionTypeRef: TypeRef = ctx.requiredClassRef("scala.TupledFunction")
733+
@threadUnsafe lazy val TupledFunctionTypeRef: TypeRef = ctx.requiredClassRef("scala.TupledFunction")
728734
def TupledFunctionClass(implicit ctx: Context): ClassSymbol = TupledFunctionTypeRef.symbol.asClass
729735

730-
lazy val InternalTupledFunctionTypeRef: TypeRef = ctx.requiredClassRef("scala.internal.TupledFunction")
736+
@threadUnsafe lazy val InternalTupledFunctionTypeRef: TypeRef = ctx.requiredClassRef("scala.internal.TupledFunction")
731737
def InternalTupleFunctionClass(implicit ctx: Context): ClassSymbol = InternalTupledFunctionTypeRef.symbol.asClass
732738
def InternalTupleFunctionModule(implicit ctx: Context): Symbol = ctx.requiredModule("scala.internal.TupledFunction")
733739

@@ -751,6 +757,7 @@ class Definitions {
751757
@threadUnsafe lazy val ForceInlineAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.forceInline"))
752758
@threadUnsafe lazy val InlineParamAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.annotation.internal.InlineParam"))
753759
@threadUnsafe lazy val InvariantBetweenAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.annotation.internal.InvariantBetween"))
760+
@threadUnsafe lazy val MainAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.main"))
754761
@threadUnsafe lazy val MigrationAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.annotation.migration"))
755762
@threadUnsafe lazy val NativeAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.native"))
756763
@threadUnsafe lazy val RepeatedAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.annotation.internal.Repeated"))

compiler/src/dotty/tools/dotc/reporting/Reporter.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,14 @@ abstract class Reporter extends interfaces.ReporterResult {
250250
*/
251251
def errorsReported: Boolean = hasErrors
252252

253+
/** Run `op` and return `true` if errors were reported by this reporter.
254+
*/
255+
def reportsErrorsFor(op: Context => Unit) given (ctx: Context): Boolean = {
256+
val initial = errorCount
257+
op(ctx)
258+
errorCount > initial
259+
}
260+
253261
private[this] var reportedFeaturesUseSites = Set[Symbol]()
254262

255263
def isReportedFeatureUseSite(featureTrait: Symbol): Boolean =

compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ private class ExtractAPICollector(implicit val ctx: Context) extends ThunkHolder
230230

231231
allNonLocalClassesInSrc += cl
232232

233-
if (sym.isStatic && defType == DefinitionType.Module && ctx.platform.hasMainMethod(sym)) {
233+
if (sym.isStatic && ctx.platform.hasMainMethod(sym)) {
234234
_mainClasses += name
235235
}
236236

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,20 @@ trait Checking {
11361136
}
11371137
}
11381138

1139+
/** check that annotation `annot` is applicable to symbol `sym` */
1140+
def checkAnnotApplicable(annot: Tree, sym: Symbol) given (ctx: Context): Boolean =
1141+
!ctx.reporter.reportsErrorsFor { implicit ctx =>
1142+
val annotCls = Annotations.annotClass(annot)
1143+
val pos = annot.sourcePos
1144+
if (annotCls == defn.MainAnnot) {
1145+
if (!sym.isRealMethod)
1146+
ctx.error(em"@main annotation cannot be applied to $sym", pos)
1147+
if (!sym.owner.is(Module) || !sym.owner.isStatic)
1148+
ctx.error(em"$sym cannot be a @main method since it cannot be accessed statically", pos)
1149+
}
1150+
// TODO: Add more checks here
1151+
}
1152+
11391153
/** Check that symbol's external name does not clash with symbols defined in the same scope */
11401154
def checkNoAlphaConflict(stats: List[Tree])(implicit ctx: Context): Unit = {
11411155
var seen = Set[Name]()
@@ -1157,6 +1171,7 @@ trait ReChecking extends Checking {
11571171
override def checkEnum(cdef: untpd.TypeDef, cls: Symbol, firstParent: Symbol)(implicit ctx: Context): Unit = ()
11581172
override def checkRefsLegal(tree: tpd.Tree, badOwner: Symbol, allowed: (Name, Symbol) => Boolean, where: String)(implicit ctx: Context): Unit = ()
11591173
override def checkEnumCaseRefsLegal(cdef: TypeDef, enumCtx: Context)(implicit ctx: Context): Unit = ()
1174+
override def checkAnnotApplicable(annot: Tree, sym: Symbol) given (ctx: Context): Boolean = true
11601175
}
11611176

11621177
trait NoChecking extends ReChecking {

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package dotc
33
package typer
44

55
import core._
6-
import ast.{tpd, _}
6+
import ast._
77
import Trees._
88
import Constants._
99
import StdNames._
@@ -1460,7 +1460,8 @@ class Typer extends Namer
14601460
sym.annotations.foreach(_.ensureCompleted)
14611461
lazy val annotCtx = annotContext(mdef, sym)
14621462
// necessary in order to mark the typed ahead annotations as definitely typed:
1463-
untpd.modsDeco(mdef).mods.annotations.foreach(typedAnnotation(_)(annotCtx))
1463+
for annot <- untpd.modsDeco(mdef).mods.annotations do
1464+
checkAnnotApplicable(typedAnnotation(annot)(annotCtx), sym)
14641465
}
14651466

14661467
def typedAnnotation(annot: untpd.Tree)(implicit ctx: Context): Tree = {
@@ -1793,7 +1794,9 @@ class Typer extends Namer
17931794
case pid1: RefTree if pkg.exists =>
17941795
if (!pkg.is(Package)) ctx.error(PackageNameAlreadyDefined(pkg), tree.sourcePos)
17951796
val packageCtx = ctx.packageContext(tree, pkg)
1796-
val stats1 = typedStats(tree.stats, pkg.moduleClass)(packageCtx)
1797+
var stats1 = typedStats(tree.stats, pkg.moduleClass)(packageCtx)
1798+
if (!ctx.isAfterTyper)
1799+
stats1 = stats1 ++ typedBlockStats(MainProxies.mainProxies(stats1))(packageCtx)._2
17971800
cpy.PackageDef(tree)(pid1, stats1).withType(pkg.termRef)
17981801
case _ =>
17991802
// Package will not exist if a duplicate type has already been entered, see `tests/neg/1708.scala`
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
layout: doc-page
3+
title: "Main Methods"
4+
---
5+
6+
Scala 3 offers a new way to define programs that can be invoked from the command line:
7+
A `@main` annotation on a method turns this method into an executable program.
8+
Example:
9+
```scala
10+
@main def happyBirthday(age: Int, name: String, others: String*) = {
11+
val suffix =
12+
(age % 100) match {
13+
case 11 | 12 | 13 => "th"
14+
case _ =>
15+
(age % 10) match {
16+
case 1 => "st"
17+
case 2 => "nd"
18+
case 3 => "rd"
19+
case _ => "th"
20+
}
21+
}
22+
val bldr = new StringBuilder(s"Happy $age$suffix birthday, $name")
23+
for other <- others do bldr.append(" and ").append(other)
24+
bldr.toString
25+
}
26+
```
27+
This would generate a main program `happyBirthday` that could be called like this
28+
```
29+
> scala happyBirthday 23 Lisa Peter
30+
Happy 23rd Birthday, Lisa and Peter!
31+
```
32+
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.
33+
For each parameter type there must be an instance of the `scala.util.FromString` typeclass
34+
that is used to convert an argument string to the required parameter type.
35+
The parameter list of a main method can end in a repeated parameter that then
36+
takes all remaining arguments given on the command line.
37+
38+
The program implemented from a `@main` method checks that there are enough arguments on
39+
the command line to fill in all parameters, and that argument strings are convertible to
40+
the required types. If a check fails, the program is terminated with an error message.
41+
Examples:
42+
```
43+
> scala happyBirthday 22
44+
Illegal command line after first argument: more arguments expected
45+
> scala happyBirthday sixty Fred
46+
Illegal command line: java.lang.NumberFormatException: For input string: "sixty"
47+
```
48+
The Scala compiler generates a program from a `@main` method `f` as follows:
49+
50+
- It creates a class named `f` in the package where the `@main` method was found
51+
- The class has a static method `main` with the usual signature. It takes an `Array[String]`
52+
as argument and returns `Unit`.
53+
- The generated `main` method calls method `f` with arguments converted using
54+
methods in the `scala.util.CommandLineParser` object.
55+
56+
For instance, the `happyBirthDay` method above would generate additional code equivalent to the following class:
57+
```scala
58+
final class happyBirthday {
59+
import scala.util.{CommndLineParser => CLP}
60+
<static> def main(args: Array[String]): Unit =
61+
try
62+
happyBirthday(
63+
CLP.parseArgument[Int](args, 0),
64+
CLP.parseArgument[String](args, 1),
65+
CLP.parseRemainingArguments[String](args, 2))
66+
catch {
67+
case error: CLP.ParseError => CLP.showError(error)
68+
}
69+
}
70+
```
71+
**Note**: The `<static>` modifier above expresses that the `main` method is generated
72+
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.
73+
74+
`@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:
75+
```scala
76+
object happyBirthday extends App {
77+
// needs by-hand parsing of arguments vector
78+
...
79+
}
80+
```
81+
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
82+
between Scala 2 and Scala 3, it is recommended to use an explicit `main` method with an `Array[String]` argument instead.

docs/sidebar.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ sidebar:
131131
url: docs/reference/changed-features/compiler-plugins.html
132132
- title: Lazy Vals initialization
133133
url: docs/reference/changed-features/lazy-vals-init.html
134+
- title: Main Functions
135+
url: docs/reference/changed-features/main-functions.html
134136
- title: Dropped Features
135137
subsection:
136138
- title: DelayedInit

0 commit comments

Comments
 (0)