-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Add scala.annotation.MainAnnotation
#14558
Conversation
2a9d894
to
edc34e8
Compare
c0136b7
to
c040077
Compare
* @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] |
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'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] = ...
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.
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]]
...
}
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 could make this change in a following PR and discuss the tradeoff there.
* | ||
* @param program A function containing the call to the main method and instantiation of its arguments | ||
*/ | ||
def run(program: () => Result): Unit |
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.
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.
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.
The current error message a type Result = Int
is also not ideal.
|Found: () => Unit
|Required: () => Int
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.
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
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.
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], | ||
) |
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.
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?
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.
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 |
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.
Should we just use StaticAnnotation
instead of ParameterAnnotation
?
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.
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 |
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.
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
What do you mean by conditional arguments? Could you give an example? |
Examples: you can pass |
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 @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 |
|
the idea would be that a library would do the processing instead of the user |
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 won't have the time to review the code in detail I am afraid. I agree with the general direction, though.
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.
Otherwise LGTM
// 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 = |
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 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.
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.
Could you elaborate on how to remove the Parser
?
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 believe those getters the user has to override and the implicit Parser
s 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 Parser
s being present in the implicit scope.
9bd6708
to
deb7d0b
Compare
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) |
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.
@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[?, ?] |
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 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 Parser
s defined with the annotation, following a common-sense logic to handle default parameters and varargs.
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.
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]
.
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.
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
.
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.
Currently Parser
does not define any API. We would not be able to call any method on it to parse the argument.
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.
We also do not know which argument to parse as the Array[String]
may need some preprocessing.
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'll investigate this possibility
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.
@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.
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.
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)
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.
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:
argGetter
/varargsGetter
/run
: https://github.com/lampepfl/dotty/pull/14841/files#diff-73b166621bb3162b59d2e39763a5883b7d44966e7e7959b260417f9a77d2be24R90-R104- Parsing and error handling: https://github.com/lampepfl/dotty/pull/14841/files#diff-73b166621bb3162b59d2e39763a5883b7d44966e7e7959b260417f9a77d2be24R173-R184
eb6148f
to
ce0b59a
Compare
* | ||
* @param program A function containing the call to the main method and instantiation of its arguments | ||
*/ | ||
def run(program: () => Result): Unit |
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 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
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.
If the user really needs it, they can always set a private mutable variable from command
.
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.
Yes, they can.
2a385ed
to
8e37659
Compare
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.
8e37659
to
813e059
Compare
Rebased and updated |
name = "foo.main", | ||
documentation = "Sum all the numbers", | ||
parameters = Seq( | ||
new Parameter("first", "scala.Int", hasDefault=false, isVarargs=false, "Fist number to sum", Seq()), |
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.
Typo here: Should be "First" not "Fist"
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