nomen-lang 0.6.2 → 0.6.4

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.
@@ -530,9 +532,17 @@ func alt_first_bytes = (string pattern, int start, int end, ref StringBuilder sb
530
532
  }
531
533
  if c == '(' {
532
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
+ }
533
542
  // The group's first bytes are the union of its content's
534
543
  // top-level alternatives' first bytes.
535
- 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 {
536
546
  return -1
537
547
  }
538
548
  var int cont = nullable_quant_after(pattern, a_end, end)
@@ -587,13 +597,128 @@ func nullable_quant_after = (string pattern, int after, int end, out int) {
587
597
  }
588
598
  return j
589
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
+ }
590
618
  return -1
591
619
  }
592
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
+
593
714
  // Append the members of character class [start..end) to sb. Ranges (a-z) are
594
715
  // expanded; negation (^) is ignored (we want all candidates).
595
- func add_class_chars = (string pattern, int start, int end, ref StringBuilder sb) {
596
- 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
597
722
  if cs < end {
598
723
  if pattern.at(cs) == '^' {
599
724
  cs += 1
@@ -714,6 +839,28 @@ func match_here = (
714
839
  return match_here(pattern, pat_pos + 1, pat_end, input, in_pos, in_len, ref caps)
715
840
  }
716
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
+
717
864
  // Determine the bounds of the current atom.
718
865
  var int a_end = atom_end_pos(pattern, pat_pos, pat_end)
719
866
 
@@ -774,6 +921,37 @@ func match_here = (
774
921
  ref caps,
775
922
  )
776
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
+ }
777
955
  }
778
956
 
779
957
  // No quantifier: match the atom once, then the rest of the pattern. A
@@ -864,7 +1042,7 @@ func match_group_then_rest = (
864
1042
  old_start = caps.at(at + 1)
865
1043
  old_end = caps.at(at + 2)
866
1044
  }
867
- var int inner_start = atom_start + 1
1045
+ var int inner_start = group_inner_start(pattern, atom_start, pat_end)
868
1046
  var int inner_end = atom_end - 1
869
1047
  var int s = inner_start
870
1048
  while s <= inner_end {
@@ -980,7 +1158,7 @@ func match_single_atom = (
980
1158
  }
981
1159
  var int next = match_here(
982
1160
  pattern,
983
- atom_start + 1,
1161
+ group_inner_start(pattern, atom_start, atom_end),
984
1162
  atom_end - 1,
985
1163
  input,
986
1164
  in_pos,
@@ -1066,6 +1244,149 @@ func char_matches_atom = (
1066
1244
  return pc == ic
1067
1245
  }
1068
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
+
1069
1390
  // Match the atom [atom_start..atom_end) followed by the rest of the pattern
1070
1391
  // pattern[rest..pat_end). min_count is 0 for '*' and 1 for '+'; lazy picks
1071
1392
  // the shortest repetition first instead of the longest. Single-character
@@ -1244,7 +1565,7 @@ func match_group_iteration = (
1244
1565
  old_start = caps.at(at + 1)
1245
1566
  old_end = caps.at(at + 2)
1246
1567
  }
1247
- var int inner_start = atom_start + 1
1568
+ var int inner_start = group_inner_start(pattern, atom_start, pat_end)
1248
1569
  var int inner_end = atom_end - 1
1249
1570
  var int s = inner_start
1250
1571
  while s <= inner_end {
@@ -1354,7 +1675,15 @@ func match_optional = (
1354
1675
  return grouped
1355
1676
  }
1356
1677
  } else {
1357
- var int next = match_single_atom(pattern, atom_start, atom_end, input, in_pos, in_len, ref caps)
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
+ )
1358
1687
  if next >= 0 {
1359
1688
  var int r = match_here(pattern, rest, pat_end, input, next, in_len, ref caps)
1360
1689
  if r >= 0 {
@@ -1445,9 +1774,70 @@ func char_in_class = (
1445
1774
  return found
1446
1775
  }
1447
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
+
1448
1838
  // The number of capture groups in the whole pattern: every '(' that is not
1449
- // escaped and not inside a character class (POSIX ERE has no non-capturing
1450
- // groups).
1839
+ // escaped, not inside a character class, and not a non-capturing prefix
1840
+ // (`(?:`, `(?=`, `(?!`, `(?<=`, `(?<!`).
1451
1841
  func capture_count = (string pattern, out int) {
1452
1842
  var int n = pattern.length
1453
1843
  var count = 0
@@ -1461,7 +1851,9 @@ func capture_count = (string pattern, out int) {
1461
1851
  i = atom_end_pos(pattern, i, n) - 1
1462
1852
  } else {
1463
1853
  if c == '(' {
1464
- count += 1
1854
+ if is_non_capturing_open(pattern, i, n) == false {
1855
+ count += 1
1856
+ }
1465
1857
  }
1466
1858
  }
1467
1859
  }
@@ -1470,8 +1862,9 @@ func capture_count = (string pattern, out int) {
1470
1862
  }
1471
1863
 
1472
1864
  // The capture-group index (1-based) of the group whose '(' opens at
1473
- // open_pos: one more than the number of capture opens before it in the
1474
- // 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.
1475
1868
  func group_index_at = (string pattern, int open_pos, out int) {
1476
1869
  var count = 0
1477
1870
  var i = 0
@@ -1484,11 +1877,16 @@ func group_index_at = (string pattern, int open_pos, out int) {
1484
1877
  i = atom_end_pos(pattern, i, open_pos) - 1
1485
1878
  } else {
1486
1879
  if c == '(' {
1487
- count += 1
1880
+ if is_non_capturing_open(pattern, i, open_pos) == false {
1881
+ count += 1
1882
+ }
1488
1883
  }
1489
1884
  }
1490
1885
  }
1491
1886
  }
1887
+ if is_non_capturing_open(pattern, open_pos, pattern.length) {
1888
+ return 0
1889
+ }
1492
1890
  return count + 1
1493
1891
  }
1494
1892
 
@@ -1497,6 +1895,9 @@ func group_index_at = (string pattern, int open_pos, out int) {
1497
1895
  // a repeated group reports its last iteration; a failed group records
1498
1896
  // (-1, -1) so stale spans from an abandoned attempt never surface.
1499
1897
  func record_capture = (ref List<int> caps, int group, int start, int end) {
1898
+ if group <= 0 {
1899
+ return
1900
+ }
1500
1901
  var int at = last_capture_index(caps, group)
1501
1902
  if at >= 0 {
1502
1903
  caps.set(at + 1, start)
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## `struct Regex`
4
4
 
5
- Regular-expression helpers (`test`/`match`/`count`/`replace_all`) using POSIX ERE syntax
5
+ Regular-expression helpers (`test`/`match`/`count`/`replace_all`) using POSIX ERE syntax plus `(?:...)`, lookaround, and `{n,m}` counted repetition
6
6
 
7
7
  **Members:**
8
8