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

Conversation

nicolasstucki
Copy link
Contributor

@nicolasstucki nicolasstucki commented Feb 24, 2022

The first part of #13727. In the following PR we will include the new @main annotation as an experimental @newMain annotation.

Initially based on https://github.com/lampepfl/dotty/blob/main/tests/pos/main-method-scheme-class-based.scala

@nicolasstucki nicolasstucki force-pushed the add-scala.annotation.MainAnnotation branch from 2a9d894 to edc34e8 Compare February 24, 2022 14:57
@nicolasstucki nicolasstucki force-pushed the add-scala.annotation.MainAnnotation branch from c0136b7 to c040077 Compare February 25, 2022 10:19
* @param info The information about the command (name, documentation and info about parameters)
* @param args The command line arguments
*/
def command(info: CommandInfo, args: Array[String]): Command[Parser, Result]
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'm considering removing this method from the API but still require MainAnnotation implementations to define it.

This would provide extra flexibility which would make it possible to define it as a macro. The following example is currently impossible but could work with this change.

class myMacroAnnot extends  MainAnnotation:
  type Parser[T] = ...
  type Result = ...
  inline def command(inline info: CommandInfo, args: Array[String]): Command[Parser, Result] = ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The same could apply to argGetter and varargGetter. There we would only need the type parameter and the index parameter and the default option parameter. The implicit parser is not really needed as we could use summonInline or the macro equivalent to conditionally try to summon parsers.

inline def argGetter[T](inline idx: Int, inline defaultArgument: Option[() => T]): () => T = {
  ...
  summonInline[Parser[T]]
  ...
}

Copy link
Contributor Author

@nicolasstucki nicolasstucki Feb 28, 2022

Choose a reason for hiding this comment

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

I could make this change in a following PR and discuss the tradeoff there.

@nicolasstucki nicolasstucki self-assigned this Feb 28, 2022
*
* @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.

Not sure how to restrict the main method to return the Result type when Result =:= Unit as any type can be coerced into a Unit.

Maybe it should be

def run[R](program: () => R)(using R <:< Result): Unit

But then we get error messages like

Cannot prove that Int <:< Unit.

Could also define something like this

def run[R](program: () => R)(using ResultType[R]): Unit

@implicitNotFound("The result type of the main method must of type ${Result}")
class ResultType[T](using T <:< Result)
object ResultType:
  given [T](using T <:< Result): ResultType[T] = new ResultType

and fail with

  |The result type of the main method must of type Unit.
  |I found:
  |
  |    cmd.ResultType.given_ResultType_T[Int](/* missing */summon[Int <:< Unit])
  |
  |But no implicit values were found that match type Int <:< Unit.

Alternatively, it could be implemented as a macro and the type could be checked before generating the code. This seems a bit overkill.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The current error message a type Result = Int is also not ideal.

  |Found:    () => Unit
  |Required: () => Int

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure how to restrict the main method to return the Result type when Result =:= Unit as any type can be coerced into a Unit.

-Ywarn-value-discard

@nicolasstucki nicolasstucki marked this pull request as ready for review February 28, 2022 10:05
Copy link
Contributor

@julienrf julienrf left a comment

Choose a reason for hiding this comment

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

Thank you for submitting this PR!

However, I must say that I am a bit skeptical about this idea.

It’s nice to improve the behavior of @main as you did in @newMain, but I am not sure we should commit to letting the users define their own main annotations. Also, your design does not support conditional arguments.

Could you please compare implementing a custom main annotation for IOApp or decline vs the status quo?

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 ParameterInfo 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

*
* @param program A function containing the call to the main method and instantiation of its arguments
*/
def run(program: () => Result): Unit
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure how to restrict the main method to return the Result type when Result =:= Unit as any type can be coerced into a Unit.

-Ywarn-value-discard

@nicolasstucki
Copy link
Contributor Author

Also, your design does not support conditional arguments.

What do you mean by conditional arguments? Could you give an example?

@julienrf
Copy link
Contributor

Examples: you can pass --foo only if --bar was present, or you can either pass --foo or --bar, but not both.

@nicolasstucki
Copy link
Contributor Author

Also, your design does not support conditional arguments.

What do you mean by conditional arguments? Could you give an example?

Examples: you can pass --foo only if --bar was present, or you can either pass --foo or --bar, but not both.

Seems trivial to support a main annotation with those kinds of semantics. Here is an example

def command(info: CommandInfo, args: Array[String]): Command[Parser, Result] =
  if args.contains("--foo") && !args.contains("--bar") then 
   println("--foo can only be used if --bar is preset")
   System.exist(0)
  ...

Less static version can be done with annotations which can be extracted form the CommandInfo

@myMain def f(@dependsOn("foo") foo: Boolean, bar: Boolean): Unit = ...

@julienrf
Copy link
Contributor

Less static version can be done with annotations which can be extracted form the CommandInfo

@myMain def f(@dependsOn("foo") foo: Boolean, bar: Boolean): Unit = ...

Yes, I was thinking of that use case. What I wanted to say is that that use case is not currently supported by @newMain (ie, there is no @dependsOn annotation). Users could add that annotation and the processing logic, but what would be the benefits over doing the same on a plain old @main def main(args: String*) method?

@nicolasstucki
Copy link
Contributor Author

newMain is just an example in this PR. It is far from the definitive version. That will be a discussion for the following PR.

@bishabosha
Copy link
Member

Yes, I was thinking of that use case. What I wanted to say is that that use case is not currently supported by @newMain (ie, there is no @dependsOn annotation). Users could add that annotation and the processing logic, but what would be the benefits over doing the same on a plain old @main def main(args: String*) method?

the idea would be that a library would do the processing instead of the user

Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

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

I won't have the time to review the code in detail I am afraid. I agree with the general direction, though.

Copy link
Contributor

@anatoliykmetyuk anatoliykmetyuk left a comment

Choose a reason for hiding this comment

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

Otherwise LGTM

tests/run/main-annotation-example.scala Outdated Show resolved Hide resolved
// This is a toy example, it only works with positional args
def command(info: CommandInfo, args: Array[String]): Command[Parser, Result] =
new Command[Parser, Result]:
override def argGetter[T](idx: Int, defaultArgument: Option[() => T])(using p: Parser[T]): () => T =
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if introducing the command function that handles argument parsing makes the Parser type redundant. Parser has been quite weak since its inception since we didn't want to overcomplicate command line parsing and reasoning that people with more advanced use cases can fall back to defining the main method manually.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could you elaborate on how to remove the Parser?

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe those getters the user has to override and the implicit Parsers server the same purpose – that is transform the raw String arguments into values of a certain type. We could do with just one of those, thus reducing the complexity. E.g. we could ditch the getter methods in favor of Parsers being present in the implicit scope.

tests/run/main-annotation-homemade-annot-1.scala Outdated Show resolved Hide resolved
tests/run/main-annotation-homemade-annot-2.scala Outdated Show resolved Hide resolved
compiler/src/dotty/tools/dotc/ast/MainProxies.scala Outdated Show resolved Hide resolved
@nicolasstucki nicolasstucki force-pushed the add-scala.annotation.MainAnnotation branch from 9bd6708 to deb7d0b Compare April 1, 2022 15:56
var lines: Seq[String] = raw.trim.split('\n').toSeq
lines = lines.map(l => l.substring(skipLineLead(l, -1), l.length).trim)
var lines: Seq[String] = raw.trim.nn.split('\n').nn.toSeq
lines = lines.map(l => l.substring(skipLineLead(l, -1), l.length).nn.trim.nn)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@olhotak here is a real-world use case where we need too many .nn

* @param info The information about the command (name, documentation and info about parameters)
* @param args The command line arguments
*/
def command(info: CommandInfo, args: Array[String]): Command[?, ?]
Copy link
Contributor

@anatoliykmetyuk anatoliykmetyuk Apr 4, 2022

Choose a reason for hiding this comment

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

I believe the result type needs to be defined on the annotation level, not the command level. This way, we force the user to define the command type that can handle the result type known to be used with this particular annotation.

Why do we need the command abstraction anyway? Why can we not just require the user to implement a single run method to define the entire annotation? That is, the entire new annotation is fully described by a single function, of a signature similar to def run(program: () => Result, rawArgs: Array[String], argInfos: CommandInfo): Unit. Do you have any use cases that cannot be covered by an annotation defined by a single function?

Parsing logic of the arguments can be fully abstracted into the compiler's desugaring and depend on the implicit Parsers defined with the annotation, following a common-sense logic to handle default parameters and varargs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How would you get the Parser instances in that encoding? The point of having the def argGetter[T]... is to have the type and be able to summon the Parser[T].

Copy link
Contributor

@anatoliykmetyuk anatoliykmetyuk Apr 4, 2022

Choose a reason for hiding this comment

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

That would happen at the same place in desugaring where argGetter[T] is called. The difference is that instead of argGetter[T], summon[Parser[T]] will be called, and also there will be supporting logic that would handle which arguments from Array[String] get processed with which summoned parsers. This logic will also take into account different styles of specifying parameters from the command line in a common-sense way: e.g. with or without dash syntax, with the possibility of params with default values omitted etc.

If the user needs something more complicated than that common sense, they can always fall back to the good-old def main.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently Parser does not define any API. We would not be able to call any method on it to parse the argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We also do not know which argument to parse as the Array[String] may need some preprocessing.

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'll investigate this possibility

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@anatoliykmetyuk I simplified the interface a significant amount thanks to your suggestions. Now we do not have the Command class anymore, everything is in MainAnnotation. I still keep the argGetter/varargsGetter as these are necessary to handle errors while parsing. The current design is as expressive as the old one.

Copy link
Contributor

@anatoliykmetyuk anatoliykmetyuk Apr 7, 2022

Choose a reason for hiding this comment

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

Looks much better! See my review comments below.

Also, can you elaborate how argGetter/varargGetter can be used for error handling the way the summoned parser can't? From the MainProxies doc, parsing of arguments:

   *           val args0: () => S = annotation.argGetter[S](info.parameters(0), cmd(0), None)

Do you believe there is something this construct can express that the following can't regarding error handling?

   *           val args0: () => S = summon[Parser[S]].parse(info.parameters(0), cmd(0), None)

Copy link
Contributor Author

@nicolasstucki nicolasstucki Apr 11, 2022

Choose a reason for hiding this comment

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

The newMain uses this to try to parse all arguments before failing. In case of a failure, the error is reported and the program continues trying to parse the following arguments. Then the run does not evaluate the program if some parsing errors happened.

See example:

@nicolasstucki nicolasstucki force-pushed the add-scala.annotation.MainAnnotation branch 5 times, most recently from eb6148f to ce0b59a Compare April 6, 2022 07:50
*
* @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.

@nicolasstucki nicolasstucki force-pushed the add-scala.annotation.MainAnnotation branch 2 times, most recently from 2a385ed to 8e37659 Compare April 11, 2022 11:51
@nicolasstucki nicolasstucki added the release-notes Should be mentioned in the release notes label Apr 13, 2022
timotheeandres and others added 2 commits April 14, 2022 14:54
See `docs/_docs/reference/experimental/main-annotation.md`
Remove the `Command` class and place the `argGetter`, `varargsGetter`
and `run` methods directly in the `MainAnnotation` interface.

Now `command` pre-processes the arguments which clearly states which
strings will be used for each argument. This simplifies the implementation
of the `MainAnnotation` methods.
@nicolasstucki nicolasstucki force-pushed the add-scala.annotation.MainAnnotation branch from 8e37659 to 813e059 Compare April 14, 2022 12:59
@nicolasstucki
Copy link
Contributor Author

Rebased and updated MiMaFilters.

@nicolasstucki nicolasstucki enabled auto-merge April 14, 2022 13:01
@nicolasstucki nicolasstucki merged commit 1bd7c77 into scala:main Apr 14, 2022
@nicolasstucki nicolasstucki deleted the add-scala.annotation.MainAnnotation branch April 14, 2022 14:39
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"

@Kordyjan Kordyjan added this to the 3.2.0 milestone Aug 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release-notes Should be mentioned in the release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants