nomen-lang 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,12 @@
2
2
  //
3
3
  // `test` reports whether a pattern matches anywhere in the input; `match`
4
4
  // returns the first matched substring (or "" when there is no match).
5
- // Patterns use POSIX Extended Regular Expression syntax.
5
+ // Patterns use POSIX Extended Regular Expression syntax, extended with
6
+ // non-capturing groups `(?:...)`, lookaround (`(?=...)` / `(?!...)` /
7
+ // `(?<=...)` / `(?<!...)`), and counted repetition (`{n}` / `{n,}` / `{n,m}`).
6
8
 
7
9
  /**
8
- * Regular-expression helpers (`test`/`match`/`count`/`replace_all`) using POSIX ERE syntax
10
+ * Regular-expression helpers (`test`/`match`/`count`/`replace_all`) using POSIX ERE syntax plus `(?:...)`, lookaround, and `{n,m}` counted repetition
9
11
  **/
10
12
  pub struct Regex {
11
13
  // Test whether pattern matches anywhere in input.
@@ -255,12 +257,13 @@ func fold_pattern = (string pattern, move out string) {
255
257
  while i < n; i += 1 {
256
258
  var char c = pattern.at(i)
257
259
  if (c as int) == 92 {
258
- // Escaped char: shorthands and backrefs keep their backslash;
259
- // other escaped letters become both-case classes; escaped
260
- // punctuation is copied literally.
260
+ // Escaped char: shorthands, backrefs, and control escapes
261
+ // (\n \r \t) keep their backslash; other escaped letters
262
+ // become both-case classes; escaped punctuation is copied
263
+ // literally.
261
264
  if i + 1 < n {
262
265
  var char e = pattern.at(i + 1)
263
- if is_shorthand(e) || is_backref_digit(e) {
266
+ if is_shorthand(e) || is_backref_digit(e) || is_control_escape(e) {
264
267
  sb.append_char(c)
265
268
  sb.append_char(e)
266
269
  } else {
@@ -317,9 +320,10 @@ func fold_class = (string pattern, int cs, int ce, ref StringBuilder dst) {
317
320
  if i + 1 < content_end {
318
321
  var char e = pattern.at(i + 1)
319
322
  dst.append_char(e)
320
- // A shorthand keeps its backslash; other escaped letters
321
- // gain their case counterpart.
322
- if is_alpha_byte(e) && !is_shorthand(e) {
323
+ // A shorthand or control escape (\n \r \t) keeps its
324
+ // backslash; other escaped letters gain their case
325
+ // counterpart.
326
+ if is_alpha_byte(e) && !is_shorthand(e) && !is_control_escape(e) {
323
327
  dst.append_char(swap_case_byte(e))
324
328
  }
325
329
  i += 1
@@ -478,9 +482,11 @@ func alt_first_bytes = (string pattern, int start, int end, ref StringBuilder sb
478
482
  while i < end {
479
483
  var char c = pattern.at(i)
480
484
  if (c as int) == 92 {
481
- // escaped literal: first byte is the escaped char — unless it's
482
- // a backreference (depends on a capture) or a complement
483
- // shorthand (not an inclusion set), which disable the prefilter.
485
+ // escaped literal: first byte is the byte the escape names
486
+ // (\n is LF, \r is CR, \t is TAB; any other escaped character
487
+ // is its own literal byte) — unless it's a backreference
488
+ // (depends on a capture) or a complement shorthand (not an
489
+ // inclusion set), which disable the prefilter.
484
490
  if i + 1 < end {
485
491
  var char e = pattern.at(i + 1)
486
492
  if is_backref_digit(e) {
@@ -491,7 +497,7 @@ func alt_first_bytes = (string pattern, int start, int end, ref StringBuilder sb
491
497
  return -1
492
498
  }
493
499
  } else {
494
- sb.append_char(e)
500
+ sb.append_char(escaped_byte_value(e) as char)
495
501
  }
496
502
  // A '?'/'*' quantifier makes the atom nullable — the first
497
503
  // byte can also come from whatever follows it.
@@ -526,9 +532,17 @@ func alt_first_bytes = (string pattern, int start, int end, ref StringBuilder sb
526
532
  }
527
533
  if c == '(' {
528
534
  var int a_end = atom_end_pos(pattern, i, end)
535
+ var int kind = group_kind(pattern, i, end)
536
+ if kind >= 2 {
537
+ // Lookaround: zero-width — contributes no byte, so the first
538
+ // byte comes from whatever follows.
539
+ i = a_end
540
+ continue
541
+ }
529
542
  // The group's first bytes are the union of its content's
530
543
  // top-level alternatives' first bytes.
531
- if collect_first_bytes(pattern, i + 1, a_end - 1, ref sb) < 0 {
544
+ var int inner = group_inner_start(pattern, i, end)
545
+ if collect_first_bytes(pattern, inner, a_end - 1, ref sb) < 0 {
532
546
  return -1
533
547
  }
534
548
  var int cont = nullable_quant_after(pattern, a_end, end)
@@ -583,13 +597,128 @@ func nullable_quant_after = (string pattern, int after, int end, out int) {
583
597
  }
584
598
  return j
585
599
  }
600
+ if q == '{' {
601
+ // A counted bound is nullable only when its minimum is 0 ({0}, {0,},
602
+ // {0,m}); the first byte then also comes from what follows.
603
+ var int qe = counted_quant_end(pattern, j, end)
604
+ if qe >= 0 {
605
+ var int qmin = counted_quant_min(pattern, j)
606
+ if qmin == 0 {
607
+ var int k = qe
608
+ if k < end {
609
+ if pattern.at(k) == '?' {
610
+ k += 1
611
+ }
612
+ }
613
+ return k
614
+ }
615
+ return -1
616
+ }
617
+ }
586
618
  return -1
587
619
  }
588
620
 
621
+ // Parse a counted repetition bound `{n}`, `{n,}`, or `{n,m}` whose '{' is at
622
+ // pos. Returns the position just past the closing '}', or -1 when the braces
623
+ // are not a valid bound (the '{' is then a literal character).
624
+ func counted_quant_end = (string pattern, int pos, int pat_end, out int) {
625
+ var int i = pos + 1
626
+ var int digits = i
627
+ while i < pat_end {
628
+ if is_digit_byte(pattern.at(i)) {
629
+ i += 1
630
+ } else {
631
+ break
632
+ }
633
+ }
634
+ if i == digits {
635
+ return -1
636
+ }
637
+ if i < pat_end {
638
+ if pattern.at(i) == ',' {
639
+ i += 1
640
+ while i < pat_end {
641
+ if is_digit_byte(pattern.at(i)) {
642
+ i += 1
643
+ } else {
644
+ break
645
+ }
646
+ }
647
+ }
648
+ }
649
+ if i < pat_end {
650
+ if pattern.at(i) == '}' {
651
+ return i + 1
652
+ }
653
+ }
654
+ return -1
655
+ }
656
+
657
+ // The minimum repetition count of a `{...}` bound at pos (assumes a valid
658
+ // bound was found by counted_quant_end).
659
+ func counted_quant_min = (string pattern, int pos, out int) {
660
+ var int i = pos + 1
661
+ var value = 0
662
+ while i < pattern.length {
663
+ if is_digit_byte(pattern.at(i)) {
664
+ value = value * 10 + ((pattern.at(i) as int) - 48)
665
+ i += 1
666
+ } else {
667
+ break
668
+ }
669
+ }
670
+ return value
671
+ }
672
+
673
+ // The maximum repetition count of a `{...}` bound at pos: the value after the
674
+ // comma for `{n,m}`, -1 for the unbounded `{n,}`, and the minimum for `{n}`.
675
+ func counted_quant_max = (string pattern, int pos, int pat_end, out int) {
676
+ var int i = pos + 1
677
+ while i < pat_end {
678
+ if is_digit_byte(pattern.at(i)) {
679
+ i += 1
680
+ } else {
681
+ break
682
+ }
683
+ }
684
+ if i >= pat_end {
685
+ return counted_quant_min(pattern, pos)
686
+ }
687
+ if pattern.at(i) != ',' {
688
+ return counted_quant_min(pattern, pos)
689
+ }
690
+ var int j = i + 1
691
+ var value = 0
692
+ var int digits = j
693
+ while j < pat_end {
694
+ if is_digit_byte(pattern.at(j)) {
695
+ value = value * 10 + ((pattern.at(j) as int) - 48)
696
+ j += 1
697
+ } else {
698
+ break
699
+ }
700
+ }
701
+ if j == digits {
702
+ // `{n,}` — unbounded maximum.
703
+ return -1
704
+ }
705
+ return value
706
+ }
707
+
708
+ // Whether a pattern character is an ASCII digit.
709
+ func is_digit_byte = (char c, out bool) {
710
+ var int code = c as int
711
+ return code >= 48 && code <= 57
712
+ }
713
+
589
714
  // Append the members of character class [start..end) to sb. Ranges (a-z) are
590
715
  // expanded; negation (^) is ignored (we want all candidates).
591
- func add_class_chars = (string pattern, int start, int end, ref StringBuilder sb) {
592
- var int cs = start
716
+ func add_class_chars = (
717
+ string pattern,
718
+ int start,
719
+ int end,
720
+ ref StringBuilder sb
721
+ ) { var int cs = start
593
722
  if cs < end {
594
723
  if pattern.at(cs) == '^' {
595
724
  cs += 1
@@ -597,22 +726,34 @@ func add_class_chars = (string pattern, int start, int end, ref StringBuilder sb
597
726
  }
598
727
  var int i = cs
599
728
  while i < end {
600
- if i + 2 < end {
601
- if pattern.at(i + 1) == '-' {
602
- var int lo = pattern.at(i) as int
603
- var int hi = pattern.at(i + 2) as int
604
- while lo <= hi {
605
- sb.append_char(lo as char)
606
- lo += 1
729
+ var char c = pattern.at(i)
730
+ if (c as int) == 92 {
731
+ // Escaped byte: contribute the byte the escape names (\n is
732
+ // LF, \r is CR, \t is TAB; any other escaped character is its
733
+ // own literal byte) so the prefilter stays a superset of the
734
+ // matcher's first bytes.
735
+ if i + 1 < end {
736
+ sb.append_char(escaped_byte_value(pattern.at(i + 1)) as char)
737
+ }
738
+ i += 2
739
+ } else {
740
+ if i + 2 < end {
741
+ if pattern.at(i + 1) == '-' {
742
+ var int lo = pattern.at(i) as int
743
+ var int hi = pattern.at(i + 2) as int
744
+ while lo <= hi {
745
+ sb.append_char(lo as char)
746
+ lo += 1
747
+ }
748
+ i += 3
749
+ } else {
750
+ sb.append_char(c)
751
+ i += 1
607
752
  }
608
- i += 3
609
753
  } else {
610
- sb.append_char(pattern.at(i))
754
+ sb.append_char(c)
611
755
  i += 1
612
756
  }
613
- } else {
614
- sb.append_char(pattern.at(i))
615
- i += 1
616
757
  }
617
758
  }
618
759
  }
@@ -698,6 +839,28 @@ func match_here = (
698
839
  return match_here(pattern, pat_pos + 1, pat_end, input, in_pos, in_len, ref caps)
699
840
  }
700
841
 
842
+ // Lookaround assertions are zero-width: evaluate the inner pattern and
843
+ // continue matching the rest at the SAME input position.
844
+ if pc == '(' && is_non_capturing_open(pattern, pat_pos, pat_end) {
845
+ var int look_kind = group_kind(pattern, pat_pos, pat_end)
846
+ if look_kind >= 2 {
847
+ var int look_end = atom_end_pos(pattern, pat_pos, pat_end)
848
+ var int look_inner = group_inner_start(pattern, pat_pos, pat_end)
849
+ if lookaround_succeeds(
850
+ look_kind,
851
+ pattern,
852
+ look_inner,
853
+ look_end - 1,
854
+ input,
855
+ in_pos,
856
+ in_len,
857
+ ) == false {
858
+ return -1
859
+ }
860
+ return match_here(pattern, look_end, pat_end, input, in_pos, in_len, ref caps)
861
+ }
862
+ }
863
+
701
864
  // Determine the bounds of the current atom.
702
865
  var int a_end = atom_end_pos(pattern, pat_pos, pat_end)
703
866
 
@@ -758,9 +921,56 @@ func match_here = (
758
921
  ref caps,
759
922
  )
760
923
  }
924
+ if q == '{' {
925
+ // Counted repetition: {n}, {n,}, {n,m}, optionally lazy ({..}?).
926
+ var int qe = counted_quant_end(pattern, a_end, pat_end)
927
+ if qe >= 0 {
928
+ var int qmin = counted_quant_min(pattern, a_end)
929
+ var int qmax = counted_quant_max(pattern, a_end, pat_end)
930
+ var counted_lazy = 0
931
+ var int counted_rest = qe
932
+ if counted_rest < pat_end {
933
+ if pattern.at(counted_rest) == '?' {
934
+ counted_lazy = 1
935
+ counted_rest += 1
936
+ }
937
+ }
938
+ return match_counted(
939
+ pattern,
940
+ pat_pos,
941
+ a_end,
942
+ counted_rest,
943
+ pat_end,
944
+ input,
945
+ in_pos,
946
+ qmin,
947
+ qmax,
948
+ counted_lazy,
949
+ 0,
950
+ in_len,
951
+ ref caps,
952
+ )
953
+ }
954
+ }
761
955
  }
762
956
 
763
- // No quantifier: match the atom once, then the rest of the pattern.
957
+ // No quantifier: match the atom once, then the rest of the pattern. A
958
+ // GROUP atom goes through the continuation-aware path: its inner
959
+ // alternatives retry when the rest fails on an earlier one (JS-style
960
+ // backtracking — `(head|header)(\s|$|>)` must still match "header>").
961
+ if pc == '(' {
962
+ return match_group_then_rest(
963
+ pattern,
964
+ pat_pos,
965
+ a_end,
966
+ a_end,
967
+ pat_end,
968
+ input,
969
+ in_pos,
970
+ in_len,
971
+ ref caps,
972
+ )
973
+ }
764
974
  var int next = match_single_atom(pattern, pat_pos, a_end, input, in_pos, in_len, ref caps)
765
975
  if next < 0 {
766
976
  return -1
@@ -768,6 +978,97 @@ func match_here = (
768
978
  return match_here(pattern, a_end, pat_end, input, next, in_len, ref caps)
769
979
  }
770
980
 
981
+ // The position of the first top-level '|' in [start, end), or -1. Escapes,
982
+ // character classes, and nested groups are skipped so only depth-0
983
+ // alternation operators count (same scanning discipline as match_here).
984
+ func next_top_level_alt = (string pattern, int start, int end, out int) {
985
+ var depth = 0
986
+ var int i = start
987
+ while i < end {
988
+ var char c = pattern.at(i)
989
+ if (c as int) == 92 {
990
+ i += 2
991
+ } else {
992
+ if c == '[' {
993
+ i = atom_end_pos(pattern, i, end)
994
+ } else {
995
+ if c == '(' {
996
+ depth += 1
997
+ }
998
+ if c == ')' {
999
+ if depth > 0 {
1000
+ depth -= 1
1001
+ }
1002
+ }
1003
+ if (c as int) == 124 {
1004
+ if depth == 0 {
1005
+ return i
1006
+ }
1007
+ }
1008
+ i += 1
1009
+ }
1010
+ }
1011
+ }
1012
+ return -1
1013
+ }
1014
+
1015
+ // Match the group atom spanning [atom_start, atom_end) ('(' at atom_start)
1016
+ // and then the continuation [rest, pat_end), retrying the group's LATER
1017
+ // alternatives whenever the continuation fails on an earlier one. Without
1018
+ // this, a group commits to the first alternative that matches the group
1019
+ // ALONE and a failing continuation aborts the whole match — the engine had
1020
+ // no backtracking into a group's alternatives (PORT.md: `(head|header)`
1021
+ // never reached `header` when `(\s|$|>)` rejected what `head` left behind).
1022
+ // A group without a top-level '|' behaves exactly like the old
1023
+ // match-atom-then-continue path, and a failed trial restores the group's
1024
+ // previous capture span before the next attempt.
1025
+ func match_group_then_rest = (
1026
+ string pattern,
1027
+ int atom_start,
1028
+ int atom_end,
1029
+ int rest,
1030
+ int pat_end,
1031
+ string input,
1032
+ int in_pos,
1033
+ int in_len,
1034
+ ref List<int> caps,
1035
+ out int
1036
+ ) {
1037
+ var int g = group_index_at(pattern, atom_start)
1038
+ var int at = last_capture_index(caps, g)
1039
+ var int old_start = -1
1040
+ var int old_end = -1
1041
+ if at >= 0 {
1042
+ old_start = caps.at(at + 1)
1043
+ old_end = caps.at(at + 2)
1044
+ }
1045
+ var int inner_start = group_inner_start(pattern, atom_start, pat_end)
1046
+ var int inner_end = atom_end - 1
1047
+ var int s = inner_start
1048
+ while s <= inner_end {
1049
+ var int bar = next_top_level_alt(pattern, s, inner_end)
1050
+ var int e = inner_end
1051
+ if bar >= 0 {
1052
+ e = bar
1053
+ }
1054
+ var int next = match_here(pattern, s, e, input, in_pos, in_len, ref caps)
1055
+ if next >= 0 {
1056
+ record_capture(ref caps, g, in_pos, next)
1057
+ var int r = match_here(pattern, rest, pat_end, input, next, in_len, ref caps)
1058
+ if r >= 0 {
1059
+ return r
1060
+ }
1061
+ }
1062
+ record_capture(ref caps, g, old_start, old_end)
1063
+ if bar >= 0 {
1064
+ s = bar + 1
1065
+ } else {
1066
+ s = inner_end + 1
1067
+ }
1068
+ }
1069
+ return -1
1070
+ }
1071
+
771
1072
  // Return the position just past the atom that starts at pat_pos. Handles
772
1073
  // character classes [...], groups (...), and escaped characters (\x).
773
1074
  func atom_end_pos = (string pattern, int pat_pos, int pat_end, out int) {
@@ -857,7 +1158,7 @@ func match_single_atom = (
857
1158
  }
858
1159
  var int next = match_here(
859
1160
  pattern,
860
- atom_start + 1,
1161
+ group_inner_start(pattern, atom_start, atom_end),
861
1162
  atom_end - 1,
862
1163
  input,
863
1164
  in_pos,
@@ -888,7 +1189,7 @@ func match_single_atom = (
888
1189
  return -1
889
1190
  }
890
1191
  if in_pos < in_len {
891
- if input.at(in_pos) == ec {
1192
+ if (input.at(in_pos) as int) == escaped_byte_value(ec) {
892
1193
  return in_pos + 1
893
1194
  }
894
1195
  }
@@ -936,13 +1237,156 @@ func char_matches_atom = (
936
1237
  if is_shorthand(ec) {
937
1238
  return matches_shorthand(ec, ic)
938
1239
  }
939
- return ic == ec
1240
+ return (ic as int) == escaped_byte_value(ec)
940
1241
  }
941
1242
  return false
942
1243
  }
943
1244
  return pc == ic
944
1245
  }
945
1246
 
1247
+ // Evaluate a lookaround assertion at in_pos:
1248
+ // 2 (?=inner) succeeds when inner matches starting here
1249
+ // 3 (?!inner) succeeds when inner does NOT match starting here
1250
+ // 4 (?<=inner) succeeds when inner matches ending exactly here
1251
+ // 5 (?<!inner) succeeds when no such match ends here
1252
+ // Captures inside lookarounds are not recorded (a throwaway capture log is
1253
+ // used) — the supported patterns are capture-free assertions.
1254
+ func lookaround_succeeds = (
1255
+ int kind,
1256
+ string pattern,
1257
+ int inner,
1258
+ int inner_end,
1259
+ string input,
1260
+ int in_pos,
1261
+ int in_len,
1262
+ out bool
1263
+ ) {
1264
+ if kind == 2 {
1265
+ var trial = List<int>()
1266
+ return match_here(pattern, inner, inner_end, input, in_pos, in_len, ref trial) >= 0
1267
+ }
1268
+ if kind == 3 {
1269
+ var trial = List<int>()
1270
+ return match_here(pattern, inner, inner_end, input, in_pos, in_len, ref trial) < 0
1271
+ }
1272
+ var found = false
1273
+ var s = 0
1274
+ while s <= in_pos {
1275
+ var trial = List<int>()
1276
+ var int e = match_here(pattern, inner, inner_end, input, s, in_len, ref trial)
1277
+ if e == in_pos {
1278
+ found = true
1279
+ break
1280
+ }
1281
+ s += 1
1282
+ }
1283
+ if kind == 4 {
1284
+ return found
1285
+ }
1286
+ return found == false
1287
+ }
1288
+
1289
+ // Match the atom [atom_start..atom_end) between min_count and max_count times
1290
+ // (max_count -1 means unbounded), then the continuation [rest..pat_end). This
1291
+ // backs counted repetition `{n}` / `{n,}` / `{n,m}`; greedy tries one more
1292
+ // iteration before stopping, lazy stops first. An atom that matches the empty
1293
+ // string is not repeated further (an empty iteration cannot make progress).
1294
+ func match_counted = (
1295
+ string pattern,
1296
+ int atom_start,
1297
+ int atom_end,
1298
+ int rest,
1299
+ int pat_end,
1300
+ string input,
1301
+ int in_pos,
1302
+ int min_count,
1303
+ int max_count,
1304
+ int lazy,
1305
+ int count,
1306
+ int in_len,
1307
+ ref List<int> caps,
1308
+ out int
1309
+ ) {
1310
+ var can_stop = count >= min_count
1311
+ var can_more = max_count < 0 || count < max_count
1312
+ if lazy > 0 {
1313
+ if can_stop {
1314
+ var int stop = match_here(pattern, rest, pat_end, input, in_pos, in_len, ref caps)
1315
+ if stop >= 0 {
1316
+ return stop
1317
+ }
1318
+ }
1319
+ if can_more {
1320
+ var int next = match_single_atom(
1321
+ pattern,
1322
+ atom_start,
1323
+ atom_end,
1324
+ input,
1325
+ in_pos,
1326
+ in_len,
1327
+ ref caps,
1328
+ )
1329
+ if next >= 0 {
1330
+ if next != in_pos {
1331
+ return match_counted(
1332
+ pattern,
1333
+ atom_start,
1334
+ atom_end,
1335
+ rest,
1336
+ pat_end,
1337
+ input,
1338
+ next,
1339
+ min_count,
1340
+ max_count,
1341
+ lazy,
1342
+ count + 1,
1343
+ in_len,
1344
+ ref caps,
1345
+ )
1346
+ }
1347
+ }
1348
+ }
1349
+ return -1
1350
+ }
1351
+ if can_more {
1352
+ var int next = match_single_atom(
1353
+ pattern,
1354
+ atom_start,
1355
+ atom_end,
1356
+ input,
1357
+ in_pos,
1358
+ in_len,
1359
+ ref caps,
1360
+ )
1361
+ if next >= 0 {
1362
+ if next != in_pos {
1363
+ var int r = match_counted(
1364
+ pattern,
1365
+ atom_start,
1366
+ atom_end,
1367
+ rest,
1368
+ pat_end,
1369
+ input,
1370
+ next,
1371
+ min_count,
1372
+ max_count,
1373
+ lazy,
1374
+ count + 1,
1375
+ in_len,
1376
+ ref caps,
1377
+ )
1378
+ if r >= 0 {
1379
+ return r
1380
+ }
1381
+ }
1382
+ }
1383
+ }
1384
+ if can_stop {
1385
+ return match_here(pattern, rest, pat_end, input, in_pos, in_len, ref caps)
1386
+ }
1387
+ return -1
1388
+ }
1389
+
946
1390
  // Match the atom [atom_start..atom_end) followed by the rest of the pattern
947
1391
  // pattern[rest..pat_end). min_count is 0 for '*' and 1 for '+'; lazy picks
948
1392
  // the shortest repetition first instead of the longest. Single-character
@@ -1046,29 +1490,99 @@ func match_star_group = (
1046
1490
  ) {
1047
1491
  if lazy > 0 {
1048
1492
  // Lazy: stop repeating first (minimum met), then try one more
1049
- // iteration before giving up.
1493
+ // iteration before giving up. The iteration retries the group's
1494
+ // top-level alternatives (see match_group_iteration).
1050
1495
  if min_count <= 0 {
1051
1496
  var int stop = match_here(pattern, rest, pat_end, input, in_pos, in_len, ref caps)
1052
1497
  if stop >= 0 {
1053
1498
  return stop
1054
1499
  }
1055
1500
  }
1056
- var int next = match_single_atom(
1501
+ return match_group_iteration(
1057
1502
  pattern,
1058
1503
  atom_start,
1059
1504
  atom_end,
1505
+ rest,
1506
+ pat_end,
1060
1507
  input,
1061
1508
  in_pos,
1509
+ min_count,
1510
+ lazy,
1062
1511
  in_len,
1063
1512
  ref caps,
1064
1513
  )
1514
+ }
1515
+ // Try matching the group one more time (greedy) — each top-level
1516
+ // alternative in turn — before stopping.
1517
+ var int r = match_group_iteration(
1518
+ pattern,
1519
+ atom_start,
1520
+ atom_end,
1521
+ rest,
1522
+ pat_end,
1523
+ input,
1524
+ in_pos,
1525
+ min_count,
1526
+ lazy,
1527
+ in_len,
1528
+ ref caps,
1529
+ )
1530
+ if r >= 0 {
1531
+ return r
1532
+ }
1533
+ // Stop repeating and try the rest of the pattern, if the minimum is met.
1534
+ if min_count <= 0 {
1535
+ return match_here(pattern, rest, pat_end, input, in_pos, in_len, ref caps)
1536
+ }
1537
+ return -1
1538
+ }
1539
+
1540
+ // Try ONE further iteration of a quantified group at in_pos, retrying the
1541
+ // group's top-level alternatives while the remaining star recursion fails.
1542
+ // Returns the recursion's result, or -1 when no alternative leads to a
1543
+ // match. An alternative that matches the empty string is not recursed into
1544
+ // (an empty iteration cannot make progress; the caller's stop path covers
1545
+ // it), and every failed trial restores the group's previous capture span.
1546
+ func match_group_iteration = (
1547
+ string pattern,
1548
+ int atom_start,
1549
+ int atom_end,
1550
+ int rest,
1551
+ int pat_end,
1552
+ string input,
1553
+ int in_pos,
1554
+ int min_count,
1555
+ int lazy,
1556
+ int in_len,
1557
+ ref List<int> caps,
1558
+ out int
1559
+ ) {
1560
+ var int g = group_index_at(pattern, atom_start)
1561
+ var int at = last_capture_index(caps, g)
1562
+ var int old_start = -1
1563
+ var int old_end = -1
1564
+ if at >= 0 {
1565
+ old_start = caps.at(at + 1)
1566
+ old_end = caps.at(at + 2)
1567
+ }
1568
+ var int inner_start = group_inner_start(pattern, atom_start, pat_end)
1569
+ var int inner_end = atom_end - 1
1570
+ var int s = inner_start
1571
+ while s <= inner_end {
1572
+ var int bar = next_top_level_alt(pattern, s, inner_end)
1573
+ var int e = inner_end
1574
+ if bar >= 0 {
1575
+ e = bar
1576
+ }
1577
+ var int next = match_here(pattern, s, e, input, in_pos, in_len, ref caps)
1065
1578
  if next >= 0 {
1579
+ record_capture(ref caps, g, in_pos, next)
1066
1580
  if next != in_pos {
1067
1581
  var new_min = 0
1068
1582
  if min_count > 0 {
1069
1583
  new_min = min_count - 1
1070
1584
  }
1071
- return match_star_group(
1585
+ var int r = match_star_group(
1072
1586
  pattern,
1073
1587
  atom_start,
1074
1588
  atom_end,
@@ -1081,40 +1595,18 @@ func match_star_group = (
1081
1595
  in_len,
1082
1596
  ref caps,
1083
1597
  )
1598
+ if r >= 0 {
1599
+ return r
1600
+ }
1084
1601
  }
1085
1602
  }
1086
- return -1
1087
- }
1088
- // Try matching the group one more time (greedy).
1089
- var int next = match_single_atom(pattern, atom_start, atom_end, input, in_pos, in_len, ref caps)
1090
- if next >= 0 {
1091
- if next != in_pos {
1092
- var new_min = 0
1093
- if min_count > 0 {
1094
- new_min = min_count - 1
1095
- }
1096
- var int r = match_star_group(
1097
- pattern,
1098
- atom_start,
1099
- atom_end,
1100
- rest,
1101
- pat_end,
1102
- input,
1103
- next,
1104
- new_min,
1105
- lazy,
1106
- in_len,
1107
- ref caps,
1108
- )
1109
- if r >= 0 {
1110
- return r
1111
- }
1603
+ record_capture(ref caps, g, old_start, old_end)
1604
+ if bar >= 0 {
1605
+ s = bar + 1
1606
+ } else {
1607
+ s = inner_end + 1
1112
1608
  }
1113
1609
  }
1114
- // Stop repeating and try the rest of the pattern, if the minimum is met.
1115
- if min_count <= 0 {
1116
- return match_here(pattern, rest, pat_end, input, in_pos, in_len, ref caps)
1117
- }
1118
1610
  return -1
1119
1611
  }
1120
1612
 
@@ -1138,6 +1630,21 @@ func match_optional = (
1138
1630
  if skip >= 0 {
1139
1631
  return skip
1140
1632
  }
1633
+ // A group atom includes continuation-aware alternative retries (see
1634
+ // match_group_then_rest) in both moods.
1635
+ if pattern.at(atom_start) == '(' {
1636
+ return match_group_then_rest(
1637
+ pattern,
1638
+ atom_start,
1639
+ atom_end,
1640
+ rest,
1641
+ pat_end,
1642
+ input,
1643
+ in_pos,
1644
+ in_len,
1645
+ ref caps,
1646
+ )
1647
+ }
1141
1648
  var int next = match_single_atom(
1142
1649
  pattern,
1143
1650
  atom_start,
@@ -1152,11 +1659,36 @@ func match_optional = (
1152
1659
  }
1153
1660
  return -1
1154
1661
  }
1155
- var int next = match_single_atom(pattern, atom_start, atom_end, input, in_pos, in_len, ref caps)
1156
- if next >= 0 {
1157
- var int r = match_here(pattern, rest, pat_end, input, next, in_len, ref caps)
1158
- if r >= 0 {
1159
- return r
1662
+ if pattern.at(atom_start) == '(' {
1663
+ var int grouped = match_group_then_rest(
1664
+ pattern,
1665
+ atom_start,
1666
+ atom_end,
1667
+ rest,
1668
+ pat_end,
1669
+ input,
1670
+ in_pos,
1671
+ in_len,
1672
+ ref caps,
1673
+ )
1674
+ if grouped >= 0 {
1675
+ return grouped
1676
+ }
1677
+ } else {
1678
+ var int next = match_single_atom(
1679
+ pattern,
1680
+ atom_start,
1681
+ atom_end,
1682
+ input,
1683
+ in_pos,
1684
+ in_len,
1685
+ ref caps,
1686
+ )
1687
+ if next >= 0 {
1688
+ var int r = match_here(pattern, rest, pat_end, input, next, in_len, ref caps)
1689
+ if r >= 0 {
1690
+ return r
1691
+ }
1160
1692
  }
1161
1693
  }
1162
1694
  return match_here(pattern, rest, pat_end, input, in_pos, in_len, ref caps)
@@ -1201,7 +1733,7 @@ func char_in_class = (
1201
1733
  found = true
1202
1734
  }
1203
1735
  } else {
1204
- if ic == e {
1736
+ if (ic as int) == escaped_byte_value(e) {
1205
1737
  found = true
1206
1738
  }
1207
1739
  }
@@ -1242,9 +1774,70 @@ func char_in_class = (
1242
1774
  return found
1243
1775
  }
1244
1776
 
1777
+ // Whether the '(' at open_pos opens a non-capturing construct: `(?:` or one
1778
+ // of the lookaround forms `(?=`, `(?!`, `(?<=`, `(?<!`. Every supported
1779
+ // group whose open paren is immediately followed by '?' is non-capturing.
1780
+ func is_non_capturing_open = (string pattern, int open_pos, int pat_end, out bool) {
1781
+ if open_pos + 1 >= pat_end {
1782
+ return false
1783
+ }
1784
+ return pattern.at(open_pos + 1) == '?'
1785
+ }
1786
+
1787
+ // Classify the group opening at open_pos:
1788
+ // 0 capturing group (...)
1789
+ // 1 non-capturing group (?:...)
1790
+ // 2 positive lookahead (?=...)
1791
+ // 3 negative lookahead (?!...)
1792
+ // 4 positive lookbehind (?<=...)
1793
+ // 5 negative lookbehind (?<!...)
1794
+ func group_kind = (string pattern, int open_pos, int pat_end, out int) {
1795
+ if is_non_capturing_open(pattern, open_pos, pat_end) == false {
1796
+ return 0
1797
+ }
1798
+ if open_pos + 2 >= pat_end {
1799
+ return 0
1800
+ }
1801
+ var char c = pattern.at(open_pos + 2)
1802
+ if c == ':' {
1803
+ return 1
1804
+ }
1805
+ if c == '=' {
1806
+ return 2
1807
+ }
1808
+ if c == '!' {
1809
+ return 3
1810
+ }
1811
+ if c == '<' {
1812
+ if open_pos + 3 < pat_end {
1813
+ if pattern.at(open_pos + 3) == '=' {
1814
+ return 4
1815
+ }
1816
+ if pattern.at(open_pos + 3) == '!' {
1817
+ return 5
1818
+ }
1819
+ }
1820
+ }
1821
+ return 0
1822
+ }
1823
+
1824
+ // The position of the first byte INSIDE the group whose '(' opens at
1825
+ // open_pos: past `(?:`, `(?=`, `(?!`, or `(?<=`/`(?<!` for the prefixed
1826
+ // forms, or open_pos + 1 for a plain capturing group.
1827
+ func group_inner_start = (string pattern, int open_pos, int pat_end, out int) {
1828
+ var int kind = group_kind(pattern, open_pos, pat_end)
1829
+ if kind == 1 || kind == 2 || kind == 3 {
1830
+ return open_pos + 3
1831
+ }
1832
+ if kind == 4 || kind == 5 {
1833
+ return open_pos + 4
1834
+ }
1835
+ return open_pos + 1
1836
+ }
1837
+
1245
1838
  // The number of capture groups in the whole pattern: every '(' that is not
1246
- // escaped and not inside a character class (POSIX ERE has no non-capturing
1247
- // groups).
1839
+ // escaped, not inside a character class, and not a non-capturing prefix
1840
+ // (`(?:`, `(?=`, `(?!`, `(?<=`, `(?<!`).
1248
1841
  func capture_count = (string pattern, out int) {
1249
1842
  var int n = pattern.length
1250
1843
  var count = 0
@@ -1258,7 +1851,9 @@ func capture_count = (string pattern, out int) {
1258
1851
  i = atom_end_pos(pattern, i, n) - 1
1259
1852
  } else {
1260
1853
  if c == '(' {
1261
- count += 1
1854
+ if is_non_capturing_open(pattern, i, n) == false {
1855
+ count += 1
1856
+ }
1262
1857
  }
1263
1858
  }
1264
1859
  }
@@ -1267,8 +1862,9 @@ func capture_count = (string pattern, out int) {
1267
1862
  }
1268
1863
 
1269
1864
  // The capture-group index (1-based) of the group whose '(' opens at
1270
- // open_pos: one more than the number of capture opens before it in the
1271
- // pattern (the whole match is group 0).
1865
+ // open_pos: one more than the number of capturing opens before it in the
1866
+ // pattern (the whole match is group 0). Returns 0 for a non-capturing
1867
+ // construct so callers record no span.
1272
1868
  func group_index_at = (string pattern, int open_pos, out int) {
1273
1869
  var count = 0
1274
1870
  var i = 0
@@ -1281,11 +1877,16 @@ func group_index_at = (string pattern, int open_pos, out int) {
1281
1877
  i = atom_end_pos(pattern, i, open_pos) - 1
1282
1878
  } else {
1283
1879
  if c == '(' {
1284
- count += 1
1880
+ if is_non_capturing_open(pattern, i, open_pos) == false {
1881
+ count += 1
1882
+ }
1285
1883
  }
1286
1884
  }
1287
1885
  }
1288
1886
  }
1887
+ if is_non_capturing_open(pattern, open_pos, pattern.length) {
1888
+ return 0
1889
+ }
1289
1890
  return count + 1
1290
1891
  }
1291
1892
 
@@ -1294,6 +1895,9 @@ func group_index_at = (string pattern, int open_pos, out int) {
1294
1895
  // a repeated group reports its last iteration; a failed group records
1295
1896
  // (-1, -1) so stale spans from an abandoned attempt never surface.
1296
1897
  func record_capture = (ref List<int> caps, int group, int start, int end) {
1898
+ if group <= 0 {
1899
+ return
1900
+ }
1297
1901
  var int at = last_capture_index(caps, group)
1298
1902
  if at >= 0 {
1299
1903
  caps.set(at + 1, start)
@@ -1456,6 +2060,29 @@ func is_shorthand = (char c, out bool) {
1456
2060
  return code == 100 || code == 68 || code == 119 || code == 87 || code == 115 || code == 83
1457
2061
  }
1458
2062
 
2063
+ // Whether the escaped character c names a control-character shorthand
2064
+ // (\n \r \t) that the engine maps to its real control byte.
2065
+ func is_control_escape = (char c, out bool) {
2066
+ var int code = c as int
2067
+ return code == 110 || code == 114 || code == 116
2068
+ }
2069
+
2070
+ // The byte an escaped atom names: \n is LF (10), \r is CR (13), \t is TAB
2071
+ // (9); every other escaped character is the literal byte of that character.
2072
+ func escaped_byte_value = (char c, out int) {
2073
+ var int code = c as int
2074
+ if code == 110 {
2075
+ return 10
2076
+ }
2077
+ if code == 114 {
2078
+ return 13
2079
+ }
2080
+ if code == 116 {
2081
+ return 9
2082
+ }
2083
+ return code
2084
+ }
2085
+
1459
2086
  // Whether byte b belongs to the shorthand class named by c. Uppercase names
1460
2087
  // are the complements (S/D/W match anything the lowercase set excludes).
1461
2088
  func matches_shorthand = (char c, char b, out bool) {