nomen-lang 0.6.0 → 0.6.2

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.
@@ -255,12 +255,13 @@ func fold_pattern = (string pattern, move out string) {
255
255
  while i < n; i += 1 {
256
256
  var char c = pattern.at(i)
257
257
  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.
258
+ // Escaped char: shorthands, backrefs, and control escapes
259
+ // (\n \r \t) keep their backslash; other escaped letters
260
+ // become both-case classes; escaped punctuation is copied
261
+ // literally.
261
262
  if i + 1 < n {
262
263
  var char e = pattern.at(i + 1)
263
- if is_shorthand(e) || is_backref_digit(e) {
264
+ if is_shorthand(e) || is_backref_digit(e) || is_control_escape(e) {
264
265
  sb.append_char(c)
265
266
  sb.append_char(e)
266
267
  } else {
@@ -317,9 +318,10 @@ func fold_class = (string pattern, int cs, int ce, ref StringBuilder dst) {
317
318
  if i + 1 < content_end {
318
319
  var char e = pattern.at(i + 1)
319
320
  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) {
321
+ // A shorthand or control escape (\n \r \t) keeps its
322
+ // backslash; other escaped letters gain their case
323
+ // counterpart.
324
+ if is_alpha_byte(e) && !is_shorthand(e) && !is_control_escape(e) {
323
325
  dst.append_char(swap_case_byte(e))
324
326
  }
325
327
  i += 1
@@ -469,18 +471,20 @@ func collect_first_bytes = (
469
471
  //
470
472
  // Handles the shapes the engine's matcher can produce: a leading anchor is
471
473
  // zero-width (skipped), a leading group contributes its CONTENT's first bytes
472
- // (recursively over the group's own top-level alternatives), and a group
473
- // quantified with '?'/'*' is nullable — what FOLLOWS it can also be first, so
474
- // the scan continues past it. A wildcard '.' or an empty alternative makes the
475
- // set indeterminable.
474
+ // (recursively over the group's own top-level alternatives), and ANY leading
475
+ // atom — literal, escaped, class, or group — quantified with '?'/'*' is
476
+ // nullable: what FOLLOWS it can also be first, so the scan continues past it.
477
+ // A wildcard '.' or an empty alternative makes the set indeterminable.
476
478
  func alt_first_bytes = (string pattern, int start, int end, ref StringBuilder sb, out int) {
477
479
  var int i = start
478
480
  while i < end {
479
481
  var char c = pattern.at(i)
480
482
  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.
483
+ // escaped literal: first byte is the byte the escape names
484
+ // (\n is LF, \r is CR, \t is TAB; any other escaped character
485
+ // is its own literal byte) — unless it's a backreference
486
+ // (depends on a capture) or a complement shorthand (not an
487
+ // inclusion set), which disable the prefilter.
484
488
  if i + 1 < end {
485
489
  var char e = pattern.at(i + 1)
486
490
  if is_backref_digit(e) {
@@ -490,9 +494,17 @@ func alt_first_bytes = (string pattern, int start, int end, ref StringBuilder sb
490
494
  if shorthand_members(e, ref sb) < 0 {
491
495
  return -1
492
496
  }
497
+ } else {
498
+ sb.append_char(escaped_byte_value(e) as char)
499
+ }
500
+ // A '?'/'*' quantifier makes the atom nullable — the first
501
+ // byte can also come from whatever follows it.
502
+ var int cont = nullable_quant_after(pattern, i + 2, end)
503
+ if cont < 0 {
493
504
  return 0
494
505
  }
495
- sb.append_char(e)
506
+ i = cont
507
+ continue
496
508
  }
497
509
  return 0
498
510
  }
@@ -509,7 +521,12 @@ func alt_first_bytes = (string pattern, int start, int end, ref StringBuilder sb
509
521
  }
510
522
  }
511
523
  add_class_chars(pattern, i + 1, a_end - 1, ref sb)
512
- return 0
524
+ var int cont = nullable_quant_after(pattern, a_end, end)
525
+ if cont < 0 {
526
+ return 0
527
+ }
528
+ i = cont
529
+ continue
513
530
  }
514
531
  if c == '(' {
515
532
  var int a_end = atom_end_pos(pattern, i, end)
@@ -518,27 +535,11 @@ func alt_first_bytes = (string pattern, int start, int end, ref StringBuilder sb
518
535
  if collect_first_bytes(pattern, i + 1, a_end - 1, ref sb) < 0 {
519
536
  return -1
520
537
  }
521
- // A '?'/'*' quantifier makes the group nullable — the first byte
522
- // can also come from whatever follows it. A second '?' marks the
523
- // quantifier lazy and is skipped either way.
524
- var int after = a_end
525
- var nullable = 0
526
- if after < end {
527
- var char q = pattern.at(after)
528
- if q == '?' || q == '*' {
529
- nullable = 1
530
- after += 1
531
- if after < end {
532
- if pattern.at(after) == '?' {
533
- after += 1
534
- }
535
- }
536
- }
537
- }
538
- if nullable == 0 {
538
+ var int cont = nullable_quant_after(pattern, a_end, end)
539
+ if cont < 0 {
539
540
  return 0
540
541
  }
541
- i = after
542
+ i = cont
542
543
  continue
543
544
  }
544
545
  if c == '^' || c == '$' {
@@ -552,9 +553,40 @@ func alt_first_bytes = (string pattern, int start, int end, ref StringBuilder sb
552
553
  return -1
553
554
  }
554
555
  sb.append_char(c)
555
- return 0
556
+ var int cont = nullable_quant_after(pattern, i + 1, end)
557
+ if cont < 0 {
558
+ return 0
559
+ }
560
+ i = cont
561
+ continue
562
+ }
563
+ // An empty alternative (nothing left after nullable atoms — or nothing at
564
+ // all) can match at any position — indeterminable.
565
+ return -1
566
+ }
567
+
568
+ // If the atom ending at `after` carries a nullable quantifier ('?' or '*',
569
+ // optionally marked lazy by a second '?'), return the position just past the
570
+ // quantifier; otherwise return -1. Callers keep scanning from the returned
571
+ // position: a nullable leading atom means the alternative's match can also
572
+ // START with whatever follows, so those bytes belong in the first-byte set
573
+ // too. ('+' is NOT nullable — the atom must match once — so it leaves the
574
+ // atom's own bytes as the whole first set.)
575
+ func nullable_quant_after = (string pattern, int after, int end, out int) {
576
+ var int j = after
577
+ if j >= end {
578
+ return -1
579
+ }
580
+ var char q = pattern.at(j)
581
+ if q == '?' || q == '*' {
582
+ j += 1
583
+ if j < end {
584
+ if pattern.at(j) == '?' {
585
+ j += 1
586
+ }
587
+ }
588
+ return j
556
589
  }
557
- // An empty alternative can match at any position — indeterminable.
558
590
  return -1
559
591
  }
560
592
 
@@ -569,22 +601,34 @@ func add_class_chars = (string pattern, int start, int end, ref StringBuilder sb
569
601
  }
570
602
  var int i = cs
571
603
  while i < end {
572
- if i + 2 < end {
573
- if pattern.at(i + 1) == '-' {
574
- var int lo = pattern.at(i) as int
575
- var int hi = pattern.at(i + 2) as int
576
- while lo <= hi {
577
- sb.append_char(lo as char)
578
- lo += 1
604
+ var char c = pattern.at(i)
605
+ if (c as int) == 92 {
606
+ // Escaped byte: contribute the byte the escape names (\n is
607
+ // LF, \r is CR, \t is TAB; any other escaped character is its
608
+ // own literal byte) so the prefilter stays a superset of the
609
+ // matcher's first bytes.
610
+ if i + 1 < end {
611
+ sb.append_char(escaped_byte_value(pattern.at(i + 1)) as char)
612
+ }
613
+ i += 2
614
+ } else {
615
+ if i + 2 < end {
616
+ if pattern.at(i + 1) == '-' {
617
+ var int lo = pattern.at(i) as int
618
+ var int hi = pattern.at(i + 2) as int
619
+ while lo <= hi {
620
+ sb.append_char(lo as char)
621
+ lo += 1
622
+ }
623
+ i += 3
624
+ } else {
625
+ sb.append_char(c)
626
+ i += 1
579
627
  }
580
- i += 3
581
628
  } else {
582
- sb.append_char(pattern.at(i))
629
+ sb.append_char(c)
583
630
  i += 1
584
631
  }
585
- } else {
586
- sb.append_char(pattern.at(i))
587
- i += 1
588
632
  }
589
633
  }
590
634
  }
@@ -732,7 +776,23 @@ func match_here = (
732
776
  }
733
777
  }
734
778
 
735
- // No quantifier: match the atom once, then the rest of the pattern.
779
+ // No quantifier: match the atom once, then the rest of the pattern. A
780
+ // GROUP atom goes through the continuation-aware path: its inner
781
+ // alternatives retry when the rest fails on an earlier one (JS-style
782
+ // backtracking — `(head|header)(\s|$|>)` must still match "header>").
783
+ if pc == '(' {
784
+ return match_group_then_rest(
785
+ pattern,
786
+ pat_pos,
787
+ a_end,
788
+ a_end,
789
+ pat_end,
790
+ input,
791
+ in_pos,
792
+ in_len,
793
+ ref caps,
794
+ )
795
+ }
736
796
  var int next = match_single_atom(pattern, pat_pos, a_end, input, in_pos, in_len, ref caps)
737
797
  if next < 0 {
738
798
  return -1
@@ -740,6 +800,97 @@ func match_here = (
740
800
  return match_here(pattern, a_end, pat_end, input, next, in_len, ref caps)
741
801
  }
742
802
 
803
+ // The position of the first top-level '|' in [start, end), or -1. Escapes,
804
+ // character classes, and nested groups are skipped so only depth-0
805
+ // alternation operators count (same scanning discipline as match_here).
806
+ func next_top_level_alt = (string pattern, int start, int end, out int) {
807
+ var depth = 0
808
+ var int i = start
809
+ while i < end {
810
+ var char c = pattern.at(i)
811
+ if (c as int) == 92 {
812
+ i += 2
813
+ } else {
814
+ if c == '[' {
815
+ i = atom_end_pos(pattern, i, end)
816
+ } else {
817
+ if c == '(' {
818
+ depth += 1
819
+ }
820
+ if c == ')' {
821
+ if depth > 0 {
822
+ depth -= 1
823
+ }
824
+ }
825
+ if (c as int) == 124 {
826
+ if depth == 0 {
827
+ return i
828
+ }
829
+ }
830
+ i += 1
831
+ }
832
+ }
833
+ }
834
+ return -1
835
+ }
836
+
837
+ // Match the group atom spanning [atom_start, atom_end) ('(' at atom_start)
838
+ // and then the continuation [rest, pat_end), retrying the group's LATER
839
+ // alternatives whenever the continuation fails on an earlier one. Without
840
+ // this, a group commits to the first alternative that matches the group
841
+ // ALONE and a failing continuation aborts the whole match — the engine had
842
+ // no backtracking into a group's alternatives (PORT.md: `(head|header)`
843
+ // never reached `header` when `(\s|$|>)` rejected what `head` left behind).
844
+ // A group without a top-level '|' behaves exactly like the old
845
+ // match-atom-then-continue path, and a failed trial restores the group's
846
+ // previous capture span before the next attempt.
847
+ func match_group_then_rest = (
848
+ string pattern,
849
+ int atom_start,
850
+ int atom_end,
851
+ int rest,
852
+ int pat_end,
853
+ string input,
854
+ int in_pos,
855
+ int in_len,
856
+ ref List<int> caps,
857
+ out int
858
+ ) {
859
+ var int g = group_index_at(pattern, atom_start)
860
+ var int at = last_capture_index(caps, g)
861
+ var int old_start = -1
862
+ var int old_end = -1
863
+ if at >= 0 {
864
+ old_start = caps.at(at + 1)
865
+ old_end = caps.at(at + 2)
866
+ }
867
+ var int inner_start = atom_start + 1
868
+ var int inner_end = atom_end - 1
869
+ var int s = inner_start
870
+ while s <= inner_end {
871
+ var int bar = next_top_level_alt(pattern, s, inner_end)
872
+ var int e = inner_end
873
+ if bar >= 0 {
874
+ e = bar
875
+ }
876
+ var int next = match_here(pattern, s, e, input, in_pos, in_len, ref caps)
877
+ if next >= 0 {
878
+ record_capture(ref caps, g, in_pos, next)
879
+ var int r = match_here(pattern, rest, pat_end, input, next, in_len, ref caps)
880
+ if r >= 0 {
881
+ return r
882
+ }
883
+ }
884
+ record_capture(ref caps, g, old_start, old_end)
885
+ if bar >= 0 {
886
+ s = bar + 1
887
+ } else {
888
+ s = inner_end + 1
889
+ }
890
+ }
891
+ return -1
892
+ }
893
+
743
894
  // Return the position just past the atom that starts at pat_pos. Handles
744
895
  // character classes [...], groups (...), and escaped characters (\x).
745
896
  func atom_end_pos = (string pattern, int pat_pos, int pat_end, out int) {
@@ -860,7 +1011,7 @@ func match_single_atom = (
860
1011
  return -1
861
1012
  }
862
1013
  if in_pos < in_len {
863
- if input.at(in_pos) == ec {
1014
+ if (input.at(in_pos) as int) == escaped_byte_value(ec) {
864
1015
  return in_pos + 1
865
1016
  }
866
1017
  }
@@ -908,7 +1059,7 @@ func char_matches_atom = (
908
1059
  if is_shorthand(ec) {
909
1060
  return matches_shorthand(ec, ic)
910
1061
  }
911
- return ic == ec
1062
+ return (ic as int) == escaped_byte_value(ec)
912
1063
  }
913
1064
  return false
914
1065
  }
@@ -1018,29 +1169,99 @@ func match_star_group = (
1018
1169
  ) {
1019
1170
  if lazy > 0 {
1020
1171
  // Lazy: stop repeating first (minimum met), then try one more
1021
- // iteration before giving up.
1172
+ // iteration before giving up. The iteration retries the group's
1173
+ // top-level alternatives (see match_group_iteration).
1022
1174
  if min_count <= 0 {
1023
1175
  var int stop = match_here(pattern, rest, pat_end, input, in_pos, in_len, ref caps)
1024
1176
  if stop >= 0 {
1025
1177
  return stop
1026
1178
  }
1027
1179
  }
1028
- var int next = match_single_atom(
1180
+ return match_group_iteration(
1029
1181
  pattern,
1030
1182
  atom_start,
1031
1183
  atom_end,
1184
+ rest,
1185
+ pat_end,
1032
1186
  input,
1033
1187
  in_pos,
1188
+ min_count,
1189
+ lazy,
1034
1190
  in_len,
1035
1191
  ref caps,
1036
1192
  )
1193
+ }
1194
+ // Try matching the group one more time (greedy) — each top-level
1195
+ // alternative in turn — before stopping.
1196
+ var int r = match_group_iteration(
1197
+ pattern,
1198
+ atom_start,
1199
+ atom_end,
1200
+ rest,
1201
+ pat_end,
1202
+ input,
1203
+ in_pos,
1204
+ min_count,
1205
+ lazy,
1206
+ in_len,
1207
+ ref caps,
1208
+ )
1209
+ if r >= 0 {
1210
+ return r
1211
+ }
1212
+ // Stop repeating and try the rest of the pattern, if the minimum is met.
1213
+ if min_count <= 0 {
1214
+ return match_here(pattern, rest, pat_end, input, in_pos, in_len, ref caps)
1215
+ }
1216
+ return -1
1217
+ }
1218
+
1219
+ // Try ONE further iteration of a quantified group at in_pos, retrying the
1220
+ // group's top-level alternatives while the remaining star recursion fails.
1221
+ // Returns the recursion's result, or -1 when no alternative leads to a
1222
+ // match. An alternative that matches the empty string is not recursed into
1223
+ // (an empty iteration cannot make progress; the caller's stop path covers
1224
+ // it), and every failed trial restores the group's previous capture span.
1225
+ func match_group_iteration = (
1226
+ string pattern,
1227
+ int atom_start,
1228
+ int atom_end,
1229
+ int rest,
1230
+ int pat_end,
1231
+ string input,
1232
+ int in_pos,
1233
+ int min_count,
1234
+ int lazy,
1235
+ int in_len,
1236
+ ref List<int> caps,
1237
+ out int
1238
+ ) {
1239
+ var int g = group_index_at(pattern, atom_start)
1240
+ var int at = last_capture_index(caps, g)
1241
+ var int old_start = -1
1242
+ var int old_end = -1
1243
+ if at >= 0 {
1244
+ old_start = caps.at(at + 1)
1245
+ old_end = caps.at(at + 2)
1246
+ }
1247
+ var int inner_start = atom_start + 1
1248
+ var int inner_end = atom_end - 1
1249
+ var int s = inner_start
1250
+ while s <= inner_end {
1251
+ var int bar = next_top_level_alt(pattern, s, inner_end)
1252
+ var int e = inner_end
1253
+ if bar >= 0 {
1254
+ e = bar
1255
+ }
1256
+ var int next = match_here(pattern, s, e, input, in_pos, in_len, ref caps)
1037
1257
  if next >= 0 {
1258
+ record_capture(ref caps, g, in_pos, next)
1038
1259
  if next != in_pos {
1039
1260
  var new_min = 0
1040
1261
  if min_count > 0 {
1041
1262
  new_min = min_count - 1
1042
1263
  }
1043
- return match_star_group(
1264
+ var int r = match_star_group(
1044
1265
  pattern,
1045
1266
  atom_start,
1046
1267
  atom_end,
@@ -1053,40 +1274,18 @@ func match_star_group = (
1053
1274
  in_len,
1054
1275
  ref caps,
1055
1276
  )
1277
+ if r >= 0 {
1278
+ return r
1279
+ }
1056
1280
  }
1057
1281
  }
1058
- return -1
1059
- }
1060
- // Try matching the group one more time (greedy).
1061
- var int next = match_single_atom(pattern, atom_start, atom_end, input, in_pos, in_len, ref caps)
1062
- if next >= 0 {
1063
- if next != in_pos {
1064
- var new_min = 0
1065
- if min_count > 0 {
1066
- new_min = min_count - 1
1067
- }
1068
- var int r = match_star_group(
1069
- pattern,
1070
- atom_start,
1071
- atom_end,
1072
- rest,
1073
- pat_end,
1074
- input,
1075
- next,
1076
- new_min,
1077
- lazy,
1078
- in_len,
1079
- ref caps,
1080
- )
1081
- if r >= 0 {
1082
- return r
1083
- }
1282
+ record_capture(ref caps, g, old_start, old_end)
1283
+ if bar >= 0 {
1284
+ s = bar + 1
1285
+ } else {
1286
+ s = inner_end + 1
1084
1287
  }
1085
1288
  }
1086
- // Stop repeating and try the rest of the pattern, if the minimum is met.
1087
- if min_count <= 0 {
1088
- return match_here(pattern, rest, pat_end, input, in_pos, in_len, ref caps)
1089
- }
1090
1289
  return -1
1091
1290
  }
1092
1291
 
@@ -1110,6 +1309,21 @@ func match_optional = (
1110
1309
  if skip >= 0 {
1111
1310
  return skip
1112
1311
  }
1312
+ // A group atom includes continuation-aware alternative retries (see
1313
+ // match_group_then_rest) in both moods.
1314
+ if pattern.at(atom_start) == '(' {
1315
+ return match_group_then_rest(
1316
+ pattern,
1317
+ atom_start,
1318
+ atom_end,
1319
+ rest,
1320
+ pat_end,
1321
+ input,
1322
+ in_pos,
1323
+ in_len,
1324
+ ref caps,
1325
+ )
1326
+ }
1113
1327
  var int next = match_single_atom(
1114
1328
  pattern,
1115
1329
  atom_start,
@@ -1124,11 +1338,28 @@ func match_optional = (
1124
1338
  }
1125
1339
  return -1
1126
1340
  }
1127
- var int next = match_single_atom(pattern, atom_start, atom_end, input, in_pos, in_len, ref caps)
1128
- if next >= 0 {
1129
- var int r = match_here(pattern, rest, pat_end, input, next, in_len, ref caps)
1130
- if r >= 0 {
1131
- return r
1341
+ if pattern.at(atom_start) == '(' {
1342
+ var int grouped = match_group_then_rest(
1343
+ pattern,
1344
+ atom_start,
1345
+ atom_end,
1346
+ rest,
1347
+ pat_end,
1348
+ input,
1349
+ in_pos,
1350
+ in_len,
1351
+ ref caps,
1352
+ )
1353
+ if grouped >= 0 {
1354
+ return grouped
1355
+ }
1356
+ } else {
1357
+ var int next = match_single_atom(pattern, atom_start, atom_end, input, in_pos, in_len, ref caps)
1358
+ if next >= 0 {
1359
+ var int r = match_here(pattern, rest, pat_end, input, next, in_len, ref caps)
1360
+ if r >= 0 {
1361
+ return r
1362
+ }
1132
1363
  }
1133
1364
  }
1134
1365
  return match_here(pattern, rest, pat_end, input, in_pos, in_len, ref caps)
@@ -1173,7 +1404,7 @@ func char_in_class = (
1173
1404
  found = true
1174
1405
  }
1175
1406
  } else {
1176
- if ic == e {
1407
+ if (ic as int) == escaped_byte_value(e) {
1177
1408
  found = true
1178
1409
  }
1179
1410
  }
@@ -1428,6 +1659,29 @@ func is_shorthand = (char c, out bool) {
1428
1659
  return code == 100 || code == 68 || code == 119 || code == 87 || code == 115 || code == 83
1429
1660
  }
1430
1661
 
1662
+ // Whether the escaped character c names a control-character shorthand
1663
+ // (\n \r \t) that the engine maps to its real control byte.
1664
+ func is_control_escape = (char c, out bool) {
1665
+ var int code = c as int
1666
+ return code == 110 || code == 114 || code == 116
1667
+ }
1668
+
1669
+ // The byte an escaped atom names: \n is LF (10), \r is CR (13), \t is TAB
1670
+ // (9); every other escaped character is the literal byte of that character.
1671
+ func escaped_byte_value = (char c, out int) {
1672
+ var int code = c as int
1673
+ if code == 110 {
1674
+ return 10
1675
+ }
1676
+ if code == 114 {
1677
+ return 13
1678
+ }
1679
+ if code == 116 {
1680
+ return 9
1681
+ }
1682
+ return code
1683
+ }
1684
+
1431
1685
  // Whether byte b belongs to the shorthand class named by c. Uppercase names
1432
1686
  // are the complements (S/D/W match anything the lowercase set excludes).
1433
1687
  func matches_shorthand = (char c, char b, out bool) {