forked from dotnet/runtime
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRegexNode.cs
More file actions
2446 lines (2167 loc) · 106 KB
/
Copy pathRegexNode.cs
File metadata and controls
2446 lines (2167 loc) · 106 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
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// This RegexNode class is internal to the Regex package.
// It is built into a parsed tree for a regular expression.
// Implementation notes:
//
// Since the node tree is a temporary data structure only used
// during compilation of the regexp to integer codes, it's
// designed for clarity and convenience rather than
// space efficiency.
//
// RegexNodes are built into a tree, linked by the _children list.
// Each node also has a _parent and _ichild member indicating
// its parent and which child # it is in its parent's list.
//
// RegexNodes come in as many types as there are constructs in
// a regular expression, for example, "concatenate", "alternate",
// "one", "rept", "group". There are also node types for basic
// peephole optimizations, e.g., "onerep", "notsetrep", etc.
//
// Because perl 5 allows "lookback" groups that scan backwards,
// each node also gets a "direction". Normally the value of
// boolean _backward = false.
//
// During parsing, top-level nodes are also stacked onto a parse
// stack (a stack of trees). For this purpose we have a _next
// pointer. [Note that to save a few bytes, we could overload the
// _parent pointer instead.]
//
// On the parse stack, each tree has a "role" - basically, the
// nonterminal in the grammar that the parser has currently
// assigned to the tree. That code is stored in _role.
//
// Finally, some of the different kinds of nodes have data.
// Two integers (for the looping constructs) are stored in
// _operands, an object (either a string or a set)
// is stored in _data
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Threading;
namespace System.Text.RegularExpressions
{
internal sealed class RegexNode
{
// RegexNode types
// The following are leaves, and correspond to primitive operations
public const int Oneloop = RegexCode.Oneloop; // c,n a*
public const int Notoneloop = RegexCode.Notoneloop; // c,n .*
public const int Setloop = RegexCode.Setloop; // set,n \d*
public const int Onelazy = RegexCode.Onelazy; // c,n a*?
public const int Notonelazy = RegexCode.Notonelazy; // c,n .*?
public const int Setlazy = RegexCode.Setlazy; // set,n \d*?
public const int One = RegexCode.One; // char a
public const int Notone = RegexCode.Notone; // char . [^a]
public const int Set = RegexCode.Set; // set [a-z] \w \s \d
public const int Multi = RegexCode.Multi; // string abcdef
public const int Ref = RegexCode.Ref; // index \1
public const int Bol = RegexCode.Bol; // ^
public const int Eol = RegexCode.Eol; // $
public const int Boundary = RegexCode.Boundary; // \b
public const int NonBoundary = RegexCode.NonBoundary; // \B
public const int ECMABoundary = RegexCode.ECMABoundary; // \b
public const int NonECMABoundary = RegexCode.NonECMABoundary; // \B
public const int Beginning = RegexCode.Beginning; // \A
public const int Start = RegexCode.Start; // \G
public const int EndZ = RegexCode.EndZ; // \Z
public const int End = RegexCode.End; // \z
public const int Oneloopatomic = RegexCode.Oneloopatomic; // c,n (?> a*)
public const int Notoneloopatomic = RegexCode.Notoneloopatomic; // c,n (?> .*)
public const int Setloopatomic = RegexCode.Setloopatomic; // set,n (?> \d*)
public const int UpdateBumpalong = RegexCode.UpdateBumpalong;
// Interior nodes do not correspond to primitive operations, but
// control structures compositing other operations
// Concat and alternate take n children, and can run forward or backwards
public const int Nothing = 22; // []
public const int Empty = 23; // ()
public const int Alternate = 24; // a|b
public const int Concatenate = 25; // ab
public const int Loop = 26; // m,x * + ? {,}
public const int Lazyloop = 27; // m,x *? +? ?? {,}?
public const int Capture = 28; // n () - capturing group
public const int Group = 29; // (?:) - noncapturing group
public const int Require = 30; // (?=) (?<=) - lookahead and lookbehind assertions
public const int Prevent = 31; // (?!) (?<!) - negative lookahead and lookbehind assertions
public const int Atomic = 32; // (?>) - atomic subexpression
public const int Testref = 33; // (?(n) | ) - alternation, reference
public const int Testgroup = 34; // (?(...) | )- alternation, expression
/// <summary>empty bit from the node's options to store data on whether a node contains captures</summary>
internal const RegexOptions HasCapturesFlag = (RegexOptions)(1 << 31);
private object? Children;
public int Type { get; private set; }
public string? Str { get; private set; }
public char Ch { get; private set; }
public int M { get; private set; }
public int N { get; private set; }
public RegexOptions Options;
public RegexNode? Next;
public RegexNode(int type, RegexOptions options)
{
Type = type;
Options = options;
}
public RegexNode(int type, RegexOptions options, char ch)
{
Type = type;
Options = options;
Ch = ch;
}
public RegexNode(int type, RegexOptions options, string str)
{
Type = type;
Options = options;
Str = str;
}
public RegexNode(int type, RegexOptions options, int m)
{
Type = type;
Options = options;
M = m;
}
public RegexNode(int type, RegexOptions options, int m, int n)
{
Type = type;
Options = options;
M = m;
N = n;
}
/// <summary>Creates a RegexNode representing a single character.</summary>
/// <param name="ch">The character.</param>
/// <param name="options">The node's options.</param>
/// <param name="culture">The culture to use to perform any required transformations.</param>
/// <returns>The created RegexNode. This might be a RegexNode.One or a RegexNode.Set.</returns>
public static RegexNode CreateOneWithCaseConversion(char ch, RegexOptions options, CultureInfo? culture)
{
// If the options specify case-insensitivity, we try to create a node that fully encapsulates that.
if ((options & RegexOptions.IgnoreCase) != 0)
{
Debug.Assert(culture is not null);
// If the character is part of a Unicode category that doesn't participate in case conversion,
// we can simply strip out the IgnoreCase option and make the node case-sensitive.
if (!RegexCharClass.ParticipatesInCaseConversion(ch))
{
return new RegexNode(One, options & ~RegexOptions.IgnoreCase, ch);
}
// Create a set for the character, trying to include all case-insensitive equivalent characters.
// If it's successful in doing so, resultIsCaseInsensitive will be false and we can strip
// out RegexOptions.IgnoreCase as part of creating the set.
string stringSet = RegexCharClass.OneToStringClass(ch, culture, out bool resultIsCaseInsensitive);
if (!resultIsCaseInsensitive)
{
return new RegexNode(Set, options & ~RegexOptions.IgnoreCase, stringSet);
}
// Otherwise, until we can get rid of ToLower usage at match time entirely (https://github.com/dotnet/runtime/issues/61048),
// lowercase the character and proceed to create an IgnoreCase One node.
ch = culture.TextInfo.ToLower(ch);
}
// Create a One node for the character.
return new RegexNode(One, options, ch);
}
/// <summary>Reverses all children of a concatenation when in RightToLeft mode.</summary>
public RegexNode ReverseConcatenationIfRightToLeft()
{
if ((Options & RegexOptions.RightToLeft) != 0 &&
Type == Concatenate &&
ChildCount() > 1)
{
((List<RegexNode>)Children!).Reverse();
}
return this;
}
/// <summary>
/// Pass type as OneLazy or OneLoop
/// </summary>
private void MakeRep(int type, int min, int max)
{
Type += type - One;
M = min;
N = max;
}
private void MakeLoopAtomic()
{
switch (Type)
{
case Oneloop or Notoneloop or Setloop:
// For loops, we simply change the Type to the atomic variant.
// Atomic greedy loops should consume as many values as they can.
Type += Oneloopatomic - Oneloop;
break;
case Onelazy or Notonelazy or Setlazy:
// For lazy, we not only change the Type, we also lower the max number of iterations
// to the minimum number of iterations, as they should end up matching as little as possible.
Type += Oneloopatomic - Onelazy;
N = M;
break;
default:
Debug.Fail($"Unexpected type: {Type}");
break;
}
}
#if DEBUG
/// <summary>Validate invariants the rest of the implementation relies on for processing fully-built trees.</summary>
[Conditional("DEBUG")]
private void ValidateFinalTreeInvariants()
{
Debug.Assert(Type == Capture, "Every generated tree should begin with a capture node");
var toExamine = new Stack<RegexNode>();
toExamine.Push(this);
while (toExamine.Count > 0)
{
RegexNode node = toExamine.Pop();
// Add all children to be examined
int childCount = node.ChildCount();
for (int i = 0; i < childCount; i++)
{
RegexNode child = node.Child(i);
Debug.Assert(child.Next == node, $"{child.Description()} missing reference to parent {node.Description()}");
toExamine.Push(child);
}
// Validate that we never see certain node types.
Debug.Assert(Type != Group, "All Group nodes should have been removed.");
// Validate node types and expected child counts.
switch (node.Type)
{
case Group:
Debug.Fail("All Group nodes should have been removed.");
break;
case Beginning:
case Bol:
case Boundary:
case ECMABoundary:
case Empty:
case End:
case EndZ:
case Eol:
case Multi:
case NonBoundary:
case NonECMABoundary:
case Nothing:
case Notone:
case Notonelazy:
case Notoneloop:
case Notoneloopatomic:
case One:
case Onelazy:
case Oneloop:
case Oneloopatomic:
case Ref:
case Set:
case Setlazy:
case Setloop:
case Setloopatomic:
case Start:
case UpdateBumpalong:
Debug.Assert(childCount == 0, $"Expected zero children for {node.TypeName}, got {childCount}.");
break;
case Atomic:
case Capture:
case Lazyloop:
case Loop:
case Prevent:
case Require:
Debug.Assert(childCount == 1, $"Expected one and only one child for {node.TypeName}, got {childCount}.");
break;
case Testref:
Debug.Assert(childCount == 2, $"Expected two children for {node.TypeName}, got {childCount}");
break;
case Testgroup:
Debug.Assert(childCount == 3, $"Expected three children for {node.TypeName}, got {childCount}");
break;
case Concatenate:
case Alternate:
Debug.Assert(childCount >= 2, $"Expected at least two children for {node.TypeName}, got {childCount}.");
break;
default:
Debug.Fail($"Unexpected node type: {node.Type}");
break;
}
// Validate node configuration.
switch (node.Type)
{
case Multi:
Debug.Assert(node.Str is not null, "Expect non-null multi string");
Debug.Assert(node.Str.Length >= 2, $"Expected {node.Str} to be at least two characters");
break;
case Set:
case Setloop:
case Setloopatomic:
case Setlazy:
Debug.Assert(!string.IsNullOrEmpty(node.Str), $"Expected non-null, non-empty string for {node.TypeName}.");
break;
default:
Debug.Assert(node.Str is null, $"Expected null string for {node.TypeName}, got \"{node.Str}\".");
break;
}
}
}
#endif
/// <summary>Performs additional optimizations on an entire tree prior to being used.</summary>
/// <remarks>
/// Some optimizations are performed by the parser while parsing, and others are performed
/// as nodes are being added to the tree. The optimizations here expect the tree to be fully
/// formed, as they inspect relationships between nodes that may not have been in place as
/// individual nodes were being processed/added to the tree.
/// </remarks>
internal RegexNode FinalOptimize()
{
RegexNode rootNode = this;
Debug.Assert(rootNode.Type == Capture);
Debug.Assert(rootNode.Next is null);
Debug.Assert(rootNode.ChildCount() == 1);
if ((Options & RegexOptions.RightToLeft) == 0) // only apply optimization when LTR to avoid needing additional code for the rarer RTL case
{
// Optimization: backtracking removal at expression end.
// If we find backtracking construct at the end of the regex, we can instead make it non-backtracking,
// since nothing would ever backtrack into it anyway. Doing this then makes the construct available
// to implementations that don't support backtracking.
rootNode.EliminateEndingBacktracking();
// Optimization: unnecessary re-processing of starting loops.
// If an expression is guaranteed to begin with a single-character unbounded loop that isn't part of an alternation (in which case it
// wouldn't be guaranteed to be at the beginning) or a capture (in which case a back reference could be influenced by its length), then we
// can update the tree with a temporary node to indicate that the implementation should use that node's ending position in the input text
// as the next starting position at which to start the next match. This avoids redoing matches we've already performed, e.g. matching
// "\w+@dot.net" against "is this a valid address@dot.net", the \w+ will initially match the "is" and then will fail to match the "@".
// Rather than bumping the scan loop by 1 and trying again to match at the "s", we can instead start at the " ". For functional correctness
// we can only consider unbounded loops, as to be able to start at the end of the loop we need the loop to have consumed all possible matches;
// otherwise, you could end up with a pattern like "a{1,3}b" matching against "aaaabc", which should match, but if we pre-emptively stop consuming
// after the first three a's and re-start from that position, we'll end up failing the match even though it should have succeeded. We can also
// apply this optimization to non-atomic loops. Even though backtracking could be necessary, such backtracking would be handled within the processing
// of a single starting position.
{
RegexNode node = rootNode.Child(0); // skip implicit root capture node
while (true)
{
switch (node.Type)
{
case Atomic:
case Concatenate:
node = node.Child(0);
continue;
case Oneloop or Oneloopatomic or Notoneloop or Notoneloopatomic or Setloop or Setloopatomic when node.N == int.MaxValue:
RegexNode? parent = node.Next;
if (parent != null && parent.Type == Concatenate)
{
parent.InsertChild(1, new RegexNode(UpdateBumpalong, node.Options));
}
break;
}
break;
}
}
}
// Done optimizing. Return the final tree.
#if DEBUG
rootNode.ValidateFinalTreeInvariants();
#endif
return rootNode;
}
/// <summary>Converts nodes at the end of the node tree to be atomic.</summary>
/// <remarks>
/// The correctness of this optimization depends on nothing being able to backtrack into
/// the provided node. That means it must be at the root of the overall expression, or
/// it must be an Atomic node that nothing will backtrack into by the very nature of Atomic.
/// </remarks>
private void EliminateEndingBacktracking()
{
if (!StackHelper.TryEnsureSufficientExecutionStack())
{
// If we can't recur further, just stop optimizing.
return;
}
// RegexOptions.NonBacktracking doesn't support atomic groups, so when that option
// is set we don't want to create atomic groups where they weren't explicitly authored.
if ((Options & RegexOptions.NonBacktracking) != 0)
{
return;
}
// Walk the tree starting from the current node.
RegexNode node = this;
while (true)
{
switch (node.Type)
{
// {One/Notone/Set}loops can be upgraded to {One/Notone/Set}loopatomic nodes, e.g. [abc]* => (?>[abc]*).
// And {One/Notone/Set}lazys can similarly be upgraded to be atomic, which really makes them into repeaters
// or even empty nodes.
case Oneloop:
case Notoneloop:
case Setloop:
case Onelazy:
case Notonelazy:
case Setlazy:
node.MakeLoopAtomic();
break;
// Just because a particular node is atomic doesn't mean all its descendants are.
// Process them as well.
case Atomic:
node = node.Child(0);
continue;
// For Capture and Concatenate, we just recur into their last child (only child in the case
// of Capture). However, if the child is Alternate, Loop, and Lazyloop, we can also make the
// node itself atomic by wrapping it in an Atomic node. Since we later check to see whether a
// node is atomic based on its parent or grandparent, we don't bother wrapping such a node in
// an Atomic one if its grandparent is already Atomic.
// e.g. [xyz](?:abc|def) => [xyz](?>abc|def)
case Capture:
case Concatenate:
RegexNode existingChild = node.Child(node.ChildCount() - 1);
if ((existingChild.Type == Alternate || existingChild.Type == Loop || existingChild.Type == Lazyloop) &&
(node.Next is null || node.Next.Type != Atomic)) // validate grandparent isn't atomic
{
var atomic = new RegexNode(Atomic, existingChild.Options);
atomic.AddChild(existingChild);
node.ReplaceChild(node.ChildCount() - 1, atomic);
}
node = existingChild;
continue;
// For alternate, we can recur into each branch separately. We use this iteration for the first branch.
// e.g. abc*|def* => ab(?>c*)|de(?>f*)
case Alternate:
{
int branches = node.ChildCount();
for (int i = 1; i < branches; i++)
{
node.Child(i).EliminateEndingBacktracking();
}
}
node = node.Child(0);
continue;
// For Loop, we search to see if there's a viable last expression, and iff there
// is we recur into processing it.
// e.g. (?:abc*)* => (?:ab(?>c*))*
case Loop:
{
RegexNode? loopDescendent = node.FindLastExpressionInLoopForAutoAtomic();
if (loopDescendent != null)
{
node = loopDescendent;
continue; // loop around to process node
}
}
break;
}
break;
}
}
/// <summary>Whether this node may be considered to be atomic based on its parent.</summary>
/// <remarks>
/// This may have false negatives, meaning the node may actually be atomic even if this returns false.
/// But any true result may be relied on to mean the node will actually be considered to be atomic.
/// </remarks>
public bool IsAtomicByParent()
{
// Walk up the parent hierarchy.
RegexNode child = this;
for (RegexNode? parent = child.Next; parent is not null; child = parent, parent = child.Next)
{
switch (parent.Type)
{
case Atomic:
case Prevent:
case Require:
// If the parent is atomic, so is the child. That's the whole purpose
// of the Atomic node, and lookarounds are also implicitly atomic.
return true;
case Alternate:
case Testref:
// Skip alternations. Each branch is considered independently,
// so any atomicity applied to the alternation also applies to
// each individual branch. This is true as well for conditional
// backreferences, where each of the yes/no branches are independent.
case Testgroup when parent.Child(0) != child:
// As with alternations, each yes/no branch of an expression conditional
// are independent from each other, but the conditional expression itself
// can be backtracked into from each of the branches, so we can't make
// it atomic just because the whole conditional is.
case Capture:
// Skip captures. They don't affect atomicity.
case Concatenate when parent.Child(parent.ChildCount() - 1) == child:
// If the parent is a concatenation and this is the last node,
// any atomicity applying to the concatenation applies to this
// node, too.
continue;
default:
// For any other parent type, give up on trying to prove atomicity.
return false;
}
}
// The parent was null, so nothing can backtrack in.
return true;
}
/// <summary>
/// Removes redundant nodes from the subtree, and returns an optimized subtree.
/// </summary>
internal RegexNode Reduce() =>
Type switch
{
Alternate => ReduceAlternation(),
Atomic => ReduceAtomic(),
Concatenate => ReduceConcatenation(),
Group => ReduceGroup(),
Loop or Lazyloop => ReduceLoops(),
Prevent => ReducePrevent(),
Set or Setloop or Setloopatomic or Setlazy => ReduceSet(),
Testgroup => ReduceTestgroup(),
Testref => ReduceTestref(),
_ => this,
};
/// <summary>Remove an unnecessary Concatenation or Alternation node</summary>
/// <remarks>
/// Simple optimization for a concatenation or alternation:
/// - if the node has only one child, use it instead
/// - if the node has zero children, turn it into an empty with the specified empty type
/// </remarks>
private RegexNode ReplaceNodeIfUnnecessary(int emptyTypeIfNoChildren)
{
Debug.Assert(
(Type == Alternate && emptyTypeIfNoChildren == Nothing) ||
(Type == Concatenate && emptyTypeIfNoChildren == Empty));
return ChildCount() switch
{
0 => new RegexNode(emptyTypeIfNoChildren, Options),
1 => Child(0),
_ => this,
};
}
/// <summary>Remove all non-capturing groups.</summary>
/// <remark>
/// Simple optimization: once parsed into a tree, non-capturing groups
/// serve no function, so strip them out.
/// e.g. (?:(?:(?:abc))) => abc
/// </remark>
private RegexNode ReduceGroup()
{
Debug.Assert(Type == Group);
RegexNode u = this;
while (u.Type == Group)
{
Debug.Assert(u.ChildCount() == 1);
u = u.Child(0);
}
return u;
}
/// <summary>
/// Remove unnecessary atomic nodes, and make appropriate descendents of the atomic node themselves atomic.
/// </summary>
/// <remarks>
/// e.g. (?>(?>(?>a*))) => (?>a*)
/// e.g. (?>(abc*)*) => (?>(abc(?>c*))*)
/// </remarks>
private RegexNode ReduceAtomic()
{
// RegexOptions.NonBacktracking doesn't support atomic groups, so when that option
// is set we don't want to create atomic groups where they weren't explicitly authored.
if ((Options & RegexOptions.NonBacktracking) != 0)
{
return this;
}
Debug.Assert(Type == Atomic);
Debug.Assert(ChildCount() == 1);
RegexNode atomic = this;
RegexNode child = Child(0);
while (child.Type == Atomic)
{
atomic = child;
child = atomic.Child(0);
}
switch (child.Type)
{
// If the child is already atomic, we can just remove the atomic node.
case Oneloopatomic:
case Notoneloopatomic:
case Setloopatomic:
return child;
// If an atomic subexpression contains only a {one/notone/set}{loop/lazy},
// change it to be an {one/notone/set}loopatomic and remove the atomic node.
case Oneloop:
case Notoneloop:
case Setloop:
case Onelazy:
case Notonelazy:
case Setlazy:
child.MakeLoopAtomic();
return child;
// Alternations have a variety of possible optimizations that can be applied
// iff they're atomic.
case Alternate:
if ((Options & RegexOptions.RightToLeft) == 0)
{
List<RegexNode>? branches = child.Children as List<RegexNode>;
Debug.Assert(branches is not null && branches.Count != 0);
// If an alternation is atomic and its first branch is Empty, the whole thing
// is a nop, as Empty will match everything trivially, and no backtracking
// into the node will be performed, making the remaining branches irrelevant.
if (branches[0].Type == Empty)
{
return new RegexNode(Empty, child.Options);
}
// Similarly, we can trim off any branches after an Empty, as they'll never be used.
// An Empty will match anything, and thus branches after that would only be used
// if we backtracked into it and advanced passed the Empty after trying the Empty...
// but if the alternation is atomic, such backtracking won't happen.
for (int i = 1; i < branches.Count - 1; i++)
{
if (branches[i].Type == Empty)
{
branches.RemoveRange(i + 1, branches.Count - (i + 1));
break;
}
}
// If an alternation is atomic, we won't ever backtrack back into it, which
// means order matters but not repetition. With backtracking, it would be incorrect
// to convert an expression like "hi|there|hello" into "hi|hello|there", as doing
// so could then change the order of results if we matched "hi" and then failed
// based on what came after it, and both "hello" and "there" could be successful
// with what came later. But without backtracking, we can reorder "hi|there|hello"
// to instead be "hi|hello|there", as "hello" and "there" can't match the same text,
// and once this atomic alternation has matched, we won't try another branch. This
// reordering is valuable as it then enables further optimizations, e.g.
// "hi|there|hello" => "hi|hello|there" => "h(?:i|ello)|there", which means we only
// need to check the 'h' once in case it's not an 'h', and it's easier to employ different
// code gen that, for example, switches on first character of the branches, enabling faster
// choice of branch without always having to walk through each.
bool reordered = false;
for (int start = 0; start < branches.Count; start++)
{
// Get the node that may start our range. If it's a one, multi, or concat of those, proceed.
RegexNode startNode = branches[start];
if (startNode.FindBranchOneOrMultiStart() is null)
{
continue;
}
// Find the contiguous range of nodes from this point that are similarly one, multi, or concat of those.
int endExclusive = start + 1;
while (endExclusive < branches.Count && branches[endExclusive].FindBranchOneOrMultiStart() is not null)
{
endExclusive++;
}
// If there's at least 3, there may be something to reorder (we won't reorder anything
// before the starting position, and so only 2 items is considered ordered).
if (endExclusive - start >= 3)
{
int compare = start;
while (compare < endExclusive)
{
// Get the starting character
char c = branches[compare].FindBranchOneOrMultiStart()!.FirstCharOfOneOrMulti();
// Move compare to point to the last branch that has the same starting value.
while (compare < endExclusive && branches[compare].FindBranchOneOrMultiStart()!.FirstCharOfOneOrMulti() == c)
{
compare++;
}
// Compare now points to the first node that doesn't match the starting node.
// If we've walked off our range, there's nothing left to reorder.
if (compare < endExclusive)
{
// There may be something to reorder. See if there are any other nodes that begin with the same character.
for (int next = compare + 1; next < endExclusive; next++)
{
RegexNode nextChild = branches[next];
if (nextChild.FindBranchOneOrMultiStart()!.FirstCharOfOneOrMulti() == c)
{
branches.RemoveAt(next);
branches.Insert(compare++, nextChild);
reordered = true;
}
}
}
}
}
// Move to the end of the range we've now explored. endExclusive is not a viable
// starting position either, and the start++ for the loop will thus take us to
// the next potential place to start a range.
start = endExclusive;
}
// If anything we reordered, there may be new optimization opportunities inside
// of the alternation, so reduce it again.
if (reordered)
{
atomic.ReplaceChild(0, child);
child = atomic.Child(0);
}
}
goto default;
// For everything else, try to reduce ending backtracking of the last contained expression.
default:
child.EliminateEndingBacktracking();
return atomic;
}
}
/// <summary>Combine nested loops where applicable.</summary>
/// <remarks>
/// Nested repeaters just get multiplied with each other if they're not too lumpy.
/// Other optimizations may have also resulted in {Lazy}loops directly containing
/// sets, ones, and notones, in which case they can be transformed into the corresponding
/// individual looping constructs.
/// </remarks>
private RegexNode ReduceLoops()
{
Debug.Assert(Type == Loop || Type == Lazyloop);
RegexNode u = this;
int type = Type;
int min = M;
int max = N;
while (u.ChildCount() > 0)
{
RegexNode child = u.Child(0);
// multiply reps of the same type only
if (child.Type != type)
{
bool valid = false;
if (type == Loop)
{
switch (child.Type)
{
case Oneloop:
case Oneloopatomic:
case Notoneloop:
case Notoneloopatomic:
case Setloop:
case Setloopatomic:
valid = true;
break;
}
}
else // type == Lazyloop
{
switch (child.Type)
{
case Onelazy:
case Notonelazy:
case Setlazy:
valid = true;
break;
}
}
if (!valid)
{
break;
}
}
// child can be too lumpy to blur, e.g., (a {100,105}) {3} or (a {2,})?
// [but things like (a {2,})+ are not too lumpy...]
if (u.M == 0 && child.M > 1 || child.N < child.M * 2)
{
break;
}
u = child;
if (u.M > 0)
{
u.M = min = ((int.MaxValue - 1) / u.M < min) ? int.MaxValue : u.M * min;
}
if (u.N > 0)
{
u.N = max = ((int.MaxValue - 1) / u.N < max) ? int.MaxValue : u.N * max;
}
}
if (min == int.MaxValue)
{
return new RegexNode(Nothing, Options);
}
// If the Loop or Lazyloop now only has one child node and its a Set, One, or Notone,
// reduce to just Setloop/lazy, Oneloop/lazy, or Notoneloop/lazy. The parser will
// generally have only produced the latter, but other reductions could have exposed
// this.
if (u.ChildCount() == 1)
{
RegexNode child = u.Child(0);
switch (child.Type)
{
case One:
case Notone:
case Set:
child.MakeRep(u.Type == Lazyloop ? Onelazy : Oneloop, u.M, u.N);
u = child;
break;
}
}
return u;
}
/// <summary>
/// Reduces set-related nodes to simpler one-related and notone-related nodes, where applicable.
/// </summary>
/// <remarks>
/// e.g.
/// [a] => a
/// [a]* => a*
/// [a]*? => a*?
/// (?>[a]*) => (?>a*)
/// [^a] => ^a
/// []* => Nothing
/// </remarks>
private RegexNode ReduceSet()
{
// Extract empty-set, one, and not-one case as special
Debug.Assert(Type == Set || Type == Setloop || Type == Setloopatomic || Type == Setlazy);
Debug.Assert(!string.IsNullOrEmpty(Str));
if (RegexCharClass.IsEmpty(Str))
{
Type = Nothing;
Str = null;
}
else if (RegexCharClass.IsSingleton(Str))
{
Ch = RegexCharClass.SingletonChar(Str);
Str = null;
Type =
Type == Set ? One :
Type == Setloop ? Oneloop :
Type == Setloopatomic ? Oneloopatomic :
Onelazy;
}
else if (RegexCharClass.IsSingletonInverse(Str))
{
Ch = RegexCharClass.SingletonChar(Str);
Str = null;
Type =
Type == Set ? Notone :
Type == Setloop ? Notoneloop :
Type == Setloopatomic ? Notoneloopatomic :
Notonelazy;
}
return this;
}
/// <summary>Optimize an alternation.</summary>
private RegexNode ReduceAlternation()
{
Debug.Assert(Type == Alternate);
switch (ChildCount())
{
case 0:
return new RegexNode(Nothing, Options);
case 1:
return Child(0);
default:
ReduceSingleLetterAndNestedAlternations();
RegexNode node = ReplaceNodeIfUnnecessary(Nothing);
node = ExtractCommonPrefixText(node);
node = ExtractCommonPrefixOneNotoneSet(node);
return node;
}
// This function performs two optimizations:
// - Single-letter alternations can be replaced by faster set specifications
// e.g. "a|b|c|def|g|h" -> "[a-c]|def|[gh]"
// - Nested alternations with no intervening operators can be flattened:
// e.g. "apple|(?:orange|pear)|grape" -> "apple|orange|pear|grape"
void ReduceSingleLetterAndNestedAlternations()
{
bool wasLastSet = false;
bool lastNodeCannotMerge = false;
RegexOptions optionsLast = 0;
RegexOptions optionsAt;
int i;
int j;
RegexNode at;
RegexNode prev;
List<RegexNode> children = (List<RegexNode>)Children!;
for (i = 0, j = 0; i < children.Count; i++, j++)
{
at = children[i];
if (j < i)
children[j] = at;
while (true)
{
if (at.Type == Alternate)
{
if (at.Children is List<RegexNode> atChildren)
{
for (int k = 0; k < atChildren.Count; k++)
{
atChildren[k].Next = this;
}
children.InsertRange(i + 1, atChildren);
}
else
{
RegexNode atChild = (RegexNode)at.Children!;
atChild.Next = this;
children.Insert(i + 1, atChild);
}
j--;
}
else if (at.Type == Set || at.Type == One)
{
// Cannot merge sets if L or I options differ, or if either are negated.