-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathArgParserGenerator.fs
More file actions
2155 lines (1945 loc) · 103 KB
/
Copy pathArgParserGenerator.fs
File metadata and controls
2155 lines (1945 loc) · 103 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
namespace WoofWare.Myriad.Plugins
open System
open System.Text
open Fantomas.FCS.Syntax
open Fantomas.FCS.Text.Range
open TypeEquality
open WoofWare.Whippet.Fantomas
type internal ArgParserOutputSpec =
{
ExtensionMethods : bool
}
type internal FlagDu =
{
Name : Ident
Case1Name : Ident
Case2Name : Ident
/// Hopefully this is simply the const bool True or False, but it might e.g. be a literal
Case1Arg : SynExpr
/// Hopefully this is simply the const bool True or False, but it might e.g. be a literal
Case2Arg : SynExpr
}
static member FromBoolean (flagDu : FlagDu) (value : SynExpr) =
SynExpr.ifThenElse
(SynExpr.equals value flagDu.Case1Arg)
(SynExpr.createLongIdent' [ flagDu.Name ; flagDu.Case2Name ])
(SynExpr.createLongIdent' [ flagDu.Name ; flagDu.Case1Name ])
/// The default value of an argument which admits default values can be pulled from different sources.
/// This defines which source a particular default value comes from.
type private ArgumentDefaultSpec =
/// From parsing the environment variable with the given name (e.g. "WOOFWARE_DISABLE_FOO" or whatever).
| EnvironmentVariable of name : SynExpr
/// From calling the static member `{typeWeParseInto}.Default{name}()`
/// For example, if `type MyArgs = { Thing : Choice<int, int> }`, then
/// we would use `MyArgs.DefaultThing () : int`.
///
| FunctionCall of name : Ident
type private Accumulation<'choice> =
| Required
| Optional
| Choice of 'choice
| List of Accumulation<'choice>
type private ParseFunction<'acc> =
{
FieldName : Ident
TargetVariable : Ident
/// Any of the forms in this set are acceptable, but make sure they all start with a dash, or we might
/// get confused with positional args or something! I haven't thought that hard about this.
/// In the default case, this is `Const("arg-name")` for the `ArgName : blah` field; note that we have
/// omitted the initial `--` that will be required at runtime.
ArgForm : SynExpr list
/// If this is a boolean-like field (e.g. a bool or a flag DU), the help text should look a bit different:
/// we should lie to the user about the value of the cases there.
/// Similarly, if we're reading from an environment variable with the laxer parsing rules of accepting e.g.
/// "0" instead of "false", we need to know if we're reading a bool.
/// In that case, `boolCases` is Some, and contains the construction of the flag (or boolean, in which case
/// you get no data).
BoolCases : Choice<FlagDu, unit> option
Help : SynExpr option
/// A function string -> %TargetType%, where TargetVariable is probably a `%TargetType% option`.
/// (Depending on `Accumulation`, we'll remove the `option` at the end of the parse, asserting that the
/// argument was supplied.)
/// This is allowed to throw if it fails to parse.
Parser : SynExpr
/// If `Accumulation` is `List`, then this is the type of the list *element*; analogously for optionals
/// and choices and so on.
TargetType : SynType
Accumulation : 'acc
/// If true, this boolean/flag field accepts --no- prefix for negation (has [<ArgumentNegateWithPrefix>])
AcceptsNegation : bool
}
/// A SynExpr of type `string` which we can display to the user at generated-program runtime to display all
/// the ways they can refer to this arg.
member arg.HumanReadableArgForm : SynExpr =
if arg.AcceptsNegation then
// Include both standard and --no- variants
// E.g., "--foo / --bar / --no-foo / --no-bar"
let standardFormatString =
List.replicate arg.ArgForm.Length "--%s" |> String.concat " / "
let negatedFormatString =
List.replicate arg.ArgForm.Length "--no-%s" |> String.concat " / "
let combinedFormatString = standardFormatString + " / " + negatedFormatString
// Apply all arg forms twice (once for standard, once for negated)
let allArgForms = arg.ArgForm @ arg.ArgForm
(SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst combinedFormatString),
allArgForms)
||> List.fold SynExpr.applyFunction
|> SynExpr.paren
else
// Standard behavior: just --foo / --bar
let formatString = List.replicate arg.ArgForm.Length "--%s" |> String.concat " / "
(SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst formatString), arg.ArgForm)
||> List.fold SynExpr.applyFunction
|> SynExpr.paren
[<RequireQualifiedAccess>]
type private ChoicePositional =
| Normal of includeFlagLike : SynExpr option
| Choice of includeFlagLike : SynExpr option
type private ParseFunctionPositional = ParseFunction<ChoicePositional>
type private ParseFunctionNonPositional = ParseFunction<Accumulation<ArgumentDefaultSpec>>
type private ParserSpec =
{
NonPositionals : ParseFunctionNonPositional list
/// The variable into which positional arguments will be accumulated.
/// In this case, the TargetVariable is a `ResizeArray` rather than the usual `option`.
Positionals : ParseFunctionPositional option
}
type private HasPositional = HasPositional
type private HasNoPositional = HasNoPositional
[<AutoOpen>]
module private TeqUtils =
let exFalso<'a> (_ : Teq<HasNoPositional, HasPositional>) : 'a = failwith "LOGIC ERROR!"
let exFalso'<'a> (_ : Teq<HasPositional, HasNoPositional>) : 'a = failwith "LOGIC ERROR!"
[<RequireQualifiedAccess>]
type private ParseTree<'hasPositional> =
| NonPositionalLeaf of ParseFunctionNonPositional * Teq<'hasPositional, HasNoPositional>
| PositionalLeaf of ParseFunctionPositional * Teq<'hasPositional, HasPositional>
/// `assemble` takes the SynExpr's (e.g. each record field contents) corresponding to each `Ident` in
/// the branch (e.g. each record field name),
/// and composes them into a `SynExpr` (e.g. the record-typed object).
| Branch of
fields : (Ident * ParseTree<HasNoPositional>) list *
assemble : (Map<string, SynExpr> -> SynExpr) *
Teq<'hasPositional, HasNoPositional>
/// `assemble` takes the SynExpr's (e.g. each record field contents) corresponding to each `Ident` in
/// the branch (e.g. each record field name),
/// and composes them into a `SynExpr` (e.g. the record-typed object).
| BranchPos of
posField : Ident *
fields : ParseTree<HasPositional> *
(Ident * ParseTree<HasNoPositional>) list *
assemble : (Map<string, SynExpr> -> SynExpr) *
Teq<'hasPositional, HasPositional>
type private ParseTreeEval<'ret> =
abstract Eval<'a> : ParseTree<'a> -> 'ret
type private ParseTreeCrate =
abstract Apply<'ret> : ParseTreeEval<'ret> -> 'ret
[<RequireQualifiedAccess>]
module private ParseTreeCrate =
let make<'a> (p : ParseTree<'a>) =
{ new ParseTreeCrate with
member _.Apply a = a.Eval p
}
[<RequireQualifiedAccess>]
module private ParseTree =
[<RequireQualifiedAccess>]
type State =
| Positional of ParseTree<HasPositional> * ParseTree<HasNoPositional> list
| NoPositional of ParseTree<HasNoPositional> list
let private cast (t : Teq<'a, 'b>) : Teq<ParseTree<'a>, ParseTree<'b>> = Teq.Cong.believeMe t
/// The `Ident` here is the field name.
let branch (assemble : Map<string, SynExpr> -> SynExpr) (subs : (Ident * ParseTreeCrate) list) : ParseTreeCrate =
let rec go
(selfIdent : Ident option)
(acc : (Ident * ParseTree<HasNoPositional>) list, pos : (Ident * ParseTree<HasPositional>) option)
(subs : (Ident * ParseTreeCrate) list)
: ParseTreeCrate
=
match subs with
| [] ->
match pos with
| None -> ParseTree.Branch (List.rev acc, assemble, Teq.refl) |> ParseTreeCrate.make
| Some (posField, pos) ->
ParseTree.BranchPos (posField, pos, List.rev acc, assemble, Teq.refl)
|> ParseTreeCrate.make
| (fieldName, sub) :: subs ->
{ new ParseTreeEval<_> with
member _.Eval (t : ParseTree<'a>) =
match t with
| ParseTree.NonPositionalLeaf (_, teq)
| ParseTree.Branch (_, _, teq) ->
go selfIdent (((fieldName, Teq.cast (cast teq) t) :: acc), pos) subs
| ParseTree.PositionalLeaf (_, teq)
| ParseTree.BranchPos (_, _, _, _, teq) ->
match pos with
| None -> go selfIdent (acc, Some (fieldName, Teq.cast (cast teq) t)) subs
| Some (ident, _) ->
failwith
$"Multiple entries tried to claim positional args! %s{ident.idText} and %s{fieldName.idText}"
}
|> sub.Apply
go None ([], None) subs
let rec accumulatorsNonPos (tree : ParseTree<HasNoPositional>) : ParseFunctionNonPositional list =
match tree with
| ParseTree.PositionalLeaf (_, teq) -> exFalso teq
| ParseTree.BranchPos (_, _, _, _, teq) -> exFalso teq
| ParseTree.NonPositionalLeaf (pf, _) -> [ pf ]
| ParseTree.Branch (trees, _, _) -> trees |> List.collect (snd >> accumulatorsNonPos)
/// Returns the positional arg separately.
let rec accumulatorsPos
(tree : ParseTree<HasPositional>)
: ParseFunctionNonPositional list * ParseFunctionPositional
=
match tree with
| ParseTree.PositionalLeaf (pf, _) -> [], pf
| ParseTree.NonPositionalLeaf (_, teq) -> exFalso' teq
| ParseTree.Branch (_, _, teq) -> exFalso' teq
| ParseTree.BranchPos (_, tree, trees, _, _) ->
let nonPos = trees |> List.collect (snd >> accumulatorsNonPos)
let nonPos2, pos = accumulatorsPos tree
nonPos @ nonPos2, pos
/// Collect all the ParseFunctions which are necessary to define variables, throwing away
/// all information relevant to composing the resulting variables into records.
/// Returns the list of non-positional parsers, and any positional parser that exists.
let accumulators<'a> (tree : ParseTree<'a>) : ParseFunctionNonPositional list * ParseFunctionPositional option =
// Sad duplication of some code here, but it was the easiest way to make it type-safe :(
match tree with
| ParseTree.PositionalLeaf (pf, _) -> [], Some pf
| ParseTree.NonPositionalLeaf (pf, _) -> [ pf ], None
| ParseTree.Branch (trees, _, _) -> trees |> List.collect (snd >> accumulatorsNonPos) |> (fun i -> i, None)
| ParseTree.BranchPos (_, tree, trees, _, _) ->
let nonPos = trees |> List.collect (snd >> accumulatorsNonPos)
let nonPos2, pos = accumulatorsPos tree
nonPos @ nonPos2, Some pos
|> fun (nonPos, pos) ->
// Extract all arg form strings for validation
let allArgForms =
Option.toList (pos |> Option.map _.ArgForm) @ (nonPos |> List.map _.ArgForm)
|> Seq.concat
|> Seq.choose (fun expr ->
match expr |> SynExpr.stripOptionalParen with
| SynExpr.Const (SynConst.String (s, _, _), _) -> Some s
| _ -> None
)
|> List.ofSeq
// Check for direct duplicates
let duplicateArgs =
allArgForms
|> List.groupBy id
|> List.choose (fun (key, v) -> if v.Length > 1 then Some key else None)
match duplicateArgs with
| dups when not dups.IsEmpty ->
let dups = dups |> String.concat " "
failwith $"Duplicate args detected! %s{dups}"
| _ ->
// Check for --no- prefix conflicts
// Build a map of arg names that have AcceptsNegation=true
let negatedForms =
nonPos
|> List.filter _.AcceptsNegation
|> List.collect (fun pf ->
pf.ArgForm
|> List.choose (fun expr ->
match expr |> SynExpr.stripOptionalParen with
| SynExpr.Const (SynConst.String (s, _, _), _) -> Some (pf.FieldName.idText, s)
| _ -> None
)
)
|> List.map (fun (fieldName, argForm) -> $"no-%s{argForm}", fieldName)
|> Map.ofList
// Check if any existing arg form conflicts with a --no- variant
let conflicts =
allArgForms
|> List.choose (fun argForm ->
match negatedForms.TryFind argForm with
| Some fieldWithNegation -> Some (argForm, fieldWithNegation)
| None -> None
)
match conflicts with
| [] -> ()
| conflicts ->
let conflictMessages =
conflicts
|> List.map (fun (argForm, fieldWithNegation) ->
$"Argument name conflict: '--%s{argForm}' collides with the --no- variant of field '%s{fieldWithNegation}' (which has [<ArgumentNegateWithPrefix>])"
)
|> String.concat "\n"
failwith $"Conflicting argument names detected:\n%s{conflictMessages}"
nonPos, pos
/// Build the return value.
let rec instantiate<'a> (tree : ParseTree<'a>) : SynExpr =
match tree with
| ParseTree.NonPositionalLeaf (pf, _) -> SynExpr.createIdent' pf.TargetVariable
| ParseTree.PositionalLeaf (pf, _) -> SynExpr.createIdent' pf.TargetVariable
| ParseTree.Branch (trees, assemble, _) ->
trees
|> List.map (fun (fieldName, contents) ->
let instantiated = instantiate contents
fieldName.idText, instantiated
)
|> Map.ofList
|> assemble
| ParseTree.BranchPos (posField, tree, trees, assemble, _) ->
let withPos = instantiate tree
trees
|> List.map (fun (fieldName, contents) ->
let instantiated = instantiate contents
fieldName.idText, instantiated
)
|> Map.ofList
|> Map.add posField.idText withPos
|> assemble
[<RequireQualifiedAccess>]
module internal ArgParserGenerator =
/// Convert e.g. "Foo" into "--foo".
let argify (ident : Ident) : string =
let result = StringBuilder ()
for c in ident.idText do
if Char.IsUpper c then
result.Append('-').Append (Char.ToLowerInvariant c) |> ignore<StringBuilder>
else
result.Append c |> ignore<StringBuilder>
result.ToString().TrimStart '-'
let private identifyAsFlag (flagDus : FlagDu list) (ty : SynType) : FlagDu option =
match ty with
| SynType.LongIdent (SynLongIdent.SynLongIdent (ident, _, _)) ->
flagDus
|> List.tryPick (fun du ->
let duName = du.Name.idText
let ident = List.last(ident).idText
if duName = ident then Some du else None
)
| _ -> None
/// Builds a function or lambda of one string argument, which returns a `ty` (as modified by the `Accumulation`;
/// for example, maybe it returns a `ty option` or a `ty list`).
/// The resulting SynType is the type of the *element* being parsed; so if the Accumulation is List, the SynType
/// is the list element.
let rec private createParseFunction<'choice>
(choice : ArgumentDefaultSpec option -> 'choice)
(flagDus : FlagDu list)
(fieldName : Ident)
(attrs : SynAttribute list)
(ty : SynType)
: SynExpr * Accumulation<'choice> * SynType
=
match ty with
| String -> SynExpr.createLambda "x" (SynExpr.createIdent "x"), Accumulation.Required, SynType.string
| PrimitiveType pt ->
SynExpr.createLambda
"x"
(SynExpr.applyFunction
(SynExpr.createLongIdent' (pt @ [ Ident.create "Parse" ]))
(SynExpr.createIdent "x")),
Accumulation.Required,
ty
| Uri ->
SynExpr.createLambda
"x"
(SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "Uri" ]) (SynExpr.createIdent "x")),
Accumulation.Required,
ty
| TimeSpan ->
let parseExact =
attrs
|> List.tryPick (fun attr ->
match attr.TypeName with
| SynLongIdent.SynLongIdent (idents, _, _) ->
match idents |> List.map (fun i -> i.idText) |> List.tryLast with
| Some "ParseExactAttribute"
| Some "ParseExact" -> Some attr.ArgExpr
| _ -> None
)
let culture =
attrs
|> List.tryPick (fun attr ->
match attr.TypeName with
| SynLongIdent.SynLongIdent (idents, _, _) ->
match idents |> List.map (fun i -> i.idText) |> List.tryLast with
| Some "InvariantCultureAttribute"
| Some "InvariantCulture" -> Some ()
| _ -> None
)
let parser =
match parseExact, culture with
| None, None ->
SynExpr.createIdent "x"
|> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "TimeSpan" ; "Parse" ])
| Some format, None ->
[
SynExpr.createIdent "x"
format
SynExpr.createLongIdent [ "System" ; "Globalization" ; "CultureInfo" ; "CurrentCulture" ]
]
|> SynExpr.tuple
|> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "TimeSpan" ; "ParseExact" ])
| None, Some () ->
[
SynExpr.createIdent "x"
SynExpr.createLongIdent [ "System" ; "Globalization" ; "CultureInfo" ; "InvariantCulture" ]
]
|> SynExpr.tuple
|> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "TimeSpan" ; "Parse" ])
| Some format, Some () ->
[
SynExpr.createIdent "x"
format
SynExpr.createLongIdent [ "System" ; "Globalization" ; "CultureInfo" ; "InvariantCulture" ]
]
|> SynExpr.tuple
|> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "TimeSpan" ; "ParseExact" ])
|> SynExpr.createLambda "x"
parser, Accumulation.Required, ty
| FileInfo ->
SynExpr.createLambda
"x"
(SynExpr.applyFunction
(SynExpr.createLongIdent [ "System" ; "IO" ; "FileInfo" ])
(SynExpr.createIdent "x")),
Accumulation.Required,
ty
| DirectoryInfo ->
SynExpr.createLambda
"x"
(SynExpr.applyFunction
(SynExpr.createLongIdent [ "System" ; "IO" ; "DirectoryInfo" ])
(SynExpr.createIdent "x")),
Accumulation.Required,
ty
| OptionType eltTy ->
let parseElt, acc, childTy =
createParseFunction choice flagDus fieldName attrs eltTy
match acc with
| Accumulation.Optional ->
failwith
$"ArgParser does not support optionals containing options at field %s{fieldName.idText}: %O{ty}"
| Accumulation.Choice _ ->
failwith
$"ArgParser does not support optionals containing choices at field %s{fieldName.idText}: %O{ty}"
| Accumulation.List _ ->
failwith $"ArgParser does not support optional lists at field %s{fieldName.idText}: %O{ty}"
| Accumulation.Required -> parseElt, Accumulation.Optional, childTy
| ChoiceType elts ->
match elts with
| [ elt1 ; elt2 ] ->
if not (SynType.provablyEqual elt1 elt2) then
failwith
$"ArgParser was unable to prove types %O{elt1} and %O{elt2} to be equal in a Choice. We require them to be equal."
let parseElt, acc, childTy = createParseFunction choice flagDus fieldName attrs elt1
match acc with
| Accumulation.Optional ->
failwith
$"ArgParser does not support choices containing options at field %s{fieldName.idText}: %O{ty}"
| Accumulation.List _ ->
failwith
$"ArgParser does not support choices containing lists at field %s{fieldName.idText}: %O{ty}"
| Accumulation.Choice _ ->
failwith
$"ArgParser does not support choices containing choices at field %s{fieldName.idText}: %O{ty}"
| Accumulation.Required ->
let relevantAttrs =
attrs
|> List.choose (fun attr ->
let (SynLongIdent.SynLongIdent (name, _, _)) = attr.TypeName
match name |> List.map _.idText with
| [ "ArgumentDefaultFunction" ]
| [ "ArgumentDefaultFunctionAttribute" ]
| [ "Plugins" ; "ArgumentDefaultFunction" ]
| [ "Plugins" ; "ArgumentDefaultFunctionAttribute" ]
| [ "Myriad" ; "Plugins" ; "ArgumentDefaultFunction" ]
| [ "Myriad" ; "Plugins" ; "ArgumentDefaultFunctionAttribute" ]
| [ "WoofWare" ; "Myriad" ; "Plugins" ; "ArgumentDefaultFunction" ]
| [ "WoofWare" ; "Myriad" ; "Plugins" ; "ArgumentDefaultFunctionAttribute" ] ->
ArgumentDefaultSpec.FunctionCall (Ident.create ("Default" + fieldName.idText))
|> Some
| [ "ArgumentDefaultEnvironmentVariable" ]
| [ "ArgumentDefaultEnvironmentVariableAttribute" ]
| [ "Plugins" ; "ArgumentDefaultEnvironmentVariable" ]
| [ "Plugins" ; "ArgumentDefaultEnvironmentVariableAttribute" ]
| [ "Myriad" ; "Plugins" ; "ArgumentDefaultEnvironmentVariable" ]
| [ "Myriad" ; "Plugins" ; "ArgumentDefaultEnvironmentVariableAttribute" ]
| [ "WoofWare" ; "Myriad" ; "Plugins" ; "ArgumentDefaultEnvironmentVariable" ]
| [ "WoofWare" ; "Myriad" ; "Plugins" ; "ArgumentDefaultEnvironmentVariableAttribute" ] ->
ArgumentDefaultSpec.EnvironmentVariable attr.ArgExpr |> Some
| _ -> None
)
let relevantAttr =
match relevantAttrs with
| [] -> None
| [ x ] -> Some x
| _ ->
failwith
$"Expected Choice to be annotated with at most one ArgumentDefaultFunction or similar, but it was annotated with multiple. Field: %s{fieldName.idText}"
parseElt, Accumulation.Choice (choice relevantAttr), childTy
| elts ->
let elts = elts |> List.map string<SynType> |> String.concat ", "
failwith
$"ArgParser requires Choice to be of the form Choice<'a, 'a>; that is, two arguments, both the same. For field %s{fieldName.idText}, got: %s{elts}"
| ListType eltTy ->
let parseElt, acc, childTy =
createParseFunction choice flagDus fieldName attrs eltTy
parseElt, Accumulation.List acc, childTy
| ty ->
match identifyAsFlag flagDus ty with
| None -> failwith $"Could not decide how to parse arguments for field %s{fieldName.idText} of type %O{ty}"
| Some flagDu ->
// Parse as a bool, and then do the `if-then` dance.
let parser =
SynExpr.createIdent "x"
|> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "Boolean" ; "Parse" ])
|> FlagDu.FromBoolean flagDu
|> SynExpr.createLambda "x"
parser, Accumulation.Required, ty
let rec private toParseSpec
(counter : int)
(flagDus : FlagDu list)
(ambientRecords : RecordType list)
(finalRecord : RecordType)
: ParseTreeCrate * int
=
finalRecord.Fields
|> List.iter (fun (SynField.SynField (isStatic = isStatic)) ->
if isStatic then
failwith "No static record fields allowed in ArgParserGenerator"
)
let counter, contents =
((counter, []), finalRecord.Fields)
||> List.fold (fun (counter, acc) (SynField.SynField (attrs, _, identOption, fieldType, _, _, _, _, _)) ->
let attrs = attrs |> List.collect (fun a -> a.Attributes)
let positionalArgAttr =
attrs
|> List.tryPick (fun a ->
match (List.last a.TypeName.LongIdent).idText with
| "PositionalArgsAttribute"
| "PositionalArgs" ->
match a.ArgExpr with
| SynExpr.Const (SynConst.Unit, _) -> Some None
| a -> Some (Some a)
| _ -> None
)
let parseExactModifier =
attrs
|> List.tryPick (fun a ->
match (List.last a.TypeName.LongIdent).idText with
| "ParseExactAttribute"
| "ParseExact" -> Some a.ArgExpr
| _ -> None
)
let helpText =
attrs
|> List.tryPick (fun a ->
match (List.last a.TypeName.LongIdent).idText with
| "ArgumentHelpTextAttribute"
| "ArgumentHelpText" -> Some a.ArgExpr
| _ -> None
)
let helpText =
match parseExactModifier, helpText with
| None, ht -> ht
| Some pe, None ->
SynExpr.createIdent "sprintf"
|> SynExpr.applyTo (SynExpr.CreateConst "[Parse format (.NET): %s]")
|> SynExpr.applyTo pe
|> Some
| Some pe, Some ht ->
SynExpr.createIdent "sprintf"
|> SynExpr.applyTo (SynExpr.CreateConst "%s [Parse format (.NET): %s]")
|> SynExpr.applyTo ht
|> SynExpr.applyTo pe
|> Some
let ident =
match identOption with
| None -> failwith "expected args field to have a name, but it did not"
| Some i -> i
let longForms =
attrs
|> List.choose (fun attr ->
match attr.TypeName with
| SynLongIdent.SynLongIdent (ident, _, _) ->
if (List.last ident).idText = "ArgumentLongForm" then
Some attr.ArgExpr
else
None
)
|> function
| [] -> List.singleton (SynExpr.CreateConst (argify ident))
| l -> List.ofSeq l
let ambientRecordMatch =
match fieldType with
| SynType.LongIdent (SynLongIdent.SynLongIdent (id, _, _)) ->
let target = List.last(id).idText
ambientRecords |> List.tryFind (fun r -> r.Name.idText = target)
| _ -> None
match ambientRecordMatch with
| Some ambient ->
// This field has a type we need to obtain from parsing another record.
let spec, counter = toParseSpec counter flagDus ambientRecords ambient
counter, (ident, spec) :: acc
| None ->
match positionalArgAttr with
| Some includeFlagLike ->
let getChoice (spec : ArgumentDefaultSpec option) : unit =
match spec with
| Some _ ->
failwith
"Positional Choice args cannot have default values. Remove [<ArgumentDefault*>] from the positional arg."
| None -> ()
let parser, accumulation, parseTy =
createParseFunction<unit> getChoice flagDus ident attrs fieldType
let isBoolLike =
match parseTy with
| PrimitiveType ident when ident |> List.map _.idText = [ "System" ; "Boolean" ] ->
Some (Choice2Of2 ())
| parseTy -> identifyAsFlag flagDus parseTy |> Option.map Choice1Of2
match accumulation with
| Accumulation.List (Accumulation.List _) ->
failwith "A list of positional args cannot contain lists."
| Accumulation.List Accumulation.Optional ->
failwith "A list of positional args cannot contain optionals. What would that even mean?"
| Accumulation.List (Accumulation.Choice ()) ->
{
FieldName = ident
Parser = parser
TargetVariable = Ident.create $"arg_%i{counter}"
Accumulation = ChoicePositional.Choice includeFlagLike
TargetType = parseTy
ArgForm = longForms
Help = helpText
BoolCases = isBoolLike
AcceptsNegation = false
}
|> fun t -> ParseTree.PositionalLeaf (t, Teq.refl)
| Accumulation.List Accumulation.Required ->
{
FieldName = ident
Parser = parser
TargetVariable = Ident.create $"arg_%i{counter}"
Accumulation = ChoicePositional.Normal includeFlagLike
TargetType = parseTy
ArgForm = longForms
Help = helpText
BoolCases = isBoolLike
AcceptsNegation = false
}
|> fun t -> ParseTree.PositionalLeaf (t, Teq.refl)
| Accumulation.Choice _
| Accumulation.Optional
| Accumulation.Required ->
failwith $"Expected positional arg accumulation type to be List, but it was %O{fieldType}"
|> ParseTreeCrate.make
| None ->
let getChoice (spec : ArgumentDefaultSpec option) : ArgumentDefaultSpec =
match spec with
| None ->
failwith
"Non-positional Choice args must have an `[<ArgumentDefault*>]` attribute on them."
| Some spec -> spec
let parser, accumulation, parseTy =
createParseFunction getChoice flagDus ident attrs fieldType
let isBoolLike =
match parseTy with
| PrimitiveType ident when ident |> List.map _.idText = [ "System" ; "Boolean" ] ->
Some (Choice2Of2 ())
| parseTy -> identifyAsFlag flagDus parseTy |> Option.map Choice1Of2
let hasNegateAttr =
attrs
|> List.exists (fun attr ->
match attr.TypeName with
| SynLongIdent.SynLongIdent (ident, _, _) ->
match (List.last ident).idText with
| "ArgumentNegateWithPrefixAttribute"
| "ArgumentNegateWithPrefix" -> true
| _ -> false
)
let acceptsNegation =
if hasNegateAttr then
match isBoolLike with
| Some _ -> true
| None ->
failwith
$"[<ArgumentNegateWithPrefix>] can only be applied to boolean or flag DU fields, but was applied to field %s{ident.idText} of type %O{fieldType}"
else
false
{
FieldName = ident
Parser = parser
TargetVariable = Ident.create $"arg_%i{counter}"
Accumulation = accumulation
TargetType = parseTy
ArgForm = longForms
Help = helpText
BoolCases = isBoolLike
AcceptsNegation = acceptsNegation
}
|> fun t -> ParseTree.NonPositionalLeaf (t, Teq.refl)
|> ParseTreeCrate.make
|> fun tree -> counter + 1, (ident, tree) :: acc
)
let tree =
contents
|> List.rev
|> ParseTree.branch (fun args ->
args
|> Map.toList
|> List.map (fun (ident, expr) -> SynLongIdent.create [ Ident.create ident ], expr)
|> SynExpr.createRecord None
)
tree, counter
/// let helpText : string = ...
let private helpText
(typeHelp : SynExpr option)
(typeName : Ident)
(positional : ParseFunctionPositional option)
(args : ParseFunctionNonPositional list)
: SynBinding
=
let describeNonPositional
(acc : Accumulation<ArgumentDefaultSpec>)
(flagCases : Choice<FlagDu, unit> option)
: SynExpr
=
match acc with
| Accumulation.Required -> SynExpr.CreateConst ""
| Accumulation.Optional -> SynExpr.CreateConst " (optional)"
| Accumulation.Choice (ArgumentDefaultSpec.EnvironmentVariable var) ->
// We don't print out the default value in case it's a secret. People often pass secrets
// through env vars!
var
|> SynExpr.pipeThroughFunction (
SynExpr.applyFunction
(SynExpr.createIdent "sprintf")
(SynExpr.CreateConst " (default value populated from env var %s)")
)
|> SynExpr.paren
| Accumulation.Choice (ArgumentDefaultSpec.FunctionCall var) ->
match flagCases with
| None -> SynExpr.callMethod var.idText (SynExpr.createIdent' typeName)
| Some (Choice2Of2 ()) -> SynExpr.callMethod var.idText (SynExpr.createIdent' typeName)
| Some (Choice1Of2 flagDu) ->
// Care required here. The return value from the Default call is not a bool,
// but we should display it as such to the user!
[
SynMatchClause.create
(SynPat.identWithArgs [ flagDu.Name ; flagDu.Case1Name ] (SynArgPats.create []))
(SynExpr.ifThenElse
(SynExpr.equals flagDu.Case1Arg (SynExpr.CreateConst true))
(SynExpr.CreateConst "false")
(SynExpr.CreateConst "true"))
SynMatchClause.create
(SynPat.identWithArgs [ flagDu.Name ; flagDu.Case2Name ] (SynArgPats.create []))
(SynExpr.ifThenElse
(SynExpr.equals flagDu.Case2Arg (SynExpr.CreateConst true))
(SynExpr.CreateConst "false")
(SynExpr.CreateConst "true"))
]
|> SynExpr.createMatch (SynExpr.callMethod var.idText (SynExpr.createIdent' typeName))
|> SynExpr.pipeThroughFunction (
SynExpr.createLambda "x" (SynExpr.callMethod "ToString" (SynExpr.createIdent "x"))
)
|> SynExpr.pipeThroughFunction (
SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst " (default value: %s)")
)
|> SynExpr.paren
| Accumulation.List _ -> SynExpr.CreateConst " (can be repeated)"
let describePositional _ _ =
SynExpr.CreateConst " (positional args) (can be repeated)"
/// We may sometimes lie about the type name, if e.g. this is a flag DU which we're pretending is a boolean.
/// So the `renderTypeName` takes the Accumulation which tells us whether we're lying.
let toPrintable (describe : 'a -> Choice<FlagDu, unit> option -> SynExpr) (arg : ParseFunction<'a>) : SynExpr =
let ty =
match arg.BoolCases with
| None -> SynType.toHumanReadableString arg.TargetType
| Some _ -> "bool"
let helpText =
match arg.Help with
| None -> SynExpr.CreateConst ""
| Some helpText ->
SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst " : %s")
|> SynExpr.applyTo (SynExpr.paren helpText)
|> SynExpr.paren
let descriptor = describe arg.Accumulation arg.BoolCases
SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst $"%%s %s{ty}%%s%%s")
|> SynExpr.applyTo arg.HumanReadableArgForm
|> SynExpr.applyTo descriptor
|> SynExpr.applyTo helpText
|> SynExpr.paren
let fieldHelp =
args
|> List.map (toPrintable describeNonPositional)
|> fun l ->
match positional with
| None -> l
| Some pos -> l @ [ toPrintable describePositional pos ]
let allHelp =
match typeHelp with
| Some helpExpr ->
// Prepend type help, followed by blank line, then field help
[ helpExpr ; SynExpr.CreateConst "" ] @ fieldHelp
| None ->
// No type help, just field help
fieldHelp
allHelp
|> SynExpr.listLiteral
|> SynExpr.pipeThroughFunction (
SynExpr.applyFunction (SynExpr.createLongIdent [ "String" ; "concat" ]) (SynExpr.CreateConst @"\n")
)
|> SynBinding.basic [ Ident.create "helpText" ] [ SynPat.unit ]
/// Helper to create a negated parser for boolean/flag fields.
/// Returns a SynExpr that represents: string -> (negated bool or negated flag DU)
/// For booleans: `fun x -> not (Boolean.Parse x)`
/// For flag DUs: `fun x -> FlagDu.FromBoolean flagDu (not (Boolean.Parse x))`
let private createNegatedParser (arg : ParseFunction<'acc>) : SynExpr =
match arg.BoolCases with
| None -> failwith $"LOGIC ERROR: createNegatedParser called on non-boolean field %s{arg.FieldName.idText}"
| Some (Choice2Of2 ()) ->
// Boolean: parse and negate
// fun x -> not (System.Boolean.Parse x)
let parseExpr =
SynExpr.createIdent "x"
|> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "Boolean" ; "Parse" ])
|> SynExpr.paren
parseExpr
|> SynExpr.applyFunction (SynExpr.createIdent "not")
|> SynExpr.createLambda "x"
| Some (Choice1Of2 flagDu) ->
// Flag DU: parse as bool, negate, then convert to flag DU
// fun x -> x |> System.Boolean.Parse |> not |> FlagDu.FromBoolean flagDu
let parseExpr =
SynExpr.createIdent "x"
|> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "Boolean" ; "Parse" ])
|> SynExpr.paren
parseExpr
|> SynExpr.applyFunction (SynExpr.createIdent "not")
|> FlagDu.FromBoolean flagDu
|> SynExpr.createLambda "x"
/// `let processKeyValue (key : string) (value : string) : Result<unit, string option> = ...`
/// Returns a possible error.
/// A parse failure might not be fatal (e.g. maybe the input was optionally of arity 0, and we failed to do
/// the parse because in fact the key decided not to take this argument); in that case we return Error None.
let private processKeyValue
(argParseErrors : Ident)
(pos : ParseFunctionPositional option)
(args : ParseFunctionNonPositional list)
: SynBinding
=
let args =
args
|> List.map (fun arg ->
let assignmentExpr =
match arg.Accumulation with
| Accumulation.Required
| Accumulation.Choice _
| Accumulation.Optional ->
let multipleErrorMessage =
SynExpr.createIdent "sprintf"
|> SynExpr.applyTo (
SynExpr.CreateConst "Argument '%s' was supplied multiple times: %s and %s"
)
|> SynExpr.applyTo arg.HumanReadableArgForm
|> SynExpr.applyTo (
SynExpr.createIdent "x" |> SynExpr.callMethod "ToString" |> SynExpr.paren
)
|> SynExpr.applyTo (
SynExpr.createIdent "value" |> SynExpr.callMethod "ToString" |> SynExpr.paren
)
let performAssignment =
[
SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction arg.Parser
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynExpr.assign (SynLongIdent.createI arg.TargetVariable)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ())
]
|> SynExpr.sequential
[
SynMatchClause.create
(SynPat.nameWithArgs "Some" [ SynPat.named "x" ])
(SynExpr.sequential
[
multipleErrorMessage
|> SynExpr.pipeThroughFunction (
SynExpr.dotGet "Add" (SynExpr.createIdent' argParseErrors)
)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ())
])
SynMatchClause.create
(SynPat.named "None")
(SynExpr.pipeThroughTryWith
SynPat.anon
(SynExpr.createLongIdent [ "exc" ; "Message" ]
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Error"))
performAssignment)
]
|> SynExpr.createMatch (SynExpr.createIdent' arg.TargetVariable)
| Accumulation.List (Accumulation.List _)
| Accumulation.List Accumulation.Optional
| Accumulation.List (Accumulation.Choice _) ->
failwith
"WoofWare.Myriad invariant violated: expected a list to contain only a Required accumulation. Non-positional lists cannot be optional or Choice, nor can they themselves contain lists."
| Accumulation.List Accumulation.Required ->
[
SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction arg.Parser
|> SynExpr.pipeThroughFunction (
SynExpr.createLongIdent' [ arg.TargetVariable ; Ident.create "Add" ]
)
SynExpr.CreateConst () |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Ok")
]
|> SynExpr.sequential
// Return (argForms, assignmentExpr), argMetadata
(arg.ArgForm, assignmentExpr), Some arg
)
let posArg =
match pos with
| None -> []
| Some pos ->
let posExpr =
[
SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction pos.Parser
|> fun p ->