-
Notifications
You must be signed in to change notification settings - Fork 33
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
C# source generator for docopt usage #77
Conversation
There is no concept or specification for the final shape of this feature. In its first draft, the source generator simply generates C# source code similar to the T4 template, but it would be a shame to leave it there. It can and should go much further; it can be a lot smarter given that the source generator has access to the syntactic and semantic model of a project at generation-time. The PR should help to explore and develop the feature. Meanwhile, take it for a spin:
|
I love this - lots to unpack for me, I will take it for a spin! |
Cool and needless to say, feature ideas welcome! I am not sure how well this is doable/mappable, but I'd like to toy with the idea that the source generate can generate code to bind the following usage:
to the following method signatures: int ShipNew(string[] name);
int ShipMove(string name, int x, int y, int speed);
int ShipShoot(int x, int y, int);
int Mine(Choice<Set, Remove> action, int x, int y, Choice<Set, Remove>? disposition);
// generated types:
enum Set { Set }
enum Remove { Remove }
enum Moored { Moored }
enum Drifting { Drifting } |
Conflicts resolved: - eg/SourceGenerator/NavalFate/NavalFate.csproj - eg/SourceGenerator/NavalFate/Program.cs - eg/SourceGenerator/NavalFate/Program.docopt.txt
For example: // Required: // Either: // Required: // Command(ship, False) -> CommandNode ship Bool // Command(new, False) -> CommandNode new Bool // OneOrMore: // Argument(<name>, []) -> ArgumentNode <name> List // Required: // Command(ship, False) -> CommandNode ship Bool // Argument(<name>, []) -> ArgumentNode <name> List // Command(move, False) -> CommandNode move Bool // Argument(<x>, ) -> ArgumentNode <x> String // Argument(<y>, ) -> ArgumentNode <y> String // Optional: // Option(,--speed,1,10) -> OptionNode speed String // Required: // Command(ship, False) -> CommandNode ship Bool // Command(shoot, False) -> CommandNode shoot Bool // Argument(<x>, ) -> ArgumentNode <x> String // Argument(<y>, ) -> ArgumentNode <y> String // Required: // Command(mine, False) -> CommandNode mine Bool // Required: // Either: // Command(set, False) -> CommandNode set Bool // Command(remove, False) -> CommandNode remove Bool // Argument(<x>, ) -> ArgumentNode <x> String // Argument(<y>, ) -> ArgumentNode <y> String // Optional: // Either: // Option(,--moored,0,False) -> OptionNode moored Bool // Option(,--drifting,0,False) -> OptionNode drifting Bool // Required: // Required: // Option(-h,--help,0,False) -> OptionNode help Bool // Required: // Option(,--version,0,False) -> OptionNode version Bool
Example of what's generated: static readonly Pattern Pattern = new Required(ImmutableArray.Create<Pattern>( new Either(ImmutableArray.Create<Pattern>( new Required(ImmutableArray.Create<Pattern>( new Command("ship"), new Command("new"), new OneOrMore( new Argument("<name>", null)))), new Required(ImmutableArray.Create<Pattern>( new Command("ship"), new Argument("<name>", null), new Command("move"), new Argument("<x>", null), new Argument("<y>", null), new Optional(ImmutableArray.Create<Pattern>( new Option("", "--speed", 1, null))))), new Required(ImmutableArray.Create<Pattern>( new Command("ship"), new Command("shoot"), new Argument("<x>", null), new Argument("<y>", null))), new Required(ImmutableArray.Create<Pattern>( new Command("mine"), new Required(ImmutableArray.Create<Pattern>( new Either(ImmutableArray.Create<Pattern>( new Command("set"), new Command("remove"))))), new Argument("<x>", null), new Argument("<y>", null), new Optional(ImmutableArray.Create<Pattern>( new Either(ImmutableArray.Create<Pattern>( new Option("", "--moored", 0, null), new Option("", "--drifting", 0, null))))))), new Required(ImmutableArray.Create<Pattern>( new Required(ImmutableArray.Create<Pattern>( new Option("-h", "--help", 0, null))))), new Required(ImmutableArray.Create<Pattern>( new Option("", "--version", 0, null)))))));
With 41bf0c0 and 54825a7, the source generator now creates a static version of the pattern tree created from the usage text: // Required:
// Either:
// Required:
// Command(ship, False) -> CommandNode ship Bool
// Command(new, False) -> CommandNode new Bool
// OneOrMore:
// Argument(<name>, []) -> ArgumentNode <name> List
// Required:
// Command(ship, False) -> CommandNode ship Bool
// Argument(<name>, []) -> ArgumentNode <name> List
// Command(move, False) -> CommandNode move Bool
// Argument(<x>, ) -> ArgumentNode <x> String
// Argument(<y>, ) -> ArgumentNode <y> String
// Optional:
// Option(,--speed,1,10) -> OptionNode speed String
// Required:
// Command(ship, False) -> CommandNode ship Bool
// Command(shoot, False) -> CommandNode shoot Bool
// Argument(<x>, ) -> ArgumentNode <x> String
// Argument(<y>, ) -> ArgumentNode <y> String
// Required:
// Command(mine, False) -> CommandNode mine Bool
// Required:
// Either:
// Command(set, False) -> CommandNode set Bool
// Command(remove, False) -> CommandNode remove Bool
// Argument(<x>, ) -> ArgumentNode <x> String
// Argument(<y>, ) -> ArgumentNode <y> String
// Optional:
// Either:
// Option(,--moored,0,False) -> OptionNode moored Bool
// Option(,--drifting,0,False) -> OptionNode drifting Bool
// Required:
// Required:
// Option(-h,--help,0,False) -> OptionNode help Bool
// Required:
// Option(,--version,0,False) -> OptionNode version Bool
static readonly Pattern Pattern =
new Required(ImmutableArray.Create<Pattern>(
new Either(ImmutableArray.Create<Pattern>(
new Required(ImmutableArray.Create<Pattern>(
new Command("ship"),
new Command("new"),
new OneOrMore(
new Argument("<name>", null)))),
new Required(ImmutableArray.Create<Pattern>(
new Command("ship"),
new Argument("<name>", null),
new Command("move"),
new Argument("<x>", null),
new Argument("<y>", null),
new Optional(ImmutableArray.Create<Pattern>(
new Option("", "--speed", 1, null))))),
new Required(ImmutableArray.Create<Pattern>(
new Command("ship"),
new Command("shoot"),
new Argument("<x>", null),
new Argument("<y>", null))),
new Required(ImmutableArray.Create<Pattern>(
new Command("mine"),
new Required(ImmutableArray.Create<Pattern>(
new Either(ImmutableArray.Create<Pattern>(
new Command("set"),
new Command("remove"))))),
new Argument("<x>", null),
new Argument("<y>", null),
new Optional(ImmutableArray.Create<Pattern>(
new Either(ImmutableArray.Create<Pattern>(
new Option("", "--moored", 0, null),
new Option("", "--drifting", 0, null))))))),
new Required(ImmutableArray.Create<Pattern>(
new Required(ImmutableArray.Create<Pattern>(
new Option("-h", "--help", 0, null))))),
new Required(ImmutableArray.Create<Pattern>(
new Option("", "--version", 0, null))))))); This means that there is zero run-time cost of parsing the document usage, which is now all passed to build-time. With the above tree, one technically only needs docopt.net/src/DocoptNet/Docopt.cs Lines 166 to 167 in ed57edb
and the |
0100f0f adds the first generated version of At this stage, I am not even sure the generated code works so the next step is to add tests, fix bugs and then refactor (with tests avoiding regressions). |
Notes for reviewersThere are a large number (75+) of files changed/added in this PR, but keep in mind that 50 of those belong to examples added (from the reference Python implementation) that helped to shape & drive the generated code and testing:
The examples include the generated code and technically don't need to be committed/tracked. They were added nonetheless to help with online review as well as regressions during development. They should/will be removed before the final merge of this PR. There are 14 such files:
When you exclude the examples, then changes to review only amount to a third (~25 files):
There are even fewer when you consider core project changes (i.e. excluding tests):
In the very core project (i.e. excluding the source generator project), the bulk of the changes went into
Apart from the technical changes, reviewers should focus on the overall design of the generated code in terms of:
|
Super helpful notes, thank you @atifaziz |
In my own review, I noticed that the |
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.
After our offline conversation about the PR, I have no additional questions or comments - thanks!
foreach (var (name, value) in arguments) | ||
Console.WriteLine($"{name} = {value}"); | ||
|
||
Console.WriteLine($@"{{ |
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 as clean and simple as the Python version. Someone on my team was commenting that there was a lot of magic happening. The good news is that magic is not as costly as one would expect.
@@ -0,0 +1,290 @@ | |||
#nullable enable annotations |
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.
Source generator is one step towards addressing the ask from issue #8. What do you think @prabirshrestha?
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 #76 kind of addressed that already.
|
||
namespace ArgumentsExample | ||
{ | ||
partial class ProgramArguments : IEnumerable<KeyValuePair<string, object?>> |
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 generated code is so legible! 😮 - And there are so many ways we could extend this to support new scenarios, really exciting stuff.
@@ -0,0 +1,21 @@ | |||
usage: git [--version] [--exec-path=<path>] [--html-path] |
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 have posted issue #114 to capture a weird scenario where exec-path is both a boolean and a string.
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.
Did you see my reply there?
Congratulations for the merge!!! 🎉🎉🎉 |
Thanks! Slowly, but surely, made it to the finishing line! 🏁 Appreciate all your trust, patience and support during the months it took to work through this PR! 😄 |
This PR implements a C# source generator for docopt. It generates strong-typed C# given a
*.docopt.txt
file.There is an example of NavalFate using the source generator.