tree-sitter-sed 0.7.0 → 0.9.0

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.
package/common/grammar.js CHANGED
@@ -13,6 +13,37 @@ function missingAtEndOrBoundary($, reason) {
13
13
  );
14
14
  }
15
15
 
16
+ function blankSeparatedFileOperand($, name, operand) {
17
+ return seq(
18
+ $._blanks,
19
+ choice(operand, missingAtEndOrBoundary($, `missing_${name}`)),
20
+ );
21
+ }
22
+
23
+ function fileOperandForms($, name, operand) {
24
+ return [
25
+ blankSeparatedFileOperand($, name, operand),
26
+ seq(issueField($, "omitted_file_separator"), operand),
27
+ missingAtEndOrBoundary($, `missing_${name}`),
28
+ ];
29
+ }
30
+
31
+ function lineOperand($, first, rest) {
32
+ return prec.right(
33
+ seq(
34
+ choice(first, issueField($, "nul_character")),
35
+ repeat(choice(rest, issueField($, "nul_character"))),
36
+ ),
37
+ );
38
+ }
39
+
40
+ function commentText($) {
41
+ return choice(
42
+ namedExternal($, $._comment_text, "comment_text"),
43
+ issueField($, "nul_character"),
44
+ );
45
+ }
46
+
16
47
  function missingCommandSeparator($) {
17
48
  return choice(
18
49
  issueNode($, "missing_command_separator"),
@@ -24,14 +55,6 @@ function functionVerb($, spelling) {
24
55
  return field("verb", alias(spelling, $.function_verb));
25
56
  }
26
57
 
27
- function leadingEmptyCommands($) {
28
- return alias($._leading_empty_commands, $.empty_command);
29
- }
30
-
31
- function withLeadingEmptyCommands($, commands) {
32
- return choice(commands, seq(leadingEmptyCommands($), optional(commands)));
33
- }
34
-
35
58
  const delimiterNames = [
36
59
  "regex_address_start",
37
60
  "escaped_regex_address_start",
@@ -139,16 +162,35 @@ const functionDefinitions = [
139
162
  },
140
163
  ];
141
164
 
142
- const nonWriteSubstitutionFlagRules = [
143
- "occurrence_flag",
144
- "global_flag",
145
- "case_insensitive_flag",
146
- "print_flag",
147
- ];
165
+ // A known verb never becomes an unknown function: address maxima are owned by
166
+ // the excess address clauses, and a token that could also match a verb would
167
+ // let a GLR branch that expects other verbs compete with the valid parse.
168
+ const unknownFunctionCharacter = new RegExp(
169
+ `[^[:blank:][:digit:]$\\/\\\\!;}\\n${functionDefinitions
170
+ .map(({ spelling }) => spelling)
171
+ .join("")}]`,
172
+ );
173
+
174
+ // Letter flags are aliased at their use sites: a rule that is only a string
175
+ // shares its token with the function verbs and would wrap an anonymous leaf.
176
+ const letterSubstitutionFlags = {
177
+ global_flag: "g",
178
+ case_insensitive_flag: "i",
179
+ print_flag: "p",
180
+ };
181
+
182
+ function nonWriteSubstitutionFlags($) {
183
+ return [
184
+ $.occurrence_flag,
185
+ ...Object.entries(letterSubstitutionFlags).map(([rule, spelling]) =>
186
+ alias(spelling, $[rule]),
187
+ ),
188
+ ];
189
+ }
148
190
 
149
191
  function postWriteSubstitutionFlagChoice($) {
150
192
  return choice(
151
- ...nonWriteSubstitutionFlagRules.map((rule) => $[rule]),
193
+ ...nonWriteSubstitutionFlags($),
152
194
  alias("w", $.substitution_flag),
153
195
  );
154
196
  }
@@ -187,11 +229,6 @@ function delimiterRules() {
187
229
  return rules;
188
230
  }
189
231
 
190
- function choiceForRules($, names) {
191
- const rules = names.map((name) => $[name]);
192
- return rules.length === 1 ? rules[0] : choice(...rules);
193
- }
194
-
195
232
  function commandListRules() {
196
233
  return {
197
234
  script: ($) => optional(alias($._script_command_list, $.command_list)),
@@ -230,28 +267,15 @@ function commandListRules() {
230
267
  "default_output_suppression",
231
268
  ),
232
269
  ),
233
- optional(namedExternal($, $._comment_text, "comment_text")),
270
+ repeat(commentText($)),
234
271
  ),
235
272
 
236
273
  _command_list: ($) =>
237
274
  choice($._final_line, seq($._terminated_line, optional($._command_list))),
238
275
 
239
- _final_line: ($) =>
240
- choice($._line_content, alias($._blanks, $.empty_command)),
241
-
242
- _terminated_line: ($) =>
243
- choice(
244
- seq($._line_content, $._newline_separator),
245
- alias($._empty_terminated_line, $.empty_command),
246
- ),
247
-
248
- _empty_terminated_line: ($) =>
249
- seq(optional($._blanks), $._newline_separator),
276
+ _final_line: ($) => choice($._command_sequence, $._blanks),
250
277
 
251
- _line_content: ($) => withLeadingEmptyCommands($, $._command_sequence),
252
-
253
- _leading_empty_commands: ($) =>
254
- seq(optional($._blanks), $._semicolon_separators),
278
+ _terminated_line: ($) => seq($._command_sequence, $._newline_separator),
255
279
 
256
280
  _command_sequence: ($) =>
257
281
  choice(
@@ -261,9 +285,9 @@ function commandListRules() {
261
285
  ),
262
286
  seq(
263
287
  $._recovered_line_terminated_command_item,
264
- optional($._line_content),
288
+ optional(choice($._command_sequence, $._blanks)),
265
289
  ),
266
- seq($._chainable_command_item, optional($._command_sequence_tail)),
290
+ seq($._separable_command_item, optional($._command_sequence_tail)),
267
291
  seq(
268
292
  $._unmatched_closing_brace_item,
269
293
  optional($._unmatched_closing_brace_tail_content),
@@ -272,7 +296,10 @@ function commandListRules() {
272
296
 
273
297
  _command_sequence_tail: ($) =>
274
298
  choice(
275
- seq($._semicolon_separators, optional($._command_sequence)),
299
+ seq(
300
+ $._semicolon_separator,
301
+ optional(choice($._command_sequence, $._blanks)),
302
+ ),
276
303
  $._unmatched_closing_brace_tail,
277
304
  ),
278
305
 
@@ -295,6 +322,9 @@ function commandListRules() {
295
322
  ),
296
323
  ),
297
324
 
325
+ _separable_command_item: ($) =>
326
+ choice($.empty_command, $._chainable_command_item),
327
+
298
328
  _chainable_command_item: ($) =>
299
329
  choice(
300
330
  alias($._chainable_editing_command, $.editing_command),
@@ -312,39 +342,32 @@ function commandListRules() {
312
342
  $._line_terminated_command_item,
313
343
  seq(
314
344
  $._recovered_line_terminated_command_item,
315
- optional($._block_line_content),
345
+ optional($._block_command_sequence),
316
346
  ),
317
347
  seq(
318
- $._chainable_command_item,
319
- optional($._block_command_sequence_tail),
348
+ $._separable_command_item,
349
+ optional(
350
+ seq($._semicolon_separator, optional($._block_command_sequence)),
351
+ ),
320
352
  ),
321
353
  ),
322
354
 
323
- _block_command_sequence_tail: ($) =>
324
- seq($._semicolon_separators, optional($._block_command_sequence)),
325
-
326
- _block_line_content: ($) =>
327
- withLeadingEmptyCommands($, $._block_command_sequence),
328
-
329
- _block_line_content_before_close: ($) =>
330
- withLeadingEmptyCommands($, $._block_command_sequence_before_close),
331
-
332
355
  _block_command_sequence_before_close: ($) =>
333
356
  choice(
334
357
  seq(
335
- $._chainable_command_item,
336
- $._semicolon_separators,
358
+ $._separable_command_item,
359
+ $._semicolon_separator,
337
360
  optional($._block_command_sequence_before_close),
338
361
  ),
339
362
  seq(
340
363
  $._recovered_line_terminated_command_item,
341
- optional($._block_line_content_before_close),
364
+ optional($._block_command_sequence_before_close),
342
365
  ),
343
366
  ),
344
367
 
345
368
  _block_command_list: ($) =>
346
369
  choice(
347
- $._block_line_content_before_close,
370
+ $._block_command_sequence_before_close,
348
371
  seq($._terminated_block_line, optional($._block_command_list)),
349
372
  ),
350
373
 
@@ -354,26 +377,18 @@ function commandListRules() {
354
377
  _block_command_list_missing_separator: ($) =>
355
378
  choice(
356
379
  missingCommandSeparator($),
357
- seq(
358
- optional(leadingEmptyCommands($)),
359
- $._block_command_sequence,
360
- missingCommandSeparator($),
361
- ),
380
+ seq($._block_command_sequence, missingCommandSeparator($)),
362
381
  seq($._terminated_block_line, $._block_command_list_missing_separator),
363
382
  ),
364
383
 
365
384
  _terminated_block_line: ($) =>
366
- choice(
367
- seq($._block_line_content, $._newline_separator),
368
- alias($._empty_terminated_line, $.empty_command),
369
- ),
385
+ seq($._block_command_sequence, $._newline_separator),
370
386
 
371
387
  _newline_separator: ($) => alias("\n", $.command_separator),
372
388
 
373
- _semicolon_separators: ($) => prec.right(repeat1($._semicolon_separator)),
389
+ _semicolon_separator: ($) => alias(";", $.command_separator),
374
390
 
375
- _semicolon_separator: ($) =>
376
- prec.right(seq(alias(";", $.command_separator), optional($._blanks))),
391
+ empty_command: ($) => seq(optional($._blanks), $._empty_command_marker),
377
392
 
378
393
  _blanks: () => token(/[[:blank:]]+/),
379
394
  };
@@ -434,12 +449,10 @@ function addressRules(mode) {
434
449
  field("second", $.address),
435
450
  ),
436
451
  seq(
437
- field("first", $.address),
438
- commaSeparator($),
439
- $._omitted_second_address_issue,
440
- ),
441
- seq(
442
- issueField($, "omitted_first_address"),
452
+ choice(
453
+ field("first", $.address),
454
+ issueField($, "omitted_first_address"),
455
+ ),
443
456
  commaSeparator($),
444
457
  $._omitted_second_address_issue,
445
458
  ),
@@ -516,7 +529,6 @@ function addressRules(mode) {
516
529
 
517
530
  address: addressChoice,
518
531
 
519
- // Keep maximum-specific GLR paths distinct until the function verb.
520
532
  _max0_address: addressChoice,
521
533
 
522
534
  _max1_address: addressChoice,
@@ -541,65 +553,67 @@ function addressRules(mode) {
541
553
  };
542
554
  }
543
555
 
556
+ function delimitedFunction($, spelling, name, first, middleReason, rest) {
557
+ return seq(
558
+ functionVerb($, spelling),
559
+ choice(
560
+ seq(
561
+ field("opening", delimiter($, `${name}_start`)),
562
+ optional(first),
563
+ choice(
564
+ seq(field("middle", delimiter($, `${name}_middle`)), ...rest),
565
+ field("middle", unterminatedDelimiter($, middleReason)),
566
+ ),
567
+ ),
568
+ missingOpeningDelimiter($),
569
+ ),
570
+ );
571
+ }
572
+
544
573
  function operandRules(mode) {
545
574
  const expression = mode === "bre" ? "basic_reg_exp" : "extended_reg_exp";
546
575
 
547
576
  function substitutionFlagChoice($) {
548
- const rules = nonWriteSubstitutionFlagRules.map((rule) => $[rule]);
549
- rules.push(issueField($, "invalid_substitution_flag"));
550
- return choice(...rules);
577
+ return choice(
578
+ ...nonWriteSubstitutionFlags($),
579
+ issueField($, "invalid_substitution_flag"),
580
+ );
551
581
  }
552
582
 
553
583
  return {
554
- _substitute_function: ($) =>
555
- choice(
556
- seq(
557
- $._complete_substitute_function,
558
- optional(
559
- field("flags", alias($._substitution_flags, $.substitution_flags)),
560
- ),
561
- ),
562
- $._incomplete_substitute_function,
563
- ),
564
-
565
- _complete_substitute_function: ($) =>
566
- seq(
567
- functionVerb($, "s"),
568
- field("opening", delimiter($, "substitute_start")),
569
- optional(field("expression", $[expression])),
570
- field("middle", delimiter($, "substitute_middle")),
571
- optional(field("replacement", $.replacement)),
572
- field("closing", delimiter($, "substitute_end")),
573
- ),
574
-
575
- _incomplete_substitute_function: ($) =>
576
- seq(
577
- functionVerb($, "s"),
578
- choice(
579
- missingOpeningDelimiter($),
580
- seq(
581
- field("opening", delimiter($, "substitute_start")),
582
- optional(field("expression", $[expression])),
583
- choice(
584
- seq(
585
- field("middle", delimiter($, "substitute_middle")),
586
- optional(field("replacement", $.replacement)),
587
- field("closing", unterminatedDelimiter($, "replacement")),
584
+ substitute_function: ($) =>
585
+ delimitedFunction(
586
+ $,
587
+ "s",
588
+ "substitute",
589
+ field("expression", $[expression]),
590
+ "regular_expression",
591
+ [
592
+ optional(field("replacement", $.replacement)),
593
+ choice(
594
+ seq(
595
+ field("closing", delimiter($, "substitute_end")),
596
+ optional(
597
+ field(
598
+ "flags",
599
+ alias($._substitution_flags, $.substitution_flags),
600
+ ),
588
601
  ),
589
- field("middle", unterminatedDelimiter($, "regular_expression")),
590
602
  ),
603
+ field("closing", unterminatedDelimiter($, "replacement")),
591
604
  ),
592
- ),
605
+ ],
593
606
  ),
594
607
 
595
608
  replacement: ($) =>
596
609
  repeat1(
597
610
  choice(
598
611
  $.replacement_literal,
612
+ issueField($, "nul_character"),
599
613
  $.matched_text_reference,
600
614
  $.replacement_backreference,
601
615
  $.replacement_escaped_delimiter,
602
- $.ambiguous_replacement_delimiter_escape,
616
+ issueField($, "replacement_ampersand_delimiter_escape"),
603
617
  $.replacement_escape,
604
618
  $.escaped_newline,
605
619
  issueField($, "unspecified_replacement_escape"),
@@ -607,45 +621,17 @@ function operandRules(mode) {
607
621
  ),
608
622
  ),
609
623
 
610
- replacement_literal: ($) =>
611
- namedExternal($, $._replacement_literal, "replacement_literal_token"),
624
+ replacement_literal: ($) => $._replacement_literal,
612
625
 
613
- matched_text_reference: ($) =>
614
- namedExternal(
615
- $,
616
- $._replacement_match_reference,
617
- "matched_text_reference_token",
618
- ),
626
+ matched_text_reference: ($) => $._replacement_match_reference,
619
627
 
620
- replacement_backreference: ($) =>
621
- namedExternal(
622
- $,
623
- $._replacement_backreference,
624
- "replacement_backreference_token",
625
- ),
628
+ replacement_backreference: ($) => $._replacement_backreference,
626
629
 
627
- replacement_escaped_delimiter: ($) =>
628
- field(
629
- "token",
630
- namedExternal(
631
- $,
632
- $._replacement_escaped_delimiter,
633
- "escaped_delimiter_token",
634
- ),
635
- ),
636
-
637
- ambiguous_replacement_delimiter_escape: ($) =>
638
- issueField($, "replacement_ampersand_delimiter_escape"),
630
+ replacement_escaped_delimiter: ($) => $._replacement_escaped_delimiter,
639
631
 
640
- replacement_escape: ($) =>
641
- namedExternal(
642
- $,
643
- $._replacement_escape_sequence,
644
- "replacement_escape_token",
645
- ),
632
+ replacement_escape: ($) => $._replacement_escape_sequence,
646
633
 
647
- escaped_newline: ($) =>
648
- namedExternal($, $._replacement_escaped_newline, "escaped_newline_token"),
634
+ escaped_newline: ($) => $._replacement_escaped_newline,
649
635
 
650
636
  _substitution_flags: ($) =>
651
637
  choice(
@@ -663,65 +649,45 @@ function operandRules(mode) {
663
649
 
664
650
  occurrence_flag: () => /[[:digit:]]+/,
665
651
 
666
- global_flag: () => "g",
667
-
668
- case_insensitive_flag: () => "i",
669
-
670
- print_flag: () => "p",
671
-
672
- write_flag: ($) =>
673
- seq(
652
+ write_flag: ($) => {
653
+ const wfile = field("wfile", alias($._substitution_wfile, $.wfile));
654
+ return seq(
674
655
  field("verb", alias("w", $.substitution_flag)),
675
656
  choice(
676
657
  seq(
677
658
  $._flag_after_write_marker,
678
659
  repeat1(postWriteSubstitutionFlagChoice($)),
679
- $._blanks,
680
- choice(
681
- field("wfile", alias($._substitution_wfile, $.wfile)),
682
- missingAtEndOrBoundary($, "missing_wfile"),
683
- ),
684
- ),
685
- seq($._blanks, field("wfile", alias($._substitution_wfile, $.wfile))),
686
- seq($._blanks, missingAtEndOrBoundary($, "missing_wfile")),
687
- seq(
688
- issueField($, "omitted_file_separator"),
689
- field("wfile", alias($._substitution_wfile, $.wfile)),
660
+ blankSeparatedFileOperand($, "wfile", wfile),
690
661
  ),
691
- missingAtEndOrBoundary($, "missing_wfile"),
662
+ ...fileOperandForms($, "wfile", wfile),
692
663
  ),
693
- ),
664
+ );
665
+ },
694
666
 
695
667
  translate_function: ($) =>
696
- seq(
697
- functionVerb($, "y"),
698
- choice(
699
- seq(
700
- field("opening", delimiter($, "translate_start")),
701
- optional(field("string1", $.translation_string)),
668
+ delimitedFunction(
669
+ $,
670
+ "y",
671
+ "translate",
672
+ field("string1", $.translation_string),
673
+ "translation",
674
+ [
675
+ optional(field("string2", $.translation_string)),
676
+ field(
677
+ "closing",
702
678
  choice(
703
- seq(
704
- field("middle", delimiter($, "translate_middle")),
705
- optional(field("string2", $.translation_string)),
706
- field(
707
- "closing",
708
- choice(
709
- delimiter($, "translate_end"),
710
- unterminatedDelimiter($, "translation"),
711
- ),
712
- ),
713
- ),
714
- field("middle", unterminatedDelimiter($, "translation")),
679
+ delimiter($, "translate_end"),
680
+ unterminatedDelimiter($, "translation"),
715
681
  ),
716
682
  ),
717
- missingOpeningDelimiter($),
718
- ),
683
+ ],
719
684
  ),
720
685
 
721
686
  translation_string: ($) =>
722
687
  repeat1(
723
688
  choice(
724
689
  $.translation_literal,
690
+ issueField($, "nul_character"),
725
691
  $.translation_escape,
726
692
  $.translation_escaped_delimiter,
727
693
  issueField($, "undefined_translation_escape"),
@@ -729,18 +695,11 @@ function operandRules(mode) {
729
695
  ),
730
696
  ),
731
697
 
732
- translation_literal: ($) =>
733
- namedExternal($, $._translate_literal, "translation_literal_token"),
698
+ translation_literal: ($) => $._translate_literal,
734
699
 
735
- translation_escape: ($) =>
736
- namedExternal($, $._translate_escape, "translation_escape_token"),
700
+ translation_escape: ($) => $._translate_escape,
737
701
 
738
- translation_escaped_delimiter: ($) =>
739
- namedExternal(
740
- $,
741
- $._translate_escaped_delimiter,
742
- "escaped_delimiter_token",
743
- ),
702
+ translation_escaped_delimiter: ($) => $._translate_escaped_delimiter,
744
703
  };
745
704
  }
746
705
 
@@ -755,57 +714,62 @@ function functionRules() {
755
714
  repeat1(
756
715
  choice(
757
716
  $.text_literal,
717
+ issueField($, "nul_character"),
758
718
  $.text_backslash_escape,
759
719
  $.text_escaped_newline,
760
720
  issueField($, "unspecified_text_escape"),
761
721
  ),
762
722
  ),
763
- optional(issueField($, "missing_text")),
764
- $._text_end,
723
+ $._text_tail,
765
724
  ),
766
725
 
726
+ _text_tail: ($) =>
727
+ seq(optional(issueField($, "missing_text")), $._text_end),
728
+
767
729
  _text_end: ($) => choice($._text_line_end, $._text_eof),
768
730
 
769
- text_literal: ($) =>
770
- namedExternal($, $._text_literal, "text_literal_token"),
731
+ text_literal: ($) => $._text_literal,
771
732
 
772
- text_backslash_escape: ($) =>
773
- namedExternal($, $._text_backslash_escape, "text_backslash_escape_token"),
733
+ text_backslash_escape: ($) => $._text_backslash_escape,
774
734
 
775
- text_escaped_newline: ($) =>
776
- namedExternal($, $._text_escaped_newline, "text_escaped_newline_token"),
735
+ text_escaped_newline: ($) => $._text_escaped_newline,
777
736
 
778
737
  text_introducer: ($) =>
779
738
  namedExternal($, $._text_command_start, "text_introducer_token"),
780
739
 
781
- rfile: ($) => namedExternal($, $._file_argument, "rfile_token"),
740
+ _incomplete_text_introducer: ($) =>
741
+ issueField($, "incomplete_text_introducer"),
782
742
 
783
- wfile: ($) => namedExternal($, $._file_argument, "wfile_token"),
743
+ rfile: ($) => lineOperand($, $._file_argument, $._line_word),
744
+
745
+ wfile: ($) => lineOperand($, $._file_argument, $._line_word),
784
746
 
785
747
  _substitution_wfile: ($) =>
786
- namedExternal($, $._substitution_wfile_argument, "wfile_token"),
748
+ lineOperand(
749
+ $,
750
+ $._substitution_wfile_argument,
751
+ $._substitution_wfile_continuation,
752
+ ),
787
753
 
788
- label: ($) => namedExternal($, $._line_word, "label_token"),
754
+ label: ($) =>
755
+ prec.right(repeat1(choice($._line_word, issueField($, "nul_character")))),
789
756
 
790
- comment: ($) => namedExternal($, $._comment_text, "comment_text"),
757
+ comment: ($) => prec.right(repeat1(commentText($))),
791
758
 
792
- closing_brace: ($) =>
793
- choice(
794
- namedExternal($, $._right_brace, "closing_brace_token"),
795
- issueField($, "missing_closing_brace"),
759
+ comment_function: ($) =>
760
+ prec.right(
761
+ seq(functionVerb($, "#"), optional(field("comment", $.comment))),
796
762
  ),
763
+
764
+ closing_brace: ($) =>
765
+ choice($._present_closing_brace, issueField($, "missing_closing_brace")),
766
+
767
+ _present_closing_brace: ($) =>
768
+ namedExternal($, $._right_brace, "closing_brace_token"),
797
769
  };
798
770
 
799
771
  function fileForm(form) {
800
- const missingFileReason = `missing_${form}`;
801
- return ($) => [
802
- choice(
803
- seq($._blanks, field(form, $[form])),
804
- seq($._blanks, missingAtEndOrBoundary($, missingFileReason)),
805
- seq(issueField($, "omitted_file_separator"), field(form, $[form])),
806
- missingAtEndOrBoundary($, missingFileReason),
807
- ),
808
- ];
772
+ return ($) => [choice(...fileOperandForms($, form, field(form, $[form])))];
809
773
  }
810
774
 
811
775
  const formArguments = {
@@ -818,10 +782,11 @@ function functionRules() {
818
782
  choice(
819
783
  seq(
820
784
  field("introducer", $.text_introducer),
821
- choice(
822
- field("text", $.text),
823
- seq(optional(issueField($, "missing_text")), $._text_end),
824
- ),
785
+ choice(field("text", $.text), $._text_tail),
786
+ ),
787
+ field(
788
+ "introducer",
789
+ alias($._incomplete_text_introducer, $.text_introducer),
825
790
  ),
826
791
  missingAtEndOrBoundary($, "missing_text_introducer"),
827
792
  ),
@@ -830,7 +795,10 @@ function functionRules() {
830
795
  optional(
831
796
  prec.right(
832
797
  seq(
833
- field("separator", alias(" ", $.argument_separator)),
798
+ field(
799
+ "separator",
800
+ namedExternal($, $._argument_separator, "argument_separator"),
801
+ ),
834
802
  optional(field("label", $.label)),
835
803
  ),
836
804
  ),
@@ -844,7 +812,6 @@ function functionRules() {
844
812
  ],
845
813
  rfile: fileForm("rfile"),
846
814
  wfile: fileForm("wfile"),
847
- comment: ($) => [optional(field("comment", $.comment))],
848
815
  };
849
816
 
850
817
  for (const { rule, spelling } of noArgumentDefinitions) {
@@ -863,124 +830,101 @@ function functionRules() {
863
830
  }
864
831
 
865
832
  function editingCommandRules() {
866
- const regularDefinitions = functionDefinitions.filter(
867
- ({ form }) => form !== "substitute",
868
- );
869
- const chainable = regularDefinitions.filter(
870
- ({ lineTerminated }) => !lineTerminated,
871
- );
872
- const lineTerminated = regularDefinitions.filter(
873
- ({ lineTerminated: isLineTerminated }) => isLineTerminated,
874
- );
875
-
876
- function functionsByMaximum(definitions, maximum) {
877
- return definitions
878
- .filter(({ maxAddresses = 2 }) => maxAddresses === maximum)
879
- .map(({ rule }) => rule);
880
- }
881
-
882
- function wrapperName(kind, maximum) {
883
- return `_${kind}_${maximum}_address_function`;
884
- }
833
+ const addressClauses = {
834
+ 0: ($) => alias($._max0_address_clause, $.address_clause),
835
+ 1: ($) => alias($._max1_address_clause, $.address_clause),
836
+ 2: ($) => $.address_clause,
837
+ };
885
838
 
886
- function functionWrapper($, name) {
887
- return alias($[name], $.function);
839
+ // Keep address, issue, and function rules separate by address maximum so
840
+ // GLR paths cannot merge before the function verb.
841
+ const wrappers = [];
842
+ for (const [kind, isLineTerminated] of [
843
+ ["chainable", false],
844
+ ["line_terminated", true],
845
+ ]) {
846
+ for (const maximum of [0, 1, 2]) {
847
+ const names = functionDefinitions
848
+ .filter(
849
+ ({ lineTerminated = false, maxAddresses = 2 }) =>
850
+ lineTerminated === isLineTerminated && maxAddresses === maximum,
851
+ )
852
+ .map(({ rule }) => rule);
853
+ if (names.length > 0) {
854
+ wrappers.push({
855
+ kind,
856
+ maximum,
857
+ name: `_${kind}_${maximum}_address_function`,
858
+ names,
859
+ });
860
+ }
861
+ }
888
862
  }
889
863
 
890
- function addressClauseForMaximum($, maximum) {
891
- if (maximum === 0) {
892
- return alias($._max0_address_clause, $.address_clause);
893
- }
894
- if (maximum === 1) {
895
- return alias($._max1_address_clause, $.address_clause);
896
- }
897
- return $.address_clause;
864
+ function addressesPrefix($, addresses) {
865
+ return seq(field("addresses", addresses), optional($._blanks));
898
866
  }
899
867
 
900
868
  function addressedCommand($, addresses, selectedFunction) {
901
869
  return seq(
902
870
  optional($._blanks),
903
- optional(seq(field("addresses", addresses), optional($._blanks))),
871
+ optional(addressesPrefix($, addresses)),
904
872
  optional(field("negation", $.negation)),
905
873
  field("function", selectedFunction),
906
874
  );
907
875
  }
908
876
 
909
- function addressedFunction($, functionRule) {
910
- return addressedCommand(
911
- $,
912
- $.address_clause,
913
- alias(functionRule, $.function),
914
- );
915
- }
916
-
917
- function addressedForms($, definitions, kind) {
918
- const forms = [];
919
- for (const maximum of [0, 1, 2]) {
920
- const names = functionsByMaximum(definitions, maximum);
921
- if (names.length === 0) {
922
- continue;
923
- }
924
- forms.push(
925
- addressedCommand(
926
- $,
927
- addressClauseForMaximum($, maximum),
928
- functionWrapper($, wrapperName(kind, maximum)),
877
+ function addressedForms($, kind) {
878
+ return choice(
879
+ ...wrappers
880
+ .filter((wrapper) => wrapper.kind === kind)
881
+ .map(({ maximum, name }) =>
882
+ addressedCommand(
883
+ $,
884
+ addressClauses[maximum]($),
885
+ alias($[name], $.function),
886
+ ),
929
887
  ),
930
- );
931
- }
932
-
933
- return choice(...forms);
888
+ );
934
889
  }
935
890
 
936
891
  const rules = {
937
892
  _chainable_editing_command: ($) =>
938
893
  seq(
939
- choice(
940
- addressedForms($, chainable, "chainable"),
941
- addressedFunction($, $._chainable_substitute_function),
942
- ),
894
+ addressedForms($, "chainable"),
943
895
  optional(issueField($, "unexpected_command_text")),
944
896
  ),
945
897
 
946
898
  _line_terminated_editing_command: ($) =>
947
899
  seq(
948
- addressedForms($, lineTerminated, "line_terminated"),
900
+ addressedForms($, "line_terminated"),
949
901
  optional(issueField($, "unexpected_command_text")),
950
902
  ),
951
903
 
952
904
  _recovered_line_terminated_editing_command: ($) =>
953
- prec.right(
954
- seq(
955
- $._line_terminated_editing_command,
956
- issueField($, "forbidden_command_separator"),
957
- optional($._blanks),
958
- ),
905
+ seq(
906
+ $._line_terminated_editing_command,
907
+ issueField($, "forbidden_command_separator"),
959
908
  ),
960
909
 
961
910
  _recovered_editing_command: ($) =>
962
911
  choice(
963
- addressedFunction($, $._unknown_function),
964
- seq(
965
- optional($._blanks),
966
- optional(
967
- seq(field("addresses", $.address_clause), optional($._blanks)),
968
- ),
969
- field("negation", $.negation),
970
- field("function", alias($._reserved_unknown_function, $.function)),
912
+ addressedCommand(
913
+ $,
914
+ $.address_clause,
915
+ alias($._unknown_function, $.function),
971
916
  ),
972
917
  seq(
973
918
  optional($._blanks),
974
- field("addresses", alias($._double_address_clause, $.address_clause)),
975
- optional($._blanks),
919
+ optional(addressesPrefix($, $.address_clause)),
920
+ field("negation", $.negation),
976
921
  field("function", alias($._reserved_unknown_function, $.function)),
977
922
  ),
978
923
  seq(
979
924
  optional($._blanks),
980
925
  choice(
981
926
  seq(
982
- field("addresses", $.address_clause),
983
- optional($._blanks),
927
+ addressesPrefix($, $.address_clause),
984
928
  optional(field("negation", $.negation)),
985
929
  ),
986
930
  field("negation", $.negation),
@@ -1003,9 +947,6 @@ function editingCommandRules() {
1003
947
 
1004
948
  _missing_function: ($) => missingAtEndOrBoundary($, "missing_function"),
1005
949
 
1006
- _chainable_substitute_function: ($) =>
1007
- alias($._substitute_function, $.substitute_function),
1008
-
1009
950
  negation: ($) =>
1010
951
  seq(
1011
952
  field("operator", alias("!", $.negation_operator)),
@@ -1019,16 +960,8 @@ function editingCommandRules() {
1019
960
  ),
1020
961
  };
1021
962
 
1022
- for (const [kind, definitions] of [
1023
- ["chainable", chainable],
1024
- ["line_terminated", lineTerminated],
1025
- ]) {
1026
- for (const maximum of [0, 1, 2]) {
1027
- const names = functionsByMaximum(definitions, maximum);
1028
- if (names.length > 0) {
1029
- rules[wrapperName(kind, maximum)] = ($) => choiceForRules($, names);
1030
- }
1031
- }
963
+ for (const { name, names } of wrappers) {
964
+ rules[name] = ($) => choice(...names.map((name) => $[name]));
1032
965
  }
1033
966
 
1034
967
  return rules;
@@ -1100,15 +1033,31 @@ function issueDefinitions(mode) {
1100
1033
  outcome: "undefined_syntax",
1101
1034
  rule: ($) => $._regex_malformed_bracket_term,
1102
1035
  },
1036
+ {
1037
+ id: "invalid_character_class_name",
1038
+ reason: "malformed_bracket_term",
1039
+ outcome: "undefined_syntax",
1040
+ rule: ($) => namedExternal($, $._regex_invalid_class_name, "class_name"),
1041
+ },
1042
+ {
1043
+ reason: "nul_character",
1044
+ outcome: "nonconforming_syntax",
1045
+ rule: ($) => $._nul_character,
1046
+ },
1047
+ {
1048
+ reason: "invalid_regular_expression_character",
1049
+ outcome: "invalid_syntax",
1050
+ rule: ($) => $._invalid_character,
1051
+ },
1103
1052
  {
1104
1053
  reason: "character_class_range_start",
1105
1054
  outcome: "undefined_syntax",
1106
- rule: ($) => $._nonportable_range_start_marker,
1055
+ rule: ($) => seq($.character_class, $._nonportable_range_start_marker),
1107
1056
  },
1108
1057
  {
1109
1058
  reason: "character_class_range_end",
1110
1059
  outcome: "undefined_syntax",
1111
- rule: ($) => $._nonportable_range_end_marker,
1060
+ rule: ($) => seq($._nonportable_range_end_marker, $.character_class),
1112
1061
  },
1113
1062
  {
1114
1063
  reason: "shared_range_endpoint",
@@ -1132,7 +1081,6 @@ function issueDefinitions(mode) {
1132
1081
  outcome: "undefined_syntax",
1133
1082
  rule: missing("omitted_first_address"),
1134
1083
  },
1135
- // The marker is shared, but maximum-specific reductions must not merge.
1136
1084
  ...[0, 1].map((maximum) => ({
1137
1085
  id: `omitted_first_address_unit${maximum}`,
1138
1086
  reason: "omitted_address",
@@ -1181,17 +1129,39 @@ function issueDefinitions(mode) {
1181
1129
  {
1182
1130
  reason: "malformed_interval",
1183
1131
  outcome: "undefined_syntax",
1184
- rule: ($) => $._regex_invalid_interval,
1132
+ rule: ($) =>
1133
+ prec.right(
1134
+ repeat1(
1135
+ choice(
1136
+ $._regex_invalid_interval,
1137
+ issueField($, "invalid_regular_expression_character"),
1138
+ ),
1139
+ ),
1140
+ ),
1185
1141
  },
1186
1142
  {
1187
1143
  reason: "leading_duplication_symbol",
1188
1144
  outcome: "undefined_syntax",
1189
- rule: ($) => $._regex_leading_duplication_marker,
1145
+ rule: ($) =>
1146
+ seq(
1147
+ $._regex_leading_duplication_marker,
1148
+ field(
1149
+ "operator",
1150
+ mode === "bre" ? $.bre_dupl_symbol : $.ere_dupl_symbol,
1151
+ ),
1152
+ ),
1190
1153
  },
1191
1154
  {
1192
1155
  reason: "adjacent_duplication_symbol",
1193
1156
  outcome: "undefined_syntax",
1194
- rule: ($) => $._regex_adjacent_duplication_marker,
1157
+ rule: ($) =>
1158
+ seq(
1159
+ $._regex_adjacent_duplication_marker,
1160
+ field(
1161
+ "operator",
1162
+ mode === "bre" ? $.bre_dupl_symbol : $.ere_dupl_symbol,
1163
+ ),
1164
+ ),
1195
1165
  },
1196
1166
  ...(mode === "ere"
1197
1167
  ? [
@@ -1215,22 +1185,26 @@ function issueDefinitions(mode) {
1215
1185
  {
1216
1186
  reason: "undefined_translation_escape",
1217
1187
  outcome: "undefined_syntax",
1218
- rule: ($) => $._translate_nonportable_escape,
1188
+ rule: ($) =>
1189
+ choice(
1190
+ $._translate_nonportable_escape,
1191
+ seq($._escape_prefix, issueField($, "nul_character")),
1192
+ ),
1219
1193
  },
1220
1194
  {
1221
1195
  reason: "equivalence_class_range_start",
1222
1196
  outcome: "unspecified_syntax",
1223
- rule: ($) => $._nonportable_range_start_marker,
1197
+ rule: ($) => seq($.equivalence_class, $._nonportable_range_start_marker),
1224
1198
  },
1225
1199
  {
1226
1200
  reason: "equivalence_class_range_end",
1227
1201
  outcome: "unspecified_syntax",
1228
- rule: ($) => $._nonportable_range_end_marker,
1202
+ rule: ($) => seq($._nonportable_range_end_marker, $.equivalence_class),
1229
1203
  },
1230
1204
  {
1231
1205
  reason: "ambiguous_bracket_expression",
1232
1206
  outcome: "unspecified_syntax",
1233
- rule: ($) => $._ambiguous_bracket_expression_marker,
1207
+ rule: ($) => alias($._ambiguous_bracket_expression, $.bracket_expression),
1234
1208
  },
1235
1209
  {
1236
1210
  reason: "blanks_after_negation",
@@ -1240,7 +1214,11 @@ function issueDefinitions(mode) {
1240
1214
  {
1241
1215
  reason: "unspecified_replacement_escape",
1242
1216
  outcome: "unspecified_syntax",
1243
- rule: ($) => $._replacement_nonportable_escape,
1217
+ rule: ($) =>
1218
+ choice(
1219
+ $._replacement_nonportable_escape,
1220
+ seq($._escape_prefix, issueField($, "nul_character")),
1221
+ ),
1244
1222
  },
1245
1223
  {
1246
1224
  reason: "special_delimiter_escape",
@@ -1255,33 +1233,49 @@ function issueDefinitions(mode) {
1255
1233
  {
1256
1234
  reason: "unspecified_text_escape",
1257
1235
  outcome: "unspecified_syntax",
1258
- rule: ($) => $._text_unspecified_escape,
1236
+ rule: ($) =>
1237
+ choice(
1238
+ $._text_unspecified_escape,
1239
+ seq($._escape_prefix, issueField($, "nul_character")),
1240
+ ),
1259
1241
  },
1260
1242
  ...(mode === "bre"
1261
1243
  ? [
1262
1244
  {
1263
1245
  reason: "bre_vertical_line_escape",
1264
1246
  outcome: "implementation_defined_syntax",
1265
- rule: ($) => $._bre_vertical_line_escape_marker,
1247
+ rule: ($) =>
1248
+ field(
1249
+ "token",
1250
+ namedExternal($, $._regex_bre_vertical_line_escape, "back_bar"),
1251
+ ),
1266
1252
  },
1267
1253
  {
1268
1254
  reason: "bre_question_mark_escape",
1269
1255
  outcome: "implementation_defined_syntax",
1270
- rule: ($) => $._bre_question_mark_escape_marker,
1256
+ rule: ($) =>
1257
+ field(
1258
+ "token",
1259
+ namedExternal($, $._regex_bre_question_mark_escape, "back_qm"),
1260
+ ),
1271
1261
  },
1272
1262
  {
1273
1263
  reason: "bre_plus_escape",
1274
1264
  outcome: "implementation_defined_syntax",
1275
- rule: ($) => $._bre_plus_escape_marker,
1265
+ rule: ($) =>
1266
+ field(
1267
+ "token",
1268
+ namedExternal($, $._regex_bre_plus_escape, "back_plus"),
1269
+ ),
1276
1270
  },
1277
1271
  {
1278
1272
  reason: "bre_subexpression_left_anchor",
1279
- outcome: "invalid_syntax",
1273
+ outcome: "nonconforming_syntax",
1280
1274
  rule: ($) => $._regex_bre_subexpression_caret,
1281
1275
  },
1282
1276
  {
1283
1277
  reason: "bre_subexpression_right_anchor",
1284
- outcome: "invalid_syntax",
1278
+ outcome: "nonconforming_syntax",
1285
1279
  rule: ($) => $._regex_bre_subexpression_dollar,
1286
1280
  },
1287
1281
  ]
@@ -1321,7 +1315,8 @@ function issueDefinitions(mode) {
1321
1315
  {
1322
1316
  reason: "unknown_function",
1323
1317
  outcome: "nonconforming_syntax",
1324
- rule: () => token(prec(-2, /[^[:blank:][:digit:]$\/\\!;{}#\n]/)),
1318
+ rule: ($) =>
1319
+ choice(token(prec(-2, unknownFunctionCharacter)), $._invalid_character),
1325
1320
  },
1326
1321
  {
1327
1322
  id: "reserved_unknown_function",
@@ -1332,16 +1327,23 @@ function issueDefinitions(mode) {
1332
1327
  {
1333
1328
  reason: "unexpected_command_text",
1334
1329
  outcome: "nonconforming_syntax",
1335
- rule: () =>
1336
- choice(
1337
- token.immediate(prec(-10, /[[:blank:]]+/)),
1338
- token.immediate(prec(-10, /[[:blank:]]*[^[:blank:];}\n][^;}\n]*/)),
1330
+ rule: ($) =>
1331
+ repeat1(
1332
+ choice(
1333
+ token.immediate(prec(-10, /[[:blank:]]+/)),
1334
+ token.immediate(
1335
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: NUL must remain a separate syntax issue.
1336
+ prec(-10, /[[:blank:]]*[^[:blank:];}\n\x00][^;}\n\x00]*/),
1337
+ ),
1338
+ issueField($, "nul_character"),
1339
+ $._invalid_character,
1340
+ ),
1339
1341
  ),
1340
1342
  },
1341
1343
  {
1342
1344
  reason: "forbidden_command_separator",
1343
1345
  outcome: "nonconforming_syntax",
1344
- rule: () => token.immediate(";"),
1346
+ rule: ($) => alias(token.immediate(";"), $.command_separator),
1345
1347
  },
1346
1348
  {
1347
1349
  reason: "blanks_around_address_separator",
@@ -1361,7 +1363,7 @@ function issueDefinitions(mode) {
1361
1363
  {
1362
1364
  reason: "unmatched_closing_brace",
1363
1365
  outcome: "nonconforming_syntax",
1364
- rule: ($) => $._right_brace,
1366
+ rule: ($) => alias($._present_closing_brace, $.closing_brace),
1365
1367
  },
1366
1368
  {
1367
1369
  reason: "missing_command_separator",
@@ -1388,7 +1390,11 @@ function issueDefinitions(mode) {
1388
1390
  {
1389
1391
  reason: "invalid_delimiter",
1390
1392
  outcome: "nonconforming_syntax",
1391
- rule: () => "\\",
1393
+ rule: ($) =>
1394
+ field(
1395
+ "token",
1396
+ alias(choice("\\", $._invalid_character), $.delimiter_token),
1397
+ ),
1392
1398
  },
1393
1399
  {
1394
1400
  reason: "unterminated_regular_expression",
@@ -1449,6 +1455,11 @@ function issueDefinitions(mode) {
1449
1455
  outcome: "incomplete_syntax",
1450
1456
  rule: missing("missing_text_introducer"),
1451
1457
  },
1458
+ {
1459
+ reason: "incomplete_text_introducer",
1460
+ outcome: "incomplete_syntax",
1461
+ rule: ($) => $._text_incomplete_introducer,
1462
+ },
1452
1463
  {
1453
1464
  reason: "missing_text",
1454
1465
  outcome: "incomplete_syntax",
@@ -1562,10 +1573,13 @@ function externalTokens($, mode) {
1562
1573
  $._translate_middle,
1563
1574
  $._translate_end,
1564
1575
  $._regex_literal,
1576
+ $._invalid_character,
1577
+ $._nul_character,
1565
1578
  $._regex_beginning_anchor,
1566
1579
  $._regex_end_anchor,
1567
1580
  $._regex_period,
1568
1581
  $._regex_quoted_escape,
1582
+ $._escape_prefix,
1569
1583
  $._regex_newline_escape,
1570
1584
  $._regex_escaped_delimiter,
1571
1585
  $._regex_special_escaped_delimiter,
@@ -1580,9 +1594,6 @@ function externalTokens($, mode) {
1580
1594
  $._regex_bre_plus_escape,
1581
1595
  $._regex_bre_subexpression_caret,
1582
1596
  $._regex_bre_subexpression_dollar,
1583
- $._bre_vertical_line_escape_marker,
1584
- $._bre_question_mark_escape_marker,
1585
- $._bre_plus_escape_marker,
1586
1597
  $._regex_unmatched_interval_close,
1587
1598
  ]
1588
1599
  : []),
@@ -1614,6 +1625,7 @@ function externalTokens($, mode) {
1614
1625
  $._regex_bracket_trailing_hyphen,
1615
1626
  $._regex_open_colon,
1616
1627
  $._regex_class_name,
1628
+ $._regex_invalid_class_name,
1617
1629
  $._regex_colon_close,
1618
1630
  $._regex_open_dot,
1619
1631
  $._regex_coll_elem_single,
@@ -1651,6 +1663,7 @@ function externalTokens($, mode) {
1651
1663
  $._invalid_substitution_flag,
1652
1664
  $._flag_after_write_marker,
1653
1665
  $._text_command_start,
1666
+ $._text_incomplete_introducer,
1654
1667
  $._text_literal,
1655
1668
  $._text_backslash_escape,
1656
1669
  $._text_escaped_newline,
@@ -1661,8 +1674,11 @@ function externalTokens($, mode) {
1661
1674
  $._comment_text,
1662
1675
  $._file_argument,
1663
1676
  $._substitution_wfile_argument,
1677
+ $._substitution_wfile_continuation,
1664
1678
  $._line_word,
1679
+ $._argument_separator,
1665
1680
  $._right_brace,
1681
+ $._empty_command_marker,
1666
1682
  $._reserved_unknown_function_token,
1667
1683
  $._regex_incomplete_group,
1668
1684
  $._regex_incomplete_bracket_term,
@@ -1692,7 +1708,6 @@ function defineGrammar(name, mode) {
1692
1708
  extras: () => [],
1693
1709
 
1694
1710
  conflicts: ($) => [
1695
- [$.address_clause, $._recovered_editing_command],
1696
1711
  [$.address_clause, $._max1_address_clause],
1697
1712
  [$._double_address_clause, $._excess_address_unit1_reason],
1698
1713
  [$.address, $._max0_address],