Skip to content

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

Merged
merged 13 commits into from
Jul 30, 2019
9 changes: 7 additions & 2 deletions compiler/src/dotty/tools/backend/jvm/GenBCode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,14 @@ class GenBCodePipeline(val entryPoints: List[Symbol], val int: DottyBackendInter
val (cl1, cl2) =
if (classSymbol.effectiveName.toString < dupClassSym.effectiveName.toString) (classSymbol, dupClassSym)
else (dupClassSym, classSymbol)
val same = classSymbol.effectiveName.toString == dupClassSym.effectiveName.toString
ctx.atPhase(ctx.typerPhase) {
the[Context].warning(s"${cl1.show} differs only in case from ${cl2.showLocated}. " +
"Such classes will overwrite one another on case-insensitive filesystems.", cl1.sourcePos)
if (same)
the[Context].warning( // FIXME: This should really be an error, but then FromTasty tests fail
s"${cl1.show} and ${cl2.showLocated} produce classes that overwrite one another", cl1.sourcePos)
else
the[Context].warning(s"${cl1.show} differs only in case from ${cl2.showLocated}. " +
"Such classes will overwrite one another on case-insensitive filesystems.", cl1.sourcePos)
}
}
}
Expand Down
107 changes: 107 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/MainProxies.scala
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
}
}
11 changes: 7 additions & 4 deletions compiler/src/dotty/tools/dotc/config/JavaPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ class JavaPlatform extends Platform {

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

/** Update classpath with a substituted subentry */
def updateClassPath(subst: Map[ClassPath, ClassPath]): Unit = currentClassPath.get match {
Expand Down
8 changes: 5 additions & 3 deletions compiler/src/dotty/tools/dotc/core/Annotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import util.Spans.Span

object Annotations {

def annotClass(tree: Tree) given Context =
if (tree.symbol.isConstructor) tree.symbol.owner
else tree.tpe.typeSymbol

abstract class Annotation {
def tree(implicit ctx: Context): Tree

def symbol(implicit ctx: Context): Symbol =
if (tree.symbol.isConstructor) tree.symbol.owner
else tree.tpe.typeSymbol
def symbol(implicit ctx: Context): Symbol = annotClass(tree)

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

Expand Down
17 changes: 12 additions & 5 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -695,10 +695,16 @@ class Definitions {

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

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

@threadUnsafe lazy val CommandLineParserModule: SymbolPerRun = perRunSym(ctx.requiredModuleRef("scala.util.CommandLineParser"))
@threadUnsafe lazy val CLP_ParseError: ClassSymbolPerRun = perRunClass(CommandLineParserModule.requiredClass("ParseError").typeRef)
@threadUnsafe lazy val CLP_parseArgument: SymbolPerRun = perRunSym(CommandLineParserModule.requiredMethodRef("parseArgument"))
@threadUnsafe lazy val CLP_parseRemainingArguments: SymbolPerRun = perRunSym(CommandLineParserModule.requiredMethodRef("parseRemainingArguments"))
@threadUnsafe lazy val CLP_showError: SymbolPerRun = perRunSym(CommandLineParserModule.requiredMethodRef("showError"))

@threadUnsafe lazy val TupleTypeRef: TypeRef = ctx.requiredClassRef("scala.Tuple")
def TupleClass(implicit ctx: Context): ClassSymbol = TupleTypeRef.symbol.asClass
@threadUnsafe lazy val Tuple_cons: SymbolPerRun = perRunSym(TupleClass.requiredMethodRef("*:"))
Expand All @@ -712,8 +718,8 @@ class Definitions {

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

lazy val DynamicTupleModule: Symbol = ctx.requiredModule("scala.runtime.DynamicTuple")
lazy val DynamicTupleModuleClass: Symbol = DynamicTupleModule.moduleClass
@threadUnsafe lazy val DynamicTupleModule: Symbol = ctx.requiredModule("scala.runtime.DynamicTuple")
@threadUnsafe lazy val DynamicTupleModuleClass: Symbol = DynamicTupleModule.moduleClass
lazy val DynamicTuple_consIterator: Symbol = DynamicTupleModule.requiredMethod("consIterator")
lazy val DynamicTuple_concatIterator: Symbol = DynamicTupleModule.requiredMethod("concatIterator")
lazy val DynamicTuple_dynamicApply: Symbol = DynamicTupleModule.requiredMethod("dynamicApply")
Expand All @@ -724,10 +730,10 @@ class Definitions {
lazy val DynamicTuple_dynamicToArray: Symbol = DynamicTupleModule.requiredMethod("dynamicToArray")
lazy val DynamicTuple_productToArray: Symbol = DynamicTupleModule.requiredMethod("productToArray")

lazy val TupledFunctionTypeRef: TypeRef = ctx.requiredClassRef("scala.TupledFunction")
@threadUnsafe lazy val TupledFunctionTypeRef: TypeRef = ctx.requiredClassRef("scala.TupledFunction")
def TupledFunctionClass(implicit ctx: Context): ClassSymbol = TupledFunctionTypeRef.symbol.asClass

lazy val InternalTupledFunctionTypeRef: TypeRef = ctx.requiredClassRef("scala.internal.TupledFunction")
@threadUnsafe lazy val InternalTupledFunctionTypeRef: TypeRef = ctx.requiredClassRef("scala.internal.TupledFunction")
def InternalTupleFunctionClass(implicit ctx: Context): ClassSymbol = InternalTupledFunctionTypeRef.symbol.asClass
def InternalTupleFunctionModule(implicit ctx: Context): Symbol = ctx.requiredModule("scala.internal.TupledFunction")

Expand All @@ -751,6 +757,7 @@ class Definitions {
@threadUnsafe lazy val ForceInlineAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.forceInline"))
@threadUnsafe lazy val InlineParamAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.annotation.internal.InlineParam"))
@threadUnsafe lazy val InvariantBetweenAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.annotation.internal.InvariantBetween"))
@threadUnsafe lazy val MainAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.main"))
@threadUnsafe lazy val MigrationAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.annotation.migration"))
@threadUnsafe lazy val NativeAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.native"))
@threadUnsafe lazy val RepeatedAnnot: ClassSymbolPerRun = perRunClass(ctx.requiredClassRef("scala.annotation.internal.Repeated"))
Expand Down
8 changes: 8 additions & 0 deletions compiler/src/dotty/tools/dotc/reporting/Reporter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,14 @@ abstract class Reporter extends interfaces.ReporterResult {
*/
def errorsReported: Boolean = hasErrors

/** Run `op` and return `true` if errors were reported by this reporter.
*/
def reportsErrorsFor(op: Context => Unit) given (ctx: Context): Boolean = {
val initial = errorCount
op(ctx)
errorCount > initial
}

private[this] var reportedFeaturesUseSites = Set[Symbol]()

def isReportedFeatureUseSite(featureTrait: Symbol): Boolean =
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ private class ExtractAPICollector(implicit val ctx: Context) extends ThunkHolder

allNonLocalClassesInSrc += cl

if (sym.isStatic && defType == DefinitionType.Module && ctx.platform.hasMainMethod(sym)) {
if (sym.isStatic && ctx.platform.hasMainMethod(sym)) {
Copy link
Contributor

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 have main method.

_mainClasses += name
}

Expand Down
15 changes: 15 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,20 @@ trait Checking {
}
}

/** check that annotation `annot` is applicable to symbol `sym` */
def checkAnnotApplicable(annot: Tree, sym: Symbol) given (ctx: Context): Boolean =
!ctx.reporter.reportsErrorsFor { implicit ctx =>
val annotCls = Annotations.annotClass(annot)
val pos = annot.sourcePos
if (annotCls == defn.MainAnnot) {
if (!sym.isRealMethod)
ctx.error(em"@main annotation cannot be applied to $sym", pos)
if (!sym.owner.is(Module) || !sym.owner.isStatic)
ctx.error(em"$sym cannot be a @main method since it cannot be accessed statically", pos)
}
// TODO: Add more checks here
}

/** Check that symbol's external name does not clash with symbols defined in the same scope */
def checkNoAlphaConflict(stats: List[Tree])(implicit ctx: Context): Unit = {
var seen = Set[Name]()
Expand All @@ -1157,6 +1171,7 @@ trait ReChecking extends Checking {
override def checkEnum(cdef: untpd.TypeDef, cls: Symbol, firstParent: Symbol)(implicit ctx: Context): Unit = ()
override def checkRefsLegal(tree: tpd.Tree, badOwner: Symbol, allowed: (Name, Symbol) => Boolean, where: String)(implicit ctx: Context): Unit = ()
override def checkEnumCaseRefsLegal(cdef: TypeDef, enumCtx: Context)(implicit ctx: Context): Unit = ()
override def checkAnnotApplicable(annot: Tree, sym: Symbol) given (ctx: Context): Boolean = true
}

trait NoChecking extends ReChecking {
Expand Down
9 changes: 6 additions & 3 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package dotc
package typer

import core._
import ast.{tpd, _}
import ast._
import Trees._
import Constants._
import StdNames._
Expand Down Expand Up @@ -1460,7 +1460,8 @@ class Typer extends Namer
sym.annotations.foreach(_.ensureCompleted)
lazy val annotCtx = annotContext(mdef, sym)
// necessary in order to mark the typed ahead annotations as definitely typed:
untpd.modsDeco(mdef).mods.annotations.foreach(typedAnnotation(_)(annotCtx))
for annot <- untpd.modsDeco(mdef).mods.annotations do
checkAnnotApplicable(typedAnnotation(annot)(annotCtx), sym)
}

def typedAnnotation(annot: untpd.Tree)(implicit ctx: Context): Tree = {
Expand Down Expand Up @@ -1793,7 +1794,9 @@ class Typer extends Namer
case pid1: RefTree if pkg.exists =>
if (!pkg.is(Package)) ctx.error(PackageNameAlreadyDefined(pkg), tree.sourcePos)
val packageCtx = ctx.packageContext(tree, pkg)
val stats1 = typedStats(tree.stats, pkg.moduleClass)(packageCtx)
var stats1 = typedStats(tree.stats, pkg.moduleClass)(packageCtx)
if (!ctx.isAfterTyper)
stats1 = stats1 ++ typedBlockStats(MainProxies.mainProxies(stats1))(packageCtx)._2
cpy.PackageDef(tree)(pid1, stats1).withType(pkg.termRef)
case _ =>
// Package will not exist if a duplicate type has already been entered, see `tests/neg/1708.scala`
Expand Down
82 changes: 82 additions & 0 deletions docs/docs/reference/changed-features/main-functions.md
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*) = {
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 encourage the use of : Unit here or not?

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
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the exact lifecycle of such a program here? What's happyBirthday? Do we need to compile it with dotc and run scala from the folder in which the class files end up? Can we do scala happyBirthday.scala 22 – that is without the necessity to compile the source? I think we need to elaborate on this in the docs.

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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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)
}

Copy link
Contributor

Choose a reason for hiding this comment

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

It is not available for user programs in Scala

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't that mean that the synthetic method main can't be called, rather than that the @main method written in source can't be called?


`@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.
2 changes: 2 additions & 0 deletions docs/sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ sidebar:
url: docs/reference/changed-features/compiler-plugins.html
- title: Lazy Vals initialization
url: docs/reference/changed-features/lazy-vals-init.html
- title: Main Functions
url: docs/reference/changed-features/main-functions.html
- title: Dropped Features
subsection:
- title: DelayedInit
Expand Down
Loading